dsh-dock 0.5.0 → 0.6.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/README.md +11 -9
- package/client.js +632 -352
- package/features/animation/host.js +150 -24
- package/features/animation/view.jsx +311 -131
- package/features/modelconfig/view.js +55 -14
- package/features/theme/view.js +35 -10
- package/features/tokenlog/view.jsx +3 -1
- package/features/visionproxy/host.js +24 -10
- package/package.json +2 -2
- package/src/client.jsx +20 -14
- package/src/host-core.js +3 -0
|
@@ -28,6 +28,8 @@ function defaultConfig() {
|
|
|
28
28
|
soundEffect: 'chime',
|
|
29
29
|
dingtalkEnabled: false,
|
|
30
30
|
dingtalkWebhook: '',
|
|
31
|
+
feishuEnabled: false,
|
|
32
|
+
feishuWebhook: '',
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -133,6 +135,10 @@ export const feature = {
|
|
|
133
135
|
phase: 'think',
|
|
134
136
|
phaseAt: Date.now(),
|
|
135
137
|
lastActivityAt: Date.now(),
|
|
138
|
+
// assistant/chunk 是实时流;用轻量活动计数给浏览器侧映射动作速度,
|
|
139
|
+
// 不等最终 usage 落盘,避免任务正在输出时机器人仍慢速。
|
|
140
|
+
motionTicks: 0,
|
|
141
|
+
lastMotionAt: Date.now(),
|
|
136
142
|
...extra,
|
|
137
143
|
}
|
|
138
144
|
activeSessions.set(sid, record)
|
|
@@ -177,8 +183,9 @@ export const feature = {
|
|
|
177
183
|
}
|
|
178
184
|
completedSessions.unshift(record)
|
|
179
185
|
if (completedSessions.length > MAX_COMPLETED) completedSessions.pop()
|
|
180
|
-
//
|
|
186
|
+
// 钉钉/飞书群机器人推送(宿主侧直发,异步不阻塞;事件筛选跟随 notifyOnComplete/notifyOnError)
|
|
181
187
|
pushDingtalkIfNeeded(record)
|
|
188
|
+
pushFeishuIfNeeded(record)
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
// ===== 钉钉群机器人推送(参照 @wycto/dsh-task-pulse 同款 markdown 消息) =====
|
|
@@ -208,13 +215,13 @@ export const feature = {
|
|
|
208
215
|
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${fmtClockOf(ts)}`
|
|
209
216
|
}
|
|
210
217
|
|
|
211
|
-
|
|
218
|
+
// 钉钉与飞书共用的消息正文(各端再按各自 markdown 方言拼接)
|
|
219
|
+
function buildMessageLines(t) {
|
|
212
220
|
const success = !t.endReason || t.endReason === 'completed'
|
|
213
221
|
const reasonLabel = END_REASON_LABELS[t.endReason] || '✅ 完成'
|
|
214
222
|
const models = t.models.length > 0 ? t.models.join(', ') : '未知'
|
|
215
223
|
const lines = []
|
|
216
224
|
lines.push(`### ${success ? '✅' : '📢'} dsh 任务${success ? '完成' : '结束'}`)
|
|
217
|
-
lines.push('')
|
|
218
225
|
lines.push(`**任务**: ${t.title}`)
|
|
219
226
|
lines.push(`**模型**: ${models}${t.provider ? `(${t.provider})` : ''}`)
|
|
220
227
|
lines.push(`**耗时**: ${fmtDurCn(t.duration)}(${fmtClockOf(t.startTime)} → ${fmtClockOf(t.endTime)})`)
|
|
@@ -222,15 +229,67 @@ export const feature = {
|
|
|
222
229
|
lines.push(`**Token**: 输入 ${t.inputTokens.toLocaleString()} / 输出 ${t.outputTokens.toLocaleString()} / 总计 ${t.totalTokens.toLocaleString()}`)
|
|
223
230
|
lines.push(`**结果**: ${reasonLabel}${t.errorMessage ? `:${truncate(t.errorMessage, 120)}` : ''}`)
|
|
224
231
|
if (t.lastText) {
|
|
225
|
-
lines.push('')
|
|
226
232
|
lines.push(`> ${truncate(t.lastText, 200)}`)
|
|
227
233
|
}
|
|
228
|
-
lines.push('')
|
|
229
234
|
lines.push('---')
|
|
230
235
|
lines.push(`*由 dsh-dock 任务动画发送 · ${fmtDateClockOf(t.endTime)}*`)
|
|
231
236
|
return {
|
|
232
237
|
title: `dsh 任务${success ? '完成' : '结束'}:${truncate(t.title, 40)}`,
|
|
233
|
-
|
|
238
|
+
success,
|
|
239
|
+
lines,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildDingtalkMessage(t) {
|
|
244
|
+
const { title, lines } = buildMessageLines(t)
|
|
245
|
+
return {
|
|
246
|
+
title,
|
|
247
|
+
// 钉钉 markdown 不渲染单个 \n,行之间必须用空行分隔才能换行
|
|
248
|
+
text: lines.join('\n\n'),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 飞书 interactive 卡片:标题进 header,正文走 lark_md(支持 **加粗** 与 \n 换行,不支持 ### 标题)
|
|
253
|
+
function buildFeishuMessage(t) {
|
|
254
|
+
const { title, success, lines } = buildMessageLines(t)
|
|
255
|
+
const body = lines.filter((l) => !l.startsWith('### ')).join('\n')
|
|
256
|
+
return {
|
|
257
|
+
title,
|
|
258
|
+
card: {
|
|
259
|
+
config: { wide_screen_mode: true },
|
|
260
|
+
header: {
|
|
261
|
+
template: success ? 'green' : 'orange',
|
|
262
|
+
title: { tag: 'plain_text', content: title },
|
|
263
|
+
},
|
|
264
|
+
elements: [
|
|
265
|
+
{ tag: 'div', text: { tag: 'lark_md', content: body } },
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 发送飞书 interactive 卡片;成功 = HTTP 200 且业务码 code/StatusCode === 0
|
|
272
|
+
async function sendFeishu(webhook, title, card) {
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch(webhook, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({ msg_type: 'interactive', card }),
|
|
278
|
+
})
|
|
279
|
+
const raw = await res.text()
|
|
280
|
+
let data = null
|
|
281
|
+
try { data = JSON.parse(raw) } catch { /* 非 JSON 响应体 */ }
|
|
282
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} ${truncate(raw, 120)}` }
|
|
283
|
+
// 新版返回 { code, msg },旧版返回 { StatusCode, StatusMessage }
|
|
284
|
+
const code = data && typeof data === 'object'
|
|
285
|
+
? (typeof data.code === 'number' ? data.code : data.StatusCode)
|
|
286
|
+
: undefined
|
|
287
|
+
if (typeof code === 'number' && code !== 0) {
|
|
288
|
+
return { ok: false, error: `飞书 code ${code}:${data.msg || data.StatusMessage || ''}` }
|
|
289
|
+
}
|
|
290
|
+
return { ok: true }
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { ok: false, error: (e && e.message) || String(e) }
|
|
234
293
|
}
|
|
235
294
|
}
|
|
236
295
|
|
|
@@ -270,6 +329,21 @@ export const feature = {
|
|
|
270
329
|
}
|
|
271
330
|
}
|
|
272
331
|
|
|
332
|
+
async function pushFeishuIfNeeded(record) {
|
|
333
|
+
try {
|
|
334
|
+
const cfg = readConfig(ctx)
|
|
335
|
+
if (!cfg.feishuEnabled || !cfg.feishuWebhook) return
|
|
336
|
+
const success = !record.endReason || record.endReason === 'completed'
|
|
337
|
+
if (success ? !cfg.notifyOnComplete : !cfg.notifyOnError) return
|
|
338
|
+
const { title, card } = buildFeishuMessage(record)
|
|
339
|
+
const r = await sendFeishu(cfg.feishuWebhook, title, card)
|
|
340
|
+
if (r.ok) console.log('[dsh-dock] animation feishu pushed:', title)
|
|
341
|
+
else console.error('[dsh-dock] animation feishu push failed:', r.error)
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.error('[dsh-dock] animation feishu push error:', e && e.message)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
273
347
|
function handleSessionEvent(sessionQuery, session, event) {
|
|
274
348
|
const sid = typeof session === 'object' && session ? (session.id || '') : String(session || '')
|
|
275
349
|
if (!sid || !event || !event.type) return
|
|
@@ -280,6 +354,8 @@ export const feature = {
|
|
|
280
354
|
s.phase = 'think' // 新步骤:模型正在推理下一步
|
|
281
355
|
s.phaseAt = event.time || Date.now()
|
|
282
356
|
s.lastActivityAt = s.phaseAt
|
|
357
|
+
s.motionTicks += 2
|
|
358
|
+
s.lastMotionAt = s.phaseAt
|
|
283
359
|
if (event.data && event.data.turn !== undefined) {
|
|
284
360
|
s.turns = Math.max(s.turns, num(event.data.turn) + 1)
|
|
285
361
|
}
|
|
@@ -290,7 +366,8 @@ export const feature = {
|
|
|
290
366
|
const now = event.time || Date.now()
|
|
291
367
|
// 流级阶段:chunk 高频到达(每 token 一条),只做最廉价的阶段切换
|
|
292
368
|
if (event.type === 'assistant/chunk') {
|
|
293
|
-
const
|
|
369
|
+
const chunk = event.data && event.data.chunk
|
|
370
|
+
const ctype = chunk && chunk.type
|
|
294
371
|
if (ctype === 'reasoning-delta') {
|
|
295
372
|
s.phase = 'think'
|
|
296
373
|
s.phaseAt = now
|
|
@@ -298,6 +375,14 @@ export const feature = {
|
|
|
298
375
|
s.phase = 'write'
|
|
299
376
|
s.phaseAt = now
|
|
300
377
|
}
|
|
378
|
+
// 不同适配器的 delta 字段名并不完全一致。按可见文本量估算,
|
|
379
|
+
// 无文本时每个 chunk 也记一个脉冲,保证推理流同样有动作反馈。
|
|
380
|
+
if (ctype === 'reasoning-delta' || ctype === 'text-delta') {
|
|
381
|
+
const delta = chunk && (chunk.text || chunk.delta || chunk.content)
|
|
382
|
+
const units = typeof delta === 'string' ? Math.max(1, Math.ceil(delta.length / 24)) : 1
|
|
383
|
+
s.motionTicks += units
|
|
384
|
+
s.lastMotionAt = now
|
|
385
|
+
}
|
|
301
386
|
s.lastActivityAt = now
|
|
302
387
|
return
|
|
303
388
|
}
|
|
@@ -312,6 +397,9 @@ export const feature = {
|
|
|
312
397
|
s.toolCalls++
|
|
313
398
|
s.phase = phaseOfToolName(event.data && event.data.name)
|
|
314
399
|
s.phaseAt = now
|
|
400
|
+
// 工具启动是稀疏事件,给一个更明显的短促动作,检索/写文件也不会显得停住。
|
|
401
|
+
s.motionTicks += s.phase === 'search' ? 7 : 9
|
|
402
|
+
s.lastMotionAt = now
|
|
315
403
|
} else if (event.type === 'user/message') {
|
|
316
404
|
if (!s.firstPrompt && event.data && event.data.content) {
|
|
317
405
|
const text = extractText(event.data.content)
|
|
@@ -416,6 +504,8 @@ export const feature = {
|
|
|
416
504
|
phase: s.phase,
|
|
417
505
|
phaseAt: s.phaseAt,
|
|
418
506
|
lastActivityAt: s.lastActivityAt,
|
|
507
|
+
motionTicks: s.motionTicks,
|
|
508
|
+
lastMotionAt: s.lastMotionAt,
|
|
419
509
|
})
|
|
420
510
|
}
|
|
421
511
|
return sendJson(res, 200, {
|
|
@@ -457,6 +547,16 @@ export const feature = {
|
|
|
457
547
|
}
|
|
458
548
|
cfg.dingtalkWebhook = hook
|
|
459
549
|
}
|
|
550
|
+
if (typeof p.feishuEnabled === 'boolean') cfg.feishuEnabled = p.feishuEnabled
|
|
551
|
+
if (typeof p.feishuWebhook === 'string') {
|
|
552
|
+
const hook = p.feishuWebhook.trim()
|
|
553
|
+
if (hook && !/^https?:\/\//i.test(hook)) {
|
|
554
|
+
const err = new Error('飞书 Webhook 地址需以 http(s):// 开头')
|
|
555
|
+
err.statusCode = 400
|
|
556
|
+
throw err
|
|
557
|
+
}
|
|
558
|
+
cfg.feishuWebhook = hook
|
|
559
|
+
}
|
|
460
560
|
|
|
461
561
|
const settings = ctx.get('settings')
|
|
462
562
|
if (!settings || typeof settings.mutate !== 'function') {
|
|
@@ -473,28 +573,54 @@ export const feature = {
|
|
|
473
573
|
return sendJson(res, 200, { ok: true, data: { config: cfg, savedAt: Date.now() } })
|
|
474
574
|
}
|
|
475
575
|
|
|
476
|
-
//
|
|
576
|
+
// 机器人连通性测试:payload.target 指定 'dingtalk'(默认)或 'feishu',用当前已保存的 Webhook 发一条测试消息
|
|
477
577
|
if (method === 'test') {
|
|
578
|
+
const target = (payload && payload.target) === 'feishu' ? 'feishu' : 'dingtalk'
|
|
478
579
|
const cfgNow = readConfig(ctx)
|
|
479
|
-
|
|
480
|
-
|
|
580
|
+
const webhook = target === 'feishu' ? cfgNow.feishuWebhook : cfgNow.dingtalkWebhook
|
|
581
|
+
if (!webhook) {
|
|
582
|
+
const err = new Error(`请先填写并保存${target === 'feishu' ? '飞书' : '钉钉'} Webhook 地址`)
|
|
481
583
|
err.statusCode = 400
|
|
482
584
|
throw err
|
|
483
585
|
}
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
586
|
+
const nameCn = target === 'feishu' ? '飞书' : '钉钉'
|
|
587
|
+
let r
|
|
588
|
+
if (target === 'feishu') {
|
|
589
|
+
const text = [
|
|
590
|
+
'**这是一条来自 dsh-dock 任务动画的测试消息。**',
|
|
591
|
+
'',
|
|
592
|
+
`**时间**: ${fmtDateClockOf(Date.now())}`,
|
|
593
|
+
'',
|
|
594
|
+
'看到这条消息说明飞书 Webhook 配置正确。',
|
|
595
|
+
'',
|
|
596
|
+
'---',
|
|
597
|
+
'*由 dsh-dock 任务动画发送*',
|
|
598
|
+
].join('\n')
|
|
599
|
+
const card = {
|
|
600
|
+
config: { wide_screen_mode: true },
|
|
601
|
+
header: {
|
|
602
|
+
template: 'blue',
|
|
603
|
+
title: { tag: 'plain_text', content: '🧪 dsh 任务动画测试' },
|
|
604
|
+
},
|
|
605
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: text } }],
|
|
606
|
+
}
|
|
607
|
+
r = await sendFeishu(webhook, 'dsh 任务动画测试', card)
|
|
608
|
+
} else {
|
|
609
|
+
const text = [
|
|
610
|
+
'### 🧪 dsh 任务动画测试',
|
|
611
|
+
'',
|
|
612
|
+
'这是一条来自 dsh-dock 任务动画的测试消息。',
|
|
613
|
+
'',
|
|
614
|
+
`**时间**: ${fmtDateClockOf(Date.now())}`,
|
|
615
|
+
'',
|
|
616
|
+
'> 看到这条消息说明钉钉 Webhook 配置正确。',
|
|
617
|
+
'',
|
|
618
|
+
'---',
|
|
619
|
+
'*由 dsh-dock 任务动画发送*',
|
|
620
|
+
].join('\n')
|
|
621
|
+
r = await sendDingtalk(webhook, 'dsh 任务动画测试', text)
|
|
622
|
+
}
|
|
623
|
+
if (!r.ok) console.error(`[dsh-dock] animation ${target} test failed:`, r.error)
|
|
498
624
|
return sendJson(res, 200, { ok: true, data: { sent: r.ok, error: r.ok ? '' : r.error } })
|
|
499
625
|
}
|
|
500
626
|
|