weflow-cli 1.7.0 → 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 +1949 -1
- package/OPERATIONS.md +774 -1
- package/README.en.md +93 -7
- package/README.md +31 -6
- package/bin/weflow-cli.ts +934 -27
- package/dist/bin/weflow-cli.js +952 -27
- 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 +19 -2
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +11 -1
- 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 +1 -1
- package/dist/src/services/chatService.d.ts.map +1 -1
- package/dist/src/services/chatService.js +5 -2
- 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/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 +8 -2
- package/dist/src/services/syncService.d.ts.map +1 -1
- package/dist/src/services/syncService.js +26 -7
- package/dist/src/services/syncService.js.map +1 -1
- 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 +1424 -0
- package/docs/EXTENDING.md +187 -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 -98
- package/docs/SETUP.md +22 -0
- package/docs/SYNC_CONTRACT.md +31 -3
- 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 +35 -33
- 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 -1460
- 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 +27 -2
- 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 +6 -2
- package/src/services/configService.ts +403 -385
- package/src/services/pythonBridge.ts +139 -0
- package/src/services/syncService.ts +37 -9
- 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
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
我的笔记 → 知识卡 — 让 AI 读你自己写的东西,再长出知识页。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/user_notes.py --dry-run --json # 看代价(只读本地、零出境)
|
|
7
|
+
python scripts/user_notes.py --yes # 读你的笔记,每条一次调用
|
|
8
|
+
python scripts/compile_wiki.py --source output/user-notes
|
|
9
|
+
|
|
10
|
+
## 这条线为什么与另外三条不同
|
|
11
|
+
|
|
12
|
+
另外三条(文章/对话/收藏)读的都是**素材**——别人写的文章、你聊过的话。这一条读的是
|
|
13
|
+
**你自己写的东西**,所以有两处必须守死:
|
|
14
|
+
|
|
15
|
+
1. **你的笔记只读,绝不修改、绝不移动**。AI 产出的是**另一份东西**(卡片),写在
|
|
16
|
+
`output/user-notes/` 里。要改要删你的笔记,只有你自己来。
|
|
17
|
+
2. **卡上的摘要标 `summary_by: model`**:那是**模型读你的笔记的理解**,不是你的原话。
|
|
18
|
+
混起来最危险——一段模型的理解被当成"我自己写的",而它可能理解偏了。
|
|
19
|
+
|
|
20
|
+
一处刻意的设计:**你笔记里自己写的 `[[链接]]` 优先**。你自己起的概念名比模型起的好,
|
|
21
|
+
所以两者合并时以你的为准(模型只补你没提到的那部分)。
|
|
22
|
+
|
|
23
|
+
## 落在哪个目录
|
|
24
|
+
|
|
25
|
+
默认读你的"人写层":`000_Inbox` / `003_Ideas` / `004_Permanent` / `005_Reference` /
|
|
26
|
+
`006_Projects` / `008_MOC` / `999_Archive`,以及**库根目录**散着的 .md(就是你在 Obsidian
|
|
27
|
+
里直接新建的那种)。绝不读 `Wiki/`(那是生成的)、`001_Daily`/`002_Literature`(管线产出)
|
|
28
|
+
和 `Sources/`(原始素材)。
|
|
29
|
+
"""
|
|
30
|
+
import argparse
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
from datetime import datetime, timezone, timedelta
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
+
from _utils import (SEPARATOR, call_deepseek, get_api_key, load_config, # noqa: E402
|
|
39
|
+
note_path, parse_frontmatter, plain, strip_wx_ads, write_with_frontmatter)
|
|
40
|
+
|
|
41
|
+
TZ = timezone(timedelta(hours=8))
|
|
42
|
+
VAULT = 'output/wechat-vault'
|
|
43
|
+
OUTPUT_ROOT = 'output/user-notes'
|
|
44
|
+
DEFAULT_LIMIT = 20
|
|
45
|
+
MIN_NOTE_CHARS = 120 # 比另外两条低:自己写的一段话往往不长,但句句是信号
|
|
46
|
+
MAX_NOTE_CHARS = 6000
|
|
47
|
+
|
|
48
|
+
# 你的"人写层"。不在这里面的目录一律不读(生成的、管线的、素材的都不动)
|
|
49
|
+
HUMAN_LAYERS = ('000_Inbox', '003_Ideas', '004_Permanent', '005_Reference',
|
|
50
|
+
'006_Projects', '008_MOC', '999_Archive')
|
|
51
|
+
|
|
52
|
+
NOTE_PROMPT = """下面是**用户自己写的一篇笔记**。你是知识库的编译者。
|
|
53
|
+
|
|
54
|
+
请只依据这篇笔记,返回一个 JSON 对象(不要解释、不要代码块围栏):
|
|
55
|
+
{{"summary": "这篇笔记在讲什么(2-3 句,只写笔记里有的,用你自己的话复述)",
|
|
56
|
+
"concepts": [{{"name": "概念名(2-12 字,不要带书名号引号方括号)", "desc": "这篇笔记关于它说了什么(一句话)"}}]}}
|
|
57
|
+
|
|
58
|
+
要求:
|
|
59
|
+
1. **只写笔记里有的**。笔记没展开的地方不许替他补;拿不准的宁可不写。
|
|
60
|
+
**尤其不要替他把结论补完整**——他不确定的地方,你也不许装作确定。
|
|
61
|
+
2. `concepts` 2-6 条、按重要度排序;概念要具体("MCP 协议"可以,"技术"不行),
|
|
62
|
+
也不是分类词("AI"、"工作"这种不算概念)。
|
|
63
|
+
3. 每条 desc 是"这篇笔记关于它的说法",不是百科定义。
|
|
64
|
+
4. 全部中文,不要 markdown 语法(不要 #、*、-)。
|
|
65
|
+
|
|
66
|
+
笔记标题:{title}
|
|
67
|
+
|
|
68
|
+
正文:
|
|
69
|
+
{body}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def list_human_notes(layers=HUMAN_LAYERS, vault=VAULT) -> list:
|
|
74
|
+
"""你的笔记:人写层里的 .md + 库根目录散着的 .md。**生成的东西一概不读。**"""
|
|
75
|
+
root = Path(vault)
|
|
76
|
+
if not root.exists():
|
|
77
|
+
return []
|
|
78
|
+
found = []
|
|
79
|
+
for path in sorted(root.glob('*.md')): # 库根目录(你在 Obsidian 里新建的)
|
|
80
|
+
found.append(path)
|
|
81
|
+
for layer in layers:
|
|
82
|
+
directory = root / layer
|
|
83
|
+
if directory.is_dir():
|
|
84
|
+
found.extend(sorted(directory.rglob('*.md')))
|
|
85
|
+
notes = []
|
|
86
|
+
for path in found:
|
|
87
|
+
if path.name == '00-Overview.md' or '.obsidian' in path.parts:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
frontmatter, body = parse_frontmatter(path.read_text(encoding='utf-8'))
|
|
91
|
+
except Exception:
|
|
92
|
+
continue
|
|
93
|
+
notes.append({
|
|
94
|
+
'path': path,
|
|
95
|
+
'rel': str(path.relative_to(root)).replace('\\', '/'),
|
|
96
|
+
'title': str(frontmatter.get('title') or path.stem),
|
|
97
|
+
'body': strip_wx_ads(body),
|
|
98
|
+
'links': own_links(body),
|
|
99
|
+
})
|
|
100
|
+
return notes
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_LINK = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def own_links(body: str) -> list:
|
|
107
|
+
"""笔记里**你自己写的** `[[链接]]`(连同破折号后的说明)。"""
|
|
108
|
+
global _LINK
|
|
109
|
+
if _LINK is None:
|
|
110
|
+
import re
|
|
111
|
+
_LINK = re.compile(r'\[\[([^\]]+)\]\](?:\s*—?\s*([^\n]+))?')
|
|
112
|
+
out = []
|
|
113
|
+
for name, desc in _LINK.findall(body or ''):
|
|
114
|
+
name = plain(name).strip().strip('[]')
|
|
115
|
+
if name and name not in [item['name'] for item in out]:
|
|
116
|
+
out.append({'name': name, 'desc': plain(desc).strip()})
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def merge_concepts(own: list, mined: list, limit: int = 6) -> list:
|
|
121
|
+
"""**你自己写的链接优先**,模型补你没提到的部分。
|
|
122
|
+
|
|
123
|
+
你自己起的概念名比模型起的好用(你以后还会用它搜),所以同名的以你的为准、
|
|
124
|
+
且排在前面;模型只在没提到的地方补。
|
|
125
|
+
"""
|
|
126
|
+
merged = []
|
|
127
|
+
seen = set()
|
|
128
|
+
for item in list(own) + list(mined):
|
|
129
|
+
name = str(item.get('name') or '').strip()
|
|
130
|
+
if not name or name in seen:
|
|
131
|
+
continue
|
|
132
|
+
seen.add(name)
|
|
133
|
+
desc = str(item.get('desc') or '').strip() or '(笔记里提到,未展开)'
|
|
134
|
+
merged.append({'name': name, 'desc': desc})
|
|
135
|
+
if len(merged) >= limit:
|
|
136
|
+
break
|
|
137
|
+
return merged
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def parse_note(raw: str):
|
|
141
|
+
"""解出 `{summary, concepts}`。解不出来返回 None(**不猜**)。"""
|
|
142
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
143
|
+
return None
|
|
144
|
+
text = raw.strip()
|
|
145
|
+
start, end = text.find('{'), text.rfind('}')
|
|
146
|
+
if start < 0 or end <= start:
|
|
147
|
+
return None
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(text[start:end + 1])
|
|
150
|
+
except Exception:
|
|
151
|
+
return None
|
|
152
|
+
if not isinstance(data, dict):
|
|
153
|
+
return None
|
|
154
|
+
summary = plain(data.get('summary')).strip()
|
|
155
|
+
if not summary:
|
|
156
|
+
return None
|
|
157
|
+
# `concepts` **必须是列表**(与另外三条线同一个契约):字段在但类型不对,说明这次返回
|
|
158
|
+
# 没按形状来——那就说没解出来,别拿它的一部分当结果(尤其是别把 dict 的键当概念名)
|
|
159
|
+
if not isinstance(data.get('concepts'), list):
|
|
160
|
+
return None
|
|
161
|
+
mined = []
|
|
162
|
+
for item in data['concepts']:
|
|
163
|
+
if isinstance(item, dict):
|
|
164
|
+
name = plain(item.get('name')).strip().strip('[]')
|
|
165
|
+
desc = plain(item.get('desc')).strip()
|
|
166
|
+
if name and desc:
|
|
167
|
+
mined.append({'name': name, 'desc': desc})
|
|
168
|
+
return {'summary': summary, 'mined': mined}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_card(note: dict, summary: str, concepts: list, updated: str):
|
|
172
|
+
"""卡片 → (frontmatter, body)。**摘要标明是模型的理解**,不是你的原话。"""
|
|
173
|
+
frontmatter = {
|
|
174
|
+
'title': note['title'],
|
|
175
|
+
'type': 'user-card',
|
|
176
|
+
'source': '我的笔记',
|
|
177
|
+
'topic': '我的笔记',
|
|
178
|
+
'tags': ['我的笔记'],
|
|
179
|
+
'from': note['rel'],
|
|
180
|
+
'summary_by': 'model', # 模型读你的笔记后的理解,不是抄你的原话
|
|
181
|
+
'noteChars': len(note['body'].strip()),
|
|
182
|
+
'updated': updated,
|
|
183
|
+
}
|
|
184
|
+
parts = ['# %s\n' % note['title'], '## AI 摘要\n',
|
|
185
|
+
'(以下是模型读你笔记后的复述,不是你的原话)\n', plain(summary) + '\n']
|
|
186
|
+
if concepts:
|
|
187
|
+
parts.append('## 概念\n')
|
|
188
|
+
for concept in concepts:
|
|
189
|
+
parts.append('- [[%s]] %s %s' % (concept['name'], SEPARATOR, concept['desc']))
|
|
190
|
+
parts.append('')
|
|
191
|
+
return frontmatter, '\n'.join(parts)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def already_carded(note: dict, out_dir: str) -> bool:
|
|
195
|
+
card = note_path(out_dir, note['title'])
|
|
196
|
+
if not card.exists():
|
|
197
|
+
return False
|
|
198
|
+
try:
|
|
199
|
+
return card.stat().st_mtime >= note['path'].stat().st_mtime
|
|
200
|
+
except OSError:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def exit_code(written: int) -> int:
|
|
205
|
+
return 0 if written else 1
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
210
|
+
parser = argparse.ArgumentParser(description='读你自己的笔记,长成知识卡(只读你的笔记,绝不修改)')
|
|
211
|
+
parser.add_argument('--limit', type=int, default=DEFAULT_LIMIT,
|
|
212
|
+
help='最多处理几篇笔记(默认 %d)' % DEFAULT_LIMIT)
|
|
213
|
+
# `--vault` 主要是给测试与演示用的:默认只读你自己的 Vault,
|
|
214
|
+
# 但"能换一个库跑"让整条链路可以在临时目录里被验证(不必往你的库里塞东西)
|
|
215
|
+
parser.add_argument('--vault', default=VAULT, help='库目录(默认 %s)' % VAULT)
|
|
216
|
+
parser.add_argument('--out', default=OUTPUT_ROOT, help='卡片输出目录')
|
|
217
|
+
parser.add_argument('--refresh', action='store_true', help='已有卡的也重做')
|
|
218
|
+
parser.add_argument('--dry-run', action='store_true', help='只报要发多少给模型,不调用')
|
|
219
|
+
parser.add_argument('--yes', action='store_true', help='确认把笔记发给生成模型')
|
|
220
|
+
parser.add_argument('--json', action='store_true')
|
|
221
|
+
args = parser.parse_args()
|
|
222
|
+
|
|
223
|
+
if args.limit < 1:
|
|
224
|
+
print(json.dumps({'success': False, 'error': '--limit 要 ≥ 1'}))
|
|
225
|
+
return 1
|
|
226
|
+
|
|
227
|
+
notes = list_human_notes(vault=args.vault)
|
|
228
|
+
runnable, empty, existing = [], [], []
|
|
229
|
+
for note in notes[:args.limit]:
|
|
230
|
+
if len(note['body'].strip()) < MIN_NOTE_CHARS:
|
|
231
|
+
empty.append({'title': note['title'][:30], 'chars': len(note['body'].strip())})
|
|
232
|
+
continue
|
|
233
|
+
(runnable if args.refresh or not already_carded(note, args.out) else existing).append(note)
|
|
234
|
+
|
|
235
|
+
if args.dry_run:
|
|
236
|
+
preview = {
|
|
237
|
+
'success': True, 'dryRun': True, 'action': 'user-notes',
|
|
238
|
+
'notes': len(notes), 'calls': len(runnable),
|
|
239
|
+
'empty': len(empty), 'alreadyCarded': len(existing),
|
|
240
|
+
'chars': sum(len(n['body']) for n in runnable),
|
|
241
|
+
'emptyTitles': [item['title'] for item in empty][:10],
|
|
242
|
+
'model': 'DeepSeek(生成)',
|
|
243
|
+
'readsLocalData': True, 'invokesAI': False, 'modifiesYourNotes': False,
|
|
244
|
+
'note': '每条笔记一次调用;你的笔记**只读**,产出写到 %s' % args.out,
|
|
245
|
+
}
|
|
246
|
+
if args.json:
|
|
247
|
+
print(json.dumps(preview, ensure_ascii=False, indent=2))
|
|
248
|
+
else:
|
|
249
|
+
print('你的笔记 %d 篇:会调用模型 %d 次(空/太短跳过 %d 篇,已有卡 %d 篇)'
|
|
250
|
+
% (len(notes), len(runnable), len(empty), len(existing)))
|
|
251
|
+
if empty:
|
|
252
|
+
print(' 跳过的:%s' % '、'.join(item['title'] for item in empty[:8]))
|
|
253
|
+
if not notes:
|
|
254
|
+
print(' 还没有笔记——在人写层(000_Inbox / 003_Ideas / 004_Permanent …)')
|
|
255
|
+
print(' 或在库根目录新建一篇 .md,写几句话再来')
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
if not runnable:
|
|
259
|
+
message = '没有要处理的笔记(%d 篇空或太短)' % len(empty) if notes else '你的笔记层还是空的'
|
|
260
|
+
print(json.dumps({'success': False, 'error': message}, ensure_ascii=False)
|
|
261
|
+
if args.json else message, file=sys.stdout if args.json else sys.stderr)
|
|
262
|
+
return 1
|
|
263
|
+
|
|
264
|
+
if not args.yes:
|
|
265
|
+
print('会把 %d 篇你自己的笔记发给 DeepSeek(每篇一次调用)。加 --yes 确认。' % len(runnable),
|
|
266
|
+
file=sys.stderr)
|
|
267
|
+
return 1
|
|
268
|
+
|
|
269
|
+
config = load_config()
|
|
270
|
+
api_key = get_api_key(config)
|
|
271
|
+
if not api_key:
|
|
272
|
+
print('没有配置 deepseekApiKey(weflow-cli config set deepseekApiKey "...")', file=sys.stderr)
|
|
273
|
+
return 1
|
|
274
|
+
|
|
275
|
+
updated = datetime.now(TZ).strftime('%Y-%m-%d %H:%M')
|
|
276
|
+
written, failed = [], []
|
|
277
|
+
for note in runnable:
|
|
278
|
+
prompt = NOTE_PROMPT.format(title=note['title'], body=note['body'][:MAX_NOTE_CHARS])
|
|
279
|
+
try:
|
|
280
|
+
raw = call_deepseek(prompt, api_key, max_tokens=2000, timeout=120)
|
|
281
|
+
except Exception as error:
|
|
282
|
+
failed.append({'title': note['title'][:30], 'reason': '调用失败:%s' % error})
|
|
283
|
+
continue
|
|
284
|
+
parsed = parse_note(raw)
|
|
285
|
+
if parsed is None:
|
|
286
|
+
failed.append({'title': note['title'][:30], 'reason': '模型没返回可用 JSON',
|
|
287
|
+
'rawTail': str(raw)[-200:] if raw else '(空)'})
|
|
288
|
+
continue
|
|
289
|
+
concepts = merge_concepts(note['links'], parsed['mined'])
|
|
290
|
+
target = note_path(args.out, note['title'])
|
|
291
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
frontmatter, body = build_card(note, parsed['summary'], concepts, updated)
|
|
293
|
+
write_with_frontmatter(str(target), frontmatter, body)
|
|
294
|
+
written.append({'title': note['title'][:30], 'file': str(target),
|
|
295
|
+
'concepts': len(concepts),
|
|
296
|
+
'ownLinks': len(note['links']), 'from': note['rel']})
|
|
297
|
+
|
|
298
|
+
result = {'success': True, 'action': 'user-notes', 'model': 'DeepSeek(生成)',
|
|
299
|
+
'sendsNothing': True, 'modifiesYourNotes': False,
|
|
300
|
+
'written': written, 'failed': failed, 'empty': empty,
|
|
301
|
+
'alreadyCarded': len(existing), 'notes': len(notes), 'calls': len(runnable)}
|
|
302
|
+
if args.json:
|
|
303
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
304
|
+
else:
|
|
305
|
+
for item in written:
|
|
306
|
+
print('✓ %s(%d 个概念,其中你自己链的 %d 个)' % (item['title'], item['concepts'], item['ownLinks']))
|
|
307
|
+
for item in failed:
|
|
308
|
+
print('✗ %s:%s' % (item['title'], item['reason']), file=sys.stderr)
|
|
309
|
+
print('\n共 %d 张卡;接着跑 python scripts/compile_wiki.py --source %s'
|
|
310
|
+
% (len(written), args.out))
|
|
311
|
+
return exit_code(len(written))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == '__main__':
|
|
315
|
+
sys.exit(main())
|
package/scripts/vault_rag.py
CHANGED
|
@@ -16,7 +16,7 @@ DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
|
|
|
16
16
|
DEFAULT_BIZ = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
|
|
17
17
|
|
|
18
18
|
sys.path.insert(0, SCRIPTS_DIR)
|
|
19
|
-
from _utils import call_deepseek
|
|
19
|
+
from _utils import CONCEPT_DIRS, call_deepseek
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def collect_context(vault: str, biz_daily: str, question: str, top_k: int) -> list[dict]:
|
|
@@ -24,9 +24,12 @@ def collect_context(vault: str, biz_daily: str, question: str, top_k: int) -> li
|
|
|
24
24
|
results = []
|
|
25
25
|
search_terms = question.lower().split()
|
|
26
26
|
|
|
27
|
-
# 1.
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
# 1. 概念页(权重最高)——**两个目录都要读**:文章知识库与聊天知识库是分开的
|
|
28
|
+
# (`_utils.CONCEPT_DIRS`),只读一个的话分出去的那一半会静默地搜不到。
|
|
29
|
+
for relative in CONCEPT_DIRS:
|
|
30
|
+
concepts_dir = Path(vault) / relative
|
|
31
|
+
if not concepts_dir.is_dir():
|
|
32
|
+
continue
|
|
30
33
|
for md in concepts_dir.glob('*.md'):
|
|
31
34
|
try:
|
|
32
35
|
text = md.read_text(encoding='utf-8')[:2000]
|
|
@@ -36,9 +39,12 @@ def collect_context(vault: str, biz_daily: str, question: str, top_k: int) -> li
|
|
|
36
39
|
if score > 0:
|
|
37
40
|
results.append({'source': 'concept', 'title': md.stem, 'content': text, 'score': score})
|
|
38
41
|
|
|
39
|
-
# 2. 阅读笔记
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
# 2. 阅读笔记 —— **`Notes/` 这个目录在本仓库里从来不存**(2026-09-27 实测:
|
|
43
|
+
# Vault 里没有它),所以这一段一直搜的是空气。笔记实际住在 `002_Literature`。
|
|
44
|
+
for relative in ('002_Literature', '003_Ideas', '008_MOC'):
|
|
45
|
+
notes_dir = Path(vault) / relative
|
|
46
|
+
if not notes_dir.is_dir():
|
|
47
|
+
continue
|
|
42
48
|
for md in notes_dir.rglob('*.md'):
|
|
43
49
|
try:
|
|
44
50
|
text = md.read_text(encoding='utf-8')[:1500]
|
package/scripts/vault_search.py
CHANGED
|
@@ -12,6 +12,8 @@ from pathlib import Path
|
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
13
|
|
|
14
14
|
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
16
|
+
from _utils import CONCEPT_DIRS # noqa: E402
|
|
15
17
|
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
|
|
16
18
|
DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
|
|
17
19
|
DEFAULT_BIZ = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
|
|
@@ -79,9 +81,14 @@ def collect_files(vault: str, biz_daily: str, search_type: str, days: int) -> li
|
|
|
79
81
|
files.append((md, 'article', fm))
|
|
80
82
|
|
|
81
83
|
if search_type in ('all', 'concept'):
|
|
84
|
+
# **两个目录都要读**(`_utils.CONCEPT_DIRS`):文章知识库与聊天知识库是分开的
|
|
85
|
+
# (用户 2026-09-27 明确要求),只读一个的话,分出去的那一半会**静默地搜不到**
|
|
86
|
+
# ——命令照样返回结果,只是少了一半。
|
|
82
87
|
vault_path = Path(vault)
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
for relative in CONCEPT_DIRS:
|
|
89
|
+
concepts_dir = vault_path / relative
|
|
90
|
+
if not concepts_dir.is_dir():
|
|
91
|
+
continue
|
|
85
92
|
for md in concepts_dir.glob('*.md'):
|
|
86
93
|
try:
|
|
87
94
|
fm = parse_fm(md.read_text(encoding='utf-8'))
|
|
@@ -90,10 +97,20 @@ def collect_files(vault: str, biz_daily: str, search_type: str, days: int) -> li
|
|
|
90
97
|
files.append((md, 'concept', fm))
|
|
91
98
|
|
|
92
99
|
if search_type in ('all', 'note'):
|
|
100
|
+
# **`Notes/` 这个目录在本仓库里从来不存。** 2026-09-27 实测:Vault 里没有它,
|
|
101
|
+
# 于是 `--type note` 一直搜的是空气——而它本该覆盖的阅读笔记有 25,676 篇,
|
|
102
|
+
# 是最大的一层。笔记实际住在下面这几个目录里。
|
|
93
103
|
vault_path = Path(vault)
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
# `Sources/Chat` 是聊天知识卡(`chat_notes --vault-copy` 放的)。它跟
|
|
105
|
+
# `Sources/WeChat`(两万六千篇原始文章,走 biz-daily 那条路)不是一回事:
|
|
106
|
+
# 这里面是一百多张**已经提炼过的**卡——会话里聊了什么、有哪些人、欠着什么。
|
|
107
|
+
for sub in ('002_Literature', '001_Daily', '003_Ideas', '008_MOC', 'Sources/Chat'):
|
|
108
|
+
notes_dir = vault_path / sub
|
|
109
|
+
if not notes_dir.is_dir():
|
|
110
|
+
continue
|
|
96
111
|
for md in notes_dir.rglob('*.md'):
|
|
112
|
+
if md.name == 'README.md':
|
|
113
|
+
continue
|
|
97
114
|
try:
|
|
98
115
|
fm = parse_fm(md.read_text(encoding='utf-8'))
|
|
99
116
|
except Exception:
|