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
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import crypto from 'crypto'
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync } from 'fs'
|
|
17
|
+
// AppMsg 的显示形态提到了 `appMsgFormat`:那是纯函数,能直接测(此前是这里的私有方法,
|
|
18
|
+
// 一行测试都没有)。4.x 走的是 Python 侧 `nt_decrypt.py` 的同名逻辑,两边要保持一致。
|
|
19
|
+
import { formatAppMsg, parseAppMsgXml } from './appMsgFormat.js'
|
|
17
20
|
import { join, basename, dirname } from 'path'
|
|
18
21
|
import os from 'os'
|
|
19
22
|
import { DatabaseSync } from 'node:sqlite'
|
|
@@ -503,11 +506,11 @@ export class SqlcipherCore {
|
|
|
503
506
|
case 49: { // AppMsg (链接/文件/引用)
|
|
504
507
|
const xml = this.decompressCompressContent(compressContent)
|
|
505
508
|
if (xml) {
|
|
506
|
-
const appInfo =
|
|
509
|
+
const appInfo = parseAppMsgXml(xml)
|
|
507
510
|
appTitle = appInfo.title
|
|
508
511
|
appDescription = appInfo.description
|
|
509
512
|
appUrl = appInfo.url
|
|
510
|
-
parsedContent =
|
|
513
|
+
parsedContent = formatAppMsg(appInfo)
|
|
511
514
|
} else {
|
|
512
515
|
// 部分 Type=49 消息内容在 StrContent 中
|
|
513
516
|
parsedContent = strContent || '[链接/文件]'
|
|
@@ -634,65 +637,6 @@ export class SqlcipherCore {
|
|
|
634
637
|
}
|
|
635
638
|
}
|
|
636
639
|
|
|
637
|
-
/**
|
|
638
|
-
* 解析 Type=49 AppMsg XML
|
|
639
|
-
*/
|
|
640
|
-
private parseAppMsgXml(xml: string): { title?: string; description?: string; url?: string; type?: string } {
|
|
641
|
-
try {
|
|
642
|
-
const titleMatch = xml.match(/<title>([^<]*)<\/title>/)
|
|
643
|
-
const desMatch = xml.match(/<des>([^<]*)<\/des>/)
|
|
644
|
-
const urlMatch = xml.match(/<url>([^<]*)<\/url>/)
|
|
645
|
-
const typeMatch = xml.match(/<type>(\d+)<\/type>/)
|
|
646
|
-
|
|
647
|
-
return {
|
|
648
|
-
title: titleMatch ? this.decodeXmlEntities(titleMatch[1]) : undefined,
|
|
649
|
-
description: desMatch ? this.decodeXmlEntities(desMatch[1]) : undefined,
|
|
650
|
-
url: urlMatch ? this.decodeXmlEntities(urlMatch[1]) : undefined,
|
|
651
|
-
type: typeMatch ? typeMatch[1] : undefined,
|
|
652
|
-
}
|
|
653
|
-
} catch {
|
|
654
|
-
return {}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* 解码 XML 实体字符
|
|
660
|
-
*/
|
|
661
|
-
private decodeXmlEntities(str: string): string {
|
|
662
|
-
return str
|
|
663
|
-
.replace(/&/g, '&')
|
|
664
|
-
.replace(/</g, '<')
|
|
665
|
-
.replace(/>/g, '>')
|
|
666
|
-
.replace(/"/g, '"')
|
|
667
|
-
.replace(/'/g, "'")
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* 格式化 AppMsg 为可读文本
|
|
672
|
-
*/
|
|
673
|
-
private formatAppMsg(info: { title?: string; description?: string; url?: string; type?: string }): string {
|
|
674
|
-
const parts: string[] = []
|
|
675
|
-
|
|
676
|
-
switch (info.type) {
|
|
677
|
-
case '5': // 链接分享
|
|
678
|
-
if (info.title) parts.push(`[分享] ${info.title}`)
|
|
679
|
-
if (info.description) parts.push(info.description)
|
|
680
|
-
if (info.url) parts.push(info.url)
|
|
681
|
-
break
|
|
682
|
-
case '6': // 文件分享
|
|
683
|
-
if (info.title) parts.push(`[文件] ${info.title}`)
|
|
684
|
-
break
|
|
685
|
-
case '57': // 引用回复
|
|
686
|
-
if (info.title) parts.push(`[引用] ${info.title}`)
|
|
687
|
-
break
|
|
688
|
-
default:
|
|
689
|
-
if (info.title) parts.push(`[AppMsg] ${info.title}`)
|
|
690
|
-
if (info.description) parts.push(info.description)
|
|
691
|
-
break
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return parts.join('\n') || '[链接/文件]'
|
|
695
|
-
}
|
|
696
640
|
|
|
697
641
|
/**
|
|
698
642
|
* 获取联系人列表
|
package/src/core/wcdbCore.ts
CHANGED
|
@@ -2015,7 +2015,7 @@ export class WcdbCore {
|
|
|
2015
2015
|
items?: Array<{
|
|
2016
2016
|
sessionId: string
|
|
2017
2017
|
sessionDisplayName?: string
|
|
2018
|
-
mediaType
|
|
2018
|
+
mediaType?: 'image' | 'video' | undefined
|
|
2019
2019
|
localId: number
|
|
2020
2020
|
serverId?: string
|
|
2021
2021
|
createTime: number
|
|
@@ -2473,7 +2473,11 @@ export class WcdbCore {
|
|
|
2473
2473
|
return {
|
|
2474
2474
|
sessionId,
|
|
2475
2475
|
sessionDisplayName: sessionNameMap.get(sessionId) || sessionId,
|
|
2476
|
-
|
|
2476
|
+
// 只有明确的图片/视频才是媒体。原先这里对**一切非视频**都写 'image'
|
|
2477
|
+
// (连纯文本也是),是个没人消费的错值——公开方法里的错值迟早会被当真。
|
|
2478
|
+
mediaType: localType === 43 ? 'video' as const
|
|
2479
|
+
: localType === 3 ? 'image' as const
|
|
2480
|
+
: undefined,
|
|
2477
2481
|
localId: toInt(row.local_id ?? row.localId),
|
|
2478
2482
|
serverId: pickString(row, ['server_id', 'serverId']) || undefined,
|
|
2479
2483
|
createTime: toInt(row.create_time ?? row.createTime),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端点客户端:`weflow-cli panel` 用它读状态、换一次性口令、问一句话。
|
|
3
|
+
*
|
|
4
|
+
* 和悬浮窗走的是**同一个端点、同一套规矩**——所以"面板与微信共用一个大脑"这件事,
|
|
5
|
+
* 可以在命令行上被端到端地测(`panel ask` 就是"面板减去像素")。
|
|
6
|
+
*
|
|
7
|
+
* 凭据从不进日志、不进错误信息:出错时只说状态码与分类,不回显 token。
|
|
8
|
+
*/
|
|
9
|
+
import type { PanelEndpoint } from './endpoint.js'
|
|
10
|
+
|
|
11
|
+
export type ClientResult<T> =
|
|
12
|
+
| { ok: true; data: T }
|
|
13
|
+
| { ok: false; code: string; error?: string }
|
|
14
|
+
|
|
15
|
+
async function request(
|
|
16
|
+
endpoint: PanelEndpoint, path: string, init: RequestInit = {}, timeoutMs = 10_000,
|
|
17
|
+
): Promise<ClientResult<any>> {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {
|
|
20
|
+
...init,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${endpoint.token}`,
|
|
23
|
+
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
24
|
+
...(init.headers ?? {}),
|
|
25
|
+
},
|
|
26
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
27
|
+
})
|
|
28
|
+
const body: any = await res.json().catch(() => ({}))
|
|
29
|
+
if (!res.ok) return { ok: false, code: String(body?.code ?? `HTTP_${res.status}`), error: body?.error }
|
|
30
|
+
return { ok: true, data: body }
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
// 连不上:进程可能刚被停掉,或端点文件是残留的。**分开说**,别都说成"失败"
|
|
33
|
+
if (error?.name === 'TimeoutError') return { ok: false, code: 'TIMEOUT' }
|
|
34
|
+
return { ok: false, code: 'UNREACHABLE' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function panelStatus(endpoint: PanelEndpoint): Promise<ClientResult<any>> {
|
|
39
|
+
return request(endpoint, '/api/status')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 换一个一次性口令,用来拼浏览器那条路的 URL(口令不是 token,可以进命令行) */
|
|
43
|
+
export function panelPair(endpoint: PanelEndpoint): Promise<ClientResult<{ url: string; expiresInMs: number }>> {
|
|
44
|
+
return request(endpoint, '/api/pair', { method: 'POST' })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function panelAsk(endpoint: PanelEndpoint, text: string): Promise<ClientResult<any>> {
|
|
48
|
+
return request(endpoint, '/api/ask', { method: 'POST', body: JSON.stringify({ text }) }, 200_000)
|
|
49
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本机面板的**发现 + 凭据**:一个文件解决两件事。
|
|
3
|
+
*
|
|
4
|
+
* 悬浮窗要跟助手说话,需要知道两样东西——**端口**和**口令**。分开存会让"端口写死但服务没起来"
|
|
5
|
+
* 变成一类难查的故障,所以合在一个文件里,由守护进程**原子写**(先写 `.tmp` 再 `rename`,
|
|
6
|
+
* 照抄 `configService.save()` 的手法)。
|
|
7
|
+
*
|
|
8
|
+
* ## 三个必须处理的失效模式
|
|
9
|
+
*
|
|
10
|
+
* 1. **崩溃残留**:文件还在,进程没了 → 用 `process.kill(pid, 0)` 探活
|
|
11
|
+
* (照抄 `assistantDaemon.isDaemonAlive()` 的写法)。
|
|
12
|
+
* 2. **端口被别的进程占了**:文件里的端口是准的,但回应的人不是我们 → 拿到之后
|
|
13
|
+
* **必须打一次 `/api/status`**,校验 `service` 与 `startedAt` 都对得上
|
|
14
|
+
* (照抄 `getDailyReaderStatus()` 里那三行;它就是为这个存在的)。
|
|
15
|
+
* 3. **退出时不删文件**:**不要指望清理**。`stopDaemon()` 发的是 SIGTERM,而 Node 默认不为它跑
|
|
16
|
+
* 清理逻辑,所以文件**一定会残留**。"退出时删掉它"看起来太像一个能用的方案了,
|
|
17
|
+
* 所以这里明说:正确做法是探活,不是删。
|
|
18
|
+
*
|
|
19
|
+
* ## 关于 Windows 上的文件权限
|
|
20
|
+
*
|
|
21
|
+
* token 写在 `~/.weflow-cli/` 下。**不要声称"权限已收紧"**:Windows 上 `fs.chmod` /
|
|
22
|
+
* `mode: 0o600` 基本无效(Node 只能映射只读位)。真正起作用的是 `%USERPROFILE%\.weflow-cli\`
|
|
23
|
+
* 的默认 ACL(只有本人 + SYSTEM + Administrators)——**那是既成事实,不是我们做的事**。
|
|
24
|
+
* `mode: 0o600` 仍然传,因为在 Linux/macOS/WSL 上有意义;在 Windows 上它是空操作。
|
|
25
|
+
*
|
|
26
|
+
* 所以这道 token 的真实作用是:挡住**本机其它程序与任意网页**(它们读不到这个文件)。
|
|
27
|
+
* 它**挡不住**以同一用户身份运行、且愿意翻 `~/.weflow-cli/` 的程序——那种程序本来就能直接读库。
|
|
28
|
+
* 这是这条设计的真实边界,别把它说成比这更强。
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
31
|
+
import { writeFileAtomic } from '../utils/atomicWrite.js'
|
|
32
|
+
import { randomBytes, timingSafeEqual } from 'crypto'
|
|
33
|
+
import { join } from 'path'
|
|
34
|
+
import os from 'os'
|
|
35
|
+
|
|
36
|
+
/** 端点文件的格式版本。读不出来或不认识就**当作不存在**,不猜、不迁移(同记忆文件的纪律) */
|
|
37
|
+
export const PANEL_ENDPOINT_VERSION = 1
|
|
38
|
+
/** `/api/status` 的身份字段:防止把别人的端口当成助手(`service !== 它` 就不认) */
|
|
39
|
+
export const PANEL_SERVICE_ID = 'weflow-assistant'
|
|
40
|
+
/**
|
|
41
|
+
* 默认端口。**固定**而不是随机:便于本人手工 `curl` 排查,也让文档能写一个确定的地址。
|
|
42
|
+
* 端口被占时**大声失败**,不静默换一个——静默换端口会让"文件里写着 A、实际在 B"成为可能。
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_PANEL_PORT = 8766
|
|
45
|
+
|
|
46
|
+
export type PanelChannelMode = 'wechat' | 'local'
|
|
47
|
+
|
|
48
|
+
export interface PanelEndpoint {
|
|
49
|
+
version: number
|
|
50
|
+
service: string
|
|
51
|
+
port: number
|
|
52
|
+
token: string
|
|
53
|
+
pid: number
|
|
54
|
+
startedAt: string
|
|
55
|
+
channel: PanelChannelMode
|
|
56
|
+
/** 这个进程给本机入口用的记忆桶(与微信直聊同一个 id 时才是"共用一个大脑") */
|
|
57
|
+
memoryBucket: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function weflowHome(): string {
|
|
61
|
+
return join(os.homedir(), '.weflow-cli')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function endpointFile(): string {
|
|
65
|
+
return join(weflowHome(), 'assistant_endpoint.json')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 每进程一份,**每次启动重新生成**。32 字节 base64url。 */
|
|
69
|
+
export function generateToken(): string {
|
|
70
|
+
return randomBytes(32).toString('base64url')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isEndpoint(value: any): value is PanelEndpoint {
|
|
74
|
+
return !!value && typeof value === 'object'
|
|
75
|
+
&& value.version === PANEL_ENDPOINT_VERSION
|
|
76
|
+
&& value.service === PANEL_SERVICE_ID
|
|
77
|
+
&& typeof value.port === 'number' && Number.isInteger(value.port) && value.port > 0 && value.port < 65536
|
|
78
|
+
&& typeof value.token === 'string' && value.token.length > 0
|
|
79
|
+
&& typeof value.pid === 'number'
|
|
80
|
+
&& typeof value.startedAt === 'string'
|
|
81
|
+
&& (value.channel === 'wechat' || value.channel === 'local')
|
|
82
|
+
&& typeof value.memoryBucket === 'string'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 原子写:先 `.tmp` 再 rename。失败不抛——端点起不来不该把助手一起弄死,调用方会记日志 */
|
|
86
|
+
export function writeEndpoint(endpoint: PanelEndpoint): boolean {
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(weflowHome(), { recursive: true })
|
|
89
|
+
// `mode` 在 Windows 上是空操作,见文件头注释;不是为了"安全",是为了别的平台。
|
|
90
|
+
// 用 writeFileAtomic:rename 在被占用时会 EPERM,重试+兜底由它负责
|
|
91
|
+
writeFileAtomic(endpointFile(), JSON.stringify(endpoint, null, 2), { mode: 0o600 })
|
|
92
|
+
return true
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 读并校验。**任何一种不可信都返回 `null`**,绝不返回一个"大概是它"的对象:
|
|
100
|
+
* 文件不存在、JSON 坏了、版本不认识、字段缺、**进程已经没了**。
|
|
101
|
+
*/
|
|
102
|
+
export function readEndpoint(): PanelEndpoint | null {
|
|
103
|
+
try {
|
|
104
|
+
const file = endpointFile()
|
|
105
|
+
if (!existsSync(file)) return null
|
|
106
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
107
|
+
if (!isEndpoint(parsed)) return null
|
|
108
|
+
if (!isProcessAlive(parsed.pid)) return null
|
|
109
|
+
return parsed
|
|
110
|
+
} catch {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 探活。照抄 `assistantDaemon.isDaemonAlive()`:`kill(pid, 0)` 不真发信号,只做权限/存在性检查 */
|
|
116
|
+
export function isProcessAlive(pid: number): boolean {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(pid, 0)
|
|
119
|
+
return true
|
|
120
|
+
} catch {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 删除端点文件。**只在"确认要停"时用**(例如 `assistant stop`),
|
|
127
|
+
* 不要指望它在崩溃/被 SIGTERM 时被调用——见文件头第 3 条。
|
|
128
|
+
*/
|
|
129
|
+
export function clearEndpoint(): void {
|
|
130
|
+
try { rmSync(endpointFile(), { force: true }) } catch { /* 删不掉不影响正确性:读取端会探活 */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* token 比较。长度不等时 `timingSafeEqual` 会**抛**,所以先比长度。
|
|
135
|
+
* 用 `timingSafeEqual` 是为了不把"前几个字符对了"这件事通过耗时泄漏出去。
|
|
136
|
+
*/
|
|
137
|
+
export function tokenMatches(expected: string, given: unknown): boolean {
|
|
138
|
+
if (typeof given !== 'string' || given.length !== expected.length) return false
|
|
139
|
+
try {
|
|
140
|
+
return timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(given, 'utf8'))
|
|
141
|
+
} catch {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板的**启动决策**:用哪个二进制、argv 是什么、URL 是什么。
|
|
3
|
+
*
|
|
4
|
+
* 全部是纯函数,返回路径与数组、没有副作用——**因为这一层能进 CI,而窗口不能**。
|
|
5
|
+
* `resources/panel/main.cjs` 里只剩"调用 spawn / new BrowserWindow",决策都在这儿,
|
|
6
|
+
* 于是"会不会把凭据写进命令行"这种问题有地方可以钉住。
|
|
7
|
+
*
|
|
8
|
+
* 一条硬规矩贯穿本文件:**任何凭据都不许进 argv**。Windows 上同机任何进程都能从
|
|
9
|
+
* `wmic process get commandline` 读到命令行,把 token 放进去就是一次**看不出破绽的泄漏**。
|
|
10
|
+
* 所以浏览器那条路只带**一次性口令**(见 `server.ts` 的 `/api/pair`),Electron 那条什么都不带
|
|
11
|
+
* (它自己去读端点文件)。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
export type ExistsFn = (path: string) => boolean
|
|
17
|
+
|
|
18
|
+
export interface ElectronLookup {
|
|
19
|
+
/** 找 Electron 的顺序:包内 → 全局 npm 目录 → 都没有就是 null */
|
|
20
|
+
candidates: string[]
|
|
21
|
+
found: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 找 Electron。**用 `existsSync` 逐个试,不用 `where`/`which`**:
|
|
26
|
+
* PATH 上可能有 shim,而 shim 的行为不比一个确定的路径更可信。
|
|
27
|
+
*/
|
|
28
|
+
export function resolveElectronBinary(env: {
|
|
29
|
+
packageRoot: string
|
|
30
|
+
localAppData?: string
|
|
31
|
+
programFiles?: string
|
|
32
|
+
exists?: ExistsFn
|
|
33
|
+
}): ElectronLookup {
|
|
34
|
+
const exists = env.exists ?? existsSync
|
|
35
|
+
const candidates = [
|
|
36
|
+
// 1. 本项目里装过(npm i -D electron)
|
|
37
|
+
join(env.packageRoot, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
|
38
|
+
// 2. 全局 npm 装过(npm i -g electron)
|
|
39
|
+
env.localAppData ? join(env.localAppData, 'npm', 'node_modules', 'electron', 'dist', 'electron.exe') : '',
|
|
40
|
+
// 3. 全局装到别处时的兜底
|
|
41
|
+
env.programFiles ? join(env.programFiles, 'nodejs', 'node_modules', 'electron', 'dist', 'electron.exe') : '',
|
|
42
|
+
].filter(Boolean)
|
|
43
|
+
return { candidates, found: candidates.find((p) => exists(p)) ?? null }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Electron 的 argv。**就一个参数**:应用目录(里面的 `package.json` 指向 `main.cjs`)。
|
|
48
|
+
* 什么都不多带——不带 token、不带端口、不带 URL。
|
|
49
|
+
*/
|
|
50
|
+
export function electronLaunchArgs(panelDir: string): string[] {
|
|
51
|
+
return [panelDir]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EdgeLookup {
|
|
55
|
+
candidates: string[]
|
|
56
|
+
found: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 找 Edge;找不到再找 Chrome(Chromium 内核的 `--app=` 都能用) */
|
|
60
|
+
export function resolveBrowserBinary(env: {
|
|
61
|
+
programFiles?: string
|
|
62
|
+
programFilesX86?: string
|
|
63
|
+
localAppData?: string
|
|
64
|
+
exists?: ExistsFn
|
|
65
|
+
}): EdgeLookup {
|
|
66
|
+
const exists = env.exists ?? existsSync
|
|
67
|
+
const roots = [env.programFilesX86, env.programFiles, env.localAppData].filter(Boolean) as string[]
|
|
68
|
+
const candidates: string[] = []
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
candidates.push(join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'))
|
|
71
|
+
}
|
|
72
|
+
for (const root of roots) {
|
|
73
|
+
candidates.push(join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'))
|
|
74
|
+
}
|
|
75
|
+
return { candidates, found: candidates.find((p) => exists(p)) ?? null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 浏览器降级的 argv。
|
|
80
|
+
*
|
|
81
|
+
* - `--app=<url>`:没有地址栏、没有标签栏,看起来就是个独立小窗。**这就是降级能给的极限**:
|
|
82
|
+
* 它不是悬浮球——不能无边框、不能置顶、没有托盘、没有全局快捷键。这件事必须对用户说清。
|
|
83
|
+
* - `--user-data-dir=<独立目录>`:**不是可选的**。不带它,`--app` 会挤进用户日常那个浏览器实例
|
|
84
|
+
* (共享 cookie 与会话,甚至可能被并进已有窗口而不是新开一个)。代价是多一个约 50MB 的
|
|
85
|
+
* profile 目录,**它要进隐私文档的敏感路径清单**。
|
|
86
|
+
* - URL 里带的是**一次性口令**(`?c=`),不是 token:口令单次使用、60 秒过期,
|
|
87
|
+
* 所以它出现在命令行与浏览器历史里都不构成泄漏。
|
|
88
|
+
*/
|
|
89
|
+
export function browserLaunchArgs(options: { url: string; profileDir: string }): string[] {
|
|
90
|
+
return [
|
|
91
|
+
`--app=${options.url}`,
|
|
92
|
+
`--user-data-dir=${options.profileDir}`,
|
|
93
|
+
'--no-first-run',
|
|
94
|
+
'--no-default-browser-check',
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 浏览器降级专用的 profile 目录(与用户日常浏览器隔离) */
|
|
99
|
+
export function browserProfileDir(weflowHome: string): string {
|
|
100
|
+
return join(weflowHome, 'panel-browser-profile')
|
|
101
|
+
}
|