dsh-remote-plugin 0.6.18 → 0.6.20
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/client.js +9 -2
- package/gateway.cjs +610 -33
- package/index.mjs +50 -3
- package/package.json +1 -1
- package/public/admin.html +16 -17
- package/public/admin.js +6 -5
- package/public/app.js +94 -16
- package/public/desktop/desktop.css +57 -11
- package/public/desktop/desktop.html +24 -20
- package/public/desktop/desktop.js +110 -18
- package/public/index.html +33 -29
- package/public/motion.js +105 -17
- package/public/plugin.html +6 -5
- package/public/styles.css +67 -27
- package/public/theme-vars.css +244 -167
- package/public/theme.js +2 -2
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/gateway.cjs
CHANGED
|
@@ -78,6 +78,7 @@ const UPSTREAM_AUTHORITY = `${UPSTREAM.hostname}${UPSTREAM.port ? ':' + UPSTREAM
|
|
|
78
78
|
const DSH_HEALTH_PATH = String(process.env.DSH_HEALTH_PATH || '/').startsWith('/')
|
|
79
79
|
? String(process.env.DSH_HEALTH_PATH || '/')
|
|
80
80
|
: '/' + String(process.env.DSH_HEALTH_PATH)
|
|
81
|
+
const DSH_UPSTREAM_COOKIE_FILE = process.env.DSH_REMOTE_DSH_COOKIE_FILE || path.join(os.homedir(), '.dsh-remote', 'dsh-upstream.cookie')
|
|
81
82
|
const TOKEN_FILE = process.env.TOKEN_FILE || path.join(os.homedir(), '.dsh-remote', 'token')
|
|
82
83
|
const NOTES_FILE = process.env.DSH_REMOTE_NOTES || path.join(os.homedir(), '.dsh-remote', 'device-notes.json')
|
|
83
84
|
const DEVICE_KEYS_FILE = process.env.DSH_REMOTE_DEVICE_KEYS || path.join(os.homedir(), '.dsh-remote', 'device-keys.json')
|
|
@@ -94,6 +95,24 @@ const HTTP_REQUEST_TIMEOUT_MS = durationEnv('GATEWAY_HTTP_REQUEST_TIMEOUT_MS', 1
|
|
|
94
95
|
const HTTP_HEADERS_TIMEOUT_MS = durationEnv('GATEWAY_HTTP_HEADERS_TIMEOUT_MS', 120000, 1000, 10 * 60 * 1000)
|
|
95
96
|
const HTTP_KEEPALIVE_TIMEOUT_MS = durationEnv('GATEWAY_HTTP_KEEPALIVE_TIMEOUT_MS', 65000, 1000, 10 * 60 * 1000)
|
|
96
97
|
|
|
98
|
+
/** 读取插件兑换的新版 DSH 会话 Cookie;动态读取允许 DSH 重启后原地刷新。 */
|
|
99
|
+
function dshUpstreamCookie() {
|
|
100
|
+
try {
|
|
101
|
+
const value = fs.readFileSync(DSH_UPSTREAM_COOKIE_FILE, 'utf8').trim()
|
|
102
|
+
if (!value.includes('=') || value.length > 4096 || /[\0\r\n]/.test(value)) return ''
|
|
103
|
+
return value
|
|
104
|
+
} catch {
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dshUpstreamHeaders(base = {}) {
|
|
110
|
+
const headers = { ...base }
|
|
111
|
+
const cookie = dshUpstreamCookie()
|
|
112
|
+
if (cookie) headers.cookie = cookie
|
|
113
|
+
return headers
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
// 更新检查: GitHub 为默认源, 可用环境变量覆盖(国内镜像 / 代理)
|
|
98
117
|
const UPDATE_CHECK_URL = process.env.UPDATE_CHECK_URL ||
|
|
99
118
|
'https://api.github.com/repos/Blank-not-black/dsh-Remote/releases/latest'
|
|
@@ -1055,6 +1074,7 @@ async function probeDshUpstream() {
|
|
|
1055
1074
|
const startedAt = Date.now()
|
|
1056
1075
|
try {
|
|
1057
1076
|
const probe = await fetch(new URL(DSH_HEALTH_PATH, UPSTREAM), {
|
|
1077
|
+
headers: dshUpstreamHeaders(),
|
|
1058
1078
|
signal: AbortSignal.timeout(Math.min(2500, UPSTREAM_REQUEST_TIMEOUT_MS)),
|
|
1059
1079
|
cache: 'no-store',
|
|
1060
1080
|
})
|
|
@@ -1314,6 +1334,211 @@ const eventCollectorState = {
|
|
|
1314
1334
|
}
|
|
1315
1335
|
const eventCollectors = { mux: null, host: null }
|
|
1316
1336
|
|
|
1337
|
+
// DSH 0.1.2-alpha.1 replaced dotted RPC names and the two downlink sockets with
|
|
1338
|
+
// generated slash RPCs over one logical-stream mux. Keep the public Remote
|
|
1339
|
+
// contract stable here so older DSH releases and newer generated Remotes can
|
|
1340
|
+
// both serve the same zero-build clients.
|
|
1341
|
+
let upstreamApiFlavor = 'unknown'
|
|
1342
|
+
let upstreamApiFlavorProbe = null
|
|
1343
|
+
const modernState = {
|
|
1344
|
+
home: '',
|
|
1345
|
+
eventClientId: '',
|
|
1346
|
+
sessions: new Map(),
|
|
1347
|
+
sessionCursors: new Map(),
|
|
1348
|
+
workspaces: { items: [], archivedSessionIds: [] },
|
|
1349
|
+
pendingEvents: new Map(),
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
async function callUpstreamRemote(endpoint, args, rpcId = crypto.randomUUID()) {
|
|
1353
|
+
const target = new URL('/api/' + endpoint, UPSTREAM)
|
|
1354
|
+
const response = await fetch(target, {
|
|
1355
|
+
method: 'POST',
|
|
1356
|
+
headers: dshUpstreamHeaders({ 'content-type': 'application/json' }),
|
|
1357
|
+
body: JSON.stringify({ type: 'client-request', rpcId, method: endpoint, payload: { args } }),
|
|
1358
|
+
signal: AbortSignal.timeout(UPSTREAM_REQUEST_TIMEOUT_MS),
|
|
1359
|
+
})
|
|
1360
|
+
const body = await response.json().catch(() => null)
|
|
1361
|
+
return { status: response.status, body }
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async function detectUpstreamApiFlavor(force = false) {
|
|
1365
|
+
if (!force && upstreamApiFlavor !== 'unknown') return upstreamApiFlavor
|
|
1366
|
+
if (!force && upstreamApiFlavorProbe) return upstreamApiFlavorProbe
|
|
1367
|
+
upstreamApiFlavorProbe = (async () => {
|
|
1368
|
+
try {
|
|
1369
|
+
const probe = await callUpstreamRemote('session/list', { _request: {} })
|
|
1370
|
+
upstreamApiFlavor = probe.status === 200
|
|
1371
|
+
&& probe.body?.result?.ok === true
|
|
1372
|
+
&& Array.isArray(probe.body?.result?.value?.items)
|
|
1373
|
+
? 'modern'
|
|
1374
|
+
: 'legacy'
|
|
1375
|
+
if (upstreamApiFlavor === 'modern' && probe.body?.result?.ok) {
|
|
1376
|
+
updateModernSessions(probe.body.result.value?.items)
|
|
1377
|
+
}
|
|
1378
|
+
} catch {
|
|
1379
|
+
upstreamApiFlavor = 'legacy'
|
|
1380
|
+
} finally {
|
|
1381
|
+
upstreamApiFlavorProbe = null
|
|
1382
|
+
}
|
|
1383
|
+
return upstreamApiFlavor
|
|
1384
|
+
})()
|
|
1385
|
+
return upstreamApiFlavorProbe
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function updateModernSessions(items) {
|
|
1389
|
+
if (!Array.isArray(items)) return
|
|
1390
|
+
modernState.sessions.clear()
|
|
1391
|
+
for (const item of items) {
|
|
1392
|
+
if (!item?.sessionId) continue
|
|
1393
|
+
modernState.sessions.set(item.sessionId, item)
|
|
1394
|
+
const cursor = Number(item.projections?.asOfSeq)
|
|
1395
|
+
if (Number.isSafeInteger(cursor) && cursor >= -1) modernState.sessionCursors.set(item.sessionId, cursor)
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function legacyEnvelope(rpcId, result) {
|
|
1400
|
+
return { rpcId, result }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function modernError(message, code = 'upstream-incompatible', details = {}) {
|
|
1404
|
+
return { ok: false, error: { code, message, details } }
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function legacyHistoryValue(value, summary) {
|
|
1408
|
+
const records = Array.isArray(value?.records) ? value.records : []
|
|
1409
|
+
return {
|
|
1410
|
+
events: records.map(record => ({ event: record?.event })).filter(entry => entry.event),
|
|
1411
|
+
hasMore: !!value?.hasMore,
|
|
1412
|
+
...(summary?.projections ? { projections: summary.projections } : {}),
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function refreshModernSessions() {
|
|
1417
|
+
const response = await callUpstreamRemote('session/list', { _request: {} })
|
|
1418
|
+
if (response.status === 200 && response.body?.result?.ok) updateModernSessions(response.body.result.value?.items)
|
|
1419
|
+
return response
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function translateModernRpc(method, payload, rpcId) {
|
|
1423
|
+
let endpoint = ''
|
|
1424
|
+
let args = {}
|
|
1425
|
+
let transform = value => value
|
|
1426
|
+
|
|
1427
|
+
if (method === 'host.describe') {
|
|
1428
|
+
return legacyEnvelope(rpcId, { ok: true, value: { home: modernState.home || os.homedir(), canOpenPath: false } })
|
|
1429
|
+
}
|
|
1430
|
+
if (method === 'workspace.list') {
|
|
1431
|
+
return legacyEnvelope(rpcId, { ok: true, value: modernState.workspaces })
|
|
1432
|
+
}
|
|
1433
|
+
if (method === 'session.list') {
|
|
1434
|
+
const response = await refreshModernSessions()
|
|
1435
|
+
return response.body || legacyEnvelope(rpcId, modernError('DSH session/list returned no JSON response'))
|
|
1436
|
+
}
|
|
1437
|
+
if (method === 'session.history') {
|
|
1438
|
+
let summary = modernState.sessions.get(payload.sessionId)
|
|
1439
|
+
if (!summary) {
|
|
1440
|
+
await refreshModernSessions()
|
|
1441
|
+
summary = modernState.sessions.get(payload.sessionId)
|
|
1442
|
+
}
|
|
1443
|
+
const throughSeq = modernState.sessionCursors.get(payload.sessionId) ?? Number(summary?.projections?.asOfSeq)
|
|
1444
|
+
if (!Number.isSafeInteger(throughSeq) || throughSeq < -1) {
|
|
1445
|
+
return legacyEnvelope(rpcId, modernError('DSH did not expose a history cursor for this session', 'session-not-found'))
|
|
1446
|
+
}
|
|
1447
|
+
endpoint = 'session/page'
|
|
1448
|
+
args = { request: {
|
|
1449
|
+
address: { kind: 'session', sessionId: payload.sessionId },
|
|
1450
|
+
throughSeq,
|
|
1451
|
+
...(payload.beforeSeq === undefined ? {} : { beforeSeq: payload.beforeSeq }),
|
|
1452
|
+
...(payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
|
|
1453
|
+
} }
|
|
1454
|
+
transform = value => legacyHistoryValue(value, summary)
|
|
1455
|
+
} else if (method === 'session.models') {
|
|
1456
|
+
endpoint = 'session/modelCatalog'
|
|
1457
|
+
args = {}
|
|
1458
|
+
transform = catalog => {
|
|
1459
|
+
const summary = modernState.sessions.get(payload.sessionId)
|
|
1460
|
+
const projected = summary?.projections?.values?.modelSelection
|
|
1461
|
+
const current = projected?.next || projected?.lastUsed || catalog?.default
|
|
1462
|
+
return {
|
|
1463
|
+
current,
|
|
1464
|
+
routable: !!current && (!Array.isArray(catalog?.routableProviders) || catalog.routableProviders.includes(current.provider)),
|
|
1465
|
+
groups: catalog?.groups || [],
|
|
1466
|
+
failures: catalog?.failures || [],
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
} else if (method.startsWith('session.')) {
|
|
1470
|
+
const verb = method.slice('session.'.length)
|
|
1471
|
+
if (!['search', 'create', 'selectModel', 'rename', 'fork', 'prompt', 'attachment', 'updateQueue', 'cancel'].includes(verb)) return null
|
|
1472
|
+
endpoint = 'session/' + verb
|
|
1473
|
+
const request = verb === 'prompt' && !payload.requestId
|
|
1474
|
+
? { ...payload, requestId: crypto.randomUUID() }
|
|
1475
|
+
: payload
|
|
1476
|
+
args = verb === 'search' ? { request } : { request }
|
|
1477
|
+
} else if (method.startsWith('workspace.')) {
|
|
1478
|
+
const verb = method.slice('workspace.'.length)
|
|
1479
|
+
if (!['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore', 'archiveSession'].includes(verb)) return null
|
|
1480
|
+
endpoint = 'workspace/' + verb
|
|
1481
|
+
args = { request: payload }
|
|
1482
|
+
} else if (method.startsWith('goal.')) {
|
|
1483
|
+
const verb = method.slice('goal.'.length)
|
|
1484
|
+
if (!['create', 'edit', 'pause', 'resume', 'complete', 'clear'].includes(verb)) return null
|
|
1485
|
+
endpoint = 'goals/' + verb
|
|
1486
|
+
args = verb === 'create'
|
|
1487
|
+
? { agentId: payload.sessionId, request: { objective: payload.objective, ...(payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: payload.maxGoalRounds }) } }
|
|
1488
|
+
: { agentId: payload.sessionId, ref: payload.ref, ...(verb === 'edit' ? { request: { ...(payload.objective === undefined ? {} : { objective: payload.objective }), ...(payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: payload.maxGoalRounds }) } } : {}) }
|
|
1489
|
+
transform = value => verb === 'create'
|
|
1490
|
+
? value
|
|
1491
|
+
: verb === 'clear'
|
|
1492
|
+
? { cleared: true }
|
|
1493
|
+
: { ref: { id: value?.id, revision: value?.revision } }
|
|
1494
|
+
} else if (method === 'subagent.list') {
|
|
1495
|
+
endpoint = 'subagents/list'
|
|
1496
|
+
args = { parentSessionId: payload.parentSessionId }
|
|
1497
|
+
} else if (method === 'subagent.interrupt') {
|
|
1498
|
+
endpoint = 'subagents/interruptByParent'
|
|
1499
|
+
args = { childSessionId: payload.childSessionId, parentSessionId: payload.parentSessionId, mode: payload.mode }
|
|
1500
|
+
} else if (method === 'settings.describe') {
|
|
1501
|
+
endpoint = 'settings/describe'
|
|
1502
|
+
args = {}
|
|
1503
|
+
} else if (method === 'settings.mutate') {
|
|
1504
|
+
endpoint = 'settings/mutate'
|
|
1505
|
+
args = { ns: payload.ns, ops: payload.ops, expectedRevision: payload.expectedRevision }
|
|
1506
|
+
} else if (method === 'settings.openDocument') {
|
|
1507
|
+
endpoint = 'settings/openSettingsDocument'
|
|
1508
|
+
args = {}
|
|
1509
|
+
} else if (method.startsWith('credentials.')) {
|
|
1510
|
+
const verb = method.slice('credentials.'.length)
|
|
1511
|
+
if (!['describe', 'set', 'unset'].includes(verb)) return null
|
|
1512
|
+
endpoint = 'credentials/' + verb
|
|
1513
|
+
args = verb === 'describe' ? { refs: payload.refs } : verb === 'set' ? { ref: payload.ref, value: payload.value } : { ref: payload.ref }
|
|
1514
|
+
if (verb !== 'describe') transform = () => ({})
|
|
1515
|
+
} else if (method === 'llm.providers') {
|
|
1516
|
+
endpoint = 'llm/listConfigurableProviders'
|
|
1517
|
+
args = {}
|
|
1518
|
+
transform = values => ({ providers: (Array.isArray(values) ? values : []).map(entry => ({
|
|
1519
|
+
provider: entry.provider,
|
|
1520
|
+
displayName: entry.displayName,
|
|
1521
|
+
settingsNs: entry.settingsNs,
|
|
1522
|
+
settingsPath: entry.settingsPath || [],
|
|
1523
|
+
active: true,
|
|
1524
|
+
...(entry.declared === undefined ? {} : { declared: entry.declared }),
|
|
1525
|
+
})) })
|
|
1526
|
+
} else if (method === 'llm.discoverModels') {
|
|
1527
|
+
endpoint = 'llm/discoverModels'
|
|
1528
|
+
args = {
|
|
1529
|
+
settingsNs: payload.settingsNs,
|
|
1530
|
+
request: Object.fromEntries(Object.entries(payload).filter(([key, value]) => key !== 'settingsNs' && value !== undefined)),
|
|
1531
|
+
}
|
|
1532
|
+
transform = models => ({ models: Array.isArray(models) ? models : [] })
|
|
1533
|
+
} else {
|
|
1534
|
+
return null
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const response = await callUpstreamRemote(endpoint, args, rpcId)
|
|
1538
|
+
if (!response.body?.result?.ok) return response.body || legacyEnvelope(rpcId, modernError(`DSH ${endpoint} returned no JSON response`))
|
|
1539
|
+
return legacyEnvelope(rpcId, { ok: true, value: transform(response.body.result.value) })
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1317
1542
|
/** 递归截断超大字段,避免单条超大事件撑爆环形缓冲。 */
|
|
1318
1543
|
function truncateEventValue(v, depth = 0) {
|
|
1319
1544
|
if (typeof v === 'string') return v.length > EVENT_MAX_STRING ? v.slice(0, EVENT_MAX_STRING) + '…[truncated]' : v
|
|
@@ -1530,7 +1755,8 @@ function startEventCollector(kind) {
|
|
|
1530
1755
|
if (stopped) return
|
|
1531
1756
|
let current
|
|
1532
1757
|
try {
|
|
1533
|
-
|
|
1758
|
+
const headers = dshUpstreamHeaders()
|
|
1759
|
+
current = Object.keys(headers).length ? new WebSocket(url, { headers }) : new WebSocket(url)
|
|
1534
1760
|
ws = current
|
|
1535
1761
|
} catch (err) {
|
|
1536
1762
|
state.lastError = String(err?.message || err)
|
|
@@ -1616,6 +1842,280 @@ function startEventCollector(kind) {
|
|
|
1616
1842
|
}
|
|
1617
1843
|
}
|
|
1618
1844
|
|
|
1845
|
+
function legacyPush(kind, payload, rpcId = crypto.randomUUID()) {
|
|
1846
|
+
pushEvent(kind, { rpcId, payload })
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function openModernSessionStream(ws, sessionId) {
|
|
1850
|
+
if (!sessionId || ws.readyState !== 1) return
|
|
1851
|
+
const streamId = 'session:' + sessionId
|
|
1852
|
+
ws.send(JSON.stringify({
|
|
1853
|
+
type: 'open', streamId, endpoint: 'session/follow',
|
|
1854
|
+
payload: { args: { request: { address: { kind: 'session', sessionId } } } },
|
|
1855
|
+
}))
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function applyModernControlFrame(value) {
|
|
1859
|
+
if (value?.type === 'baseline') {
|
|
1860
|
+
for (const [sessionId, items] of Object.entries(value.value?.queues || {})) {
|
|
1861
|
+
legacyPush('mux', { type: 'session/queue', sessionId, items })
|
|
1862
|
+
}
|
|
1863
|
+
for (const [sessionId, jobs] of Object.entries(value.value?.jobs || {})) {
|
|
1864
|
+
legacyPush('mux', { type: 'session/jobs', sessionId, jobs })
|
|
1865
|
+
}
|
|
1866
|
+
for (const [sessionId, block] of Object.entries(value.value?.projections || {})) {
|
|
1867
|
+
for (const [key, projection] of Object.entries(block?.values || {})) {
|
|
1868
|
+
legacyPush('mux', { type: 'session/projection', sessionId, key, value: projection, seq: block?.asOfSeq ?? 0 })
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return
|
|
1872
|
+
}
|
|
1873
|
+
if (value?.type === 'queue') legacyPush('mux', { type: 'session/queue', sessionId: value.sessionId, items: value.items || [] })
|
|
1874
|
+
else if (value?.type === 'jobs') legacyPush('mux', { type: 'session/jobs', sessionId: value.sessionId, jobs: value.jobs || [] })
|
|
1875
|
+
else if (value?.type === 'projection') legacyPush('mux', {
|
|
1876
|
+
type: 'session/projection', sessionId: value.sessionId, key: value.key, value: value.value, seq: value.seq,
|
|
1877
|
+
})
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function applyModernWorkspaceFrame(value) {
|
|
1881
|
+
if (value?.type === 'baseline') {
|
|
1882
|
+
modernState.workspaces = {
|
|
1883
|
+
items: Array.isArray(value.value?.items) ? value.value.items : [],
|
|
1884
|
+
archivedSessionIds: Array.isArray(value.value?.archivedSessionIds) ? value.value.archivedSessionIds : [],
|
|
1885
|
+
}
|
|
1886
|
+
return
|
|
1887
|
+
}
|
|
1888
|
+
if (value?.type === 'upsert' && value.workspace) {
|
|
1889
|
+
const items = modernState.workspaces.items.filter(item => item.workspaceId !== value.workspace.workspaceId)
|
|
1890
|
+
items.push(value.workspace)
|
|
1891
|
+
modernState.workspaces = { ...modernState.workspaces, items }
|
|
1892
|
+
legacyPush('host', { type: 'host/workspace-changed', workspace: value.workspace })
|
|
1893
|
+
} else if (value?.type === 'remove') {
|
|
1894
|
+
modernState.workspaces = {
|
|
1895
|
+
...modernState.workspaces,
|
|
1896
|
+
items: modernState.workspaces.items.filter(item => item.workspaceId !== value.workspaceId),
|
|
1897
|
+
}
|
|
1898
|
+
legacyPush('host', { type: 'host/workspace-removed', workspaceId: value.workspaceId })
|
|
1899
|
+
} else if (value?.type === 'order') {
|
|
1900
|
+
const byId = new Map(modernState.workspaces.items.map(item => [item.workspaceId, item]))
|
|
1901
|
+
const ordered = (value.workspaceIds || []).map(id => byId.get(id)).filter(Boolean)
|
|
1902
|
+
for (const item of modernState.workspaces.items) if (!value.workspaceIds?.includes(item.workspaceId)) ordered.push(item)
|
|
1903
|
+
modernState.workspaces = { ...modernState.workspaces, items: ordered }
|
|
1904
|
+
legacyPush('host', { type: 'host/workspace-order-changed', workspaceIds: value.workspaceIds || [] })
|
|
1905
|
+
} else if (value?.type === 'archived') {
|
|
1906
|
+
modernState.workspaces = { ...modernState.workspaces, archivedSessionIds: value.archivedSessionIds || [] }
|
|
1907
|
+
legacyPush('host', { type: 'host/archived-sessions-changed', archivedSessionIds: value.archivedSessionIds || [] })
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function applyModernSessionFrame(sessionId, value) {
|
|
1912
|
+
if (value?.type === 'snapshot') {
|
|
1913
|
+
modernState.sessionCursors.set(sessionId, value.cursor)
|
|
1914
|
+
legacyPush('mux', { type: 'session/subscribed', sessionId, lastSeq: value.cursor })
|
|
1915
|
+
for (const record of value.records || []) {
|
|
1916
|
+
if (record?.event) legacyPush('mux', { type: 'session/event', sessionId, event: record.event })
|
|
1917
|
+
}
|
|
1918
|
+
for (const [key, projection] of Object.entries(value.projections?.values || {})) {
|
|
1919
|
+
legacyPush('mux', { type: 'session/projection', sessionId, key, value: projection, seq: value.projections?.asOfSeq ?? value.cursor })
|
|
1920
|
+
}
|
|
1921
|
+
return
|
|
1922
|
+
}
|
|
1923
|
+
if (value?.type === 'event' && value.event) {
|
|
1924
|
+
modernState.sessionCursors.set(sessionId, value.event.seq)
|
|
1925
|
+
legacyPush('mux', { type: 'session/event', sessionId, event: value.event })
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function resolveModernPendingEvent(eventId, cancelled = false) {
|
|
1930
|
+
const pending = modernState.pendingEvents.get(eventId)
|
|
1931
|
+
if (!pending) return
|
|
1932
|
+
modernState.pendingEvents.delete(eventId)
|
|
1933
|
+
if (pending.event === 'approval/request') {
|
|
1934
|
+
legacyPush('mux', {
|
|
1935
|
+
type: 'approval/resolved', sessionId: pending.sessionId, approvalId: eventId,
|
|
1936
|
+
outcome: cancelled ? 'cancelled' : pending.outcome,
|
|
1937
|
+
})
|
|
1938
|
+
} else if (pending.event === 'user-questions/request') {
|
|
1939
|
+
legacyPush('mux', {
|
|
1940
|
+
type: 'question/resolved', sessionId: pending.sessionId, questionRpcId: eventId,
|
|
1941
|
+
outcome: cancelled ? 'cancelled' : 'answered',
|
|
1942
|
+
})
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
function applyModernRemoteEvent(ws, value) {
|
|
1947
|
+
if (value?.type === 'ready') {
|
|
1948
|
+
modernState.eventClientId = value.clientId || ''
|
|
1949
|
+
modernState.home = value.host?.home || modernState.home
|
|
1950
|
+
return
|
|
1951
|
+
}
|
|
1952
|
+
if (value?.type === 'cancel') {
|
|
1953
|
+
resolveModernPendingEvent(value.eventId, true)
|
|
1954
|
+
return
|
|
1955
|
+
}
|
|
1956
|
+
if (value?.type === 'waterfall') {
|
|
1957
|
+
const sessionId = value.agentId
|
|
1958
|
+
modernState.pendingEvents.set(value.eventId, { event: value.event, sessionId, outcome: '' })
|
|
1959
|
+
if (value.event === 'approval/request') {
|
|
1960
|
+
legacyPush('mux', {
|
|
1961
|
+
type: 'approval/requested', sessionId, approvalId: value.eventId,
|
|
1962
|
+
toolName: value.request?.toolName || '',
|
|
1963
|
+
...(value.request?.callId === undefined ? {} : { callId: value.request.callId }),
|
|
1964
|
+
...(value.request?.reason === undefined ? {} : { reason: value.request.reason }),
|
|
1965
|
+
}, value.eventId)
|
|
1966
|
+
} else if (value.event === 'user-questions/request') {
|
|
1967
|
+
legacyPush('mux', { type: 'question/requested', sessionId, questions: value.request?.questions || [] }, value.eventId)
|
|
1968
|
+
}
|
|
1969
|
+
return
|
|
1970
|
+
}
|
|
1971
|
+
if (value?.type !== 'emit') return
|
|
1972
|
+
const args = value.args || []
|
|
1973
|
+
if (value.event === 'api-session/added') {
|
|
1974
|
+
const summary = args[0]
|
|
1975
|
+
if (summary?.sessionId) {
|
|
1976
|
+
modernState.sessions.set(summary.sessionId, summary)
|
|
1977
|
+
openModernSessionStream(ws, summary.sessionId)
|
|
1978
|
+
legacyPush('host', {
|
|
1979
|
+
type: 'host/session-added', sessionId: summary.sessionId, blank: !!summary.blank,
|
|
1980
|
+
...(summary.parentSessionId === undefined ? {} : { parentSessionId: summary.parentSessionId }),
|
|
1981
|
+
...(summary.origin === undefined ? {} : { origin: summary.origin }),
|
|
1982
|
+
...(summary.cwd === undefined ? {} : { cwd: summary.cwd }),
|
|
1983
|
+
})
|
|
1984
|
+
}
|
|
1985
|
+
} else if (value.event === 'api-session/removed') {
|
|
1986
|
+
modernState.sessions.delete(args[0])
|
|
1987
|
+
modernState.sessionCursors.delete(args[0])
|
|
1988
|
+
try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'session:' + args[0] })) } catch {}
|
|
1989
|
+
legacyPush('host', { type: 'host/session-removed', sessionId: args[0] })
|
|
1990
|
+
} else if (value.event === 'api-session/status') {
|
|
1991
|
+
legacyPush('host', { type: 'host/session-status', sessionId: args[0], running: !!args[1] })
|
|
1992
|
+
} else if (value.event === 'api-session/error') {
|
|
1993
|
+
legacyPush('host', { type: 'host/agent-error', sessionId: args[0], message: String(args[1] || '') })
|
|
1994
|
+
} else {
|
|
1995
|
+
legacyPush('host', { type: 'host/remote-event', event: value.event, args })
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function startModernEventCollector() {
|
|
2000
|
+
if (typeof WebSocket !== 'function') return null
|
|
2001
|
+
let ws = null
|
|
2002
|
+
let stopped = false
|
|
2003
|
+
let retryTimer = null
|
|
2004
|
+
let connectTimer = null
|
|
2005
|
+
let attempt = 0
|
|
2006
|
+
const scheme = UPSTREAM.protocol === 'https:' ? 'wss' : 'ws'
|
|
2007
|
+
const url = `${scheme}://${UPSTREAM_AUTHORITY}/api/remote.mux`
|
|
2008
|
+
const setConnected = (connected, error = '') => {
|
|
2009
|
+
for (const state of Object.values(eventCollectorState)) {
|
|
2010
|
+
state.connected = connected
|
|
2011
|
+
if (connected) {
|
|
2012
|
+
state.lastConnectAt = Date.now()
|
|
2013
|
+
state.lastError = ''
|
|
2014
|
+
state.attempt = 0
|
|
2015
|
+
} else if (error) state.lastError = error
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const schedule = () => {
|
|
2019
|
+
if (stopped || retryTimer) return
|
|
2020
|
+
const delay = Math.round(Math.min(1500 * Math.pow(2, attempt++), 60000) * (0.8 + Math.random() * 0.4))
|
|
2021
|
+
retryTimer = setTimeout(() => { retryTimer = null; connect() }, delay)
|
|
2022
|
+
retryTimer.unref?.()
|
|
2023
|
+
}
|
|
2024
|
+
const connect = () => {
|
|
2025
|
+
if (stopped) return
|
|
2026
|
+
let current
|
|
2027
|
+
try {
|
|
2028
|
+
const headers = dshUpstreamHeaders()
|
|
2029
|
+
current = Object.keys(headers).length ? new WebSocket(url, { headers }) : new WebSocket(url)
|
|
2030
|
+
ws = current
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
setConnected(false, String(error?.message || error))
|
|
2033
|
+
schedule()
|
|
2034
|
+
return
|
|
2035
|
+
}
|
|
2036
|
+
let finished = false
|
|
2037
|
+
const finish = (code = 0, reason = '', error = '') => {
|
|
2038
|
+
if (finished) return
|
|
2039
|
+
finished = true
|
|
2040
|
+
clearTimeout(connectTimer)
|
|
2041
|
+
modernState.eventClientId = ''
|
|
2042
|
+
setConnected(false, error)
|
|
2043
|
+
for (const state of Object.values(eventCollectorState)) {
|
|
2044
|
+
state.lastCloseCode = Number(code) || 0
|
|
2045
|
+
state.lastCloseReason = String(reason || '')
|
|
2046
|
+
state.reconnects++
|
|
2047
|
+
}
|
|
2048
|
+
if (ws === current) ws = null
|
|
2049
|
+
if (!stopped) schedule()
|
|
2050
|
+
}
|
|
2051
|
+
connectTimer = setTimeout(() => {
|
|
2052
|
+
finish(0, '', 'websocket connect timeout')
|
|
2053
|
+
try { current.close() } catch {}
|
|
2054
|
+
}, WS_UPGRADE_TIMEOUT_MS)
|
|
2055
|
+
connectTimer.unref?.()
|
|
2056
|
+
current.onopen = () => {
|
|
2057
|
+
if (finished || stopped || ws !== current) return
|
|
2058
|
+
clearTimeout(connectTimer)
|
|
2059
|
+
attempt = 0
|
|
2060
|
+
setConnected(true)
|
|
2061
|
+
current.send(JSON.stringify({ type: 'open', streamId: 'events', endpoint: '$events', payload: { args: {} } }))
|
|
2062
|
+
current.send(JSON.stringify({ type: 'open', streamId: 'control', endpoint: 'session/control', payload: { args: {} } }))
|
|
2063
|
+
current.send(JSON.stringify({ type: 'open', streamId: 'workspaces', endpoint: 'workspace/follow', payload: { args: {} } }))
|
|
2064
|
+
for (const sessionId of modernState.sessions.keys()) openModernSessionStream(current, sessionId)
|
|
2065
|
+
}
|
|
2066
|
+
current.onmessage = ev => {
|
|
2067
|
+
try {
|
|
2068
|
+
const data = typeof ev.data === 'string' ? ev.data : Buffer.isBuffer(ev.data) ? ev.data.toString() : String(ev.data)
|
|
2069
|
+
const frame = JSON.parse(data)
|
|
2070
|
+
if (frame.type === 'error') {
|
|
2071
|
+
const message = frame.error?.message || 'modern stream error'
|
|
2072
|
+
for (const state of Object.values(eventCollectorState)) state.lastError = message
|
|
2073
|
+
legacyPush('mux', { type: 'stream/error', error: frame.error || { message } })
|
|
2074
|
+
return
|
|
2075
|
+
}
|
|
2076
|
+
if (frame.type !== 'item') return
|
|
2077
|
+
if (frame.streamId === 'events') applyModernRemoteEvent(current, frame.value)
|
|
2078
|
+
else if (frame.streamId === 'control') applyModernControlFrame(frame.value)
|
|
2079
|
+
else if (frame.streamId === 'workspaces') applyModernWorkspaceFrame(frame.value)
|
|
2080
|
+
else if (frame.streamId.startsWith('session:')) applyModernSessionFrame(frame.streamId.slice(8), frame.value)
|
|
2081
|
+
} catch {}
|
|
2082
|
+
}
|
|
2083
|
+
current.onclose = ev => finish(ev?.code, ev?.reason)
|
|
2084
|
+
current.onerror = err => {
|
|
2085
|
+
finish(0, '', String(err?.error?.message || err?.message || 'websocket error'))
|
|
2086
|
+
try { current.close() } catch {}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
connect()
|
|
2090
|
+
return {
|
|
2091
|
+
kind: 'modern',
|
|
2092
|
+
reconnectNow() {
|
|
2093
|
+
if (stopped || ws?.readyState === 0 || ws?.readyState === 1) return
|
|
2094
|
+
clearTimeout(retryTimer)
|
|
2095
|
+
retryTimer = null
|
|
2096
|
+
attempt = 0
|
|
2097
|
+
connect()
|
|
2098
|
+
},
|
|
2099
|
+
close() {
|
|
2100
|
+
stopped = true
|
|
2101
|
+
clearTimeout(retryTimer)
|
|
2102
|
+
clearTimeout(connectTimer)
|
|
2103
|
+
try { ws?.close() } catch {}
|
|
2104
|
+
},
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
async function startCompatibleEventCollectors() {
|
|
2109
|
+
if (await detectUpstreamApiFlavor() === 'modern') {
|
|
2110
|
+
const collector = startModernEventCollector()
|
|
2111
|
+
eventCollectors.mux = collector
|
|
2112
|
+
eventCollectors.host = collector
|
|
2113
|
+
return
|
|
2114
|
+
}
|
|
2115
|
+
eventCollectors.mux = startEventCollector('mux')
|
|
2116
|
+
eventCollectors.host = startEventCollector('host')
|
|
2117
|
+
}
|
|
2118
|
+
|
|
1619
2119
|
// ---------- 统计 API ----------
|
|
1620
2120
|
let statsScanning = false
|
|
1621
2121
|
async function scanStatsOnce(delay) {
|
|
@@ -2048,6 +2548,7 @@ function upstreamReachable(cb) {
|
|
|
2048
2548
|
port: UPSTREAM_PORT,
|
|
2049
2549
|
method: 'GET',
|
|
2050
2550
|
path: '/health',
|
|
2551
|
+
headers: dshUpstreamHeaders(),
|
|
2051
2552
|
timeout: 1500
|
|
2052
2553
|
}, (res) => {
|
|
2053
2554
|
res.resume()
|
|
@@ -2330,16 +2831,21 @@ async function loadFsWorkspaceRoots(force = false) {
|
|
|
2330
2831
|
if (fsWorkspaceRootsFetch) return fsWorkspaceRootsFetch
|
|
2331
2832
|
fsWorkspaceRootsFetch = (async () => {
|
|
2332
2833
|
try {
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2834
|
+
let value
|
|
2835
|
+
if (await detectUpstreamApiFlavor() === 'modern') {
|
|
2836
|
+
value = modernState.workspaces
|
|
2837
|
+
} else {
|
|
2838
|
+
const target = new URL('/api/workspace.list', UPSTREAM)
|
|
2839
|
+
const res = await fetch(target, {
|
|
2840
|
+
method: 'POST',
|
|
2841
|
+
headers: dshUpstreamHeaders({ 'content-type': 'application/json' }),
|
|
2842
|
+
body: JSON.stringify({ type: 'client-request', rpcId: crypto.randomUUID(), method: 'workspace.list', payload: {} }),
|
|
2843
|
+
signal: AbortSignal.timeout(Math.min(8000, UPSTREAM_REQUEST_TIMEOUT_MS)),
|
|
2844
|
+
})
|
|
2845
|
+
if (!res.ok) throw new Error(`workspace.list HTTP ${res.status}`)
|
|
2846
|
+
const body = await res.json()
|
|
2847
|
+
value = body?.result?.ok ? body.result.value : null
|
|
2848
|
+
}
|
|
2343
2849
|
const items = Array.isArray(value?.items) ? value.items : []
|
|
2344
2850
|
const roots = [...new Set(items.map(fsWorkspacePath).filter(Boolean))]
|
|
2345
2851
|
const reals = roots.map(root => { try { return fs.realpathSync(root) } catch { return null } }).filter(Boolean)
|
|
@@ -3263,38 +3769,84 @@ function serveWorkbench(req, res, url) {
|
|
|
3263
3769
|
}
|
|
3264
3770
|
|
|
3265
3771
|
// ---------- /api 代理 ----------
|
|
3266
|
-
function
|
|
3267
|
-
if (req.method
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3772
|
+
async function proxyModernApi(req, res, url) {
|
|
3773
|
+
if (req.method !== 'POST' || url.pathname.startsWith('/remote/')) return false
|
|
3774
|
+
if (await detectUpstreamApiFlavor() !== 'modern') return false
|
|
3775
|
+
let raw = ''
|
|
3776
|
+
try {
|
|
3777
|
+
raw = await new Promise((resolve, reject) => {
|
|
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
|
+
})
|
|
3787
|
+
const body = JSON.parse(raw || '{}')
|
|
3277
3788
|
cors(res)
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3789
|
+
if (url.pathname === '/api/respond') {
|
|
3790
|
+
const eventId = body.rpcId
|
|
3791
|
+
const pending = modernState.pendingEvents.get(eventId)
|
|
3792
|
+
if (!pending || !modernState.eventClientId) {
|
|
3793
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
3794
|
+
res.end(JSON.stringify({ accepted: false }))
|
|
3795
|
+
return true
|
|
3796
|
+
}
|
|
3797
|
+
const value = body.result?.value
|
|
3798
|
+
const answer = pending.event === 'approval/request' ? value?.outcome : value?.answer
|
|
3799
|
+
pending.outcome = answer
|
|
3800
|
+
const response = await callUpstreamRemote('$events/result', {
|
|
3801
|
+
clientId: modernState.eventClientId,
|
|
3802
|
+
eventId,
|
|
3803
|
+
outcome: { kind: 'result', value: answer },
|
|
3804
|
+
})
|
|
3805
|
+
const accepted = response.status === 200 && response.body?.result?.ok === true
|
|
3806
|
+
if (accepted) resolveModernPendingEvent(eventId, false)
|
|
3807
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
3808
|
+
res.end(JSON.stringify({ accepted }))
|
|
3809
|
+
return true
|
|
3810
|
+
}
|
|
3811
|
+
if (body.type !== 'client-request' || typeof body.rpcId !== 'string' || typeof body.method !== 'string') {
|
|
3812
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
|
|
3813
|
+
res.end(JSON.stringify({ error: 'bad-request' }))
|
|
3814
|
+
return true
|
|
3815
|
+
}
|
|
3816
|
+
const translated = await translateModernRpc(body.method, body.payload || {}, body.rpcId)
|
|
3817
|
+
if (translated === null) {
|
|
3818
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' })
|
|
3819
|
+
res.end(JSON.stringify(legacyEnvelope(body.rpcId, modernError(`Remote method ${body.method} is unavailable on this DSH version`, 'method-unavailable'))))
|
|
3820
|
+
return true
|
|
3821
|
+
}
|
|
3822
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
3823
|
+
res.end(JSON.stringify(translated))
|
|
3824
|
+
return true
|
|
3825
|
+
} catch (error) {
|
|
3826
|
+
if (!res.headersSent) {
|
|
3827
|
+
cors(res)
|
|
3828
|
+
res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' })
|
|
3829
|
+
}
|
|
3830
|
+
if (!res.writableEnded) res.end(JSON.stringify({ error: 'upstream-incompatible', detail: String(error?.message || error) }))
|
|
3831
|
+
return true
|
|
3281
3832
|
}
|
|
3833
|
+
}
|
|
3282
3834
|
|
|
3835
|
+
function proxyLegacyApi(req, res, url) {
|
|
3283
3836
|
const headers = {}
|
|
3284
3837
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
3285
3838
|
if (v === undefined) continue
|
|
3286
3839
|
const key = k.toLowerCase()
|
|
3287
|
-
if (['host', 'authorization', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
|
|
3840
|
+
if (['host', 'authorization', 'cookie', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
|
|
3288
3841
|
'proxy-connection', 'accept-encoding', 'origin', 'referer',
|
|
3289
3842
|
'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest', 'sec-fetch-user',
|
|
3290
3843
|
'x-dsh-remote-client'].includes(key)) continue
|
|
3291
3844
|
headers[k] = v
|
|
3292
3845
|
}
|
|
3293
3846
|
headers.host = UPSTREAM.host
|
|
3847
|
+
Object.assign(headers, dshUpstreamHeaders())
|
|
3294
3848
|
// /remote/* 由 DSH 插件端点处理;插件侧用网关自身 token 鉴权。
|
|
3295
|
-
if (url.pathname.startsWith('/remote/'))
|
|
3296
|
-
headers.authorization = 'Bearer ' + TOKEN
|
|
3297
|
-
}
|
|
3849
|
+
if (url.pathname.startsWith('/remote/')) headers.authorization = 'Bearer ' + TOKEN
|
|
3298
3850
|
|
|
3299
3851
|
let responseDone = false
|
|
3300
3852
|
const upstreamReq = UPSTREAM_TRANSPORT.request({
|
|
@@ -3336,6 +3888,31 @@ function proxyApi(req, res, url) {
|
|
|
3336
3888
|
req.pipe(upstreamReq)
|
|
3337
3889
|
}
|
|
3338
3890
|
|
|
3891
|
+
function proxyApi(req, res, url) {
|
|
3892
|
+
if (req.method === 'OPTIONS') {
|
|
3893
|
+
cors(res)
|
|
3894
|
+
res.writeHead(204)
|
|
3895
|
+
res.end()
|
|
3896
|
+
return
|
|
3897
|
+
}
|
|
3898
|
+
const ok = authorized(req, url)
|
|
3899
|
+
touchDevice(req, ok ? {} : { failedAuth: true })
|
|
3900
|
+
if (!ok) {
|
|
3901
|
+
authFailures++
|
|
3902
|
+
cors(res)
|
|
3903
|
+
res.writeHead(401, { 'content-type': 'application/json; charset=utf-8' })
|
|
3904
|
+
res.end(JSON.stringify({ error: 'unauthorized' }))
|
|
3905
|
+
return
|
|
3906
|
+
}
|
|
3907
|
+
void proxyModernApi(req, res, url).then(handled => {
|
|
3908
|
+
if (!handled) proxyLegacyApi(req, res, url)
|
|
3909
|
+
}).catch(error => {
|
|
3910
|
+
cors(res)
|
|
3911
|
+
if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' })
|
|
3912
|
+
if (!res.writableEnded) res.end(JSON.stringify({ error: 'upstream-unreachable', detail: String(error?.message || error) }))
|
|
3913
|
+
})
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3339
3916
|
// ---------- 其它 ----------
|
|
3340
3917
|
async function serveHealth(req, res, url) {
|
|
3341
3918
|
const eventHealth = Object.fromEntries(Object.entries(eventCollectorState).map(([kind, state]) => [kind, {
|
|
@@ -3364,7 +3941,7 @@ async function serveHealth(req, res, url) {
|
|
|
3364
3941
|
const ctrl = new AbortController()
|
|
3365
3942
|
timer = setTimeout(() => ctrl.abort(), 5000)
|
|
3366
3943
|
const probeUrl = new URL(DSH_HEALTH_PATH, UPSTREAM).toString()
|
|
3367
|
-
const probe = await fetch(probeUrl, { signal: ctrl.signal, cache: 'no-store' })
|
|
3944
|
+
const probe = await fetch(probeUrl, { headers: dshUpstreamHeaders(), signal: ctrl.signal, cache: 'no-store' })
|
|
3368
3945
|
upstreamReachable = true
|
|
3369
3946
|
upstreamStatus = probe.status
|
|
3370
3947
|
upstreamOk = probe.ok
|
|
@@ -3637,7 +4214,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
3637
4214
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
3638
4215
|
if (v === undefined) continue
|
|
3639
4216
|
const key = k.toLowerCase()
|
|
3640
|
-
if (['host', 'authorization', 'connection', 'upgrade', 'sec-websocket-key',
|
|
4217
|
+
if (['host', 'authorization', 'cookie', 'connection', 'upgrade', 'sec-websocket-key',
|
|
3641
4218
|
'sec-websocket-version', 'sec-websocket-extensions', 'sec-websocket-protocol',
|
|
3642
4219
|
'proxy-connection', 'accept-encoding', 'origin', 'referer',
|
|
3643
4220
|
'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest', 'sec-fetch-user',
|
|
@@ -3645,6 +4222,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
3645
4222
|
headers[k] = v
|
|
3646
4223
|
}
|
|
3647
4224
|
headers.host = UPSTREAM.host
|
|
4225
|
+
Object.assign(headers, dshUpstreamHeaders())
|
|
3648
4226
|
headers.connection = 'Upgrade'
|
|
3649
4227
|
headers.upgrade = 'websocket'
|
|
3650
4228
|
if (req.headers['sec-websocket-key']) headers['sec-websocket-key'] = req.headers['sec-websocket-key']
|
|
@@ -3747,8 +4325,7 @@ server.listen(PORT, HOST, () => {
|
|
|
3747
4325
|
}
|
|
3748
4326
|
console.log(' 上游: ' + UPSTREAM.origin + ' (Ctrl+C 退出)')
|
|
3749
4327
|
// 事件轮询缓冲:网关自身上游 WS 采集,断线自动重连
|
|
3750
|
-
|
|
3751
|
-
eventCollectors.host = startEventCollector('host')
|
|
4328
|
+
void startCompatibleEventCollectors()
|
|
3752
4329
|
// 启动 8 秒后首查, 之后每 6 小时查一次 GitHub/镜像最新版
|
|
3753
4330
|
setTimeout(() => checkForUpdates(false), 8000)
|
|
3754
4331
|
setInterval(() => checkForUpdates(false), UPDATE_INTERVAL_MS)
|