weflow-cli 1.7.0 → 1.8.1
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 +2108 -2
- package/OPERATIONS.md +810 -1
- package/README.en.md +96 -10
- package/README.md +34 -9
- package/bin/weflow-cli.ts +937 -28
- package/dist/bin/weflow-cli.js +955 -28
- 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 +25 -6
- 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 +599 -75
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +217 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +1116 -59
- 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/services/wereadService.d.ts +33 -6
- package/dist/src/services/wereadService.d.ts.map +1 -1
- package/dist/src/services/wereadService.js +27 -2
- package/dist/src/services/wereadService.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/BRANCHES.md +13 -4
- 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 +210 -9
- 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 +483 -0
- package/resources/panel/mascot-happy.png +0 -0
- package/resources/panel/mascot-sorry.png +0 -0
- package/resources/panel/mascot-thinking.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 +415 -0
- package/resources/panel/preload.cjs +79 -0
- package/resources/panel/quick-menu.cjs +197 -0
- package/resources/panel/renderer.js +470 -0
- package/resources/panel/tray-menu.cjs +40 -0
- package/resources/panel/tray.png +0 -0
- package/scripts/_utils.py +425 -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 +1060 -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 +429 -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 +25 -6
- package/src/services/assistantRouter.ts +191 -0
- package/src/services/assistantService.ts +681 -74
- package/src/services/assistantTools.ts +1149 -58
- 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/services/wereadService.ts +43 -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,440 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Jev 对照实验:衡量"决策模型 vs 现有标签",而不是接线(D-031 已把判断接进日报)。
|
|
3
|
+
|
|
4
|
+
日报现在用 `scripts/jev_client.py` 判断主题与相关度。**这个脚本是那个决定的证据来源,
|
|
5
|
+
不是第二套实现**:契约、客户端、判据文本全部从 `jev_client` import,这里只负责取样、
|
|
6
|
+
对照和统计。抄一份过来会漂——事实上已经漂过一次:产品版把判据写成了裸的「低/中/高」,
|
|
7
|
+
而实测用的是带说明的版本(说明文字就是模型判档的依据),两边对齐之前,"测过的"
|
|
8
|
+
和"上线的"根本不是同一个东西。
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
|
|
12
|
+
# 只打印将要发送的内容,不联网、不需要 key
|
|
13
|
+
python scripts/jev_probe.py --dry-run
|
|
14
|
+
|
|
15
|
+
# 通路验证:3 个小样本,把原始 answers 原样打出来
|
|
16
|
+
python scripts/jev_probe.py --smoke --yes
|
|
17
|
+
|
|
18
|
+
# 对照实验:真实文章 vs 文件里记的标签
|
|
19
|
+
python scripts/jev_probe.py --against-daily --limit 60 --yes
|
|
20
|
+
|
|
21
|
+
**它给出的是一致率,不是准确率**:比对基准是 DeepSeek 自己写进 frontmatter 的
|
|
22
|
+
`topic`,两边完全可能一起错。要准确率得人工定标——这是仍然欠着的一步。
|
|
23
|
+
|
|
24
|
+
**数据出境**:--against-daily 会把文章正文发给 api.typesafe.ai,所以要求显式
|
|
25
|
+
`--yes`,且默认只发正文前 N 字符。
|
|
26
|
+
"""
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import urllib.error
|
|
32
|
+
import urllib.request
|
|
33
|
+
|
|
34
|
+
# Windows 控制台默认 GBK,中文日报正文里有下标之类 GBK 编不出的字符,直接
|
|
35
|
+
# print 会 UnicodeEncodeError 崩掉。改成 UTF-8 + replace,让脚本在任何 codepage
|
|
36
|
+
# 下都不会因为“打印”这件事失败。
|
|
37
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
38
|
+
try:
|
|
39
|
+
_stream.reconfigure(encoding='utf-8', errors='replace')
|
|
40
|
+
except (AttributeError, ValueError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# 契约与客户端只有一份实现(scripts/jev_client.py,产品代码用的就是它)。
|
|
44
|
+
# 这里原先抄了一份,抄本的判据文本和产品版一旦漂开,"测过的"和"上线的"就不是同一个东西。
|
|
45
|
+
from _utils import TOPICS # noqa: E402
|
|
46
|
+
from jev_client import ( # noqa: E402 (scripts/ 已由 main 插进 sys.path)
|
|
47
|
+
DEFAULT_MODEL,
|
|
48
|
+
ENDPOINT,
|
|
49
|
+
INPUT_USD_PER_TOKEN,
|
|
50
|
+
JevError,
|
|
51
|
+
TOPIC_CRITERIA,
|
|
52
|
+
build_questions,
|
|
53
|
+
create_client,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# 取样词表也从 _utils 来:分类法只有一份定义(曾经在五个文件里各有一份,其中
|
|
57
|
+
# 提示词里那份还漂成了五类而 TOPICS 是六类)。
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def split_frontmatter(text):
|
|
62
|
+
"""把 md 的 YAML frontmatter 和正文分开。
|
|
63
|
+
|
|
64
|
+
**这一步是实验成立的前提,不是整洁问题**:日报产物在 frontmatter 里已经写着
|
|
65
|
+
`topic: AI` / `relevance: 中` / `tags: [AI]`,那是 DeepSeek 自己的答案。连它
|
|
66
|
+
一起发过去,模型只要照着抄就"一致"了,一致率会漂亮地指向一个假的结论。
|
|
67
|
+
"""
|
|
68
|
+
meta = {}
|
|
69
|
+
body = text
|
|
70
|
+
if text.startswith('---'):
|
|
71
|
+
end = text.find('\n---', 3)
|
|
72
|
+
if end != -1:
|
|
73
|
+
for line in text[3:end].splitlines():
|
|
74
|
+
if ':' in line:
|
|
75
|
+
key, _, value = line.partition(':')
|
|
76
|
+
meta[key.strip()] = value.strip().strip('"')
|
|
77
|
+
body = text[end + 4:]
|
|
78
|
+
return meta, body.strip()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def collect_samples(limit, max_chars):
|
|
82
|
+
"""从日报产物里取真实文章,**按主题分层轮取**。
|
|
83
|
+
|
|
84
|
+
直接按目录顺序切前 N 篇会严重偏样:日报是逐日生成的,`days/topics` 的字典序
|
|
85
|
+
决定了前面几十篇几乎全来自第一天和第一个主题(实际就是 AI)。那样算出来的
|
|
86
|
+
一致率和置信度分桶,测的是"AI 类文章",不是"中文文章"。
|
|
87
|
+
|
|
88
|
+
所以:先全量读进来按标签主题分组,再轮转取,保证每个主题都被覆盖到。
|
|
89
|
+
|
|
90
|
+
标签取 frontmatter(写进文件的那份)而不是目录名——两者理论上一致、实际不一定,
|
|
91
|
+
不一致的会被单独数出来。
|
|
92
|
+
"""
|
|
93
|
+
root = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
94
|
+
'output', 'biz-daily')
|
|
95
|
+
if not os.path.isdir(root):
|
|
96
|
+
raise JevError(f'找不到日报产物目录: {root}')
|
|
97
|
+
|
|
98
|
+
buckets = {topic: [] for topic in TOPICS}
|
|
99
|
+
for day in sorted(os.listdir(root)):
|
|
100
|
+
day_dir = os.path.join(root, day)
|
|
101
|
+
if not os.path.isdir(day_dir):
|
|
102
|
+
continue
|
|
103
|
+
for topic in TOPICS:
|
|
104
|
+
topic_dir = os.path.join(day_dir, topic)
|
|
105
|
+
if not os.path.isdir(topic_dir):
|
|
106
|
+
continue
|
|
107
|
+
for name in sorted(os.listdir(topic_dir)):
|
|
108
|
+
if not name.endswith('.md'):
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
with open(os.path.join(topic_dir, name), encoding='utf-8') as handle:
|
|
112
|
+
raw = handle.read()
|
|
113
|
+
except OSError:
|
|
114
|
+
continue
|
|
115
|
+
meta, body = split_frontmatter(raw)
|
|
116
|
+
# 只收真文章:`行动建议.md` 这类报告产物也躺在主题目录里、也带
|
|
117
|
+
# topic 字段,但它是前一阶段的输出,拿去"分类对照"是自问自答
|
|
118
|
+
# (第一次跑就混进来 2 条)。真文章都有 url。
|
|
119
|
+
if not meta.get('url'):
|
|
120
|
+
continue
|
|
121
|
+
label = meta.get('topic') or topic
|
|
122
|
+
buckets.setdefault(label, []).append({
|
|
123
|
+
'day': day,
|
|
124
|
+
'title': meta.get('title') or name[:-3],
|
|
125
|
+
'body': body[:max_chars],
|
|
126
|
+
'labelTopic': label,
|
|
127
|
+
'labelRelevance': meta.get('relevance') or '',
|
|
128
|
+
'dirTopic': topic,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# 轮转:每轮从每个主题各取一篇,直到取够或取空。
|
|
132
|
+
samples = []
|
|
133
|
+
index = 0
|
|
134
|
+
while len(samples) < limit:
|
|
135
|
+
took_any = False
|
|
136
|
+
for topic in sorted(buckets):
|
|
137
|
+
bucket = buckets[topic]
|
|
138
|
+
if index < len(bucket):
|
|
139
|
+
samples.append(bucket[index])
|
|
140
|
+
took_any = True
|
|
141
|
+
if len(samples) >= limit:
|
|
142
|
+
break
|
|
143
|
+
if not took_any:
|
|
144
|
+
break
|
|
145
|
+
index += 1
|
|
146
|
+
return samples
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def strate(title, body):
|
|
150
|
+
"""state 只放生产环境真会给模型的东西:标题 + 正文。
|
|
151
|
+
|
|
152
|
+
日期、目录名、frontmatter 里的一切都不放(都是有答案味道的东西)。
|
|
153
|
+
"""
|
|
154
|
+
return f'标题:{title}\n\n正文:\n{body}'
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cmd_dry_run(samples, max_chars):
|
|
158
|
+
questions = build_questions(TOPICS, TOPIC_CRITERIA)
|
|
159
|
+
print(f'将要发送 {len(samples)} 篇文章到 {ENDPOINT}')
|
|
160
|
+
print(f'questions 形状:{json.dumps(questions, ensure_ascii=False)[:300]}...')
|
|
161
|
+
print(f'state(每篇正文截断到 {max_chars} 字,不含 frontmatter):\n')
|
|
162
|
+
for sample in samples[:3]:
|
|
163
|
+
state = strate(sample['title'], sample['body'])
|
|
164
|
+
print(' ' + '-' * 66)
|
|
165
|
+
for line in state[:400].splitlines():
|
|
166
|
+
print(' | ' + line)
|
|
167
|
+
print(f' |(标签只在本地用于比对:topic={sample["labelTopic"]} '
|
|
168
|
+
f'relevance={sample["labelRelevance"]},不发出去)')
|
|
169
|
+
total = sum(len(strate(s['title'], s['body'])) for s in samples)
|
|
170
|
+
print(f'\n合计约 {total} 字符。正文出境,所以真正发送要加 --yes。')
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_smoke(client):
|
|
174
|
+
"""三个小样本,把原始返回打出来 —— 目的是确认契约,不是确认质量。"""
|
|
175
|
+
print(f'model 别名:{client.models()}')
|
|
176
|
+
cases = [
|
|
177
|
+
('英文 string + noul', 'The support agent issued a full refund of $49 to the customer.',
|
|
178
|
+
{'is_refund': {'type': 'noul', 'instructions': 'Was a refund issued?'}}),
|
|
179
|
+
('中文 string + noul/choice/score',
|
|
180
|
+
'客户在微信里说:发票 4411 被重复扣款了,三天没人回复,请今天退款。',
|
|
181
|
+
{'wants_refund': {'type': 'noul', 'instructions': '对方明确要求退款吗?'},
|
|
182
|
+
'urgency': {'type': 'score', 'instructions': '紧急程度?',
|
|
183
|
+
'criteria': ['不急', '一般', '紧急', '十万火急']},
|
|
184
|
+
'dept': {'type': 'choice', 'instructions': '该转给哪个团队?',
|
|
185
|
+
'criteria': {'billing': '账单/退款/付款问题',
|
|
186
|
+
'tech': '程序报错/崩溃'}}}),
|
|
187
|
+
('中文 dict 形式的 state(试探 state 能否传对象)',
|
|
188
|
+
{'标题': '大气污染模拟新方法', '正文': '本文提出一种基于神经网络的大气污染模拟降尺度方法。'},
|
|
189
|
+
{'is_paper': {'type': 'noul', 'instructions': '这是一篇科研论文吗?'},
|
|
190
|
+
'topic': {'type': 'choice', 'instructions': '主题?',
|
|
191
|
+
'criteria': TOPIC_CRITERIA}}),
|
|
192
|
+
]
|
|
193
|
+
total_in = 0
|
|
194
|
+
for label, state, questions in cases:
|
|
195
|
+
print(f'\n### {label}')
|
|
196
|
+
try:
|
|
197
|
+
answers, usage = client.decide(state, questions)
|
|
198
|
+
except JevError as error:
|
|
199
|
+
print(f' 失败:{error}')
|
|
200
|
+
continue
|
|
201
|
+
total_in += usage.get('input_tokens', 0)
|
|
202
|
+
print(' answers :', json.dumps(answers, ensure_ascii=False))
|
|
203
|
+
print(' usage :', usage,
|
|
204
|
+
f"-> ${usage.get('input_tokens', 0) * INPUT_USD_PER_TOKEN:.6f}")
|
|
205
|
+
print(f'\n合计约 ${total_in * INPUT_USD_PER_TOKEN:.6f}')
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_against_daily(samples, client):
|
|
209
|
+
"""对照:Jev 的主题 vs 文件里记的 DeepSeek 主题。
|
|
210
|
+
|
|
211
|
+
**这是「一致率」,不是「准确率」**:标签来自 DeepSeek 自己的输出,两边完全
|
|
212
|
+
可能一起错。要真的拿到准确率,得人抽一批做金标准——那是下一步,不是这一步。
|
|
213
|
+
"""
|
|
214
|
+
questions = build_questions(TOPICS, TOPIC_CRITERIA)
|
|
215
|
+
agree = 0
|
|
216
|
+
done = 0
|
|
217
|
+
total_in = 0
|
|
218
|
+
rows = []
|
|
219
|
+
dirty_labels = [s for s in samples if s['labelTopic'] != s['dirTopic']]
|
|
220
|
+
for index, sample in enumerate(samples, 1):
|
|
221
|
+
state = strate(sample['title'], sample['body'])
|
|
222
|
+
try:
|
|
223
|
+
answers, usage = client.decide(state, questions)
|
|
224
|
+
except JevError as error:
|
|
225
|
+
print(f'[{index}] 失败:{error}')
|
|
226
|
+
break
|
|
227
|
+
total_in += usage.get('input_tokens', 0)
|
|
228
|
+
topic = answers.get('topic', {})
|
|
229
|
+
relevance = answers.get('relevance', {})
|
|
230
|
+
picked = topic.get('choice')
|
|
231
|
+
same = picked == sample['labelTopic']
|
|
232
|
+
agree += 1 if same else 0
|
|
233
|
+
done += 1
|
|
234
|
+
rows.append({'day': sample['day'], 'title': sample['title'][:40],
|
|
235
|
+
'label': sample['labelTopic'], 'jev': picked, 'same': same,
|
|
236
|
+
'conf': topic.get('confidence'),
|
|
237
|
+
'probs': topic.get('probabilities'),
|
|
238
|
+
'relevance': relevance.get('score'),
|
|
239
|
+
'relLabel': sample['labelRelevance'],
|
|
240
|
+
'relConf': relevance.get('confidence'),
|
|
241
|
+
'isPaper': answers.get('is_research_paper', {}).get('noul')})
|
|
242
|
+
print(f'[{index}/{len(samples)}] {sample["day"]} {sample["title"][:32]}')
|
|
243
|
+
print(f' DeepSeek={sample["labelTopic"]} Jev={picked} '
|
|
244
|
+
f'conf={topic.get("confidence")} 一致={same} '
|
|
245
|
+
f'相关度 Jev={relevance.get("score")} / 标签={sample["labelRelevance"]}')
|
|
246
|
+
|
|
247
|
+
if done:
|
|
248
|
+
print(f'\n一致率 {agree}/{done} = {agree / done:.1%}(不是准确率,见函数说明)')
|
|
249
|
+
if dirty_labels:
|
|
250
|
+
print(f'注意:有 {len(dirty_labels)} 篇的 frontmatter 主题与目录名不一致,'
|
|
251
|
+
f'比对以 frontmatter 为准。')
|
|
252
|
+
print(f'输入 token 合计 {total_in},约 ${total_in * INPUT_USD_PER_TOKEN:.6f}')
|
|
253
|
+
|
|
254
|
+
# 一致度与置信度是否相关,是「概率能不能拿来定阈值」的唯一直接证据。
|
|
255
|
+
with_conf = [r for r in rows if isinstance(r['conf'], (int, float))]
|
|
256
|
+
if with_conf:
|
|
257
|
+
for threshold in (0.8, 0.6):
|
|
258
|
+
hi = [r for r in with_conf if r['conf'] >= threshold]
|
|
259
|
+
lo = [r for r in with_conf if r['conf'] < threshold]
|
|
260
|
+
hi_rate = sum(r['same'] for r in hi) / len(hi) if hi else 0
|
|
261
|
+
lo_rate = sum(r['same'] for r in lo) / len(lo) if lo else 0
|
|
262
|
+
print(f' 阈值 {threshold}: 高置信 {len(hi)} 条一致率 {hi_rate:.1%} / '
|
|
263
|
+
f'低置信 {len(lo)} 条一致率 {lo_rate:.1%}')
|
|
264
|
+
print(' (高置信组明显更准,才说明概率有区分度、阈值能定;两组接近则说明'
|
|
265
|
+
'概率没有信息量,只当分类器用就好)')
|
|
266
|
+
return rows
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---- 判据变体:英文 + 具体情景 --------------------------------------------------
|
|
270
|
+
#
|
|
271
|
+
# 来自 `jev-chat-jarvis`(TypeSafe 官方那个聊天副驾)的两条口径,我们现在的版本两条都
|
|
272
|
+
# 反着来:
|
|
273
|
+
# 1. **instructions 与 criteria 用英文**(Jev 主训练语言是英文;state 里的正文保持中文
|
|
274
|
+
# 原文不动)—— 他们的 `tools/jev/TASK.md` 把这写成了硬约束;
|
|
275
|
+
# 2. **score 的每一档写具体情景,不写抽象程度**(官方明确要求)—— 我们写的是
|
|
276
|
+
# `低:信息性阅读` 这种抽象标签。
|
|
277
|
+
#
|
|
278
|
+
# 键保持中文不动:`choice` 的键就是我们回填的值,下游按字面量比对。
|
|
279
|
+
#
|
|
280
|
+
# **这是候选判据,不是生产判据。** 换不换由 `--criteria-ab` 的数字决定,而不是由
|
|
281
|
+
# "官方这么做"决定——本机实测(24 篇真实文章):主题 5/24 会变,相关度原始分平均只差
|
|
282
|
+
# 0.17。改了什么能量出来,改好没有需要一个标注集,那是另一件事。
|
|
283
|
+
EN_TOPIC_CRITERIA = {
|
|
284
|
+
'AI': 'AI, large language models, agents, programming, open-source software, developer '
|
|
285
|
+
'tooling, and hands-on tutorials for them. Not for a research paper whose main '
|
|
286
|
+
'content is a method or a result rather than a tool.',
|
|
287
|
+
'投资': 'Stocks, funds, fundraising, macro-economics, business and market analysis, '
|
|
288
|
+
'company financials. Not for an economic-policy announcement by a government '
|
|
289
|
+
'body - that is 政治, or 新闻 if it is merely reported.',
|
|
290
|
+
'新闻': 'Current affairs, social hot topics, entertainment, job postings, promotions and '
|
|
291
|
+
'event notices. Informative but not actionable. Not for an official policy '
|
|
292
|
+
'interpretation (政治) or an investment analysis (投资).',
|
|
293
|
+
'文学': 'Essays, fiction, food and travel writing, everyday-life reflection, history and '
|
|
294
|
+
'culture. The tone is literary or personal rather than informational.',
|
|
295
|
+
'学术': 'Peer-reviewed papers, journal articles, lab results, academic conferences, '
|
|
296
|
+
'university research and scientific methodology. Not for a popular write-up of a '
|
|
297
|
+
'product or a tool (AI).',
|
|
298
|
+
'政治': 'Party theory, policy interpretation, speeches by officials, and official '
|
|
299
|
+
'commentary published by a government or party organ. Not for general social '
|
|
300
|
+
'news that merely mentions a policy.',
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# 每一档写**具体情景**。抽象程度("低/中/高")在这里不算判据:模型没法把"低"对到一篇文章上。
|
|
304
|
+
EN_RELEVANCE_LEVELS = [
|
|
305
|
+
'Nothing usable: pure news, entertainment, literary or lifestyle writing, or an event '
|
|
306
|
+
'notice. A reader who acts on it learns nothing they could apply.',
|
|
307
|
+
'Background value: explains a trend, an idea or a cross-domain connection worth knowing, '
|
|
308
|
+
'but hands the reader nothing to run, try or cite today.',
|
|
309
|
+
'Directly usable: names a new tool, method, dataset, code repository, or a reproducible '
|
|
310
|
+
'result that this reader could pick up and use in their own work.',
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def build_variant_questions(topics):
|
|
315
|
+
"""与 `jev_client.build_questions` 同一形状,只换判据语言与写法。"""
|
|
316
|
+
return {
|
|
317
|
+
'topic': {'type': 'choice',
|
|
318
|
+
'instructions': 'Which single topic does this article belong to? '
|
|
319
|
+
'Choose by what the article is mainly about, not by a '
|
|
320
|
+
'keyword it happens to mention.',
|
|
321
|
+
'criteria': {topic: EN_TOPIC_CRITERIA.get(topic, topic) for topic in topics}},
|
|
322
|
+
'relevance': {'type': 'score',
|
|
323
|
+
'instructions': 'How useful is this article to a reader who is a graduate '
|
|
324
|
+
'student in environmental science working at the '
|
|
325
|
+
'intersection of computing and the environment? Judge what '
|
|
326
|
+
'they could do with it, not how pleasant it is to read.',
|
|
327
|
+
'criteria': list(EN_RELEVANCE_LEVELS)},
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_criteria_ab(samples, client, max_chars):
|
|
332
|
+
"""同一批文章问两遍:现行判据(中文抽象) vs 变体(英文具体情景)。
|
|
333
|
+
|
|
334
|
+
**只回答"改了什么",不回答"改好没有"**:两个版本两个都可能错,而"更准"需要标注集。
|
|
335
|
+
能诚实给出的是:主题变了多少篇、相关度原始分差多少、分歧落在哪几篇——那几篇就是要
|
|
336
|
+
人工去看的那几篇。
|
|
337
|
+
"""
|
|
338
|
+
rows = []
|
|
339
|
+
for sample in samples:
|
|
340
|
+
state = strate(sample['title'], sample['body'])
|
|
341
|
+
try:
|
|
342
|
+
current, _ = client.decide(state, build_questions(TOPICS))
|
|
343
|
+
variant, _ = client.decide(state, build_variant_questions(TOPICS))
|
|
344
|
+
except JevError as error:
|
|
345
|
+
print(' [WARN] %s 失败:%s' % (sample['title'][:20], error))
|
|
346
|
+
continue
|
|
347
|
+
rows.append({
|
|
348
|
+
'title': sample['title'],
|
|
349
|
+
'storedTopic': sample['labelTopic'],
|
|
350
|
+
'currentTopic': (current.get('topic') or {}).get('choice'),
|
|
351
|
+
'variantTopic': (variant.get('topic') or {}).get('choice'),
|
|
352
|
+
'currentScore': (current.get('relevance') or {}).get('score'),
|
|
353
|
+
'variantScore': (variant.get('relevance') or {}).get('score'),
|
|
354
|
+
'currentConfidence': (current.get('topic') or {}).get('confidence'),
|
|
355
|
+
'variantConfidence': (variant.get('topic') or {}).get('confidence'),
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
same = sum(1 for r in rows if r['currentTopic'] == r['variantTopic'])
|
|
359
|
+
print()
|
|
360
|
+
print('=== 主题:两版一致 %d/%d(%.0f%%)==='
|
|
361
|
+
% (same, len(rows), 100.0 * same / max(len(rows), 1)))
|
|
362
|
+
for row in rows:
|
|
363
|
+
if row['currentTopic'] != row['variantTopic']:
|
|
364
|
+
print(' 存档=%-4s 现行=%-4s 变体=%-4s %s'
|
|
365
|
+
% (row['storedTopic'], row['currentTopic'], row['variantTopic'],
|
|
366
|
+
row['title'][:36]))
|
|
367
|
+
diffs = [abs((r['currentScore'] or 0) - (r['variantScore'] or 0)) for r in rows]
|
|
368
|
+
if diffs:
|
|
369
|
+
print()
|
|
370
|
+
print('=== 相关度原始分(0~2)===')
|
|
371
|
+
print(' 平均绝对差 %.2f · 差 >=0.5 的 %d/%d'
|
|
372
|
+
% (sum(diffs) / len(diffs), sum(1 for d in diffs if d >= 0.5), len(diffs)))
|
|
373
|
+
print()
|
|
374
|
+
print('这两个数说的是"改动会改变什么"。要判断哪个更对,得先有人工标注。')
|
|
375
|
+
return rows
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def main():
|
|
379
|
+
parser = argparse.ArgumentParser(description='Jev 中文对照探针(不接线)')
|
|
380
|
+
parser.add_argument('--api-key', default='', help='不传则读 TYPESAFE_API_KEY')
|
|
381
|
+
parser.add_argument('--model', default=DEFAULT_MODEL)
|
|
382
|
+
parser.add_argument('--smoke', action='store_true', help='3 个小样本验证通路')
|
|
383
|
+
parser.add_argument('--against-daily', action='store_true', help='用日报文章做对照')
|
|
384
|
+
parser.add_argument('--criteria-ab', action='store_true',
|
|
385
|
+
help='同一批文章问两遍:现行判据 vs 英文具体情景变体(只报"改了什么")')
|
|
386
|
+
parser.add_argument('--dry-run', action='store_true', help='只打印要发什么,不联网')
|
|
387
|
+
parser.add_argument('--limit', type=int, default=40, help='对照取多少篇')
|
|
388
|
+
parser.add_argument('--max-chars', type=int, default=2000, help='每篇正文截断长度')
|
|
389
|
+
parser.add_argument('--yes', action='store_true', help='确认把这些正文发到第三方')
|
|
390
|
+
parser.add_argument('--json', action='store_true', help='对照结果按 JSON 输出')
|
|
391
|
+
args = parser.parse_args()
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
samples = collect_samples(args.limit, args.max_chars) if (
|
|
395
|
+
args.dry_run or args.against_daily or args.criteria_ab) else []
|
|
396
|
+
|
|
397
|
+
if args.dry_run:
|
|
398
|
+
if not samples:
|
|
399
|
+
print('没有取到样本')
|
|
400
|
+
return 1
|
|
401
|
+
cmd_dry_run(samples, args.max_chars)
|
|
402
|
+
return 0
|
|
403
|
+
|
|
404
|
+
if not (args.smoke or args.against_daily or args.criteria_ab):
|
|
405
|
+
parser.print_help()
|
|
406
|
+
return 1
|
|
407
|
+
if not args.yes:
|
|
408
|
+
print('这会把你本机的文章正文发到 api.typesafe.ai。确认无误后加 --yes,\n'
|
|
409
|
+
'或先跑 --dry-run 看清楚要发什么。')
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
client = create_client(args.api_key, model=args.model)
|
|
413
|
+
if client is None:
|
|
414
|
+
raise JevError(
|
|
415
|
+
'缺少 TypeSafe key。用 --api-key,或 environment TYPESAFE_API_KEY,'
|
|
416
|
+
'或 weflow-cli config set typesafeApiKey')
|
|
417
|
+
if args.smoke:
|
|
418
|
+
cmd_smoke(client)
|
|
419
|
+
elif args.criteria_ab:
|
|
420
|
+
if not samples:
|
|
421
|
+
print('没有取到样本')
|
|
422
|
+
return 1
|
|
423
|
+
rows = cmd_criteria_ab(samples, client, args.max_chars)
|
|
424
|
+
if args.json:
|
|
425
|
+
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
426
|
+
else:
|
|
427
|
+
if not samples:
|
|
428
|
+
print('没有取到样本')
|
|
429
|
+
return 1
|
|
430
|
+
rows = cmd_against_daily(samples, client)
|
|
431
|
+
if args.json:
|
|
432
|
+
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
433
|
+
return 0
|
|
434
|
+
except JevError as error:
|
|
435
|
+
print(f'错误:{error}', file=sys.stderr)
|
|
436
|
+
return 2
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
if __name__ == '__main__':
|
|
440
|
+
sys.exit(main())
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""NT 读取的两个入口共用的东西:分片发现、密钥派生、列探测。
|
|
2
|
+
|
|
3
|
+
为什么要单独一个模块:`nt_decrypt.py`(messages / sessions / CLI 那条路)和
|
|
4
|
+
`export_chat_html.py`(导出那条路)**各自实现过同名函数,而它们并不相同**。
|
|
5
|
+
差异不会报错,只在边界处让两条路给出不同结果:
|
|
6
|
+
|
|
7
|
+
* `discover_message_shards` 的**契约**不同——`nt_decrypt` 版在 glob 不到分片时返回
|
|
8
|
+
`[配置的那个库]`(永不返回空),导出器版返回空表、由调用点
|
|
9
|
+
`if not shards: shards = [db_path]` 自己补。补偿写在调用点意味着**任何新调用方
|
|
10
|
+
忘了它就会静默拿到零个分片**。
|
|
11
|
+
* `derive_database_key` 两份的代码逐字相同,只有 docstring 不同——纯重复。
|
|
12
|
+
* `table_columns` 也逐字相同(两份都是我加的)。
|
|
13
|
+
|
|
14
|
+
所以这里只留一份实现,**契约取更安全的那一版**(下面的 docstring 各自写明)。
|
|
15
|
+
导入本模块不需要 sqlcipher3:这几个函数都不连数据库。
|
|
16
|
+
"""
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# 与 message_*.db 同目录、但不是消息分片的派生库。
|
|
20
|
+
SHARD_EXCLUDED = {'message_fts.db', 'message_resource.db'}
|
|
21
|
+
|
|
22
|
+
# 消息行的"锚点"列:任意一列在场,一行才谈得上身份与顺序。
|
|
23
|
+
#
|
|
24
|
+
# **两处语义,顺序只对其中一处有意义**:
|
|
25
|
+
# * 身份/模式检查——`nt_decrypt` 用它判断"这个分片是不是根本没有消息表该有的列",
|
|
26
|
+
# 这一处是集合语义,顺序无关。
|
|
27
|
+
# * `ORDER BY`——两个读取入口都按这个顺序排:`create_time` 优先。这一处顺序**就是
|
|
28
|
+
# 行为**:把 `local_id` 排到前面会让会话顺序变成另一回事,而且没有任何报错。
|
|
29
|
+
#
|
|
30
|
+
# 收在这里是因为它原本有三份:`nt_decrypt` 一份具名、同文件里 `ORDER BY` 又写了一份
|
|
31
|
+
# 字面量、`export_chat_html` 再写一份。三份一致时看不出问题,谁改了一处的顺序,
|
|
32
|
+
# 另一处就静默按别的顺序排消息——正是 `nt_common` 存在的理由那类分叉。
|
|
33
|
+
MESSAGE_ANCHOR_COLUMNS = ('create_time', 'local_id', 'server_id')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def discover_message_shards(db_path):
|
|
37
|
+
"""配置库旁边的每一个 NT 消息分片。
|
|
38
|
+
|
|
39
|
+
**永不返回空表**:glob 不到任何分片时返回 `[db_path]` 本身。调用方不必自己
|
|
40
|
+
兜底——两份实现里原本只有一份保证这一点,而导出器是在调用点补的。
|
|
41
|
+
"""
|
|
42
|
+
path = Path(db_path)
|
|
43
|
+
if not path.parent.is_dir():
|
|
44
|
+
return [str(path)]
|
|
45
|
+
shards = sorted(str(p) for p in path.parent.glob('message_*.db')
|
|
46
|
+
if p.name.lower() not in SHARD_EXCLUDED)
|
|
47
|
+
return shards or [str(path)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def derive_database_key(path, fallback_key, fallback_salt, passphrase=''):
|
|
51
|
+
"""每分片的 SQLCipher 密钥(微信 4.1.12.26+)。
|
|
52
|
+
|
|
53
|
+
共享的 passphrase 会跟**每个分片自己的** 16 字节文件头 salt 做一次
|
|
54
|
+
PBKDF2-HMAC-SHA512。没有 passphrase 时原样返回配置里那一对——老安装的
|
|
55
|
+
分片 0 就是用那个开的。
|
|
56
|
+
"""
|
|
57
|
+
import hashlib
|
|
58
|
+
if not passphrase:
|
|
59
|
+
return fallback_key, fallback_salt
|
|
60
|
+
try:
|
|
61
|
+
with open(path, 'rb') as fh:
|
|
62
|
+
salt = fh.read(16)
|
|
63
|
+
if len(salt) != 16:
|
|
64
|
+
return fallback_key, fallback_salt
|
|
65
|
+
raw_passphrase = bytes.fromhex(passphrase)
|
|
66
|
+
key = hashlib.pbkdf2_hmac('sha512', raw_passphrase, salt, 256000, 32).hex()
|
|
67
|
+
return key, salt.hex()
|
|
68
|
+
except (OSError, ValueError):
|
|
69
|
+
return fallback_key, fallback_salt
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def table_columns(cursor, table):
|
|
73
|
+
"""`table` 的列名,取不到就返回空集合。
|
|
74
|
+
|
|
75
|
+
消息表的列在不同微信版本之间会增删,所以读之前要问一句——直接按位置取字段的话,
|
|
76
|
+
少一列会让后面每个字段都错位,而且不报错。
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
rows = cursor.execute('PRAGMA table_info("%s")' % table).fetchall()
|
|
80
|
+
except Exception:
|
|
81
|
+
return set()
|
|
82
|
+
return {row[1] for row in rows}
|