dsh-deepmoyu 2.3.0 → 2.4.1
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/AGENTS.md +91 -153
- package/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +51 -0
- package/CONTRIBUTORS.md +3 -7
- package/NOTICE.md +3 -3
- package/README.md +141 -81
- package/SECURITY.md +21 -0
- package/assets/screenshots/memory.png +0 -0
- package/assets/screenshots/menu.png +0 -0
- package/assets/screenshots/workspace.png +0 -0
- package/client.js +1348 -441
- package/cordis.patch.yml +1 -1
- package/docs/ASSETS.md +25 -0
- package/docs/DEPLOYMENT.md +102 -0
- package/docs/PRIVACY.md +32 -0
- package/docs/RELEASE_AUDIT.md +70 -0
- package/docs/RELEASING.md +94 -0
- package/docs/TEST_MATRIX.md +77 -0
- package/docs/assets-permissions.json +20 -0
- package/docs/tool-evidence.json +196 -0
- package/index.js +953 -270
- package/lib/asr.py +41 -35
- package/lib/background-output.mjs +64 -17
- package/lib/background-tools.mjs +1 -0
- package/lib/companion.mjs +43 -0
- package/lib/config-policy.mjs +20 -0
- package/lib/favops.py +9 -2
- package/lib/ids.mjs +41 -0
- package/lib/login-session.mjs +34 -0
- package/lib/login.mjs +5 -4
- package/lib/mcp.mjs +3 -2
- package/lib/memory-center.mjs +74 -0
- package/lib/mode.mjs +6 -7
- package/lib/model-relay.mjs +5 -4
- package/lib/open-directory.mjs +93 -0
- package/lib/pace.mjs +92 -12
- package/lib/persona.mjs +46 -0
- package/lib/product-status.mjs +123 -0
- package/lib/prompts.mjs +40 -5
- package/lib/pv-production.mjs +280 -0
- package/lib/pv-session.mjs +316 -0
- package/lib/pv.mjs +398 -306
- package/lib/pv.py +84 -22
- package/lib/pv_cover.py +108 -26
- package/lib/pv_editorial.py +336 -0
- package/lib/pv_verify.py +267 -0
- package/lib/python-env.mjs +61 -0
- package/lib/reply.mjs +10 -3
- package/lib/run-settings.mjs +6 -0
- package/lib/sticker-automation.mjs +5 -4
- package/lib/sticker.mjs +135 -25
- package/lib/tool-scope.mjs +65 -0
- package/lib/uvfetch.mjs +165 -153
- package/lib/verify.mjs +111 -127
- package/marketplace/pyyxzi__dsh-deepmoyu.yml +7 -0
- package/package.json +23 -12
- package/presets/deepmoyu.patch.yml +1 -1
- package/prompts/duty.md +4 -3
- package/screenshots.json +5 -0
- package/selfcheck.mjs +21 -6
- package/vendor/bilibili-mcp/requirements.in +9 -0
- package/vendor/bilibili-mcp/requirements.txt +1936 -11
- package/vendor/bilibili-mcp/runtime_guard.py +35 -10
- package/lib/__pycache__/pv.cpython-310.pyc +0 -0
- package/lib/__pycache__/pv_cover.cpython-310.pyc +0 -0
- package/tests/automation-routes.test.mjs +0 -183
- package/tests/automation.test.mjs +0 -79
- package/tests/background-tools.test.mjs +0 -182
- package/tests/background-window.test.mjs +0 -75
- package/tests/dup-cross-channel.test.mjs +0 -158
- package/tests/manual-browsing.test.mjs +0 -671
- package/tests/mcp-restart.test.mjs +0 -131
- package/tests/memory-chain.test.mjs +0 -413
- package/tests/model-relay.test.mjs +0 -116
- package/tests/persona.test.mjs +0 -67
- package/tests/pet-run-routes.test.mjs +0 -127
- package/tests/pet-run-settings.test.mjs +0 -38
- package/tests/pet-session.test.mjs +0 -125
- package/tests/pv-stage1.test.mjs +0 -203
- package/tests/sticker-routes.test.mjs +0 -141
- package/tests/sticker-upload-routes.test.mjs +0 -97
- package/tests/sticker-upload.test.mjs +0 -122
- package/tests/sticker.test.mjs +0 -169
- package/tests/test_guard_cross_channel.py +0 -141
- package/tests/test_pv_cover.py +0 -95
- package/tests/test_pv_stage1.py +0 -250
- package/tests/test_reply_pictures.py +0 -255
- package/tests/token-ledger.test.mjs +0 -78
- package/tests/verify_pv_sample.py +0 -91
package/lib/uvfetch.mjs
CHANGED
|
@@ -1,179 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* 两个源,按顺序试:
|
|
9
|
-
* ① **GitHub 官方 release**(`uv-x86_64-pc-windows-msvc.zip`)—— 有加速器时最快 ✓
|
|
10
|
-
* ② ⭐ **PyPI 清华镜像的 uv wheel** —— wheel 本身就是个 zip,里面装着 `uv.exe` ✓
|
|
11
|
-
* (国内速度快、不依赖 GitHub ✓)
|
|
12
|
-
*
|
|
13
|
-
* 解压:优先用 **系统自带的 `tar`**(Windows 10+ 的 bsdtar 能直接读 zip ✓),
|
|
14
|
-
* 再退到 `unzip`;然后**在解压出来的整棵树里按文件名找** ✓
|
|
15
|
-
* —— 不管官方把它放在哪一层都能找着 ✓
|
|
16
|
-
* ============================================================================
|
|
17
|
-
*/
|
|
18
|
-
import { spawnSync } from 'node:child_process'
|
|
19
|
-
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, chmodSync, rmSync, readFileSync, renameSync } from 'node:fs'
|
|
20
|
-
import { createHash } from 'node:crypto'
|
|
21
|
-
import { join } from 'node:path'
|
|
22
|
-
import { Readable } from 'node:stream'
|
|
23
|
-
import { pipeline } from 'node:stream/promises'
|
|
1
|
+
/** Verified, bounded uv bootstrap. User data and install artifacts stay outside the package. */
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, copyFileSync, chmodSync, rmSync, readFileSync, renameSync } from 'node:fs'
|
|
4
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { open, unlink } from 'node:fs/promises'
|
|
24
7
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
34
|
-
}
|
|
8
|
+
const inflight = new Map()
|
|
9
|
+
const MAX_ARCHIVE = 192 * 1024 * 1024
|
|
10
|
+
const SHA = /^[0-9a-f]{64}$/i
|
|
11
|
+
const nativeRun = (command, args, options = {}) => new Promise(resolveRun => {
|
|
12
|
+
execFile(command, args, { timeout: 15_000, maxBuffer: 1024 * 1024, windowsHide: true, ...options }, error => {
|
|
13
|
+
resolveRun({ status: error ? error.code || 1 : 0, error })
|
|
14
|
+
})
|
|
15
|
+
})
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async function expectedSha(source, officialUrl) {
|
|
44
|
-
if (source.sha) return String(source.sha).toLowerCase()
|
|
45
|
-
try {
|
|
46
|
-
const r = await fetch(String(officialUrl || source.url) + '.sha256')
|
|
47
|
-
if (!r.ok) return null
|
|
48
|
-
const m = /([0-9a-f]{64})/i.exec(await r.text())
|
|
49
|
-
return m ? m[1].toLowerCase() : null
|
|
50
|
-
} catch { return null }
|
|
17
|
+
export function uvAssetFor(platform = process.platform, arch = process.arch, libc = 'gnu') {
|
|
18
|
+
const cpu = { x64: 'x86_64', arm64: 'aarch64', ia32: 'i686' }[arch]
|
|
19
|
+
if (!cpu) return null
|
|
20
|
+
if (platform === 'win32') return { name: `uv-${cpu}-pc-windows-msvc.zip`, bin: 'uv.exe', wheel: { x64: 'win_amd64', arm64: 'win_arm64', ia32: 'win32' }[arch] }
|
|
21
|
+
if (platform === 'darwin' && arch !== 'ia32') return { name: `uv-${cpu}-apple-darwin.tar.gz`, bin: 'uv' }
|
|
22
|
+
if (platform === 'linux') return { name: `uv-${cpu}-unknown-linux-${libc === 'musl' ? 'musl' : 'gnu'}.tar.gz`, bin: 'uv' }
|
|
23
|
+
return null
|
|
51
24
|
}
|
|
52
25
|
|
|
53
|
-
/** 在目录树里按文件名找可执行文件(递归) */
|
|
54
26
|
export function findBin(name, dir) {
|
|
55
|
-
let entries
|
|
27
|
+
let entries
|
|
56
28
|
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return null }
|
|
57
|
-
for (const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.isSymbolicLink()) continue
|
|
31
|
+
const path = join(dir, entry.name)
|
|
32
|
+
if (entry.isDirectory()) { const hit = findBin(name, path); if (hit) return hit }
|
|
33
|
+
else if (entry.isFile() && entry.name === name) return path
|
|
61
34
|
}
|
|
62
35
|
return null
|
|
63
36
|
}
|
|
64
37
|
|
|
65
|
-
|
|
66
|
-
export function extractArchive(binName, archive, into) {
|
|
38
|
+
export async function extractArchive(name, archive, into, run = nativeRun) {
|
|
67
39
|
const attempts = [['tar', ['-xf', archive, '-C', into]], ['unzip', ['-o', archive, '-d', into]]]
|
|
68
|
-
for (const [
|
|
40
|
+
for (const [command, args] of attempts) {
|
|
69
41
|
try {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
} catch { /*
|
|
42
|
+
const result = await run(command, args, { timeout: 90_000 })
|
|
43
|
+
if (result.status === 0) { const hit = findBin(name, into); if (hit) return hit }
|
|
44
|
+
} catch { /* An unavailable extractor must not hide the next supported one. */ }
|
|
73
45
|
}
|
|
74
46
|
return null
|
|
75
47
|
}
|
|
76
48
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
49
|
+
/** The deadline covers headers and the whole streamed response, including slow bodies. */
|
|
50
|
+
async function consumeUvResponse(url, { fetchImpl = globalThis.fetch, timeoutMs = 120_000, maxBytes = MAX_ARCHIVE } = {}, consume) {
|
|
51
|
+
const controller = new AbortController()
|
|
52
|
+
const timer = setTimeout(() => controller.abort(new Error('UV_DOWNLOAD_TIMEOUT')), timeoutMs)
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetchImpl(url, { signal: controller.signal, redirect: 'follow' })
|
|
55
|
+
if (!response.ok) throw new Error('UV_HTTP_' + response.status)
|
|
56
|
+
if (Number(response.headers.get('content-length')) > maxBytes) throw new Error('UV_DOWNLOAD_TOO_LARGE')
|
|
57
|
+
if (!response.body) throw new Error('UV_EMPTY_RESPONSE')
|
|
58
|
+
let total = 0
|
|
59
|
+
for await (const chunk of response.body) {
|
|
60
|
+
total += chunk.length
|
|
61
|
+
if (total > maxBytes) { controller.abort(); throw new Error('UV_DOWNLOAD_TOO_LARGE') }
|
|
62
|
+
await consume(chunk)
|
|
63
|
+
}
|
|
64
|
+
if (!total) throw new Error('UV_EMPTY_RESPONSE')
|
|
65
|
+
return total
|
|
66
|
+
} finally { clearTimeout(timer); controller.abort() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function fetchUvBytes(url, options = {}) {
|
|
70
|
+
const chunks = []
|
|
71
|
+
const length = await consumeUvResponse(url, { maxBytes: 8 * 1024 * 1024, ...options }, chunk => { chunks.push(Buffer.from(chunk)) })
|
|
72
|
+
return Buffer.concat(chunks, length)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Large binaries stream to a private exclusive file rather than buffering hundreds of MB in the host. */
|
|
76
|
+
export async function downloadUvArchive(url, file, options = {}) {
|
|
77
|
+
const handle = await open(file, 'wx', 0o600)
|
|
78
|
+
const digest = createHash('sha256')
|
|
79
|
+
let complete = false
|
|
80
|
+
try {
|
|
81
|
+
await consumeUvResponse(url, options, async chunk => {
|
|
82
|
+
digest.update(chunk)
|
|
83
|
+
let offset = 0
|
|
84
|
+
while (offset < chunk.length) {
|
|
85
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.length - offset)
|
|
86
|
+
if (!bytesWritten) throw new Error('UV_DOWNLOAD_WRITE_FAILED')
|
|
87
|
+
offset += bytesWritten
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
await handle.sync()
|
|
91
|
+
complete = true
|
|
92
|
+
return digest.digest('hex')
|
|
93
|
+
} finally {
|
|
94
|
+
await handle.close()
|
|
95
|
+
if (!complete) try { await unlink(file) } catch { /* Only this newly created file is ours. */ }
|
|
91
96
|
}
|
|
97
|
+
}
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
function linuxLibc() {
|
|
100
|
+
if (process.platform !== 'linux') return 'gnu'
|
|
101
|
+
try { return process.report.getReport().header.glibcVersionRuntime ? 'gnu' : 'musl' } catch { return 'gnu' }
|
|
102
|
+
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
export function officialUvWheel(metadata, wheel) {
|
|
105
|
+
const candidates = (Array.isArray(metadata?.urls) ? metadata.urls : []).filter(entry => {
|
|
106
|
+
const filename = String(entry.filename || '')
|
|
107
|
+
if (!new RegExp('^uv-\\d+\\.\\d+\\.\\d+-py3-none-' + wheel + '\\.whl$').test(filename) || !SHA.test(String(entry.digests?.sha256 || ''))) return false
|
|
108
|
+
try { const url = new URL(entry.url); return url.protocol === 'https:' && url.hostname === 'files.pythonhosted.org' && !url.username && !url.password && !url.port } catch { return false }
|
|
109
|
+
})
|
|
110
|
+
candidates.sort((a, b) => {
|
|
111
|
+
const x = a.filename.split('-')[1].split('.').map(Number), y = b.filename.split('-')[1].split('.').map(Number)
|
|
112
|
+
return y[0] - x[0] || y[1] - x[1] || y[2] - x[2]
|
|
113
|
+
})
|
|
114
|
+
return candidates[0] || null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function installUv({ dir, log, asset, fetchImpl, run, timeoutMs }) {
|
|
118
|
+
const target = join(dir, asset.bin)
|
|
119
|
+
const usable = async path => {
|
|
120
|
+
try { return (await run(path, ['--version'], { timeout: 15_000 })).status === 0 } catch { return false }
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(target) && await usable(target)) return target
|
|
123
|
+
mkdirSync(dir, { recursive: true })
|
|
124
|
+
// Each installer owns its extraction tree and staging file; concurrent hosts
|
|
125
|
+
// cannot delete another installer's archive or partially overwrite uv.exe.
|
|
126
|
+
const tmp = mkdtempSync(join(dir, '.uv-install-'))
|
|
127
|
+
const staged = `${target}.${process.pid}.${randomUUID()}.staging`
|
|
128
|
+
const official = `https://github.com/astral-sh/uv/releases/latest/download/${asset.name}`
|
|
101
129
|
const sources = [
|
|
102
|
-
{ label: 'GitHub
|
|
103
|
-
{ label: 'GitHub
|
|
104
|
-
{ label: 'GitHub
|
|
130
|
+
{ label: 'GitHub 官方', url: official, file: asset.name },
|
|
131
|
+
{ label: 'GitHub 下载代理 gh-proxy.com', url: `https://gh-proxy.com/${official}`, file: asset.name },
|
|
132
|
+
{ label: 'GitHub 下载代理 ghproxy.net', url: `https://ghproxy.net/${official}`, file: asset.name },
|
|
105
133
|
]
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
const w = (j.urls || [])
|
|
118
|
-
.filter((u) => /win_amd64\.whl$/.test(String(u.filename)))
|
|
119
|
-
.sort((x, y) => cmpVer(x.filename, y.filename))[0]
|
|
120
|
-
// ⭐ 2026-10-26:镜像 JSON 里的文件名要消毒后再拼路径(`..\x` / 绝对分量不能进 tmp),
|
|
121
|
-
// 且必须自带 sha256 —— 没有 digest 的源直接不用(fail closed,见下面的校验段)
|
|
122
|
-
if (w?.url && /^[\w.-]+$/.test(String(w.filename)) && w.digests?.sha256) {
|
|
123
|
-
sources.push({ label: 'PyPI 清华镜像(uv wheel)', url: w.url, file: join(tmp, String(w.filename)), sha: w.digests.sha256 })
|
|
124
|
-
log(` (备选源已就绪:${w.filename})`)
|
|
125
|
-
}
|
|
126
|
-
} catch (e) {
|
|
127
|
-
log(` 列 PyPI 版本时出错(不影响,继续试别的源):${e?.message ?? e}`)
|
|
134
|
+
const bytes = (url, limit = MAX_ARCHIVE, deadline = timeoutMs) => fetchUvBytes(url, { fetchImpl, maxBytes: limit, timeoutMs: deadline })
|
|
135
|
+
let githubDigest
|
|
136
|
+
try {
|
|
137
|
+
if (asset.wheel) {
|
|
138
|
+
try {
|
|
139
|
+
// Trust metadata from PyPI itself, never a mirror that can replace both
|
|
140
|
+
// a wheel and its claimed checksum. Download hosts are restricted too.
|
|
141
|
+
const metadata = JSON.parse((await bytes('https://pypi.org/pypi/uv/json', 8 * 1024 * 1024, 20_000)).toString())
|
|
142
|
+
const wheel = officialUvWheel(metadata, asset.wheel)
|
|
143
|
+
if (wheel) sources.push({ label: 'PyPI 官方 wheel', url: wheel.url, file: wheel.filename, sha: wheel.digests.sha256.toLowerCase() })
|
|
144
|
+
} catch (error) { log('PyPI 元数据不可用,继续其他来源:' + String(error.message)) }
|
|
128
145
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for (const s of sources) {
|
|
132
|
-
try {
|
|
133
|
-
log(`正在下载 uv —— ${s.label}`)
|
|
134
|
-
const res = await fetch(s.url)
|
|
135
|
-
if (!res.ok) { log(` → 返回 ${res.status},换下一个源`); continue }
|
|
136
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(s.file))
|
|
137
|
-
const mb = (statSync(s.file).size / 1048576).toFixed(1)
|
|
138
|
-
log(` → 下载完成 ${mb} MB`)
|
|
139
|
-
// ⭐ 完整性校验:下载下来是要**被执行**的,必须跟官方 sha256 对上(防的是第三方代理动过包 ✗)
|
|
140
|
-
// 2026-10-26 起从"拿不到就算了"改成 **fail closed**:拿不到官方校验值 ⇒ 这个源作废,
|
|
141
|
-
// 绝不执行未校验的二进制(uv 会拉起整个 Python venv,投毒等于完整 RCE 链)。
|
|
142
|
-
const want = await expectedSha(s, s.official || asset)
|
|
143
|
-
if (!want) {
|
|
144
|
-
log(' → 拿不到官方 sha256 ⇒ 拒绝使用这个源(不执行未校验的二进制)')
|
|
145
|
-
continue
|
|
146
|
-
}
|
|
147
|
-
const gotSha = sha256Of(s.file)
|
|
148
|
-
if (gotSha !== want) {
|
|
149
|
-
log(` → ⚠️ sha256 校验不过(期望 ${want.slice(0, 12)}… 实际 ${gotSha.slice(0, 12)}…)⇒ 丢弃这个源`)
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
log(' → 官方 sha256 校验通过 ✓')
|
|
153
|
-
log(' → 开始解压')
|
|
154
|
-
const hit = extractArchive(a.bin, s.file, tmp)
|
|
155
|
-
if (!hit) { log(' → 解压后没找到可执行文件,换下一个源'); continue }
|
|
156
|
-
// ⭐ 原子落盘(2026-10-26):直接 copyFileSync 被中断会留半截 uv.exe,
|
|
157
|
-
// 下次启动 `existsSync(target)` 又把它当好的(上面的可跑检查只能兜住一部分)
|
|
158
|
-
const staging = `${target}.staging`
|
|
159
|
-
copyFileSync(hit, staging)
|
|
160
|
-
try { chmodSync(staging, 0o755) } catch { /* Windows 上不需要 */ }
|
|
161
|
-
renameSync(staging, target)
|
|
146
|
+
for (const source of sources) {
|
|
162
147
|
try {
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
148
|
+
log('正在下载 uv:' + source.label)
|
|
149
|
+
if (!source.sha && githubDigest === undefined) {
|
|
150
|
+
try {
|
|
151
|
+
const text = (await bytes(official + '.sha256', 4096, 20_000)).toString()
|
|
152
|
+
githubDigest = /\b([0-9a-f]{64})\b/i.exec(text)?.[1]?.toLowerCase() || null
|
|
153
|
+
} catch { githubDigest = null }
|
|
154
|
+
}
|
|
155
|
+
const wanted = source.sha || githubDigest
|
|
156
|
+
if (!SHA.test(String(wanted || ''))) { log('缺少官方 SHA-256,已跳过该来源'); continue }
|
|
157
|
+
const file = join(tmp, randomUUID() + '-' + source.file)
|
|
158
|
+
const digest = await downloadUvArchive(source.url, file, { fetchImpl, timeoutMs, maxBytes: MAX_ARCHIVE })
|
|
159
|
+
if (digest !== wanted) { log('SHA-256 不一致,已拒绝该文件'); continue }
|
|
160
|
+
const into = mkdtempSync(join(tmp, 'extract-'))
|
|
161
|
+
const executable = await extractArchive(asset.bin, file, into, run)
|
|
162
|
+
if (!executable) { log('归档中缺少可执行文件'); continue }
|
|
163
|
+
copyFileSync(executable, staged)
|
|
164
|
+
try { chmodSync(staged, 0o755) } catch { /* Windows permissions differ. */ }
|
|
165
|
+
if (!await usable(staged)) { log('uv 版本检查失败,未替换目标文件'); continue }
|
|
166
|
+
renameSync(staged, target)
|
|
167
|
+
if (!await usable(target)) { log('uv 安装后不可执行'); continue }
|
|
168
|
+
log('uv 已通过官方校验和版本检查')
|
|
169
|
+
return target
|
|
170
|
+
} catch (error) { log(source.label + ' 失败:' + String(error.message)) }
|
|
172
171
|
}
|
|
172
|
+
log('未获得通过官方校验且可执行的 uv。可手动安装 Python/uv 后重试。')
|
|
173
|
+
return null
|
|
174
|
+
} finally {
|
|
175
|
+
try { rmSync(staged, { force: true }) } catch { /* Never remove another installer staging file. */ }
|
|
176
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
173
177
|
}
|
|
174
|
-
|
|
175
|
-
rmSync(tmp, { recursive: true, force: true })
|
|
176
|
-
log('所有源都没拿到**通过官方校验**的 uv(网络到不了 GitHub / PyPI 镜像,或代理给的包校验不过)。'
|
|
177
|
-
+ '可以手动下载 uv 放进上面那个目录,或先在机器上装好 uv 再点安装。')
|
|
178
|
-
return null
|
|
179
178
|
}
|
|
179
|
+
|
|
180
|
+
/** Optional I/O implementations make install failures testable without executing a downloaded binary. */
|
|
181
|
+
export function ensureUv({ dir, log = () => {}, fetchImpl = globalThis.fetch, run = nativeRun,
|
|
182
|
+
platform = process.platform, arch = process.arch, libc = linuxLibc(), timeoutMs = 120_000 }) {
|
|
183
|
+
const asset = uvAssetFor(platform, arch, libc)
|
|
184
|
+
if (!asset) { log(`当前平台不支持自动下载 uv:${platform}/${arch}`); return Promise.resolve(null) }
|
|
185
|
+
const destination = resolve(dir)
|
|
186
|
+
const key = destination + '/' + asset.name
|
|
187
|
+
if (inflight.has(key)) return inflight.get(key)
|
|
188
|
+
const pending = installUv({ dir: destination, log, asset, fetchImpl, run, timeoutMs }).finally(() => inflight.delete(key))
|
|
189
|
+
inflight.set(key, pending)
|
|
190
|
+
return pending
|
|
191
|
+
}
|