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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
本地收藏服务器 — 浏览器点击收藏实时同步到磁盘 收藏/ 文件夹。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/fav_server.py --date 2026-05-19
|
|
7
|
+
python scripts/fav_server.py --date 2026-05-19 --port 8765
|
|
8
|
+
|
|
9
|
+
打开 http://localhost:8765 即可使用,点击 ☆ 收藏按钮实时写入磁盘。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys, os, json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
15
|
+
import urllib.request
|
|
16
|
+
|
|
17
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
18
|
+
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
|
|
19
|
+
SOURCE_ROOT = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FavHandler(SimpleHTTPRequestHandler):
|
|
23
|
+
"""处理静态文件 + /api/fav/* 端点。"""
|
|
24
|
+
date_str = None
|
|
25
|
+
date_dir = None
|
|
26
|
+
|
|
27
|
+
# 确保文本文件以 UTF-8 编码发送,避免浏览器乱码
|
|
28
|
+
extensions_map = {**SimpleHTTPRequestHandler.extensions_map,
|
|
29
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
30
|
+
'.html': 'text/html; charset=utf-8',
|
|
31
|
+
'.css': 'text/css; charset=utf-8',
|
|
32
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
33
|
+
'.json': 'application/json; charset=utf-8',
|
|
34
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def end_headers(self):
|
|
38
|
+
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
39
|
+
self.send_header('Pragma', 'no-cache')
|
|
40
|
+
self.send_header('Expires', '0')
|
|
41
|
+
super().end_headers()
|
|
42
|
+
|
|
43
|
+
def do_GET(self):
|
|
44
|
+
try:
|
|
45
|
+
if self.path.startswith('/api/fav/list'):
|
|
46
|
+
self._handle_fav_list()
|
|
47
|
+
elif self.path == '/api/read/list':
|
|
48
|
+
self._handle_read_list()
|
|
49
|
+
elif self.path.startswith('/api/notes'):
|
|
50
|
+
self._handle_notes_get()
|
|
51
|
+
elif self.path.startswith('/proxy?url='):
|
|
52
|
+
self._handle_proxy()
|
|
53
|
+
else:
|
|
54
|
+
super().do_GET()
|
|
55
|
+
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
|
|
56
|
+
pass
|
|
57
|
+
except Exception:
|
|
58
|
+
try: self.send_error(500)
|
|
59
|
+
except: pass
|
|
60
|
+
|
|
61
|
+
def do_POST(self):
|
|
62
|
+
try:
|
|
63
|
+
if self.path == '/api/fav/toggle':
|
|
64
|
+
self._handle_fav_toggle()
|
|
65
|
+
elif self.path == '/api/read/toggle':
|
|
66
|
+
self._handle_read_toggle()
|
|
67
|
+
elif self.path == '/api/explain':
|
|
68
|
+
self._handle_explain()
|
|
69
|
+
elif self.path.startswith('/api/notes'):
|
|
70
|
+
self._handle_notes_post()
|
|
71
|
+
else:
|
|
72
|
+
self.send_error(404)
|
|
73
|
+
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
|
|
74
|
+
pass
|
|
75
|
+
except Exception:
|
|
76
|
+
try: self.send_error(500)
|
|
77
|
+
except: pass
|
|
78
|
+
|
|
79
|
+
def handle_one_request(self):
|
|
80
|
+
"""Override to catch any unhandled exceptions at the lowest level."""
|
|
81
|
+
try:
|
|
82
|
+
super().handle_one_request()
|
|
83
|
+
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
|
|
84
|
+
pass
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def _handle_proxy(self):
|
|
89
|
+
"""代理微信CDN图片,绕过防盗链Referer检查。"""
|
|
90
|
+
from urllib.parse import unquote
|
|
91
|
+
qs = self.path.split('?', 1)[1] if '?' in self.path else ''
|
|
92
|
+
url = ''
|
|
93
|
+
for p in qs.split('&'):
|
|
94
|
+
if p.startswith('url='):
|
|
95
|
+
url = unquote(p[4:])
|
|
96
|
+
break
|
|
97
|
+
if not url or not url.startswith(('http://', 'https://')):
|
|
98
|
+
self.send_error(400, 'Missing url')
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
req = urllib.request.Request(url, headers={
|
|
102
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
103
|
+
'Referer': 'https://mp.weixin.qq.com/',
|
|
104
|
+
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
|
|
105
|
+
})
|
|
106
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
107
|
+
data = resp.read(10 * 1024 * 1024) # max 10MB
|
|
108
|
+
content_type = resp.headers.get('Content-Type', 'image/jpeg')
|
|
109
|
+
self.send_response(200)
|
|
110
|
+
self.send_header('Content-Type', content_type)
|
|
111
|
+
self.send_header('Content-Length', len(data))
|
|
112
|
+
self.send_header('Cache-Control', 'public, max-age=86400')
|
|
113
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
114
|
+
self.end_headers()
|
|
115
|
+
self.wfile.write(data)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self.send_error(502, f'Proxy failed: {e}')
|
|
118
|
+
|
|
119
|
+
def _handle_fav_list(self):
|
|
120
|
+
favs = self._read_favs()
|
|
121
|
+
self._send_json(favs)
|
|
122
|
+
|
|
123
|
+
def _handle_fav_toggle(self):
|
|
124
|
+
content_len = int(self.headers.get('Content-Length', 0))
|
|
125
|
+
body = self.rfile.read(content_len)
|
|
126
|
+
try:
|
|
127
|
+
data = json.loads(body.decode('utf-8'))
|
|
128
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
129
|
+
self.send_error(400, 'Invalid JSON')
|
|
130
|
+
return
|
|
131
|
+
article_id = data.get('id', '')
|
|
132
|
+
if not article_id:
|
|
133
|
+
self.send_error(400, 'Missing id')
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
favs = self._read_favs()
|
|
137
|
+
if article_id in favs:
|
|
138
|
+
favs.remove(article_id)
|
|
139
|
+
action = 'removed'
|
|
140
|
+
else:
|
|
141
|
+
favs.append(article_id)
|
|
142
|
+
action = 'added'
|
|
143
|
+
|
|
144
|
+
self._write_favs(favs)
|
|
145
|
+
self._sync_folder(favs)
|
|
146
|
+
self._send_json({'ok': True, 'action': action, 'id': article_id, 'count': len(favs)})
|
|
147
|
+
|
|
148
|
+
def _read_favs(self):
|
|
149
|
+
fav_file = Path(self.date_dir) / '.fav_state.json'
|
|
150
|
+
if fav_file.exists():
|
|
151
|
+
try:
|
|
152
|
+
with open(fav_file, 'r', encoding='utf-8') as f:
|
|
153
|
+
return json.load(f)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
def _write_favs(self, favs):
|
|
159
|
+
fav_file = Path(self.date_dir) / '.fav_state.json'
|
|
160
|
+
with open(fav_file, 'w', encoding='utf-8') as f:
|
|
161
|
+
json.dump(favs, f, ensure_ascii=False, indent=2)
|
|
162
|
+
|
|
163
|
+
def _sync_folder(self, favs):
|
|
164
|
+
"""实时同步 收藏/ 文件夹 — 添加/移除文章文件。"""
|
|
165
|
+
fav_dir = Path(self.date_dir) / '收藏'
|
|
166
|
+
fav_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
desired_names = set()
|
|
169
|
+
for rel_path in favs:
|
|
170
|
+
src = Path(self.date_dir) / rel_path
|
|
171
|
+
if src.exists():
|
|
172
|
+
desired_names.add(src.name)
|
|
173
|
+
link = fav_dir / src.name
|
|
174
|
+
if not link.exists():
|
|
175
|
+
try:
|
|
176
|
+
link.symlink_to(os.path.relpath(src, fav_dir))
|
|
177
|
+
except OSError:
|
|
178
|
+
import shutil
|
|
179
|
+
shutil.copy2(src, link)
|
|
180
|
+
|
|
181
|
+
# 移除不在收藏列表中的文件
|
|
182
|
+
for item in fav_dir.iterdir():
|
|
183
|
+
if item.name not in desired_names:
|
|
184
|
+
if item.is_symlink() or item.is_file():
|
|
185
|
+
item.unlink()
|
|
186
|
+
|
|
187
|
+
# ---- 已读状态 ----
|
|
188
|
+
def _read_run_state(self):
|
|
189
|
+
f = Path(self.date_dir) / '.read_state.json'
|
|
190
|
+
if f.exists():
|
|
191
|
+
try: return json.loads(f.read_text(encoding='utf-8'))
|
|
192
|
+
except: pass
|
|
193
|
+
return {}
|
|
194
|
+
def _write_run_state(self, state):
|
|
195
|
+
(Path(self.date_dir) / '.read_state.json').write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
196
|
+
|
|
197
|
+
def _handle_read_list(self):
|
|
198
|
+
self._send_json(self._read_run_state())
|
|
199
|
+
|
|
200
|
+
def _handle_read_toggle(self):
|
|
201
|
+
content_len = int(self.headers.get('Content-Length', 0))
|
|
202
|
+
body = self.rfile.read(content_len)
|
|
203
|
+
try: data = json.loads(body.decode('utf-8'))
|
|
204
|
+
except: self.send_error(400); return
|
|
205
|
+
aid = data.get('id', '')
|
|
206
|
+
if not aid: self.send_error(400); return
|
|
207
|
+
state = self._read_run_state()
|
|
208
|
+
if aid in state: del state[aid]; action = 'unread'
|
|
209
|
+
else: state[aid] = True; action = 'read'
|
|
210
|
+
self._write_run_state(state)
|
|
211
|
+
self._send_json({'ok': True, 'action': action, 'id': aid})
|
|
212
|
+
|
|
213
|
+
def _handle_explain(self):
|
|
214
|
+
content_len = int(self.headers.get('Content-Length', 0))
|
|
215
|
+
body = self.rfile.read(content_len)
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(body.decode('utf-8'))
|
|
218
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
219
|
+
self.send_error(400, 'Invalid JSON')
|
|
220
|
+
return
|
|
221
|
+
text = data.get('text', '').strip()
|
|
222
|
+
context = data.get('context', '').strip()
|
|
223
|
+
if not text:
|
|
224
|
+
self.send_error(400, 'Missing text')
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# 读 DeepSeek API key
|
|
228
|
+
api_key = os.environ.get('ANTHROPIC_AUTH_TOKEN', '') or os.environ.get('DEEPSEEK_API_KEY', '')
|
|
229
|
+
|
|
230
|
+
prompt = f'''你是一个阅读助手。用户正在读一篇文章,有问题要问你。
|
|
231
|
+
|
|
232
|
+
文章全文(供参考):
|
|
233
|
+
{context[:8000]}
|
|
234
|
+
|
|
235
|
+
用户提问:"{text}"
|
|
236
|
+
|
|
237
|
+
回答规则:
|
|
238
|
+
1. 优先用文章内容回答;如果问题与文章相关但文章信息不足,先用文章已有的部分,再用你的知识补充
|
|
239
|
+
2. 如果问题与文章完全无关,直接用自己的知识回答,不要拒绝
|
|
240
|
+
3. 用大白话解释,假设用户是非技术背景,避免术语堆砌
|
|
241
|
+
4. 简短直接,不要啰嗦'''
|
|
242
|
+
|
|
243
|
+
payload = json.dumps({
|
|
244
|
+
'model': 'deepseek-chat',
|
|
245
|
+
'messages': [
|
|
246
|
+
{'role': 'system', 'content': '你是一个简洁准确的知识助手,用中文回答。'},
|
|
247
|
+
{'role': 'user', 'content': prompt}
|
|
248
|
+
],
|
|
249
|
+
'max_tokens': 1000,
|
|
250
|
+
'temperature': 0.3
|
|
251
|
+
}).encode('utf-8')
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
req = urllib.request.Request('https://api.deepseek.com/v1/chat/completions', data=payload)
|
|
255
|
+
req.add_header('Content-Type', 'application/json; charset=utf-8')
|
|
256
|
+
req.add_header('Authorization', f'Bearer {api_key}')
|
|
257
|
+
resp = urllib.request.urlopen(req, timeout=15)
|
|
258
|
+
result = json.loads(resp.read().decode('utf-8'))
|
|
259
|
+
explanation = result['choices'][0]['message']['content']
|
|
260
|
+
self._send_json({'ok': True, 'text': text, 'explanation': explanation})
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self._send_json({'ok': False, 'error': f'AI 调用失败: {str(e)}'})
|
|
263
|
+
|
|
264
|
+
# ---- 笔记功能 ----
|
|
265
|
+
def _notes_file(self):
|
|
266
|
+
return Path(self.date_dir) / 'notes.json'
|
|
267
|
+
|
|
268
|
+
def _read_notes(self):
|
|
269
|
+
f = self._notes_file()
|
|
270
|
+
if f.exists():
|
|
271
|
+
try: return json.loads(f.read_text(encoding='utf-8'))
|
|
272
|
+
except: pass
|
|
273
|
+
return {}
|
|
274
|
+
|
|
275
|
+
def _write_notes(self, notes):
|
|
276
|
+
self._notes_file().write_text(json.dumps(notes, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
277
|
+
|
|
278
|
+
def _handle_notes_get(self):
|
|
279
|
+
"""GET /api/notes?article=xxx → 返回该文章的所有笔记"""
|
|
280
|
+
qs = self.path.split('?', 1)[1] if '?' in self.path else ''
|
|
281
|
+
params = {}
|
|
282
|
+
for p in qs.split('&'):
|
|
283
|
+
if '=' in p:
|
|
284
|
+
k, v = p.split('=', 1)
|
|
285
|
+
params[k] = v
|
|
286
|
+
article_id = params.get('article', '')
|
|
287
|
+
notes = self._read_notes()
|
|
288
|
+
if article_id:
|
|
289
|
+
self._send_json(notes.get(article_id, []))
|
|
290
|
+
else:
|
|
291
|
+
self._send_json(notes)
|
|
292
|
+
|
|
293
|
+
def _handle_notes_post(self):
|
|
294
|
+
"""POST /api/notes → 添加笔记, DELETE /api/notes → 删除笔记"""
|
|
295
|
+
content_len = int(self.headers.get('Content-Length', 0))
|
|
296
|
+
body = self.rfile.read(content_len)
|
|
297
|
+
try:
|
|
298
|
+
data = json.loads(body.decode('utf-8'))
|
|
299
|
+
except:
|
|
300
|
+
self.send_error(400); return
|
|
301
|
+
|
|
302
|
+
if self.path == '/api/notes/delete':
|
|
303
|
+
article_id = data.get('article', '')
|
|
304
|
+
highlight_id = data.get('id', '')
|
|
305
|
+
if not article_id or not highlight_id:
|
|
306
|
+
self.send_error(400); return
|
|
307
|
+
notes = self._read_notes()
|
|
308
|
+
if article_id in notes:
|
|
309
|
+
notes[article_id] = [h for h in notes[article_id] if h.get('id') != highlight_id]
|
|
310
|
+
if not notes[article_id]:
|
|
311
|
+
del notes[article_id]
|
|
312
|
+
self._write_notes(notes)
|
|
313
|
+
self._send_json({'ok': True})
|
|
314
|
+
else:
|
|
315
|
+
# POST /api/notes → add note
|
|
316
|
+
article_id = data.get('article', '')
|
|
317
|
+
text = data.get('text', '').strip()
|
|
318
|
+
note = data.get('note', '').strip()
|
|
319
|
+
if not article_id or not text:
|
|
320
|
+
self.send_error(400); return
|
|
321
|
+
import uuid, time
|
|
322
|
+
hid = uuid.uuid4().hex[:8]
|
|
323
|
+
notes = self._read_notes()
|
|
324
|
+
if article_id not in notes:
|
|
325
|
+
notes[article_id] = []
|
|
326
|
+
notes[article_id].append({
|
|
327
|
+
'id': hid,
|
|
328
|
+
'text': text,
|
|
329
|
+
'note': note,
|
|
330
|
+
'created': time.strftime('%Y-%m-%d %H:%M', time.localtime())
|
|
331
|
+
})
|
|
332
|
+
self._write_notes(notes)
|
|
333
|
+
self._send_json({'ok': True, 'id': hid})
|
|
334
|
+
|
|
335
|
+
def _send_json(self, data):
|
|
336
|
+
body = json.dumps(data, ensure_ascii=False).encode('utf-8')
|
|
337
|
+
self.send_response(200)
|
|
338
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
339
|
+
self.send_header('Content-Length', len(body))
|
|
340
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
341
|
+
self.end_headers()
|
|
342
|
+
self.wfile.write(body)
|
|
343
|
+
|
|
344
|
+
def do_OPTIONS(self):
|
|
345
|
+
self.send_response(200)
|
|
346
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
347
|
+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
348
|
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
349
|
+
self.end_headers()
|
|
350
|
+
|
|
351
|
+
def log_message(self, format, *args):
|
|
352
|
+
# 简洁日志
|
|
353
|
+
if '/api/' in str(args[0]):
|
|
354
|
+
print(f' {args[0]}')
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def main():
|
|
358
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
359
|
+
import argparse
|
|
360
|
+
parser = argparse.ArgumentParser(description='本地收藏服务器')
|
|
361
|
+
parser.add_argument('--date', required=True, help='日期 YYYY-MM-DD')
|
|
362
|
+
parser.add_argument('--port', type=int, default=8765, help='端口 (默认 8765)')
|
|
363
|
+
args = parser.parse_args()
|
|
364
|
+
|
|
365
|
+
date_dir = os.path.join(SOURCE_ROOT, args.date)
|
|
366
|
+
if not os.path.isdir(date_dir):
|
|
367
|
+
print(f'[ERROR] 目录不存在: {date_dir}')
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
|
|
370
|
+
# 配置 Handler
|
|
371
|
+
FavHandler.date_str = args.date
|
|
372
|
+
FavHandler.date_dir = date_dir
|
|
373
|
+
|
|
374
|
+
# 切换到日报目录以提供静态文件服务
|
|
375
|
+
os.chdir(date_dir)
|
|
376
|
+
|
|
377
|
+
server = HTTPServer(('0.0.0.0', args.port), FavHandler)
|
|
378
|
+
print(f'⭐ 收藏服务器已启动')
|
|
379
|
+
print(f' 打开: http://localhost:{args.port}')
|
|
380
|
+
print(f' 日期: {args.date}')
|
|
381
|
+
print(f' 收藏文件夹: {date_dir}/收藏/')
|
|
382
|
+
print(f' Ctrl+C 停止')
|
|
383
|
+
print()
|
|
384
|
+
try:
|
|
385
|
+
server.serve_forever()
|
|
386
|
+
except KeyboardInterrupt:
|
|
387
|
+
print('\n已停止')
|
|
388
|
+
server.server_close()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == '__main__':
|
|
392
|
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""修复已生成文章的主题分类(关键词 fallback)。用法: python scripts/fix_topics.py 2026-05-19"""
|
|
3
|
+
import sys, os, re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
7
|
+
from biz_daily import _guess_topic
|
|
8
|
+
|
|
9
|
+
TOPICS = ['AI', '学术', '新闻', '文学', '投资']
|
|
10
|
+
|
|
11
|
+
def fix_date(date_str: str):
|
|
12
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
13
|
+
base = Path('output/biz-daily') / date_str
|
|
14
|
+
if not base.is_dir():
|
|
15
|
+
print(f'[ERROR] 目录不存在: {base}')
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
fixed = 0
|
|
19
|
+
for topic in TOPICS:
|
|
20
|
+
topic_dir = base / topic
|
|
21
|
+
if not topic_dir.is_dir():
|
|
22
|
+
continue
|
|
23
|
+
for md_file in topic_dir.glob('*.md'):
|
|
24
|
+
try:
|
|
25
|
+
content = md_file.read_text(encoding='utf-8')
|
|
26
|
+
except:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
# Extract current topic from frontmatter
|
|
30
|
+
m = re.search(r'^topic:\s*(.+)', content, re.MULTILINE)
|
|
31
|
+
old_topic = m.group(1).strip() if m else ''
|
|
32
|
+
|
|
33
|
+
# Extract title
|
|
34
|
+
title = md_file.stem
|
|
35
|
+
m2 = re.search(r'^title:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
|
|
36
|
+
if m2:
|
|
37
|
+
title = m2.group(1).strip().strip('"').strip("'")
|
|
38
|
+
|
|
39
|
+
# Guess topic
|
|
40
|
+
article = {'title': title, 'account_name': ''}
|
|
41
|
+
m3 = re.search(r'^source:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
|
|
42
|
+
if m3:
|
|
43
|
+
article['account_name'] = m3.group(1).strip().strip('"').strip("'")
|
|
44
|
+
|
|
45
|
+
guessed = _guess_topic(article)
|
|
46
|
+
|
|
47
|
+
if guessed != old_topic:
|
|
48
|
+
# Update frontmatter
|
|
49
|
+
new_content = re.sub(
|
|
50
|
+
r'^topic:\s*.+', f'topic: {guessed}',
|
|
51
|
+
content, flags=re.MULTILINE
|
|
52
|
+
)
|
|
53
|
+
# Update topic folder if needed - move file
|
|
54
|
+
if guessed != topic:
|
|
55
|
+
new_dir = base / guessed
|
|
56
|
+
new_dir.mkdir(exist_ok=True)
|
|
57
|
+
new_path = new_dir / md_file.name
|
|
58
|
+
md_file.rename(new_path)
|
|
59
|
+
# Also update the file at new location
|
|
60
|
+
new_path.write_text(new_content, encoding='utf-8')
|
|
61
|
+
print(f' [FIX] {title[:50]} -> {guessed} (was {topic})')
|
|
62
|
+
else:
|
|
63
|
+
md_file.write_text(new_content, encoding='utf-8')
|
|
64
|
+
fixed += 1
|
|
65
|
+
|
|
66
|
+
print(f'\n✓ 修复 {fixed} 篇,未变动 {sum(1 for _ in base.rglob("*.md") if _.name != "README.md") - fixed} 篇')
|
|
67
|
+
|
|
68
|
+
# Rebuild README
|
|
69
|
+
from biz_daily import sanitize_filename
|
|
70
|
+
all_articles = []
|
|
71
|
+
for topic in TOPICS:
|
|
72
|
+
topic_dir = base / topic
|
|
73
|
+
if not topic_dir.is_dir():
|
|
74
|
+
continue
|
|
75
|
+
for md_file in sorted(topic_dir.glob('*.md')):
|
|
76
|
+
content = md_file.read_text(encoding='utf-8')
|
|
77
|
+
title = md_file.stem
|
|
78
|
+
source = ''
|
|
79
|
+
article_time = ''
|
|
80
|
+
for line in content.split('\n'):
|
|
81
|
+
if line.startswith('title:'):
|
|
82
|
+
title = line.split(':', 1)[1].strip().strip('"').strip("'")
|
|
83
|
+
elif line.startswith('source:'):
|
|
84
|
+
source = line.split(':', 1)[1].strip().strip('"').strip("'")
|
|
85
|
+
mm = re.search(r'时间:\d{4}-\d{2}-\d{2} (\d{2}:\d{2})', content)
|
|
86
|
+
if mm: article_time = mm.group(1)
|
|
87
|
+
all_articles.append({'title': title, 'source': source, 'time': article_time, 'topic': topic, 'file': md_file.name})
|
|
88
|
+
|
|
89
|
+
index_lines = [f'# 公众号日报 — {date_str}', '', f'共 {len(all_articles)} 篇推送,按主题分类', '']
|
|
90
|
+
for topic in TOPICS:
|
|
91
|
+
group = [a for a in all_articles if a['topic'] == topic]
|
|
92
|
+
if not group: continue
|
|
93
|
+
index_lines.append(f'## {topic} ({len(group)}篇)')
|
|
94
|
+
index_lines.append('')
|
|
95
|
+
index_lines.append('| # | 时间 | 公众号 | 标题 |')
|
|
96
|
+
index_lines.append('|---|------|--------|------|')
|
|
97
|
+
for j, a in enumerate(group):
|
|
98
|
+
index_lines.append(f'| {j+1} | {a["time"]} | {a["source"]} | [{a["title"]}](./{topic}/{a["file"]}) |')
|
|
99
|
+
index_lines.append('')
|
|
100
|
+
|
|
101
|
+
with open(base / 'README.md', 'w', encoding='utf-8') as f:
|
|
102
|
+
f.write('\n'.join(index_lines))
|
|
103
|
+
|
|
104
|
+
print(f' README 已重建')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == '__main__':
|
|
108
|
+
if len(sys.argv) < 2:
|
|
109
|
+
print('用法: python scripts/fix_topics.py 2026-05-19')
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
fix_date(sys.argv[1])
|