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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站发送的**那一份策略**:黑名单 → 白名单 → 限速,以及"每次尝试都写审计"。
|
|
3
|
+
*
|
|
4
|
+
* 为什么抽成一份:这条链是一条**安全策略**,而 2026-10-01 起有了第二条发送路
|
|
5
|
+
* (`draft --pick N --send`,见 D-063)。两份写同一件事迟早分叉,而这里分叉的后果具体是
|
|
6
|
+
* "有一条路绕过了白名单" —— 悄悄多出一个能发给任何人的入口。
|
|
7
|
+
*
|
|
8
|
+
* **它不面向模型。** 助手与 MCP 的工具表里没有任何发送工具(`docs/EXTENDING.md`:
|
|
9
|
+
* *sending is structurally unreachable from a model-driven path*)。这个模块只被**人在命令行上
|
|
10
|
+
* 显式驱动**的命令调用,而且第二条路还要求先点名"发第几条"。
|
|
11
|
+
*
|
|
12
|
+
* 一条实测出来的硬约束写在错误信息里:发送需要**对方的 context_token**,也就是对方得先给你
|
|
13
|
+
* 发过消息。没有它,"主动发给任何人"这条路本来就不通。
|
|
14
|
+
*/
|
|
15
|
+
import { configService } from './configService.js'
|
|
16
|
+
import { whitelistService } from './whitelistService.js'
|
|
17
|
+
import { WechatMessageService } from './wechatMessageService.js'
|
|
18
|
+
|
|
19
|
+
export type OutboundKind = 'text' | 'image' | 'file'
|
|
20
|
+
|
|
21
|
+
export interface OutboundAttempt {
|
|
22
|
+
wxid: string
|
|
23
|
+
displayName: string
|
|
24
|
+
kind: OutboundKind
|
|
25
|
+
/** 审计与预览里那一行(文本截断后的样子,或 `[image] 文件名 (字节)`) */
|
|
26
|
+
preview: string
|
|
27
|
+
/** 纯文本内容(kind 为 text 时用;媒体走 mediaPath) */
|
|
28
|
+
text?: string
|
|
29
|
+
mediaPath?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OutboundPolicy {
|
|
33
|
+
windowMs: number
|
|
34
|
+
max: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type OutboundRefusalCode =
|
|
38
|
+
| 'INVALID_RATE_LIMIT'
|
|
39
|
+
| 'TARGET_BLOCKED'
|
|
40
|
+
| 'TARGET_NOT_ALLOWED'
|
|
41
|
+
| 'RATE_LIMITED'
|
|
42
|
+
|
|
43
|
+
export interface OutboundRefusal {
|
|
44
|
+
code: OutboundRefusalCode
|
|
45
|
+
error: string
|
|
46
|
+
/** 限速时带上窗口与计数,好让调用方原样报出来 */
|
|
47
|
+
rateLimit?: { count: number; max: number; windowMs: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function auditRefusal(attempt: OutboundAttempt, error: string): void {
|
|
51
|
+
whitelistService.auditSend({
|
|
52
|
+
timestamp: Date.now(), action: 'send', targetWxid: attempt.wxid,
|
|
53
|
+
targetName: attempt.displayName, kind: attempt.kind, success: false,
|
|
54
|
+
preview: attempt.preview, error,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 速率参数合法性。`send` 与 `draft --send` 用的是同一条规则。 */
|
|
59
|
+
export function parseOutboundPolicy(windowMs: number, max: number): OutboundPolicy | OutboundRefusal {
|
|
60
|
+
if (!Number.isInteger(windowMs) || windowMs < 1000 || windowMs > 3_600_000
|
|
61
|
+
|| !Number.isInteger(max) || max < 1 || max > 100) {
|
|
62
|
+
return { code: 'INVALID_RATE_LIMIT', error: 'rate-window 必须为 1000-3600000,rate-max 必须为 1-100' }
|
|
63
|
+
}
|
|
64
|
+
return { windowMs, max }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface OutboundRate { count: number; max: number; windowMs: number }
|
|
68
|
+
export type OutboundDecision =
|
|
69
|
+
| { ok: true; rate: OutboundRate }
|
|
70
|
+
| { ok: false; refusal: OutboundRefusal }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 发送前的三道闸。拒绝时**先写审计**再返回原因("每次尝试都留痕"是策略的一部分,不只是成功的那些)。
|
|
74
|
+
*
|
|
75
|
+
* 放行时把**当时的速率计数**一起带回去 —— 调用方要拿它写进预览(原来那一条命令里
|
|
76
|
+
* 是先 `checkRateLimit` 再复用同一个对象)。少一次重复查询,也免得两处看到的数不一样。
|
|
77
|
+
*/
|
|
78
|
+
export function authorizeOutbound(attempt: OutboundAttempt, policy: OutboundPolicy): OutboundDecision {
|
|
79
|
+
if (whitelistService.isBlocked(attempt.wxid)) {
|
|
80
|
+
auditRefusal(attempt, 'blocked by blacklist')
|
|
81
|
+
return { ok: false, refusal: { code: 'TARGET_BLOCKED', error: '目标在黑名单中' } }
|
|
82
|
+
}
|
|
83
|
+
if (!whitelistService.isAllowed(attempt.wxid)) {
|
|
84
|
+
// **白名单是这一层真正的边界**:不在名单上的人,任何一条命令行都发不出去。
|
|
85
|
+
// 这一条**不写审计** —— 与黑名单那条不一样,原实现就是如此,保持一致。
|
|
86
|
+
return { ok: false, refusal: { code: 'TARGET_NOT_ALLOWED', error: '目标不在白名单中' } }
|
|
87
|
+
}
|
|
88
|
+
const rate = whitelistService.checkRateLimit(policy.windowMs, policy.max)
|
|
89
|
+
if (!rate.allowed) {
|
|
90
|
+
auditRefusal(attempt, `rate limited (${rate.count}/${rate.max})`)
|
|
91
|
+
return { ok: false, refusal: { code: 'RATE_LIMITED', error: '触发发送速率限制', rateLimit: rate } }
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, rate }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 真的发出去,并按结果写审计。返回 `{success, error}`(不抛)。 */
|
|
97
|
+
export async function performOutbound(attempt: OutboundAttempt): Promise<{ success: boolean; error?: string }> {
|
|
98
|
+
const token = configService.get('wechatOcToken')
|
|
99
|
+
const service = new WechatMessageService({ token })
|
|
100
|
+
let success = false
|
|
101
|
+
let error: string | undefined
|
|
102
|
+
try {
|
|
103
|
+
if (attempt.kind === 'image' && attempt.mediaPath) {
|
|
104
|
+
success = await service.sendImage(attempt.wxid, attempt.mediaPath)
|
|
105
|
+
} else if (attempt.kind === 'file' && attempt.mediaPath) {
|
|
106
|
+
success = await service.sendFile(attempt.wxid, attempt.mediaPath)
|
|
107
|
+
} else {
|
|
108
|
+
success = await service.sendText(attempt.wxid, attempt.text ?? '')
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
error = '消息通道调用失败'
|
|
112
|
+
}
|
|
113
|
+
if (!success && !error) {
|
|
114
|
+
// 最像的原因就是缺 context_token —— 说清楚,别让人以为是"发失败"这种含糊的锅
|
|
115
|
+
const hasToken = !!(configService.getContextTokens()[attempt.wxid])
|
|
116
|
+
error = hasToken
|
|
117
|
+
? '发送失败 (接口返回错误, 可能 token 过期)'
|
|
118
|
+
: '缺少 context_token — 需先收到对方一条消息 (或运行 weflow-cli listen 等待对方消息)'
|
|
119
|
+
}
|
|
120
|
+
whitelistService.auditSend({
|
|
121
|
+
timestamp: Date.now(), action: 'send', targetWxid: attempt.wxid,
|
|
122
|
+
targetName: attempt.displayName, kind: attempt.kind, success,
|
|
123
|
+
preview: attempt.preview, error: success ? undefined : error,
|
|
124
|
+
})
|
|
125
|
+
return { success, error }
|
|
126
|
+
}
|
|
@@ -45,6 +45,7 @@ function realRunner(scriptPath: string, args: string[],
|
|
|
45
45
|
options: { timeoutMs: number; stdin?: string; env?: Record<string, string> }): Promise<ScriptRun> {
|
|
46
46
|
return new Promise((resolve, reject) => {
|
|
47
47
|
const child = spawn(getPythonCommand(), [scriptPath, ...args], {
|
|
48
|
+
windowsHide: true,
|
|
48
49
|
// 用户输入走**环境变量**而不是 argv:这是仓库写进测试的隐私纪律(进程列表里看不到正文)
|
|
49
50
|
env: createPythonProcessEnv(options.env ?? {}),
|
|
50
51
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/src/utils/python.ts
CHANGED
|
@@ -38,7 +38,7 @@ function candidates(): string[] {
|
|
|
38
38
|
|
|
39
39
|
function canRun(cmd: string): boolean {
|
|
40
40
|
try {
|
|
41
|
-
execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 })
|
|
41
|
+
execFileSync(cmd, ['--version'], { windowsHide: true, stdio: 'ignore', timeout: 5000 })
|
|
42
42
|
return true
|
|
43
43
|
} catch {
|
|
44
44
|
return false
|
|
@@ -47,7 +47,7 @@ function canRun(cmd: string): boolean {
|
|
|
47
47
|
|
|
48
48
|
function hasSqlcipher(cmd: string): boolean {
|
|
49
49
|
try {
|
|
50
|
-
execFileSync(cmd, ['-c', 'import sqlcipher3'], { stdio: 'ignore', timeout: 10000 })
|
|
50
|
+
execFileSync(cmd, ['-c', 'import sqlcipher3'], { windowsHide: true, stdio: 'ignore', timeout: 10000 })
|
|
51
51
|
return true
|
|
52
52
|
} catch {
|
|
53
53
|
return false
|