dsh-remote-plugin 0.6.22 → 0.6.24
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 +271 -27
- package/package.json +1 -1
- package/public/app.js +24 -7
- package/public/desktop/desktop.css +10 -0
- package/public/desktop/desktop.html +28 -6
- package/public/desktop/desktop.js +103 -20
- package/public/index.html +5 -4
- package/public/styles.css +3 -0
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway.cjs
CHANGED
|
@@ -183,6 +183,8 @@ const CAPABILITIES = Object.freeze({
|
|
|
183
183
|
dshLifecycle: DSH_CONTROL_SUPPORT.supported ? 2 : 0,
|
|
184
184
|
centralAnnouncements: 2,
|
|
185
185
|
feedback: 1,
|
|
186
|
+
compatibilityAdapter: 2,
|
|
187
|
+
diagnostics: 1,
|
|
186
188
|
deviceKeys: 1,
|
|
187
189
|
healthProbes: 1,
|
|
188
190
|
resumableUploads: 2,
|
|
@@ -1340,6 +1342,58 @@ const eventCollectors = { mux: null, host: null }
|
|
|
1340
1342
|
// both serve the same zero-build clients.
|
|
1341
1343
|
let upstreamApiFlavor = 'unknown'
|
|
1342
1344
|
let upstreamApiFlavorProbe = null
|
|
1345
|
+
let upstreamApiFlavorCheckedAt = 0
|
|
1346
|
+
let upstreamApiFlavorChangedAt = 0
|
|
1347
|
+
let compatibleCollectorFlavor = ''
|
|
1348
|
+
let compatibleCollectorRestartScheduled = false
|
|
1349
|
+
const UPSTREAM_API_FLAVOR_RECHECK_MS = durationEnv('DSH_REMOTE_UPSTREAM_API_RECHECK_MS', 15_000, 3_000, 10 * 60_000)
|
|
1350
|
+
const COMPATIBILITY_LOG_MAX = durationEnv('DSH_REMOTE_COMPATIBILITY_LOG_MAX', 80, 10, 500)
|
|
1351
|
+
const compatibilityLog = []
|
|
1352
|
+
|
|
1353
|
+
// Compatibility diagnostics deliberately record protocol facts only. RPC payloads,
|
|
1354
|
+
// token values, cookies, host paths, and DSH conversation content must never leave
|
|
1355
|
+
// the local machine as part of a support report.
|
|
1356
|
+
function redactDiagnosticText(value, limit = 240) {
|
|
1357
|
+
return String(value || '')
|
|
1358
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
|
|
1359
|
+
.replace(/\b(token|access[_-]?token|cookie|authorization)(\s*[=:]\s*)[^\s,;]+/gi, '$1$2[redacted]')
|
|
1360
|
+
.replace(/https?:\/\/[^\s,;]+/gi, '<url>')
|
|
1361
|
+
.replace(/(?:[A-Za-z]:)?[/\\](?:[^\s/\\]+[/\\])+[^\s/\\]*/g, '<path>')
|
|
1362
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
1363
|
+
.slice(0, limit)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function recordCompatibility(kind, fields = {}) {
|
|
1367
|
+
const item = {
|
|
1368
|
+
at: new Date().toISOString(),
|
|
1369
|
+
kind: String(kind || 'unknown').replace(/[^a-z0-9._/-]/gi, '').slice(0, 48) || 'unknown',
|
|
1370
|
+
...fields,
|
|
1371
|
+
}
|
|
1372
|
+
if (item.method !== undefined) item.method = String(item.method).replace(/[^a-z0-9._/-]/gi, '').slice(0, 120)
|
|
1373
|
+
if (item.detail !== undefined) item.detail = redactDiagnosticText(item.detail)
|
|
1374
|
+
if (item.status !== undefined) item.status = Number(item.status) || 0
|
|
1375
|
+
compatibilityLog.push(item)
|
|
1376
|
+
while (compatibilityLog.length > COMPATIBILITY_LOG_MAX) compatibilityLog.shift()
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function diagnosticErrorCategory(value) {
|
|
1380
|
+
const text = String(value || '').toLowerCase()
|
|
1381
|
+
if (!text) return ''
|
|
1382
|
+
if (/timeout|timed out|abort/.test(text)) return 'timeout'
|
|
1383
|
+
if (/401|403|auth|cookie/.test(text)) return 'authentication'
|
|
1384
|
+
if (/404|405|501|not found|unavailable/.test(text)) return 'method-unavailable'
|
|
1385
|
+
if (/websocket|socket|econn|network/.test(text)) return 'transport'
|
|
1386
|
+
return 'upstream-error'
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function scheduleCompatibleCollectorRestart() {
|
|
1390
|
+
if (compatibleCollectorRestartScheduled) return
|
|
1391
|
+
compatibleCollectorRestartScheduled = true
|
|
1392
|
+
setImmediate(() => {
|
|
1393
|
+
compatibleCollectorRestartScheduled = false
|
|
1394
|
+
void startCompatibleEventCollectors()
|
|
1395
|
+
})
|
|
1396
|
+
}
|
|
1343
1397
|
const modernState = {
|
|
1344
1398
|
home: '',
|
|
1345
1399
|
eventClientId: '',
|
|
@@ -1362,21 +1416,34 @@ async function callUpstreamRemote(endpoint, args, rpcId = crypto.randomUUID()) {
|
|
|
1362
1416
|
}
|
|
1363
1417
|
|
|
1364
1418
|
async function detectUpstreamApiFlavor(force = false) {
|
|
1365
|
-
|
|
1419
|
+
const fresh = Date.now() - upstreamApiFlavorCheckedAt < UPSTREAM_API_FLAVOR_RECHECK_MS
|
|
1420
|
+
if (!force && upstreamApiFlavor !== 'unknown' && fresh) return upstreamApiFlavor
|
|
1366
1421
|
if (!force && upstreamApiFlavorProbe) return upstreamApiFlavorProbe
|
|
1367
1422
|
upstreamApiFlavorProbe = (async () => {
|
|
1423
|
+
const previous = upstreamApiFlavor
|
|
1368
1424
|
try {
|
|
1369
1425
|
const probe = await callUpstreamRemote('session/list', { _request: {} })
|
|
1370
|
-
|
|
1426
|
+
const detected = probe.status === 200
|
|
1371
1427
|
&& probe.body?.result?.ok === true
|
|
1372
1428
|
&& Array.isArray(probe.body?.result?.value?.items)
|
|
1373
1429
|
? 'modern'
|
|
1374
|
-
: 'legacy'
|
|
1430
|
+
: ((probe.status === 404 || probe.status === 405 || (probe.status >= 200 && probe.status < 300)) ? 'legacy' : previous)
|
|
1431
|
+
// A transient DSH restart or an authentication problem must not turn a known
|
|
1432
|
+
// modern server into "legacy" and strand its live event stream.
|
|
1433
|
+
if (detected !== 'unknown') upstreamApiFlavor = detected
|
|
1434
|
+
upstreamApiFlavorCheckedAt = Date.now()
|
|
1375
1435
|
if (upstreamApiFlavor === 'modern' && probe.body?.result?.ok) {
|
|
1376
1436
|
updateModernSessions(probe.body.result.value?.items)
|
|
1377
1437
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1438
|
+
if (previous !== upstreamApiFlavor) {
|
|
1439
|
+
upstreamApiFlavorChangedAt = upstreamApiFlavorCheckedAt
|
|
1440
|
+
recordCompatibility('protocol-switch', { from: previous, to: upstreamApiFlavor, status: probe.status })
|
|
1441
|
+
scheduleCompatibleCollectorRestart()
|
|
1442
|
+
}
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
upstreamApiFlavorCheckedAt = Date.now()
|
|
1445
|
+
if (upstreamApiFlavor === 'unknown') upstreamApiFlavor = 'legacy'
|
|
1446
|
+
recordCompatibility('protocol-probe-failed', { detail: error?.message || error })
|
|
1380
1447
|
} finally {
|
|
1381
1448
|
upstreamApiFlavorProbe = null
|
|
1382
1449
|
}
|
|
@@ -1404,6 +1471,23 @@ function modernError(message, code = 'upstream-incompatible', details = {}) {
|
|
|
1404
1471
|
return { ok: false, error: { code, message, details } }
|
|
1405
1472
|
}
|
|
1406
1473
|
|
|
1474
|
+
function modernResponseNeedsLegacyFallback(response) {
|
|
1475
|
+
if (!response) return false
|
|
1476
|
+
if ([404, 405, 501].includes(Number(response.status))) return true
|
|
1477
|
+
const code = String(response.body?.result?.error?.code || response.body?.error?.code || response.body?.error || '').toLowerCase()
|
|
1478
|
+
return ['method-unavailable', 'not-found', 'unsupported-method', 'unsupported_endpoint'].includes(code)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function remoteFailureKind(response) {
|
|
1482
|
+
const raw = String(response?.body?.result?.error?.code || response?.body?.error?.code || response?.body?.error || '')
|
|
1483
|
+
const code = raw.replace(/[^a-z0-9._/-]/gi, '').slice(0, 80)
|
|
1484
|
+
return code || `http-${Number(response?.status) || 0}`
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function legacyFallback(reason) {
|
|
1488
|
+
return { __dshRemoteLegacyFallback: true, reason: redactDiagnosticText(reason, 120) }
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1407
1491
|
function legacyHistoryValue(value, summary) {
|
|
1408
1492
|
const records = Array.isArray(value?.records) ? value.records : []
|
|
1409
1493
|
return {
|
|
@@ -1432,6 +1516,7 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1432
1516
|
}
|
|
1433
1517
|
if (method === 'session.list') {
|
|
1434
1518
|
const response = await refreshModernSessions()
|
|
1519
|
+
if (modernResponseNeedsLegacyFallback(response)) return legacyFallback('generated RPC session/list unavailable')
|
|
1435
1520
|
return response.body || legacyEnvelope(rpcId, modernError('DSH session/list returned no JSON response'))
|
|
1436
1521
|
}
|
|
1437
1522
|
if (method === 'session.history') {
|
|
@@ -1468,7 +1553,7 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1468
1553
|
}
|
|
1469
1554
|
} else if (method.startsWith('session.')) {
|
|
1470
1555
|
const verb = method.slice('session.'.length)
|
|
1471
|
-
if (!['search', 'create', 'selectModel', 'rename', 'fork', 'prompt', 'attachment', 'updateQueue', 'cancel'].includes(verb)) return
|
|
1556
|
+
if (!['search', 'create', 'selectModel', 'rename', 'fork', 'prompt', 'attachment', 'updateQueue', 'cancel'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
1472
1557
|
endpoint = 'session/' + verb
|
|
1473
1558
|
const request = verb === 'prompt' && !payload.requestId
|
|
1474
1559
|
? { ...payload, requestId: crypto.randomUUID() }
|
|
@@ -1476,12 +1561,12 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1476
1561
|
args = verb === 'search' ? { request } : { request }
|
|
1477
1562
|
} else if (method.startsWith('workspace.')) {
|
|
1478
1563
|
const verb = method.slice('workspace.'.length)
|
|
1479
|
-
if (!['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore', 'archiveSession'].includes(verb)) return
|
|
1564
|
+
if (!['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore', 'archiveSession'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
1480
1565
|
endpoint = 'workspace/' + verb
|
|
1481
1566
|
args = { request: payload }
|
|
1482
1567
|
} else if (method.startsWith('goal.')) {
|
|
1483
1568
|
const verb = method.slice('goal.'.length)
|
|
1484
|
-
if (!['create', 'edit', 'pause', 'resume', 'complete', 'clear'].includes(verb)) return
|
|
1569
|
+
if (!['create', 'edit', 'pause', 'resume', 'complete', 'clear'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
1485
1570
|
endpoint = 'goals/' + verb
|
|
1486
1571
|
args = verb === 'create'
|
|
1487
1572
|
? { agentId: payload.sessionId, request: { objective: payload.objective, ...(payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: payload.maxGoalRounds }) } }
|
|
@@ -1508,7 +1593,7 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1508
1593
|
args = {}
|
|
1509
1594
|
} else if (method.startsWith('credentials.')) {
|
|
1510
1595
|
const verb = method.slice('credentials.'.length)
|
|
1511
|
-
if (!['describe', 'set', 'unset'].includes(verb)) return
|
|
1596
|
+
if (!['describe', 'set', 'unset'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
1512
1597
|
endpoint = 'credentials/' + verb
|
|
1513
1598
|
args = verb === 'describe' ? { refs: payload.refs } : verb === 'set' ? { ref: payload.ref, value: payload.value } : { ref: payload.ref }
|
|
1514
1599
|
if (verb !== 'describe') transform = () => ({})
|
|
@@ -1531,11 +1616,15 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1531
1616
|
}
|
|
1532
1617
|
transform = models => ({ models: Array.isArray(models) ? models : [] })
|
|
1533
1618
|
} else {
|
|
1534
|
-
return
|
|
1619
|
+
return legacyFallback('no generated RPC adapter')
|
|
1535
1620
|
}
|
|
1536
1621
|
|
|
1537
1622
|
const response = await callUpstreamRemote(endpoint, args, rpcId)
|
|
1538
|
-
if (
|
|
1623
|
+
if (modernResponseNeedsLegacyFallback(response)) return legacyFallback(`generated RPC ${endpoint} unavailable`)
|
|
1624
|
+
if (!response.body?.result?.ok) {
|
|
1625
|
+
recordCompatibility('generated-rpc-error', { method, status: response.status, detail: remoteFailureKind(response) })
|
|
1626
|
+
return response.body || legacyEnvelope(rpcId, modernError(`DSH ${endpoint} returned no JSON response`))
|
|
1627
|
+
}
|
|
1539
1628
|
return legacyEnvelope(rpcId, { ok: true, value: transform(response.body.result.value) })
|
|
1540
1629
|
}
|
|
1541
1630
|
|
|
@@ -1989,6 +2078,12 @@ function applyModernRemoteEvent(ws, value) {
|
|
|
1989
2078
|
legacyPush('host', { type: 'host/session-removed', sessionId: args[0] })
|
|
1990
2079
|
} else if (value.event === 'api-session/status') {
|
|
1991
2080
|
legacyPush('host', { type: 'host/session-status', sessionId: args[0], running: !!args[1] })
|
|
2081
|
+
} else if (value.event === 'api-session/activity') {
|
|
2082
|
+
const sessionId = String(args[0] || '')
|
|
2083
|
+
const updatedAt = Number(args[1]) || Date.now()
|
|
2084
|
+
const summary = modernState.sessions.get(sessionId)
|
|
2085
|
+
if (summary) modernState.sessions.set(sessionId, { ...summary, updatedAt })
|
|
2086
|
+
legacyPush('host', { type: 'host/session-activity', sessionId, updatedAt })
|
|
1992
2087
|
} else if (value.event === 'api-session/error') {
|
|
1993
2088
|
legacyPush('host', { type: 'host/agent-error', sessionId: args[0], message: String(args[1] || '') })
|
|
1994
2089
|
} else {
|
|
@@ -2106,7 +2201,16 @@ function startModernEventCollector() {
|
|
|
2106
2201
|
}
|
|
2107
2202
|
|
|
2108
2203
|
async function startCompatibleEventCollectors() {
|
|
2109
|
-
|
|
2204
|
+
const flavor = await detectUpstreamApiFlavor()
|
|
2205
|
+
if (compatibleCollectorFlavor === flavor && eventCollectors.mux) return
|
|
2206
|
+
for (const collector of new Set(Object.values(eventCollectors).filter(Boolean))) {
|
|
2207
|
+
try { collector.close?.() } catch {}
|
|
2208
|
+
}
|
|
2209
|
+
eventCollectors.mux = null
|
|
2210
|
+
eventCollectors.host = null
|
|
2211
|
+
compatibleCollectorFlavor = flavor
|
|
2212
|
+
recordCompatibility('collector-mode', { detail: flavor })
|
|
2213
|
+
if (flavor === 'modern') {
|
|
2110
2214
|
const collector = startModernEventCollector()
|
|
2111
2215
|
eventCollectors.mux = collector
|
|
2112
2216
|
eventCollectors.host = collector
|
|
@@ -2355,6 +2459,52 @@ async function validatePollVote(payload) {
|
|
|
2355
2459
|
return result
|
|
2356
2460
|
}
|
|
2357
2461
|
|
|
2462
|
+
function compatibilityDiagnostics() {
|
|
2463
|
+
const events = Object.fromEntries(Object.entries(eventCollectorState).map(([kind, state]) => [kind, {
|
|
2464
|
+
connected: state.connected === true,
|
|
2465
|
+
reconnects: Number(state.reconnects) || 0,
|
|
2466
|
+
lastError: diagnosticErrorCategory(state.lastError),
|
|
2467
|
+
lastCloseCode: Number(state.lastCloseCode) || 0,
|
|
2468
|
+
}]))
|
|
2469
|
+
return {
|
|
2470
|
+
schema: 1,
|
|
2471
|
+
capturedAt: new Date().toISOString(),
|
|
2472
|
+
gateway: { version: VERSION, protocol: PROTOCOL_VERSION, platform: process.platform, node: process.versions.node },
|
|
2473
|
+
upstream: {
|
|
2474
|
+
apiFlavor: upstreamApiFlavor,
|
|
2475
|
+
checkedAt: upstreamApiFlavorCheckedAt ? new Date(upstreamApiFlavorCheckedAt).toISOString() : '',
|
|
2476
|
+
changedAt: upstreamApiFlavorChangedAt ? new Date(upstreamApiFlavorChangedAt).toISOString() : '',
|
|
2477
|
+
collectorFlavor: compatibleCollectorFlavor,
|
|
2478
|
+
},
|
|
2479
|
+
events,
|
|
2480
|
+
recent: compatibilityLog.slice(-20),
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function serveDiagnostics(req, res, url) {
|
|
2485
|
+
cors(res)
|
|
2486
|
+
if (req.method === 'OPTIONS') {
|
|
2487
|
+
res.writeHead(204)
|
|
2488
|
+
res.end()
|
|
2489
|
+
return
|
|
2490
|
+
}
|
|
2491
|
+
if (req.method !== 'GET') {
|
|
2492
|
+
res.writeHead(405, { allow: 'GET' })
|
|
2493
|
+
res.end()
|
|
2494
|
+
return
|
|
2495
|
+
}
|
|
2496
|
+
if (!authorized(req, url)) {
|
|
2497
|
+
authFailures++
|
|
2498
|
+
touchDevice(req, { failedAuth: true })
|
|
2499
|
+
res.writeHead(401, { 'content-type': 'application/json; charset=utf-8' })
|
|
2500
|
+
res.end(JSON.stringify({ error: 'unauthorized' }))
|
|
2501
|
+
return
|
|
2502
|
+
}
|
|
2503
|
+
touchDevice(req)
|
|
2504
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
2505
|
+
res.end(JSON.stringify(compatibilityDiagnostics()))
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2358
2508
|
function serveFeedback(req, res, url) {
|
|
2359
2509
|
cors(res)
|
|
2360
2510
|
if (req.method === 'OPTIONS') {
|
|
@@ -2391,6 +2541,7 @@ function serveFeedback(req, res, url) {
|
|
|
2391
2541
|
let message = String(payload.message || '').trim()
|
|
2392
2542
|
const contact = String(payload.contact || '').trim()
|
|
2393
2543
|
const appVersion = String(payload.appVersion || '').trim()
|
|
2544
|
+
const includeDiagnostics = payload.includeDiagnostics === true
|
|
2394
2545
|
if (!['bug', 'suggestion', 'other', 'poll'].includes(type)) {
|
|
2395
2546
|
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
|
|
2396
2547
|
res.end(JSON.stringify({ error: 'invalid type', expect: 'bug|suggestion|other|poll' }))
|
|
@@ -2442,6 +2593,7 @@ function serveFeedback(req, res, url) {
|
|
|
2442
2593
|
appVersion: appVersion || 'unknown',
|
|
2443
2594
|
gatewayVersion: VERSION,
|
|
2444
2595
|
clientIp: maskIp(ip),
|
|
2596
|
+
...(includeDiagnostics ? { diagnostics: compatibilityDiagnostics() } : {}),
|
|
2445
2597
|
...(pollVote || {})
|
|
2446
2598
|
}),
|
|
2447
2599
|
signal: AbortSignal.timeout(8000)
|
|
@@ -2834,6 +2986,19 @@ async function loadFsWorkspaceRoots(force = false) {
|
|
|
2834
2986
|
let value
|
|
2835
2987
|
if (await detectUpstreamApiFlavor() === 'modern') {
|
|
2836
2988
|
value = modernState.workspaces
|
|
2989
|
+
// The slash protocol receives workspace state over a stream. Immediately
|
|
2990
|
+
// after a protocol switch that baseline may not have arrived yet; retain
|
|
2991
|
+
// a working dotted workspace.list implementation when this particular
|
|
2992
|
+
// DSH release still exposes it instead of denying an otherwise valid root.
|
|
2993
|
+
if (!Array.isArray(value?.items) || !value.items.length) {
|
|
2994
|
+
try {
|
|
2995
|
+
const legacy = await forwardLegacyRpc(new URL('/api/workspace.list', UPSTREAM), JSON.stringify({
|
|
2996
|
+
type: 'client-request', rpcId: crypto.randomUUID(), method: 'workspace.list', payload: {},
|
|
2997
|
+
}))
|
|
2998
|
+
const body = JSON.parse(legacy.raw || '{}')
|
|
2999
|
+
if (legacy.status === 200 && body?.result?.ok) value = body.result.value
|
|
3000
|
+
} catch {}
|
|
3001
|
+
}
|
|
2837
3002
|
} else {
|
|
2838
3003
|
const target = new URL('/api/workspace.list', UPSTREAM)
|
|
2839
3004
|
const res = await fetch(target, {
|
|
@@ -3769,21 +3934,94 @@ function serveWorkbench(req, res, url) {
|
|
|
3769
3934
|
}
|
|
3770
3935
|
|
|
3771
3936
|
// ---------- /api 代理 ----------
|
|
3937
|
+
const ADAPTIVE_RPC_METHODS = new Set([
|
|
3938
|
+
'host.describe', 'session.list', 'session.history', 'session.models', 'session.search',
|
|
3939
|
+
'session.create', 'session.selectModel', 'session.rename', 'session.fork', 'session.prompt',
|
|
3940
|
+
'session.attachment', 'session.updateQueue', 'session.cancel', 'workspace.list',
|
|
3941
|
+
'workspace.create', 'workspace.rename', 'workspace.delete', 'workspace.insertBefore',
|
|
3942
|
+
'workspace.insertSessionBefore', 'workspace.archiveSession', 'goal.create', 'goal.edit',
|
|
3943
|
+
'goal.pause', 'goal.resume', 'goal.complete', 'goal.clear', 'subagent.list',
|
|
3944
|
+
'subagent.interrupt', 'settings.describe', 'settings.mutate', 'settings.openDocument',
|
|
3945
|
+
'credentials.describe', 'credentials.set', 'credentials.unset', 'llm.providers', 'llm.discoverModels',
|
|
3946
|
+
])
|
|
3947
|
+
|
|
3948
|
+
function readApiRequest(req, maxBytes = 4 * 1024 * 1024) {
|
|
3949
|
+
return new Promise((resolve, reject) => {
|
|
3950
|
+
let raw = ''
|
|
3951
|
+
req.setEncoding('utf8')
|
|
3952
|
+
req.on('data', chunk => {
|
|
3953
|
+
raw += chunk
|
|
3954
|
+
if (Buffer.byteLength(raw) > maxBytes) reject(new Error('request body too large'))
|
|
3955
|
+
})
|
|
3956
|
+
req.once('end', () => resolve(raw))
|
|
3957
|
+
req.once('error', reject)
|
|
3958
|
+
req.once('aborted', () => reject(new Error('request aborted')))
|
|
3959
|
+
})
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
async function forwardLegacyRpc(url, raw) {
|
|
3963
|
+
const target = new URL(url.pathname + url.search, UPSTREAM)
|
|
3964
|
+
const response = await fetch(target, {
|
|
3965
|
+
method: 'POST',
|
|
3966
|
+
headers: dshUpstreamHeaders({ 'content-type': 'application/json' }),
|
|
3967
|
+
body: raw,
|
|
3968
|
+
signal: AbortSignal.timeout(UPSTREAM_REQUEST_TIMEOUT_MS),
|
|
3969
|
+
})
|
|
3970
|
+
return { status: response.status, headers: response.headers, raw: await response.text() }
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
function sendBufferedUpstreamResponse(res, response) {
|
|
3974
|
+
cors(res)
|
|
3975
|
+
res.writeHead(response.status || 502, {
|
|
3976
|
+
'content-type': response.headers?.get?.('content-type') || 'application/json; charset=utf-8',
|
|
3977
|
+
'cache-control': 'no-store',
|
|
3978
|
+
})
|
|
3979
|
+
res.end(response.raw || '')
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
async function proxyLegacyRpcWithModernFallback(req, res, url) {
|
|
3983
|
+
const raw = await readApiRequest(req)
|
|
3984
|
+
const legacy = await forwardLegacyRpc(url, raw)
|
|
3985
|
+
if (![404, 405, 501].includes(legacy.status)) {
|
|
3986
|
+
if (legacy.status >= 400) recordCompatibility('legacy-rpc-error', { method: url.pathname.slice('/api/'.length), status: legacy.status })
|
|
3987
|
+
sendBufferedUpstreamResponse(res, legacy)
|
|
3988
|
+
return
|
|
3989
|
+
}
|
|
3990
|
+
let body
|
|
3991
|
+
try { body = JSON.parse(raw || '{}') } catch { body = null }
|
|
3992
|
+
if (body?.type !== 'client-request' || typeof body.rpcId !== 'string' || !ADAPTIVE_RPC_METHODS.has(body.method)) {
|
|
3993
|
+
sendBufferedUpstreamResponse(res, legacy)
|
|
3994
|
+
return
|
|
3995
|
+
}
|
|
3996
|
+
recordCompatibility('legacy-404-fallback', { method: body.method, status: legacy.status })
|
|
3997
|
+
const translated = await translateModernRpc(body.method, body.payload || {}, body.rpcId)
|
|
3998
|
+
if (!translated || translated.__dshRemoteLegacyFallback) {
|
|
3999
|
+
sendBufferedUpstreamResponse(res, legacy)
|
|
4000
|
+
return
|
|
4001
|
+
}
|
|
4002
|
+
// A generated RPC completed, so subsequent requests and live collectors can use
|
|
4003
|
+
// the modern contract immediately instead of waiting for the periodic probe.
|
|
4004
|
+
if (body.method !== 'host.describe') {
|
|
4005
|
+
const previous = upstreamApiFlavor
|
|
4006
|
+
upstreamApiFlavor = 'modern'
|
|
4007
|
+
upstreamApiFlavorCheckedAt = Date.now()
|
|
4008
|
+
if (previous !== 'modern') {
|
|
4009
|
+
upstreamApiFlavorChangedAt = upstreamApiFlavorCheckedAt
|
|
4010
|
+
recordCompatibility('protocol-switch', { from: previous, to: 'modern', detail: 'legacy dotted RPC returned 404' })
|
|
4011
|
+
scheduleCompatibleCollectorRestart()
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
cors(res)
|
|
4015
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
4016
|
+
res.end(JSON.stringify(translated))
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3772
4019
|
async function proxyModernApi(req, res, url) {
|
|
3773
4020
|
if (req.method !== 'POST' || url.pathname.startsWith('/remote/')) return false
|
|
3774
4021
|
if (await detectUpstreamApiFlavor() !== 'modern') return false
|
|
3775
4022
|
let raw = ''
|
|
3776
4023
|
try {
|
|
3777
|
-
raw = await
|
|
3778
|
-
req.setEncoding('utf8')
|
|
3779
|
-
req.on('data', chunk => {
|
|
3780
|
-
raw += chunk
|
|
3781
|
-
if (raw.length > 4 * 1024 * 1024) reject(new Error('request body too large'))
|
|
3782
|
-
})
|
|
3783
|
-
req.once('end', () => resolve(raw))
|
|
3784
|
-
req.once('error', reject)
|
|
3785
|
-
req.once('aborted', () => reject(new Error('request aborted')))
|
|
3786
|
-
})
|
|
4024
|
+
raw = await readApiRequest(req)
|
|
3787
4025
|
const body = JSON.parse(raw || '{}')
|
|
3788
4026
|
cors(res)
|
|
3789
4027
|
if (url.pathname === '/api/respond') {
|
|
@@ -3814,9 +4052,10 @@ async function proxyModernApi(req, res, url) {
|
|
|
3814
4052
|
return true
|
|
3815
4053
|
}
|
|
3816
4054
|
const translated = await translateModernRpc(body.method, body.payload || {}, body.rpcId)
|
|
3817
|
-
if (translated
|
|
3818
|
-
|
|
3819
|
-
|
|
4055
|
+
if (translated?.__dshRemoteLegacyFallback) {
|
|
4056
|
+
recordCompatibility('modern-legacy-fallback', { method: body.method, detail: translated.reason })
|
|
4057
|
+
const legacy = await forwardLegacyRpc(url, raw)
|
|
4058
|
+
sendBufferedUpstreamResponse(res, legacy)
|
|
3820
4059
|
return true
|
|
3821
4060
|
}
|
|
3822
4061
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
@@ -3832,7 +4071,11 @@ async function proxyModernApi(req, res, url) {
|
|
|
3832
4071
|
}
|
|
3833
4072
|
}
|
|
3834
4073
|
|
|
3835
|
-
function proxyLegacyApi(req, res, url) {
|
|
4074
|
+
async function proxyLegacyApi(req, res, url) {
|
|
4075
|
+
const method = url.pathname.startsWith('/api/') ? decodeURIComponent(url.pathname.slice('/api/'.length)) : ''
|
|
4076
|
+
if (req.method === 'POST' && ADAPTIVE_RPC_METHODS.has(method)) {
|
|
4077
|
+
return proxyLegacyRpcWithModernFallback(req, res, url)
|
|
4078
|
+
}
|
|
3836
4079
|
const headers = {}
|
|
3837
4080
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
3838
4081
|
if (v === undefined) continue
|
|
@@ -3905,7 +4148,7 @@ function proxyApi(req, res, url) {
|
|
|
3905
4148
|
return
|
|
3906
4149
|
}
|
|
3907
4150
|
void proxyModernApi(req, res, url).then(handled => {
|
|
3908
|
-
if (!handled) proxyLegacyApi(req, res, url)
|
|
4151
|
+
if (!handled) return proxyLegacyApi(req, res, url)
|
|
3909
4152
|
}).catch(error => {
|
|
3910
4153
|
cors(res)
|
|
3911
4154
|
if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' })
|
|
@@ -3994,6 +4237,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3994
4237
|
const url = new URL(req.url, 'http://dsh-remote.local')
|
|
3995
4238
|
if (url.pathname === '/fs' || url.pathname.startsWith('/fs/')) return await serveFs(req, res, url)
|
|
3996
4239
|
if (url.pathname === '/workbench' || url.pathname.startsWith('/workbench/')) return serveWorkbench(req, res, url)
|
|
4240
|
+
if (url.pathname === '/diagnostics') return serveDiagnostics(req, res, url)
|
|
3997
4241
|
if (url.pathname === '/feedback') return serveFeedback(req, res, url)
|
|
3998
4242
|
if (url.pathname.startsWith('/admin/api')) return serveAdminApi(req, res, url)
|
|
3999
4243
|
if (url.pathname.startsWith('/stats')) return serveStats(req, res, url)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.24",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/public/app.js
CHANGED
|
@@ -293,6 +293,7 @@ function openFeedbackModal() {
|
|
|
293
293
|
document.querySelectorAll('#fb-chips .fb-chip').forEach(b => b.classList.toggle('current', b.dataset.fbType === 'bug'))
|
|
294
294
|
$('fb-msg').value = ''
|
|
295
295
|
$('fb-contact').value = ''
|
|
296
|
+
$('fb-include-diagnostics').checked = false
|
|
296
297
|
$('modal-feedback').classList.remove('hidden')
|
|
297
298
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
298
299
|
}
|
|
@@ -309,6 +310,7 @@ async function submitFeedback() {
|
|
|
309
310
|
const type = state.feedbackType || 'bug'
|
|
310
311
|
const message = $('fb-msg').value.trim()
|
|
311
312
|
const contact = $('fb-contact').value.trim()
|
|
313
|
+
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
312
314
|
if (!message) { toast(t('feedback.empty'), 'err'); return }
|
|
313
315
|
if (message.length > 2000) { toast(t('feedback.tooLong'), 'err'); return }
|
|
314
316
|
const btn = $('fb-submit')
|
|
@@ -319,7 +321,7 @@ async function submitFeedback() {
|
|
|
319
321
|
const res = await fetch(base + '/feedback', {
|
|
320
322
|
method: 'POST',
|
|
321
323
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
322
|
-
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion })
|
|
324
|
+
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics })
|
|
323
325
|
})
|
|
324
326
|
let json = {}
|
|
325
327
|
try { json = await res.json() } catch {}
|
|
@@ -1589,6 +1591,11 @@ function onHostFrame(full) {
|
|
|
1589
1591
|
const f = full.payload
|
|
1590
1592
|
if (!f) return
|
|
1591
1593
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) return scheduleRefresh()
|
|
1594
|
+
if (f.type === 'host/session-activity') {
|
|
1595
|
+
const session = state.byId.get(f.sessionId)
|
|
1596
|
+
if (session) { session.updatedAt = Number(f.updatedAt) || Date.now(); renderSessions(); renderOverview() }
|
|
1597
|
+
return
|
|
1598
|
+
}
|
|
1592
1599
|
if (f.type === 'host/session-status') {
|
|
1593
1600
|
const s = state.byId.get(f.sessionId)
|
|
1594
1601
|
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessionCards(); updateCancelBtn(); renderSessionSub(); updateSessionStatus() } }
|
|
@@ -1830,7 +1837,17 @@ function resyncAfterStreamOpen() {
|
|
|
1830
1837
|
state.lastStreamResyncAt = Date.now()
|
|
1831
1838
|
void refreshAll().then(() => resyncCurrentSession())
|
|
1832
1839
|
}
|
|
1833
|
-
function
|
|
1840
|
+
function sessionTitleValue(s) {
|
|
1841
|
+
const value = proj(s, 'title', '')
|
|
1842
|
+
return value == null ? '' : String(value).trim()
|
|
1843
|
+
}
|
|
1844
|
+
function hasSessionTitle(s) { return !!sessionTitleValue(s) }
|
|
1845
|
+
function titleOf(s) { return sessionTitleValue(s) || (s?.sessionId ? t('session.untitled') : t('session.unknown')) }
|
|
1846
|
+
function sessionLabelOf(s) {
|
|
1847
|
+
const title = titleOf(s)
|
|
1848
|
+
if (!s?.sessionId || hasSessionTitle(s)) return title
|
|
1849
|
+
return `${title} · ${String(s.sessionId).slice(-8)}`
|
|
1850
|
+
}
|
|
1834
1851
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
1835
1852
|
function isTopLevelSession(session) {
|
|
1836
1853
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
@@ -2103,7 +2120,7 @@ function renderWorkbench() {
|
|
|
2103
2120
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
2104
2121
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
|
|
2105
2122
|
<span class="wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
|
|
2106
|
-
<span class="wb-session-title">${esc(
|
|
2123
|
+
<span class="wb-session-title">${esc(sessionLabelOf(s))}</span>
|
|
2107
2124
|
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(sessionSortTime(s)))}</span>
|
|
2108
2125
|
</button>
|
|
2109
2126
|
<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
|
|
@@ -2195,7 +2212,7 @@ function renderSessions() {
|
|
|
2195
2212
|
const renderSession = s => {
|
|
2196
2213
|
const workspace = sessionWorkspaceLabel(s)
|
|
2197
2214
|
const workspaceTitle = sessionWorkspaceName(s)
|
|
2198
|
-
const title =
|
|
2215
|
+
const title = sessionLabelOf(s)
|
|
2199
2216
|
const goal = goalOf(s)
|
|
2200
2217
|
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
2201
2218
|
const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
|
|
@@ -3403,7 +3420,7 @@ function renameSession(sessionId = state.current) {
|
|
|
3403
3420
|
const session = state.byId.get(sessionId)
|
|
3404
3421
|
if (!session) return
|
|
3405
3422
|
renamePendingSessionId = sessionId
|
|
3406
|
-
$('rename-session-input').value =
|
|
3423
|
+
$('rename-session-input').value = sessionTitleValue(session)
|
|
3407
3424
|
$('modal-rename').classList.remove('hidden')
|
|
3408
3425
|
setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
|
|
3409
3426
|
}
|
|
@@ -3594,7 +3611,7 @@ function renderOverview() {
|
|
|
3594
3611
|
]
|
|
3595
3612
|
$('overview-attention-count').textContent = pending.length ? t('overview.pendingCount', { n: pending.length }) : '—'
|
|
3596
3613
|
$('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
|
|
3597
|
-
const title =
|
|
3614
|
+
const title = sessionLabelOf(state.byId.get(item.sessionId))
|
|
3598
3615
|
if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
|
|
3599
3616
|
<span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(approvalDetail(item))} · ${esc(title)}</span></span>
|
|
3600
3617
|
<span class="overview-item-actions"><button type="button" class="mini-btn" data-overview-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-overview-approve="0">${t('pending.reject')}</button></span>
|
|
@@ -3643,7 +3660,7 @@ function renderOverview() {
|
|
|
3643
3660
|
$('overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'overview.poll' : 'overview.ws') : '—'
|
|
3644
3661
|
$('overview-active-count').textContent = running ? t('overview.activeCount', { n: running }) : ''
|
|
3645
3662
|
$('overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="overview-session-item ${s.running ? 'running' : ''}" data-overview-session="${esc(s.sessionId)}">
|
|
3646
|
-
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(
|
|
3663
|
+
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(sessionLabelOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="overview-item-arrow">›</span>
|
|
3647
3664
|
</button>`).join('') : `<div class="overview-empty">${t('overview.noSession')}</div>`
|
|
3648
3665
|
$('overview-session-list').querySelectorAll('[data-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.overviewSession)))
|
|
3649
3666
|
}
|
|
@@ -166,6 +166,9 @@ html.reorder-scroll-lock body { overscroll-behavior: none; }
|
|
|
166
166
|
}
|
|
167
167
|
.ds-fb-textarea:focus, .ds-fb-input:focus { border-color: var(--dsr-accent-line); }
|
|
168
168
|
.ds-fb-input { min-height: 42px; }
|
|
169
|
+
.ds-fb-diagnostics { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px 8px; align-items:start; margin:-1px 1px 4px; color:var(--dsr-text); font-size:13px; line-height:1.45; cursor:pointer; }
|
|
170
|
+
.ds-fb-diagnostics input { width:17px; height:17px; margin:1px 0 0; accent-color:var(--dsr-accent); }
|
|
171
|
+
.ds-fb-diagnostics small { grid-column:2; color:var(--dsr-muted); font-size:11px; line-height:1.45; }
|
|
169
172
|
|
|
170
173
|
.ds-main { flex: 1; min-width: 0; display: flex; flex-direction: column; background: radial-gradient(900px 560px at 96% -18%, var(--dsr-accent-soft), transparent 64%), linear-gradient(180deg, var(--dsr-bg), var(--dsr-bg-2)); }
|
|
171
174
|
.ds-topbar { height: 58px; flex: none; display: flex; align-items: center; gap: 10px; padding: 0 20px; border-bottom: 1px solid var(--dsr-divider); background: linear-gradient(180deg, var(--dsr-head-bg), transparent); backdrop-filter: blur(16px); }
|
|
@@ -525,6 +528,13 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
525
528
|
.ds-workspace-create-location code { min-width: 0; flex: 1; padding: 7px 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-text); background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
|
|
526
529
|
.ds-workspace-name { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
|
|
527
530
|
.ds-workspace-name:focus { border-color: var(--dsr-accent-line); }
|
|
531
|
+
.ds-new-session-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
|
|
532
|
+
.ds-new-session-label { font-size: 12px; color: var(--dsr-muted); }
|
|
533
|
+
.ds-new-session-select { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
|
|
534
|
+
.ds-new-session-select:focus { border-color: var(--dsr-accent-line); }
|
|
535
|
+
.ds-new-session-details { display: flex; flex-direction: column; gap: 4px; min-width: 0; padding: 8px 10px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
|
|
536
|
+
.ds-new-session-name { font-size: 13px; font-weight: 600; }
|
|
537
|
+
.ds-new-session-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-muted); font-size: 11.5px; }
|
|
528
538
|
.ds-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
|
|
529
539
|
.ds-q-item { border: 1px solid var(--dsr-line); border-radius: 10px; padding: 9px 11px; }
|
|
530
540
|
.ds-q-text { font-size: 13.5px; margin-bottom: 6px; }
|
|
@@ -368,6 +368,7 @@
|
|
|
368
368
|
</div>
|
|
369
369
|
<textarea id="fb-msg" class="ds-fb-textarea" rows="5" maxlength="2000" data-i18n-placeholder="ds.feedbackMessagePlaceholder" placeholder="请描述遇到的问题或建议(必填,≤2000 字)"></textarea>
|
|
370
370
|
<input id="fb-contact" class="ds-fb-input" maxlength="200" data-i18n-placeholder="ds.feedbackContactPlaceholder" placeholder="联系方式(可选):邮箱 / 微信 / B站 ID">
|
|
371
|
+
<label class="ds-fb-diagnostics"><input id="fb-include-diagnostics" type="checkbox"> <span data-i18n="ds.feedbackIncludeDiagnostics">附带兼容性诊断日志(可选)</span><small data-i18n="ds.feedbackDiagnosticsPrivacy">仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。</small></label>
|
|
371
372
|
</div>
|
|
372
373
|
<div class="ds-modal-actions">
|
|
373
374
|
<button id="fb-cancel" class="ds-btn" data-i18n="ds.feedbackCancel">取消</button>
|
|
@@ -392,6 +393,27 @@
|
|
|
392
393
|
</div>
|
|
393
394
|
</div>
|
|
394
395
|
|
|
396
|
+
<!-- 新建会话工作区模态 -->
|
|
397
|
+
<div id="modal-new-session" class="ds-modal hidden" role="dialog" aria-modal="true">
|
|
398
|
+
<div class="ds-modal-card">
|
|
399
|
+
<div class="ds-modal-title" data-i18n="ds.newSessionTitle">新建会话</div>
|
|
400
|
+
<div class="ds-modal-body">
|
|
401
|
+
<p class="ds-new-session-desc" data-i18n="ds.newSessionDesc">选择一个 DSH 工作区,新会话将在该工作区中创建。</p>
|
|
402
|
+
<label class="ds-new-session-label" for="new-session-workspace" data-i18n="ds.newSessionWorkspace">工作区</label>
|
|
403
|
+
<select id="new-session-workspace" class="ds-new-session-select"></select>
|
|
404
|
+
<div id="new-session-workspace-details" class="ds-new-session-details">
|
|
405
|
+
<span id="new-session-workspace-name" class="ds-new-session-name"></span>
|
|
406
|
+
<code id="new-session-workspace-path" class="ds-new-session-path"></code>
|
|
407
|
+
</div>
|
|
408
|
+
<div id="new-session-empty" class="ds-empty hidden" data-i18n="ds.newSessionNoWorkspaces">暂无可用工作区,请先创建一个工作区。</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="ds-modal-actions">
|
|
411
|
+
<button id="new-session-cancel" class="ds-btn" data-i18n="ds.cancel">取消</button>
|
|
412
|
+
<button id="new-session-create" class="ds-btn primary" data-i18n="ds.newSessionCreate">创建</button>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
395
417
|
<!-- 预设提示词管理模态 -->
|
|
396
418
|
<div id="modal-presets" class="ds-modal hidden" role="dialog" aria-modal="true">
|
|
397
419
|
<div class="ds-modal-card">
|
|
@@ -469,7 +491,7 @@
|
|
|
469
491
|
<script>
|
|
470
492
|
window.DESKTOP_STR = {
|
|
471
493
|
zh: {
|
|
472
|
-
'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.newWorkspace': '新建工作区', 'ds.sessions': '会话', 'ds.overview': '主页', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': '实时状态与需要你处理的事项',
|
|
494
|
+
'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.sessionUntitled': '新会话', 'ds.newWorkspace': '新建工作区', 'ds.sessions': '会话', 'ds.overview': '主页', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': '实时状态与需要你处理的事项',
|
|
473
495
|
'ds.systemPulse': '系统链路', 'ds.systemNominal': '系统链路正常', 'ds.systemDegraded': '部分链路需要关注', 'ds.systemOffline': '连接已断开', 'ds.checking': '检查中…', 'ds.components': '{n}/4 个模块在线', 'ds.live': 'LIVE', 'ds.allLinked': '全部链路', 'ds.offlineCore': 'OFFLINE',
|
|
474
496
|
'ds.gateway': '网关', 'ds.dshUpstream': 'DSH 上游', 'ds.mux': '实时 mux', 'ds.host': '实时 host', 'ds.online': '在线', 'ds.offlineShort': '离线',
|
|
475
497
|
'ds.attention': '需要你处理', 'ds.runtime': '运行指标', 'ds.recentActivity': '近期活动', 'ds.dshVersion': 'DSH 版本', 'ds.gatewayStatus': '网关状态', 'ds.activeSessions': '活跃会话', 'ds.connectionMode': '连接模式', 'ds.liveWs': '实时 WS', 'ds.poll': '降级轮询', 'ds.running': '运行中', 'ds.noReason': '无说明', 'ds.noActivity': '暂无后台活动', 'ds.noSessions': '暂无会话', 'ds.refreshOverview': '刷新总览', 'ds.pendingCount': '{n} 项待处理', 'ds.activeCount': '{n} 个活跃会话', 'ds.nothingPending': '暂无事项', 'ds.action.openSession': '打开最近会话', 'ds.action.newSession': '新建会话', 'ds.action.attention': '处理待办', 'ds.action.connect': '去设置连接', 'ds.action.refresh': '重新检查连接',
|
|
@@ -481,7 +503,7 @@
|
|
|
481
503
|
'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
|
|
482
504
|
'ds.cmdPermission': '/permission 切换权限预设', 'ds.cmdPlan': '/plan 进入/退出计划模式',
|
|
483
505
|
'ds.fsUp': '上级', 'ds.fsRoot': '允许根目录', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
484
|
-
'ds.workspaceCreateTitle': '新建工作区', 'ds.workspaceCreateDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'ds.workspaceParent': '父目录', 'ds.workspaceNamePlaceholder': '例如:my-project', 'ds.workspaceCreate': '创建并打开', 'ds.cancel': '取消', 'ds.workspaceNameRequired': '请输入工作区名称', 'ds.workspaceExists': '该目录已存在', 'ds.workspaceInvalidName': '名称不能包含路径分隔符', 'ds.workspaceCreateFailed': '创建工作区失败', 'ds.workspaceCreated': '工作区已创建', 'ds.workspaceCreatedNoSession': '工作区已创建,但新会话未能打开',
|
|
506
|
+
'ds.workspaceCreateTitle': '新建工作区', 'ds.workspaceCreateDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'ds.workspaceParent': '父目录', 'ds.workspaceNamePlaceholder': '例如:my-project', 'ds.workspaceCreate': '创建并打开', 'ds.cancel': '取消', 'ds.workspaceNameRequired': '请输入工作区名称', 'ds.workspaceExists': '该目录已存在', 'ds.workspaceInvalidName': '名称不能包含路径分隔符', 'ds.workspaceCreateFailed': '创建工作区失败', 'ds.workspaceCreated': '工作区已创建', 'ds.workspaceCreatedNoSession': '工作区已创建,但新会话未能打开', 'ds.newSessionTitle': '新建会话', 'ds.newSessionDesc': '选择一个 DSH 工作区,新会话将在该工作区中创建。', 'ds.newSessionWorkspace': '工作区', 'ds.newSessionNoWorkspaces': '暂无可用工作区,请先创建一个工作区。', 'ds.newSessionCreate': '创建', 'ds.newSessionChooseWorkspace': '请选择一个工作区',
|
|
485
507
|
'ds.groupGeneral': '通用', 'ds.groupGeneralDesc': '工具调用、预设提示词',
|
|
486
508
|
'ds.groupServers': '服务器', 'ds.groupServersDesc': '服务器地址与令牌',
|
|
487
509
|
'ds.groupNotify': '通知', 'ds.groupNotifyDesc': '通知与提醒',
|
|
@@ -497,7 +519,7 @@
|
|
|
497
519
|
'ds.feedback': '反馈', 'ds.feedbackGithubDesc': '反馈 bug / 提建议', 'ds.feedbackGiteeDesc': '国内镜像,无需代理',
|
|
498
520
|
'ds.feedbackBiliDesc': 'UP 动态页交流', 'ds.feedbackCopyLink': '复制项目链接', 'ds.feedbackCopyDesc': '手动分享给朋友',
|
|
499
521
|
'ds.feedbackCopied': '项目链接已复制', 'ds.feedbackCopyFailed': '复制失败,请手动复制',
|
|
500
|
-
'ds.feedbackWrite': '写反馈', 'ds.feedbackWriteDesc': 'App 内直接提交', 'ds.feedbackModalTitle': '写反馈',
|
|
522
|
+
'ds.feedbackWrite': '写反馈', 'ds.feedbackWriteDesc': 'App 内直接提交', 'ds.feedbackModalTitle': '写反馈', 'ds.feedbackIncludeDiagnostics': '附带兼容性诊断日志(可选)', 'ds.feedbackDiagnosticsPrivacy': '仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。',
|
|
501
523
|
'ds.feedbackTypeBug': 'Bug', 'ds.feedbackTypeSuggestion': '建议', 'ds.feedbackTypeOther': '其他',
|
|
502
524
|
'ds.feedbackMessagePlaceholder': '请描述遇到的问题或建议(必填,≤2000 字)',
|
|
503
525
|
'ds.feedbackContactPlaceholder': '联系方式(可选):邮箱 / 微信 / B站 ID',
|
|
@@ -574,7 +596,7 @@
|
|
|
574
596
|
'menu.modelTitle': '模型切换', 'menu.effortTitle': '思考深度',
|
|
575
597
|
},
|
|
576
598
|
en: {
|
|
577
|
-
'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.newWorkspace': 'New workspace', 'ds.sessions': 'Sessions', 'ds.overview': 'Home', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': 'Live status and items needing your attention',
|
|
599
|
+
'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.sessionUntitled': 'New session', 'ds.newWorkspace': 'New workspace', 'ds.sessions': 'Sessions', 'ds.overview': 'Home', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': 'Live status and items needing your attention',
|
|
578
600
|
'ds.systemPulse': 'System pulse', 'ds.systemNominal': 'System nominal', 'ds.systemDegraded': 'Some links need attention', 'ds.systemOffline': 'Connection offline', 'ds.checking': 'Checking…', 'ds.components': '{n}/4 components online', 'ds.live': 'LIVE', 'ds.allLinked': 'ALL LINKED', 'ds.offlineCore': 'OFFLINE',
|
|
579
601
|
'ds.gateway': 'Gateway', 'ds.dshUpstream': 'DSH upstream', 'ds.mux': 'Live mux', 'ds.host': 'Live host', 'ds.online': 'Online', 'ds.offlineShort': 'Offline',
|
|
580
602
|
'ds.attention': 'Needs attention', 'ds.runtime': 'Runtime metrics', 'ds.recentActivity': 'Recent activity', 'ds.dshVersion': 'DSH version', 'ds.gatewayStatus': 'Gateway status', 'ds.activeSessions': 'Active sessions', 'ds.connectionMode': 'Connection mode', 'ds.liveWs': 'Live WS', 'ds.poll': 'Degraded polling', 'ds.running': 'Running', 'ds.noReason': 'No reason given', 'ds.noActivity': 'No background activity', 'ds.noSessions': 'No sessions yet', 'ds.refreshOverview': 'Refresh overview', 'ds.pendingCount': '{n} pending', 'ds.activeCount': '{n} active sessions', 'ds.nothingPending': 'Nothing pending', 'ds.action.openSession': 'Open recent session', 'ds.action.newSession': 'New session', 'ds.action.attention': 'Review pending', 'ds.action.connect': 'Set up connection', 'ds.action.refresh': 'Check connection',
|
|
@@ -586,7 +608,7 @@
|
|
|
586
608
|
'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
|
|
587
609
|
'ds.cmdPermission': '/permission Switch permission preset', 'ds.cmdPlan': '/plan Enter/exit plan mode',
|
|
588
610
|
'ds.fsUp': 'Up', 'ds.fsRoot': 'Allowed root', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
589
|
-
'ds.workspaceCreateTitle': 'New workspace', 'ds.workspaceCreateDesc': 'Create a folder in the current file directory and open a new session there.', 'ds.workspaceParent': 'Parent folder', 'ds.workspaceNamePlaceholder': 'For example: my-project', 'ds.workspaceCreate': 'Create & open', 'ds.cancel': 'Cancel', 'ds.workspaceNameRequired': 'Enter a workspace name', 'ds.workspaceExists': 'That folder already exists', 'ds.workspaceInvalidName': 'The name cannot contain path separators', 'ds.workspaceCreateFailed': 'Could not create workspace', 'ds.workspaceCreated': 'Workspace created', 'ds.workspaceCreatedNoSession': 'Workspace created, but the new session could not be opened',
|
|
611
|
+
'ds.workspaceCreateTitle': 'New workspace', 'ds.workspaceCreateDesc': 'Create a folder in the current file directory and open a new session there.', 'ds.workspaceParent': 'Parent folder', 'ds.workspaceNamePlaceholder': 'For example: my-project', 'ds.workspaceCreate': 'Create & open', 'ds.cancel': 'Cancel', 'ds.workspaceNameRequired': 'Enter a workspace name', 'ds.workspaceExists': 'That folder already exists', 'ds.workspaceInvalidName': 'The name cannot contain path separators', 'ds.workspaceCreateFailed': 'Could not create workspace', 'ds.workspaceCreated': 'Workspace created', 'ds.workspaceCreatedNoSession': 'Workspace created, but the new session could not be opened', 'ds.newSessionTitle': 'New session', 'ds.newSessionDesc': 'Choose a DSH workspace for the new session.', 'ds.newSessionWorkspace': 'Workspace', 'ds.newSessionNoWorkspaces': 'No workspaces are available. Create a workspace first.', 'ds.newSessionCreate': 'Create', 'ds.newSessionChooseWorkspace': 'Choose a workspace',
|
|
590
612
|
'ds.groupGeneral': 'General', 'ds.groupGeneralDesc': 'Tool calls, prompt presets',
|
|
591
613
|
'ds.groupServers': 'Servers', 'ds.groupServersDesc': 'Server address and token',
|
|
592
614
|
'ds.groupNotify': 'Notifications', 'ds.groupNotifyDesc': 'Notifications & reminders',
|
|
@@ -602,7 +624,7 @@
|
|
|
602
624
|
'ds.feedback': 'Feedback', 'ds.feedbackGithubDesc': 'Report bugs · suggest features', 'ds.feedbackGiteeDesc': 'Mirror in China, no proxy needed',
|
|
603
625
|
'ds.feedbackBiliDesc': 'Chat on the UP\'s Bilibili page', 'ds.feedbackCopyLink': 'Copy project link', 'ds.feedbackCopyDesc': 'Share it manually',
|
|
604
626
|
'ds.feedbackCopied': 'Project link copied', 'ds.feedbackCopyFailed': 'Copy failed, copy manually',
|
|
605
|
-
'ds.feedbackWrite': 'Write feedback', 'ds.feedbackWriteDesc': 'Submit from the app', 'ds.feedbackModalTitle': 'Write feedback',
|
|
627
|
+
'ds.feedbackWrite': 'Write feedback', 'ds.feedbackWriteDesc': 'Submit from the app', 'ds.feedbackModalTitle': 'Write feedback', 'ds.feedbackIncludeDiagnostics': 'Include compatibility diagnostics (optional)', 'ds.feedbackDiagnosticsPrivacy': 'Uploads only gateway/protocol versions, interface failure summaries, and realtime status; no token, cookie, conversation content, or file path.',
|
|
606
628
|
'ds.feedbackTypeBug': 'Bug', 'ds.feedbackTypeSuggestion': 'Suggestion', 'ds.feedbackTypeOther': 'Other',
|
|
607
629
|
'ds.feedbackMessagePlaceholder': 'Describe the bug or suggestion (required, ≤2000 chars)',
|
|
608
630
|
'ds.feedbackContactPlaceholder': 'Contact (optional): email / WeChat / Bilibili ID',
|
|
@@ -95,6 +95,7 @@ const state = {
|
|
|
95
95
|
pollSeq: { mux: 0, host: 0 },
|
|
96
96
|
fs: { path: null, initial: null, loaded: false, roots: [], rootIndex: 0 },
|
|
97
97
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
98
|
+
workspaces: [],
|
|
98
99
|
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
99
100
|
archivedIds: [],
|
|
100
101
|
view: 'sessions',
|
|
@@ -385,6 +386,7 @@ function openFeedbackModal() {
|
|
|
385
386
|
document.querySelectorAll('#fb-chips .ds-fb-chip').forEach(b => b.classList.toggle('current', b.dataset.fbType === 'bug'))
|
|
386
387
|
$('fb-msg').value = ''
|
|
387
388
|
$('fb-contact').value = ''
|
|
389
|
+
$('fb-include-diagnostics').checked = false
|
|
388
390
|
$('modal-feedback').classList.remove('hidden')
|
|
389
391
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
390
392
|
}
|
|
@@ -507,6 +509,7 @@ async function submitFeedback() {
|
|
|
507
509
|
const type = document.querySelector('#fb-chips .ds-fb-chip.current')?.dataset.fbType || 'bug'
|
|
508
510
|
const message = $('fb-msg').value.trim()
|
|
509
511
|
const contact = $('fb-contact').value.trim()
|
|
512
|
+
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
510
513
|
if (!message) { toast(t('ds.feedbackEmpty'), 'err'); return }
|
|
511
514
|
if (message.length > 2000) { toast(t('ds.feedbackTooLong'), 'err'); return }
|
|
512
515
|
const btn = $('fb-submit')
|
|
@@ -515,7 +518,7 @@ async function submitFeedback() {
|
|
|
515
518
|
const res = await fetch(apiUrl('/feedback'), {
|
|
516
519
|
method: 'POST',
|
|
517
520
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
518
|
-
body: JSON.stringify({ type, message, contact, appVersion: '' })
|
|
521
|
+
body: JSON.stringify({ type, message, contact, appVersion: '', includeDiagnostics })
|
|
519
522
|
})
|
|
520
523
|
let json = {}
|
|
521
524
|
try { json = await res.json() } catch {}
|
|
@@ -1374,6 +1377,11 @@ function onHostFrame(full) {
|
|
|
1374
1377
|
const f = full.payload
|
|
1375
1378
|
if (!f) return
|
|
1376
1379
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) refreshSessions()
|
|
1380
|
+
if (f.type === 'host/session-activity') {
|
|
1381
|
+
const session = state.byId.get(f.sessionId)
|
|
1382
|
+
if (session) { session.updatedAt = Number(f.updatedAt) || Date.now(); renderSessions(); renderOverviewDesktop() }
|
|
1383
|
+
return
|
|
1384
|
+
}
|
|
1377
1385
|
if (f.type === 'host/session-status') {
|
|
1378
1386
|
const s = state.byId.get(f.sessionId)
|
|
1379
1387
|
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
@@ -1523,7 +1531,17 @@ function resyncAfterStreamOpen() {
|
|
|
1523
1531
|
void refreshSessions().then(() => resyncCurrentSession())
|
|
1524
1532
|
}
|
|
1525
1533
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1526
|
-
function
|
|
1534
|
+
function sessionTitleValue(s) {
|
|
1535
|
+
const value = proj(s, 'title', '')
|
|
1536
|
+
return value == null ? '' : String(value).trim()
|
|
1537
|
+
}
|
|
1538
|
+
function hasSessionTitle(s) { return !!sessionTitleValue(s) }
|
|
1539
|
+
function titleOf(s) { return sessionTitleValue(s) || (s?.sessionId ? t('ds.sessionUntitled') : t('ds.sessions')) }
|
|
1540
|
+
function sessionLabelOf(s) {
|
|
1541
|
+
const title = titleOf(s)
|
|
1542
|
+
if (!s?.sessionId || hasSessionTitle(s)) return title
|
|
1543
|
+
return `${title} · ${String(s.sessionId).slice(-8)}`
|
|
1544
|
+
}
|
|
1527
1545
|
function isTopLevelSession(session) {
|
|
1528
1546
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1529
1547
|
}
|
|
@@ -1653,7 +1671,7 @@ function renderSessions() {
|
|
|
1653
1671
|
const renderSession = s => {
|
|
1654
1672
|
const workspace = sessionWorkspaceLabel(s)
|
|
1655
1673
|
const workspaceName = workspaceDisplayName(workspace)
|
|
1656
|
-
const title =
|
|
1674
|
+
const title = sessionLabelOf(s)
|
|
1657
1675
|
return `<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1658
1676
|
<span class="ds-session-title">${esc(title)}</span>
|
|
1659
1677
|
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
@@ -2139,7 +2157,7 @@ function renameSession(sessionId = state.current) {
|
|
|
2139
2157
|
const session = state.byId.get(sessionId)
|
|
2140
2158
|
if (!session) return
|
|
2141
2159
|
renamePendingSessionId = sessionId
|
|
2142
|
-
$('rename-session-input').value =
|
|
2160
|
+
$('rename-session-input').value = sessionTitleValue(session)
|
|
2143
2161
|
$('modal-rename').classList.remove('hidden')
|
|
2144
2162
|
setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
|
|
2145
2163
|
}
|
|
@@ -2473,9 +2491,19 @@ async function createWorkspace() {
|
|
|
2473
2491
|
}
|
|
2474
2492
|
closeWorkspaceModal()
|
|
2475
2493
|
await loadFs(parent || null, true)
|
|
2476
|
-
|
|
2494
|
+
let workspace = null
|
|
2495
|
+
try {
|
|
2496
|
+
const created = await rpc('workspace.create', { path: data.path })
|
|
2497
|
+
workspace = created?.workspace || null
|
|
2498
|
+
} catch {}
|
|
2499
|
+
if (workspace?.workspaceId) {
|
|
2500
|
+
state.workspaces = [...state.workspaces.filter(item => item.workspaceId !== workspace.workspaceId), workspace]
|
|
2501
|
+
}
|
|
2502
|
+
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
2503
|
+
const v = await safeRpc('session.create', sessionPayload, t('ds.toastOpFailed'))
|
|
2477
2504
|
await refreshSessions()
|
|
2478
2505
|
if (v?.sessionId) {
|
|
2506
|
+
if (workspace?.workspaceId) LS.set('lastNewSessionWorkspaceV1', workspace.workspaceId)
|
|
2479
2507
|
toast(t('ds.workspaceCreated'), 'ok')
|
|
2480
2508
|
openSession(v.sessionId)
|
|
2481
2509
|
} else {
|
|
@@ -2559,6 +2587,22 @@ function wbBaseName(p) {
|
|
|
2559
2587
|
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
2560
2588
|
return value.split(/[\\/]/).pop() || value
|
|
2561
2589
|
}
|
|
2590
|
+
function workspaceItems() {
|
|
2591
|
+
return (state.workspaces || []).filter(w => w && typeof w.workspaceId === 'string' && w.workspaceId && typeof w.path === 'string' && w.path)
|
|
2592
|
+
}
|
|
2593
|
+
function workspaceById(workspaceId) {
|
|
2594
|
+
return workspaceItems().find(w => w.workspaceId === workspaceId) || null
|
|
2595
|
+
}
|
|
2596
|
+
function workspaceName(workspace) {
|
|
2597
|
+
return String(workspace?.title || wbBaseName(workspace?.path) || workspace?.path || '').trim()
|
|
2598
|
+
}
|
|
2599
|
+
function workspaceOptionLabel(workspace) {
|
|
2600
|
+
const name = workspaceName(workspace)
|
|
2601
|
+
return workspace.path && workspace.path !== name ? `${name} — ${workspace.path}` : name
|
|
2602
|
+
}
|
|
2603
|
+
function workspaceOptionsHtml(selected = '') {
|
|
2604
|
+
return workspaceItems().map(workspace => `<option value="${esc(workspace.workspaceId)}"${workspace.workspaceId === selected ? ' selected' : ''}>${esc(workspaceOptionLabel(workspace))}</option>`).join('')
|
|
2605
|
+
}
|
|
2562
2606
|
function wbStrictInside(pathValue, rootValue) {
|
|
2563
2607
|
const pathKey = wbPathKey(pathValue)
|
|
2564
2608
|
const rootKey = wbPathKey(rootValue)
|
|
@@ -2661,6 +2705,7 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2661
2705
|
}
|
|
2662
2706
|
const wl = await safeRpc('workspace.list', {}, '')
|
|
2663
2707
|
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
2708
|
+
state.workspaces = wl && Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2664
2709
|
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
2665
2710
|
if (!wb.bound) {
|
|
2666
2711
|
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
@@ -2689,7 +2734,11 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2689
2734
|
if (have.has(wbPathKey(projectPath))) continue
|
|
2690
2735
|
try {
|
|
2691
2736
|
const created = await rpc('workspace.create', { path: projectPath })
|
|
2692
|
-
if (created?.workspace) {
|
|
2737
|
+
if (created?.workspace) {
|
|
2738
|
+
items.push(created.workspace)
|
|
2739
|
+
state.workspaces.push(created.workspace)
|
|
2740
|
+
have.add(wbPathKey(projectPath))
|
|
2741
|
+
}
|
|
2693
2742
|
} catch {}
|
|
2694
2743
|
}
|
|
2695
2744
|
}
|
|
@@ -2737,7 +2786,7 @@ function renderWorkbench() {
|
|
|
2737
2786
|
</button>
|
|
2738
2787
|
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
2739
2788
|
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
2740
|
-
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}"><span class="ds-wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(
|
|
2789
|
+
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}"><span class="ds-wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(sessionLabelOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
|
|
2741
2790
|
</div>
|
|
2742
2791
|
</div>`
|
|
2743
2792
|
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
@@ -2767,6 +2816,46 @@ function renderWorkbench() {
|
|
|
2767
2816
|
panel.querySelectorAll('[data-wb-session]').forEach(button => button.addEventListener('click', () => openSession(button.dataset.wbSession)))
|
|
2768
2817
|
panel.querySelectorAll('[data-wb-unbind-panel]').forEach(button => button.addEventListener('click', unbindWorkbench))
|
|
2769
2818
|
}
|
|
2819
|
+
function renderNewSessionWorkspace(preferredId = '') {
|
|
2820
|
+
const items = workspaceItems()
|
|
2821
|
+
const select = $('new-session-workspace')
|
|
2822
|
+
if (!select) return
|
|
2823
|
+
const current = workspaceById(preferredId || select.value)?.workspaceId || items[0]?.workspaceId || ''
|
|
2824
|
+
select.innerHTML = workspaceOptionsHtml(current)
|
|
2825
|
+
select.disabled = !items.length
|
|
2826
|
+
const workspace = workspaceById(select.value)
|
|
2827
|
+
$('new-session-workspace-name').textContent = workspace ? workspaceName(workspace) : ''
|
|
2828
|
+
$('new-session-workspace-path').textContent = workspace?.path || ''
|
|
2829
|
+
$('new-session-empty').classList.toggle('hidden', !!items.length)
|
|
2830
|
+
$('new-session-create').disabled = !workspace
|
|
2831
|
+
}
|
|
2832
|
+
async function openNewSessionModal() {
|
|
2833
|
+
if (!state.token) return showView('view-settings')
|
|
2834
|
+
if (!workspaceItems().length) await refreshWorkbench({ silent: true })
|
|
2835
|
+
renderNewSessionWorkspace(LS.get('lastNewSessionWorkspaceV1', ''))
|
|
2836
|
+
$('modal-new-session').classList.remove('hidden')
|
|
2837
|
+
setTimeout(() => $('new-session-workspace')?.focus(), 50)
|
|
2838
|
+
}
|
|
2839
|
+
function closeNewSessionModal() { $('modal-new-session').classList.add('hidden') }
|
|
2840
|
+
async function createSessionInWorkspace() {
|
|
2841
|
+
if (createSessionInWorkspace.busy) return
|
|
2842
|
+
const workspaceId = $('new-session-workspace').value
|
|
2843
|
+
if (!workspaceById(workspaceId)) return toast(t('ds.newSessionChooseWorkspace'), 'err')
|
|
2844
|
+
createSessionInWorkspace.busy = true
|
|
2845
|
+
const button = $('new-session-create')
|
|
2846
|
+
button.disabled = true
|
|
2847
|
+
try {
|
|
2848
|
+
const value = await safeRpc('session.create', { workspaceId }, '')
|
|
2849
|
+
if (!value?.sessionId) return
|
|
2850
|
+
LS.set('lastNewSessionWorkspaceV1', workspaceId)
|
|
2851
|
+
closeNewSessionModal()
|
|
2852
|
+
await refreshSessions()
|
|
2853
|
+
openSession(value.sessionId)
|
|
2854
|
+
} finally {
|
|
2855
|
+
createSessionInWorkspace.busy = false
|
|
2856
|
+
button.disabled = false
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2770
2859
|
const wbFs = { path: null, initial: null }
|
|
2771
2860
|
function openWorkbenchModal() {
|
|
2772
2861
|
$('modal-workbench').classList.remove('hidden')
|
|
@@ -2968,7 +3057,7 @@ function renderOverviewDesktop() {
|
|
|
2968
3057
|
]
|
|
2969
3058
|
$('ds-overview-attention-count').textContent = pending.length ? t('ds.pendingCount', { n: pending.length }) : '—'
|
|
2970
3059
|
$('ds-overview-attention-list').innerHTML = pending.length ? pending.slice(0, 4).map(({ kind, item }) => {
|
|
2971
|
-
const title =
|
|
3060
|
+
const title = sessionLabelOf(state.byId.get(item.sessionId))
|
|
2972
3061
|
if (kind === 'approval') return `<div class="ds-overview-attention-item" data-ds-overview-approval="${esc(item.approvalId)}">
|
|
2973
3062
|
<span class="ds-overview-mark">⌁</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(item.toolName || t('ds.toolDefault'))}</span><span class="ds-overview-item-desc">${esc(desktopApprovalDetail(item))} · ${esc(title)}</span></span>
|
|
2974
3063
|
<span class="ds-overview-actions"><button class="ds-btn allow" data-ds-overview-approve="1">${t('ds.allow')}</button><button class="ds-btn reject" data-ds-overview-approve="0">${t('ds.reject')}</button></span>
|
|
@@ -3012,7 +3101,7 @@ function renderOverviewDesktop() {
|
|
|
3012
3101
|
$('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
|
|
3013
3102
|
$('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
|
|
3014
3103
|
$('ds-overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="ds-overview-session-item ${s.running ? 'running' : ''}" data-ds-overview-session="${esc(s.sessionId)}">
|
|
3015
|
-
<span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(
|
|
3104
|
+
<span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(sessionLabelOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="ds-overview-arrow">›</span>
|
|
3016
3105
|
</button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
|
|
3017
3106
|
$('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
|
|
3018
3107
|
}
|
|
@@ -3121,17 +3210,7 @@ function updateConn() {
|
|
|
3121
3210
|
|
|
3122
3211
|
/* ---------------- 初始化 ---------------- */
|
|
3123
3212
|
function bindUi() {
|
|
3124
|
-
$('btn-new-session').addEventListener('click',
|
|
3125
|
-
let payload = {}
|
|
3126
|
-
// 与移动端保持一致:新会话继承 DSH 当前工作目录;查询失败时兼容回退。
|
|
3127
|
-
try {
|
|
3128
|
-
const host = await rpc('host.describe', {}, 5000)
|
|
3129
|
-
const cwd = typeof host?.cwd === 'string' ? host.cwd.trim() : ''
|
|
3130
|
-
if (cwd) payload = { cwd }
|
|
3131
|
-
} catch {}
|
|
3132
|
-
const v = await safeRpc('session.create', payload, '')
|
|
3133
|
-
if (v?.sessionId) { await refreshSessions(); openSession(v.sessionId) }
|
|
3134
|
-
})
|
|
3213
|
+
$('btn-new-session').addEventListener('click', openNewSessionModal)
|
|
3135
3214
|
$('btn-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
3136
3215
|
$('session-sort')?.addEventListener('change', (e) => {
|
|
3137
3216
|
state.sessionSort = e.target.value === 'workspace' ? 'workspace' : 'time'
|
|
@@ -3254,6 +3333,10 @@ function bindUi() {
|
|
|
3254
3333
|
$('workspace-create').addEventListener('click', createWorkspace)
|
|
3255
3334
|
$('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
|
|
3256
3335
|
$('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
|
|
3336
|
+
$('new-session-workspace').addEventListener('change', () => renderNewSessionWorkspace())
|
|
3337
|
+
$('new-session-cancel').addEventListener('click', closeNewSessionModal)
|
|
3338
|
+
$('new-session-create').addEventListener('click', createSessionInWorkspace)
|
|
3339
|
+
$('modal-new-session').addEventListener('click', (e) => { if (e.target === $('modal-new-session')) closeNewSessionModal() })
|
|
3257
3340
|
$('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
|
|
3258
3341
|
// 反馈
|
|
3259
3342
|
$('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackMenu() })
|
package/public/index.html
CHANGED
|
@@ -665,6 +665,7 @@
|
|
|
665
665
|
</div>
|
|
666
666
|
<textarea id="fb-msg" class="fb-textarea" rows="5" maxlength="2000" data-i18n-placeholder="feedback.messagePlaceholder" placeholder="请描述遇到的问题或建议(必填,≤2000 字)"></textarea>
|
|
667
667
|
<input id="fb-contact" class="fb-input" maxlength="200" data-i18n-placeholder="feedback.contactPlaceholder" placeholder="联系方式(可选):邮箱 / 微信 / B站 ID">
|
|
668
|
+
<label class="fb-diagnostics"><input id="fb-include-diagnostics" type="checkbox"> <span data-i18n="feedback.includeDiagnostics">附带兼容性诊断日志(可选)</span><small data-i18n="feedback.diagnosticsPrivacy">仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。</small></label>
|
|
668
669
|
</div>
|
|
669
670
|
<div class="modal-actions">
|
|
670
671
|
<button id="fb-cancel" class="btn subtle" data-i18n="feedback.cancel">取消</button>
|
|
@@ -981,7 +982,7 @@
|
|
|
981
982
|
'groups.drawerOpen': '切换服务器组', 'groups.drawerClose': '关闭组列表', 'groups.drawerEyebrow': '快速切换', 'groups.drawerTitle': '切换服务器组', 'groups.drawerCurrent': '当前连接组',
|
|
982
983
|
'groups.drawerManage': '管理服务器与组', 'groups.drawerManageDesc': '编辑、测速和调整组', 'groups.serverCount': '{count} 台服务器',
|
|
983
984
|
'stream.error': '事件流错误:{msg}',
|
|
984
|
-
'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
|
|
985
|
+
'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.untitled': '新会话', 'session.running': '运行中', 'session.interrupted': '已中断',
|
|
985
986
|
'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
|
|
986
987
|
'session.confirmStop': '停止当前回合?排队中的消息不会被删除。', 'session.stopFailed': '停止失败', 'session.stopRequested': '已请求停止本轮', 'session.archive': '归档', 'session.archiveConfirm': '归档这个会话?归档后可在“显示已归档会话”中打开。', 'session.archiveFailed': '归档失败:{msg}', 'session.archived': '会话已归档', 'session.archiveTitle': '确认归档会话', 'session.archiveDesc': '归档后会话将从普通列表隐藏,但不会删除对话记录。', 'session.archiveConversation': '对话', 'session.archiveWorkspace': '工作区', 'session.archiveCancel': '取消', 'session.archiveConfirmAction': '确认归档', 'session.rename': '重命名会话', 'session.renameTitle': '重命名会话', 'session.renamePlaceholder': '输入新的会话名称', 'session.renameCancel': '取消', 'session.renameConfirm': '保存', 'session.renameEmpty': '会话名称不能为空', 'session.renameFailed': '重命名失败', 'session.renamed': '会话名称已更新',
|
|
987
988
|
'notify.approvalTitle': '工具审批', 'notify.approvalBody': '{tool} 需要批准', 'notify.questionTitle': 'DSH 提问', 'notify.questionBody': '需要你回答',
|
|
@@ -1129,7 +1130,7 @@
|
|
|
1129
1130
|
'feedback.title': '反馈', 'feedback.githubDesc': '反馈 bug / 提建议', 'feedback.giteeDesc': '国内镜像,无需代理',
|
|
1130
1131
|
'feedback.biliDesc': 'UP 动态页交流', 'feedback.copyLink': '复制项目链接', 'feedback.copyDesc': '手动分享给朋友',
|
|
1131
1132
|
'feedback.copied': '项目链接已复制', 'feedback.copyFailed': '复制失败,请手动复制',
|
|
1132
|
-
'feedback.write': '写反馈', 'feedback.writeDesc': 'App 内直接提交', 'feedback.modalTitle': '写反馈',
|
|
1133
|
+
'feedback.write': '写反馈', 'feedback.writeDesc': 'App 内直接提交', 'feedback.modalTitle': '写反馈', 'feedback.includeDiagnostics': '附带兼容性诊断日志(可选)', 'feedback.diagnosticsPrivacy': '仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。',
|
|
1133
1134
|
'feedback.typeBug': 'Bug', 'feedback.typeSuggestion': '建议', 'feedback.typeOther': '其他',
|
|
1134
1135
|
'feedback.messagePlaceholder': '请描述遇到的问题或建议(必填,≤2000 字)',
|
|
1135
1136
|
'feedback.contactPlaceholder': '联系方式(可选):邮箱 / 微信 / B站 ID',
|
|
@@ -1214,7 +1215,7 @@
|
|
|
1214
1215
|
'groups.drawerOpen': 'Switch server group', 'groups.drawerClose': 'Close group list', 'groups.drawerEyebrow': 'QUICK SWITCH', 'groups.drawerTitle': 'Switch server group', 'groups.drawerCurrent': 'Current connection group',
|
|
1215
1216
|
'groups.drawerManage': 'Manage servers and groups', 'groups.drawerManageDesc': 'Edit, test, and organize groups', 'groups.serverCount': '{count} servers',
|
|
1216
1217
|
'stream.error': 'Event stream error: {msg}',
|
|
1217
|
-
'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
|
|
1218
|
+
'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.untitled': 'New session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
|
|
1218
1219
|
'session.stop': 'Stop turn', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}', 'session.recovering': 'Restoring session…', 'session.recoveryReady': 'Session ready', 'session.recoveryFailed': 'Session restore failed', 'session.recoveryCached': 'Viewing cached history',
|
|
1219
1220
|
'session.confirmStop': 'Stop the current turn? Queued messages will be kept.', 'session.stopFailed': 'Stop failed', 'session.stopRequested': 'Stop requested', 'session.archive': 'Archive', 'session.archiveConfirm': 'Archive this session? You can open it from “Show archived”.', 'session.archiveFailed': 'Archive failed: {msg}', 'session.archived': 'Session archived', 'session.archiveTitle': 'Confirm archive', 'session.archiveDesc': 'The session will be hidden from the normal list, but its conversation will not be deleted.', 'session.archiveConversation': 'Conversation', 'session.archiveWorkspace': 'Workspace', 'session.archiveCancel': 'Cancel', 'session.archiveConfirmAction': 'Archive session', 'session.rename': 'Rename session', 'session.renameTitle': 'Rename session', 'session.renamePlaceholder': 'Enter a new session name', 'session.renameCancel': 'Cancel', 'session.renameConfirm': 'Save', 'session.renameEmpty': 'Session name cannot be empty', 'session.renameFailed': 'Rename failed', 'session.renamed': 'Session name updated',
|
|
1220
1221
|
'notify.approvalTitle': 'Tool approval', 'notify.approvalBody': '{tool} needs approval', 'notify.questionTitle': 'DSH question', 'notify.questionBody': 'Needs your answer',
|
|
@@ -1362,7 +1363,7 @@
|
|
|
1362
1363
|
'feedback.title': 'Feedback', 'feedback.githubDesc': 'Report bugs · suggest features', 'feedback.giteeDesc': 'Mirror in China, no proxy needed',
|
|
1363
1364
|
'feedback.biliDesc': 'Chat on the UP\'s Bilibili page', 'feedback.copyLink': 'Copy project link', 'feedback.copyDesc': 'Share it manually',
|
|
1364
1365
|
'feedback.copied': 'Project link copied', 'feedback.copyFailed': 'Copy failed, copy manually',
|
|
1365
|
-
'feedback.write': 'Write feedback', 'feedback.writeDesc': 'Submit from the app', 'feedback.modalTitle': 'Write feedback',
|
|
1366
|
+
'feedback.write': 'Write feedback', 'feedback.writeDesc': 'Submit from the app', 'feedback.modalTitle': 'Write feedback', 'feedback.includeDiagnostics': 'Include compatibility diagnostics (optional)', 'feedback.diagnosticsPrivacy': 'Uploads only gateway/protocol versions, interface failure summaries, and realtime status; no token, cookie, conversation content, or file path.',
|
|
1366
1367
|
'feedback.typeBug': 'Bug', 'feedback.typeSuggestion': 'Suggestion', 'feedback.typeOther': 'Other',
|
|
1367
1368
|
'feedback.messagePlaceholder': 'Describe the bug or suggestion (required, ≤2000 chars)',
|
|
1368
1369
|
'feedback.contactPlaceholder': 'Contact (optional): email / WeChat / Bilibili ID',
|
package/public/styles.css
CHANGED
|
@@ -1151,6 +1151,9 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
|
|
|
1151
1151
|
}
|
|
1152
1152
|
.fb-textarea:focus, .fb-input:focus { border-color: var(--dsr-accent-line); }
|
|
1153
1153
|
.fb-input { min-height: 42px; }
|
|
1154
|
+
.fb-diagnostics { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px 8px; align-items:start; margin:-1px 1px 4px; color:var(--dsr-text); font-size:13px; line-height:1.45; cursor:pointer; }
|
|
1155
|
+
.fb-diagnostics input { width:17px; height:17px; margin:1px 0 0; accent-color:var(--dsr-accent); }
|
|
1156
|
+
.fb-diagnostics small { grid-column:2; color:var(--dsr-muted); font-size:11px; line-height:1.45; }
|
|
1154
1157
|
.feedback-success-card { max-width: 360px; text-align: center; padding: 22px 20px 18px; }
|
|
1155
1158
|
.feedback-success-icon {
|
|
1156
1159
|
width: 52px; height: 52px; display: grid; place-items: center; margin: 0 auto 13px; border-radius: 50%;
|
package/public/update.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.24",
|
|
3
3
|
"apkUrl": "dsh-remote.apk",
|
|
4
|
-
"sha256": "
|
|
5
|
-
"releasedAt": "2026-09-
|
|
6
|
-
"notes": "0.6.
|
|
4
|
+
"sha256": "524f38fd3c1f067ab1ce8bad692649366ca9b775dcf16c99ee9e21058c422529",
|
|
5
|
+
"releasedAt": "2026-09-05T13:01:00.792Z",
|
|
6
|
+
"notes": "0.6.24:同步 App 与宽屏客户端的会话命名体验;未命名会话显示为“新会话”并保留短标识,创建会话时可选择指定工作区;补充空会话清理与相关回归验证。",
|
|
7
7
|
"history": [
|
|
8
|
+
{
|
|
9
|
+
"version": "0.6.24",
|
|
10
|
+
"notes": "0.6.24:同步 App 与宽屏客户端的会话命名体验;未命名会话显示为“新会话”并保留短标识,创建会话时可选择指定工作区;补充空会话清理与相关回归验证。"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.6.23",
|
|
14
|
+
"notes": "0.6.23:加强 DSH 版本兼容。旧版点号 RPC 与新版 slash Remote API 可自动协商和回退,修复部分新版 DSH 中 host.describe、创建会话等请求返回 404 的问题;DSH 重启或升级后会重新识别协议并恢复实时通道。反馈时可由用户选择附带脱敏兼容性诊断,便于定位接口问题;诊断不包含令牌、Cookie、对话内容或文件路径。"
|
|
15
|
+
},
|
|
8
16
|
{
|
|
9
17
|
"version": "0.6.22",
|
|
10
18
|
"notes": "0.6.22:斜杠命令统一后台受理并显示运行状态与耗时,/compact 不再受两分钟请求等待限制;/export 可在手机保存到系统下载目录、在浏览器按下载设置保存会话 ZIP;修复命令桥接超时后被误作为普通消息发送的问题。首次有多条未读公告时改为单一分页窗口,可左右切换,确认一次即可全部标为已读。"
|
|
@@ -36,14 +44,6 @@
|
|
|
36
44
|
{
|
|
37
45
|
"version": "0.6.15",
|
|
38
46
|
"notes": "0.6.15:修复拖拽候选项拦截普通竖向滑动的问题,卡片中间也可自然滚动;仅在长按真正进入拖拽后锁定页面,并补充指针结束、取消、失焦和页面隐藏时的解锁清理。保留边缘自动滚动、占位符同步重排、主列表/工作台工作区与组内对话拖动。"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"version": "0.6.14",
|
|
42
|
-
"notes": "0.6.14-rc.7:将 Android 后台轮询服务接入与前台 App 相同的 client ID,修复 Dalvik 后台请求被显示为第二台未知设备;保留旧记录的顺序无关迁移;补齐文件上传请求的持久设备 ID;实时扫码继续使用 Worker 解码并降低取样负载;修复非用户来源的 DSH user/message 被误显示为用户输入;新增子代理折叠、排队消息插话、运行中提示和按轮次稳定排序;Markdown 支持 GFM 表格。"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"version": "0.6.13",
|
|
46
|
-
"notes": "0.6.13 正式版:新增首次连接 Doctor,集中检查 DSH 服务、远程网关、局域网地址、防火墙、终端配对和实时消息通道;网关控制台新增可选的独立设备密钥,支持设备备注、最近 IP、二维码、令牌轮换、复制和退出,共享令牌在启用后仅保留管理权限;手机端和桌面端支持实时思考内容,并为未声明推理档位的模型提供 low、high、max 三档选择;普通会话列表、工作区树和主页统计不再混入子代理内部会话;Android App 落后于网关版本时显示明确更新提醒;新增网关协议与能力协商并兼容旧网关,同时补充设备隔离、重启持久化、推理显示、会话过滤和版本差异回归测试。"
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
49
|
}
|
package/public/version.json
CHANGED