dsh-remote-plugin 0.7.0 → 0.7.2
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 +12 -2
- package/gateway.cjs +81 -9
- package/index.mjs +10 -0
- package/insights.mjs +73 -0
- package/package.json +3 -2
- package/plugin-center.mjs +118 -21
- package/public/admin.html +3 -0
- package/public/admin.js +1 -0
- package/public/app.js +100 -174
- package/public/desktop/desktop.html +5 -0
- package/public/desktop/desktop.js +76 -8
- package/public/index.html +14 -64
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/plugin-center.css +89 -1
- package/public/plugin-center.js +148 -39
- package/public/plugin.js +7 -1
- package/public/styles.css +0 -93
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/client.js
CHANGED
|
@@ -20,7 +20,11 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
var DRAWER_STYLE = {
|
|
23
|
-
|
|
23
|
+
// Desktop's hidden title bar overlays the page (40px on Windows).
|
|
24
|
+
// Electron exposes its current height through the CSS environment value;
|
|
25
|
+
// keep a safe fallback for Desktop hosts without that value.
|
|
26
|
+
position: 'fixed', top: window.location && window.location.protocol === 'dsh-app:' ? 'calc(12px + env(titlebar-area-height, 40px))' : 12,
|
|
27
|
+
right: 12, bottom: 12, zIndex: 2147483000,
|
|
24
28
|
width: 'min(520px, calc(100vw - 24px))', background: '#0b0e1a',
|
|
25
29
|
border: '1px solid rgba(125, 207, 255, .22)', borderRadius: 24, overflow: 'hidden',
|
|
26
30
|
boxShadow: '-16px 18px 48px rgba(0,0,0,.46), 0 0 0 1px rgba(91,140,255,.08)',
|
|
@@ -141,7 +145,13 @@ window.__ModuleLoader__.load({
|
|
|
141
145
|
}, [])
|
|
142
146
|
return React.createElement('div', {
|
|
143
147
|
'aria-hidden': !open,
|
|
144
|
-
style: Object.assign({}, DRAWER_STYLE, {
|
|
148
|
+
style: Object.assign({}, DRAWER_STYLE, {
|
|
149
|
+
transform: open ? 'translateX(0)' : 'translateX(102%)',
|
|
150
|
+
boxShadow: open ? DRAWER_STYLE.boxShadow : 'none',
|
|
151
|
+
pointerEvents: open ? 'auto' : 'none',
|
|
152
|
+
visibility: open ? 'visible' : 'hidden',
|
|
153
|
+
transition: open ? 'transform .22s ease' : 'transform .22s ease, visibility 0s linear .22s',
|
|
154
|
+
}),
|
|
145
155
|
},
|
|
146
156
|
loaded.current ? React.createElement('iframe', {
|
|
147
157
|
src: '/remote/plugin.html',
|
package/gateway.cjs
CHANGED
|
@@ -1703,6 +1703,12 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1703
1703
|
if (!['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore', 'archiveSession'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
1704
1704
|
endpoint = 'workspace/' + verb
|
|
1705
1705
|
args = { request: payload }
|
|
1706
|
+
if (verb === 'archiveSession') transform = value => {
|
|
1707
|
+
// The HTTP receipt precedes workspace/follow on another connection.
|
|
1708
|
+
// Publish the committed archive set before a client's immediate refresh.
|
|
1709
|
+
if (Array.isArray(value?.archivedSessionIds)) applyModernWorkspaceFrame({ type: 'archived', ...value })
|
|
1710
|
+
return value
|
|
1711
|
+
}
|
|
1706
1712
|
} else if (method.startsWith('goal.')) {
|
|
1707
1713
|
const verb = method.slice('goal.'.length)
|
|
1708
1714
|
if (!['create', 'edit', 'pause', 'resume', 'complete', 'clear'].includes(verb)) return legacyFallback('no generated RPC adapter')
|
|
@@ -1767,6 +1773,31 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1767
1773
|
return legacyEnvelope(rpcId, modernError(error.message, 'image-vision-failed'))
|
|
1768
1774
|
}
|
|
1769
1775
|
}
|
|
1776
|
+
if (method === 'subagent.list' && modernResponseNeedsLegacyFallback(response)) {
|
|
1777
|
+
// DSH 0.2 removed subagents/list; discovery is now a non-activating
|
|
1778
|
+
// read of the parent's durable catalog and each child's projections.
|
|
1779
|
+
const parent = await callUpstreamRemote('session/projections', { request: { sessionId: payload.parentSessionId } }, rpcId)
|
|
1780
|
+
if (modernResponseNeedsLegacyFallback(parent)) return legacyFallback('generated subagent catalog unavailable')
|
|
1781
|
+
if (!parent.body?.result?.ok) return parent.body || legacyEnvelope(rpcId, modernError('DSH subagent catalog read failed'))
|
|
1782
|
+
const catalog = parent.body.result.value?.values?.subagentCatalog
|
|
1783
|
+
if (!Array.isArray(catalog)) return legacyEnvelope(rpcId, modernError('DSH did not expose a subagent catalog projection', 'subagent/projections-unavailable'))
|
|
1784
|
+
await refreshModernSessions()
|
|
1785
|
+
const entries = await Promise.all(catalog.map(async child => {
|
|
1786
|
+
if (!['one-shot', 'continuable'].includes(child.mode)) return { kind: 'diagnostic', id: child.id, reason: 'unsupported' }
|
|
1787
|
+
try {
|
|
1788
|
+
const read = await callUpstreamRemote('session/projections', { request: { sessionId: child.id } })
|
|
1789
|
+
if (!read.body?.result?.ok || !read.body.result.value) return { kind: 'diagnostic', id: child.id, reason: 'unavailable' }
|
|
1790
|
+
const values = read.body.result.value.values
|
|
1791
|
+
const identity = values?.subagent
|
|
1792
|
+
if (!identity || identity.mode !== child.mode) return { kind: 'diagnostic', id: child.id, reason: 'corrupt' }
|
|
1793
|
+
return { kind: 'child', id: child.id, mode: identity.mode,
|
|
1794
|
+
...(identity.label === undefined ? {} : { label: identity.label }),
|
|
1795
|
+
activity: modernState.sessions.get(child.id)?.running ? 'running' : 'inactive',
|
|
1796
|
+
hasChildren: Array.isArray(values.subagentCatalog) && values.subagentCatalog.length > 0 }
|
|
1797
|
+
} catch { return { kind: 'diagnostic', id: child.id, reason: 'unavailable' } }
|
|
1798
|
+
}))
|
|
1799
|
+
return legacyEnvelope(rpcId, { ok: true, value: { entries, parentAvailable: !!modernState.sessions.get(payload.parentSessionId)?.agentAvailable } })
|
|
1800
|
+
}
|
|
1770
1801
|
if (modernResponseNeedsLegacyFallback(response)) return legacyFallback(`generated RPC ${endpoint} unavailable`)
|
|
1771
1802
|
if (!response.body?.result?.ok) {
|
|
1772
1803
|
recordCompatibility('generated-rpc-error', { method, status: response.status, detail: remoteFailureKind(response) })
|
|
@@ -1818,6 +1849,8 @@ function rememberCollectorReplay(kind, full, raw) {
|
|
|
1818
1849
|
let key = ''
|
|
1819
1850
|
if (payload.type === 'session/subscribed' && payload.sessionId) key = `session:${payload.sessionId}`
|
|
1820
1851
|
else if (payload.type === 'session/reasoning' && payload.sessionId) key = `reasoning:${payload.sessionId}`
|
|
1852
|
+
else if (payload.type === 'session/queue' && payload.sessionId) key = `queue:${payload.sessionId}`
|
|
1853
|
+
else if (payload.type === 'session/jobs' && payload.sessionId) key = `jobs:${payload.sessionId}`
|
|
1821
1854
|
else if (payload.type === 'approval/requested' && payload.approvalId) key = `approval:${payload.approvalId}`
|
|
1822
1855
|
else if (payload.type === 'question/requested' && full.rpcId) key = `question:${full.rpcId}`
|
|
1823
1856
|
else if (payload.type === 'approval/resolved' && payload.approvalId) replay.delete(`approval:${payload.approvalId}`)
|
|
@@ -2095,6 +2128,28 @@ function openModernSessionStream(ws, sessionId) {
|
|
|
2095
2128
|
type: 'open', streamId, endpoint: 'session/follow',
|
|
2096
2129
|
payload: { args: { request: { address: { kind: 'session', sessionId }, assistantStream: true } } },
|
|
2097
2130
|
}))
|
|
2131
|
+
ws.send(JSON.stringify({
|
|
2132
|
+
type: 'open', streamId: 'jobs:' + sessionId, endpoint: 'job/list',
|
|
2133
|
+
payload: { args: { request: { sessionId } } },
|
|
2134
|
+
}))
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function applyModernProjection(sessionId, key, value, seq) {
|
|
2138
|
+
legacyPush('mux', { type: 'session/projection', sessionId, key, value, seq })
|
|
2139
|
+
if (key === 'inbox') {
|
|
2140
|
+
// DSH 0.2 stores queue/steer state in the durable Inbox projection.
|
|
2141
|
+
const items = ['next-turn', 'next-step'].flatMap(target =>
|
|
2142
|
+
(Array.isArray(value?.[target]) ? value[target] : []).filter(message => message?.id).map(message => ({
|
|
2143
|
+
id: message.id, placement: target === 'next-turn' ? 'queued' : 'context', message,
|
|
2144
|
+
})))
|
|
2145
|
+
legacyPush('mux', { type: 'session/queue', sessionId, items })
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function applyModernJobFrame(sessionId, value) {
|
|
2150
|
+
if (value?.type === 'rows' && Array.isArray(value.jobs)) {
|
|
2151
|
+
legacyPush('mux', { type: 'session/jobs', sessionId, jobs: value.jobs })
|
|
2152
|
+
}
|
|
2098
2153
|
}
|
|
2099
2154
|
|
|
2100
2155
|
function applyModernControlFrame(value) {
|
|
@@ -2107,16 +2162,14 @@ function applyModernControlFrame(value) {
|
|
|
2107
2162
|
}
|
|
2108
2163
|
for (const [sessionId, block] of Object.entries(value.value?.projections || {})) {
|
|
2109
2164
|
for (const [key, projection] of Object.entries(block?.values || {})) {
|
|
2110
|
-
|
|
2165
|
+
applyModernProjection(sessionId, key, projection, block?.asOfSeq ?? 0)
|
|
2111
2166
|
}
|
|
2112
2167
|
}
|
|
2113
2168
|
return
|
|
2114
2169
|
}
|
|
2115
2170
|
if (value?.type === 'queue') legacyPush('mux', { type: 'session/queue', sessionId: value.sessionId, items: value.items || [] })
|
|
2116
2171
|
else if (value?.type === 'jobs') legacyPush('mux', { type: 'session/jobs', sessionId: value.sessionId, jobs: value.jobs || [] })
|
|
2117
|
-
else if (value?.type === 'projection')
|
|
2118
|
-
type: 'session/projection', sessionId: value.sessionId, key: value.key, value: value.value, seq: value.seq,
|
|
2119
|
-
})
|
|
2172
|
+
else if (value?.type === 'projection') applyModernProjection(value.sessionId, value.key, value.value, value.seq)
|
|
2120
2173
|
}
|
|
2121
2174
|
|
|
2122
2175
|
function applyModernWorkspaceFrame(value) {
|
|
@@ -2209,7 +2262,7 @@ function applyModernSessionFrame(sessionId, value) {
|
|
|
2209
2262
|
if (record?.event) legacyPush('mux', { type: 'session/event', sessionId, event: record.event })
|
|
2210
2263
|
}
|
|
2211
2264
|
for (const [key, projection] of Object.entries(value.projections?.values || {})) {
|
|
2212
|
-
|
|
2265
|
+
applyModernProjection(sessionId, key, projection, value.projections?.asOfSeq ?? value.cursor)
|
|
2213
2266
|
}
|
|
2214
2267
|
return
|
|
2215
2268
|
}
|
|
@@ -2282,7 +2335,11 @@ function applyModernRemoteEvent(ws, value) {
|
|
|
2282
2335
|
modernState.sessionCursors.delete(args[0])
|
|
2283
2336
|
modernState.assistantStreams.delete(args[0])
|
|
2284
2337
|
collectorReplay.mux.delete(`reasoning:${args[0]}`)
|
|
2338
|
+
collectorReplay.mux.delete(`session:${args[0]}`)
|
|
2339
|
+
collectorReplay.mux.delete(`queue:${args[0]}`)
|
|
2340
|
+
collectorReplay.mux.delete(`jobs:${args[0]}`)
|
|
2285
2341
|
try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'session:' + args[0] })) } catch {}
|
|
2342
|
+
try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'jobs:' + args[0] })) } catch {}
|
|
2286
2343
|
legacyPush('host', { type: 'host/session-removed', sessionId: args[0] })
|
|
2287
2344
|
} else if (value.event === 'api-session/status') {
|
|
2288
2345
|
legacyPush('host', { type: 'host/session-status', sessionId: args[0], running: !!args[1] })
|
|
@@ -2371,6 +2428,9 @@ function startModernEventCollector() {
|
|
|
2371
2428
|
const data = typeof ev.data === 'string' ? ev.data : Buffer.isBuffer(ev.data) ? ev.data.toString() : String(ev.data)
|
|
2372
2429
|
const frame = JSON.parse(data)
|
|
2373
2430
|
if (frame.type === 'error') {
|
|
2431
|
+
// Old generated-RPC Hosts may have no job namespace. This optional
|
|
2432
|
+
// roster must not degrade the working session and approval streams.
|
|
2433
|
+
if (frame.streamId?.startsWith('jobs:') && ['gateway/definition-unavailable', 'gateway/invocation-unavailable', 'gateway/service-unavailable', 'gateway/method-unavailable', 'method-unavailable', 'not-found'].includes(frame.error?.code)) return
|
|
2374
2434
|
const message = frame.error?.message || 'modern stream error'
|
|
2375
2435
|
for (const state of Object.values(eventCollectorState)) state.lastError = message
|
|
2376
2436
|
legacyPush('mux', { type: 'stream/error', error: frame.error || { message } })
|
|
@@ -2380,6 +2440,7 @@ function startModernEventCollector() {
|
|
|
2380
2440
|
if (frame.streamId === 'events') applyModernRemoteEvent(current, frame.value)
|
|
2381
2441
|
else if (frame.streamId === 'control') applyModernControlFrame(frame.value)
|
|
2382
2442
|
else if (frame.streamId === 'workspaces') applyModernWorkspaceFrame(frame.value)
|
|
2443
|
+
else if (frame.streamId.startsWith('jobs:')) applyModernJobFrame(frame.streamId.slice(5), frame.value)
|
|
2383
2444
|
else if (frame.streamId.startsWith('session:')) applyModernSessionFrame(frame.streamId.slice(8), frame.value)
|
|
2384
2445
|
} catch {}
|
|
2385
2446
|
}
|
|
@@ -4197,11 +4258,14 @@ function serveHandoff(req, res, url) {
|
|
|
4197
4258
|
fsJson(res, 400, { error: 'sessionId required' })
|
|
4198
4259
|
return
|
|
4199
4260
|
}
|
|
4261
|
+
// 白名单字段只接受字符串:对象/数字/数组等一律按空串处理,
|
|
4262
|
+
// 避免 String({}) 产出 '[object Object]' 之类垃圾落盘并广播到其他设备
|
|
4263
|
+
const str = (v) => typeof v === 'string' ? v : ''
|
|
4200
4264
|
handoffSave({
|
|
4201
4265
|
sessionId: sessionId.slice(0, 128),
|
|
4202
|
-
title:
|
|
4203
|
-
device:
|
|
4204
|
-
clientId:
|
|
4266
|
+
title: str(payload.title).slice(0, 200),
|
|
4267
|
+
device: str(payload.device).slice(0, 80),
|
|
4268
|
+
clientId: str(payload.clientId).slice(0, 96),
|
|
4205
4269
|
at: Date.now()
|
|
4206
4270
|
})
|
|
4207
4271
|
fsJson(res, 200, { ok: true })
|
|
@@ -4440,6 +4504,8 @@ async function proxyLegacyRpcWithModernFallback(req, res, url) {
|
|
|
4440
4504
|
|
|
4441
4505
|
async function proxyModernApi(req, res, url) {
|
|
4442
4506
|
if (req.method !== 'POST' || url.pathname.startsWith('/remote/')) return false
|
|
4507
|
+
// dsh-context is a Connection Fetch route with its own JSON contract, not RPC.
|
|
4508
|
+
if (url.pathname === '/api/dsh-context/detail') return false
|
|
4443
4509
|
if (await detectUpstreamApiFlavor() !== 'modern') return false
|
|
4444
4510
|
let raw = ''
|
|
4445
4511
|
try {
|
|
@@ -4560,7 +4626,13 @@ function proxyApi(req, res, url) {
|
|
|
4560
4626
|
res.end()
|
|
4561
4627
|
return
|
|
4562
4628
|
}
|
|
4563
|
-
|
|
4629
|
+
// Credential exchange is restricted to named client endpoints. Administrative
|
|
4630
|
+
// plugin routes must authenticate the master token before reaching DSH.
|
|
4631
|
+
const remoteClientPath = ['/remote/api/command', '/remote/api/command-status', '/remote/admin/api/dsh'].includes(url.pathname)
|
|
4632
|
+
|| (url.pathname === '/remote/api/insights/cost' && req.method === 'GET')
|
|
4633
|
+
|| /^\/remote\/api\/plugins\/(state|details|market|operations|recover)$/.test(url.pathname)
|
|
4634
|
+
const remoteAdminPath = url.pathname.startsWith('/remote/') && !remoteClientPath
|
|
4635
|
+
const ok = remoteAdminPath ? adminAuthorized(req, url) : remoteClientPath ? controlAuthorized(req, url) : authorized(req, url)
|
|
4564
4636
|
touchDevice(req, ok ? {} : { failedAuth: true })
|
|
4565
4637
|
if (!ok) {
|
|
4566
4638
|
authFailures++
|
package/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir, hostname, networkInterfaces } from 'node:os'
|
|
|
13
13
|
import { dirname, extname, normalize, resolve } from 'node:path'
|
|
14
14
|
import { fileURLToPath } from 'node:url'
|
|
15
15
|
import { createPluginCenter, detectProfile } from './plugin-center.mjs'
|
|
16
|
+
import { readCostInsights } from './insights.mjs'
|
|
16
17
|
|
|
17
18
|
export const name = 'dsh-remote'
|
|
18
19
|
export const inject = ['webServer', 'commands', 'agents', 'connection']
|
|
@@ -637,6 +638,15 @@ async function resolveFile(pathname) {
|
|
|
637
638
|
|
|
638
639
|
async function serveStatic(req, res, ctx) {
|
|
639
640
|
const pathname = new URL(req.url ?? '/', 'http://x').pathname
|
|
641
|
+
if (pathname === `${MOUNT}/api/insights/cost`) {
|
|
642
|
+
if (!remoteCommandAuthorized(req)) return sendJson(res, 401, { ok: false, code: 'unauthorized' })
|
|
643
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, code: 'read-only' })
|
|
644
|
+
const query = new URL(req.url, 'http://x').searchParams
|
|
645
|
+
if ([...query.keys()].some(key => key !== 'sessionId') || query.getAll('sessionId').length > 1) return sendJson(res, 400, { ok: false, code: 'invalid-query' })
|
|
646
|
+
const result = await readCostInsights(ctx, query.get('sessionId') || '')
|
|
647
|
+
res.setHeader('cache-control', 'no-store')
|
|
648
|
+
return sendJson(res, result.status, result.body)
|
|
649
|
+
}
|
|
640
650
|
if (pathname.startsWith(`${MOUNT}/api/plugins/`)) {
|
|
641
651
|
if (!remoteCommandAuthorized(req)) return sendJson(res, 401, { ok: false, message: 'unauthorized' })
|
|
642
652
|
try {
|
package/insights.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Read-only, bounded adapter for dsh-cost-meter 1.8.x. Never forward its config.
|
|
2
|
+
const states = new WeakMap()
|
|
3
|
+
const record = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
4
|
+
const number = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
|
|
5
|
+
const text = (value, max = 80) => typeof value === 'string' ? value.slice(0, max) : ''
|
|
6
|
+
const status = value => ['ok', 'off', 'error', 'stale', 'loading'].includes(value) ? value : 'unknown'
|
|
7
|
+
function amount(value) {
|
|
8
|
+
const row = record(value)
|
|
9
|
+
return { cost: number(row.cost), apiCost: number(row.apiCost ?? row.cost), calls: number(row.calls) }
|
|
10
|
+
}
|
|
11
|
+
function windows(value) {
|
|
12
|
+
return Object.entries(record(value)).slice(0, 8).map(([key, raw]) => {
|
|
13
|
+
const row = record(raw), percent = number(row.percent)
|
|
14
|
+
return { name: text(key), percent: percent !== null && percent <= 100 ? percent : null,
|
|
15
|
+
resetsAt: text(row.resetsAt), unlimited: row.unlimited === true }
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
export function summarizeCost(state, session, sessionId) {
|
|
19
|
+
const raw = record(state), config = record(raw.config), balance = record(raw.balance)
|
|
20
|
+
const plans = Object.entries(record(raw.codingPlans)).slice(0, 20)
|
|
21
|
+
.filter(([, row]) => record(row).enabled === true && record(row).display !== 'off')
|
|
22
|
+
.map(([name, value]) => {
|
|
23
|
+
const row = record(value)
|
|
24
|
+
return { name: text(name), status: status(row.status), fetchedAt: number(row.fetchedAt), windows: windows(row.windows) }
|
|
25
|
+
})
|
|
26
|
+
const go = record(raw.goQuota)
|
|
27
|
+
if (record(config.goQuota).enabled === true && record(config.goQuota).display !== 'off') {
|
|
28
|
+
plans.unshift({ name: 'OpenCode Go', status: status(go.status), fetchedAt: number(go.fetchedAt),
|
|
29
|
+
windows: windows({ rolling: go.rolling, weekly: go.weekly, monthly: go.monthly }).filter(row => row.percent !== null) })
|
|
30
|
+
}
|
|
31
|
+
const s = record(session)
|
|
32
|
+
return { schema: 1, available: true, source: 'dsh-cost-meter', generatedAt: Date.now(), currency: 'USD',
|
|
33
|
+
day: text(record(raw.runtime).dayKey || record(raw.today).date, 10),
|
|
34
|
+
today: config.hideTodayCost === true ? null : amount(raw.today),
|
|
35
|
+
session: sessionId ? { found: s.found === true, own: s.found === true ? amount(s.own) : null,
|
|
36
|
+
subagents: s.found === true ? amount(s.subagents) : null, subagentCount: number(s.subagentCount) } : null,
|
|
37
|
+
balance: config.hideOfficialBalance === true || record(config.balance).display === 'off' ? null : {
|
|
38
|
+
status: status(balance.status), fetchedAt: number(balance.fetchedAt),
|
|
39
|
+
currency: /^[A-Z]{3}$/.test(balance.currency) ? balance.currency : '', total: number(balance.totalBalance) }, plans }
|
|
40
|
+
}
|
|
41
|
+
function bounded(task, ms = 20000) {
|
|
42
|
+
let timer
|
|
43
|
+
return Promise.race([task, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), ms) })])
|
|
44
|
+
.finally(() => clearTimeout(timer))
|
|
45
|
+
}
|
|
46
|
+
export async function readCostInsights(ctx, sessionId = '') {
|
|
47
|
+
if (typeof sessionId !== 'string' || sessionId.length > 200 || /[\s\x00-\x1f\x7f]/.test(sessionId)) {
|
|
48
|
+
return { status: 400, body: { ok: false, code: 'invalid-session' } }
|
|
49
|
+
}
|
|
50
|
+
let service
|
|
51
|
+
try { service = ctx.get?.('costMeter') } catch {}
|
|
52
|
+
if (!service) return { status: 200, body: { ok: true, schema: 1, available: false, code: 'not-installed' } }
|
|
53
|
+
if (typeof service.getState !== 'function') return { status: 200, body: { ok: true, schema: 1, available: false, code: 'unsupported' } }
|
|
54
|
+
try {
|
|
55
|
+
// A short shared cache coalesces device reads; no credentials or settings are sent.
|
|
56
|
+
let cache = states.get(ctx)
|
|
57
|
+
const identity = service[Symbol.for('cordis.original')] ?? service
|
|
58
|
+
if (!cache || cache.service !== identity) { cache = { service: identity, at: 0 }; states.set(ctx, cache) }
|
|
59
|
+
if (!cache.task && (!cache.value || Date.now() - cache.at > 15000)) {
|
|
60
|
+
cache.task = bounded(Promise.resolve().then(() => service.getState())).then(value => {
|
|
61
|
+
if (!value || typeof value !== 'object' || !value.today) throw new Error('unsupported')
|
|
62
|
+
cache.value = value; cache.at = Date.now(); return value
|
|
63
|
+
}).finally(() => { cache.task = null })
|
|
64
|
+
}
|
|
65
|
+
const raw = cache.task ? await cache.task : cache.value
|
|
66
|
+
const session = sessionId && typeof service.getSessionCost === 'function'
|
|
67
|
+
? await bounded(Promise.resolve().then(() => service.getSessionCost(sessionId))) : null
|
|
68
|
+
return { status: 200, body: { ok: true, ...summarizeCost(raw, session, sessionId) } }
|
|
69
|
+
} catch {
|
|
70
|
+
// Plugin failures may mention credentials, URLs or paths. Return a fixed code.
|
|
71
|
+
return { status: 502, body: { ok: false, code: 'cost-unavailable' } }
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"apk",
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"*.md",
|
|
22
|
-
"plugin-center.mjs"
|
|
22
|
+
"plugin-center.mjs",
|
|
23
|
+
"insights.mjs"
|
|
23
24
|
],
|
|
24
25
|
"keywords": [
|
|
25
26
|
"dsh-plugin",
|
package/plugin-center.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Profile-scoped plugin management. No shell input, dependencies or client-selected paths. */
|
|
2
|
-
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, realpathSync, openSync, closeSync } from 'node:fs'
|
|
3
3
|
import { dirname, basename, join, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
@@ -20,19 +20,66 @@ const fail = (message, status = 400) => Object.assign(new Error(message), { stat
|
|
|
20
20
|
const protectedPackage = name => name === 'dsh-remote-plugin' || name.startsWith('@deepseek-ai/')
|
|
21
21
|
const revision = path => createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
function alive(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
25
|
+
try { process.kill(pid, 0); return true } catch (error) { return error.code !== 'ESRCH' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Never clear a live worker or its package-manager child. PID reuse fails closed.
|
|
29
|
+
function recoverDeadLock(storage) {
|
|
30
|
+
const path = join(storage, 'lock.json')
|
|
31
|
+
if (!existsSync(path)) return
|
|
32
|
+
const owner = read(path)
|
|
33
|
+
if (!idPattern.test(owner.id || '')) throw fail('插件任务锁损坏,请在主机检查', 409)
|
|
34
|
+
const launchPath = join(storage, 'launch-' + owner.id + '.json')
|
|
35
|
+
const workerPid = owner.workerPid || (existsSync(launchPath) ? read(launchPath).pid : null)
|
|
36
|
+
if (alive(workerPid) || alive(owner.cliPid)) return
|
|
37
|
+
if (!workerPid && (!owner.launcherPid || alive(owner.launcherPid) || Date.now() - owner.createdAt < 30000)) return
|
|
38
|
+
const jobPath = join(storage, 'job-' + owner.id + '.json')
|
|
39
|
+
if (existsSync(jobPath)) {
|
|
40
|
+
const job = read(jobPath)
|
|
41
|
+
if (['queued', 'running'].includes(job.phase)) {
|
|
42
|
+
job.phase = 'interrupted'; job.endedAt = Date.now(); job.restartRequired = true
|
|
43
|
+
job.message = '任务进程已退出,已解除锁定。文件可能已改变,请核对插件状态;未执行自动回滚。'
|
|
44
|
+
atomic(jobPath, job)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// All reads/writes here are synchronous; recheck ownership before unlinking.
|
|
48
|
+
if (read(path).id === owner.id) unlinkSync(path)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function detectProfile(ctx, cli = process.argv[1], runtime = process) {
|
|
24
52
|
try {
|
|
25
53
|
const dir = realpathSync(fileURLToPath(ctx.root?.baseUrl || ctx.baseUrl))
|
|
26
54
|
const manifest = read(join(dir, 'package.json'))
|
|
27
55
|
if (basename(dirname(dir)) !== 'profiles' || !Array.isArray(manifest.dsh?.profile?.bundles)) return null
|
|
28
56
|
const name = basename(dir)
|
|
29
57
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*(?![\s\S])/.test(name)) return null
|
|
30
|
-
|
|
58
|
+
const base = { dir, name, home: dirname(dirname(dir)), cli: null }
|
|
59
|
+
// Desktop reserves this profile for its installation-owned carrier CLI.
|
|
60
|
+
// Bind to the host that booted this root, never a global CLI or PATH entry.
|
|
61
|
+
if (name === 'desktop') {
|
|
62
|
+
try {
|
|
63
|
+
const entry = realpathSync(cli), hostRoot = dirname(dirname(entry))
|
|
64
|
+
const host = read(join(hostRoot, 'package.json'))
|
|
65
|
+
const runtimeDir = realpathSync(resolve(hostRoot, '../../..'))
|
|
66
|
+
const bundled = read(join(runtimeDir, 'package.json'))
|
|
67
|
+
const carrier = join(hostRoot, 'lib', 'cli.js')
|
|
68
|
+
if (runtime.versions?.electron && host.name === '@deepseek-ai/dsh-desktop-host'
|
|
69
|
+
&& entry === realpathSync(join(hostRoot, 'lib', 'index.js'))
|
|
70
|
+
&& bundled.name === '@deepseek-ai/dsh-desktop-runtime' && bundled.version === host.version
|
|
71
|
+
&& runtimeDir === realpathSync(runtime.argv[2]) && dir === realpathSync(runtime.argv[3])
|
|
72
|
+
&& existsSync(carrier)) return { ...base, cli: carrier, cliMode: 'desktop' }
|
|
73
|
+
} catch {}
|
|
74
|
+
return base
|
|
75
|
+
}
|
|
76
|
+
let root
|
|
77
|
+
try { root = dirname(realpathSync(cli)) } catch { return base }
|
|
31
78
|
let found = null
|
|
32
79
|
for (let i = 0; i < 5; i++, root = dirname(root)) {
|
|
33
80
|
try { if (read(join(root, 'package.json')).name === '@deepseek-ai/dsh') { found = join(root, 'lib', 'bin.js'); break } } catch {}
|
|
34
81
|
}
|
|
35
|
-
return {
|
|
82
|
+
return { ...base, cli: found && existsSync(found) ? found : null }
|
|
36
83
|
} catch { return null }
|
|
37
84
|
}
|
|
38
85
|
|
|
@@ -68,10 +115,18 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
68
115
|
const profile = options.profile === undefined ? detectProfile(ctx) : options.profile
|
|
69
116
|
const details = options.details || pluginDetails
|
|
70
117
|
const launch = options.launch || (args => {
|
|
71
|
-
const
|
|
118
|
+
const desktop = profile?.cliMode === 'desktop'
|
|
119
|
+
const errorLog = openSync(join(profile.dir, '.remote-plugin-center', 'worker-' + args[2] + '.log'), 'a', 0o600)
|
|
120
|
+
let child
|
|
121
|
+
try {
|
|
122
|
+
child = spawn(process.execPath, [...(desktop ? ['--expose-internals'] : []), SELF, '--worker', ...args], {
|
|
123
|
+
detached: true, windowsHide: true, stdio: ['ignore', 'ignore', errorLog],
|
|
124
|
+
env: { ...process.env, ...(desktop ? { ELECTRON_RUN_AS_NODE: '1' } : {}) },
|
|
125
|
+
})
|
|
126
|
+
} finally { closeSync(errorLog) }
|
|
72
127
|
return new Promise((resolveLaunch, reject) => {
|
|
73
128
|
child.once('error', reject)
|
|
74
|
-
child.once('spawn', () => { child.unref(); resolveLaunch() })
|
|
129
|
+
child.once('spawn', () => { child.unref(); resolveLaunch(child.pid) })
|
|
75
130
|
})
|
|
76
131
|
})
|
|
77
132
|
const manifestPath = profile && join(profile.dir, 'package.json')
|
|
@@ -79,6 +134,7 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
79
134
|
const storage = profile && join(profile.dir, '.remote-plugin-center')
|
|
80
135
|
let submitting = false
|
|
81
136
|
async function inventory() {
|
|
137
|
+
if (storage) recoverDeadLock(storage)
|
|
82
138
|
let runtime = [], runtimeAvailable = false
|
|
83
139
|
const loader = ctx.get?.('loader') || ctx.loader
|
|
84
140
|
if (loader?.entries) {
|
|
@@ -97,19 +153,46 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
97
153
|
if (packagePattern.test(name)) {
|
|
98
154
|
try { pkg = read(join(profile.dir, 'node_modules', ...name.split('/'), 'package.json')) } catch {}
|
|
99
155
|
}
|
|
100
|
-
|
|
156
|
+
const displayName = typeof pkg?.displayName === 'string' ? pkg.displayName : typeof pkg?.dsh?.displayName === 'string' ? pkg.dsh.displayName : name
|
|
157
|
+
return { name, displayName, version: pkg?.version || '', requested: manifest.dependencies?.[name] || '',
|
|
101
158
|
enabled: bundles.includes(name), bundle: !!pkg?.dsh?.bundle?.patch || bundles.includes(name),
|
|
102
159
|
managed: !!manifest.dependencies?.[name] && !protectedPackage(name),
|
|
103
|
-
|
|
160
|
+
description: pkg?.description || '', runtime: runtime.filter(entry => entry.name === name) }
|
|
104
161
|
})
|
|
105
162
|
let operations = []
|
|
106
163
|
if (existsSync(storage)) operations = readdirSync(storage).filter(name => /^job-[a-zA-Z0-9-]+\.json$/.test(name)).map(name => {
|
|
107
164
|
try { return read(join(storage, name)) } catch { return null }
|
|
108
165
|
}).filter(Boolean).sort((a, b) => b.startedAt - a.startedAt).slice(0, 20).map(publicJob)
|
|
166
|
+
const lockPath = join(storage, 'lock.json')
|
|
167
|
+
const owner = existsSync(lockPath) ? read(lockPath) : null
|
|
168
|
+
const legacyLock = owner && idPattern.test(owner.id || '') && !owner.launcherPid && !owner.workerPid && !owner.cliPid && !existsSync(join(storage, 'launch-' + owner.id + '.json'))
|
|
109
169
|
return { ok: true, profile: profile.name, revision: revision(manifestPath), writable: !!profile.cli,
|
|
110
|
-
reason: profile.cli ? '' :
|
|
111
|
-
|
|
112
|
-
|
|
170
|
+
reason: profile.cli ? '' : profile.name === 'desktop'
|
|
171
|
+
? '未能验证当前桌面安装的插件管理入口;仅显示状态,请检查 DSH Desktop 版本与安装完整性。'
|
|
172
|
+
: '当前启动方式没有可验证的 DSH CLI,管理操作不可用。',
|
|
173
|
+
pendingRestart: revision(manifestPath) !== initialRevision || operations.some(job => job.phase === 'interrupted' && job.restartRequired),
|
|
174
|
+
busy: submitting || !!owner, recovery: legacyLock ? { id: owner.id, reason: '旧任务锁没有进程信息,需在主机核对后手动解除。' } : null,
|
|
175
|
+
items, runtime, runtimeAvailable, operations }
|
|
176
|
+
}
|
|
177
|
+
function recoverLegacy(body) {
|
|
178
|
+
if (!profile?.cli || submitting) throw fail('当前不能恢复任务', 409)
|
|
179
|
+
const lockPath = join(storage, 'lock.json')
|
|
180
|
+
if (!existsSync(lockPath)) throw fail('任务锁已变化,请刷新', 409)
|
|
181
|
+
const owner = read(lockPath)
|
|
182
|
+
if (!idPattern.test(body.id || '') || body.id !== owner.id || owner.launcherPid || owner.workerPid || owner.cliPid || existsSync(join(storage, 'launch-' + owner.id + '.json'))) throw fail('仅支持人工恢复无进程信息的旧任务锁', 409)
|
|
183
|
+
if (revision(manifestPath) !== body.revision) throw fail('插件列表已变化,请刷新', 409)
|
|
184
|
+
if (body.confirmNoRunningProcess !== true) throw fail('请先在主机确认插件安装进程已退出', 409)
|
|
185
|
+
const jobPath = join(storage, 'job-' + owner.id + '.json')
|
|
186
|
+
if (existsSync(jobPath)) {
|
|
187
|
+
const job = read(jobPath)
|
|
188
|
+
if (['queued', 'running'].includes(job.phase)) {
|
|
189
|
+
job.phase = 'interrupted'; job.endedAt = Date.now(); job.restartRequired = true
|
|
190
|
+
job.message = '用户核对进程后解除了旧任务锁;保留现有文件和备份,未执行自动回滚。'
|
|
191
|
+
atomic(jobPath, job)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
renameSync(lockPath, join(storage, 'recovered-lock-' + owner.id + '.json'))
|
|
195
|
+
return { ok: true }
|
|
113
196
|
}
|
|
114
197
|
async function submit(body) {
|
|
115
198
|
if (!profile?.cli) throw fail('Current DSH profile is read-only', 409)
|
|
@@ -118,6 +201,7 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
118
201
|
if (!idPattern.test(id || '') || !packagePattern.test(name || '') || !['install', 'update', 'remove', 'enable', 'disable'].includes(action)) throw fail('Invalid operation')
|
|
119
202
|
if (protectedPackage(name)) throw fail('核心插件与 Remote 自身请通过主机维护流程管理', 409)
|
|
120
203
|
mkdirSync(storage, { recursive: true, mode: 0o700 })
|
|
204
|
+
recoverDeadLock(storage)
|
|
121
205
|
const jobPath = join(storage, 'job-' + id + '.json')
|
|
122
206
|
if (existsSync(jobPath)) {
|
|
123
207
|
const previous = read(jobPath)
|
|
@@ -142,12 +226,14 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
142
226
|
}
|
|
143
227
|
if (revision(manifestPath) !== body.revision) throw fail('插件配置已变化,请刷新', 409)
|
|
144
228
|
const lock = join(storage, 'lock.json')
|
|
145
|
-
try { writeFileSync(lock, JSON.stringify({ id }), { flag: 'wx', mode: 0o600 }) }
|
|
229
|
+
try { writeFileSync(lock, JSON.stringify({ id, launcherPid: process.pid, createdAt: Date.now() }), { flag: 'wx', mode: 0o600 }) }
|
|
146
230
|
catch (error) { if (error.code === 'EEXIST') throw fail('已有插件任务运行中;请查看操作记录', 409); throw error }
|
|
147
231
|
const job = { id, action, name, version: version || '', revision: body.revision, profile: profile.name, phase: 'queued', startedAt: Date.now(), log: '', restartRequired: false }
|
|
148
232
|
try {
|
|
149
233
|
atomic(jobPath, job)
|
|
150
|
-
await launch([profile.dir, profile.cli, id])
|
|
234
|
+
const pid = await launch([profile.dir, profile.cli, id, profile.cliMode || 'node'])
|
|
235
|
+
// Separate ledger: the launcher must never overwrite a worker's CLI PID.
|
|
236
|
+
if (Number.isInteger(pid) && pid > 0) atomic(join(storage, 'launch-' + id + '.json'), { pid })
|
|
151
237
|
} catch (error) {
|
|
152
238
|
job.phase = 'failed'; job.message = '无法启动插件任务'; job.endedAt = Date.now()
|
|
153
239
|
atomic(jobPath, job); unlinkSync(lock); throw error
|
|
@@ -167,6 +253,7 @@ export function createPluginCenter(ctx, options = {}) {
|
|
|
167
253
|
return { ok: true, source: REGISTRY, total: data.total || 0, items: (data.objects || []).map(({ package: p }) => ({ name: p.name, version: p.version, description: p.description || '' })) }
|
|
168
254
|
}
|
|
169
255
|
if (method === 'POST' && sub === '/operations') return { ok: true, operation: await submit(body) }
|
|
256
|
+
if (method === 'POST' && sub === '/recover') return recoverLegacy(body)
|
|
170
257
|
throw fail('Unknown plugin endpoint', 404)
|
|
171
258
|
}
|
|
172
259
|
return { handle, inventory, submit }
|
|
@@ -177,11 +264,13 @@ function publicJob(job) {
|
|
|
177
264
|
return { id, action, name, version, profile, phase, startedAt, endedAt, log, message, restartRequired }
|
|
178
265
|
}
|
|
179
266
|
|
|
180
|
-
export async function runWorker(dir, cli, id, execute = runCli) {
|
|
267
|
+
export async function runWorker(dir, cli, id, execute = runCli, cliMode = 'node') {
|
|
181
268
|
if (!idPattern.test(id || '')) throw fail('Invalid worker id')
|
|
182
269
|
const storage = join(dir, '.remote-plugin-center')
|
|
183
270
|
const lock = join(storage, 'lock.json')
|
|
184
|
-
|
|
271
|
+
const owner = read(lock)
|
|
272
|
+
if (owner.id !== id || owner.workerPid && owner.workerPid !== process.pid) throw fail('Worker does not own lock')
|
|
273
|
+
atomic(lock, { ...owner, workerPid: process.pid })
|
|
185
274
|
const jobPath = join(storage, 'job-' + id + '.json')
|
|
186
275
|
const job = read(jobPath)
|
|
187
276
|
const manifestPath = join(dir, 'package.json')
|
|
@@ -199,11 +288,11 @@ export async function runWorker(dir, cli, id, execute = runCli) {
|
|
|
199
288
|
}
|
|
200
289
|
if (existsSync(disabledPath)) copyFileSync(disabledPath, join(backup, 'disabled.json'))
|
|
201
290
|
if (['install', 'update', 'remove'].includes(job.action)) {
|
|
202
|
-
const args = job.action === 'remove' ? ['remove', job.name, '--ignore-scripts'] : ['add', job.name + '@' + job.version, '--save-exact', '--ignore-scripts', '--registry=' + REGISTRY]
|
|
291
|
+
const args = job.action === 'remove' ? ['remove', job.name, '--config.ignore-scripts=true'] : ['add', job.name + '@' + job.version, '--save-exact', '--ignore-scripts', '--registry=' + REGISTRY]
|
|
203
292
|
await execute(cli, ['plugin', '--profile', basename(dir), ...args], dir, chunk => {
|
|
204
293
|
job.log = (job.log + chunk.replace(/\x1b\[[0-9;]*m/g, '').replace(/(token|password|authorization)\s*[=:]\s*\S+/gi, '$1=[redacted]')).slice(-12000)
|
|
205
294
|
save()
|
|
206
|
-
})
|
|
295
|
+
}, pid => atomic(lock, { ...read(lock), cliPid: pid }), cliMode)
|
|
207
296
|
}
|
|
208
297
|
if (job.action === 'disable') disabled.add(job.name)
|
|
209
298
|
if (job.action === 'enable' || job.action === 'remove') disabled.delete(job.name)
|
|
@@ -230,10 +319,18 @@ export async function runWorker(dir, cli, id, execute = runCli) {
|
|
|
230
319
|
}
|
|
231
320
|
}
|
|
232
321
|
|
|
233
|
-
function
|
|
322
|
+
export function cliInvocation(cli, args, dir, cliMode = 'node', runtime = process) {
|
|
323
|
+
if (!['node', 'desktop'].includes(cliMode)) throw fail('Unsupported CLI mode')
|
|
324
|
+
return { command: runtime.execPath, args: [...(cliMode === 'desktop' ? ['--expose-internals'] : []), cli, ...args],
|
|
325
|
+
env: { ...runtime.env, DSH_HOME: dirname(dirname(dir)), npm_config_ignore_scripts: 'true',
|
|
326
|
+
...(cliMode === 'desktop' ? { ELECTRON_RUN_AS_NODE: '1' } : {}) } }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function runCli(cli, args, dir, output, onSpawn, cliMode) {
|
|
234
330
|
return new Promise((resolveRun, reject) => {
|
|
235
|
-
const
|
|
236
|
-
const child = spawn(
|
|
331
|
+
const invocation = cliInvocation(cli, args, dir, cliMode)
|
|
332
|
+
const child = spawn(invocation.command, invocation.args, { cwd: dir, env: invocation.env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
333
|
+
child.once('spawn', () => onSpawn(child.pid))
|
|
237
334
|
child.stdout.on('data', chunk => output(chunk.toString()))
|
|
238
335
|
child.stderr.on('data', chunk => output(chunk.toString()))
|
|
239
336
|
child.once('error', reject)
|
|
@@ -242,5 +339,5 @@ function runCli(cli, args, dir, output) {
|
|
|
242
339
|
}
|
|
243
340
|
|
|
244
341
|
if (process.argv[1] && resolve(process.argv[1]) === SELF && process.argv[2] === '--worker') {
|
|
245
|
-
await runWorker(process.argv[3], process.argv[4], process.argv[5])
|
|
342
|
+
await runWorker(process.argv[3], process.argv[4], process.argv[5], undefined, process.argv[6])
|
|
246
343
|
}
|
package/public/admin.html
CHANGED
|
@@ -342,6 +342,7 @@
|
|
|
342
342
|
<div class="admin-title">
|
|
343
343
|
<div class="left">
|
|
344
344
|
<a id="btn-console" class="mini-btn tb-btn" href="../" data-i18n-title="consoleTitle">‹ <span data-i18n="console">控制台</span></a>
|
|
345
|
+
<a id="btn-plugin-panel" class="mini-btn tb-btn hidden" href="../plugin.html" data-i18n-title="pluginPanel">‹ <span data-i18n="pluginPanel">状态面板</span></a>
|
|
345
346
|
<button id="btn-close-drawer" class="mini-btn tb-btn hidden" data-i18n-title="collapse">‹ <span data-i18n="collapse">收起面板</span></button>
|
|
346
347
|
<h1>DSH Remote <span data-i18n="admin">管理</span></h1>
|
|
347
348
|
</div>
|
|
@@ -542,6 +543,7 @@
|
|
|
542
543
|
zh: {
|
|
543
544
|
'console': '控制台',
|
|
544
545
|
'consoleTitle': '返回控制台',
|
|
546
|
+
'pluginPanel': '状态面板',
|
|
545
547
|
'collapse': '收起面板',
|
|
546
548
|
'admin': '管理',
|
|
547
549
|
'hero.eyebrow': 'GATEWAY CONTROL', 'hero.running': '网关运行正常', 'hero.attention': '网关需要关注', 'hero.plugin': 'DSH 插件已连接', 'hero.offline': '网关未运行', 'hero.checking': '正在检查网关',
|
|
@@ -652,6 +654,7 @@
|
|
|
652
654
|
en: {
|
|
653
655
|
'console': 'Console',
|
|
654
656
|
'consoleTitle': 'Back to console',
|
|
657
|
+
'pluginPanel': 'Status panel',
|
|
655
658
|
'collapse': 'Collapse panel',
|
|
656
659
|
'admin': 'Admin',
|
|
657
660
|
'hero.eyebrow': 'GATEWAY CONTROL', 'hero.running': 'Gateway is healthy', 'hero.attention': 'Gateway needs attention', 'hero.plugin': 'DSH plugin connected', 'hero.offline': 'Gateway is not running', 'hero.checking': 'Checking gateway',
|
package/public/admin.js
CHANGED
|
@@ -1028,6 +1028,7 @@ function start(showLogin) {
|
|
|
1028
1028
|
if (pluginMode) {
|
|
1029
1029
|
$('login-view').classList.add('hidden')
|
|
1030
1030
|
$('btn-console').classList.add('hidden')
|
|
1031
|
+
if (location.protocol === 'dsh-app:' && window.parent !== window) $('btn-plugin-panel').classList.remove('hidden')
|
|
1031
1032
|
$('btn-close-drawer').classList.remove('hidden')
|
|
1032
1033
|
start(false)
|
|
1033
1034
|
} else if (token) {
|