@mhfire/dsh-im-bridge 0.1.2 → 0.1.7
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/README.en.md +101 -0
- package/README.md +101 -54
- package/cordis.patch.yml +79 -6
- package/lib/client.js +2 -0
- package/package.json +45 -42
- package/persona.default.en.md +22 -0
- package/persona.default.md +22 -0
- package/src/index.js +401 -247
- package/src/wecom.js +192 -91
package/src/wecom.js
CHANGED
|
@@ -1,91 +1,192 @@
|
|
|
1
|
-
|
|
2
|
-
* wecom.js — 企业微信交互封装: 流式动画(v3) + 最终简报 + 发送助手。
|
|
3
|
-
* 逻辑移植自旧版 im-bridge/bridge.js, 改为纯 ESM 供插件使用。
|
|
4
|
-
*/
|
|
5
|
-
import { generateReqId } from '@wecom/aibot-node-sdk'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return ()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
/**
|
|
2
|
+
* wecom.js — 企业微信交互封装: 流式动画(v3) + 最终简报 + 发送助手。
|
|
3
|
+
* 逻辑移植自旧版 im-bridge/bridge.js, 改为纯 ESM 供插件使用。
|
|
4
|
+
*/
|
|
5
|
+
import { generateReqId } from '@wecom/aibot-node-sdk'
|
|
6
|
+
|
|
7
|
+
/** 流式思考动画的默认素材(可被 Config.thinking / cordis patch 覆盖) */
|
|
8
|
+
export const DEFAULT_THINKING = {
|
|
9
|
+
/** 尚无 assistant/chunk 时的时间轴兜底文案(与模型是否在推理无关) */
|
|
10
|
+
phases: [
|
|
11
|
+
{ atSec: 0, text: '🤔 正在理解你的需求…' },
|
|
12
|
+
{ atSec: 8, text: '📋 正在整理任务清单' },
|
|
13
|
+
{ atSec: 25, text: '🔍 正在查找相关资料' },
|
|
14
|
+
{ atSec: 55, text: '✍️ 正在处理文档/数据' },
|
|
15
|
+
{ atSec: 120, text: '🧠 正在思考最佳方案…' },
|
|
16
|
+
{ atSec: 240, text: '⏳ 任务较繁琐,请稍候…' },
|
|
17
|
+
{ atSec: 420, text: '☕ 快好了,正在收尾…' },
|
|
18
|
+
],
|
|
19
|
+
spin: ['🧠', '💭', '✨', '🔎', '⚡'],
|
|
20
|
+
eggs: [
|
|
21
|
+
'📎 顺手把要点整理好了,稍后一起给你',
|
|
22
|
+
'📶 网络有点忙,让它慢慢跑',
|
|
23
|
+
'🎯 结果快出来了,坚持一下',
|
|
24
|
+
'🗂️ 资料较多,正在汇总中',
|
|
25
|
+
'🌙 别盯着了,完成会自动通知你',
|
|
26
|
+
],
|
|
27
|
+
eggAfterSec: 240,
|
|
28
|
+
intervalMs: 1500,
|
|
29
|
+
activityPrefix: '🛠️ 正在执行 ',
|
|
30
|
+
/** 收到 reasoning-delta 时的状态行文案(按刷新轮换) */
|
|
31
|
+
reasoningStatus: ['💭 模型思考中…', '🧠 深入分析中…', '✨ 梳理思路中…'],
|
|
32
|
+
/** 收到 text-delta 时的状态行文案(按刷新轮换) */
|
|
33
|
+
outputStatus: ['✍️ 正在输出回复…', '📝 组织文字中…', '💬 生成回答中…'],
|
|
34
|
+
/** 思考阶段专用旋转表情 */
|
|
35
|
+
reasoningSpin: ['💭', '🧠', '🌀', '✨'],
|
|
36
|
+
/** 输出阶段专用旋转表情 */
|
|
37
|
+
outputSpin: ['✍️', '📝', '💬', '⚡'],
|
|
38
|
+
/** 工具名 → 企微活动文案友好名(可被 thinking.toolLabels 覆盖) */
|
|
39
|
+
toolLabels: {
|
|
40
|
+
pwsh: 'PowerShell',
|
|
41
|
+
bash: 'Shell',
|
|
42
|
+
read_file: '读文件',
|
|
43
|
+
read: '读文件',
|
|
44
|
+
write_file: '写文件',
|
|
45
|
+
write: '写文件',
|
|
46
|
+
edit_file: '编辑文件',
|
|
47
|
+
str_replace: '编辑文件',
|
|
48
|
+
glob: '查找文件',
|
|
49
|
+
grep: '搜索内容',
|
|
50
|
+
web_search: '网页搜索',
|
|
51
|
+
web_fetch: '抓取网页',
|
|
52
|
+
todo_write: '更新待办',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 将工具注册名映射为企微可见友好名。
|
|
58
|
+
* @param {string} name - 工具名(如 pwsh)
|
|
59
|
+
* @param {typeof DEFAULT_THINKING | undefined} thinking - 含可选 toolLabels 覆盖
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function labelTool(name, thinking) {
|
|
63
|
+
const labels = {
|
|
64
|
+
...DEFAULT_THINKING.toolLabels,
|
|
65
|
+
...(thinking?.toolLabels && typeof thinking.toolLabels === 'object' ? thinking.toolLabels : {}),
|
|
66
|
+
}
|
|
67
|
+
return labels[name] || name
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 从 string | string[] 配置中按 tick 取一条状态文案。
|
|
72
|
+
* @param {string | string[] | undefined} value
|
|
73
|
+
* @param {string[]} fallback
|
|
74
|
+
* @param {number} tick
|
|
75
|
+
*/
|
|
76
|
+
export function pickStatusLine(value, fallback, tick) {
|
|
77
|
+
const list = Array.isArray(value) && value.length > 0
|
|
78
|
+
? value
|
|
79
|
+
: (typeof value === 'string' && value !== '' ? [value] : fallback)
|
|
80
|
+
return list[Math.abs(tick) % list.length]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 根据 assistant/chunk 判别模型流式阶段。
|
|
85
|
+
* @param {{ type?: string, blockType?: string } | undefined} chunk
|
|
86
|
+
* @returns {'reasoning' | 'outputting' | null}
|
|
87
|
+
*/
|
|
88
|
+
export function streamPhaseFromChunk(chunk) {
|
|
89
|
+
if (!chunk || typeof chunk !== 'object') return null
|
|
90
|
+
if (chunk.type === 'reasoning-delta') return 'reasoning'
|
|
91
|
+
if (chunk.type === 'text-delta') return 'outputting'
|
|
92
|
+
if (chunk.type === 'block-start') {
|
|
93
|
+
if (chunk.blockType === 'reasoning') return 'reasoning'
|
|
94
|
+
if (chunk.blockType === 'text') return 'outputting'
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 毫秒 → 人类可读时长(如 "9 分 47 秒") */
|
|
100
|
+
export function fmtDuration(ms) {
|
|
101
|
+
const s = Math.floor(ms / 1000)
|
|
102
|
+
if (s < 60) return `${s} 秒`
|
|
103
|
+
const m = Math.floor(s / 60)
|
|
104
|
+
const r = s % 60
|
|
105
|
+
return r > 0 ? `${m} 分 ${r} 秒` : `${m} 分钟`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 按耗时给个速度评价 */
|
|
109
|
+
export function speedOf(ms) {
|
|
110
|
+
if (ms < 60000) return '⚡ 神速'
|
|
111
|
+
if (ms < 180000) return '🚀 正常速度'
|
|
112
|
+
return '🐢 耗时较长'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 执行完成的尾部简报 */
|
|
116
|
+
export function footerOf(ms) {
|
|
117
|
+
if (ms >= 180000) {
|
|
118
|
+
return `\n\n---\n✅ 执行完成 · 🐢 耗时较长(${fmtDuration(ms)})\n💡 如需提速,可让我把诊断步骤合并成更少的 SSH 批次`
|
|
119
|
+
}
|
|
120
|
+
return `\n\n---\n✅ 执行完成 · ${speedOf(ms)}(${fmtDuration(ms)})`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 截断(按字节) */
|
|
124
|
+
export function truncate(text, max) {
|
|
125
|
+
if (Buffer.byteLength(text, 'utf8') <= max) return text
|
|
126
|
+
let t = text
|
|
127
|
+
while (Buffer.byteLength(t, 'utf8') > max) t = t.slice(0, -100)
|
|
128
|
+
return t + '\n\n...(内容过长已截断)'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 流式动画 v3: 阶段化台词 + 旋转表情 + 进度条 + 已用时/剩余估算 + 长任务彩蛋。
|
|
133
|
+
* 每 intervalMs 更新一次同一条流式消息(共用 streamId), 直到 agent 完成。返回停止函数。
|
|
134
|
+
* @param {() => string} [activity] - 可选: 返回当前真实活动描述, 覆盖阶段台词。
|
|
135
|
+
* @param {typeof DEFAULT_THINKING} [thinking] - 动画素材; 缺省用 {@link DEFAULT_THINKING}。
|
|
136
|
+
* @param {() => 'idle' | 'reasoning' | 'outputting' | string} [getStreamPhase] - 模型流式阶段, 影响旋转表情池。
|
|
137
|
+
*/
|
|
138
|
+
export function startThinking(ws, frame, streamId, startedAt, timeoutSec, activity, thinking, getStreamPhase) {
|
|
139
|
+
const t = { ...DEFAULT_THINKING, ...(thinking || {}) }
|
|
140
|
+
const phases = Array.isArray(t.phases) && t.phases.length > 0 ? t.phases : DEFAULT_THINKING.phases
|
|
141
|
+
const spin = Array.isArray(t.spin) && t.spin.length > 0 ? t.spin : DEFAULT_THINKING.spin
|
|
142
|
+
const reasoningSpin = Array.isArray(t.reasoningSpin) && t.reasoningSpin.length > 0
|
|
143
|
+
? t.reasoningSpin
|
|
144
|
+
: DEFAULT_THINKING.reasoningSpin
|
|
145
|
+
const outputSpin = Array.isArray(t.outputSpin) && t.outputSpin.length > 0
|
|
146
|
+
? t.outputSpin
|
|
147
|
+
: DEFAULT_THINKING.outputSpin
|
|
148
|
+
const eggs = Array.isArray(t.eggs) && t.eggs.length > 0 ? t.eggs : DEFAULT_THINKING.eggs
|
|
149
|
+
const eggAfterSec = Number.isFinite(t.eggAfterSec) ? t.eggAfterSec : DEFAULT_THINKING.eggAfterSec
|
|
150
|
+
const intervalMs = Number.isFinite(t.intervalMs) && t.intervalMs > 0 ? t.intervalMs : DEFAULT_THINKING.intervalMs
|
|
151
|
+
const total = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 600
|
|
152
|
+
let i = 0
|
|
153
|
+
const timer = setInterval(() => {
|
|
154
|
+
const secs = Math.floor((Date.now() - startedAt) / 1000)
|
|
155
|
+
const live = activity ? activity() : ''
|
|
156
|
+
let stage = phases[0].text
|
|
157
|
+
if (!live) {
|
|
158
|
+
for (const p of phases) {
|
|
159
|
+
if (secs >= p.atSec) stage = p.text
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const pct = Math.min(Math.floor((secs / total) * 100), 99)
|
|
163
|
+
const filled = '█'.repeat(Math.floor(pct / 10))
|
|
164
|
+
const bar = secs < 3 ? '' : `\n${filled}${'░'.repeat(10 - filled.length)} ${String(pct).padStart(2)}%`
|
|
165
|
+
const remain = Math.max(total - secs, 0)
|
|
166
|
+
const remainTxt = secs < 3 ? '' : ` · 预计还剩 ${Math.floor(remain / 60)}分${remain % 60}秒`
|
|
167
|
+
const egg = secs >= eggAfterSec && eggs.length > 0
|
|
168
|
+
? `\n${eggs[Math.floor(secs / 60) % eggs.length]}`
|
|
169
|
+
: ''
|
|
170
|
+
const phase = getStreamPhase ? getStreamPhase() : 'idle'
|
|
171
|
+
const emojiPool = phase === 'reasoning'
|
|
172
|
+
? reasoningSpin
|
|
173
|
+
: phase === 'outputting'
|
|
174
|
+
? outputSpin
|
|
175
|
+
: spin
|
|
176
|
+
const emoji = emojiPool[i % emojiPool.length]
|
|
177
|
+
i++
|
|
178
|
+
const status = live || stage
|
|
179
|
+
ws.replyStream(frame, streamId, `${emoji} ${status} ⏱ ${secs} 秒${remainTxt}${bar}${egg}`, false).catch(() => {})
|
|
180
|
+
}, intervalMs)
|
|
181
|
+
return () => clearInterval(timer)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 最终回复: 原流失败(如 WeCom 10 分钟过期)时用新流重试一次 */
|
|
185
|
+
export async function sendFinal(ws, frame, streamId, content) {
|
|
186
|
+
try {
|
|
187
|
+
await ws.replyStream(frame, streamId, content, true)
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error(`[im-bridge] 原流最终回复失败(${e.message}), 尝试新流...`)
|
|
190
|
+
await ws.replyStream(frame, generateReqId('stream'), content, true)
|
|
191
|
+
}
|
|
192
|
+
}
|