weflow-cli 1.9.0 → 1.11.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 +62 -26
- package/CHANGELOG.md +1051 -0
- package/OPERATIONS.md +99 -13
- package/README.en.md +14 -5
- package/README.md +23 -8
- package/bin/weflow-cli.ts +364 -40
- package/dist/bin/weflow-cli.js +381 -69
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +19 -1
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +12 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +20 -3
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +35 -0
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +19 -0
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +20 -0
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +27 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +169 -35
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts +2 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/exportService.d.ts +43 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +116 -11
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +18 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +29 -0
- package/dist/src/services/messageQuery.js.map +1 -1
- package/dist/src/services/wechatClient.d.ts +125 -0
- package/dist/src/services/wechatClient.d.ts.map +1 -0
- package/dist/src/services/wechatClient.js +344 -0
- package/dist/src/services/wechatClient.js.map +1 -0
- package/dist/src/services/wechatMedia.d.ts +44 -0
- package/dist/src/services/wechatMedia.d.ts.map +1 -0
- package/dist/src/services/wechatMedia.js +155 -0
- package/dist/src/services/wechatMedia.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +11 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +55 -11
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.d.ts.map +1 -1
- package/docs/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1274 -0
- package/docs/EXTENDING.md +69 -7
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +27 -0
- package/docs/HEALTH-CHECK.md +12 -7
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
- package/docs/MCP.md +2 -2
- package/docs/PROJECT_STATE.md +64 -5
- package/docs/RELEASING.md +128 -1
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/logo.png +0 -0
- package/mcp-server/index.ts +18 -3
- package/package.json +5 -3
- package/resources/js/graph3d/NOTICE.txt +35 -0
- package/resources/js/graph3d/d3-binarytree.min.js +2 -0
- package/resources/js/graph3d/d3-dispatch.min.js +2 -0
- package/resources/js/graph3d/d3-force-3d.min.js +2 -0
- package/resources/js/graph3d/d3-octree.min.js +2 -0
- package/resources/js/graph3d/d3-quadtree.min.js +2 -0
- package/resources/js/graph3d/d3-timer.min.js +2 -0
- package/resources/js/graph3d/layout.mjs +63 -0
- package/resources/js/graph3d/three.min.js +6 -0
- package/resources/js/graph3d/viewer2d.js +448 -0
- package/resources/panel/index.html +12 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.png +0 -0
- package/resources/panel/mascot-lift-down-1.png +0 -0
- package/resources/panel/mascot-lift-down-2.png +0 -0
- package/resources/panel/mascot-lift-down-3.png +0 -0
- package/resources/panel/mascot-lift-down-4.png +0 -0
- package/resources/panel/mascot-lift-down-5.png +0 -0
- package/resources/panel/mascot-lift-down-6.png +0 -0
- package/resources/panel/mascot-lift-up-1.png +0 -0
- package/resources/panel/mascot-lift-up-2.png +0 -0
- package/resources/panel/mascot-lift-up-3.png +0 -0
- package/resources/panel/mascot-lift-up-4.png +0 -0
- package/resources/panel/mascot-lift-up-5.png +0 -0
- package/resources/panel/mascot-lift-up-6.png +0 -0
- package/resources/panel/mascot-tickle-1.png +0 -0
- package/resources/panel/mascot-tickle-2.png +0 -0
- package/resources/panel/mascot-tickle-3.png +0 -0
- package/resources/panel/mascot-tickle-4.png +0 -0
- package/resources/panel/panel.css +111 -0
- package/resources/panel/renderer.js +480 -8
- package/scripts/_utils.py +1203 -977
- package/scripts/biz_daily.py +25 -10
- package/scripts/chat_notes.py +15 -5
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +163 -57
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- package/scripts/export_chat_html.py +346 -6
- package/scripts/fav_notes.py +4 -1
- package/scripts/graph_2d.py +151 -0
- package/scripts/graph_3d.py +646 -0
- package/scripts/health_check.py +163 -26
- package/scripts/nt_decrypt.py +3 -3
- package/scripts/panel_frames.py +683 -0
- package/scripts/pipeline.py +21 -10
- package/scripts/quality_eval.py +5 -0
- package/scripts/rag_chat.py +33 -16
- package/scripts/reply_debt.py +26 -6
- package/scripts/semantic_search.py +212 -58
- package/scripts/vault_rag.py +42 -16
- package/scripts/vault_search.py +44 -8
- package/scripts/wechat_emoticon.py +6 -0
- package/scripts/wiki_lint.py +94 -17
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +33 -0
- package/src/services/assistantService.ts +26 -0
- package/src/services/assistantTools.ts +2452 -2319
- package/src/services/configService.ts +5 -2
- package/src/services/exportService.ts +129 -12
- package/src/services/messageQuery.ts +29 -0
- package/src/services/wechatClient.ts +380 -0
- package/src/services/wechatMedia.ts +166 -0
- package/src/services/wechatMessageService.ts +62 -11
- package/src/types.ts +4 -0
- package/dist/src/utils/wechatEmoji.d.ts +0 -5
- package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
- package/dist/src/utils/wechatEmoji.js +0 -35
- package/dist/src/utils/wechatEmoji.js.map +0 -1
package/scripts/pipeline.py
CHANGED
|
@@ -43,8 +43,11 @@ def main():
|
|
|
43
43
|
parser.add_argument('--interest', default='AI', help='兴趣主题(默认 AI)')
|
|
44
44
|
parser.add_argument('--wiki-limit', type=int, default=20, help='概念编译数量(默认 20)')
|
|
45
45
|
parser.add_argument('--skip-classify', action='store_true', help='跳过后处理')
|
|
46
|
-
parser.add_argument('--
|
|
47
|
-
|
|
46
|
+
parser.add_argument('--with-wiki', action='store_true',
|
|
47
|
+
help='把概念页写进 Obsidian Vault。**默认不做** —— Obsidian 自带的图谱\n'
|
|
48
|
+
'视图是实时的,写一次 vault 图谱就重画一次,所以只有你明确要它重建时才写')
|
|
49
|
+
parser.add_argument('--with-vault', action='store_true',
|
|
50
|
+
help='把当天的内容副本同步进 Obsidian Vault(Sources/WeChat/<日期>/)。**默认不做**')
|
|
48
51
|
parser.add_argument('--skip-html', action='store_true', help='跳过 HTML 生成')
|
|
49
52
|
parser.add_argument('--skip-ai-report', action='store_true', help='跳过 AI 深度阅读报告')
|
|
50
53
|
parser.add_argument('--no-ai', action='store_true', help='关闭所有 AI 调用,但保留抓取和本地输出')
|
|
@@ -52,7 +55,7 @@ def main():
|
|
|
52
55
|
help='biz_daily 只做判断不做生成(不调 LLM 写摘要/标签/简报,'
|
|
53
56
|
'主题与相关度仍由 Jev 判断),因此**不需要 DeepSeek key**。'
|
|
54
57
|
'注意:下游步骤(行动建议/概念编译/AI 报告)仍会用 LLM,'
|
|
55
|
-
'要全关请再加 --skip-classify --skip-
|
|
58
|
+
'要全关请再加 --skip-classify --skip-ai-report')
|
|
56
59
|
parser.add_argument('--ai-report-range', type=int, default=1, help='AI 报告覆盖最近 N 天(默认 1=仅当天)')
|
|
57
60
|
parser.add_argument('--source', action='append', default=[], metavar='NAME',
|
|
58
61
|
help='仅处理指定公众号,可重复或用逗号分隔')
|
|
@@ -71,10 +74,10 @@ def main():
|
|
|
71
74
|
#
|
|
72
75
|
# **只算真的会调 LLM 的那两步**(行动建议、概念编译):`generate_ai_report.py` 虽然
|
|
73
76
|
# 叫 AI 报告,但一次 LLM 都不调(它读已落盘的概率与判断),把它算进来会让
|
|
74
|
-
# `--no-summary --skip-classify
|
|
77
|
+
# `--no-summary --skip-classify` 仍然索要 DeepSeek key——那正是这条
|
|
75
78
|
# 路要摆脱的东西。
|
|
76
79
|
api_key = args.api_key or ''
|
|
77
|
-
downstream_ai = not
|
|
80
|
+
downstream_ai = (not args.skip_classify) or args.with_wiki
|
|
78
81
|
needs_key = args.engine in ('deepseek', 'claude') and not args.no_ai and (
|
|
79
82
|
not args.no_summary or downstream_ai)
|
|
80
83
|
if needs_key and not api_key:
|
|
@@ -104,7 +107,7 @@ def main():
|
|
|
104
107
|
print(' [提示] --no-summary 只关掉本步骤的 LLM 生成;下游仍会调 LLM 的步骤:'
|
|
105
108
|
'%s。要全关请补上对应 --skip-*'
|
|
106
109
|
% '、'.join(n for n, skipped in (('classify_daily(行动建议)', args.skip_classify),
|
|
107
|
-
('compile_wiki(概念编译)', args.
|
|
110
|
+
('compile_wiki(概念编译)', not args.with_wiki))
|
|
108
111
|
if not skipped))
|
|
109
112
|
if not run_step('biz_daily — 抓取+摘要', step1_args):
|
|
110
113
|
sys.exit(1)
|
|
@@ -121,8 +124,8 @@ def main():
|
|
|
121
124
|
if not run_step('classify_daily — 后处理', step2_args):
|
|
122
125
|
print('[WARN] classify_daily 失败,继续后续步骤')
|
|
123
126
|
|
|
124
|
-
# Step 3: Vault sync
|
|
125
|
-
if
|
|
127
|
+
# Step 3: Vault sync(**默认不做**,见 --help)
|
|
128
|
+
if args.with_vault:
|
|
126
129
|
date_str = args.date or time.strftime('%Y-%m-%d')
|
|
127
130
|
source_dir = os.path.join(SOURCE_ROOT, date_str)
|
|
128
131
|
vault_dir = os.path.join(os.path.dirname(SCRIPTS_DIR), 'output', 'wechat-vault',
|
|
@@ -134,14 +137,22 @@ def main():
|
|
|
134
137
|
file_count = sum(1 for _ in Path(vault_dir).rglob('*.md'))
|
|
135
138
|
print(f'\n Vault 同步: {file_count} 个文件 → {vault_dir}')
|
|
136
139
|
|
|
137
|
-
# Step 4: wiki compile
|
|
138
|
-
if
|
|
140
|
+
# Step 4: wiki compile(**默认不做**,见 --help)
|
|
141
|
+
if args.with_wiki and not args.no_ai:
|
|
139
142
|
step3_args = [
|
|
140
143
|
os.path.join(SCRIPTS_DIR, 'compile_wiki.py'),
|
|
141
144
|
'--limit', str(args.wiki_limit),
|
|
142
145
|
]
|
|
143
146
|
run_step('wiki compile — 概念编译', step3_args)
|
|
144
147
|
|
|
148
|
+
# 默认不写 Vault:**Obsidian 自带的图谱视图是实时的** —— 它盯着 vault 里的文件,文件一变
|
|
149
|
+
# 就重画,所以"内容一更新图谱就重建"只在"自动改 vault"时发生。要它变,显式加那两个开关。
|
|
150
|
+
if not args.with_vault or not args.with_wiki:
|
|
151
|
+
missing = [name for name, on in (('--with-vault(当天内容副本)', args.with_vault),
|
|
152
|
+
('--with-wiki(概念页)', args.with_wiki)) if not on]
|
|
153
|
+
print('\n Obsidian 那边没有动:%s。要写进去就显式加上 —— 图谱只在你要它重建时才变。'
|
|
154
|
+
% '、'.join(missing))
|
|
155
|
+
|
|
145
156
|
# Step 5: HTML 生成(可选)
|
|
146
157
|
if not args.skip_html:
|
|
147
158
|
date_str = args.date or time.strftime('%Y-%m-%d')
|
package/scripts/quality_eval.py
CHANGED
|
@@ -44,6 +44,11 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
44
44
|
from _utils import TOPICS, RELEVANCE_NAMES # noqa: E402
|
|
45
45
|
from jev_client import create_client # noqa: E402
|
|
46
46
|
|
|
47
|
+
# 手跑或被重定向时 stdout 走本机编码(Windows 是 GBK):非 GBK 字符(本文件里的 `⚠️`)会
|
|
48
|
+
# UnicodeEncodeError、整次输出没了;桥接跑有 PYTHONIOENCODING=utf-8 所以看不出来。
|
|
49
|
+
# 这条让两条路的输出一致(同 docs/EXTENDING.md Recipe E,守它的测试是 test/script_stdout_encoding_test.py)。
|
|
50
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
51
|
+
|
|
47
52
|
TZ = timezone(timedelta(hours=8))
|
|
48
53
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
49
54
|
DAILY_ROOT = os.path.join(ROOT, 'output', 'biz-daily')
|
package/scripts/rag_chat.py
CHANGED
|
@@ -19,7 +19,8 @@ import sys, os, json
|
|
|
19
19
|
|
|
20
20
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
21
21
|
from _utils import load_config, create_engine, get_api_key, get_dashscope_key
|
|
22
|
-
from semantic_search import search as semantic_search, build_index, INDEX_DIR, VECTORS_FILE
|
|
22
|
+
from semantic_search import (search as semantic_search, build_index, INDEX_DIR, VECTORS_FILE,
|
|
23
|
+
LINE_CHOICES, line_label)
|
|
23
24
|
|
|
24
25
|
PROMPT_TEMPLATE = """你是一个个人知识助手,可以访问用户的微信聊天记录和公众号文章。
|
|
25
26
|
|
|
@@ -36,8 +37,12 @@ PROMPT_TEMPLATE = """你是一个个人知识助手,可以访问用户的微
|
|
|
36
37
|
请用中文简洁回答,在关键信息处引用来源编号(如 [1]、[2])。"""
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def format_context(results: list[dict]) -> str:
|
|
40
|
-
"""将搜索结果格式化为 RAG context。
|
|
40
|
+
def format_context(results: list[dict], line: str = 'all') -> str:
|
|
41
|
+
"""将搜索结果格式化为 RAG context。
|
|
42
|
+
|
|
43
|
+
没有内容时**说清是"这条线里没有"**:`(未找到相关信息)` 会被模型读成"用户的知识库里
|
|
44
|
+
没有",而它其实只是"这一条线里没有"——答案的事实性就错在这里。
|
|
45
|
+
"""
|
|
41
46
|
chunks = []
|
|
42
47
|
for i, item in enumerate(results, 1):
|
|
43
48
|
if item.get('type') == 'article':
|
|
@@ -55,12 +60,13 @@ def format_context(results: list[dict]) -> str:
|
|
|
55
60
|
chunks.append(
|
|
56
61
|
f"[{i}] 内容: {item.get('text', str(item)[:500])}"
|
|
57
62
|
)
|
|
58
|
-
return '\n\n'.join(chunks) if chunks else '
|
|
63
|
+
return '\n\n'.join(chunks) if chunks else '(**%s**里没找到相关信息)' % line_label(line)
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
def build_prompt(question: str, results: list[dict], history: list[dict] = None
|
|
66
|
+
def build_prompt(question: str, results: list[dict], history: list[dict] = None,
|
|
67
|
+
line: str = 'all') -> str:
|
|
62
68
|
"""构建 RAG prompt。"""
|
|
63
|
-
context = format_context(results)
|
|
69
|
+
context = format_context(results, line)
|
|
64
70
|
|
|
65
71
|
if history and len(history) > 0:
|
|
66
72
|
history_text = '\n'.join(
|
|
@@ -73,10 +79,14 @@ def build_prompt(question: str, results: list[dict], history: list[dict] = None)
|
|
|
73
79
|
|
|
74
80
|
|
|
75
81
|
def query_rag(question: str, embed_key: str, chat_key: str, top_k: int = 10,
|
|
76
|
-
talker: str = None, history: list[dict] = None) -> dict:
|
|
77
|
-
"""执行一次 RAG 查询。
|
|
82
|
+
talker: str = None, history: list[dict] = None, line: str = 'all') -> dict:
|
|
83
|
+
"""执行一次 RAG 查询。
|
|
84
|
+
|
|
85
|
+
`line` 一路透到语义检索。**答案里要写清搜的是哪条线**:RAG 的坏法不是报错,是
|
|
86
|
+
"这条线里没有"被说成"你的知识库里没有"。函数默认 `all`,用户看到的默认在 CLI 那一层。
|
|
87
|
+
"""
|
|
78
88
|
# 1. 语义检索
|
|
79
|
-
results = semantic_search(question, embed_key, top_k=top_k)
|
|
89
|
+
results = semantic_search(question, embed_key, top_k=top_k, line=line)
|
|
80
90
|
|
|
81
91
|
# 2. 过滤 talker(如果指定)
|
|
82
92
|
if talker:
|
|
@@ -88,7 +98,7 @@ def query_rag(question: str, embed_key: str, chat_key: str, top_k: int = 10,
|
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
# 3. 构建 prompt
|
|
91
|
-
prompt = build_prompt(question, results, history)
|
|
101
|
+
prompt = build_prompt(question, results, history, line)
|
|
92
102
|
|
|
93
103
|
# 4. 调用 AI
|
|
94
104
|
config = load_config()
|
|
@@ -104,15 +114,17 @@ def query_rag(question: str, embed_key: str, chat_key: str, top_k: int = 10,
|
|
|
104
114
|
else:
|
|
105
115
|
sources.append(f"[{i}] {item.get('title', item.get('id', '?'))[:60]}")
|
|
106
116
|
|
|
107
|
-
return {'answer': answer, 'sources': sources}
|
|
117
|
+
return {'answer': answer, 'sources': sources, 'line': line, 'lineLabel': line_label(line)}
|
|
108
118
|
|
|
109
119
|
|
|
110
|
-
def interactive_mode(embed_key: str, chat_key: str):
|
|
120
|
+
def interactive_mode(embed_key: str, chat_key: str, line: str = 'all'):
|
|
111
121
|
"""交互式多轮对话。"""
|
|
112
122
|
config = load_config()
|
|
113
123
|
engine_type = config.get('aiEngine', 'deepseek')
|
|
114
124
|
|
|
115
125
|
print(f'\n🤖 RAG 智能助手 ({engine_type})')
|
|
126
|
+
# **把范围写在脸上**:交互模式一开就是十几轮,用户中途改了 `--line` 是看不到的
|
|
127
|
+
print(f' 检索范围: {line_label(line)}(`all` 才两条线一起搜)')
|
|
116
128
|
print(' 输入问题开始对话,输入 /help 查看帮助,输入 /exit 退出\n')
|
|
117
129
|
|
|
118
130
|
history: list[dict] = []
|
|
@@ -143,7 +155,7 @@ def interactive_mode(embed_key: str, chat_key: str):
|
|
|
143
155
|
print(f'✓ {result}\n')
|
|
144
156
|
continue
|
|
145
157
|
|
|
146
|
-
result = query_rag(question, embed_key, chat_key, top_k=10, history=history)
|
|
158
|
+
result = query_rag(question, embed_key, chat_key, top_k=10, history=history, line=line)
|
|
147
159
|
history.append({'question': question, 'answer': result['answer']})
|
|
148
160
|
|
|
149
161
|
print(f'\n🤖 助手:\n{result["answer"]}')
|
|
@@ -161,6 +173,8 @@ def main():
|
|
|
161
173
|
parser.add_argument('--top-k', type=int, default=10, help='检索数量(默认 10)')
|
|
162
174
|
parser.add_argument('--talker', help='限定联系人/群聊')
|
|
163
175
|
parser.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
|
|
176
|
+
parser.add_argument('--line', choices=list(LINE_CHOICES), default='wiki',
|
|
177
|
+
help='只看一条知识库:wiki=文章线(默认)、chat=聊天线、all=两条都看')
|
|
164
178
|
parser.add_argument('--json', action='store_true', help='JSON 输出')
|
|
165
179
|
args = parser.parse_args()
|
|
166
180
|
|
|
@@ -179,13 +193,16 @@ def main():
|
|
|
179
193
|
sys.exit(1)
|
|
180
194
|
|
|
181
195
|
if args.interactive or not question:
|
|
182
|
-
interactive_mode(embed_key, chat_key)
|
|
196
|
+
interactive_mode(embed_key, chat_key, line=args.line)
|
|
183
197
|
else:
|
|
184
|
-
result = query_rag(question, embed_key, chat_key, top_k=args.top_k, talker=talker
|
|
198
|
+
result = query_rag(question, embed_key, chat_key, top_k=args.top_k, talker=talker,
|
|
199
|
+
line=args.line)
|
|
185
200
|
if args.json:
|
|
186
201
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
187
202
|
else:
|
|
188
|
-
|
|
203
|
+
# 范围写在这句话里:`--line chat` 没答上来时,读的人要能一眼看出
|
|
204
|
+
# 是"聊天线里没有"还是"整个知识库里没有"
|
|
205
|
+
print(f'\n(检索范围:{result["lineLabel"]})\n{result["answer"]}')
|
|
189
206
|
if result.get('sources'):
|
|
190
207
|
print(f"\n📎 来源: {' | '.join(result['sources'])}")
|
|
191
208
|
|
package/scripts/reply_debt.py
CHANGED
|
@@ -327,9 +327,20 @@ def main():
|
|
|
327
327
|
try:
|
|
328
328
|
picked = collect_conversations(conns, name_map, own_wxid, args.days, args.limit)
|
|
329
329
|
if not picked:
|
|
330
|
-
|
|
330
|
+
# `--json` 时 stdout 必须是一整份 JSON。这条早退原来只打散文 ⇒ 机器消费者**拿不到任何
|
|
331
|
+
# 可解析的东西**(空目录/闲置机器上就会撞到)。形状与下面的 dry-run 预览一致,只是计数为 0。
|
|
332
|
+
if args.json:
|
|
333
|
+
print(json.dumps({'success': True, 'dryRun': args.dry_run,
|
|
334
|
+
'action': 'reply-debt.scan', 'days': args.days,
|
|
335
|
+
'conversations': 0, 'stateChars': 0,
|
|
336
|
+
'readsLocalChat': True, 'invokesAI': True,
|
|
337
|
+
'writesNothing': True}, ensure_ascii=False))
|
|
338
|
+
else:
|
|
339
|
+
print('最近 %d 天没有活跃会话' % args.days)
|
|
331
340
|
return 0
|
|
332
|
-
|
|
341
|
+
# 进度走 stderr(同文件下面两处 `file=sys.stderr` 的写法):`--json` 时 stdout 只放结果,
|
|
342
|
+
# 这一行原来无条件打 stdout ⇒ `awaiting --dry-run --json | jq` 会断在第一个字符上。
|
|
343
|
+
print('扫描 %d 个最近 %d 天有动静的会话…\n' % (len(picked), args.days), file=sys.stderr)
|
|
333
344
|
|
|
334
345
|
# 每个会话的最近若干条消息,一次读完(连接只开一次)
|
|
335
346
|
transcripts = {}
|
|
@@ -389,7 +400,8 @@ def main():
|
|
|
389
400
|
verdict, transcripts.get(item['talker'], []))
|
|
390
401
|
verdict['evidence'] = proofs.get(item['talker'], {})
|
|
391
402
|
rows.append(verdict)
|
|
392
|
-
print('判断完成 %d/%d,耗时 %.1fs\n' % (len(rows), len(futures), time.time() - started)
|
|
403
|
+
print('判断完成 %d/%d,耗时 %.1fs\n' % (len(rows), len(futures), time.time() - started),
|
|
404
|
+
file=sys.stderr) # 进度:`--json` 时不许进 stdout(本机跑不到这条路径,按读码改的)
|
|
393
405
|
finally:
|
|
394
406
|
for conn in conns:
|
|
395
407
|
conn.close()
|
|
@@ -413,6 +425,8 @@ def main():
|
|
|
413
425
|
noise = [r for r in rows if r['kind'] == '服务']
|
|
414
426
|
uncertain = [r for r in rows if 0.3 <= (r['waiting'] or 0) < args.min_prob]
|
|
415
427
|
|
|
428
|
+
report_path = None # 只有真的写了 HTML 才有值;json 模式下它会进输出
|
|
429
|
+
|
|
416
430
|
if args.html:
|
|
417
431
|
# 名字是外部数据,转义由 render_html 负责;这里只保证目录存在。
|
|
418
432
|
target = os.path.abspath(args.html)
|
|
@@ -424,11 +438,17 @@ def main():
|
|
|
424
438
|
'days': args.days, 'minProb': args.min_prob,
|
|
425
439
|
'generatedAt': now.strftime('%Y-%m-%d %H:%M'),
|
|
426
440
|
}))
|
|
427
|
-
|
|
441
|
+
report_path = target
|
|
442
|
+
# 人在看时打这一行;机器在看时**同一件事进 JSON**(`report` 那格)—— 只把它挪到 stderr 会
|
|
443
|
+
# 让机器消费者再也拿不到"写到哪了",那不是修,是换一种丢。
|
|
444
|
+
if not args.json:
|
|
445
|
+
print('已写出:%s(不含聊天内容,可直接分享)' % target)
|
|
428
446
|
|
|
429
447
|
if args.json:
|
|
430
|
-
|
|
431
|
-
|
|
448
|
+
payload = {'debts': debts, 'excluded_service': len(noise), 'uncertain': uncertain}
|
|
449
|
+
if report_path:
|
|
450
|
+
payload['report'] = report_path
|
|
451
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
432
452
|
return 0
|
|
433
453
|
|
|
434
454
|
print('=' * 62)
|