weflow-cli 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +378 -0
  2. package/OPERATIONS.md +359 -11
  3. package/README.md +4 -0
  4. package/bin/weflow-cli.ts +289 -37
  5. package/dist/bin/weflow-cli.js +300 -35
  6. package/dist/bin/weflow-cli.js.map +1 -1
  7. package/dist/src/core/dbPathService.d.ts.map +1 -1
  8. package/dist/src/core/dbPathService.js +5 -0
  9. package/dist/src/core/dbPathService.js.map +1 -1
  10. package/dist/src/core/ntCore.d.ts +12 -1
  11. package/dist/src/core/ntCore.d.ts.map +1 -1
  12. package/dist/src/core/ntCore.js +23 -3
  13. package/dist/src/core/ntCore.js.map +1 -1
  14. package/dist/src/core/sqlcipherCore.d.ts +8 -0
  15. package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
  16. package/dist/src/core/sqlcipherCore.js +46 -11
  17. package/dist/src/core/sqlcipherCore.js.map +1 -1
  18. package/dist/src/services/assistantEval.d.ts.map +1 -1
  19. package/dist/src/services/assistantEval.js +3 -1
  20. package/dist/src/services/assistantEval.js.map +1 -1
  21. package/dist/src/services/assistantTools.d.ts.map +1 -1
  22. package/dist/src/services/assistantTools.js +25 -7
  23. package/dist/src/services/assistantTools.js.map +1 -1
  24. package/dist/src/services/configService.d.ts +3 -0
  25. package/dist/src/services/configService.d.ts.map +1 -1
  26. package/dist/src/services/configService.js +6 -3
  27. package/dist/src/services/configService.js.map +1 -1
  28. package/dist/src/services/diagnosticReport.d.ts +80 -0
  29. package/dist/src/services/diagnosticReport.d.ts.map +1 -0
  30. package/dist/src/services/diagnosticReport.js +191 -0
  31. package/dist/src/services/diagnosticReport.js.map +1 -0
  32. package/dist/src/services/exportService.d.ts +7 -0
  33. package/dist/src/services/exportService.d.ts.map +1 -1
  34. package/dist/src/services/exportService.js +1 -1
  35. package/dist/src/services/exportService.js.map +1 -1
  36. package/dist/src/services/wechatClient.d.ts +9 -3
  37. package/dist/src/services/wechatClient.d.ts.map +1 -1
  38. package/dist/src/services/wechatClient.js +55 -22
  39. package/dist/src/services/wechatClient.js.map +1 -1
  40. package/docs/DECISIONS.md +662 -1
  41. package/docs/MCP.md +1 -1
  42. package/docs/NEXT_ACTIONS.md +236 -0
  43. package/docs/POSITIONING.md +68 -0
  44. package/docs/PROJECT_STATE.md +119 -17
  45. package/docs/RELEASING.md +14 -0
  46. package/package.json +88 -86
  47. package/scripts/_utils.py +120 -4
  48. package/scripts/backfill_articles.py +64 -14
  49. package/scripts/biz_daily.py +118 -11
  50. package/scripts/daily_task.ps1 +132 -0
  51. package/scripts/export_chat_html.py +79 -31
  52. package/scripts/install_tasks.cmd +32 -0
  53. package/scripts/install_tasks.ps1 +91 -0
  54. package/scripts/jev_client.py +335 -3
  55. package/scripts/nt_decrypt.py +79 -14
  56. package/scripts/pipeline.py +21 -0
  57. package/scripts/recall_eval.py +247 -0
  58. package/scripts/repro_customer_init.cmd +86 -0
  59. package/scripts/semantic_search.py +926 -51
  60. package/src/core/dbPathService.ts +5 -0
  61. package/src/core/ntCore.ts +34 -3
  62. package/src/core/sqlcipherCore.ts +47 -11
  63. package/src/services/assistantEval.ts +3 -1
  64. package/src/services/assistantTools.ts +2471 -2452
  65. package/src/services/configService.ts +12 -3
  66. package/src/services/diagnosticReport.ts +237 -0
  67. package/src/services/exportService.ts +14 -2
  68. package/src/services/wechatClient.ts +418 -380
@@ -70,6 +70,11 @@ export class DbPathService {
70
70
  return [
71
71
  '$recycle.bin', 'node_modules', 'windows', 'program files',
72
72
  'program files (x86)', 'programdata', 'system volume information',
73
+ // 临时目录不是微信数据的合理位置,但**测试夹具**就建在这下面
74
+ // (`%LOCALAPPDATA%\Temp\weflow-custom-nt-*\xwechat_files`)。不排除的话,
75
+ // 空配置下的"自动发现"会把一个测试残留当成用户的微信目录 ——
76
+ // 2026-10-19 模拟新用户时实测命中,然后一路对着假数据初始化。
77
+ 'temp', 'tmp',
73
78
  ].includes(name.toLowerCase())
74
79
  }
