weflow-cli 1.5.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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/weflow-cli-electron.cjs +59 -0
  4. package/bin/weflow-cli.ts +2311 -0
  5. package/cli.cjs +3 -0
  6. package/dist/bin/weflow-cli.d.ts +3 -0
  7. package/dist/bin/weflow-cli.d.ts.map +1 -0
  8. package/dist/bin/weflow-cli.js +2270 -0
  9. package/dist/bin/weflow-cli.js.map +1 -0
  10. package/dist/mcp-server/index.d.ts +3 -0
  11. package/dist/mcp-server/index.d.ts.map +1 -0
  12. package/dist/mcp-server/index.js +690 -0
  13. package/dist/mcp-server/index.js.map +1 -0
  14. package/dist/src/core/dbPathService.d.ts +40 -0
  15. package/dist/src/core/dbPathService.d.ts.map +1 -0
  16. package/dist/src/core/dbPathService.js +324 -0
  17. package/dist/src/core/dbPathService.js.map +1 -0
  18. package/dist/src/core/keyService.d.ts +59 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -0
  20. package/dist/src/core/keyService.js +368 -0
  21. package/dist/src/core/keyService.js.map +1 -0
  22. package/dist/src/core/ntCore.d.ts +62 -0
  23. package/dist/src/core/ntCore.d.ts.map +1 -0
  24. package/dist/src/core/ntCore.js +191 -0
  25. package/dist/src/core/ntCore.js.map +1 -0
  26. package/dist/src/core/sqlcipherCore.d.ts +119 -0
  27. package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
  28. package/dist/src/core/sqlcipherCore.js +693 -0
  29. package/dist/src/core/sqlcipherCore.js.map +1 -0
  30. package/dist/src/core/wcdbCore.d.ts +738 -0
  31. package/dist/src/core/wcdbCore.d.ts.map +1 -0
  32. package/dist/src/core/wcdbCore.js +4545 -0
  33. package/dist/src/core/wcdbCore.js.map +1 -0
  34. package/dist/src/core/wechatClient.d.ts +23 -0
  35. package/dist/src/core/wechatClient.d.ts.map +1 -0
  36. package/dist/src/core/wechatClient.js +166 -0
  37. package/dist/src/core/wechatClient.js.map +1 -0
  38. package/dist/src/services/chatService.d.ts +25 -0
  39. package/dist/src/services/chatService.d.ts.map +1 -0
  40. package/dist/src/services/chatService.js +291 -0
  41. package/dist/src/services/chatService.js.map +1 -0
  42. package/dist/src/services/configService.d.ts +43 -0
  43. package/dist/src/services/configService.d.ts.map +1 -0
  44. package/dist/src/services/configService.js +156 -0
  45. package/dist/src/services/configService.js.map +1 -0
  46. package/dist/src/services/exportService.d.ts +30 -0
  47. package/dist/src/services/exportService.d.ts.map +1 -0
  48. package/dist/src/services/exportService.js +273 -0
  49. package/dist/src/services/exportService.js.map +1 -0
  50. package/dist/src/services/wechat-formatter.d.ts +69 -0
  51. package/dist/src/services/wechat-formatter.d.ts.map +1 -0
  52. package/dist/src/services/wechat-formatter.js +248 -0
  53. package/dist/src/services/wechat-formatter.js.map +1 -0
  54. package/dist/src/services/wechatMessageService.d.ts +28 -0
  55. package/dist/src/services/wechatMessageService.d.ts.map +1 -0
  56. package/dist/src/services/wechatMessageService.js +341 -0
  57. package/dist/src/services/wechatMessageService.js.map +1 -0
  58. package/dist/src/services/wereadService.d.ts +206 -0
  59. package/dist/src/services/wereadService.d.ts.map +1 -0
  60. package/dist/src/services/wereadService.js +145 -0
  61. package/dist/src/services/wereadService.js.map +1 -0
  62. package/dist/src/services/whitelistService.d.ts +20 -0
  63. package/dist/src/services/whitelistService.d.ts.map +1 -0
  64. package/dist/src/services/whitelistService.js +60 -0
  65. package/dist/src/services/whitelistService.js.map +1 -0
  66. package/dist/src/types.d.ts +140 -0
  67. package/dist/src/types.d.ts.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils/errors.d.ts +25 -0
  71. package/dist/src/utils/errors.d.ts.map +1 -0
  72. package/dist/src/utils/errors.js +43 -0
  73. package/dist/src/utils/errors.js.map +1 -0
  74. package/dist/src/utils/pathUtils.d.ts +5 -0
  75. package/dist/src/utils/pathUtils.d.ts.map +1 -0
  76. package/dist/src/utils/pathUtils.js +16 -0
  77. package/dist/src/utils/pathUtils.js.map +1 -0
  78. package/dist/src/utils/pythonRunner.d.ts +27 -0
  79. package/dist/src/utils/pythonRunner.d.ts.map +1 -0
  80. package/dist/src/utils/pythonRunner.js +62 -0
  81. package/dist/src/utils/pythonRunner.js.map +1 -0
  82. package/dist/src/utils/talkerUtils.d.ts +14 -0
  83. package/dist/src/utils/talkerUtils.d.ts.map +1 -0
  84. package/dist/src/utils/talkerUtils.js +73 -0
  85. package/dist/src/utils/talkerUtils.js.map +1 -0
  86. package/mcp-server/index.ts +757 -0
  87. package/package.json +72 -0
  88. package/resources/key/win32/x64/wx_key.dll +0 -0
  89. package/resources/wcdb/win32/x64/SDL2.dll +0 -0
  90. package/resources/wcdb/win32/x64/WCDB.dll +0 -0
  91. package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
  92. package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
  93. package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
  94. package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
  95. package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
  96. package/scripts/_batch_link_sources.py +73 -0
  97. package/scripts/_utils.py +531 -0
  98. package/scripts/annual_report.py +796 -0
  99. package/scripts/auto_tag.py +128 -0
  100. package/scripts/biz_daily.py +896 -0
  101. package/scripts/chat_report.py +397 -0
  102. package/scripts/chat_stats.py +702 -0
  103. package/scripts/classify_daily.py +411 -0
  104. package/scripts/compile_wiki.py +258 -0
  105. package/scripts/create_reading_notes.py +370 -0
  106. package/scripts/enrich_backlinks.py +163 -0
  107. package/scripts/export_chat_html.py +766 -0
  108. package/scripts/extract_3x_key.py +86 -0
  109. package/scripts/extract_todos.py +404 -0
  110. package/scripts/fav_server.py +392 -0
  111. package/scripts/fix_topics.py +111 -0
  112. package/scripts/generate_ai_report.py +367 -0
  113. package/scripts/generate_html.py +1372 -0
  114. package/scripts/generate_review.py +170 -0
  115. package/scripts/mcp_bridge.py +398 -0
  116. package/scripts/nt_decrypt.py +562 -0
  117. package/scripts/pipeline.py +145 -0
  118. package/scripts/promote_all.py +310 -0
  119. package/scripts/promote_ideas.py +297 -0
  120. package/scripts/rag_chat.py +191 -0
  121. package/scripts/scan_decrypt_4x.py +329 -0
  122. package/scripts/semantic_search.py +431 -0
  123. package/scripts/sync_fav.py +146 -0
  124. package/scripts/sync_weread.py +192 -0
  125. package/scripts/vault_rag.py +139 -0
  126. package/scripts/vault_search.py +141 -0
  127. package/src/core/dbPathService.ts +348 -0
  128. package/src/core/keyService.ts +409 -0
  129. package/src/core/ntCore.ts +224 -0
  130. package/src/core/sqlcipherCore.ts +764 -0
  131. package/src/core/wcdbCore.ts +4568 -0
  132. package/src/core/wechatClient.ts +204 -0
  133. package/src/lz4.d.ts +10 -0
  134. package/src/services/chatService.ts +296 -0
  135. package/src/services/configService.ts +197 -0
  136. package/src/services/exportService.ts +305 -0
  137. package/src/services/wechat-formatter.ts +348 -0
  138. package/src/services/wechatMessageService.ts +383 -0
  139. package/src/services/wereadService.ts +308 -0
  140. package/src/services/whitelistService.ts +72 -0
  141. package/src/types.ts +141 -0
  142. package/src/utils/errors.ts +42 -0
  143. package/src/utils/pathUtils.ts +16 -0
  144. package/src/utils/pythonRunner.ts +81 -0
  145. package/src/utils/talkerUtils.ts +84 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 端到端流水线 — biz_daily → classify_daily → wiki compile → AI report 一键串联。
