dsh-in-hand 1.7.6 → 1.7.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.md +14 -0
- package/lib/page.html +129 -15
- package/lib/sw.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -252,6 +252,20 @@ 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
|
+
|
|
255
269
|
### 页面为什么不做整页重建
|
|
256
270
|
|
|
257
271
|
页面每 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;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}
|
|
@@ -2098,6 +2119,74 @@ function probePath(p){
|
|
|
2098
2119
|
} catch (e) { /* 忽略 */ }
|
|
2099
2120
|
return holder
|
|
2100
2121
|
}
|
|
2122
|
+
/** 破图提示条里的图标。走 createElementNS —— markdown 渲染段内不拼 HTML 字符串
|
|
2123
|
+
* (契约测试会扫这一段),所以这里也不破例。 */
|
|
2124
|
+
function brokenImgIconNode(){
|
|
2125
|
+
var NS = 'http://www.w3.org/2000/svg'
|
|
2126
|
+
var svg = document.createElementNS(NS, 'svg')
|
|
2127
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
2128
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
2129
|
+
// 相框 + 山形 + 一道斜杠:一眼读作"图裂了",不依赖任何字体字形。
|
|
2130
|
+
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']
|
|
2131
|
+
for (var i = 0; i < ds.length; i++) {
|
|
2132
|
+
var p = document.createElementNS(NS, 'path')
|
|
2133
|
+
p.setAttribute('d', ds[i])
|
|
2134
|
+
svg.appendChild(p)
|
|
2135
|
+
}
|
|
2136
|
+
return svg
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/** 图能不能"打开原图":只有同源路由 / http(s) 才给 <a>;data: / blob: 只能就地重试。 */
|
|
2140
|
+
function canReopenImage(url){
|
|
2141
|
+
var s = String(url || '')
|
|
2142
|
+
return /^https?:\/\//i.test(s) || s.charAt(0) === '/'
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/** 图片加载失败的兜底 UI(v1.7.7)。
|
|
2146
|
+
*
|
|
2147
|
+
* 以前这里换成一张整宽的 .filecard(63px、两行说明):视觉上仍是一块"占位框",
|
|
2148
|
+
* 把一段话从中间截断。现在统一成一条 **36px 单行提示条**:
|
|
2149
|
+
* · 能打开原地址的给 <a target=_blank>("图片未能加载 · 打开原图");
|
|
2150
|
+
* · data: / blob: 这类重开没意义的给 <button>,点一下就地重新加载。
|
|
2151
|
+
* 高度写死在 CSS(36px / max 44px),文字单行 ellipsis,所以不会再撑高正文。
|
|
2152
|
+
*/
|
|
2153
|
+
function brokenImgBar(url, alt){
|
|
2154
|
+
var reopen = canReopenImage(url)
|
|
2155
|
+
var el = document.createElement(reopen ? 'a' : 'button')
|
|
2156
|
+
el.className = 'imgerr'
|
|
2157
|
+
if (reopen) {
|
|
2158
|
+
el.href = url
|
|
2159
|
+
el.target = '_blank'
|
|
2160
|
+
el.rel = 'noopener noreferrer'
|
|
2161
|
+
} else {
|
|
2162
|
+
el.type = 'button'
|
|
2163
|
+
el.addEventListener('click', function(){
|
|
2164
|
+
if (!el.parentNode) return
|
|
2165
|
+
var fresh = imgNode(url, alt)
|
|
2166
|
+
el.parentNode.replaceChild(fresh, el)
|
|
2167
|
+
})
|
|
2168
|
+
}
|
|
2169
|
+
el.setAttribute('aria-label', (alt ? alt + ':' : '') + '图片未能加载,' + (reopen ? '点一下打开原图' : '点一下重试'))
|
|
2170
|
+
el.title = (alt ? alt + ' · ' : '') + (url || '')
|
|
2171
|
+
el.appendChild(brokenImgIconNode())
|
|
2172
|
+
var t = document.createElement('span')
|
|
2173
|
+
t.className = 't'
|
|
2174
|
+
t.textContent = '图片未能加载 · ' + (reopen ? '打开原图' : '点一下重试')
|
|
2175
|
+
el.appendChild(t)
|
|
2176
|
+
return el
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/** 把失败的 <img>(连同它的 .md-imgwrap)换成提示条。幂等:已被换掉就什么都不做。 */
|
|
2180
|
+
function swapBrokenImg(img, url, alt){
|
|
2181
|
+
if (!img || !img.parentNode) return false
|
|
2182
|
+
var wrap = img.closest ? img.closest('.md-imgwrap') : null
|
|
2183
|
+
if (wrap && !wrap.parentNode) return false // 已经换过一次了,别再动
|
|
2184
|
+
var target = wrap || img
|
|
2185
|
+
if (!target.parentNode) return false
|
|
2186
|
+
target.parentNode.replaceChild(brokenImgBar(url, alt), target)
|
|
2187
|
+
return true
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2101
2190
|
function imgNode(url, alt){
|
|
2102
2191
|
var wrap = document.createElement('button')
|
|
2103
2192
|
wrap.type = 'button'
|
|
@@ -2108,13 +2197,17 @@ function imgNode(url, alt){
|
|
|
2108
2197
|
img.loading = 'lazy'
|
|
2109
2198
|
img.decoding = 'async'
|
|
2110
2199
|
img.alt = alt || ''
|
|
2200
|
+
// 事件必须**先挂再赋 src**:同一条坏 URL 第二次出现时(浏览器已经记住这次失败),
|
|
2201
|
+
// error 可能在赋值那一刻就排队,挂晚了就再也收不到,页面上只留一个破图空框。
|
|
2202
|
+
img.addEventListener('error', function(){ swapBrokenImg(img, url, alt) })
|
|
2111
2203
|
img.src = url
|
|
2112
|
-
img.addEventListener('error', function(){
|
|
2113
|
-
if (!wrap.parentNode) return
|
|
2114
|
-
wrap.parentNode.replaceChild(brokenFileNode(alt || '图片'), wrap)
|
|
2115
|
-
})
|
|
2116
2204
|
wrap.appendChild(img)
|
|
2117
2205
|
wrap.addEventListener('click', function(){ openLightbox(url, alt) })
|
|
2206
|
+
// 兜底二:命中"失败缓存"时 complete 会立刻为 true 且 naturalWidth 还是 0,
|
|
2207
|
+
// 而那一次 error 可能已经错过 —— 追加进 DOM 之后补一刀(此时 wrap 已有父节点)。
|
|
2208
|
+
setTimeout(function(){
|
|
2209
|
+
if (img.complete && !img.naturalWidth) swapBrokenImg(img, url, alt)
|
|
2210
|
+
}, 0)
|
|
2118
2211
|
return wrap
|
|
2119
2212
|
}
|
|
2120
2213
|
function fileCard(url, label, kind){
|
|
@@ -3009,6 +3102,14 @@ var ICON_CTX = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h12M4
|
|
|
3009
3102
|
var ICON_DOWN = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M6 13l6 6 6-6"/></svg>'
|
|
3010
3103
|
var ICON_UP = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>'
|
|
3011
3104
|
var ICON_CLOSE = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>'
|
|
3105
|
+
// v1.7.7:抽屉(会话列表)里原来用字符当图标的两处 —— `+ 开一个新会话` 与
|
|
3106
|
+
// `工具轨迹 ›` 这类行尾箭头。真机上曾把某个字符渲染成「廿」这种错误字形
|
|
3107
|
+
// (回退字体里恰好有这个码位),所以连同新加的「子代理」入口图标一起换成 SVG:
|
|
3108
|
+
// 只依赖 path,不依赖设备上装了哪个字体。class 由 CSS 定尺寸(16 / 12px)。
|
|
3109
|
+
var ICON_USERS = '<svg class="pico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3.2"/>' +
|
|
3110
|
+
'<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>'
|
|
3111
|
+
var ICON_PLUS = '<svg class="pico" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>'
|
|
3112
|
+
var ICON_GO = '<svg class="pchev" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>'
|
|
3012
3113
|
function syncSend(){
|
|
3013
3114
|
var b = $('send')
|
|
3014
3115
|
// 只读的子代理视图:发送键永久禁用(宿主不接受往子代理会话里写 prompt)。
|
|
@@ -3705,6 +3806,12 @@ function renderAttachmentTray(){
|
|
|
3705
3806
|
var chip = document.createElement('div'); chip.className = 'attach-chip' + (a.status === 'error' ? ' bad' : '')
|
|
3706
3807
|
if (a.kind === 'image') {
|
|
3707
3808
|
var thumb = document.createElement('img'); thumb.className = 'thumb'; thumb.src = a.previewURL; thumb.alt = ''
|
|
3809
|
+
// 预览图(blob:)也可能失败:删掉它,留格式徽标占位,绝不在 32px 的框里显示破图标。
|
|
3810
|
+
thumb.addEventListener('error', function(){
|
|
3811
|
+
if (!thumb.parentNode) return
|
|
3812
|
+
var badge = document.createElement('span'); badge.className = 'kind'; badge.textContent = 'IMG'
|
|
3813
|
+
thumb.parentNode.replaceChild(badge, thumb)
|
|
3814
|
+
})
|
|
3708
3815
|
chip.appendChild(thumb)
|
|
3709
3816
|
} else {
|
|
3710
3817
|
var kind = document.createElement('span'); kind.className = 'kind'; kind.textContent = kindLabel(a.name)
|
|
@@ -3877,19 +3984,20 @@ async function openSessions(){
|
|
|
3877
3984
|
h += '<button class="pill" data-act="chatonly"><span class="label">只看对话</span><span class="hint" id="chatOnlyHint">' +
|
|
3878
3985
|
(S.chatOnly ? '已隐藏工具步骤' : '显示工具步骤') + '</span></button>'
|
|
3879
3986
|
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">后台任务
|
|
3987
|
+
'<button class="pill navpill" data-act="workspace"><span class="label">工作区</span><span class="hint">新建时选择' + ICON_GO + '</span></button>' +
|
|
3988
|
+
'<button class="pill navpill" data-act="plugins"><span class="label">插件</span><span class="hint">工具轨迹' + ICON_GO + '</span></button>' +
|
|
3989
|
+
'<button class="pill navpill" data-act="automation"><span class="label">自动化</span><span class="hint">后台任务' + ICON_GO + '</span></button></div><h3>操作</h3>'
|
|
3883
3990
|
// 子代理不混进上面的会话列表:这里给唯一的入口,点开是二级页。
|
|
3884
3991
|
if (agents.length) {
|
|
3885
|
-
h += '<button class="pill" data-act="subagents"
|
|
3886
|
-
'
|
|
3992
|
+
h += '<button class="pill" data-act="subagents">' + ICON_USERS +
|
|
3993
|
+
'<span class="label">子代理 ' + agents.length +
|
|
3994
|
+
'</span><span class="hint">单独查看' + ICON_GO + '</span></button>'
|
|
3887
3995
|
}
|
|
3888
|
-
h += '<button class="pill" data-act="new"
|
|
3889
|
-
h += '<button class="pill" data-act="poker">实验功能:德州扑克</button>'
|
|
3996
|
+
h += '<button class="pill" data-act="new">' + ICON_PLUS + '<span class="label">开一个新会话</span></button>'
|
|
3997
|
+
h += '<button class="pill" data-act="poker"><span class="label">实验功能:德州扑克</span></button>'
|
|
3890
3998
|
h += '<button class="pill" data-act="theme"><span class="label">' +
|
|
3891
3999
|
(document.documentElement.getAttribute('data-theme') === 'dark' ? '切换到亮色' : '切换到暗色') +
|
|
3892
|
-
'</span><span class="hint">外观
|
|
4000
|
+
'</span><span class="hint">外观' + ICON_GO + '</span></button>'
|
|
3893
4001
|
openSheet(h)
|
|
3894
4002
|
bindSheetClose()
|
|
3895
4003
|
function paintSessions(query){
|
|
@@ -5198,6 +5306,12 @@ window.fetch = function (u, o) {
|
|
|
5198
5306
|
if (!w) return
|
|
5199
5307
|
w.classList.remove('loading')
|
|
5200
5308
|
w.classList.add('done')
|
|
5309
|
+
// v1.7.7 兜底三:任何一条路径(包括以后新加的渲染代码)插进来的图,
|
|
5310
|
+
// 只要 error 之后 naturalWidth 还是 0,就在这里统一换成提示条。
|
|
5311
|
+
// 这样"页面上不留破图 img"是页面级的不变量,而不是某个函数的自觉。
|
|
5312
|
+
if (e.type === 'error' && !im.naturalWidth) {
|
|
5313
|
+
swapBrokenImg(im, im.getAttribute('src') || '', im.alt || '')
|
|
5314
|
+
}
|
|
5201
5315
|
}
|
|
5202
5316
|
doc.addEventListener('load', settle, true)
|
|
5203
5317
|
doc.addEventListener('error', settle, true)
|
package/lib/sw.js
CHANGED
|
@@ -44,7 +44,9 @@ 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
|
+
const CACHE = 'taskwatch-v17'
|
|
48
50
|
|
|
49
51
|
const JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
|
50
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-in-hand",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
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",
|