dsh-in-hand 1.7.6 → 1.7.8
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 +29 -0
- package/lib/page.html +186 -19
- package/lib/sw.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -252,6 +252,35 @@ PDF / ZIP / MP4)。这是**按内容识别**,不是"按内容放行" ——
|
|
|
252
252
|
`scripts/check-mobile.mjs` 会在 390 / 360 两个宽度下跑 `window.twSelfCheck()`、切会话
|
|
253
253
|
残留断言与左基线断言。
|
|
254
254
|
|
|
255
|
+
### v1.7.7:坏图不再留空框、图标不再依赖字体
|
|
256
|
+
|
|
257
|
+
图片加载失败(宿主文件路由只服务 `~/.dsh/attachments` / `~/.dsh/outbox`,外面的路径
|
|
258
|
+
是 403;外域 URL 也可能不可达)时,v1.7.6 会铺一张 63px 的整宽 ERR 卡片,视觉上仍是一块
|
|
259
|
+
把正文截断的占位框。v1.7.7 换成 36px 单行提示条,并且页面级保证 DOM 里**不留破图 `img`**:
|
|
260
|
+
|
|
261
|
+
| 改前(63px ERR 占位卡) | 改后(36px 提示条,可点开原图) |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
|  |  |
|
|
264
|
+
|
|
265
|
+
会话抽屉里「子代理 N」入口的图标改成内联 SVG(不再用字符,真机上曾出现「廿」这种回退字形):
|
|
266
|
+
|
|
267
|
+

