dsh-remote-plugin 0.6.24 → 0.6.25
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/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +106 -84
- package/gateway.cjs +78 -7
- package/package.json +1 -1
- package/public/app.js +153 -244
- package/public/desktop/desktop.html +2 -0
- package/public/desktop/desktop.js +50 -2
- package/public/genui.css +2 -0
- package/public/genui.js +144 -0
- package/public/index.html +11 -26
- package/public/md.js +16 -3
- package/public/styles.css +4 -11
- package/public/update.json +8 -8
- package/public/version.json +1 -1
|
@@ -78,6 +78,7 @@ const state = {
|
|
|
78
78
|
approvals: [],
|
|
79
79
|
questions: [],
|
|
80
80
|
queues: {},
|
|
81
|
+
sessionActivity: new Set(),
|
|
81
82
|
queueSteering: {},
|
|
82
83
|
compactions: {},
|
|
83
84
|
pendingCommands: {},
|
|
@@ -1354,6 +1355,10 @@ document.addEventListener('visibilitychange', () => {
|
|
|
1354
1355
|
function onMuxFrame(full) {
|
|
1355
1356
|
const f = full.payload
|
|
1356
1357
|
if (!f) return
|
|
1358
|
+
if (f.type === 'session/reasoning') {
|
|
1359
|
+
if (f.sessionId === state.current) { applyReasoningBaseline(f.partialReasoning); scheduleReasoningRender() }
|
|
1360
|
+
return
|
|
1361
|
+
}
|
|
1357
1362
|
if (f.type === 'session/event') return onSessionEvent(f.sessionId, f.event)
|
|
1358
1363
|
if (f.type === 'approval/requested') {
|
|
1359
1364
|
state.approvals = state.approvals.filter(a => a.approvalId !== f.approvalId)
|
|
@@ -1728,7 +1733,31 @@ function renderSessions() {
|
|
|
1728
1733
|
renderWorkbench()
|
|
1729
1734
|
}
|
|
1730
1735
|
|
|
1736
|
+
const emptySessionCleanup = new Set()
|
|
1737
|
+
async function archiveEmptySessionOnLeave(sessionId) {
|
|
1738
|
+
const base = state.server
|
|
1739
|
+
const protectedSession = () => state.server !== base || state.current !== sessionId
|
|
1740
|
+
|| !state.byId.has(sessionId) || state.byId.get(sessionId)?.running
|
|
1741
|
+
|| state.sessionActivity?.has(sessionId) || state.pendingPrompts?.has(sessionId)
|
|
1742
|
+
|| (state.queues[sessionId] || []).length > 0
|
|
1743
|
+
|| (state.composerImages || []).length > 0 || !!$('composer')?.value?.trim()
|
|
1744
|
+
if (!sessionId || emptySessionCleanup.has(sessionId) || protectedSession()) return false
|
|
1745
|
+
emptySessionCleanup.add(sessionId)
|
|
1746
|
+
try {
|
|
1747
|
+
const history = await rpc('session.history', { sessionId, maxMessages: 1 })
|
|
1748
|
+
if (protectedSession() || !Array.isArray(history?.events) || history.events.some(item => !['permission/preset', 'sandbox/mode', 'approval/policy'].includes(item?.event?.type || item?.type))
|
|
1749
|
+
|| history.hasMore || (history.partialReasoning || []).length) return false
|
|
1750
|
+
const result = await rpc('workspace.archiveSession', { sessionId })
|
|
1751
|
+
if (!Array.isArray(result?.archivedSessionIds) || !result.archivedSessionIds.includes(sessionId)) return false
|
|
1752
|
+
if (state.server !== base) return false
|
|
1753
|
+
await refreshSessions()
|
|
1754
|
+
return true
|
|
1755
|
+
} catch { return false } // 离线、历史未知或归档失败时保留会话。
|
|
1756
|
+
finally { emptySessionCleanup.delete(sessionId) }
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1731
1759
|
async function openSession(id) {
|
|
1760
|
+
if (state.current && state.current !== id) await archiveEmptySessionOnLeave(state.current)
|
|
1732
1761
|
state.current = id
|
|
1733
1762
|
setSessionRecovery('loading')
|
|
1734
1763
|
state.history = emptyDesktopHistory()
|
|
@@ -1745,7 +1774,10 @@ async function openSession(id) {
|
|
|
1745
1774
|
void refreshCompactionStatus(id)
|
|
1746
1775
|
await loadHistory()
|
|
1747
1776
|
}
|
|
1748
|
-
function closeSession() {
|
|
1777
|
+
async function closeSession() {
|
|
1778
|
+
const sessionId = state.current
|
|
1779
|
+
if (sessionId) await archiveEmptySessionOnLeave(sessionId)
|
|
1780
|
+
if (state.current !== sessionId) return
|
|
1749
1781
|
state.current = null
|
|
1750
1782
|
setSessionRecovery('idle')
|
|
1751
1783
|
state.history = emptyDesktopHistory()
|
|
@@ -1760,17 +1792,22 @@ function closeSession() {
|
|
|
1760
1792
|
async function loadHistory() {
|
|
1761
1793
|
const id = state.current
|
|
1762
1794
|
if (!id || state.history.loading) return
|
|
1795
|
+
const history = state.history
|
|
1796
|
+
const reasoningVersion = history.reasoningVersion || 0
|
|
1763
1797
|
state.history.loading = true
|
|
1764
1798
|
setSessionRecovery('loading')
|
|
1765
1799
|
let v
|
|
1766
1800
|
try { v = await rpc('session.history', { sessionId: id, maxMessages: 60 }) }
|
|
1767
1801
|
catch (e) {
|
|
1802
|
+
if (state.current !== id || state.history !== history) return
|
|
1768
1803
|
state.history.loading = false
|
|
1769
1804
|
if (e.message === 'AUTH') return
|
|
1770
1805
|
setSessionRecovery('error', e.message)
|
|
1771
1806
|
$('history').innerHTML = `<div class="ds-empty">${e.message}</div>`
|
|
1772
1807
|
return
|
|
1773
1808
|
}
|
|
1809
|
+
if (state.current !== id || state.history !== history) return
|
|
1810
|
+
const liveReasoning = (history.reasoningVersion || 0) !== reasoningVersion ? new Map(history.partialReasoning) : null
|
|
1774
1811
|
hydrateSessionProjections(id, v.projections)
|
|
1775
1812
|
for (const entry of v.events || []) {
|
|
1776
1813
|
const ev = entry?.event
|
|
@@ -1783,6 +1820,8 @@ async function loadHistory() {
|
|
|
1783
1820
|
state.history.visible.push({ seq, event: ev })
|
|
1784
1821
|
}
|
|
1785
1822
|
state.history.visible.sort((a, b) => a.seq - b.seq)
|
|
1823
|
+
if (liveReasoning) history.partialReasoning = liveReasoning
|
|
1824
|
+
else applyReasoningBaseline(v.partialReasoning)
|
|
1786
1825
|
state.history.hasMore = !!v.hasMore
|
|
1787
1826
|
state.history.loading = false
|
|
1788
1827
|
setSessionRecovery('ready')
|
|
@@ -1817,6 +1856,12 @@ function shouldShowEvent(type, event) {
|
|
|
1817
1856
|
return true
|
|
1818
1857
|
}
|
|
1819
1858
|
function reasoningStreamKey(data, index) { return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}` }
|
|
1859
|
+
function applyReasoningBaseline(items) {
|
|
1860
|
+
if (!Array.isArray(items)) return
|
|
1861
|
+
state.history.reasoningVersion = (state.history.reasoningVersion || 0) + 1
|
|
1862
|
+
state.history.partialReasoning.clear()
|
|
1863
|
+
for (const item of items) state.history.partialReasoning.set(reasoningStreamKey(item, item.index), item)
|
|
1864
|
+
}
|
|
1820
1865
|
function applyReasoningStreamEvent(event) {
|
|
1821
1866
|
const h = state.history
|
|
1822
1867
|
const data = event?.data || {}
|
|
@@ -1842,7 +1887,7 @@ function applyReasoningStreamEvent(event) {
|
|
|
1842
1887
|
item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
|
|
1843
1888
|
h.partialReasoning.set(key, item)
|
|
1844
1889
|
changed = true
|
|
1845
|
-
} else if (event?.type === 'assistant/message') {
|
|
1890
|
+
} else if (event?.type === 'assistant/message' || event?.type === 'assistant/attempt') {
|
|
1846
1891
|
for (const [key, item] of h.partialReasoning) {
|
|
1847
1892
|
if (item.turn === data.turn && item.step === data.step) { h.partialReasoning.delete(key); changed = true }
|
|
1848
1893
|
}
|
|
@@ -1868,6 +1913,7 @@ function safeJson(v) { try { return JSON.stringify(v, null, 2) } catch { return
|
|
|
1868
1913
|
function blockHtml(b) {
|
|
1869
1914
|
if (!b) return ''
|
|
1870
1915
|
if (b.type === 'text') return `<div class="md">${window.mdToHtml ? window.mdToHtml(b.text ?? '') : esc(b.text ?? '')}</div>`
|
|
1916
|
+
if (b.type === 'file') return `<div class="ds-tool">📎 ${esc(b.attachment?.name || b.name || 'file')} <small>${esc(b.attachment?.bytes ?? b.bytes ?? '')} bytes</small></div>`
|
|
1871
1917
|
if (b.type === 'thinking' || b.type === 'reasoning') return `<details><summary>${esc(t('block.thinking'))}</summary><div style="opacity:.82">${esc(b.text ?? b.content ?? '')}</div></details>`
|
|
1872
1918
|
if (b.type === 'tool-call') return `<div>🔧 ${esc(b.name || '')}</div>`
|
|
1873
1919
|
if (b.type === 'tool-result') return `<div>📦</div>`
|
|
@@ -2133,6 +2179,7 @@ async function sendMessage() {
|
|
|
2133
2179
|
const input = $('composer')
|
|
2134
2180
|
const text = input.value.trim()
|
|
2135
2181
|
if (!text || !state.current) return
|
|
2182
|
+
state.sessionActivity.add(state.current)
|
|
2136
2183
|
if (await runSlashCommand(text)) { input.value = ''; return }
|
|
2137
2184
|
input.value = ''
|
|
2138
2185
|
setSessionRecovery('resuming')
|
|
@@ -3121,6 +3168,7 @@ function setMobileSidebar(open) {
|
|
|
3121
3168
|
}
|
|
3122
3169
|
|
|
3123
3170
|
function showView(id) {
|
|
3171
|
+
if (state.view === 'view-chat' && id !== 'view-chat' && state.current) void archiveEmptySessionOnLeave(state.current)
|
|
3124
3172
|
state.view = id
|
|
3125
3173
|
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
3126
3174
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
package/public/genui.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.genui{margin:14px 0;padding:14px;border:1px solid #8884;border-radius:12px;line-height:1.65;overflow:hidden;color:inherit;background:#88808;font-size:14px}
|
|
2
|
+
.genui *{box-sizing:border-box}.genui h1,.genui h2,.genui h3,.genui h4,.genui p{margin:0 0 10px}.genui-caption{font-size:11px;opacity:.65;margin-bottom:12px}.genui-row{display:flex;flex-wrap:wrap;gap:12px}.genui-row>*{flex:1;min-width:120px}.genui-col{display:grid;gap:12px}.genui-grid{display:grid;grid-template-columns:repeat(var(--genui-cols,2),minmax(0,1fr));gap:12px}.genui-card{border:1px solid #8884;border-radius:8px;padding:12px;min-width:0}.genui-card>h4{font-size:16px}.genui-stat{display:flex;flex-direction:column;min-width:0;padding:8px 0}.genui-stat strong{font-size:28px;line-height:1.25;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.genui-stat small,.genui-stat span{opacity:.75}.genui-badge{display:inline-block;padding:2px 9px;border:1px solid #8885;border-radius:20px}.genui progress{display:block;width:100%;accent-color:#2563eb;height:10px;margin:7px 0}.genui-spacer{height:12px}.genui-callout{border-left:3px solid #2563eb;padding:10px 12px;background:#2563eb12}.genui-chart{margin:10px 0}.genui-chart figcaption{font-weight:600}.genui-chart svg{display:block;width:100%;height:auto;max-height:520px}.genui-chart svg text{fill:currentColor;font:11px system-ui}.genui-gridline{stroke:currentColor;opacity:.15;fill:none}.genui-legend{display:flex;flex-wrap:wrap;gap:7px 14px;font-size:12px}.genui-legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}.genui-scroll{overflow:auto;max-height:420px}.genui table{width:100%;border-collapse:collapse;font-size:12px}.genui td,.genui th{text-align:left;padding:7px;border-bottom:1px solid #8884;white-space:nowrap}.genui-source{margin-top:12px}.genui summary{cursor:pointer;font-size:12px}.genui pre{max-height:320px;overflow:auto;white-space:pre-wrap;word-break:break-word}.genui-unsupported{border-left:2px solid #d97706;padding:8px;margin:8px 0}.genui-html{display:block;width:100%;height:440px;border:1px solid #8884;border-radius:7px;background:white}.genui dl{display:grid;grid-template-columns:auto 1fr;gap:6px 12px}.genui dd{margin:0}.genui li{margin:4px 0}@media(max-width:520px){.genui{padding:10px}.genui-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.genui-stat strong{font-size:23px}.genui-html{height:380px}}
|
package/public/genui.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* DSH Remote: dependency-free, read-only dsh-ui subset. Never executes model code. */
|
|
2
|
+
(function (root, factory) {
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = factory()
|
|
4
|
+
else root.DshGenUi = factory()
|
|
5
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
6
|
+
'use strict'
|
|
7
|
+
const esc = value => String(value == null ? '' : value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
8
|
+
const palette = ['#2563eb', '#059669', '#d97706', '#db2777', '#7c3aed', '#0891b2']
|
|
9
|
+
const array = value => Array.isArray(value) ? value : []
|
|
10
|
+
const number = value => typeof value === 'number' && Number.isFinite(value) && Math.abs(value) <= 1e12
|
|
11
|
+
const color = (value, i) => /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(value || '') ? value : palette[i % palette.length]
|
|
12
|
+
const raw = value => '<pre><code>' + esc(typeof value === 'string' ? value : JSON.stringify(value, null, 2)) + '</code></pre>'
|
|
13
|
+
const details = value => '<details class="genui-source"><summary>JSON / 源码</summary>' + raw(value) + '</details>'
|
|
14
|
+
function unsupported(node, reason) {
|
|
15
|
+
return '<div class="genui-unsupported">暂不支持 / Unsupported: ' + esc(reason || node?.type || 'spec') + details(node) + '</div>'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function chart(node) {
|
|
19
|
+
const kind = node.kind || 'bars'
|
|
20
|
+
if (!['bars', 'line', 'donut'].includes(kind) || node.stacked || node.filter || node.sortField) return unsupported(node, 'chart options')
|
|
21
|
+
const series = array(node.series).length ? node.series : [{ label: node.title || '', data: node.data }]
|
|
22
|
+
if (series.length > 6 || (kind === 'donut' && series.length !== 1)) return unsupported(node, 'chart series')
|
|
23
|
+
if (series.some(s => !s || !Array.isArray(s.data) || !s.data.length || s.data.length > 100 || s.data.some(d => !d || !number(d.value)))) return unsupported(node, 'chart data')
|
|
24
|
+
const data = series[0].data
|
|
25
|
+
if (series.some(s => s.data.length !== data.length || s.data.some((d, i) => d.label !== data[i].label))) return unsupported(node, 'unaligned series')
|
|
26
|
+
const colors = series.map((s, i) => color(s.color || array(node.palette)[i], i))
|
|
27
|
+
const table = '<div class="genui-scroll"><table><thead><tr><th></th>' + series.map(s => '<th>' + esc(s.label || 'Value') + '</th>').join('') + '</tr></thead><tbody>' + data.map((d, i) => '<tr><th>' + esc(d.label) + '</th>' + series.map(s => '<td>' + esc(s.data[i].value) + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>'
|
|
28
|
+
let svg = ''
|
|
29
|
+
if (kind === 'donut') {
|
|
30
|
+
const total = data.reduce((sum, d) => sum + d.value, 0)
|
|
31
|
+
if (data.some(d => d.value < 0) || total <= 0) return unsupported(node, 'donut requires non-negative values and positive total')
|
|
32
|
+
let offset = 0
|
|
33
|
+
svg = data.map((d, i) => {
|
|
34
|
+
const fraction = d.value / total * 100
|
|
35
|
+
const segment = '<circle cx="160" cy="120" r="75" fill="none" stroke="' + color(d.color || array(node.palette)[i], i) + '" stroke-width="34" pathLength="100" stroke-dasharray="' + fraction + ' ' + (100 - fraction) + '" stroke-dashoffset="' + (-offset) + '" transform="rotate(-90 160 120)"><title>' + esc(d.label) + ': ' + esc(d.value) + ' (' + fraction.toFixed(1) + '%)</title></circle>'
|
|
36
|
+
offset += fraction
|
|
37
|
+
return segment
|
|
38
|
+
}).join('') + '<text x="160" y="124" text-anchor="middle" fill="currentColor">' + esc(total) + '</text>'
|
|
39
|
+
svg = '<svg viewBox="0 0 320 240" role="img" aria-label="' + esc(node.title || 'Donut chart') + '">' + svg + '</svg>'
|
|
40
|
+
svg += '<div class="genui-legend">' + data.map((d, i) => '<span><i style="background:' + color(d.color || array(node.palette)[i], i) + '"></i>' + esc(d.label) + ' · ' + (d.value / total * 100).toFixed(1) + '%</span>').join('') + '</div>'
|
|
41
|
+
} else {
|
|
42
|
+
const values = series.flatMap(s => s.data.map(d => d.value))
|
|
43
|
+
let min = Math.min(0, ...values), max = Math.max(0, ...values)
|
|
44
|
+
if (min === max) max = min + 1
|
|
45
|
+
const horizontal = kind === 'bars' && node.horizontal === true
|
|
46
|
+
const width = 560, height = horizontal ? Math.max(240, data.length * 35 + 50) : 270
|
|
47
|
+
const left = horizontal ? 110 : 64, right = 20, top = 20, bottom = 45
|
|
48
|
+
const w = width - left - right, h = height - top - bottom
|
|
49
|
+
const scale = value => horizontal ? left + (value - min) / (max - min) * w : top + (max - value) / (max - min) * h
|
|
50
|
+
for (let i = 0; i <= 4; i++) {
|
|
51
|
+
const value = min + (max - min) * i / 4, p = scale(value)
|
|
52
|
+
svg += horizontal
|
|
53
|
+
? '<path d="M' + p + ' ' + top + 'V' + (top + h) + '" class="genui-gridline"/><text x="' + p + '" y="' + (height - 12) + '" text-anchor="middle">' + esc(Number(value.toPrecision(4))) + '</text>'
|
|
54
|
+
: '<path d="M' + left + ' ' + p + 'H' + (left + w) + '" class="genui-gridline"/><text x="' + (left - 7) + '" y="' + (p + 4) + '" text-anchor="end">' + esc(Number(value.toPrecision(4))) + '</text>'
|
|
55
|
+
}
|
|
56
|
+
const step = (horizontal ? h : w) / data.length
|
|
57
|
+
series.forEach((s, si) => {
|
|
58
|
+
const points = []
|
|
59
|
+
s.data.forEach((d, i) => {
|
|
60
|
+
const center = (horizontal ? top : left) + step * (i + 0.5), pos = scale(d.value)
|
|
61
|
+
const hint = '<title>' + esc((s.label ? s.label + ' · ' : '') + d.label + ': ' + d.value) + '</title>'
|
|
62
|
+
if (kind === 'line') {
|
|
63
|
+
points.push(center + ',' + pos)
|
|
64
|
+
svg += '<circle cx="' + center + '" cy="' + pos + '" r="3" fill="' + colors[si] + '">' + hint + '</circle>'
|
|
65
|
+
} else {
|
|
66
|
+
const size = step * 0.7 / series.length, lane = center - step * 0.35 + si * size
|
|
67
|
+
const zero = scale(0)
|
|
68
|
+
svg += '<rect x="' + (horizontal ? Math.min(zero, pos) : lane) + '" y="' + (horizontal ? lane : Math.min(zero, pos)) + '" width="' + (horizontal ? Math.abs(zero - pos) : size) + '" height="' + (horizontal ? size : Math.abs(zero - pos)) + '" fill="' + (series.length === 1 ? color(d.color || array(node.palette)[i], i) : colors[si]) + '">' + hint + '</rect>'
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
if (kind === 'line') svg += '<polyline points="' + points.join(' ') + '" fill="none" stroke="' + colors[si] + '" stroke-width="2.5"/>'
|
|
72
|
+
})
|
|
73
|
+
data.forEach((d, i) => {
|
|
74
|
+
if (!horizontal && i % Math.ceil(data.length / 8) !== 0) return
|
|
75
|
+
const text = String(d.label || '')
|
|
76
|
+
svg += '<text x="' + (horizontal ? left - 6 : left + step * (i + 0.5)) + '" y="' + (horizontal ? top + step * (i + 0.5) + 4 : height - 21) + '" text-anchor="' + (horizontal ? 'end' : 'middle') + '">' + esc(text.length > 12 ? text.slice(0, 11) + '…' : text) + '</text>'
|
|
77
|
+
})
|
|
78
|
+
svg = '<svg viewBox="0 0 ' + width + ' ' + height + '" role="img" aria-label="' + esc(node.title || kind + ' chart') + '">' + svg + '</svg>'
|
|
79
|
+
if (series.length > 1) svg += '<div class="genui-legend">' + series.map((s, i) => '<span><i style="background:' + colors[i] + '"></i>' + esc(s.label) + '</span>').join('') + '</div>'
|
|
80
|
+
}
|
|
81
|
+
return '<figure class="genui-chart">' + (node.title ? '<figcaption>' + esc(node.title) + '</figcaption>' : '') + svg + '<details><summary>数据 / Data</summary>' + table + '</details></figure>'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function render(spec) {
|
|
85
|
+
if (!spec || typeof spec !== 'object') throw Error('Invalid spec')
|
|
86
|
+
let count = 0
|
|
87
|
+
function nodes(items, depth) { return array(items).map(n => node(n, depth)).join('') }
|
|
88
|
+
function node(n, depth) {
|
|
89
|
+
if (++count > 240 || depth > 12) throw Error('Component limit')
|
|
90
|
+
if (!n || typeof n !== 'object' || Array.isArray(n)) return unsupported(n)
|
|
91
|
+
const type = n.type
|
|
92
|
+
// Reject data-transforming options rather than silently displaying different data.
|
|
93
|
+
if (n.filter || n.sortField) return unsupported(n, type + ' filter/sort')
|
|
94
|
+
if ((type === 'table' && (array(n.rows).length > 200 || array(n.columns).length > 30)) ||
|
|
95
|
+
(['list', 'timeline', 'steps', 'keyvalue'].includes(type) && array(n.items || n.steps || n.pairs).length > 100)) return unsupported(n, type + ' size limit')
|
|
96
|
+
if (['row', 'col', 'grid', 'card'].includes(type)) {
|
|
97
|
+
const cols = Math.max(1, Math.min(4, Math.floor(Number(n.cols) || 2)))
|
|
98
|
+
return '<section class="genui-' + type + '"' + (type === 'grid' ? ' style="--genui-cols:' + cols + '"' : '') + '>' + (n.title ? '<h4>' + esc(n.title) + '</h4>' : '') + nodes(n.items, depth + 1) + '</section>'
|
|
99
|
+
}
|
|
100
|
+
if (type === 'text') { const tag = ['h1', 'h2', 'h3'].includes(n.size) ? n.size : 'p'; return '<' + tag + '>' + esc(n.content) + '</' + tag + '>' }
|
|
101
|
+
if (type === 'hero') return '<div class="genui-stat"><small>' + esc(n.label) + '</small><strong>' + esc(n.value) + '</strong><h4>' + esc(n.title) + '</h4><span>' + esc(n.subtitle) + '</span><span>' + esc(n.delta) + '</span></div>'
|
|
102
|
+
if (type === 'stat') return '<div class="genui-stat"><small>' + esc(n.label) + '</small><strong>' + esc(n.value) + '</strong><span>' + esc(n.delta) + '</span></div>'
|
|
103
|
+
if (type === 'badge') return '<span class="genui-badge">' + esc(n.label) + '</span>'
|
|
104
|
+
if (type === 'divider') return '<hr>'
|
|
105
|
+
if (type === 'spacer') return '<div class="genui-spacer"></div>'
|
|
106
|
+
if (type === 'progress' && !number(n.value)) return unsupported(n, 'progress value')
|
|
107
|
+
if (type === 'progress') return '<div>' + esc(n.label) + '<progress max="100" value="' + Math.max(0, Math.min(100, Number(n.value) || 0)) + '"></progress><small>' + esc(n.valueLabel || String(n.value || 0) + '%') + '</small>' + (number(n.target) ? '<small> · 目标 / Target: ' + esc(n.target) + '%</small>' : '') + '</div>'
|
|
108
|
+
if (type === 'callout') return '<aside class="genui-callout"><strong>' + esc(n.title) + '</strong><p>' + esc(n.content) + '</p></aside>'
|
|
109
|
+
if (type === 'code' || type === 'json') return raw(type === 'code' ? n.code : n.value)
|
|
110
|
+
if (type === 'list' || type === 'timeline' || type === 'steps') return '<ul>' + array(n.items || n.steps).slice(0, 100).map(v => '<li>' + (v && typeof v === 'object' ? (v.type ? node(v, depth + 1) : '<strong>' + esc(v.title) + '</strong> ' + esc(v.desc) + ' ' + esc(v.time)) : esc(v)) + '</li>').join('') + '</ul>'
|
|
111
|
+
if (type === 'keyvalue') return '<dl>' + array(n.pairs).slice(0, 100).map(v => '<dt>' + esc(v?.key) + '</dt><dd>' + esc(v?.value) + '</dd>').join('') + '</dl>'
|
|
112
|
+
if (type === 'table') return '<div class="genui-scroll"><table><thead><tr>' + array(n.columns).slice(0, 30).map(v => '<th>' + esc(v) + '</th>').join('') + '</tr></thead><tbody>' + array(n.rows).slice(0, 200).map(row => '<tr>' + array(row).slice(0, 30).map(v => '<td>' + esc(v) + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>'
|
|
113
|
+
if (type === 'chart') return chart(n)
|
|
114
|
+
if (type === 'echart' && !n.option && ['bar', 'line', 'pie'].includes(n.preset)) return '<small>ECharts 基础数据预览 / Basic preview</small>' + chart({ ...n, type: 'chart', kind: { bar: 'bars', line: 'line', pie: 'donut' }[n.preset] })
|
|
115
|
+
if (type === 'accordion' || type === 'tabs') return array(n.tabs || n.items).slice(0, 30).map(v => '<details><summary>' + esc(v?.label || v?.title) + '</summary>' + nodes(v?.items, depth + 1) + '</details>').join('')
|
|
116
|
+
return unsupported(n)
|
|
117
|
+
}
|
|
118
|
+
const body = Array.isArray(spec) ? nodes(spec, 0) : spec.type ? node(spec, 0) : nodes(spec.items, 0)
|
|
119
|
+
if (!body) throw Error('No components')
|
|
120
|
+
return '<section class="genui"><div class="genui-caption">DSH UI · 只读预览 / Read-only</div>' + (spec.title ? '<h3>' + esc(spec.title) + '</h3>' : '') + body + details(spec) + '</section>'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function html(source) {
|
|
124
|
+
if (typeof document === 'undefined' || String(source).length > 200000) return raw(source)
|
|
125
|
+
// Template contents are inert: source images cannot start fetching during sanitization.
|
|
126
|
+
const template = document.createElement('template')
|
|
127
|
+
template.innerHTML = String(source)
|
|
128
|
+
const allowed = new Set('title style div span p br hr h1 h2 h3 h4 h5 h6 strong em b i u s small sub sup pre code blockquote ul ol li dl dt dd table thead tbody tfoot tr td th caption colgroup col section article header footer main aside figure figcaption details summary progress'.split(' '))
|
|
129
|
+
function clean(parent) {
|
|
130
|
+
for (const child of Array.from(parent.children)) {
|
|
131
|
+
if (!allowed.has(child.localName)) { child.remove(); continue }
|
|
132
|
+
for (const attr of Array.from(child.attributes)) {
|
|
133
|
+
if (!['style', 'class', 'colspan', 'rowspan', 'value', 'max'].includes(attr.name)) child.removeAttribute(attr.name)
|
|
134
|
+
}
|
|
135
|
+
clean(child)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
clean(template.content)
|
|
139
|
+
const policy = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'"
|
|
140
|
+
const srcDoc = '<!doctype html><html><head><meta http-equiv="Content-Security-Policy" content="' + policy + '"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font:14px/1.6 system-ui;margin:16px;overflow-wrap:anywhere}table{border-collapse:collapse}td,th{padding:6px;border:1px solid #ccc}pre{white-space:pre-wrap}*{max-width:100%;box-sizing:border-box}</style></head><body>' + template.innerHTML + '</body></html>'
|
|
141
|
+
return '<section class="genui"><div class="genui-caption">HTML · 只读预览(脚本及外部资源已禁用)</div><iframe class="genui-html" sandbox="" referrerpolicy="no-referrer" title="HTML preview" srcdoc="' + esc(srcDoc) + '"></iframe></section>'
|
|
142
|
+
}
|
|
143
|
+
return { render, html }
|
|
144
|
+
})
|
package/public/index.html
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<link rel="manifest" href="manifest.webmanifest">
|
|
13
13
|
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
|
14
14
|
<link rel="stylesheet" href="styles.css">
|
|
15
|
+
<link rel="stylesheet" href="genui.css">
|
|
15
16
|
</head>
|
|
16
17
|
<body>
|
|
17
18
|
<header class="topbar">
|
|
@@ -410,26 +411,6 @@
|
|
|
410
411
|
<div class="setting-name" data-i18n="settings.groupTests">功能测试</div>
|
|
411
412
|
</div>
|
|
412
413
|
</div>
|
|
413
|
-
<div class="settings-group feature-test-shell">
|
|
414
|
-
<div class="feature-test-head">
|
|
415
|
-
<div>
|
|
416
|
-
<div class="setting-name" data-i18n="settings.asrTestTitle">ASR 模型测试(小米手机)</div>
|
|
417
|
-
<div class="setting-desc expanded" data-i18n="settings.asrTestDesc">使用 Android 系统 SpeechRecognizer 验证小米/系统云端语音识别,不上传测试日志。</div>
|
|
418
|
-
</div>
|
|
419
|
-
</div>
|
|
420
|
-
<div class="feature-test-note" data-i18n="settings.asrTestInstruction">请保持页面在前台,朗读一段中文,包含停顿、数字和英文 API 名称。测试约 60 秒,会自动记录 partial、最终结果、重建 session 和错误。</div>
|
|
421
|
-
<div class="feature-test-actions">
|
|
422
|
-
<button id="btn-asr-test-start" class="mini-btn primary" type="button" data-i18n="settings.asrTestStart">开始测试</button>
|
|
423
|
-
<button id="btn-asr-test-stop" class="mini-btn" type="button" data-i18n="settings.asrTestStop" disabled>停止测试</button>
|
|
424
|
-
<button id="btn-asr-test-copy" class="mini-btn" type="button" data-i18n="settings.asrTestCopy" disabled>复制日志</button>
|
|
425
|
-
<button id="btn-asr-test-clear" class="mini-btn" type="button" data-i18n="settings.asrTestClear">清空日志</button>
|
|
426
|
-
<button id="btn-asr-test-permission" class="mini-btn hidden" type="button" data-i18n="settings.asrTestPermissionSettings">权限设置</button>
|
|
427
|
-
<button id="btn-asr-test-engine" class="mini-btn hidden" type="button" data-i18n="settings.asrTestEngineSettings">语音引擎设置</button>
|
|
428
|
-
</div>
|
|
429
|
-
<div id="asr-test-status" class="feature-test-status muted" role="status" data-i18n="settings.asrTestNativeOnly">仅 Android App 可用</div>
|
|
430
|
-
<div id="asr-test-summary" class="feature-test-summary"></div>
|
|
431
|
-
<pre id="asr-test-log" class="feature-test-log" data-i18n="settings.asrTestLogEmpty">暂无测试日志</pre>
|
|
432
|
-
</div>
|
|
433
414
|
</div>
|
|
434
415
|
|
|
435
416
|
<div id="settings-page-servers" class="settings-subpage hidden" data-motion-panel>
|
|
@@ -553,6 +534,11 @@
|
|
|
553
534
|
<div class="setting-row">
|
|
554
535
|
<div><div class="setting-name" data-i18n="settings.updateTitle">检查更新</div><div class="setting-desc" id="update-desc" data-i18n="settings.updateLoading">加载中…</div>
|
|
555
536
|
<button id="btn-download-update" class="mini-btn hidden" style="margin-top:6px" data-i18n="settings.downloadUpdate">下载并安装更新</button>
|
|
537
|
+
<div id="update-download-progress" class="update-download-progress hidden">
|
|
538
|
+
<div id="update-download-status" role="status" aria-live="polite"></div>
|
|
539
|
+
<progress id="update-download-bar" max="100" aria-label="APK download progress"></progress>
|
|
540
|
+
<div id="update-download-detail"></div>
|
|
541
|
+
</div>
|
|
556
542
|
<button id="btn-update-expand" class="mini-btn hidden" style="margin-top:6px" data-i18n="update.expand">展开</button></div>
|
|
557
543
|
<button id="btn-check-update" class="mini-btn" data-i18n="settings.check">检查</button>
|
|
558
544
|
</div>
|
|
@@ -1034,7 +1020,7 @@
|
|
|
1034
1020
|
'fs.notFound': '目录不存在', 'fs.forbidden': '路径不在允许范围内;DSH 已登记工作区会自动放行,其他目录请配置 DSH_REMOTE_FS_ROOT',
|
|
1035
1021
|
'fs.loadFailed': '加载失败:{msg}', 'fs.loadFailedToast': '文件列表加载失败:{msg}',
|
|
1036
1022
|
'fs.emptyDir': '空目录', 'fs.dir': '目录',
|
|
1037
|
-
'fs.downloadStarted': '开始下载到「下载/dsh-remote」目录', 'fs.downloadFailed': '
|
|
1023
|
+
'fs.downloadStarted': '开始下载到「下载/dsh-remote」目录', 'fs.downloadFailed': '下载失败:{msg}',
|
|
1038
1024
|
'fs.downloadUnsupported': '当前 App 版本不支持系统下载,请先更新 App',
|
|
1039
1025
|
'fs.uploadCancelled': '已取消上传', 'fs.pausedErr': '已暂停', 'fs.cancelledErr': '已取消',
|
|
1040
1026
|
'fs.noTokenToast': '请先到「设置」页设置令牌', 'fs.tooLarge': '单文件超过 2GB 上限', 'fs.uploadBusy': '已有上传任务,先暂停或取消',
|
|
@@ -1050,6 +1036,7 @@
|
|
|
1050
1036
|
'update.found': '发现新版本 v{version}{notes}', 'update.foundTitle': '发现新版本', 'update.foundBody': 'v{version} 可更新',
|
|
1051
1037
|
'update.latestV': '已是最新 v{version}', 'update.latestRemote': '最新版本 v{version}', 'update.latestToast': '已是最新版本',
|
|
1052
1038
|
'update.checkFailedDesc': '检查失败:{msg}', 'update.checkFailed': '检查更新失败:{msg}',
|
|
1039
|
+
'update.phase.downloading': '正在下载…', 'update.phase.verifying': '正在校验安装包…', 'update.phase.complete': '下载完成', 'update.phase.legacy': '系统正在下载安装包;安装新版后可显示完整进度', 'update.busy': '已有更新下载正在进行',
|
|
1053
1040
|
'update.downloadStarted': '开始下载,完成后会弹出安装页', 'update.downloadFailed': '无法启动下载:{msg}',
|
|
1054
1041
|
'update.corrupted': '下载文件损坏,请重试', 'update.serverFileMissing': '服务器上还没有对应版本的文件,请稍后再试',
|
|
1055
1042
|
'update.installUnsupported': '当前版本不支持 App 内安装,已转浏览器下载',
|
|
@@ -1085,9 +1072,7 @@
|
|
|
1085
1072
|
'settings.modelNoModels': '暂无自定义模型,当前使用提供方默认目录', 'settings.modelCandidates': '发现的模型', 'settings.modelSelectAll': '全选', 'settings.modelSelectNone': '取消全选', 'settings.modelAddSelected': '加入选中模型',
|
|
1086
1073
|
'settings.modelAdded': '已加入模型目录', 'settings.modelSaved': '模型配置已保存', 'settings.modelSaveFailed': '保存模型配置失败:{msg}', 'settings.modelOpenConfigFailed': '打开配置文件失败:{msg}', 'settings.modelOpenedConfig': '已请求打开 DSH 配置文件',
|
|
1087
1074
|
'settings.modelLive': '保存后立即生效', 'settings.modelRestart': '保存后需重启 DSH 才生效',
|
|
1088
|
-
'settings.asrTestTitle': 'ASR 模型测试(小米手机)', 'settings.asrTestDesc': '使用 Android 系统 SpeechRecognizer 验证小米/系统云端语音识别,不上传测试日志。', 'settings.asrTestInstruction': '请保持页面在前台,朗读一段中文,包含停顿、数字和英文 API 名称。测试约 60 秒,会自动记录 partial、最终结果、重建 session 和错误。', 'settings.asrTestStart': '开始测试', 'settings.asrTestStop': '停止测试', 'settings.asrTestCopy': '复制日志', 'settings.asrTestClear': '清空日志', 'settings.asrTestPermissionSettings': '权限设置', 'settings.asrTestNativeOnly': '仅 Android App 可用', 'settings.asrTestWebUnsupported': '浏览器不支持原生 ASR 诊断,请在 Android App 中打开。', 'settings.asrTestConsent': '测试会使用麦克风,并可能将音频发送给 Android 系统默认语音识别服务。是否继续?', 'settings.asrTestStarted': 'ASR 测试已开始,请朗读测试内容', 'settings.asrTestStopped': 'ASR 测试已停止', 'settings.asrTestRestarting': '识别 session 重建中…', 'settings.asrTestPermission': '正在请求麦克风权限…', 'settings.asrTestPermissionDenied': '未授予麦克风权限,无法测试 ASR', 'settings.asrTestPermissionError': '系统语音识别服务未获麦克风授权,请打开权限设置后重试', 'settings.asrTestUnavailable': '当前设备没有可用的系统语音识别服务', 'settings.asrTestBusy': '已有 ASR 测试正在运行', 'settings.asrTestCopyOk': '测试日志已复制', 'settings.asrTestCopyFailed': '复制测试日志失败', 'settings.asrTestLogEmpty': '暂无测试日志', 'settings.asrTestMeta': '设备信息', 'settings.asrTestSummary': '测试摘要', 'settings.asrTestEvent': '事件日志', 'settings.asrTestCloud': '系统默认识别服务可能使用云端 ASR;本测试不上传日志', 'settings.asrTestStatus': '状态:{status}',
|
|
1089
1075
|
'settings.servers': '服务器地址(可多个)', 'settings.speedTest': '测速', 'settings.add': '添加',
|
|
1090
|
-
'settings.asrTestEngineSettings': '语音引擎设置',
|
|
1091
1076
|
'settings.scanTitle': '扫码连接', 'settings.scanDesc': '实时相机识别二维码,也可拍照/相册本地解码;无需谷歌服务', 'settings.scan': '扫码',
|
|
1092
1077
|
'settings.notifyTitle': '通知', 'settings.notifyDesc': '收到审批/提问时推送', 'settings.announcementHistoryTitle': '历史公告', 'settings.announcementHistoryDesc': '查看以前收到的公告', 'settings.announcementHistory': '查看记录',
|
|
1093
1078
|
'settings.testNotifyTitle': '测试通知', 'settings.testNotifyDesc': '立即排一条 3 秒后的本地通知,用于排查系统通知限制', 'settings.testNotify': '发送测试通知',
|
|
@@ -1267,7 +1252,7 @@
|
|
|
1267
1252
|
'fs.notFound': 'Directory not found', 'fs.forbidden': 'Path is outside the allowed roots. Registered DSH workspaces are allowed automatically; configure DSH_REMOTE_FS_ROOT for other directories.',
|
|
1268
1253
|
'fs.loadFailed': 'Load failed: {msg}', 'fs.loadFailedToast': 'Failed to load file list: {msg}',
|
|
1269
1254
|
'fs.emptyDir': 'Empty directory', 'fs.dir': 'Directory',
|
|
1270
|
-
'fs.downloadStarted': 'Downloading to "Download/dsh-remote"', 'fs.downloadFailed': '
|
|
1255
|
+
'fs.downloadStarted': 'Downloading to "Download/dsh-remote"', 'fs.downloadFailed': 'Download failed: {msg}',
|
|
1271
1256
|
'fs.downloadUnsupported': 'This app version cannot download to the system folder, please update',
|
|
1272
1257
|
'fs.uploadCancelled': 'Upload cancelled', 'fs.pausedErr': 'Paused', 'fs.cancelledErr': 'Cancelled',
|
|
1273
1258
|
'fs.noTokenToast': 'Set a token in Settings first', 'fs.tooLarge': 'File exceeds the 2GB limit', 'fs.uploadBusy': 'Another upload is active, pause or cancel it first',
|
|
@@ -1283,6 +1268,7 @@
|
|
|
1283
1268
|
'update.found': 'New version v{version}{notes}', 'update.foundTitle': 'Update available', 'update.foundBody': 'v{version} is available',
|
|
1284
1269
|
'update.latestV': 'Up to date v{version}', 'update.latestRemote': 'Latest version v{version}', 'update.latestToast': 'Already up to date',
|
|
1285
1270
|
'update.checkFailedDesc': 'Check failed: {msg}', 'update.checkFailed': 'Update check failed: {msg}',
|
|
1271
|
+
'update.phase.downloading': 'Downloading…', 'update.phase.verifying': 'Verifying APK…', 'update.phase.complete': 'Download complete', 'update.phase.legacy': 'System download started; install this update for full progress', 'update.busy': 'An update is already downloading',
|
|
1286
1272
|
'update.downloadStarted': 'Downloading, the install page will open when ready', 'update.downloadFailed': 'Could not start download: {msg}',
|
|
1287
1273
|
'update.corrupted': 'Downloaded file is corrupted, please retry', 'update.serverFileMissing': 'The file for this version is not on the server yet, please try again later',
|
|
1288
1274
|
'update.installUnsupported': 'This version cannot install in-app, opening browser download',
|
|
@@ -1318,9 +1304,7 @@
|
|
|
1318
1304
|
'settings.modelNoModels': 'No custom models; using the provider default catalog', 'settings.modelCandidates': 'Discovered models', 'settings.modelSelectAll': 'Select all', 'settings.modelSelectNone': 'Clear selection', 'settings.modelAddSelected': 'Add selected models',
|
|
1319
1305
|
'settings.modelAdded': 'Models added to the catalog', 'settings.modelSaved': 'Model configuration saved', 'settings.modelSaveFailed': 'Could not save model configuration: {msg}', 'settings.modelOpenConfigFailed': 'Could not open config file: {msg}', 'settings.modelOpenedConfig': 'Requested DSH config file',
|
|
1320
1306
|
'settings.modelLive': 'Takes effect immediately', 'settings.modelRestart': 'Restart DSH to apply',
|
|
1321
|
-
'settings.asrTestTitle': 'ASR model test (Xiaomi)', 'settings.asrTestDesc': 'Use Android SpeechRecognizer to test Xiaomi/system cloud speech recognition. Test logs are not uploaded.', 'settings.asrTestInstruction': 'Keep this page in the foreground and read Chinese text containing pauses, numbers, and an English API name. The test runs for about 60 seconds and records partial/final results, session rebuilds, and errors.', 'settings.asrTestStart': 'Start test', 'settings.asrTestStop': 'Stop test', 'settings.asrTestRestarting': 'Rebuilding recognition session…', 'settings.asrTestCopy': 'Copy log', 'settings.asrTestClear': 'Clear log', 'settings.asrTestPermissionSettings': 'Permission settings', 'settings.asrTestNativeOnly': 'Available only in the Android app', 'settings.asrTestWebUnsupported': 'Native ASR diagnostics are unavailable in a browser. Open this page in the Android app.', 'settings.asrTestConsent': 'This test uses the microphone and may send audio to the Android system default speech recognition service. Continue?', 'settings.asrTestStarted': 'ASR test started; read the test content aloud', 'settings.asrTestStopped': 'ASR test stopped', 'settings.asrTestPermission': 'Requesting microphone permission…', 'settings.asrTestPermissionDenied': 'Microphone permission was not granted; ASR cannot be tested', 'settings.asrTestPermissionError': 'The system speech service lacks microphone authorization. Open permission settings and try again.', 'settings.asrTestUnavailable': 'No system speech recognition service is available on this device', 'settings.asrTestBusy': 'An ASR test is already running', 'settings.asrTestCopyOk': 'Test log copied', 'settings.asrTestCopyFailed': 'Could not copy the test log', 'settings.asrTestLogEmpty': 'No test log yet', 'settings.asrTestMeta': 'Device information', 'settings.asrTestSummary': 'Test summary', 'settings.asrTestEvent': 'Event log', 'settings.asrTestCloud': 'The system default recognition service may use cloud ASR; this test does not upload logs', 'settings.asrTestStatus': 'Status: {status}',
|
|
1322
1307
|
'settings.servers': 'Servers (one or more)', 'settings.speedTest': 'Test', 'settings.add': 'Add',
|
|
1323
|
-
'settings.asrTestEngineSettings': 'Speech engine settings',
|
|
1324
1308
|
'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': 'Use the live camera or photo/gallery QR decoding; no Google services required', 'settings.scan': 'Scan',
|
|
1325
1309
|
'settings.notifyTitle': 'Notifications', 'settings.notifyDesc': 'Push on approvals / questions', 'settings.announcementHistoryTitle': 'Announcement history', 'settings.announcementHistoryDesc': 'Review announcements you received before', 'settings.announcementHistory': 'View history',
|
|
1326
1310
|
'settings.testNotifyTitle': 'Test notification', 'settings.testNotifyDesc': 'Schedule a local notification in 3 seconds to verify the notification pipeline', 'settings.testNotify': 'Send test notification',
|
|
@@ -1390,6 +1374,7 @@
|
|
|
1390
1374
|
<script src="jsqr.min.js"></script>
|
|
1391
1375
|
<script src="i18n.js"></script>
|
|
1392
1376
|
<script src="theme.js"></script>
|
|
1377
|
+
<script src="genui.js"></script>
|
|
1393
1378
|
<script src="md.js"></script>
|
|
1394
1379
|
<script src="vendor/gsap/gsap.min.js"></script>
|
|
1395
1380
|
<script src="motion.js"></script>
|
package/public/md.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* 同时支持浏览器全局 window.mdToHtml 与 Node CommonJS module.exports。
|
|
7
7
|
*/
|
|
8
8
|
(function (root, factory) {
|
|
9
|
-
if (typeof module === 'object' && module.exports) module.exports = factory()
|
|
10
|
-
else root.mdToHtml = factory()
|
|
11
|
-
})(typeof self !== 'undefined' ? self : this, function () {
|
|
9
|
+
if (typeof module === 'object' && module.exports) module.exports = factory(require('./genui.js'))
|
|
10
|
+
else root.mdToHtml = factory(root.DshGenUi)
|
|
11
|
+
})(typeof self !== 'undefined' ? self : this, function (genui) {
|
|
12
12
|
'use strict'
|
|
13
13
|
|
|
14
14
|
function escapeHtml(s) {
|
|
@@ -172,6 +172,19 @@
|
|
|
172
172
|
var code = parts[i]
|
|
173
173
|
code = code.replace(/^\n/, '')
|
|
174
174
|
if (code.slice(-1) === '\n') code = code.slice(0, -1)
|
|
175
|
+
var fence = code.match(/^([^\n]*)\n([\s\S]*)$/)
|
|
176
|
+
// Wait for the closing fence: streaming partial JSON stays readable as source.
|
|
177
|
+
if (genui && fence && i < parts.length - 1 && fence[2].length <= 200000) {
|
|
178
|
+
var language = fence[1].trim().toLowerCase()
|
|
179
|
+
try {
|
|
180
|
+
if (/^(?:dsh-ui|json dsh-ui|dsh-ui json)$/.test(language)) {
|
|
181
|
+
out += genui.render(JSON.parse(fence[2])); continue
|
|
182
|
+
}
|
|
183
|
+
if (language === 'html' || language === 'htm') {
|
|
184
|
+
out += genui.html(fence[2]) + '<details><summary>HTML / 源码</summary><pre><code>' + escapeHtml(fence[2]) + '</code></pre></details>'; continue
|
|
185
|
+
}
|
|
186
|
+
} catch (_) { /* Invalid or over-budget specs remain source, never execute. */ }
|
|
187
|
+
}
|
|
175
188
|
out += '<pre><code>' + escapeHtml(code) + '</code></pre>'
|
|
176
189
|
} else {
|
|
177
190
|
var escaped = escapeHtml(parts[i])
|
package/public/styles.css
CHANGED
|
@@ -907,15 +907,6 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
|
|
|
907
907
|
.model-editor-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
|
908
908
|
.model-editor-error { margin: 0; color: var(--dsr-error); font-size: 12px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
909
909
|
.model-readonly { color: var(--dsr-warning); font-size: 12px; }
|
|
910
|
-
.feature-test-shell { padding: 14px; }
|
|
911
|
-
.feature-test-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--dsr-line); }
|
|
912
|
-
.feature-test-note { margin-top: 12px; padding: 10px 11px; border: 1px solid var(--dsr-info-line); border-radius: 9px; background: var(--dsr-info-soft); color: var(--dsr-info); font-size: 12px; line-height: 1.55; }
|
|
913
|
-
.feature-test-actions { display: flex; flex-wrap: wrap; gap: 7px; padding: 12px 0 8px; }
|
|
914
|
-
.feature-test-status { min-height: 20px; padding: 4px 0; font-size: 12px; }
|
|
915
|
-
.feature-test-status.error { color: var(--dsr-error); }
|
|
916
|
-
.feature-test-status.ok { color: var(--dsr-success); }
|
|
917
|
-
.feature-test-summary { margin-top: 6px; color: var(--dsr-text); font-size: 12px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
918
|
-
.feature-test-log { max-height: 420px; min-height: 170px; margin: 10px 0 0; padding: 11px; overflow: auto; border: 1px solid var(--dsr-line); border-radius: 9px; background: var(--dsr-bg-2); color: var(--dsr-text); font: 11px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
919
910
|
@media (max-width: 520px) {
|
|
920
911
|
.model-settings-head { flex-direction: column; }
|
|
921
912
|
.model-settings-head .setting-actions { width: 100%; justify-content: flex-start; }
|
|
@@ -926,8 +917,6 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
|
|
|
926
917
|
.model-reasoning-entry .model-input:nth-child(2), .model-reasoning-entry .model-input:nth-child(3) { grid-column: 1 / -1; }
|
|
927
918
|
.model-reasoning-entry .model-entry-remove { grid-column: 2; grid-row: 1; }
|
|
928
919
|
.model-reasoning-default { grid-template-columns: 1fr; gap: 4px; }
|
|
929
|
-
.feature-test-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
930
|
-
.feature-test-actions .mini-btn { width: 100%; }
|
|
931
920
|
}
|
|
932
921
|
.setting-row {
|
|
933
922
|
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
@@ -1326,3 +1315,7 @@ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible
|
|
|
1326
1315
|
.overview-pulse-card { grid-template-columns:1fr auto; }
|
|
1327
1316
|
.overview-metric-grid { grid-template-columns:repeat(4,minmax(0,1fr)); }
|
|
1328
1317
|
}
|
|
1318
|
+
|
|
1319
|
+
.update-download-progress { margin-top: 12px; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
1320
|
+
.update-download-progress progress { display: block; width: 100%; height: 10px; margin: 7px 0; accent-color: var(--dsr-accent, #2563eb); }
|
|
1321
|
+
#update-download-detail { color: var(--dsr-text-muted); font-variant-numeric: tabular-nums; }
|
package/public/update.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.25",
|
|
3
3
|
"apkUrl": "dsh-remote.apk",
|
|
4
|
-
"sha256": "
|
|
5
|
-
"releasedAt": "2026-09-
|
|
6
|
-
"notes": "0.6.
|
|
4
|
+
"sha256": "56e3aa905bcc4a746c604af7b09a5bfe50c21deafd1b04756a345bd03cc44828",
|
|
5
|
+
"releasedAt": "2026-09-16T06:56:53.046Z",
|
|
6
|
+
"notes": "0.6.25:适配 DSH 0.1.5 的实时思考、文件消息与 V3 会话统计;修复 Windows 跨盘文件访问和空会话持久归档;完善 APK 下载进度与校验;新增 dsh-ui 常用组件及柱状图、折线图、环形图只读预览,HTML 代码块支持隔离预览,未支持的高级组件保留原始规格。",
|
|
7
7
|
"history": [
|
|
8
|
+
{
|
|
9
|
+
"version": "0.6.25",
|
|
10
|
+
"notes": "0.6.25:适配 DSH 0.1.5 的实时思考、文件消息与 V3 会话统计;修复 Windows 跨盘文件访问和空会话持久归档;完善 APK 下载进度与校验;新增 dsh-ui 常用组件及柱状图、折线图、环形图只读预览,HTML 代码块支持隔离预览,未支持的高级组件保留原始规格。"
|
|
11
|
+
},
|
|
8
12
|
{
|
|
9
13
|
"version": "0.6.24",
|
|
10
14
|
"notes": "0.6.24:同步 App 与宽屏客户端的会话命名体验;未命名会话显示为“新会话”并保留短标识,创建会话时可选择指定工作区;补充空会话清理与相关回归验证。"
|
|
@@ -40,10 +44,6 @@
|
|
|
40
44
|
{
|
|
41
45
|
"version": "0.6.16",
|
|
42
46
|
"notes": "0.6.16:Windows 端 DSH 服务启动/重启接入 Windows Service(实验性功能),启动后继续检查 DSH HTTP 与 mux/host 实时通道;空对话退出时清理 App 本地残留;网关控制台主机 IP 改为可逐项启用的地址表,关闭地址不再进入配对二维码、诊断和防火墙建议;Android 设置新增模型配置、自定义模型思考深度档位和小米/系统 ASR 功能测试,支持复制设备、权限、partial/final、session 重建和错误诊断日志。"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"version": "0.6.15",
|
|
46
|
-
"notes": "0.6.15:修复拖拽候选项拦截普通竖向滑动的问题,卡片中间也可自然滚动;仅在长按真正进入拖拽后锁定页面,并补充指针结束、取消、失焦和页面隐藏时的解锁清理。保留边缘自动滚动、占位符同步重排、主列表/工作台工作区与组内对话拖动。"
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
49
|
}
|
package/public/version.json
CHANGED