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.
@@ -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
- // 手机端排版/行为补丁已迁到【手机适配】模块(features/mobile/host.js)——
129
- // 远程访问只保留「账号密码登录的远程入口」本身,窄屏排版与底部 Tab 栏归手机适配。
130
- // 这里只留 LAN 非安全上下文的 crypto.randomUUID 兜底(远程设备经 http://<局域网IP>
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
- * 手机端 chrome(底部 Tab 栏 / 抽屉遮罩 / 面板互斥)归【手机适配】
163
- * (features/mobile/view.js + features/mobile/host.js)。远程访问只负责「远程入口」本身。
162
+ * 手机端排版/行为(旧【手机适配】)已整体移除,远程访问只负责「远程入口」本身。
164
163
  */
165
164
 
166
165
  export function MobileRelayHomeStat() {
@@ -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
- function buildMessageLines(t) {
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)}(${fmtClockOf(t.startTime)} → ${fmtClockOf(t.endTime)})`)
117
- lines.push(`**回合**: ${t.turns} · 步骤 ${t.steps}${t.toolCalls ? ` · 工具 ${t.toolCalls} 次` : ''}`)
118
- lines.push(`**Token**: 输入 ${t.inputTokens.toLocaleString()} / 输出 ${t.outputTokens.toLocaleString()} / 总计 ${t.totalTokens.toLocaleString()}`)
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
- // ===== 群机器人推送(宿主侧直发,异步不阻塞;事件筛选跟随 notifyOnComplete/notifyOnError) =====
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
- if (success ? !cfg.notifyOnComplete : !cfg.notifyOnError) return
232
- const { title, text } = buildDingtalkMessage(record)
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
- if (success ? !cfg.notifyOnComplete : !cfg.notifyOnError) return
247
- const { title, card } = buildFeishuMessage(record)
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()