dsh-remote-plugin 0.6.6 → 0.6.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/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/app.js +419 -39
- package/public/desktop/desktop.css +45 -2
- package/public/desktop/desktop.html +40 -0
- package/public/desktop/desktop.js +283 -17
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +58 -2
- package/public/styles.css +44 -0
- package/public/update.json +8 -4
- package/public/version.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
html, body { height: 100%; margin: 0; overflow: hidden; overscroll-behavior: none; background: var(--dsr-bg); color: var(--dsr-text); }
|
|
3
3
|
.hidden { display: none !important; }
|
|
4
4
|
.ds-app { position: fixed; inset: 0; display: flex; overflow: hidden; }
|
|
5
|
-
.ds-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; border-right: 1px solid var(--dsr-line); background: var(--dsr-panel); }
|
|
5
|
+
.ds-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; border-right: 1px solid var(--dsr-line); background: var(--dsr-panel); overflow: hidden; }
|
|
6
6
|
.ds-brand { display: flex; align-items: center; gap: 8px; padding: 14px 14px 10px; font-weight: 700; font-size: 15px; }
|
|
7
7
|
.ds-logo { color: var(--dsr-accent-strong); }
|
|
8
8
|
.ds-brand-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -41,7 +41,8 @@ a.ds-btn { text-decoration: none; }
|
|
|
41
41
|
.ds-session-meta { display: flex; gap: 7px; font-size: 11px; color: var(--dsr-muted); align-items: center; }
|
|
42
42
|
.ds-session-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsr-line); }
|
|
43
43
|
.ds-session-dot.running { background: var(--dsr-warning); }
|
|
44
|
-
.ds-sidebar-foot { border-top: 1px solid var(--dsr-line); padding: 8px; display: flex; flex-direction: column; gap: 3px; }
|
|
44
|
+
.ds-sidebar-foot { border-top: 1px solid var(--dsr-line); padding: 8px; display: flex; flex-direction: column; gap: 3px; max-height: 58vh; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
|
|
45
|
+
.ds-sidebar-foot > * { flex-shrink: 0; }
|
|
45
46
|
.ds-nav-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: none; background: transparent; color: var(--dsr-text); font: inherit; font-size: 13px; padding: 9px 10px; border-radius: 9px; cursor: pointer; }
|
|
46
47
|
.ds-nav-item:hover { background: var(--dsr-bg-2); }
|
|
47
48
|
.ds-nav-item.active { background: var(--dsr-accent-soft); color: var(--dsr-accent-strong); }
|
|
@@ -80,6 +81,48 @@ a.ds-btn { text-decoration: none; }
|
|
|
80
81
|
.ds-feedback-desc { font-size: 11px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
81
82
|
@keyframes ds-feedback-in { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: translateY(0) } }
|
|
82
83
|
|
|
84
|
+
/* ---------- 工作台 ---------- */
|
|
85
|
+
.ds-workbench { border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-bg-2); padding: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
86
|
+
.ds-wb-badge { font-size: 11.5px; font-weight: 700; color: var(--dsr-accent-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
87
|
+
.ds-wb-unbound, .ds-wb-bound { display: flex; flex-direction: column; gap: 6px; }
|
|
88
|
+
.ds-wb-bind-btn { justify-content: center; width: 100%; }
|
|
89
|
+
.ds-wb-api-hint { font-size: 10.5px; color: var(--dsr-warning); line-height: 1.4; }
|
|
90
|
+
.ds-wb-head { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; border: none; background: transparent; color: var(--dsr-text); font: inherit; font-size: 12.5px; font-weight: 700; padding: 2px; cursor: pointer; }
|
|
91
|
+
.ds-wb-caret { flex: none; color: var(--dsr-muted); font-size: 10px; width: 12px; text-align: center; }
|
|
92
|
+
.ds-wb-head-text { flex: 1; min-width: 0; }
|
|
93
|
+
.ds-wb-actions { display: flex; gap: 6px; }
|
|
94
|
+
.ds-wb-actions .ds-mini-btn { flex: 1; }
|
|
95
|
+
.ds-wb-panel { border-top: 1px solid var(--dsr-divider); padding-top: 6px; display: flex; flex-direction: column; gap: 5px; max-height: 42vh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
|
|
96
|
+
.ds-wb-panel > * { flex-shrink: 0; }
|
|
97
|
+
.ds-wb-panel-title { font-size: 10.5px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; text-transform: uppercase; }
|
|
98
|
+
.ds-wb-project { border: 1px solid var(--dsr-line); border-radius: 9px; background: var(--dsr-panel); overflow: hidden; }
|
|
99
|
+
.ds-wb-project-head { width: 100%; display: flex; align-items: center; gap: 6px; border: none; background: transparent; color: var(--dsr-text); font: inherit; font-size: 12.5px; padding: 6px 8px; cursor: pointer; text-align: left; }
|
|
100
|
+
.ds-wb-project-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
101
|
+
.ds-wb-project-count { flex: none; font-size: 10.5px; color: var(--dsr-muted); background: var(--dsr-bg-2); border-radius: 999px; padding: 0 7px; line-height: 16px; }
|
|
102
|
+
.ds-wb-project-body { display: flex; flex-direction: column; gap: 3px; padding: 2px 6px 7px; }
|
|
103
|
+
.ds-wb-new-session { align-self: flex-start; margin: 2px 0 3px; }
|
|
104
|
+
.ds-wb-session { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; border: none; background: transparent; color: var(--dsr-text); font: inherit; font-size: 12px; padding: 5px 7px; border-radius: 7px; cursor: pointer; }
|
|
105
|
+
.ds-wb-session.current { background: var(--dsr-accent-soft); color: var(--dsr-accent-strong); }
|
|
106
|
+
.ds-wb-session-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsr-line); }
|
|
107
|
+
.ds-wb-session-dot.running { background: var(--dsr-warning); }
|
|
108
|
+
.ds-wb-session-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
109
|
+
.ds-wb-session-empty, .ds-wb-empty { font-size: 11px; color: var(--dsr-muted); padding: 5px 7px; line-height: 1.5; }
|
|
110
|
+
.ds-wb-unbind-panel { align-self: flex-end; }
|
|
111
|
+
.ds-archived-toggle { width: 100%; border: none; background: transparent; color: var(--dsr-muted); font: inherit; font-size: 11px; letter-spacing: .4px; padding: 7px 4px; cursor: pointer; text-align: center; }
|
|
112
|
+
|
|
113
|
+
/* ---------- 工作台绑定弹窗 ---------- */
|
|
114
|
+
.ds-wb-modal-card { width: min(620px, 100%); }
|
|
115
|
+
.ds-wb-modal-desc { font-size: 12.5px; color: var(--dsr-muted); line-height: 1.5; }
|
|
116
|
+
.ds-wb-fsbar, .ds-wb-manual { display: flex; align-items: center; gap: 8px; }
|
|
117
|
+
.ds-wb-fs-path { flex: 1; min-width: 0; font-size: 12px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
118
|
+
.ds-wb-fs-list { border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); max-height: 42vh; overflow-y: auto; }
|
|
119
|
+
.ds-wb-fs-row { display: flex; align-items: center; gap: 9px; padding: 8px 11px; border-bottom: 1px solid var(--dsr-divider); font-size: 13px; cursor: pointer; }
|
|
120
|
+
.ds-wb-fs-row:last-child { border-bottom: none; }
|
|
121
|
+
.ds-wb-fs-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
122
|
+
.ds-wb-select { flex: none; }
|
|
123
|
+
.ds-wb-manual input { flex: 1; min-width: 0; background: var(--dsr-panel); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 10px; padding: 8px 10px; font: inherit; font-size: 13px; outline: none; }
|
|
124
|
+
.ds-wb-manual input:focus { border-color: var(--dsr-accent-line); }
|
|
125
|
+
|
|
83
126
|
/* 写反馈弹层 */
|
|
84
127
|
.ds-fb-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
85
128
|
.ds-fb-chip {
|
|
@@ -30,6 +30,24 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
<div class="ds-session-list" id="session-list"></div>
|
|
32
32
|
<div class="ds-sidebar-foot">
|
|
33
|
+
<div id="workbench-box" class="ds-workbench">
|
|
34
|
+
<div id="wb-unbound" class="ds-wb-unbound">
|
|
35
|
+
<div class="ds-wb-badge" data-i18n="wb.unbound">DSH Remote(未绑定)</div>
|
|
36
|
+
<button id="btn-wb-bind" class="ds-btn ds-wb-bind-btn" data-i18n="wb.bind">绑定工作台</button>
|
|
37
|
+
<div id="wb-api-hint" class="ds-wb-api-hint hidden" data-i18n="wb.apiMissing">工作台接口不可用(需更新网关)</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="wb-bound" class="ds-wb-bound hidden">
|
|
40
|
+
<button id="wb-head" class="ds-wb-head" type="button" aria-expanded="false">
|
|
41
|
+
<span id="wb-caret" class="ds-wb-caret" aria-hidden="true">▸</span>
|
|
42
|
+
<span id="wb-head-text" class="ds-wb-head-text"></span>
|
|
43
|
+
</button>
|
|
44
|
+
<div class="ds-wb-actions">
|
|
45
|
+
<button id="btn-wb-path" class="ds-mini-btn" data-i18n="wb.viewPath">已绑定 workspace</button>
|
|
46
|
+
<button id="btn-wb-unbind" class="ds-mini-btn" data-i18n="wb.unbind">解绑</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div id="wb-panel" class="ds-wb-panel hidden"></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
33
51
|
<button class="ds-nav-item" data-view="view-files"><span class="ds-nav-ico">⇅</span><span data-i18n="ds.files">文件传输</span></button>
|
|
34
52
|
<button class="ds-nav-item" data-view="view-settings"><span class="ds-nav-ico">⚙</span><span data-i18n="ds.settings">设置</span></button>
|
|
35
53
|
<div class="ds-feedback">
|
|
@@ -307,6 +325,28 @@
|
|
|
307
325
|
</div>
|
|
308
326
|
</div>
|
|
309
327
|
|
|
328
|
+
<div id="modal-workbench" class="ds-modal hidden">
|
|
329
|
+
<div class="ds-modal-card ds-wb-modal-card">
|
|
330
|
+
<div class="ds-modal-title" data-i18n="wb.modalTitle">绑定工作台</div>
|
|
331
|
+
<div class="ds-modal-body">
|
|
332
|
+
<div class="ds-wb-modal-desc" data-i18n="wb.modalDesc">选择工作台根目录,其下每个子文件夹会自动成为项目工作区。</div>
|
|
333
|
+
<div class="ds-wb-fsbar">
|
|
334
|
+
<button id="wb-fs-up" class="ds-btn" data-i18n="wb.up">上级</button>
|
|
335
|
+
<span id="wb-fs-path" class="ds-wb-fs-path">~</span>
|
|
336
|
+
<button id="wb-fs-home" class="ds-btn" data-i18n="wb.home">根目录</button>
|
|
337
|
+
</div>
|
|
338
|
+
<div id="wb-fs-list" class="ds-wb-fs-list"></div>
|
|
339
|
+
<div class="ds-wb-manual">
|
|
340
|
+
<input id="wb-path-input" type="text" autocomplete="off" data-i18n-placeholder="wb.pathPlaceholder" placeholder="或手动输入绝对路径…">
|
|
341
|
+
<button id="btn-wb-bind-manual" class="ds-btn primary" data-i18n="wb.bind">绑定</button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="ds-modal-actions">
|
|
345
|
+
<button id="btn-wb-modal-close" class="ds-btn" data-i18n="ds.cancel">取消</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
310
350
|
<!-- 赞赏支持模态 -->
|
|
311
351
|
<div id="modal-donate" class="ds-modal hidden">
|
|
312
352
|
<div class="ds-modal-card">
|
|
@@ -62,6 +62,8 @@ const state = {
|
|
|
62
62
|
pollSeq: { mux: 0, host: 0 },
|
|
63
63
|
fs: { path: null, initial: null, loaded: false },
|
|
64
64
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
65
|
+
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
66
|
+
archivedIds: [],
|
|
65
67
|
view: 'sessions'
|
|
66
68
|
}
|
|
67
69
|
const streams = {}
|
|
@@ -1102,6 +1104,7 @@ async function refreshSessions() {
|
|
|
1102
1104
|
state.sessions = v.items || []
|
|
1103
1105
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1104
1106
|
renderSessions()
|
|
1107
|
+
scheduleWorkbenchRefresh()
|
|
1105
1108
|
}
|
|
1106
1109
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1107
1110
|
function sessionWorkspaceLabel(s) {
|
|
@@ -1128,31 +1131,49 @@ function sortedSessions() {
|
|
|
1128
1131
|
return items.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1129
1132
|
}
|
|
1130
1133
|
function renderSessions() {
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1134
|
+
const allItems = sortedSessions()
|
|
1135
|
+
const wbIds = new Set()
|
|
1136
|
+
if (state.wb.bound && state.wb.projects) for (const w of state.wb.projects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1137
|
+
const root = state.wb.bound ? state.wb.path : ''
|
|
1138
|
+
const visible = allItems.filter(s => !(state.wb.bound && (wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))))
|
|
1139
|
+
const archivedSet = new Set(state.archivedIds || [])
|
|
1140
|
+
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1141
|
+
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1142
|
+
const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
|
|
1143
|
+
const renderItems = (items) => {
|
|
1144
|
+
let lastWorkspace = null
|
|
1145
|
+
const rows = []
|
|
1146
|
+
for (const s of items) {
|
|
1147
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1148
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1149
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1150
|
+
rows.push(`<div class="ds-session-group" title="${esc(workspace)}"><span class="ds-session-group-icon" aria-hidden="true">⌂</span><span class="ds-session-group-name">${esc(workspaceName)}</span></div>`)
|
|
1151
|
+
lastWorkspace = workspace
|
|
1152
|
+
}
|
|
1153
|
+
const title = titleOf(s)
|
|
1154
|
+
rows.push(`<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1155
|
+
<span class="ds-session-title">${esc(title)}</span>
|
|
1156
|
+
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1157
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s.updatedAt)}</span>
|
|
1158
|
+
</button>`)
|
|
1140
1159
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
</button>`)
|
|
1147
|
-
}
|
|
1148
|
-
const html = rows.join('') || `<div class="ds-empty">${t('ds.sessionsEmpty')}</div>`
|
|
1160
|
+
return rows.join('')
|
|
1161
|
+
}
|
|
1162
|
+
const divider = archived.length ? `<button class="ds-archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1163
|
+
const hiddenByWorkbench = allItems.length - visible.length
|
|
1164
|
+
const html = renderItems(main) + divider + (showArchived ? renderItems(archived) : '') || `<div class="ds-empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('ds.sessionsEmpty'))}</div>`
|
|
1149
1165
|
$('session-list').innerHTML = html
|
|
1150
1166
|
$('mobile-session-list').innerHTML = html
|
|
1151
1167
|
$('session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1152
1168
|
$('mobile-session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1153
1169
|
const sort = $('session-sort')
|
|
1154
1170
|
if (sort) sort.value = state.sessionSort
|
|
1171
|
+
document.querySelectorAll('[data-archived-toggle]').forEach(b => b.addEventListener('click', () => {
|
|
1172
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
1173
|
+
renderSessions()
|
|
1174
|
+
}))
|
|
1155
1175
|
document.querySelectorAll('[data-id]').forEach(b => b.addEventListener('click', () => openSession(b.dataset.id)))
|
|
1176
|
+
renderWorkbench()
|
|
1156
1177
|
}
|
|
1157
1178
|
|
|
1158
1179
|
async function openSession(id) {
|
|
@@ -1592,6 +1613,228 @@ function fsUp() {
|
|
|
1592
1613
|
}
|
|
1593
1614
|
}
|
|
1594
1615
|
|
|
1616
|
+
/* ---------------- 工作台绑定 / 项目会话 ---------------- */
|
|
1617
|
+
function wbPathKey(p) {
|
|
1618
|
+
let value = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1619
|
+
if (value.length > 1) value = value.replace(/\/+$/, '')
|
|
1620
|
+
const windows = /^[A-Za-z]:\//.test(value) || /Windows/i.test(navigator.platform || navigator.userAgent || '')
|
|
1621
|
+
return windows ? value.toLowerCase() : value
|
|
1622
|
+
}
|
|
1623
|
+
function wbBaseName(p) {
|
|
1624
|
+
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
1625
|
+
return value.split(/[\\/]/).pop() || value
|
|
1626
|
+
}
|
|
1627
|
+
function wbStrictInside(pathValue, rootValue) {
|
|
1628
|
+
const pathKey = wbPathKey(pathValue)
|
|
1629
|
+
const rootKey = wbPathKey(rootValue)
|
|
1630
|
+
if (!pathKey || !rootKey || pathKey === rootKey) return false
|
|
1631
|
+
return pathKey.startsWith(rootKey.endsWith('/') ? rootKey : rootKey + '/')
|
|
1632
|
+
}
|
|
1633
|
+
function wbJoin(root, name) {
|
|
1634
|
+
const raw = String(root || '')
|
|
1635
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1636
|
+
return raw.replace(/[\\/]+$/, '') + separator + String(name || '')
|
|
1637
|
+
}
|
|
1638
|
+
function wbFsParent(p) {
|
|
1639
|
+
if (!p) return null
|
|
1640
|
+
const raw = String(p)
|
|
1641
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1642
|
+
const index = raw.lastIndexOf(separator)
|
|
1643
|
+
if (index <= 0 || /^[A-Za-z]:$/.test(raw.slice(0, index))) return null
|
|
1644
|
+
return raw.slice(0, index)
|
|
1645
|
+
}
|
|
1646
|
+
async function wbGateway(method, pathname, body) {
|
|
1647
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
1648
|
+
if (body !== undefined) {
|
|
1649
|
+
options.headers['content-type'] = 'application/json'
|
|
1650
|
+
options.body = JSON.stringify(body)
|
|
1651
|
+
}
|
|
1652
|
+
const res = await fetch(apiUrl(pathname), options)
|
|
1653
|
+
if (res.status === 401) throw new Error('AUTH')
|
|
1654
|
+
const data = await res.json().catch(() => ({}))
|
|
1655
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1656
|
+
return data
|
|
1657
|
+
}
|
|
1658
|
+
async function refreshWorkbench({ silent = false } = {}) {
|
|
1659
|
+
if (!state.token) { renderWorkbench(); return }
|
|
1660
|
+
let wb = null
|
|
1661
|
+
try {
|
|
1662
|
+
wb = await wbGateway('GET', '/workbench')
|
|
1663
|
+
state.wb.apiMissing = false
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
1666
|
+
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
1667
|
+
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
1668
|
+
}
|
|
1669
|
+
const wl = await safeRpc('workspace.list', {}, '')
|
|
1670
|
+
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
1671
|
+
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
1672
|
+
if (!wb.bound) {
|
|
1673
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1674
|
+
renderWorkbench()
|
|
1675
|
+
renderSessions()
|
|
1676
|
+
return
|
|
1677
|
+
}
|
|
1678
|
+
state.wb.bound = true
|
|
1679
|
+
state.wb.path = wb.path || ''
|
|
1680
|
+
state.wb.title = wb.title || ''
|
|
1681
|
+
if (!wl) { state.wb.projects = []; renderWorkbench(); renderSessions(); return }
|
|
1682
|
+
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
1683
|
+
try {
|
|
1684
|
+
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1685
|
+
if (listRes.ok) {
|
|
1686
|
+
const listData = await listRes.json().catch(() => ({}))
|
|
1687
|
+
const have = new Set(items.map(w => wbPathKey(w.path)))
|
|
1688
|
+
for (const entry of listData.entries || []) {
|
|
1689
|
+
if (entry.type !== 'dir') continue
|
|
1690
|
+
const projectPath = wbJoin(state.wb.path, entry.name)
|
|
1691
|
+
if (have.has(wbPathKey(projectPath))) continue
|
|
1692
|
+
try {
|
|
1693
|
+
const created = await rpc('workspace.create', { path: projectPath })
|
|
1694
|
+
if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
|
|
1695
|
+
} catch {}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
} catch {}
|
|
1699
|
+
state.wb.projects = items
|
|
1700
|
+
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
1701
|
+
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true }))
|
|
1702
|
+
renderWorkbench()
|
|
1703
|
+
renderSessions()
|
|
1704
|
+
}
|
|
1705
|
+
function renderWorkbench() {
|
|
1706
|
+
const box = $('workbench-box')
|
|
1707
|
+
if (!box) return
|
|
1708
|
+
const unbound = $('wb-unbound')
|
|
1709
|
+
const bound = $('wb-bound')
|
|
1710
|
+
const hint = $('wb-api-hint')
|
|
1711
|
+
if (!state.wb.bound) {
|
|
1712
|
+
unbound.classList.remove('hidden')
|
|
1713
|
+
bound.classList.add('hidden')
|
|
1714
|
+
hint?.classList.toggle('hidden', !state.wb.apiMissing)
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1717
|
+
unbound.classList.add('hidden')
|
|
1718
|
+
bound.classList.remove('hidden')
|
|
1719
|
+
$('wb-head-text').textContent = t('wb.bound', { title: state.wb.title || wbBaseName(state.wb.path) })
|
|
1720
|
+
$('wb-head').setAttribute('aria-expanded', state.wb.expanded ? 'true' : 'false')
|
|
1721
|
+
$('wb-caret').textContent = state.wb.expanded ? '▾' : '▸'
|
|
1722
|
+
const panel = $('wb-panel')
|
|
1723
|
+
panel.classList.toggle('hidden', !state.wb.expanded)
|
|
1724
|
+
if (!state.wb.expanded) return
|
|
1725
|
+
const projects = state.wb.projects || []
|
|
1726
|
+
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1727
|
+
html += projects.length ? projects.map(w => {
|
|
1728
|
+
const id = String(w.workspaceId || '')
|
|
1729
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1730
|
+
const open = state.wb.open === id
|
|
1731
|
+
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
1732
|
+
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
1733
|
+
<span class="ds-wb-caret" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1734
|
+
<span class="ds-wb-project-title" title="${esc(w.path)}">${esc(w.title || wbBaseName(w.path) || short(id))}</span>
|
|
1735
|
+
<span class="ds-wb-project-count">${sessions.length}</span>
|
|
1736
|
+
</button>
|
|
1737
|
+
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
1738
|
+
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
1739
|
+
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}"><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(titleOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
|
|
1740
|
+
</div>
|
|
1741
|
+
</div>`
|
|
1742
|
+
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1743
|
+
html += `<button type="button" class="ds-mini-btn ds-wb-unbind-panel" data-wb-unbind-panel>${esc(t('wb.unbind'))}</button>`
|
|
1744
|
+
panel.innerHTML = html
|
|
1745
|
+
panel.querySelectorAll('[data-wb-head]').forEach(button => button.addEventListener('click', () => {
|
|
1746
|
+
state.wb.open = state.wb.open === button.dataset.wbHead ? null : button.dataset.wbHead
|
|
1747
|
+
renderWorkbench()
|
|
1748
|
+
}))
|
|
1749
|
+
panel.querySelectorAll('[data-wb-new]').forEach(button => button.addEventListener('click', async () => {
|
|
1750
|
+
const value = await safeRpc('session.create', { workspaceId: button.dataset.wbNew }, '')
|
|
1751
|
+
if (value?.sessionId) { await refreshSessions(); openSession(value.sessionId) }
|
|
1752
|
+
}))
|
|
1753
|
+
panel.querySelectorAll('[data-wb-session]').forEach(button => button.addEventListener('click', () => openSession(button.dataset.wbSession)))
|
|
1754
|
+
panel.querySelectorAll('[data-wb-unbind-panel]').forEach(button => button.addEventListener('click', unbindWorkbench))
|
|
1755
|
+
}
|
|
1756
|
+
const wbFs = { path: null, initial: null }
|
|
1757
|
+
function openWorkbenchModal() {
|
|
1758
|
+
$('modal-workbench').classList.remove('hidden')
|
|
1759
|
+
wbFs.path = null
|
|
1760
|
+
wbFs.initial = null
|
|
1761
|
+
wbFsLoad(null)
|
|
1762
|
+
setTimeout(() => $('wb-path-input').focus(), 50)
|
|
1763
|
+
}
|
|
1764
|
+
function closeWorkbenchModal() { $('modal-workbench').classList.add('hidden') }
|
|
1765
|
+
async function wbFsLoad(dir) {
|
|
1766
|
+
const box = $('wb-fs-list')
|
|
1767
|
+
const target = dir ?? wbFs.path ?? ''
|
|
1768
|
+
box.innerHTML = `<div class="ds-empty">${esc(t('ds.loading'))}</div>`
|
|
1769
|
+
$('wb-fs-path').textContent = target ? '…' + target.slice(-40) : '~'
|
|
1770
|
+
try {
|
|
1771
|
+
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
1772
|
+
const data = await res.json().catch(() => ({}))
|
|
1773
|
+
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1774
|
+
wbFs.path = data.path
|
|
1775
|
+
if (!wbFs.initial) wbFs.initial = data.path
|
|
1776
|
+
$('wb-fs-path').textContent = data.path
|
|
1777
|
+
const dirs = (data.entries || []).filter(e => e.type === 'dir')
|
|
1778
|
+
box.innerHTML = dirs.length ? dirs.map(e => {
|
|
1779
|
+
const p = wbJoin(data.path, e.name)
|
|
1780
|
+
return `<div class="ds-wb-fs-row" data-wb-dir="${esc(p)}"><span>📁</span><span class="ds-wb-fs-name">${esc(e.name)}</span><button type="button" class="ds-btn ds-wb-select" data-wb-select="${esc(p)}">${esc(t('wb.selectDir'))}</button></div>`
|
|
1781
|
+
}).join('') : `<div class="ds-empty">${esc(t('wb.empty'))}</div>`
|
|
1782
|
+
box.querySelectorAll('[data-wb-dir]').forEach(row => row.addEventListener('click', e => { if (!e.target.closest('[data-wb-select]')) wbFsLoad(row.dataset.wbDir) }))
|
|
1783
|
+
box.querySelectorAll('[data-wb-select]').forEach(button => button.addEventListener('click', () => bindWorkbench(button.dataset.wbSelect)))
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
$('wb-fs-path').textContent = target || '~'
|
|
1786
|
+
box.innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function wbFsUp() {
|
|
1790
|
+
if (wbFs.path && wbFs.initial && wbFs.path !== wbFs.initial) {
|
|
1791
|
+
const parent = wbFsParent(wbFs.path)
|
|
1792
|
+
if (parent) wbFsLoad(parent)
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
async function bindWorkbench(rawPath) {
|
|
1796
|
+
const value = String(rawPath || '').trim()
|
|
1797
|
+
if (!value) return toast(t('wb.pathEmpty'), 'err')
|
|
1798
|
+
try {
|
|
1799
|
+
const wb = await wbGateway('POST', '/workbench/bind', { path: value })
|
|
1800
|
+
state.wb = { bound: true, path: wb.path || value, title: wb.title || '', expanded: true, projects: null, open: null, apiMissing: false }
|
|
1801
|
+
const paths = [state.wb.path]
|
|
1802
|
+
try {
|
|
1803
|
+
const res = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1804
|
+
const data = await res.json().catch(() => ({}))
|
|
1805
|
+
for (const e of data.entries || []) if (e.type === 'dir') paths.push(wbJoin(state.wb.path, e.name))
|
|
1806
|
+
} catch {}
|
|
1807
|
+
for (const projectPath of paths) {
|
|
1808
|
+
try { await rpc('workspace.create', { path: projectPath }) } catch {}
|
|
1809
|
+
}
|
|
1810
|
+
closeWorkbenchModal()
|
|
1811
|
+
await refreshWorkbench({ silent: true })
|
|
1812
|
+
await refreshSessions()
|
|
1813
|
+
toast(t('wb.boundOk', { path: state.wb.path }), 'ok')
|
|
1814
|
+
} catch (e) {
|
|
1815
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1816
|
+
toast(t('wb.bindFailed', { msg: e.message }), 'err')
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
async function unbindWorkbench() {
|
|
1820
|
+
if (!confirm(t('wb.unbindConfirm'))) return
|
|
1821
|
+
try {
|
|
1822
|
+
await wbGateway('POST', '/workbench/unbind')
|
|
1823
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1824
|
+
renderWorkbench()
|
|
1825
|
+
renderSessions()
|
|
1826
|
+
toast(t('wb.unboundOk'), 'ok')
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1829
|
+
toast(t('wb.unbindFailed', { msg: e.message }), 'err')
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
let wbRefreshTimer = null
|
|
1833
|
+
function scheduleWorkbenchRefresh() {
|
|
1834
|
+
clearTimeout(wbRefreshTimer)
|
|
1835
|
+
wbRefreshTimer = setTimeout(() => refreshWorkbench({ silent: true }), 400)
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1595
1838
|
/* ---------------- 统计 ---------------- */
|
|
1596
1839
|
function bucketTokens(b) { return (b.input || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0) + (b.output || 0) }
|
|
1597
1840
|
let statsDrawerOpened = false
|
|
@@ -1749,9 +1992,30 @@ function bindUi() {
|
|
|
1749
1992
|
})
|
|
1750
1993
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
|
|
1751
1994
|
$('session-list').addEventListener('click', (e) => {
|
|
1995
|
+
if (e.target.closest('[data-archived-toggle]')) {
|
|
1996
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
1997
|
+
renderSessions()
|
|
1998
|
+
return
|
|
1999
|
+
}
|
|
1752
2000
|
const item = e.target.closest('[data-id]')
|
|
1753
2001
|
if (item) openSession(item.dataset.id)
|
|
1754
2002
|
})
|
|
2003
|
+
$('btn-wb-bind').addEventListener('click', openWorkbenchModal)
|
|
2004
|
+
$('btn-wb-bind-manual').addEventListener('click', () => bindWorkbench($('wb-path-input').value))
|
|
2005
|
+
$('wb-path-input').addEventListener('keydown', e => {
|
|
2006
|
+
if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); bindWorkbench($('wb-path-input').value) }
|
|
2007
|
+
})
|
|
2008
|
+
$('wb-fs-up').addEventListener('click', wbFsUp)
|
|
2009
|
+
$('wb-fs-home').addEventListener('click', () => wbFsLoad(wbFs.initial || null))
|
|
2010
|
+
$('btn-wb-modal-close').addEventListener('click', closeWorkbenchModal)
|
|
2011
|
+
$('modal-workbench').addEventListener('click', e => { if (e.target === $('modal-workbench')) closeWorkbenchModal() })
|
|
2012
|
+
$('wb-head').addEventListener('click', () => {
|
|
2013
|
+
state.wb.expanded = !state.wb.expanded
|
|
2014
|
+
if (state.wb.expanded && !state.wb.projects) refreshWorkbench({ silent: false })
|
|
2015
|
+
else renderWorkbench()
|
|
2016
|
+
})
|
|
2017
|
+
$('btn-wb-path').addEventListener('click', () => { if (state.wb.path) toast(t('wb.boundPath', { path: state.wb.path }), 'ok') })
|
|
2018
|
+
$('btn-wb-unbind').addEventListener('click', unbindWorkbench)
|
|
1755
2019
|
$('btn-send').addEventListener('click', sendMessage)
|
|
1756
2020
|
$('composer').addEventListener('keydown', (e) => {
|
|
1757
2021
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
@@ -1885,12 +2149,14 @@ async function start() {
|
|
|
1885
2149
|
}
|
|
1886
2150
|
$('token-desc').textContent = state.token ? '● ' + state.token.slice(0, 12) + '…' : t('ds.toastAuth')
|
|
1887
2151
|
bindUi()
|
|
2152
|
+
renderWorkbench()
|
|
1888
2153
|
updateConn()
|
|
1889
2154
|
checkNotesOnStart()
|
|
1890
2155
|
if (state.token) {
|
|
1891
2156
|
if (state.servers.length) await selectFastestServer({ silent: true, reconnect: false })
|
|
1892
2157
|
openStreams()
|
|
1893
2158
|
refreshSessions()
|
|
2159
|
+
refreshWorkbench({ silent: true })
|
|
1894
2160
|
}
|
|
1895
2161
|
}
|
|
1896
2162
|
|
package/public/desktop/i18n.js
CHANGED
|
@@ -10,6 +10,38 @@
|
|
|
10
10
|
if (saved === 'zh' || saved === 'en') return saved
|
|
11
11
|
return (navigator.language || 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
12
12
|
}
|
|
13
|
+
const BUILTIN = {
|
|
14
|
+
zh: {
|
|
15
|
+
'wb.unbound': 'DSH Remote(未绑定)', 'wb.bind': '绑定工作台', 'wb.bound': 'DSH Remote(绑定 {title})',
|
|
16
|
+
'wb.viewPath': '已绑定 workspace', 'wb.unbind': '解绑', 'wb.unbindConfirm': '确定解绑当前工作台?解绑后这些会话将回到扁平会话列表。',
|
|
17
|
+
'wb.modalTitle': '绑定工作台', 'wb.modalDesc': '选择工作台根目录,其下每个子文件夹会自动成为项目工作区。',
|
|
18
|
+
'wb.up': '上级', 'wb.home': '根目录', 'wb.selectDir': '选择此目录', 'wb.pathPlaceholder': '或手动输入绝对路径…',
|
|
19
|
+
'wb.pathEmpty': '请输入要绑定的路径', 'wb.empty': '此目录没有子文件夹', 'wb.boundOk': '工作台已绑定:{path}',
|
|
20
|
+
'wb.bindFailed': '绑定失败:{msg}', 'wb.unboundOk': '已解绑工作台', 'wb.unbindFailed': '解绑失败:{msg}',
|
|
21
|
+
'wb.boundPath': '绑定路径:{path}', 'wb.projects': '项目', 'wb.noProjects': '暂无项目(根目录下新建文件夹后会自动出现)',
|
|
22
|
+
'wb.newSession': '新会话', 'wb.noSessions': '暂无会话', 'wb.flatHidden': '工作台会话已收起({n} 个在工作台面板)',
|
|
23
|
+
'wb.apiMissing': '工作台接口不可用(需更新网关)', 'wb.loadFailed': '工作台加载失败:{msg}',
|
|
24
|
+
'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------'
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
'wb.unbound': 'DSH Remote (unbound)', 'wb.bind': 'Bind workbench', 'wb.bound': 'DSH Remote (bound {title})',
|
|
28
|
+
'wb.viewPath': 'Bound workspace', 'wb.unbind': 'Unbind', 'wb.unbindConfirm': 'Unbind the current workbench? These sessions will return to the flat list.',
|
|
29
|
+
'wb.modalTitle': 'Bind workbench', 'wb.modalDesc': 'Choose the workbench root; each subfolder becomes a project workspace automatically.',
|
|
30
|
+
'wb.up': 'Up', 'wb.home': 'Root', 'wb.selectDir': 'Select this folder', 'wb.pathPlaceholder': 'Or type an absolute path…',
|
|
31
|
+
'wb.pathEmpty': 'Enter a path to bind', 'wb.empty': 'No subfolders in this folder', 'wb.boundOk': 'Workbench bound: {path}',
|
|
32
|
+
'wb.bindFailed': 'Bind failed: {msg}', 'wb.unboundOk': 'Workbench unbound', 'wb.unbindFailed': 'Unbind failed: {msg}',
|
|
33
|
+
'wb.boundPath': 'Bound path: {path}', 'wb.projects': 'Projects', 'wb.noProjects': 'No projects yet',
|
|
34
|
+
'wb.newSession': 'New session', 'wb.noSessions': 'No sessions', 'wb.flatHidden': 'Workbench sessions folded ({n} in the workbench panel)',
|
|
35
|
+
'wb.apiMissing': 'Workbench API unavailable (update gateway)', 'wb.loadFailed': 'Workbench load failed: {msg}',
|
|
36
|
+
'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mergeDicts(strings) {
|
|
40
|
+
return {
|
|
41
|
+
zh: Object.assign({}, BUILTIN.zh, strings?.zh || {}),
|
|
42
|
+
en: Object.assign({}, BUILTIN.en, strings?.en || {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
13
45
|
let dict = null
|
|
14
46
|
let lang = detect()
|
|
15
47
|
|
|
@@ -46,7 +78,7 @@
|
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
window.I18N = {
|
|
49
|
-
init(strings) { dict = strings || dict; return apply(document) },
|
|
81
|
+
init(strings) { dict = mergeDicts(strings || dict || {}); return apply(document) },
|
|
50
82
|
t, setLang, apply,
|
|
51
83
|
get lang() { return lang }
|
|
52
84
|
}
|