weflow-cli 1.8.1 → 1.9.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/CHANGELOG.md +359 -0
- package/OPERATIONS.md +79 -0
- package/README.en.md +9 -2
- package/README.md +12 -2
- package/bin/weflow-cli-electron.cjs +2 -0
- package/bin/weflow-cli.ts +648 -79
- package/dist/bin/weflow-cli.js +675 -78
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/http.d.ts +14 -0
- package/dist/mcp-server/http.d.ts.map +1 -0
- package/dist/mcp-server/http.js +84 -0
- package/dist/mcp-server/http.js.map +1 -0
- package/dist/mcp-server/httpConfig.d.ts +50 -0
- package/dist/mcp-server/httpConfig.d.ts.map +1 -0
- package/dist/mcp-server/httpConfig.js +104 -0
- package/dist/mcp-server/httpConfig.js.map +1 -0
- package/dist/mcp-server/index.d.ts +13 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +39 -373
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +26 -1
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +69 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +270 -8
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantScenes.d.ts +81 -0
- package/dist/src/services/assistantScenes.d.ts.map +1 -0
- package/dist/src/services/assistantScenes.js +322 -0
- package/dist/src/services/assistantScenes.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +14 -4
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +82 -6
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantSkills.d.ts +94 -0
- package/dist/src/services/assistantSkills.d.ts.map +1 -0
- package/dist/src/services/assistantSkills.js +330 -0
- package/dist/src/services/assistantSkills.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +7 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +770 -61
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +4 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -1
- package/dist/src/services/assistantTrace.js +9 -0
- package/dist/src/services/assistantTrace.js.map +1 -1
- package/dist/src/services/configService.d.ts +7 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +4 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/outboundSend.d.ts +53 -0
- package/dist/src/services/outboundSend.d.ts.map +1 -0
- package/dist/src/services/outboundSend.js +90 -0
- package/dist/src/services/outboundSend.js.map +1 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -1
- package/dist/src/services/pythonBridge.js +1 -0
- package/dist/src/services/pythonBridge.js.map +1 -1
- package/dist/src/utils/python.js +2 -2
- package/dist/src/utils/python.js.map +1 -1
- package/docs/BRANCHES.md +37 -0
- package/docs/DECISIONS.md +350 -0
- package/docs/EXTENDING.md +29 -3
- package/docs/MCP.md +56 -2
- package/docs/PROJECT_STATE.md +121 -9
- package/mcp-server/http.ts +108 -0
- package/mcp-server/httpConfig.ts +126 -0
- package/mcp-server/index.ts +344 -681
- package/package.json +1 -1
- package/resources/panel/ball-position.cjs +48 -2
- package/resources/panel/index.html +6 -4
- package/resources/panel/main.cjs +137 -5
- package/resources/panel/mascot-base.png +0 -0
- package/resources/panel/mascot-iris.png +0 -0
- package/resources/panel/mascot-peek.png +0 -0
- package/resources/panel/panel.css +107 -63
- package/resources/panel/preload.cjs +26 -0
- package/resources/panel/renderer.js +167 -10
- package/scripts/_utils.py +47 -1
- package/scripts/backfill_articles.py +202 -21
- package/scripts/biz_daily.py +8 -31
- package/scripts/bonds.py +331 -0
- package/scripts/chat_notes.py +551 -470
- package/scripts/chat_stats.py +8 -7
- package/scripts/compile_wiki.py +24 -2
- package/scripts/export_chat_html.py +3 -1
- package/scripts/mcp_bridge.py +6 -7
- package/scripts/wechat_voice.py +9 -0
- package/scripts/wiki_lint.py +30 -7
- package/src/panel/server.ts +25 -1
- package/src/services/assistantEval.ts +323 -12
- package/src/services/assistantScenes.ts +342 -0
- package/src/services/assistantService.ts +81 -6
- package/src/services/assistantSkills.ts +364 -0
- package/src/services/assistantTools.ts +738 -59
- package/src/services/assistantTrace.ts +9 -0
- package/src/services/configService.ts +412 -403
- package/src/services/outboundSend.ts +126 -0
- package/src/services/pythonBridge.ts +1 -0
- package/src/utils/python.ts +2 -2
package/scripts/chat_stats.py
CHANGED
|
@@ -83,13 +83,14 @@ def get_biz_keys(config):
|
|
|
83
83
|
contact_salt = config.get('contactSalt', '')
|
|
84
84
|
contact_key = decrypt_lock(contact_key_enc) if contact_key_enc else ''
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
# 钥匙派生统一到 `_utils.biz_message_db()`:原来这里只认配置里的 bizKey/bizSalt,
|
|
87
|
+
# 而那两个键在本机根本不存在(微信 4.x 的钥匙由全库 passphrase 派生)——
|
|
88
|
+
# 照这条提示去 `config set bizKey` 还会撞上"该键不在 CLI 可写白名单里"。
|
|
89
|
+
from _utils import biz_message_db
|
|
90
|
+
try:
|
|
91
|
+
biz_db, biz_key, biz_salt = biz_message_db(config)
|
|
92
|
+
except RuntimeError as exc:
|
|
93
|
+
print('[ERROR] %s' % exc)
|
|
93
94
|
sys.exit(1)
|
|
94
95
|
|
|
95
96
|
return {
|
package/scripts/compile_wiki.py
CHANGED
|
@@ -681,11 +681,20 @@ def build_pages_from_cards(pages_dir: str, card_dirs=None, dry_run: bool = False
|
|
|
681
681
|
existing = {p.stem.casefold() for p in Path(pages_dir).glob('*.md')}
|
|
682
682
|
out_dir = Path(pages_dir)
|
|
683
683
|
today = time.strftime('%Y-%m-%d')
|
|
684
|
-
|
|
684
|
+
# **只差大小写的两个概念名,在 Windows 上是同一个文件。** 原来的实现让后一个覆盖前一个:
|
|
685
|
+
# 一页**没了**,而 `built` 把两个都算了(实测 49,953 页里丢 53:AI 那批 44、这批 9)。
|
|
686
|
+
# 按"文件名(大小写折叠后)"分组 —— 一个文件一张页,其余写法进 `aliases`。这不是新规矩:
|
|
687
|
+
# `--merge-duplicates` 对"同一个概念的不同写法"做的就是这件事,`wiki_lint` 也一直认别名。
|
|
688
|
+
groups: dict = {}
|
|
685
689
|
for name, refs in merged.items():
|
|
690
|
+
groups.setdefault(re.sub(r'[\/:*?"<>|]', '_', name)[:60].casefold(), []).append((name, refs))
|
|
691
|
+
|
|
692
|
+
built, skipped, unwritable, no_material, bad_source = [], 0, 0, 0, 0
|
|
693
|
+
for members in groups.values():
|
|
694
|
+
name, refs = members[0] # 组里第一个当正式名,其余进 aliases
|
|
686
695
|
safe = re.sub(r'[\/:*?"<>|]', '_', name)[:60]
|
|
687
696
|
if safe.casefold() in existing:
|
|
688
|
-
skipped +=
|
|
697
|
+
skipped += len(members)
|
|
689
698
|
continue
|
|
690
699
|
if '[' in name or ']' in name or name.endswith('.md'):
|
|
691
700
|
# 同 `--refresh-sources`:这两类名字体检会报断链(`wiki_lint.resolve` 见到 `.md`
|
|
@@ -723,6 +732,19 @@ def build_pages_from_cards(pages_dir: str, card_dirs=None, dry_run: bool = False
|
|
|
723
732
|
'sources': [ref['file']],
|
|
724
733
|
'summary_by': 'card',
|
|
725
734
|
}
|
|
735
|
+
# 别名有两类来源,都是**指向这一页的名字**:
|
|
736
|
+
# 1. 需要净化/截断时保留的原名 —— 否则 Obsidian 里 `[[Qwen3.5:9B]]` 点不开
|
|
737
|
+
# `Qwen3.5_9B.md`,`wiki_lint` 也会把这页报成孤儿(2026-10-01 实测 28 张);
|
|
738
|
+
# 2. 同一批里只差大小写的另一种写法 —— 它们本来指向同一张页。
|
|
739
|
+
# 组里的写法也要过"写不得"那关:带方括号或以 `.md` 结尾的名字进了 aliases
|
|
740
|
+
# 只会给体检添一批假断链(与上面概念名那条同一个理由)。
|
|
741
|
+
aliases = [name] if safe != name else []
|
|
742
|
+
for other, _ in members[1:]:
|
|
743
|
+
if other in aliases or '[' in other or ']' in other or other.endswith('.md'):
|
|
744
|
+
continue
|
|
745
|
+
aliases.append(other)
|
|
746
|
+
if aliases:
|
|
747
|
+
fm['aliases'] = aliases
|
|
726
748
|
if not dry_run:
|
|
727
749
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
728
750
|
write_with_frontmatter(str(out_dir / ('%s.md' % safe)), fm, body)
|
|
@@ -2379,7 +2379,9 @@ def main():
|
|
|
2379
2379
|
# Voice: load the payloads so a cached transcript can be shown. Anything
|
|
2380
2380
|
# not yet transcribed simply renders as `[语音 N″]`.
|
|
2381
2381
|
if _WECHAT_VOICE:
|
|
2382
|
-
|
|
2382
|
+
# 缓存的家由 `wechat_voice` 定义:以前这里按 `--out` 拼,于是同一段音频在不同
|
|
2383
|
+
# 输出目录下会被各转写一遍,而卡片那条线读的又是另一处(2026-10-01)。
|
|
2384
|
+
VOICE_STATE['cache'] = wechat_voice.TranscriptCache(wechat_voice.DEFAULT_CACHE_DIR)
|
|
2383
2385
|
try:
|
|
2384
2386
|
VOICE_STATE['map'] = wechat_voice.load_voice_map(
|
|
2385
2387
|
args.db, args.key, args.salt, args.passphrase, args.talker)
|
package/scripts/mcp_bridge.py
CHANGED
|
@@ -69,13 +69,12 @@ def get_biz_keys(config):
|
|
|
69
69
|
contact_key_enc = config.get('contactKey', '')
|
|
70
70
|
contact_salt = config.get('contactSalt', '')
|
|
71
71
|
contact_key = decrypt_lock(contact_key_enc) if contact_key_enc else ''
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {'error': '缺少 biz_message_0.db 密钥,请运行 python scripts/nt_decrypt.py scan --json'}
|
|
72
|
+
# 钥匙派生统一到 `_utils.biz_message_db()`(原来只认 bizKey/bizSalt,见那个函数的注释)
|
|
73
|
+
from _utils import biz_message_db
|
|
74
|
+
try:
|
|
75
|
+
biz_db, biz_key, biz_salt = biz_message_db(config)
|
|
76
|
+
except RuntimeError as exc:
|
|
77
|
+
return {'error': str(exc)}
|
|
79
78
|
return {
|
|
80
79
|
'biz_db': biz_db, 'biz_key': biz_key, 'biz_salt': biz_salt,
|
|
81
80
|
'contact_db': contact_db, 'contact_key': contact_key, 'contact_salt': contact_salt,
|
package/scripts/wechat_voice.py
CHANGED
|
@@ -33,6 +33,15 @@ EMPTY_MARKER = '\x00empty'
|
|
|
33
33
|
|
|
34
34
|
DEFAULT_MODEL = 'small'
|
|
35
35
|
|
|
36
|
+
# **转写缓存的家**,由这个模块定义,别处一律引用它。
|
|
37
|
+
#
|
|
38
|
+
# 2026-10-01 踩过一次:`chat_notes` 拿自己的输出目录(`output/chat-notes`)去拼缓存路径,于是
|
|
39
|
+
# 它读到的是 `output/chat-notes/.voice-cache` —— 一个刚被它自己建出来的空目录,而真正的缓存
|
|
40
|
+
# 在 `output/.voice-cache`(1,788 条)。它「命中 16 条」那个数字是自洽的,只是与真缓存不相通,
|
|
41
|
+
# 报告看不出来。位置只要有两份定义就会漂,所以从这里定一份。
|
|
42
|
+
# 可用 `WEFLOW_VOICE_CACHE` 覆盖(测试与自定义布局用)。
|
|
43
|
+
DEFAULT_CACHE_DIR = os.environ.get('WEFLOW_VOICE_CACHE') or os.path.join('output', '.voice-cache')
|
|
44
|
+
|
|
36
45
|
# A Cantonese-tuned model, fetched separately (see the README note in
|
|
37
46
|
# OPERATIONS.md). Worth preferring automatically when present: stock Whisper
|
|
38
47
|
# answers Cantonese speech with fluent-looking Mandarin nonsense - text that
|
package/scripts/wiki_lint.py
CHANGED
|
@@ -144,15 +144,28 @@ def inspect(pages: list, exists, card_links=()) -> dict:
|
|
|
144
144
|
"""
|
|
145
145
|
stems = {page['stem'] for page in pages}
|
|
146
146
|
inbound = {stem: 0 for stem in stems}
|
|
147
|
+
# **别名要归位到它那一页,否则那张页永远被报成孤儿。** 文件名是概念名的一个有损变换
|
|
148
|
+
# (`:` `/ ? * " < > |` 换成 `_`,再截到 60 字符),而指向它的链接用的是**原名**:
|
|
149
|
+
# `resolvable_names` 早就把别名算进"存在"了(所以断链是 0),但**入链计数**这里原来只认页码
|
|
150
|
+
# —— 名字对不上,卡片那条链接既不计入链也不报断链,那一页于是在图里是孤岛。
|
|
151
|
+
# (2026-10-01 实测:`[[Qwen3.5:9B]]` → `Qwen3.5_9B.md`,49,953 页里有 28 张。)
|
|
152
|
+
alias_to_stem = {str(alias): page['stem']
|
|
153
|
+
for page in pages
|
|
154
|
+
for alias in (page.get('aliases') or [])
|
|
155
|
+
if str(alias) not in stems}
|
|
156
|
+
|
|
157
|
+
def stem_of(name: str) -> str:
|
|
158
|
+
return alias_to_stem.get(name, name)
|
|
159
|
+
|
|
147
160
|
for name in card_links: # 卡片 → 概念,这是主要入链
|
|
148
|
-
if not (name.endswith('.md') or '/' in name) and name in inbound:
|
|
149
|
-
inbound[name] += 1
|
|
161
|
+
if not (name.endswith('.md') or '/' in name) and stem_of(name) in inbound:
|
|
162
|
+
inbound[stem_of(name)] += 1
|
|
150
163
|
broken, aspirational, empty = [], [], []
|
|
151
164
|
for page in pages:
|
|
152
165
|
for section, name in links_by_section(page['body']):
|
|
153
166
|
if exists(name):
|
|
154
|
-
if not (name.endswith('.md') or '/' in name) and name in inbound:
|
|
155
|
-
inbound[name] += 1
|
|
167
|
+
if not (name.endswith('.md') or '/' in name) and stem_of(name) in inbound:
|
|
168
|
+
inbound[stem_of(name)] += 1
|
|
156
169
|
continue
|
|
157
170
|
entry = {'page': page['stem'], 'target': name, 'section': section}
|
|
158
171
|
if any(section.startswith(prefix) for prefix in RELATED_SECTIONS):
|
|
@@ -326,12 +339,16 @@ def collect(pages_dir: str, card_dirs) -> list:
|
|
|
326
339
|
# 判重按目录分开比(两个目录是两个知识库),所以要记住这张页住哪儿
|
|
327
340
|
'dir': str(pages_dir),
|
|
328
341
|
'topic': compile_frontmatter_topic(frontmatter),
|
|
329
|
-
|
|
342
|
+
# `parse_frontmatter` 已经把外层引号拆掉了,**这里不能再剥一次**:
|
|
343
|
+
# `strip('"')` 会连名字**自己末尾**的引号一起吃掉,`AI 长出"手脚"` 变成
|
|
344
|
+
# `AI 长出"手脚` —— 然后它与卡片里那条链接差一个字符,永远匹配不上,
|
|
345
|
+
# 那页就被报成孤儿(2026-10-01 实测,49,953 页里 1 张)。
|
|
346
|
+
'title': str(frontmatter.get('title') or ''),
|
|
330
347
|
# **别名也算"指向这一页的名字"**:`--merge-duplicates` 把
|
|
331
348
|
# `GPT-5.6` 并进 `GPT 5.6` 时就是靠 aliases 让旧链接不断的,
|
|
332
349
|
# Obsidian 认它。体检不认的话,会把这些名字报成"还没建页、
|
|
333
350
|
# 建议再跑 compile" —— 而它们已经有页了。
|
|
334
|
-
'aliases': [str(a).strip()
|
|
351
|
+
'aliases': [str(a).strip() for a in raw_aliases],
|
|
335
352
|
'links': extract_links(body),
|
|
336
353
|
'body': body})
|
|
337
354
|
return pages
|
|
@@ -364,8 +381,14 @@ def main():
|
|
|
364
381
|
# 只查卡片目录会把这类全报成断链——实测 1,950 条,而它们在 Obsidian 里都是好的
|
|
365
382
|
# (两边文件名本来就不同:卡片保留完整标题,阅读笔记截到 50 字)。
|
|
366
383
|
card_stems = collect_card_stems(tuple(CARD_DIRS) + MATERIAL_DIRS)
|
|
384
|
+
resolvable = resolvable_names(pages) # 只算一次(见下面那条注释)
|
|
367
385
|
report = inspect(pages,
|
|
368
|
-
|
|
386
|
+
|
|
387
|
+
# **注意别把它写回 lambda 里**:闭包体是**调用时**求值的,
|
|
388
|
+
# 写在里面等于每条链接都重建一次全部页名的集合 —— 实测文章线
|
|
389
|
+
# (3,623 页)因此要 9 分 29 秒,聊天线(1,527 页)只要 3.2 秒。
|
|
390
|
+
# 提出来算一次之后是同一条线的秒级。
|
|
391
|
+
lambda name: resolve(name, resolvable, CARD_DIRS, card_stems),
|
|
369
392
|
card_links)
|
|
370
393
|
report['success'] = True
|
|
371
394
|
report['pagesDir'] = existing
|
package/src/panel/server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
writeEndpoint, type PanelChannelMode, type PanelEndpoint,
|
|
35
35
|
} from './endpoint.js'
|
|
36
36
|
import type { AssistantService, TurnOutcome } from '../services/assistantService.js'
|
|
37
|
+
import { describeForPanel } from '../services/assistantTrace.js'
|
|
37
38
|
import { privacyGate } from '../services/assistantPrivacy.js'
|
|
38
39
|
import { configService } from '../services/configService.js'
|
|
39
40
|
import { resolvePackageRoot } from '../utils/packageRoot.js'
|
|
@@ -66,6 +67,16 @@ const STATIC_FILES: Record<string, string> = {
|
|
|
66
67
|
'/panel/mascot-thinking.png': 'mascot-thinking.png',
|
|
67
68
|
'/panel/mascot-sorry.png': 'mascot-sorry.png',
|
|
68
69
|
'/panel/mascot-tired.png': 'mascot-tired.png',
|
|
70
|
+
// 眼球跟随鼠标用的两层(2026-09-30):`mascot-base.png` 是**把虹膜挖掉**的球面、
|
|
71
|
+
// `mascot-iris.png` 是可以单独平移的虹膜层(含瞳孔与高光)。零偏移叠回去与原图**逐像素相同**
|
|
72
|
+
// (脚本验过),所以换上去不会看出任何差别,只是眼睛能动了。
|
|
73
|
+
// 同上:**漏一张,球在那个瞬间闪成空图**,而 PANEL_ASSET_MISSING 只写在响应里、页面上看不到。
|
|
74
|
+
'/panel/mascot-base.png': 'mascot-base.png',
|
|
75
|
+
'/panel/mascot-iris.png': 'mascot-iris.png',
|
|
76
|
+
// 半隐时那张脸(2026-09-30):身子藏在屏幕边后面、只把脑袋探出来。
|
|
77
|
+
// 它的左边**必须是一条笔直的切边**(内容贴着画布 x=0),贴上去才跟屏幕边严丝合缝——
|
|
78
|
+
// 所以半隐时球的 `border-radius` 也要去掉(圆会把那条切边剪成弧)。
|
|
79
|
+
'/panel/mascot-peek.png': 'mascot-peek.png',
|
|
69
80
|
}
|
|
70
81
|
const CONTENT_TYPES: Record<string, string> = {
|
|
71
82
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -346,7 +357,20 @@ export async function startPanelServer(options: PanelServerOptions): Promise<Pan
|
|
|
346
357
|
}
|
|
347
358
|
const outcome = raced.value
|
|
348
359
|
if (outcome.status === 'replied' || outcome.status === 'quota-exceeded') {
|
|
349
|
-
|
|
360
|
+
// 顺带把这一轮的**轨迹**给页面:它折叠着显示成"思考过程"(默认收起)。
|
|
361
|
+
// 与微信里发「轨迹」看到的**是同一份**(都来自 `traces`,同一个 `describeForChat` 格式,
|
|
362
|
+
// 同样不含 userId)。没有轨迹就给空对象——页面据此不显示那一块。
|
|
363
|
+
//
|
|
364
|
+
// **它出错不许把答复弄丢**:轨迹是附加物,答复才是这一轮的结果。所以这里 catch 住——
|
|
365
|
+
// 顶多少一块"思考过程",不能因为附加物把 200 变成 500(第一版没包,测试立刻抓到:
|
|
366
|
+
// 桩服务没有 `recentTrace`,于是每条 ask 都成了 500)。
|
|
367
|
+
let trace = { lines: [] as string[], reasoning: '' }
|
|
368
|
+
try {
|
|
369
|
+
trace = describeForPanel(service.recentTrace(memoryBucket))
|
|
370
|
+
} catch { /* 附加物失败不影响答复 */ }
|
|
371
|
+
json(res, 200, {
|
|
372
|
+
ok: true, status: outcome.status, reply: outcome.text, memoryBucket, trace,
|
|
373
|
+
})
|
|
350
374
|
return
|
|
351
375
|
}
|
|
352
376
|
if (outcome.status === 'not-running') {
|