75
80
 
@@ -21,6 +21,17 @@ function getPackageRoot(): string {
21
21
  return resolvePackageRoot(import.meta.url)
22
22
  }
23
23
 
24
+ /**
25
+ * `scan` 的执行器。默认就是真的 `execFile`;测试注入假执行器来钉住**错误映射**。
26
+ *
27
+ * 为什么需要这个口子:Windows 上"找不到命令"的表现是子进程以 **9009** 退出,
28
+ * 而它没有可移植的造假方式(要有真 exe / 真 shell 行为,Linux CI 上做不出来)。
29
+ * 少了这条测试,`9009` 那段用户提示就会在以后的改动里悄悄退回
30
+ * `NT scan failed (exit 9009)` —— 2026-10-18 那位用户卡住时看到的就是这句。
31
+ */
32
+ export type NtExecRunner =
33
+ (file: string, args: string[], options: Record<string, unknown>) => Promise<{ stdout: string }>
34
+
24
35
  export interface NtResult {
25
36
  success: boolean
26
37
  error?: string
@@ -191,11 +202,11 @@ export class NtCore {
191
202
  * Scan memory for NT keys and match to databases.
192
203
  * Requires WeChat to be running.
193
204
  */
194
- static async scan(root?: string): Promise<NtScanResult> {
205
+ static async scan(root?: string, run: NtExecRunner = execFileAsync as unknown as NtExecRunner): Promise<NtScanResult> {
195
206
  try {
196
207
  const scriptPath = join(getPackageRoot(), 'scripts', 'nt_decrypt.py')
197
208
  const scanArgs = ['scan', '--json']
198
- const { stdout } = await execFileAsync(getPythonCommand(), [scriptPath, ...scanArgs], {
209
+ const { stdout } = await run(getPythonCommand(), [scriptPath, ...scanArgs], {
199
210
  timeout: 300_000,
200
211
  maxBuffer: 10 * 1024 * 1024,
201
212
  env: createPythonProcessEnv(root ? { WEFLOW_SCAN_ROOT: root } : {}),
@@ -234,7 +245,27 @@ export class NtCore {
234
245
  return { success: false, error: 'Scan produced no valid output' }
235
246
  } catch (e: any) {
236
247
  if (e?.code === 'ENOENT') {
237
- return { success: false, error: 'Python not found. Install Python and add to PATH.' }
248
+ // 与 9009 那条**同一件事、同一句话**:一条是"命令名解析不到",一条是"解析到了但跑不起来",
249
+ // 对用户来说都是"这台机器没有可用的 Python"。以前这里留的是英文
250
+ // `Python not found. Install Python and add to PATH.`,于是同一个 init 里
251
+ // 上面刚用中文解释完"商店占位程序",下面又冒出一句英文(2026-10-19 实测)。
252
+ return {
253
+ success: false,
254
+ error: '没有可用的 Python:NT 扫描需要 Python 3.10+ 且能 `import sqlcipher3`。'
255
+ + '在终端里运行 `python --version`,能打出版本号才算装对',
256
+ }
257
+ }
258
+ if (e?.code === 9009) {
259
+ // Windows 的 9009 是"找不到命令"。最常见的原因是 PATH 上只有**微软商店的 python
260
+ // 占位程序**:`getPythonCommand()` 在所有候选都不过(没有真 Python / 没有 sqlcipher3)
261
+ // 时兜底返回裸名 `python`,正好交给那个占位程序,它一跑就以 9009 退出。
262
+ // 不把这条翻译出来,用户看到的是"NT scan failed (exit 9009)",而下一步该做什么完全没有线索
263
+ // (2026-10-18 一位用户就是这么卡住的)。
264
+ return {
265
+ success: false,
266
+ error: '没有可用的 Python:NT 扫描需要 Python 3.10+ 且能 `import sqlcipher3`。'
267
+ + 'exit 9009 在 Windows 上通常意味着只装了商店的 python 占位程序,装一个真正的 Python 后重试',
268
+ }
238
269
  }
239
270
  return { success: false, error: safeSubprocessError(e, 'NT scan failed') }
240
271
  }
@@ -375,13 +375,17 @@ export class SqlcipherCore {
375
375
  if (!this.db) return { success: false, error: '数据库未打开' }
376
376
 
377
377
  try {
378
+ // 预览必须取**类型**,不能只取正文:非文本消息的 `StrContent` 是 XML/base64 碎片,
379
+ // 直接打出来就是一屏 `(/`$` 这种看不懂的字符(2026-10-19 用户在 `sessions` 里看到)。
380
+ // `Type` 与 `StrContent` 用 char(1) 拼在一次查询里取回 —— 分两条子查询等于把
381
+ // 每个会话的排序做两遍(400 个会话就是两倍的排序)。
378
382
  const stmt = this.db.prepare(`
379
383
  SELECT
380
384
  m.StrTalker as username,
381
385
  MAX(m.CreateTime) as lastTimestamp,
382
- (SELECT m2.StrContent FROM MSG m2
386
+ (SELECT m2.Type || char(1) || COALESCE(m2.StrContent, '') FROM MSG m2
383
387
  WHERE m2.StrTalker = m.StrTalker
384
- ORDER BY m2.CreateTime DESC LIMIT 1) as summary
388
+ ORDER BY m2.CreateTime DESC LIMIT 1) as lastMsg
385
389
  FROM MSG m
386
390
  WHERE m.StrTalker != ''
387
391
  GROUP BY m.StrTalker
@@ -390,15 +394,21 @@ export class SqlcipherCore {
390
394
  `)
391
395
  const rows = stmt.all() as any[]
392
396
 
393
- const sessions: ChatSession[] = rows.map((row: any) => ({
394
- username: row.username || '',
395
- type: (row.username || '').includes('@chatroom') ? 1 : 0,
396
- unreadCount: 0,
397
- summary: row.summary || '',
398
- sortTimestamp: row.lastTimestamp || 0,
399
- lastTimestamp: row.lastTimestamp || 0,
400
- displayName: row.username || '',
401
- }))
397
+ const sessions: ChatSession[] = rows.map((row: any) => {
398
+ const raw = String(row.lastMsg || '')
399
+ const sep = raw.indexOf('\u0001')
400
+ const summaryType = sep >= 0 ? Number(raw.slice(0, sep)) || 0 : 0
401
+ const summaryBody = sep >= 0 ? raw.slice(sep + 1) : raw
402
+ return {
403
+ username: row.username || '',
404
+ type: (row.username || '').includes('@chatroom') ? 1 : 0,
405
+ unreadCount: 0,
406
+ summary: this.previewOf(summaryType, summaryBody),
407
+ sortTimestamp: row.lastTimestamp || 0,
408
+ lastTimestamp: row.lastTimestamp || 0,
409
+ displayName: row.username || '',
410
+ }
411
+ })
402
412
 
403
413
  return { success: true, sessions }
404
414
  } catch (e) {
@@ -453,6 +463,32 @@ export class SqlcipherCore {
453
463
  }
454
464
  }
455
465
 
466
+ /**
467
+ * 会话列表里那一列的**显示形态**(不是完整解析)。
468
+ *
469
+ * 只做两件事:按 `Type` 给标签、把控制字符清掉。不做缩略图、不解 appmsg 的完整 XML ——
470
+ * 这里是列表预览,400 行里每行都解析一次会把 `sessions` 拖成几秒。
471
+ * 口径与 `processMessage` 保持一致(`[图片]` / `[语音]` / `[链接]`…),免得出两套说法。
472
+ */
473
+ private previewOf(localType: number, raw: string): string {
474
+ switch (localType) {
475
+ case 1: // 文本:清掉控制字符后原样显示
476
+ return raw.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '').replace(/\s+/g, ' ').trim()
477
+ case 3: return '[图片]'
478
+ case 34: return '[语音]'
479
+ case 43: return '[视频]'
480
+ case 47: return '[表情]'
481
+ case 49: return '[链接/文件]'
482
+ case 48: return '[位置]'
483
+ case 42: return '[名片]'
484
+ case 10000: return '[系统消息]'
485
+ case 10002: return '[系统消息]'
486
+ default:
487
+ // 未知类型:给个可读的兜底,**绝不把原始二进制/XML 打到终端里**
488
+ return localType ? `[消息类型 ${localType}]` : '[非文本]'
489
+ }
490
+ }
491
+
456
492
  /**
457
493
  * 处理单条消息,根据类型解析内容
458
494
  */
@@ -574,7 +574,9 @@ export const EVAL_CASES: EvalCase[] = [
574
574
  export const EVAL_UNCOVERED: Record<string, string> = {
575
575
  fetch_article: '出网工具,评测器的 fetch 包装会让它抛错("评测里出现第二个出口就说明有地方漏了")。',
576
576
  search_public: '同上:搜狗搜索也是出网,评测里跑不了。',
577
- search_semantic: '要 dashscopeApiKey **且**要建过语义索引,这台机器两样都没有。代码路径由 '
577
+ search_semantic: '要 dashscopeApiKey **且**要建过语义索引。2026-10-15 这台机器上两样都有了(索引 16,027 条、'
578
+ + '真机跑过 CLI),所以原因已经不是"没有",而是它是**第二个出网出口**(查询向量要发给百炼)——'
579
+ + '评测只允许模型那一次调用离开进程。代码路径由 '
578
580
  + '`assistant-tools-branches.test.ts`(12 处,含"查询词走环境变量、不进 argv")与 '
579
581
  + '`assistant-service.test.ts`(没配 key 时不出现在工具表里)覆盖。',
580
582
  save_memory: '**故意不按工具判**:记忆是按**结果**判的(`memoryContains`),'