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/deploy.js
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
* reach a backend it can:
|
|
6
6
|
*
|
|
7
7
|
* - start an already-installed Miloco (`miloco-cli service start`), or
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - download the official release installer and run it without OpenClaw
|
|
9
|
+
* (`install.sh --skip-openclaw`), then install this package's adapter.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The release installer is downloaded directly from GitHub first and then
|
|
12
|
+
* through the configured mirrors. It is a self-contained script and downloads
|
|
13
|
+
* its own release assets through the mirrors embedded in its manifest.
|
|
14
14
|
*
|
|
15
|
-
* This module never vendors upstream code: the
|
|
16
|
-
* runtime, on the owner's machine, after
|
|
15
|
+
* This module never vendors upstream code: the release installer download and
|
|
16
|
+
* install happen at runtime, on the owner's machine, after a button press.
|
|
17
17
|
*
|
|
18
18
|
* @module dsh-miloco-bridge/deploy
|
|
19
19
|
*/
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
import { spawn } from 'node:child_process'
|
|
22
22
|
import { randomBytes } from 'node:crypto'
|
|
23
23
|
import { constants, existsSync } from 'node:fs'
|
|
24
|
-
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
24
|
+
import { access, chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
25
25
|
import { homedir } from 'node:os'
|
|
26
26
|
import { dirname, join } from 'node:path'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
28
|
import { readMilocoConfig } from './miloco-client.js'
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_GITHUB_MIRRORS,
|
|
31
|
+
normalizeRepo,
|
|
32
|
+
releaseInstallerUrls,
|
|
33
|
+
viaMirror,
|
|
34
|
+
} from './github-release.js'
|
|
29
35
|
|
|
30
36
|
/** Upstream source repository used when no local checkout exists. */
|
|
31
37
|
export const MILOCO_REPO = 'https://github.com/XiaoMi/xiaomi-miloco'
|
|
@@ -48,6 +54,46 @@ function expandHome(value) {
|
|
|
48
54
|
return text
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Mask credentials in command logs.
|
|
59
|
+
* @param {string[]} args - Command arguments.
|
|
60
|
+
* @returns {string[]} Arguments safe to log.
|
|
61
|
+
*/
|
|
62
|
+
export function redactCommandArgs(args) {
|
|
63
|
+
const out = [...args]
|
|
64
|
+
const secretFlags = new Set(['agent.auth_bearer', 'auth_bearer', '--token', '--bearer'])
|
|
65
|
+
for (let index = 0; index < out.length - 1; index += 1) {
|
|
66
|
+
if (secretFlags.has(out[index])) out[index + 1] = '<redacted>'
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Force release-installer arguments.
|
|
73
|
+
*
|
|
74
|
+
* The old source flow used `--dev`; a release installer rejects it. OpenClaw is
|
|
75
|
+
* used only to satisfy upstream platform detection, while `--skip-openclaw`
|
|
76
|
+
* prevents its plugin from being installed.
|
|
77
|
+
* @param {string[]} [raw] - Configured arguments.
|
|
78
|
+
* @returns {string[]} Safe release-installer arguments.
|
|
79
|
+
*/
|
|
80
|
+
function normalizeInstallArgs(raw) {
|
|
81
|
+
const args = Array.isArray(raw) ? raw.map(String) : []
|
|
82
|
+
const filtered = args.filter(arg =>
|
|
83
|
+
arg !== '--dev'
|
|
84
|
+
&& !arg.startsWith('--dev=')
|
|
85
|
+
&& !arg.startsWith('--agent-prepare')
|
|
86
|
+
&& !arg.startsWith('--agent-finish'))
|
|
87
|
+
if (!filtered.includes('--skip-openclaw')) filtered.push('--skip-openclaw')
|
|
88
|
+
if (!filtered.some(arg => arg === '--agent-platform' || arg.startsWith('--agent-platform='))) {
|
|
89
|
+
filtered.push('--agent-platform=openclaw')
|
|
90
|
+
}
|
|
91
|
+
if (!filtered.some(arg => arg === '--lang' || arg.startsWith('--lang='))) {
|
|
92
|
+
filtered.push('--lang=zh')
|
|
93
|
+
}
|
|
94
|
+
return filtered
|
|
95
|
+
}
|
|
96
|
+
|
|
51
97
|
/**
|
|
52
98
|
* Create a directory and prove the DSH process can write to it.
|
|
53
99
|
* @param {string} dir - Directory path.
|
|
@@ -85,22 +131,45 @@ export function createDeployService(options) {
|
|
|
85
131
|
)
|
|
86
132
|
const milocoHome = expandHome(config.home || '~/.miloco')
|
|
87
133
|
const repo = String(deployConfig.repo || MILOCO_REPO)
|
|
88
|
-
const
|
|
89
|
-
? deployConfig.
|
|
90
|
-
:
|
|
134
|
+
const githubMirrors = Array.isArray(deployConfig.githubMirrors) && deployConfig.githubMirrors.length
|
|
135
|
+
? deployConfig.githubMirrors.map(String)
|
|
136
|
+
: DEFAULT_GITHUB_MIRRORS
|
|
137
|
+
const installArgs = normalizeInstallArgs(deployConfig.installArgs)
|
|
138
|
+
const repoUrl = `https://github.com/${normalizeRepo(repo)}`
|
|
139
|
+
const downloadSite = String(deployConfig.downloadSite || '').trim()
|
|
140
|
+
|| (githubMirrors[0] ? viaMirror(githubMirrors[0], `${repoUrl}/releases/download`) : '')
|
|
141
|
+
const installEnv = {
|
|
142
|
+
UV_CACHE_DIR: join(milocoHome, '.uv-cache'),
|
|
143
|
+
UV_INDEX: String(deployConfig.uvIndex || 'https://pypi.org/simple'),
|
|
144
|
+
...(downloadSite ? { MILOCO_DOWNLOAD_URL: downloadSite } : {}),
|
|
145
|
+
}
|
|
91
146
|
const logPath = expandHome(deployConfig.logPath || join(milocoHome, 'log', 'dsh-miloco-deploy.log'))
|
|
147
|
+
const upgradeLogPath = expandHome(
|
|
148
|
+
deployConfig.upgradeLogPath || join(milocoHome, 'log', 'dsh-miloco-upgrade.log'),
|
|
149
|
+
)
|
|
92
150
|
|
|
93
151
|
/** @type {{running: boolean, step: string, startedAt: string | null, exitCode: number | null}} */
|
|
94
152
|
const state = { running: false, step: 'idle', startedAt: null, exitCode: null }
|
|
153
|
+
/** @type {{running: boolean, step: string, startedAt: string | null, exitCode: number | null}} */
|
|
154
|
+
const upgradeState = { running: false, step: 'idle', startedAt: null, exitCode: null }
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Write a line into a log file.
|
|
158
|
+
* @param {string} path - Log path.
|
|
159
|
+
* @param {string} text - The line.
|
|
160
|
+
*/
|
|
161
|
+
async function logTo(path, text) {
|
|
162
|
+
const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19)
|
|
163
|
+
await mkdir(dirname(path), { recursive: true })
|
|
164
|
+
await writeFile(path, `[${stamp}] ${text}\n`, { flag: 'a' })
|
|
165
|
+
}
|
|
95
166
|
|
|
96
167
|
/**
|
|
97
168
|
* Write a line into the deployment log.
|
|
98
169
|
* @param {string} text - The line.
|
|
99
170
|
*/
|
|
100
171
|
async function logLine(text) {
|
|
101
|
-
|
|
102
|
-
await mkdir(dirname(logPath), { recursive: true })
|
|
103
|
-
await writeFile(logPath, `[${stamp}] ${text}\n`, { flag: 'a' })
|
|
172
|
+
return logTo(logPath, text)
|
|
104
173
|
}
|
|
105
174
|
|
|
106
175
|
/**
|
|
@@ -112,16 +181,27 @@ export function createDeployService(options) {
|
|
|
112
181
|
*/
|
|
113
182
|
function run(command, args, extra = {}) {
|
|
114
183
|
return new Promise(resolve => {
|
|
115
|
-
|
|
184
|
+
const secrets = (extra.secrets ?? []).filter(Boolean)
|
|
185
|
+
const sanitize = value => secrets.reduce(
|
|
186
|
+
(text, secret) => text.split(secret).join('<redacted>'),
|
|
187
|
+
String(value),
|
|
188
|
+
)
|
|
189
|
+
const commandLog = extra.logPath || logPath
|
|
190
|
+
void logTo(commandLog, `$ ${command} ${redactCommandArgs(args).join(' ')}`)
|
|
116
191
|
const child = spawn(command, args, {
|
|
117
192
|
cwd: extra.cwd,
|
|
118
|
-
env: {
|
|
193
|
+
env: {
|
|
194
|
+
...process.env,
|
|
195
|
+
PATH: [join(homedir(), '.local', 'bin'), process.env.PATH].filter(Boolean).join(':'),
|
|
196
|
+
MILOCO_HOME: milocoHome,
|
|
197
|
+
...(extra.env ?? {}),
|
|
198
|
+
},
|
|
119
199
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
200
|
})
|
|
121
201
|
const pump = stream => {
|
|
122
202
|
stream.on('data', chunk => {
|
|
123
|
-
for (const line of
|
|
124
|
-
if (line.trim() !== '') void
|
|
203
|
+
for (const line of sanitize(chunk).split('\n')) {
|
|
204
|
+
if (line.trim() !== '') void logTo(commandLog, line.trim())
|
|
125
205
|
}
|
|
126
206
|
})
|
|
127
207
|
}
|
|
@@ -191,6 +271,37 @@ export function createDeployService(options) {
|
|
|
191
271
|
return false
|
|
192
272
|
}
|
|
193
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Download the self-contained official release installer.
|
|
276
|
+
* @returns {Promise<string>} Path to the installer script.
|
|
277
|
+
*/
|
|
278
|
+
async function downloadReleaseInstaller(logTarget = logPath) {
|
|
279
|
+
const target = join(milocoHome, '.dsh-release-install.sh')
|
|
280
|
+
const urls = releaseInstallerUrls({ repo, mirrors: githubMirrors })
|
|
281
|
+
const failures = []
|
|
282
|
+
for (const url of urls) {
|
|
283
|
+
await logTo(logTarget, `downloading release installer: ${url}`)
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(url, {
|
|
286
|
+
headers: { 'user-agent': 'dsh-miloco-bridge' },
|
|
287
|
+
redirect: 'follow',
|
|
288
|
+
signal: AbortSignal.timeout(30_000),
|
|
289
|
+
})
|
|
290
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
291
|
+
const text = await response.text()
|
|
292
|
+
if (!text.startsWith('#!')) throw new Error('响应不是 shell 安装脚本')
|
|
293
|
+
await writeFile(target, text, { encoding: 'utf8', mode: 0o700 })
|
|
294
|
+
await chmod(target, 0o700)
|
|
295
|
+
return target
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
298
|
+
failures.push(`${url}: ${message}`)
|
|
299
|
+
await logTo(logTarget, `download failed: ${message}`)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`无法下载 Miloco release 安装器:${failures.join('; ')}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
194
305
|
/**
|
|
195
306
|
* Resolve the effective Miloco URL/token from `$MILOCO_HOME/config.json`.
|
|
196
307
|
* @returns {Promise<{baseUrl: string, token: string}>} Connection settings.
|
|
@@ -244,10 +355,13 @@ export function createDeployService(options) {
|
|
|
244
355
|
async function wireMiloco(opts) {
|
|
245
356
|
const webhook = `${opts.origin}/miloco/webhook`
|
|
246
357
|
if (opts.cli) {
|
|
247
|
-
await run(opts.cli, ['config', 'set', 'agent.platform', 'dsh'], { cwd:
|
|
248
|
-
await run(opts.cli, ['config', 'set', 'agent.webhook_url', webhook], { cwd:
|
|
249
|
-
await run(opts.cli, ['config', 'set', 'agent.auth_bearer', opts.token], {
|
|
250
|
-
|
|
358
|
+
await run(opts.cli, ['config', 'set', 'agent.platform', 'dsh'], { cwd: milocoHome })
|
|
359
|
+
await run(opts.cli, ['config', 'set', 'agent.webhook_url', webhook], { cwd: milocoHome })
|
|
360
|
+
await run(opts.cli, ['config', 'set', 'agent.auth_bearer', opts.token], {
|
|
361
|
+
cwd: milocoHome,
|
|
362
|
+
secrets: [opts.token],
|
|
363
|
+
})
|
|
364
|
+
await run(opts.cli, ['service', 'restart'], { cwd: milocoHome })
|
|
251
365
|
}
|
|
252
366
|
if (options.overlay) {
|
|
253
367
|
const current = await options.overlay.current()
|
|
@@ -279,7 +393,6 @@ export function createDeployService(options) {
|
|
|
279
393
|
void (async () => {
|
|
280
394
|
try {
|
|
281
395
|
await ensureWritableDir(milocoHome, 'MILOCO_HOME')
|
|
282
|
-
await ensureWritableDir(dirname(sourceDir), 'deploy.sourceDir 的父目录')
|
|
283
396
|
await logLine('=== dsh-miloco-bridge: 一键部署 Miloco 开始 ===')
|
|
284
397
|
|
|
285
398
|
if (await healthy()) {
|
|
@@ -290,28 +403,28 @@ export function createDeployService(options) {
|
|
|
290
403
|
if (cli && existsSync(join(milocoHome, 'config.json'))) {
|
|
291
404
|
state.step = 'starting-existing'
|
|
292
405
|
await logLine(`检测到已安装的 Miloco,尝试启动:${cli}`)
|
|
293
|
-
await run(cli, ['service', 'start'], { cwd:
|
|
406
|
+
await run(cli, ['service', 'start'], { cwd: milocoHome })
|
|
294
407
|
await waitHealthy(DEFAULT_MILOCO_URL, 45_000)
|
|
295
408
|
}
|
|
296
409
|
|
|
297
410
|
if (!await healthy()) {
|
|
298
|
-
state.step = '
|
|
299
|
-
|
|
300
|
-
if (!/^https:\/\//i.test(repo)) {
|
|
301
|
-
throw new Error('deploy.repo 必须是 https:// 地址')
|
|
302
|
-
}
|
|
303
|
-
await mkdir(dirname(sourceDir), { recursive: true })
|
|
304
|
-
await logLine(`cloning ${repo} -> ${sourceDir}`)
|
|
305
|
-
const clone = await run('git', ['clone', '--depth', '1', repo, sourceDir], { cwd: homedir() })
|
|
306
|
-
if (clone !== 0) throw new Error(`git clone 失败(exit ${clone})`)
|
|
307
|
-
}
|
|
411
|
+
state.step = 'downloading-installer'
|
|
412
|
+
const installer = await downloadReleaseInstaller()
|
|
308
413
|
state.step = 'installing'
|
|
309
|
-
const
|
|
310
|
-
|
|
414
|
+
const prepare = await run('bash', [installer, '--agent-prepare', ...installArgs], {
|
|
415
|
+
cwd: milocoHome,
|
|
416
|
+
env: installEnv,
|
|
417
|
+
})
|
|
418
|
+
if (prepare !== 0) throw new Error(`Miloco release 安装器 prepare 返回 ${prepare}`)
|
|
419
|
+
const finish = await run('bash', [installer, '--agent-finish', ...installArgs], {
|
|
420
|
+
cwd: milocoHome,
|
|
421
|
+
env: installEnv,
|
|
422
|
+
})
|
|
423
|
+
if (finish !== 0) throw new Error(`Miloco release 安装器 finish 返回 ${finish}`)
|
|
311
424
|
state.step = 'starting-service'
|
|
312
425
|
const cliAfter = await findMilocoCli()
|
|
313
|
-
if (cliAfter) await run(cliAfter, ['service', 'start'], { cwd:
|
|
314
|
-
if (!await waitHealthy(DEFAULT_MILOCO_URL,
|
|
426
|
+
if (cliAfter) await run(cliAfter, ['service', 'start'], { cwd: milocoHome })
|
|
427
|
+
if (!await waitHealthy(DEFAULT_MILOCO_URL, 120_000)) {
|
|
315
428
|
throw new Error('Miloco 安装完成但没有在 127.0.0.1:1810 上响应')
|
|
316
429
|
}
|
|
317
430
|
}
|
|
@@ -346,6 +459,88 @@ export function createDeployService(options) {
|
|
|
346
459
|
return { started: true, logPath }
|
|
347
460
|
}
|
|
348
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Run a mirror-backed upgrade through the same release installer as deploy.
|
|
464
|
+
* @param {{origin?: string}} [opts] - Request origin.
|
|
465
|
+
* @returns {Promise<{started: boolean, reason?: string, logPath?: string}>} Outcome.
|
|
466
|
+
*/
|
|
467
|
+
async function startUpgrade(opts = {}) {
|
|
468
|
+
if (upgradeState.running) {
|
|
469
|
+
return { started: false, reason: 'already-running', logPath: upgradeLogPath }
|
|
470
|
+
}
|
|
471
|
+
if (state.running) {
|
|
472
|
+
return { started: false, reason: 'deploy-running', logPath: upgradeLogPath }
|
|
473
|
+
}
|
|
474
|
+
upgradeState.running = true
|
|
475
|
+
upgradeState.step = 'starting'
|
|
476
|
+
upgradeState.startedAt = new Date().toISOString()
|
|
477
|
+
upgradeState.exitCode = null
|
|
478
|
+
const origin = String(opts.origin || '').replace(/\/+$/, '') || 'http://127.0.0.1:3080'
|
|
479
|
+
|
|
480
|
+
void (async () => {
|
|
481
|
+
try {
|
|
482
|
+
await ensureWritableDir(milocoHome, 'MILOCO_HOME')
|
|
483
|
+
await logTo(upgradeLogPath, '=== Miloco 升级开始 ===')
|
|
484
|
+
upgradeState.step = 'downloading-installer'
|
|
485
|
+
const installer = await downloadReleaseInstaller(upgradeLogPath)
|
|
486
|
+
upgradeState.step = 'installing'
|
|
487
|
+
const installOpts = { cwd: milocoHome, env: installEnv, logPath: upgradeLogPath }
|
|
488
|
+
const prepare = await run('bash', [installer, '--agent-prepare', ...installArgs], installOpts)
|
|
489
|
+
if (prepare !== 0) throw new Error(`Miloco release 安装器 prepare 返回 ${prepare}`)
|
|
490
|
+
const finish = await run('bash', [installer, '--agent-finish', ...installArgs], installOpts)
|
|
491
|
+
if (finish !== 0) throw new Error(`Miloco release 安装器 finish 返回 ${finish}`)
|
|
492
|
+
upgradeState.step = 'starting-service'
|
|
493
|
+
const cli = await findMilocoCli()
|
|
494
|
+
if (cli) {
|
|
495
|
+
await run(cli, ['service', 'start'], { cwd: milocoHome, logPath: upgradeLogPath })
|
|
496
|
+
}
|
|
497
|
+
if (!await waitHealthy(DEFAULT_MILOCO_URL, 120_000)) {
|
|
498
|
+
throw new Error('Miloco 升级后没有在 127.0.0.1:1810 上响应')
|
|
499
|
+
}
|
|
500
|
+
upgradeState.step = 'wiring'
|
|
501
|
+
const connection = await localConnection()
|
|
502
|
+
const fileConfig = await readMilocoConfig(milocoHome)
|
|
503
|
+
const existingToken = String(fileConfig?.agent?.auth_bearer ?? '').trim()
|
|
504
|
+
const pushToken = existingToken || randomBytes(32).toString('base64url')
|
|
505
|
+
await installAdapter({ origin, token: pushToken })
|
|
506
|
+
await wireMiloco({ cli, origin, token: pushToken, connection })
|
|
507
|
+
upgradeState.step = 'done'
|
|
508
|
+
upgradeState.exitCode = 0
|
|
509
|
+
await logTo(upgradeLogPath, '=== 升级完成 ===')
|
|
510
|
+
} catch (error) {
|
|
511
|
+
upgradeState.step = 'failed'
|
|
512
|
+
upgradeState.exitCode = 1
|
|
513
|
+
await logTo(upgradeLogPath, `升级异常:${error instanceof Error ? error.message : String(error)}`)
|
|
514
|
+
} finally {
|
|
515
|
+
upgradeState.running = false
|
|
516
|
+
}
|
|
517
|
+
})()
|
|
518
|
+
|
|
519
|
+
return { started: true, logPath: upgradeLogPath }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @returns {any} Upgrade state for the settings API.
|
|
524
|
+
*/
|
|
525
|
+
function upgradeStateView() {
|
|
526
|
+
return { ...upgradeState, logPath: upgradeLogPath, milocoHome }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Read the tail of the upgrade log.
|
|
531
|
+
* @param {number} [lines] - Number of trailing lines.
|
|
532
|
+
* @returns {Promise<string>} The log tail.
|
|
533
|
+
*/
|
|
534
|
+
async function upgradeTail(lines = 60) {
|
|
535
|
+
try {
|
|
536
|
+
const text = await readFile(upgradeLogPath, 'utf8')
|
|
537
|
+
const all = text.split('\n').filter(line => line.trim() !== '')
|
|
538
|
+
return all.slice(-lines).join('\n')
|
|
539
|
+
} catch {
|
|
540
|
+
return ''
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
349
544
|
/**
|
|
350
545
|
* Read the tail of the deployment log.
|
|
351
546
|
* @param {number} [lines] - Number of trailing lines.
|
|
@@ -371,5 +566,8 @@ export function createDeployService(options) {
|
|
|
371
566
|
}),
|
|
372
567
|
tail,
|
|
373
568
|
start,
|
|
569
|
+
upgradeState: upgradeStateView,
|
|
570
|
+
upgradeTail,
|
|
571
|
+
startUpgrade,
|
|
374
572
|
}
|
|
375
573
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub release lookup with mirror fallback.
|
|
3
|
+
*
|
|
4
|
+
* Miloco only publishes on GitHub. Direct GitHub access is preferred; when it
|
|
5
|
+
* is unavailable, the same API/installer URLs are requested through public
|
|
6
|
+
* pull-through mirrors.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-miloco-bridge/github-release
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_GITHUB_REPO = 'XiaoMi/xiaomi-miloco'
|
|
12
|
+
export const DEFAULT_GITHUB_API = 'https://api.github.com'
|
|
13
|
+
export const DEFAULT_GITHUB_MIRRORS = [
|
|
14
|
+
'https://gh-proxy.com/',
|
|
15
|
+
'https://gh-proxy.org/',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalise an owner/repo or GitHub URL to `owner/repo`.
|
|
20
|
+
* @param {string} [repo] - Repository URL or slug.
|
|
21
|
+
* @returns {string} Repository slug.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeRepo(repo) {
|
|
24
|
+
const text = String(repo ?? '').trim().replace(/\/+$/, '').replace(/\.git$/i, '')
|
|
25
|
+
const match = /^(?:https?:\/\/github\.com\/)?([^/\s]+\/[^/\s]+)$/i.exec(text)
|
|
26
|
+
return match ? match[1] : DEFAULT_GITHUB_REPO
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prefix a URL with a mirror, or expand a `{url}` template.
|
|
31
|
+
* @param {string} mirror - Mirror base or template.
|
|
32
|
+
* @param {string} url - Original URL.
|
|
33
|
+
* @returns {string} Mirrored URL.
|
|
34
|
+
*/
|
|
35
|
+
export function viaMirror(mirror, url) {
|
|
36
|
+
const base = String(mirror ?? '').trim()
|
|
37
|
+
if (!base) return url
|
|
38
|
+
if (base.includes('{url}')) return base.replaceAll('{url}', url)
|
|
39
|
+
return `${base.replace(/\/+$/, '')}/${url}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build direct + mirrored URLs for the latest-release API.
|
|
44
|
+
* @param {{repo?: string, apiBase?: string, mirrors?: string[]}} [opts] - Options.
|
|
45
|
+
* @returns {string[]} Candidate URLs, direct first.
|
|
46
|
+
*/
|
|
47
|
+
export function releaseApiUrls(opts = {}) {
|
|
48
|
+
const repo = normalizeRepo(opts.repo)
|
|
49
|
+
const apiBase = String(opts.apiBase || DEFAULT_GITHUB_API).replace(/\/+$/, '')
|
|
50
|
+
const direct = `${apiBase}/repos/${repo}/releases/latest`
|
|
51
|
+
const mirrors = (opts.mirrors ?? DEFAULT_GITHUB_MIRRORS).map(mirror => viaMirror(mirror, direct))
|
|
52
|
+
return [...new Set([direct, ...mirrors])]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build direct + mirrored URLs for the release installer script.
|
|
57
|
+
* @param {{repo?: string, mirrors?: string[]}} [opts] - Options.
|
|
58
|
+
* @returns {string[]} Candidate URLs, direct first.
|
|
59
|
+
*/
|
|
60
|
+
export function releaseInstallerUrls(opts = {}) {
|
|
61
|
+
const repo = normalizeRepo(opts.repo)
|
|
62
|
+
const direct = `https://github.com/${repo}/releases/latest/download/install.sh`
|
|
63
|
+
const mirrors = (opts.mirrors ?? DEFAULT_GITHUB_MIRRORS).map(mirror => viaMirror(mirror, direct))
|
|
64
|
+
return [...new Set([direct, ...mirrors])]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch the latest release, trying direct GitHub then mirrors.
|
|
69
|
+
* @param {{repo?: string, apiBase?: string, mirrors?: string[], timeoutMs?: number, fetchImpl?: typeof fetch}} [opts] - Options.
|
|
70
|
+
* @returns {Promise<{tag: string, html_url: string, source: 'github'|'mirror', mirror: string|null, release_url: string, request_url: string}>} Latest release.
|
|
71
|
+
*/
|
|
72
|
+
export async function fetchLatestRelease(opts = {}) {
|
|
73
|
+
const doFetch = opts.fetchImpl ?? fetch
|
|
74
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 6_000
|
|
75
|
+
const urls = releaseApiUrls(opts)
|
|
76
|
+
const errors = []
|
|
77
|
+
for (const [index, url] of urls.entries()) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await doFetch(url, {
|
|
80
|
+
headers: {
|
|
81
|
+
accept: 'application/vnd.github+json',
|
|
82
|
+
'user-agent': 'dsh-miloco-bridge',
|
|
83
|
+
},
|
|
84
|
+
redirect: 'follow',
|
|
85
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
86
|
+
})
|
|
87
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
88
|
+
const body = await response.json()
|
|
89
|
+
const tag = String(body?.tag_name ?? '').trim()
|
|
90
|
+
if (!tag) throw new Error('响应缺少 tag_name')
|
|
91
|
+
const htmlUrl = String(body?.html_url ?? '').trim()
|
|
92
|
+
const mirrored = index > 0
|
|
93
|
+
const mirror = mirrored ? (opts.mirrors ?? DEFAULT_GITHUB_MIRRORS)[index - 1] : null
|
|
94
|
+
return {
|
|
95
|
+
tag,
|
|
96
|
+
html_url: htmlUrl,
|
|
97
|
+
source: mirrored ? 'mirror' : 'github',
|
|
98
|
+
mirror,
|
|
99
|
+
release_url: mirrored && htmlUrl ? viaMirror(mirror, htmlUrl) : htmlUrl,
|
|
100
|
+
request_url: url,
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`最新版本查询失败:${errors.join('; ')}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a CalVer/PEP440-style release string.
|
|
111
|
+
* @param {string} value - Version string.
|
|
112
|
+
* @returns {{parts: number[], post: number, dev: number|null}|null} Parsed version.
|
|
113
|
+
*/
|
|
114
|
+
export function parseReleaseVersion(value) {
|
|
115
|
+
const text = String(value ?? '').trim().replace(/^v/i, '').split('+')[0]
|
|
116
|
+
const match = /^(\d+(?:\.\d+)*)(?:\.post(\d+))?(?:\.dev(\d+))?$/.exec(text)
|
|
117
|
+
if (!match) return null
|
|
118
|
+
return {
|
|
119
|
+
parts: match[1].split('.').map(Number),
|
|
120
|
+
post: Number(match[2] ?? 0),
|
|
121
|
+
dev: match[3] === undefined ? null : Number(match[3]),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalise a release tag for display.
|
|
127
|
+
* @param {string} value - Tag or version.
|
|
128
|
+
* @returns {string} Version without a leading `v`.
|
|
129
|
+
*/
|
|
130
|
+
export function normalizeReleaseVersion(value) {
|
|
131
|
+
return String(value ?? '').trim().replace(/^v/i, '')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether `latest` is strictly newer than `current`.
|
|
136
|
+
* Dev builds and unparseable versions are treated conservatively as "no update".
|
|
137
|
+
* @param {string} current - Current version.
|
|
138
|
+
* @param {string} latest - Latest release tag.
|
|
139
|
+
* @returns {boolean} Whether an update is available.
|
|
140
|
+
*/
|
|
141
|
+
export function isNewerRelease(current, latest) {
|
|
142
|
+
const left = parseReleaseVersion(current)
|
|
143
|
+
const right = parseReleaseVersion(latest)
|
|
144
|
+
if (!left || !right) return false
|
|
145
|
+
if (left.dev !== null) return false
|
|
146
|
+
const length = Math.max(left.parts.length, right.parts.length)
|
|
147
|
+
for (let index = 0; index < length; index += 1) {
|
|
148
|
+
const a = left.parts[index] ?? 0
|
|
149
|
+
const b = right.parts[index] ?? 0
|
|
150
|
+
if (b !== a) return b > a
|
|
151
|
+
}
|
|
152
|
+
return right.post > left.post
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Infer release/dev deployment kind from a package version.
|
|
157
|
+
* @param {string} version - Package version.
|
|
158
|
+
* @returns {'release'|'dev'} Deployment kind.
|
|
159
|
+
*/
|
|
160
|
+
export function inferDeployKind(version) {
|
|
161
|
+
return /\.dev\d*|\+/.test(String(version ?? '')) ? 'dev' : 'release'
|
|
162
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -96,7 +96,17 @@ export function normalizeConfig(raw) {
|
|
|
96
96
|
: join(resolveDshHome(), 'plugin-data', 'dsh-miloco-bridge', 'xiaomi-miloco'),
|
|
97
97
|
installArgs: Array.isArray(deployRaw.installArgs)
|
|
98
98
|
? deployRaw.installArgs.map(String)
|
|
99
|
-
: ['--
|
|
99
|
+
: ['--skip-openclaw', '--agent-platform=openclaw', '--lang=zh'],
|
|
100
|
+
githubMirrors: Array.isArray(deployRaw.githubMirrors)
|
|
101
|
+
? deployRaw.githubMirrors.map(String).filter(Boolean)
|
|
102
|
+
: ['https://gh-proxy.com/', 'https://gh-proxy.org/'],
|
|
103
|
+
githubApi: typeof deployRaw.githubApi === 'string' && deployRaw.githubApi
|
|
104
|
+
? deployRaw.githubApi
|
|
105
|
+
: 'https://api.github.com',
|
|
106
|
+
downloadSite: typeof deployRaw.downloadSite === 'string' ? deployRaw.downloadSite : '',
|
|
107
|
+
uvIndex: typeof deployRaw.uvIndex === 'string' && deployRaw.uvIndex
|
|
108
|
+
? deployRaw.uvIndex
|
|
109
|
+
: 'https://pypi.org/simple',
|
|
100
110
|
logPath: typeof deployRaw.logPath === 'string' ? deployRaw.logPath : '',
|
|
101
111
|
},
|
|
102
112
|
}
|