|
|
268
|
+
|
|
269
|
+
### v1.7.8:长会话不再整屏闪、附件信封不再露标签
|
|
270
|
+
|
|
271
|
+
图片的骨架流光用 `inset:0` 绝对定位,却忘了给 `.md-imgwrap` 定位,包含块一路退到**视口**。
|
|
272
|
+
长会话里总有还没进入视口的懒加载图,它们永远带着 `.loading`,于是整屏多出一条 1.35s
|
|
273
|
+
无限循环的亮带(用户原话「反复闪屏」),`translateX(55%)` 还把页面从 390 撑到 601。现在
|
|
274
|
+
流光被关进图片盒、不吃点击,离屏图也不再挂任何动画:
|
|
275
|
+
|
|
276
|
+
| 改前(整屏亮带,可横向滚到 601) | 改后(390,静止) |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
|  |  |
|
|
279
|
+
|
|
280
|
+
同一轮的第二个修复:DSH 给模型看的 `<path>…</path><type>image</type><content>…</content>`
|
|
281
|
+
信封,以前在手机上原样显示成 XML(某会话第一条消息整条都是它),现在渲染成图片卡,
|
|
282
|
+
拿不到图时仍是那条 36px 提示条。
|
|
283
|
+
|
|
255
284
|
### 页面为什么不做整页重建
|
|
256
285
|
|
|
257
286
|
页面每 3 秒轮询一次,但**数据没变就完全不碰 DOM**,只刷新右上角时间戳。首版是
|
package/lib/page.html
CHANGED
|
@@ -1078,11 +1078,32 @@ html,body{overflow-x:clip}
|
|
|
1078
1078
|
手机上「看结果」主要就是看图、看文档:图铺满气泡宽度、点开能放大;
|
|
1079
1079
|
文档没法内嵌预览,至少把名字 / 大小 / 类型说清楚,再给一个能点开的入口。
|
|
1080
1080
|
点击目标都不小于 44px —— 这是手机页既定的底线。 */
|
|
1081
|
-
.
|
|
1082
|
-
|
|
1083
|
-
|
|
1081
|
+
/* v1.7.7:图片盒子的三条硬约束 ——
|
|
1082
|
+
① 宽度永远不超过气泡(max-width + height:auto,横图竖图都不撑宽);
|
|
1083
|
+
② min-height:0:没拿到 intrinsic size 之前**不占位撑高**,加载完再长出来,
|
|
1084
|
+
避免"先铺一块 44px 空气、再跳一下"的布局抖动;
|
|
1085
|
+
③ 圆角与边框在图片盒子上,加载失败换成提示条时形状/位置不变。 */
|
|
1086
|
+
.md-imgwrap{display:block;position:relative;width:100%;max-width:100%;min-height:0;padding:0;margin:var(--sp-2) 0;border:0;
|
|
1087
|
+
background:none;text-align:left;border-radius:var(--dsw-radius-md);overflow:hidden}
|
|
1088
|
+
.md-img{display:block;width:100%;max-width:100%;height:auto;min-height:0;border:1px solid var(--line);
|
|
1084
1089
|
border-radius:var(--dsw-radius-md);background:var(--panel2)}
|
|
1085
1090
|
.md-imgwrap:active .md-img{opacity:.86}
|
|
1091
|
+
/* v1.7.7:图片加载失败 → 一条 36px 的提示条(≤44px),可点打开原图 / 重试。
|
|
1092
|
+
绝不再留一个破图空框把正文截成两半;文字单行 ellipsis,不换行、不撑宽。 */
|
|
1093
|
+
.imgerr{display:flex;align-items:center;gap:var(--sp-2);width:100%;max-width:100%;min-width:0;
|
|
1094
|
+
height:36px;max-height:44px;margin:var(--sp-2) 0;padding:0 var(--sp-3);
|
|
1095
|
+
border:1px solid var(--line);border-radius:var(--dsw-radius-md);background:var(--panel2);
|
|
1096
|
+
color:var(--dim);font-size:var(--fs-2);line-height:1;text-align:left;text-decoration:none;overflow:hidden}
|
|
1097
|
+
.imgerr svg{width:16px;height:16px;flex:0 0 auto;fill:none;stroke:currentColor;
|
|
1098
|
+
stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
1099
|
+
.imgerr .t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1100
|
+
.imgerr:active{background:var(--panel);color:var(--fg)}
|
|
1101
|
+
/* 抽屉里的字符图标(+ / ›)现在都是内联 SVG:不再依赖字体里有没有那个字形。 */
|
|
1102
|
+
.pill .pico{width:16px;height:16px;flex:0 0 auto;color:var(--dim);fill:none;stroke:currentColor;
|
|
1103
|
+
stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
|
|
1104
|
+
.pill .pchev{width:12px;height:12px;flex:0 0 auto;fill:none;stroke:currentColor;
|
|
1105
|
+
stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
1106
|
+
.pill .hint{display:flex;align-items:center;gap:4px}
|
|
1086
1107
|
.filecard{display:flex;align-items:flex-start;gap:var(--sp-3);width:100%;min-width:0;min-height:52px;margin:var(--sp-2) 0;
|
|
1087
1108
|
padding:var(--sp-2) var(--sp-3);border:1px solid var(--line);border-radius:var(--dsw-radius-md);
|
|
1088
1109
|
background:var(--panel2);color:var(--fg);text-align:left;text-decoration:none}
|
|
@@ -1219,12 +1240,17 @@ html,body{overflow-x:clip}
|
|
|
1219
1240
|
@keyframes nudge{0%,100%{transform:translateX(0)}30%{transform:translateX(-2px)}70%{transform:translateX(2px)}}
|
|
1220
1241
|
|
|
1221
1242
|
/* 图片:先落一块纸纹,显影时轻轻放大一点。注意 .loading 是 JS **之后**才加的 ——
|
|
1222
|
-
JS 不跑时 .md-img 完全可见,绝不留隐形图。
|
|
1243
|
+
JS 不跑时 .md-img 完全可见,绝不留隐形图。
|
|
1244
|
+
2026-10-22(v1.7.8):.md-imgwrap 必须 position:relative —— 否则下面这条
|
|
1245
|
+
`inset:0` 的绝对定位流光会以**视口**为包含块:长会话里只要有一张懒加载图片
|
|
1246
|
+
还没进入视口,它就一直带着 .loading,于是整屏出现一条 1.35s 无限循环的亮带
|
|
1247
|
+
(用户报的"反复闪屏"),还把它自己的 translateX(55%) 算进文档宽度,
|
|
1248
|
+
把 390 的页面撑到 601(横向可滚)。同时流光绝不能吃点击。 */
|
|
1223
1249
|
.md-img{transition:opacity var(--mo-slow) var(--mo-ease),transform var(--mo-slow) var(--mo-ease)}
|
|
1224
1250
|
.md-imgwrap.loading .md-img{opacity:0;transform:scale(1.012)}
|
|
1225
1251
|
.md-imgwrap.done .md-img{opacity:1;transform:none}
|
|
1226
1252
|
.md-imgwrap.loading::after{
|
|
1227
|
-
content:'';position:absolute;inset:0;
|
|
1253
|
+
content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
|
|
1228
1254
|
background:linear-gradient(100deg,transparent 18%,var(--mo-ink) 42%,transparent 62%);
|
|
1229
1255
|
animation:sheen 1.35s var(--mo-ease) infinite;
|
|
1230
1256
|
}
|
|
@@ -2098,6 +2124,74 @@ function probePath(p){
|
|
|
2098
2124
|
} catch (e) { /* 忽略 */ }
|
|
2099
2125
|
return holder
|
|
2100
2126
|
}
|
|
2127
|
+
/** 破图提示条里的图标。走 createElementNS —— markdown 渲染段内不拼 HTML 字符串
|
|
2128
|
+
* (契约测试会扫这一段),所以这里也不破例。 */
|
|
2129
|
+
function brokenImgIconNode(){
|
|
2130
|
+
var NS = 'http://www.w3.org/2000/svg'
|
|
2131
|
+
var svg = document.createElementNS(NS, 'svg')
|
|
2132
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
2133
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
2134
|
+
// 相框 + 山形 + 一道斜杠:一眼读作"图裂了",不依赖任何字体字形。
|
|
2135
|
+
var ds = ['M4 5h16v14H4z', 'M4 16.5l4.5-4.5 3.5 3.5 3-3 4.5 4.5', 'M3.5 3.5l17 17']
|
|
2136
|
+
for (var i = 0; i < ds.length; i++) {
|
|
2137
|
+
var p = document.createElementNS(NS, 'path')
|
|
2138
|
+
p.setAttribute('d', ds[i])
|
|
2139
|
+
svg.appendChild(p)
|
|
2140
|
+
}
|
|
2141
|
+
return svg
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/** 图能不能"打开原图":只有同源路由 / http(s) 才给 <a>;data: / blob: 只能就地重试。 */
|
|
2145
|
+
function canReopenImage(url){
|
|
2146
|
+
var s = String(url || '')
|
|
2147
|
+
return /^https?:\/\//i.test(s) || s.charAt(0) === '/'
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/** 图片加载失败的兜底 UI(v1.7.7)。
|
|
2151
|
+
*
|
|
2152
|
+
* 以前这里换成一张整宽的 .filecard(63px、两行说明):视觉上仍是一块"占位框",
|
|
2153
|
+
* 把一段话从中间截断。现在统一成一条 **36px 单行提示条**:
|
|
2154
|
+
* · 能打开原地址的给 <a target=_blank>("图片未能加载 · 打开原图");
|
|
2155
|
+
* · data: / blob: 这类重开没意义的给 <button>,点一下就地重新加载。
|
|
2156
|
+
* 高度写死在 CSS(36px / max 44px),文字单行 ellipsis,所以不会再撑高正文。
|
|
2157
|
+
*/
|
|
2158
|
+
function brokenImgBar(url, alt){
|
|
2159
|
+
var reopen = canReopenImage(url)
|
|
2160
|
+
var el = document.createElement(reopen ? 'a' : 'button')
|
|
2161
|
+
el.className = 'imgerr'
|
|
2162
|
+
if (reopen) {
|
|
2163
|
+
el.href = url
|
|
2164
|
+
el.target = '_blank'
|
|
2165
|
+
el.rel = 'noopener noreferrer'
|
|
2166
|
+
} else {
|
|
2167
|
+
el.type = 'button'
|
|
2168
|
+
el.addEventListener('click', function(){
|
|
2169
|
+
if (!el.parentNode) return
|
|
2170
|
+
var fresh = imgNode(url, alt)
|
|
2171
|
+
el.parentNode.replaceChild(fresh, el)
|
|
2172
|
+
})
|
|
2173
|
+
}
|
|
2174
|
+
el.setAttribute('aria-label', (alt ? alt + ':' : '') + '图片未能加载,' + (reopen ? '点一下打开原图' : '点一下重试'))
|
|
2175
|
+
el.title = (alt ? alt + ' · ' : '') + (url || '')
|
|
2176
|
+
el.appendChild(brokenImgIconNode())
|
|
2177
|
+
var t = document.createElement('span')
|
|
2178
|
+
t.className = 't'
|
|
2179
|
+
t.textContent = '图片未能加载 · ' + (reopen ? '打开原图' : '点一下重试')
|
|
2180
|
+
el.appendChild(t)
|
|
2181
|
+
return el
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/** 把失败的 <img>(连同它的 .md-imgwrap)换成提示条。幂等:已被换掉就什么都不做。 */
|
|
2185
|
+
function swapBrokenImg(img, url, alt){
|
|
2186
|
+
if (!img || !img.parentNode) return false
|
|
2187
|
+
var wrap = img.closest ? img.closest('.md-imgwrap') : null
|
|
2188
|
+
if (wrap && !wrap.parentNode) return false // 已经换过一次了,别再动
|
|
2189
|
+
var target = wrap || img
|
|
2190
|
+
if (!target.parentNode) return false
|
|
2191
|
+
target.parentNode.replaceChild(brokenImgBar(url, alt), target)
|
|
2192
|
+
return true
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2101
2195
|
function imgNode(url, alt){
|
|
2102
2196
|
var wrap = document.createElement('button')
|
|
2103
2197
|
wrap.type = 'button'
|
|
@@ -2108,13 +2202,17 @@ function imgNode(url, alt){
|
|
|
2108
2202
|
img.loading = 'lazy'
|
|
2109
2203
|
img.decoding = 'async'
|
|
2110
2204
|
img.alt = alt || ''
|
|
2205
|
+
// 事件必须**先挂再赋 src**:同一条坏 URL 第二次出现时(浏览器已经记住这次失败),
|
|
2206
|
+
// error 可能在赋值那一刻就排队,挂晚了就再也收不到,页面上只留一个破图空框。
|
|
2207
|
+
img.addEventListener('error', function(){ swapBrokenImg(img, url, alt) })
|
|
2111
2208
|
img.src = url
|
|
2112
|
-
img.addEventListener('error', function(){
|
|
2113
|
-
if (!wrap.parentNode) return
|
|
2114
|
-
wrap.parentNode.replaceChild(brokenFileNode(alt || '图片'), wrap)
|
|
2115
|
-
})
|
|
2116
2209
|
wrap.appendChild(img)
|
|
2117
2210
|
wrap.addEventListener('click', function(){ openLightbox(url, alt) })
|
|
2211
|
+
// 兜底二:命中"失败缓存"时 complete 会立刻为 true 且 naturalWidth 还是 0,
|
|
2212
|
+
// 而那一次 error 可能已经错过 —— 追加进 DOM 之后补一刀(此时 wrap 已有父节点)。
|
|
2213
|
+
setTimeout(function(){
|
|
2214
|
+
if (img.complete && !img.naturalWidth) swapBrokenImg(img, url, alt)
|
|
2215
|
+
}, 0)
|
|
2118
2216
|
return wrap
|
|
2119
2217
|
}
|
|
2120
2218
|
function fileCard(url, label, kind){
|
|
@@ -2343,12 +2441,54 @@ function blocksInto(el, text){
|
|
|
2343
2441
|
return el
|
|
2344
2442
|
}
|
|
2345
2443
|
|
|
2444
|
+
/* DSH 把「用户贴了一个附件」序列化成一段**信封文本**给模型看:
|
|
2445
|
+
<path>C:\…\a.png</path><type>image</type><content>image/png image, 1398x1542 px, 437517 bytes</content>
|
|
2446
|
+
这段协议在手机上也原样显示过 —— 用户看到的是 XML 标签(2026-10-22 的
|
|
2447
|
+
「Token Router 说明书与印刷 PDF」会话,第一条消息整条都是它)。v1.7.8 解析成
|
|
2448
|
+
真正的图片 / 文件卡(同样走 imgNode 的失败兜底),文本里不再留标签。 */
|
|
2449
|
+
var ENVELOPE_RE = /<path>([\s\S]*?)<\/path>\s*<type>\s*(image|file)\s*<\/type>\s*<content>([\s\S]*?)<\/content>/g
|
|
2450
|
+
function splitAttachmentEnvelopes(text){
|
|
2451
|
+
var images = [], files = []
|
|
2452
|
+
var clean = String(text == null ? '' : text).replace(ENVELOPE_RE, function(all, p, kind, meta){
|
|
2453
|
+
var path = String(p || '').trim()
|
|
2454
|
+
if (!path) return ''
|
|
2455
|
+
var info = { path:path, meta:String(meta || '').replace(/\s+/g, ' ').trim() }
|
|
2456
|
+
if (kind === 'image') images.push(info)
|
|
2457
|
+
else files.push(info)
|
|
2458
|
+
return ''
|
|
2459
|
+
})
|
|
2460
|
+
return {
|
|
2461
|
+
text: clean.replace(/\n{3,}/g, '\n\n').replace(/^\s+|\s+$/g, ''),
|
|
2462
|
+
images: images,
|
|
2463
|
+
files: files
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function envelopeAttachmentsNode(env){
|
|
2467
|
+
var wrap = document.createElement('div'); wrap.className = 'message-attachments'
|
|
2468
|
+
var i
|
|
2469
|
+
for (i = 0; i < env.images.length; i++) {
|
|
2470
|
+
var a = env.images[i]
|
|
2471
|
+
var node = imgNode(fileUrlFor(a.path), baseName(a.path))
|
|
2472
|
+
if (a.meta) node.title = (node.title ? node.title + ' · ' : '') + a.meta
|
|
2473
|
+
wrap.appendChild(node)
|
|
2474
|
+
}
|
|
2475
|
+
for (i = 0; i < env.files.length; i++) {
|
|
2476
|
+
var f = env.files[i]
|
|
2477
|
+
wrap.appendChild(fileCard(fileUrlFor(f.path), baseName(f.path), '文件'))
|
|
2478
|
+
}
|
|
2479
|
+
return wrap.children.length ? wrap : null
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2346
2482
|
function bubNode(text, opts){
|
|
2347
2483
|
var o = opts || {}
|
|
2348
2484
|
var wrap = document.createElement('div'); wrap.className = 'bubwrap'
|
|
2349
2485
|
var b = document.createElement('div')
|
|
2350
2486
|
b.className = 'bub'
|
|
2351
2487
|
var s = String(text == null ? '' : text)
|
|
2488
|
+
var env = splitAttachmentEnvelopes(s)
|
|
2489
|
+
if (env.images.length || env.files.length) s = env.text
|
|
2490
|
+
var attach = envelopeAttachmentsNode(env)
|
|
2491
|
+
if (attach) b.appendChild(attach)
|
|
2352
2492
|
var parts = s.split('```')
|
|
2353
2493
|
if (parts.length > 1) {
|
|
2354
2494
|
for (var i = 0; i < parts.length; i++) {
|
|
@@ -3009,6 +3149,14 @@ var ICON_CTX = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h12M4
|
|
|
3009
3149
|
var ICON_DOWN = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M6 13l6 6 6-6"/></svg>'
|
|
3010
3150
|
var ICON_UP = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>'
|
|
3011
3151
|
var ICON_CLOSE = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>'
|
|
3152
|
+
// v1.7.7:抽屉(会话列表)里原来用字符当图标的两处 —— `+ 开一个新会话` 与
|
|
3153
|
+
// `工具轨迹 ›` 这类行尾箭头。真机上曾把某个字符渲染成「廿」这种错误字形
|
|
3154
|
+
// (回退字体里恰好有这个码位),所以连同新加的「子代理」入口图标一起换成 SVG:
|
|
3155
|
+
// 只依赖 path,不依赖设备上装了哪个字体。class 由 CSS 定尺寸(16 / 12px)。
|
|
3156
|
+
var ICON_USERS = '<svg class="pico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3.2"/>' +
|
|
3157
|
+
'<path d="M3.4 19c.5-3.2 2.8-5 5.6-5s5.1 1.8 5.6 5"/><path d="M16 5.4a3 3 0 0 1 0 5.6M17.2 14.4c2 .5 3.2 2.1 3.5 4.6"/></svg>'
|
|
3158
|
+
var ICON_PLUS = '<svg class="pico" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>'
|
|
3159
|
+
var ICON_GO = '<svg class="pchev" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>'
|
|
3012
3160
|
function syncSend(){
|
|
3013
3161
|
var b = $('send')
|
|
3014
3162
|
// 只读的子代理视图:发送键永久禁用(宿主不接受往子代理会话里写 prompt)。
|
|
@@ -3705,6 +3853,12 @@ function renderAttachmentTray(){
|
|
|
3705
3853
|
var chip = document.createElement('div'); chip.className = 'attach-chip' + (a.status === 'error' ? ' bad' : '')
|
|
3706
3854
|
if (a.kind === 'image') {
|
|
3707
3855
|
var thumb = document.createElement('img'); thumb.className = 'thumb'; thumb.src = a.previewURL; thumb.alt = ''
|
|
3856
|
+
// 预览图(blob:)也可能失败:删掉它,留格式徽标占位,绝不在 32px 的框里显示破图标。
|
|
3857
|
+
thumb.addEventListener('error', function(){
|
|
3858
|
+
if (!thumb.parentNode) return
|
|
3859
|
+
var badge = document.createElement('span'); badge.className = 'kind'; badge.textContent = 'IMG'
|
|
3860
|
+
thumb.parentNode.replaceChild(badge, thumb)
|
|
3861
|
+
})
|
|
3708
3862
|
chip.appendChild(thumb)
|
|
3709
3863
|
} else {
|
|
3710
3864
|
var kind = document.createElement('span'); kind.className = 'kind'; kind.textContent = kindLabel(a.name)
|
|
@@ -3877,19 +4031,20 @@ async function openSessions(){
|
|
|
3877
4031
|
h += '<button class="pill" data-act="chatonly"><span class="label">只看对话</span><span class="hint" id="chatOnlyHint">' +
|
|
3878
4032
|
(S.chatOnly ? '已隐藏工具步骤' : '显示工具步骤') + '</span></button>'
|
|
3879
4033
|
h += '<div id="sessionList"></div><div class="workbench-nav"><h3>工作台</h3>' +
|
|
3880
|
-
'<button class="pill navpill" data-act="workspace"><span class="label">工作区</span><span class="hint">新建时选择
|
|
3881
|
-
'<button class="pill navpill" data-act="plugins"><span class="label">插件</span><span class="hint">工具轨迹
|
|
3882
|
-
'<button class="pill navpill" data-act="automation"><span class="label">自动化</span><span class="hint">后台任务
|
|
4034
|
+
'<button class="pill navpill" data-act="workspace"><span class="label">工作区</span><span class="hint">新建时选择' + ICON_GO + '</span></button>' +
|
|
4035
|
+
'<button class="pill navpill" data-act="plugins"><span class="label">插件</span><span class="hint">工具轨迹' + ICON_GO + '</span></button>' +
|
|
4036
|
+
'<button class="pill navpill" data-act="automation"><span class="label">自动化</span><span class="hint">后台任务' + ICON_GO + '</span></button></div><h3>操作</h3>'
|
|
3883
4037
|
// 子代理不混进上面的会话列表:这里给唯一的入口,点开是二级页。
|
|
3884
4038
|
if (agents.length) {
|
|
3885
|
-
h += '<button class="pill" data-act="subagents"
|
|
3886
|
-
'
|
|
4039
|
+
h += '<button class="pill" data-act="subagents">' + ICON_USERS +
|
|
4040
|
+
'<span class="label">子代理 ' + agents.length +
|
|
4041
|
+
'</span><span class="hint">单独查看' + ICON_GO + '</span></button>'
|
|
3887
4042
|
}
|
|
3888
|
-
h += '<button class="pill" data-act="new"
|
|
3889
|
-
h += '<button class="pill" data-act="poker">实验功能:德州扑克</button>'
|
|
4043
|
+
h += '<button class="pill" data-act="new">' + ICON_PLUS + '<span class="label">开一个新会话</span></button>'
|
|
4044
|
+
h += '<button class="pill" data-act="poker"><span class="label">实验功能:德州扑克</span></button>'
|
|
3890
4045
|
h += '<button class="pill" data-act="theme"><span class="label">' +
|
|
3891
4046
|
(document.documentElement.getAttribute('data-theme') === 'dark' ? '切换到亮色' : '切换到暗色') +
|
|
3892
|
-
'</span><span class="hint">外观
|
|
4047
|
+
'</span><span class="hint">外观' + ICON_GO + '</span></button>'
|
|
3893
4048
|
openSheet(h)
|
|
3894
4049
|
bindSheetClose()
|
|
3895
4050
|
function paintSessions(query){
|
|
@@ -5172,8 +5327,11 @@ window.fetch = function (u, o) {
|
|
|
5172
5327
|
}
|
|
5173
5328
|
|
|
5174
5329
|
/* 4c. 图片:没加载完的才标 .loading(骨架流光),加载完 .done 显影。
|
|
5175
|
-
注意 .loading 是这里**加上**的 —— JS 不跑时图片本来就直接可见。
|
|
5330
|
+
注意 .loading 是这里**加上**的 —— JS 不跑时图片本来就直接可见。
|
|
5331
|
+
2026-10-22(v1.7.8):只给**贴着视口**的图挂流光。长会话里几十张懒加载图
|
|
5332
|
+
永远在视口外,以前它们会一直挂着 .loading,流光动画也就永远在跑(闪屏)。 */
|
|
5176
5333
|
var wraps = doc.querySelectorAll('.md-imgwrap')
|
|
5334
|
+
var vh = window.innerHeight || 800
|
|
5177
5335
|
for (var j = 0; j < wraps.length; j++) {
|
|
5178
5336
|
var w = wraps[j]
|
|
5179
5337
|
if (w.classList.contains('done')) continue
|
|
@@ -5183,7 +5341,10 @@ window.fetch = function (u, o) {
|
|
|
5183
5341
|
w.classList.remove('loading')
|
|
5184
5342
|
w.classList.add('done')
|
|
5185
5343
|
} else {
|
|
5186
|
-
w.
|
|
5344
|
+
var wr = w.getBoundingClientRect()
|
|
5345
|
+
var near = wr.bottom > -vh && wr.top < vh * 2
|
|
5346
|
+
if (near) w.classList.add('loading')
|
|
5347
|
+
else w.classList.remove('loading')
|
|
5187
5348
|
}
|
|
5188
5349
|
}
|
|
5189
5350
|
|
|
@@ -5198,6 +5359,12 @@ window.fetch = function (u, o) {
|
|
|
5198
5359
|
if (!w) return
|
|
5199
5360
|
w.classList.remove('loading')
|
|
5200
5361
|
w.classList.add('done')
|
|
5362
|
+
// v1.7.7 兜底三:任何一条路径(包括以后新加的渲染代码)插进来的图,
|
|
5363
|
+
// 只要 error 之后 naturalWidth 还是 0,就在这里统一换成提示条。
|
|
5364
|
+
// 这样"页面上不留破图 img"是页面级的不变量,而不是某个函数的自觉。
|
|
5365
|
+
if (e.type === 'error' && !im.naturalWidth) {
|
|
5366
|
+
swapBrokenImg(im, im.getAttribute('src') || '', im.alt || '')
|
|
5367
|
+
}
|
|
5201
5368
|
}
|
|
5202
5369
|
doc.addEventListener('load', settle, true)
|
|
5203
5370
|
doc.addEventListener('error', settle, true)
|
package/lib/sw.js
CHANGED
|
@@ -44,7 +44,10 @@ const SHELL = ['/taskwatch', '/taskwatch/icon-192.png', '/taskwatch/icon-512.png
|
|
|
44
44
|
// 那次重载是 cache-first 时代的补丁(旧外壳永远不会自己变新)。现在页面已经是
|
|
45
45
|
// **网络优先**(见下面 fetch 里的注释),每次导航本来就取新版,这个补丁既没必要、
|
|
46
46
|
// 又足以把整站打死。别再加回来。
|
|
47
|
-
|
|
47
|
+
// v17(v1.7.7):页面换了正文图片的兜底渲染(坏图 → 44px 以内提示条)与抽屉图标
|
|
48
|
+
// (字符字形 → 内联 SVG)。外壳里的 icon-192 也是预缓存的,一起升号清掉旧壳。
|
|
49
|
+
// v18(v1.7.8):图片流光的包含块修好(长会话不再整屏闪)+ 附件信封改成图片卡。
|
|
50
|
+
const CACHE = 'taskwatch-v18'
|
|
48
51
|
|
|
49
52
|
const JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
|
50
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-in-hand",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"description": "掌上 DSH:DeepSeek Harness 的手机端,不用装 App——会话、实时对话、交付文件、后台任务与待批准。 / DSH in hand: the phone client for DeepSeek Harness with no app to install.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|