4
+
5
+ 默认策略:
6
+ - AI 推理优先走本地(Ollama/LM Studio/Claude Code 本地服务),不需要 API key
7
+ - 如果没有本地服务,再显式指定 --engine deepseek/claude
8
+
9
+ 用法:
10
+ python scripts/pipeline.py # 全部步骤,默认走本地推理
11
+ python scripts/pipeline.py --engine deepseek --api-key <key>
12
+ python scripts/pipeline.py --engine claude --api-key <key>
13
+ python scripts/pipeline.py --date 2026-06-09 --skip-classify
14
+ """
15
+ import sys, os, subprocess, time, shutil
16
+ from pathlib import Path
17
+
18
+ SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
19
+ SOURCE_ROOT = 'output/biz-daily'
20
+ PYTHON = sys.executable
21
+
22
+
23
+ def run_step(name: str, args: list[str]) -> bool:
24
+ """运行一个步骤,返回是否成功。"""
25
+ print(f'\n{"="*50}')
26
+ print(f' Step: {name}')
27
+ print(f'{"="*50}')
28
+ result = subprocess.run([PYTHON] + args, cwd=os.path.dirname(SCRIPTS_DIR))
29
+ if result.returncode != 0:
30
+ print(f'\n[FAIL] {name} 失败 (exit={result.returncode})')
31
+ return False
32
+ return True
33
+
34
+
35
+ def main():
36
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
37
+ import argparse
38
+ parser = argparse.ArgumentParser(description='端到端公众号日报流水线')
39
+ parser.add_argument('--api-key', help='云端 API key(--engine 为 deepseek/claude 时需要)')
40
+ parser.add_argument('--engine', default='local',
41
+ help='AI 引擎: local(自动检测·默认) / deepseek / claude / ollama')
42
+ parser.add_argument('--date', help='日期 YYYY-MM-DD')
43
+ parser.add_argument('--interest', default='AI', help='兴趣主题(默认 AI)')
44
+ parser.add_argument('--wiki-limit', type=int, default=20, help='概念编译数量(默认 20)')
45
+ parser.add_argument('--skip-classify', action='store_true', help='跳过后处理')
46
+ parser.add_argument('--skip-wiki', action='store_true', help='跳过概念编译')
47
+ parser.add_argument('--skip-vault', action='store_true', help='跳过 Vault 同步')
48
+ parser.add_argument('--skip-html', action='store_true', help='跳过 HTML 生成')
49
+ parser.add_argument('--skip-ai-report', action='store_true', help='跳过 AI 深度阅读报告')
50
+ parser.add_argument('--ai-report-range', type=int, default=1, help='AI 报告覆盖最近 N 天(默认 1=仅当天)')
51
+ args = parser.parse_args()
52
+
53
+ sys.path.insert(0, SCRIPTS_DIR)
54
+ from _utils import load_config
55
+ config = load_config()
56
+
57
+ # api_key:本地/ollama 引擎不需要;云端需要
58
+ api_key = args.api_key or ''
59
+ if args.engine in ('deepseek', 'claude') and not api_key:
60
+ api_key = os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
61
+ if not api_key:
62
+ print(f'[ERROR] --engine {args.engine} 需要 API key。请通过 --api-key、'
63
+ f'环境变量 DEEPSEEK_API_KEY 或 ~/.weflow-cli/config.json 提供')
64
+ sys.exit(1)
65
+
66
+ started = time.time()
67
+
68
+ # Step 1: biz_daily
69
+ step1_args = [os.path.join(SCRIPTS_DIR, 'biz_daily.py')]
70
+ if api_key:
71
+ step1_args += ['--api-key', api_key]
72
+ if args.date:
73
+ step1_args += ['--date', args.date]
74
+ if not run_step('biz_daily — 抓取+摘要', step1_args):
75
+ sys.exit(1)
76
+
77
+ # Step 2: classify_daily(可选)
78
+ if not args.skip_classify:
79
+ step2_args = [
80
+ os.path.join(SCRIPTS_DIR, 'classify_daily.py'),
81
+ '--api-key', api_key if api_key else 'local',
82
+ '--interest', args.interest,
83
+ ]
84
+ if args.date:
85
+ step2_args.insert(1, args.date)
86
+ run_step('classify_daily — 后处理', step2_args)
87
+
88
+ # Step 3: Vault sync(可选)
89
+ if not args.skip_vault:
90
+ date_str = args.date or time.strftime('%Y-%m-%d')
91
+ source_dir = os.path.join(SOURCE_ROOT, date_str)
92
+ vault_dir = os.path.join(os.path.dirname(SCRIPTS_DIR), 'output', 'wechat-vault',
93
+ 'Sources', 'WeChat', date_str)
94
+ if os.path.exists(source_dir):
95
+ if os.path.exists(vault_dir):
96
+ shutil.rmtree(vault_dir)
97
+ shutil.copytree(source_dir, vault_dir)
98
+ file_count = sum(1 for _ in Path(vault_dir).rglob('*.md'))
99
+ print(f'\n Vault 同步: {file_count} 个文件 → {vault_dir}')
100
+
101
+ # Step 4: wiki compile(可选)
102
+ if not args.skip_wiki:
103
+ step3_args = [
104
+ os.path.join(SCRIPTS_DIR, 'compile_wiki.py'),
105
+ '--api-key', api_key if api_key else 'local',
106
+ '--limit', str(args.wiki_limit),
107
+ ]
108
+ run_step('wiki compile — 概念编译', step3_args)
109
+
110
+ # Step 5: HTML 生成(可选)
111
+ if not args.skip_html:
112
+ date_str = args.date or time.strftime('%Y-%m-%d')
113
+ run_step('generate_html',
114
+ [os.path.join(SCRIPTS_DIR, 'generate_html.py'), '--date', date_str])
115
+
116
+ # Step 6: 双向链接增强
117
+ date_str = args.date or time.strftime('%Y-%m-%d')
118
+ run_step('enrich_backlinks — 双向链接',
119
+ [os.path.join(SCRIPTS_DIR, 'enrich_backlinks.py'), '--date', date_str])
120
+
121
+ # Step 7: 阅读笔记生成
122
+ run_step('create_reading_notes — 阅读笔记',
123
+ [os.path.join(SCRIPTS_DIR, 'create_reading_notes.py'), '--date', date_str])
124
+
125
+ # Step 8: AI 深度阅读报告
126
+ if not args.skip_ai_report:
127
+ step8_args = [
128
+ os.path.join(SCRIPTS_DIR, 'generate_ai_report.py'),
129
+ '--engine', args.engine,
130
+ '--range', str(args.ai_report_range),
131
+ ]
132
+ if api_key:
133
+ step8_args += ['--api-key', api_key]
134
+ if args.date and args.ai_report_range == 1:
135
+ step8_args += ['--date', args.date]
136
+ run_step('generate_ai_report — AI 阅读日报', step8_args)
137
+
138
+ elapsed = time.time() - started
139
+ print(f'\n{"="*50}')
140
+ print(f' 流水线完成!耗时 {elapsed/60:.1f} 分钟')
141
+ print(f'{"="*50}')
142
+
143
+
144
+ if __name__ == '__main__':
145
+ main()
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 全自动知识升级 — 003_Ideas → 004_Permanent → 005_Reference → 006_Projects
4
+
5
+ 用法:
6
+ python scripts/promote_all.py --api-key <key>
7
+ """
8
+
9
+ import sys, os, re
10
+ from pathlib import Path
11
+ from collections import Counter, defaultdict
12
+ from datetime import datetime
13
+
14
+ SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
15
+ PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
16
+ DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
17
+
18
+ sys.path.insert(0, SCRIPTS_DIR)
19
+ from _utils import call_deepseek
20
+
21
+
22
+ def read_frontmatter(path: Path) -> dict:
23
+ c = path.read_text(encoding='utf-8')
24
+ if not c.startswith('---'):
25
+ return {}
26
+ end = c.find('---', 3)
27
+ if end == -1:
28
+ return {}
29
+ fm = {}
30
+ for line in c[3:end].split('\n'):
31
+ if ':' in line:
32
+ k, _, v = line.partition(':')
33
+ fm[k.strip()] = v.strip().strip('"').strip("'")
34
+ return fm
35
+
36
+
37
+ def collect_articles(vault_path: str) -> list[dict]:
38
+ lit = Path(vault_path) / '002_Literature' / 'WeChat'
39
+ articles = []
40
+ for d in lit.iterdir():
41
+ if not d.is_dir(): continue
42
+ for f in d.glob('*.md'):
43
+ fm = read_frontmatter(f)
44
+ articles.append({
45
+ 'title': fm.get('title', f.stem),
46
+ 'source': fm.get('source', ''),
47
+ 'date': d.name,
48
+ 'topic': fm.get('hasTopic', '').replace('[[','').replace(']]',''),
49
+ 'path': f,
50
+ })
51
+ return articles
52
+
53
+
54
+ def generate_permanent(api_key, vault_path, articles):
55
+ """004_Permanent: 将 Best Ideas 扩展为原子永久笔记。"""
56
+ perm_dir = Path(vault_path) / '004_Permanent'
57
+ perm_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ # 按主题取 articles 样本
60
+ by_topic = defaultdict(list)
61
+ for a in articles:
62
+ t = a['topic'] or '综合'
63
+ by_topic[t].append(a)
64
+
65
+ created = 0
66
+ for topic, arts in by_topic.items():
67
+ if len(arts) < 10: continue
68
+ if (perm_dir / f'Perm-{topic}.md').exists(): continue
69
+
70
+ samples = sorted(arts, key=lambda a: len(a['title']))[-10:]
71
+ titles = '\n'.join(f'- {a["title"][:50]}' for a in samples)
72
+
73
+ prompt = f'''基于以下 {topic} 主题文章,写一篇 300 字的原子笔记,提炼该领域的核心知识。
74
+ 格式:1个核心概念 → 3个关键洞察 → 1个行动建议。
75
+
76
+ 文章样本:
77
+ {titles}
78
+
79
+ 只返回笔记内容,用 Markdown 格式。'''
80
+
81
+ try:
82
+ text = call_deepseek(prompt, api_key, max_tokens=600)
83
+ except Exception as e:
84
+ print(f' [WARN] {topic}: {e}')
85
+ continue
86
+
87
+ content = f'''---
88
+ title: "{topic} 核心知识"
89
+ created: {datetime.now().strftime('%Y-%m-%d')}
90
+ type: permanent-note
91
+ hasTopic: [[{topic}]]
92
+ tags: [permanent, {topic.lower()}, atomic]
93
+ sources: {len(arts)} articles
94
+ ---
95
+
96
+ # {topic} 核心知识
97
+
98
+ {text}
99
+
100
+ ---
101
+
102
+ ## 来源文章
103
+ '''
104
+ for a in arts[:15]:
105
+ content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:50]}]]\n'
106
+
107
+ (perm_dir / f'Perm-{topic}.md').write_text(content, encoding='utf-8')
108
+ created += 1
109
+ print(f' [{topic}] → Perm-{topic}.md')
110
+ return created
111
+
112
+
113
+ def generate_reference(vault_path, articles):
114
+ """005_Reference: 自动提取工具和方法论。"""
115
+ ref_dir = Path(vault_path) / '005_Reference'
116
+
117
+ # 关键词扫描
118
+ tool_keywords = ['工具', '开源', 'GitHub', 'Star', 'CLI', 'API', '框架', '平台', '模型',
119
+ '插件', 'Python', 'R包', '库', 'Agent', 'Skill', 'MCP', 'RAG']
120
+ method_keywords = ['方法', '算法', '流程', '技术', '策略', '方案', '分析', '优化',
121
+ '评估', '验证', '建模', '预测', '训练', '调优', '部署']
122
+
123
+ tools = defaultdict(list)
124
+ methods = defaultdict(list)
125
+
126
+ for a in articles:
127
+ title = a['title']
128
+ for kw in tool_keywords:
129
+ if kw in title:
130
+ tools[kw].append(a)
131
+ break
132
+ for kw in method_keywords:
133
+ if kw in title:
134
+ methods[kw].append(a)
135
+ break
136
+
137
+ created = 0
138
+
139
+ # Tools index
140
+ if tools:
141
+ (ref_dir / 'Tools').mkdir(parents=True, exist_ok=True)
142
+ content = f'''---
143
+ title: "工具索引"
144
+ created: {datetime.now().strftime('%Y-%m-%d')}
145
+ type: reference
146
+ tags: [reference, tools]
147
+ ---
148
+
149
+ # 工具索引
150
+
151
+ > 自动从 {len(articles)} 篇文章中提取
152
+
153
+ '''
154
+ for kw in sorted(tools, key=lambda k: len(tools[k]), reverse=True):
155
+ if len(tools[kw]) < 3: continue
156
+ content += f'\n## {kw} ({len(tools[kw])} 篇)\n'
157
+ for a in tools[kw][:5]:
158
+ content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:60]}]]\n'
159
+
160
+ (ref_dir / 'Tools' / '工具索引.md').write_text(content, encoding='utf-8')
161
+ created += 1
162
+ print(f' Tools: {len(tools)} categories')
163
+
164
+ # Methods index
165
+ if methods:
166
+ (ref_dir / 'Methods').mkdir(parents=True, exist_ok=True)
167
+ content = f'''---
168
+ title: "方法论索引"
169
+ created: {datetime.now().strftime('%Y-%m-%d')}
170
+ type: reference
171
+ tags: [reference, methods]
172
+ ---
173
+
174
+ # 方法论索引
175
+
176
+ > 自动从 {len(articles)} 篇文章中提取
177
+
178
+ '''
179
+ for kw in sorted(methods, key=lambda k: len(methods[k]), reverse=True):
180
+ if len(methods[kw]) < 3: continue
181
+ content += f'\n## {kw} ({len(methods[kw])} 篇)\n'
182
+ for a in methods[kw][:5]:
183
+ content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:60]}]]\n'
184
+
185
+ (ref_dir / 'Methods' / '方法论索引.md').write_text(content, encoding='utf-8')
186
+ created += 1
187
+ print(f' Methods: {len(methods)} categories')
188
+
189
+ return created
190
+
191
+
192
+ def generate_projects(api_key, vault_path, articles):
193
+ """006_Projects: AI 生成项目提案。"""
194
+ proj_dir = Path(vault_path) / '006_Projects'
195
+ proj_dir.mkdir(parents=True, exist_ok=True)
196
+
197
+ # 取最大的主题生成项目
198
+ by_topic = defaultdict(list)
199
+ for a in articles:
200
+ t = a['topic'] or '综合'
201
+ by_topic[t].append(a)
202
+
203
+ # 只对文章数 > 50 的主题生成项目
204
+ created = 0
205
+ for topic, arts in sorted(by_topic.items(), key=lambda x: len(x[1]), reverse=True):
206
+ if len(arts) < 50: continue
207
+ if (proj_dir / f'Proj-{topic}.md').exists(): continue
208
+
209
+ samples = sorted(arts, key=lambda a: len(a['title']))[-8:]
210
+ titles = '\n'.join(f'- {a["title"][:50]}' for a in samples)
211
+
212
+ prompt = f'''为 {topic} 领域生成一个研究项目提案(150字),格式:
213
+ ## 项目名称
214
+ ## 目标
215
+ ## 关键里程碑 (3个)
216
+ ## 所需资源
217
+
218
+ 基于文章样本:
219
+ {titles}
220
+
221
+ 只返回提案内容。'''
222
+
223
+ try:
224
+ text = call_deepseek(prompt, api_key, max_tokens=400)
225
+ except Exception as e:
226
+ print(f' [WARN] {topic}: {e}')
227
+ continue
228
+
229
+ content = f'''---
230
+ title: "{topic} 研究项目"
231
+ created: {datetime.now().strftime('%Y-%m-%d')}
232
+ type: project
233
+ hasTopic: [[{topic}]]
234
+ tags: [project, {topic.lower()}]
235
+ status: proposed
236
+ article_count: {len(arts)}
237
+ ---
238
+
239
+ # {topic} 研究项目
240
+
241
+ {text}
242
+
243
+ ---
244
+
245
+ ## 📊 数据基础
246
+
247
+ - **文章数**: {len(arts)} 篇
248
+ - **覆盖日期**: {len(set(a["date"] for a in arts))} 天
249
+
250
+ ## 📚 相关文献
251
+
252
+ '''
253
+ for a in arts[:20]:
254
+ content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:50]}]]\n'
255
+
256
+ content += f'''
257
+
258
+ ## 🔗 相关资源
259
+
260
+ - [[MOC-{topic}]] — 主题导航
261
+ - [[Perm-{topic}]] — 核心知识
262
+ - [[idea-{topic}-研究想法]] — 研究想法
263
+ '''
264
+
265
+ (proj_dir / f'Proj-{topic}.md').write_text(content, encoding='utf-8')
266
+ created += 1
267
+ print(f' [{topic}] → Proj-{topic}.md ({len(arts)} articles)')
268
+
269
+ return created
270
+
271
+
272
+ def main():
273
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
274
+ import argparse
275
+ p = argparse.ArgumentParser(description='全自动知识升级管道')
276
+ p.add_argument('--vault', default=DEFAULT_VAULT)
277
+ p.add_argument('--api-key', default='')
278
+ p.add_argument('--skip-ai', action='store_true')
279
+ args = p.parse_args()
280
+
281
+ api_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '')
282
+
283
+ print('📊 收集数据...')
284
+ articles = collect_articles(args.vault)
285
+ print(f' {len(articles)} 篇文章')
286
+
287
+ # 004: Permanent Notes (AI)
288
+ if not args.skip_ai and api_key:
289
+ print('\n📝 生成 004_Permanent 原子笔记...')
290
+ p_count = generate_permanent(api_key, args.vault, articles)
291
+ else:
292
+ p_count = 0
293
+
294
+ # 005: Reference (keyword-based, no AI needed)
295
+ print('\n📚 生成 005_Reference 工具/方法论索引...')
296
+ r_count = generate_reference(args.vault, articles)
297
+
298
+ # 006: Projects (AI)
299
+ if not args.skip_ai and api_key:
300
+ print('\n🎯 生成 006_Projects 项目提案...')
301
+ j_count = generate_projects(api_key, args.vault, articles)
302
+ else:
303
+ j_count = 0
304
+
305
+ total = p_count + r_count + j_count
306
+ print(f'\n✓ 全自动完成: {total} 个文件 (004:{p_count} + 005:{r_count} + 006:{j_count})')
307
+
308
+
309
+ if __name__ == '__main__':
310
+ main()