dsh-miloco-bridge 0.1.0 → 0.1.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/README.md +18 -10
- package/cordis.patch.yml +80 -5
- package/{doc → docs}/ARCHITECTURE.md +13 -6
- package/{doc → docs}/INSTALL.md +25 -11
- package/{doc → docs}/SKILL-REWRITES.md +3 -3
- package/{doc → docs}/TOOLS.md +13 -10
- package/{doc → docs}/VERIFICATION.md +22 -6
- package/docs/images/agent-presets.png +0 -0
- package/docs/images/miloco-settings.png +0 -0
- package/lib/client.js +693 -119
- package/lib/deploy.js +236 -38
- package/lib/github-release.js +162 -0
- package/lib/index.js +11 -1
- package/lib/settings-api.js +170 -1
- package/lib/tools.js +365 -40
- package/package.json +2 -2
- package/skills/miloco-create-task/SKILL.md +63 -14
- package/skills/miloco-devices/SKILL.md +3 -2
- package/doc/images/agent-presets.png +0 -0
- package/doc/images/miloco-settings.png +0 -0
package/lib/settings-api.js
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { access } from 'node:fs/promises'
|
|
24
|
-
import { join } from 'node:path'
|
|
24
|
+
import { dirname, join } from 'node:path'
|
|
25
|
+
import { fetchLatestRelease, inferDeployKind, isNewerRelease, normalizeReleaseVersion } from './github-release.js'
|
|
25
26
|
import { MilocoError, readMilocoConfig } from './miloco-client.js'
|
|
26
27
|
import { normalizePerceptionLevel, PERCEPTION_LEVELS } from './perception.js'
|
|
27
28
|
import { AGENT_MODES, describeMode, readSessions } from './state.js'
|
|
@@ -212,6 +213,8 @@ export function registerSettingsApi(options) {
|
|
|
212
213
|
const { ctx, config, client, overlay } = options
|
|
213
214
|
const log = options.logger ?? ctx.logger
|
|
214
215
|
const disposers = []
|
|
216
|
+
/** @type {null | {at: number, source: string, has_update: boolean, current: string, latest: string}} */
|
|
217
|
+
let lastUpgradeCheck = null
|
|
215
218
|
|
|
216
219
|
/**
|
|
217
220
|
* Register one exact route.
|
|
@@ -470,6 +473,172 @@ export function registerSettingsApi(options) {
|
|
|
470
473
|
})
|
|
471
474
|
})
|
|
472
475
|
|
|
476
|
+
route(`${SETTINGS_PREFIX}/version`, async (req, res) => {
|
|
477
|
+
if (req.method !== 'GET') {
|
|
478
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
const force = new URL(req.url, 'http://127.0.0.1').searchParams.get('refresh') === '1'
|
|
482
|
+
const endpoint = await client.endpoint()
|
|
483
|
+
const fileConfig = await readMilocoConfig(endpoint.home)
|
|
484
|
+
const pythonBin = String(fileConfig?.server?.python_bin ?? '')
|
|
485
|
+
// server.python_bin = <source>/backend/.venv/bin/python
|
|
486
|
+
const sourceRoot = pythonBin
|
|
487
|
+
? dirname(dirname(dirname(dirname(pythonBin))))
|
|
488
|
+
: (config.deploy?.sourceDir || '')
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const [version, backendUpgrade, upgradeStatus] = await Promise.all([
|
|
492
|
+
client.get('/api/admin/version', { signal: AbortSignal.timeout(8_000) }),
|
|
493
|
+
client.get('/api/admin/upgrade/check', {
|
|
494
|
+
query: { force: force ? 'true' : 'false' },
|
|
495
|
+
signal: AbortSignal.timeout(15_000),
|
|
496
|
+
}).catch(() => null),
|
|
497
|
+
client.get('/api/admin/upgrade/status', { signal: AbortSignal.timeout(8_000) }).catch(() => null),
|
|
498
|
+
])
|
|
499
|
+
let upgrade = backendUpgrade
|
|
500
|
+
if (!upgrade || upgrade.reachable === false) {
|
|
501
|
+
const current = String(upgrade?.current || version?.version || '')
|
|
502
|
+
const deployKind = upgrade?.deploy_kind || inferDeployKind(current)
|
|
503
|
+
try {
|
|
504
|
+
const latest = await fetchLatestRelease({
|
|
505
|
+
repo: config.deploy?.repo,
|
|
506
|
+
apiBase: config.deploy?.githubApi,
|
|
507
|
+
mirrors: config.deploy?.githubMirrors,
|
|
508
|
+
timeoutMs: 6_000,
|
|
509
|
+
})
|
|
510
|
+
upgrade = {
|
|
511
|
+
current,
|
|
512
|
+
latest: normalizeReleaseVersion(latest.tag),
|
|
513
|
+
has_update: Boolean(current) && deployKind === 'release'
|
|
514
|
+
&& isNewerRelease(current, latest.tag),
|
|
515
|
+
deploy_kind: deployKind,
|
|
516
|
+
release_url: latest.release_url || latest.html_url,
|
|
517
|
+
reachable: true,
|
|
518
|
+
source: latest.source,
|
|
519
|
+
mirror: latest.mirror || null,
|
|
520
|
+
checked_at: Math.floor(Date.now() / 1000),
|
|
521
|
+
}
|
|
522
|
+
} catch (fallbackError) {
|
|
523
|
+
const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
|
|
524
|
+
upgrade = {
|
|
525
|
+
...(upgrade ?? {}),
|
|
526
|
+
current,
|
|
527
|
+
latest: null,
|
|
528
|
+
has_update: false,
|
|
529
|
+
deploy_kind: deployKind,
|
|
530
|
+
release_url: null,
|
|
531
|
+
reachable: false,
|
|
532
|
+
source: 'none',
|
|
533
|
+
error: message,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
upgrade = { ...upgrade, source: upgrade.source || 'backend' }
|
|
538
|
+
}
|
|
539
|
+
if (upgrade) {
|
|
540
|
+
lastUpgradeCheck = {
|
|
541
|
+
at: Date.now(),
|
|
542
|
+
source: upgrade.source || 'backend',
|
|
543
|
+
has_update: Boolean(upgrade.has_update),
|
|
544
|
+
current: String(upgrade.current || ''),
|
|
545
|
+
latest: String(upgrade.latest || ''),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
sendJson(res, 200, {
|
|
549
|
+
ok: true,
|
|
550
|
+
reachable: true,
|
|
551
|
+
sourceRoot,
|
|
552
|
+
version,
|
|
553
|
+
upgrade,
|
|
554
|
+
upgradeStatus,
|
|
555
|
+
})
|
|
556
|
+
} catch (error) {
|
|
557
|
+
sendJson(res, 200, {
|
|
558
|
+
ok: true,
|
|
559
|
+
reachable: false,
|
|
560
|
+
sourceRoot,
|
|
561
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
562
|
+
version: null,
|
|
563
|
+
upgrade: null,
|
|
564
|
+
upgradeStatus: null,
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
route(`${SETTINGS_PREFIX}/upgrade`, async (req, res) => {
|
|
570
|
+
if (req.method !== 'POST') {
|
|
571
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
const body = await mutateBody(req, res)
|
|
575
|
+
if (body === null) return
|
|
576
|
+
const recent = lastUpgradeCheck && Date.now() - lastUpgradeCheck.at < 10 * 60_000
|
|
577
|
+
const pluginUpgrade = recent
|
|
578
|
+
&& lastUpgradeCheck.source !== 'backend'
|
|
579
|
+
&& lastUpgradeCheck.has_update
|
|
580
|
+
&& typeof options.deploy?.startUpgrade === 'function'
|
|
581
|
+
if (pluginUpgrade) {
|
|
582
|
+
const proto = String(req.headers['x-forwarded-proto'] || 'http').split(',')[0].trim()
|
|
583
|
+
const origin = typeof req.headers.origin === 'string' && req.headers.origin
|
|
584
|
+
? req.headers.origin
|
|
585
|
+
: `${proto}://${req.headers.host ?? '127.0.0.1:3080'}`
|
|
586
|
+
const outcome = await options.deploy.startUpgrade({ origin })
|
|
587
|
+
if (!outcome.started) {
|
|
588
|
+
sendJson(res, 409, { ok: false, error: outcome.reason || '升级未启动', ...outcome })
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
sendJson(res, 200, { ok: true, source: 'plugin', ...outcome })
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
// Upstream only allows /upgrade/run for release deployments. It performs
|
|
595
|
+
// its own has_update check; surface its 400 reason verbatim.
|
|
596
|
+
try {
|
|
597
|
+
const data = await client.post('/api/admin/upgrade/run', undefined, {
|
|
598
|
+
signal: AbortSignal.timeout(30_000),
|
|
599
|
+
})
|
|
600
|
+
sendJson(res, 200, { ok: true, data })
|
|
601
|
+
} catch (error) {
|
|
602
|
+
sendJson(res, 400, {
|
|
603
|
+
ok: false,
|
|
604
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
route(`${SETTINGS_PREFIX}/upgrade/status`, async (req, res) => {
|
|
610
|
+
if (req.method !== 'GET') {
|
|
611
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
const pluginState = options.deploy?.upgradeState?.()
|
|
615
|
+
if (pluginState && pluginState.step !== 'idle') {
|
|
616
|
+
const phaseMap = {
|
|
617
|
+
'downloading-installer': 'downloading',
|
|
618
|
+
'starting-service': 'restarting',
|
|
619
|
+
wiring: 'installing',
|
|
620
|
+
}
|
|
621
|
+
sendJson(res, 200, {
|
|
622
|
+
ok: true,
|
|
623
|
+
source: 'plugin',
|
|
624
|
+
status: { ...pluginState, phase: phaseMap[pluginState.step] || pluginState.step },
|
|
625
|
+
})
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const status = await client.get('/api/admin/upgrade/status', {
|
|
630
|
+
signal: AbortSignal.timeout(8_000),
|
|
631
|
+
})
|
|
632
|
+
sendJson(res, 200, { ok: true, status })
|
|
633
|
+
} catch (error) {
|
|
634
|
+
sendJson(res, 200, {
|
|
635
|
+
ok: true,
|
|
636
|
+
status: null,
|
|
637
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
|
|
473
642
|
route(`${SETTINGS_PREFIX}/deploy`, async (req, res) => {
|
|
474
643
|
if (!options.deploy) {
|
|
475
644
|
sendJson(res, 503, { ok: false, error: '部署功能未启用' })
|