dsh-dock 0.19.0 → 0.21.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 +21 -20
- package/client.js +1426 -931
- package/features/animation/view.jsx +26 -12
- package/features/balance/view.js +8 -6
- package/features/mobile/host.js +18 -311
- package/features/mobile/view.js +295 -480
- package/features/mobile-relay/host.js +3 -4
- package/features/mobile-relay/view.jsx +1 -2
- package/features/notify/host.js +52 -16
- package/features/notify/view.jsx +884 -377
- package/features/runstate/host.js +1 -11
- package/features/runstate/view.jsx +32 -15
- package/features/tokenlog/view.jsx +8 -3
- package/index.js +1 -1
- package/package.json +2 -2
- package/screenshots/dock-notify.png +0 -0
- package/src/client.jsx +36 -6
- package/src/host-core.js +8 -1
- package/src/shared.js +86 -0
- package/src/task-track.js +87 -0
|
@@ -125,10 +125,9 @@ function removeRemotePatches(list) {
|
|
|
125
125
|
// 回环/安全上下文下 randomUUID 原生存在,脚本自我短路零副作用。
|
|
126
126
|
const LAN_COMPAT_JS = "(function(){var g=typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:(typeof self!=='undefined'?self:undefined));if(!g)return;if(!g.crypto){try{g.crypto={}}catch(e){return}}var c=g.crypto;var nativeRng=typeof c.getRandomValues==='function'?c.getRandomValues.bind(c):null;function rng(bytes){if(nativeRng){nativeRng(bytes);return bytes}for(var i=0;i<bytes.length;i++)bytes[i]=Math.floor(Math.random()*256)&255;return bytes}function uuid(){var b=rng(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;var h=[];for(var i=0;i<16;i++)h.push((b[i]+256).toString(16).slice(1));return h.slice(0,4).join('')+'-'+h.slice(4,6).join('')+'-'+h.slice(6,8).join('')+'-'+h.slice(8,10).join('')+'-'+h.slice(10).join('')}function fill(array){rng(array);return array}function define(obj,name,value){if(!obj||typeof obj[name]==='function')return;try{Object.defineProperty(obj,name,{value:value,configurable:true})}catch(e){try{obj[name]=value}catch(e2){}}}define(c,'randomUUID',uuid);if(!nativeRng)define(c,'getRandomValues',fill);define(g.Crypto&&g.Crypto.prototype||null,'randomUUID',uuid);if(!nativeRng)define(g.Crypto&&g.Crypto.prototype||null,'getRandomValues',fill);if(typeof c.randomUUID!=='function'){var fresh={getRandomValues:fill,randomUUID:uuid};if(c.subtle)fresh.subtle=c.subtle;var keyOrigin=Object.create(null);for(var k in c){try{keyOrigin[k]=c[k]}catch(e3){}}for(var k2 in keyOrigin){if(typeof fresh[k2]==='undefined')fresh[k2]=keyOrigin[k2]}try{Object.defineProperty(g,'crypto',{value:fresh,configurable:true})}catch(e4){try{g.crypto=fresh}catch(e5){}}}})()"
|
|
127
127
|
const LAN_COMPAT_MARKER = 'data-dsh-lan-compat'
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// 访问时缺该 API,DSH 客户端引导会整片失败)。
|
|
128
|
+
// 远程访问只注入 LAN 非安全上下文的 crypto.randomUUID 兜底(远程设备经
|
|
129
|
+
// http://<局域网IP> 访问时缺该 API,DSH 客户端引导会整片失败)。
|
|
130
|
+
// 手机端排版/行为(旧【手机适配】)已整体移除,本模块不再注入任何窄屏 chrome。
|
|
132
131
|
/** 把 LAN 兼容脚本内联注入 index.html 的 <head> 之后(幂等)。 */
|
|
133
132
|
function injectLanCompat(html) {
|
|
134
133
|
let out = html
|
|
@@ -159,8 +159,7 @@ export function MobileRelayView() {
|
|
|
159
159
|
* 【远程访问】不再挂 Overlay:「预览版说明」自动确认已迁到插件级
|
|
160
160
|
* (src/welcome-ack.js 经 index.js 注册 tapIndex,不随任何功能开关启停)——
|
|
161
161
|
* 关掉远程访问、主实例直接绑 0.0.0.0 的直连形态同样每次重弹,归属不能在本模块。
|
|
162
|
-
*
|
|
163
|
-
* (features/mobile/view.js + features/mobile/host.js)。远程访问只负责「远程入口」本身。
|
|
162
|
+
* 手机端排版/行为(旧【手机适配】)已整体移除,远程访问只负责「远程入口」本身。
|
|
164
163
|
*/
|
|
165
164
|
|
|
166
165
|
export function MobileRelayHomeStat() {
|
package/features/notify/host.js
CHANGED
|
@@ -37,7 +37,8 @@ function defaultConfig() {
|
|
|
37
37
|
dingtalkWebhook: '',
|
|
38
38
|
feishuEnabled: false,
|
|
39
39
|
feishuWebhook: '',
|
|
40
|
-
// 语音播报(浏览器 speechSynthesis
|
|
40
|
+
// 语音播报(浏览器 speechSynthesis 短句播报):服务端总开关,默认关闭。
|
|
41
|
+
// 客户端另有按浏览器保存的「本机播报」开关;服务端开 + 本机开才出声。
|
|
41
42
|
voiceNotify: false,
|
|
42
43
|
voiceMode: 'brief',
|
|
43
44
|
voiceOnComplete: true,
|
|
@@ -49,6 +50,11 @@ function defaultConfig() {
|
|
|
49
50
|
voiceLang: '',
|
|
50
51
|
// 指定音色(speechSynthesis voiceURI,设备相关;空 = 按语言自动挑,本机没有时客户端回落)
|
|
51
52
|
voiceURI: '',
|
|
53
|
+
// 事件 × 渠道矩阵(扁平键 card:complete / sound:error / system:confirm / ...;
|
|
54
|
+
// 未设置的键回退旧字段,升级不改变既有行为)
|
|
55
|
+
eventMatrix: {},
|
|
56
|
+
// 群机器人消息口径三挡:brief / medium / detailed
|
|
57
|
+
pushMode: 'detailed',
|
|
52
58
|
// 迁移标记(schema 默认 false):settings 不可用时的兜底取 true——迁移由
|
|
53
59
|
// src/host-core.js migrateNotifyConfig 统一负责,这里只是原样带回,不影响通知行为。
|
|
54
60
|
migratedFromAnimation: true,
|
|
@@ -68,11 +74,21 @@ function readConfig(ctx) {
|
|
|
68
74
|
if (!SOUND_EFFECTS.includes(cfg.soundEffect)) cfg.soundEffect = 'chime'
|
|
69
75
|
if (cfg.voiceMode !== 'brief' && cfg.voiceMode !== 'medium' && cfg.voiceMode !== 'detailed') cfg.voiceMode = 'brief'
|
|
70
76
|
if (cfg.cardMode !== 'brief' && cfg.cardMode !== 'medium' && cfg.cardMode !== 'detailed') cfg.cardMode = 'detailed'
|
|
77
|
+
if (cfg.pushMode !== 'brief' && cfg.pushMode !== 'medium' && cfg.pushMode !== 'detailed') cfg.pushMode = 'detailed'
|
|
78
|
+
if (!cfg.eventMatrix || typeof cfg.eventMatrix !== 'object' || Array.isArray(cfg.eventMatrix)) cfg.eventMatrix = {}
|
|
71
79
|
}
|
|
72
80
|
} catch { /* settings 未挂载,用默认 */ }
|
|
73
81
|
return cfg
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
/** 事件 × 渠道开关:矩阵里显式设置过就用它,否则回退到旧字段(fallback)。 */
|
|
85
|
+
function matrixEnabled(cfg, channel, event, fallback) {
|
|
86
|
+
const key = channel + ':' + event
|
|
87
|
+
const matrix = cfg && cfg.eventMatrix
|
|
88
|
+
if (matrix && typeof matrix === 'object' && typeof matrix[key] === 'boolean') return matrix[key]
|
|
89
|
+
return fallback !== false
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
function truncate(str, max = 160) {
|
|
77
93
|
if (!str) return ''
|
|
78
94
|
return String(str).length > max ? String(str).slice(0, max) + '…' : str
|
|
@@ -105,19 +121,23 @@ const END_REASON_LABELS = {
|
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
// 钉钉与飞书共用的消息正文(各端再按各自 markdown 方言拼接)
|
|
108
|
-
|
|
124
|
+
// mode: brief = 任务与结果;medium = 加模型/耗时;detailed = 再加回合、Token 与摘要。
|
|
125
|
+
function buildMessageLines(t, mode) {
|
|
109
126
|
const success = !t.endReason || t.endReason === 'completed'
|
|
110
127
|
const reasonLabel = END_REASON_LABELS[t.endReason] || '✅ 完成'
|
|
111
128
|
const models = t.models.length > 0 ? t.models.join(', ') : '未知'
|
|
129
|
+
const tone = mode === 'brief' || mode === 'medium' ? mode : 'detailed'
|
|
112
130
|
const lines = []
|
|
113
131
|
lines.push(`### ${success ? '✅' : '📢'} dsh 任务${success ? '完成' : '结束'}`)
|
|
114
132
|
lines.push(`**任务**: ${t.title}`)
|
|
115
|
-
lines.push(`**模型**: ${models}${t.provider ? `(${t.provider})` : ''}`)
|
|
116
|
-
lines.push(`**耗时**: ${fmtDurCn(t.duration)}
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
if (tone !== 'brief') lines.push(`**模型**: ${models}${t.provider ? `(${t.provider})` : ''}`)
|
|
134
|
+
lines.push(`**耗时**: ${fmtDurCn(t.duration)}${tone === 'brief' ? '' : `(${fmtClockOf(t.startTime)} → ${fmtClockOf(t.endTime)})`}`)
|
|
135
|
+
if (tone === 'detailed') {
|
|
136
|
+
lines.push(`**回合**: ${t.turns} · 步骤 ${t.steps}${t.toolCalls ? ` · 工具 ${t.toolCalls} 次` : ''}`)
|
|
137
|
+
lines.push(`**Token**: 输入 ${t.inputTokens.toLocaleString()} / 输出 ${t.outputTokens.toLocaleString()} / 总计 ${t.totalTokens.toLocaleString()}`)
|
|
138
|
+
}
|
|
119
139
|
lines.push(`**结果**: ${reasonLabel}${t.errorMessage ? `:${truncate(t.errorMessage, 120)}` : ''}`)
|
|
120
|
-
if (t.lastText) {
|
|
140
|
+
if (tone !== 'brief' && t.lastText) {
|
|
121
141
|
lines.push(`> ${truncate(t.lastText, 200)}`)
|
|
122
142
|
}
|
|
123
143
|
lines.push('---')
|
|
@@ -129,8 +149,8 @@ function buildMessageLines(t) {
|
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
function buildDingtalkMessage(t) {
|
|
133
|
-
const { title, lines } = buildMessageLines(t)
|
|
152
|
+
function buildDingtalkMessage(t, mode) {
|
|
153
|
+
const { title, lines } = buildMessageLines(t, mode)
|
|
134
154
|
return {
|
|
135
155
|
title,
|
|
136
156
|
// 钉钉 markdown 不渲染单个 \n,行之间必须用空行分隔才能换行
|
|
@@ -139,8 +159,8 @@ function buildDingtalkMessage(t) {
|
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
// 飞书 interactive 卡片:标题进 header,正文走 lark_md(支持 **加粗** 与 \n 换行,不支持 ### 标题)
|
|
142
|
-
function buildFeishuMessage(t) {
|
|
143
|
-
const { title, success, lines } = buildMessageLines(t)
|
|
162
|
+
function buildFeishuMessage(t, mode) {
|
|
163
|
+
const { title, success, lines } = buildMessageLines(t, mode)
|
|
144
164
|
const body = lines.filter((l) => !l.startsWith('### ')).join('\n')
|
|
145
165
|
return {
|
|
146
166
|
title,
|
|
@@ -222,14 +242,15 @@ export const feature = {
|
|
|
222
242
|
disposers.push(() => lease.release())
|
|
223
243
|
const tracker = lease.tracker
|
|
224
244
|
|
|
225
|
-
// =====
|
|
245
|
+
// ===== 群机器人推送(宿主侧直发,异步不阻塞;事件筛选走 eventMatrix,缺省回退 notifyOn*) =====
|
|
226
246
|
async function pushDingtalkIfNeeded(record) {
|
|
227
247
|
try {
|
|
228
248
|
const cfg = readConfig(ctx)
|
|
229
249
|
if (!cfg.dingtalkEnabled || !cfg.dingtalkWebhook) return
|
|
230
250
|
const success = !record.endReason || record.endReason === 'completed'
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
const event = success ? 'complete' : 'error'
|
|
252
|
+
if (!matrixEnabled(cfg, 'dingtalk', event, success ? cfg.notifyOnComplete : cfg.notifyOnError)) return
|
|
253
|
+
const { title, text } = buildDingtalkMessage(record, cfg.pushMode)
|
|
233
254
|
const r = await sendDingtalk(cfg.dingtalkWebhook, title, text)
|
|
234
255
|
if (r.ok) console.log('[dsh-dock] notify dingtalk pushed:', title)
|
|
235
256
|
else console.error('[dsh-dock] notify dingtalk push failed:', r.error)
|
|
@@ -243,8 +264,9 @@ export const feature = {
|
|
|
243
264
|
const cfg = readConfig(ctx)
|
|
244
265
|
if (!cfg.feishuEnabled || !cfg.feishuWebhook) return
|
|
245
266
|
const success = !record.endReason || record.endReason === 'completed'
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
const event = success ? 'complete' : 'error'
|
|
268
|
+
if (!matrixEnabled(cfg, 'feishu', event, success ? cfg.notifyOnComplete : cfg.notifyOnError)) return
|
|
269
|
+
const { title, card } = buildFeishuMessage(record, cfg.pushMode)
|
|
248
270
|
const r = await sendFeishu(cfg.feishuWebhook, title, card)
|
|
249
271
|
if (r.ok) console.log('[dsh-dock] notify feishu pushed:', title)
|
|
250
272
|
else console.error('[dsh-dock] notify feishu push failed:', r.error)
|
|
@@ -297,6 +319,20 @@ export const feature = {
|
|
|
297
319
|
if (typeof p.systemNotify === 'boolean') cfg.systemNotify = p.systemNotify
|
|
298
320
|
if (typeof p.soundNotify === 'boolean') cfg.soundNotify = p.soundNotify
|
|
299
321
|
if (typeof p.soundEffect === 'string' && SOUND_EFFECTS.includes(p.soundEffect)) cfg.soundEffect = p.soundEffect
|
|
322
|
+
// 群机器人消息口径三挡
|
|
323
|
+
if (typeof p.pushMode === 'string' && (p.pushMode === 'brief' || p.pushMode === 'medium' || p.pushMode === 'detailed')) {
|
|
324
|
+
cfg.pushMode = p.pushMode
|
|
325
|
+
}
|
|
326
|
+
// 事件 × 渠道矩阵:只接受白名单键 + 布尔值,合并写回
|
|
327
|
+
if (p.eventMatrix && typeof p.eventMatrix === 'object' && !Array.isArray(p.eventMatrix)) {
|
|
328
|
+
const matrix = Object.assign({}, cfg.eventMatrix || {})
|
|
329
|
+
for (const [key, value] of Object.entries(p.eventMatrix)) {
|
|
330
|
+
if (typeof value === 'boolean' && /^(card|sound|system|voice|dingtalk|feishu):(complete|error|confirm|stuck)$/.test(key)) {
|
|
331
|
+
matrix[key] = value
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
cfg.eventMatrix = matrix
|
|
335
|
+
}
|
|
300
336
|
if (typeof p.dingtalkEnabled === 'boolean') cfg.dingtalkEnabled = p.dingtalkEnabled
|
|
301
337
|
if (typeof p.dingtalkWebhook === 'string') {
|
|
302
338
|
const hook = p.dingtalkWebhook.trim()
|