dsh-bili-scroll 2.0.2 → 2.1.0
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 +3 -1
- package/CONTRIBUTORS.md +5 -1
- package/README.md +12 -2
- package/client.js +12 -0
- package/index.js +555 -47
- package/lib/login.mjs +19 -0
- package/lib/mcp.mjs +34 -9
- package/lib/state.mjs +11 -12
- package/lib/uvfetch.mjs +217 -161
- package/package.json +2 -1
- package/profile-template/bili-duty/cordis.patch.yml +116 -0
- package/profile-template/bili-duty/cordis.yml +4 -0
- package/profile-template/bili-duty/package.json +13 -0
- package/prompts/duty.md +109 -11
- package/vendor/bilibili-mcp/__pycache__/mcp_server.cpython-314.pyc +0 -0
- package/vendor/bilibili-mcp/mcp_server.py +557 -312
- package/vendor/bilibili-mcp/requirements.txt +2 -0
package/lib/login.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from 'node:child_process'
|
|
18
18
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { execFileSync } from 'node:child_process'
|
|
19
20
|
import { join } from 'node:path'
|
|
20
21
|
|
|
21
22
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
|
|
@@ -75,6 +76,23 @@ export async function loginPoll(key) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/** 把凭证写进 MCP 读的那个文件(家目录里的 bili_credential.json) */
|
|
79
|
+
/**
|
|
80
|
+
* ⭐ 2026-10-23 安全加固(审计 + issue #67):凭证文件权限收紧到"只有自己能读" ——
|
|
81
|
+
* Linux/macOS: chmod 0600;Windows: icacls 去掉继承(只留当前用户)。
|
|
82
|
+
* 失败也不报错(有些文件系统不支持),但会返回 false 让调用方可以说明。
|
|
83
|
+
*/
|
|
84
|
+
export function hardenPerms(file) {
|
|
85
|
+
try {
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
const user = process.env.USERNAME || ''
|
|
88
|
+
execFileSync('icacls', [file, '/inheritance:r', '/grant:r', user + ':F'], { stdio: 'ignore', windowsHide: true })
|
|
89
|
+
} else {
|
|
90
|
+
execFileSync('chmod', ['600', file], { stdio: 'ignore' })
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
} catch { return false }
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
export function saveCredential(stateDir, credential) {
|
|
79
97
|
mkdirSync(stateDir, { recursive: true })
|
|
80
98
|
const file = join(stateDir, 'bili_credential.json')
|
|
@@ -84,6 +102,7 @@ export function saveCredential(stateDir, credential) {
|
|
|
84
102
|
buvid3: credential.buvid3 || '',
|
|
85
103
|
dedeuserid: credential.dedeuserid ?? '',
|
|
86
104
|
}, null, 2), 'utf8')
|
|
105
|
+
try { hardenPerms(file) } catch { /* 收紧失败也不影响登录 */ }
|
|
87
106
|
return file
|
|
88
107
|
}
|
|
89
108
|
|
package/lib/mcp.mjs
CHANGED
|
@@ -67,12 +67,23 @@ export class McpClient {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** 调一个 MCP 工具:能解析成 JSON 就返回对象,否则返回字符串 */
|
|
70
|
-
async call(name, args = {},
|
|
70
|
+
async call(name, args = {}, opts = {}) {
|
|
71
|
+
return (await this.callRaw(name, args, opts)).json
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 同 call,但保留图片块:返回 { json, images },images 每项是 { data, mimeType } */
|
|
75
|
+
async callRaw(name, args = {}, { timeoutMs = 180000, signal } = {}) {
|
|
71
76
|
await this.start()
|
|
72
|
-
const result = await this.#request('tools/call', { name, arguments: args }, timeoutMs)
|
|
73
|
-
const
|
|
77
|
+
const result = await this.#request('tools/call', { name, arguments: args }, timeoutMs, signal)
|
|
78
|
+
const blocks = result?.content ?? []
|
|
79
|
+
const text = blocks.map((c) => c?.text ?? '').filter(Boolean).join('\n').trim()
|
|
74
80
|
if (result?.isError) throw new Error(text || `MCP 工具 ${name} 报错`)
|
|
75
|
-
|
|
81
|
+
const images = blocks
|
|
82
|
+
.filter((c) => c?.type === 'image' && c?.data)
|
|
83
|
+
.map((c) => ({ data: c.data, mimeType: c.mimeType || c.mime_type || 'image/jpeg' }))
|
|
84
|
+
let json = {}
|
|
85
|
+
try { json = text ? JSON.parse(text) : {} } catch { json = text }
|
|
86
|
+
return { json, images }
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
#onData(chunk) {
|
|
@@ -100,19 +111,33 @@ export class McpClient {
|
|
|
100
111
|
|
|
101
112
|
#notify(method, params) { this.#send({ jsonrpc: '2.0', method, params }) }
|
|
102
113
|
|
|
103
|
-
#request(method, params, timeoutMs = 60000) {
|
|
114
|
+
#request(method, params, timeoutMs = 60000, signal) {
|
|
104
115
|
const id = this.nextId++
|
|
105
116
|
return new Promise((resolve, reject) => {
|
|
106
|
-
|
|
117
|
+
if (signal?.aborted) {
|
|
118
|
+
reject(new Error(`MCP ${method} 已取消`))
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
const cleanup = () => {
|
|
122
|
+
clearTimeout(timer)
|
|
107
123
|
this.pending.delete(id)
|
|
124
|
+
signal?.removeEventListener?.('abort', onAbort)
|
|
125
|
+
}
|
|
126
|
+
const onAbort = () => {
|
|
127
|
+
cleanup()
|
|
128
|
+
reject(new Error(`MCP ${method} 已取消`))
|
|
129
|
+
}
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
cleanup()
|
|
108
132
|
reject(new Error(`MCP ${method} 超时(${timeoutMs}ms)${this.stderr ? ':' + this.stderr.slice(-200) : ''}`))
|
|
109
133
|
}, timeoutMs)
|
|
110
134
|
this.pending.set(id, {
|
|
111
|
-
resolve: (v) => {
|
|
112
|
-
reject: (e) => {
|
|
135
|
+
resolve: (v) => { cleanup(); resolve(v) },
|
|
136
|
+
reject: (e) => { cleanup(); reject(e) },
|
|
113
137
|
})
|
|
138
|
+
signal?.addEventListener?.('abort', onAbort, { once: true })
|
|
114
139
|
try { this.#send({ jsonrpc: '2.0', id, method, params }) } catch (e) {
|
|
115
|
-
|
|
140
|
+
cleanup(); reject(e)
|
|
116
141
|
}
|
|
117
142
|
})
|
|
118
143
|
}
|
package/lib/state.mjs
CHANGED
|
@@ -88,18 +88,17 @@ export function ensureHomeFiles(pluginDir, stateDir, log = () => {}) {
|
|
|
88
88
|
}
|
|
89
89
|
const onDisk = readFileSync(to, 'utf8')
|
|
90
90
|
if (onDisk === incoming) { if (!shipped) writeMark(incoming); continue } // 本来就是同一份
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
91
|
+
// ⭐⭐ 2.1.0(2026-10-24 实证):剩下两种情况的处理可以并成一条 ——
|
|
92
|
+
// 这几个文件是**我们发的运行时代码**(mcp_server.py / asr.py / favops.py),
|
|
93
|
+
// 用户改过也不该拦着升级:否则"宿主新、Python 侧旧",新工具根本不出现 ✗
|
|
94
|
+
// (今天就这么栽的:家目录那份停在 11:50,值班轮里说"没有 bili_cover 这个工具")
|
|
95
|
+
// ⇒ **先备份 .bak-<时间戳>,再覆盖**,改了什么都能自己捞回来 ✓
|
|
96
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
|
|
97
|
+
const bak = to + ".bak-" + stamp
|
|
98
|
+
try { copyFileSync(to, bak) } catch { /* 备份不了也继续,原件在插件包里 */ }
|
|
99
|
+
copyFileSync(from, to)
|
|
100
|
+
writeMark(incoming)
|
|
101
|
+
log(`已覆盖:${to}(旧的那份备份在 ${bak})`)
|
|
103
102
|
} catch (e) { log(`铺 ${to} 失败:${e?.message ?? e}`) }
|
|
104
103
|
}
|
|
105
104
|
}
|
package/lib/uvfetch.mjs
CHANGED
|
@@ -1,161 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/uvfetch.mjs —— 「机器上连 uv 和 Python 都没有」时,**插件自己把 uv 弄到手**
|
|
3
|
-
* ============================================================================
|
|
4
|
-
* 为什么值得单独做:uv 是**单个可执行文件**(Windows 上 ~32MB 的 uv.exe ✓),
|
|
5
|
-
* **解压就能用、不用管理员、不用进 PATH** ✓,而且它还能自己把 Python 拉下来 ✓
|
|
6
|
-
* ⇒ 只要拿到它,"首次安装"就从「先去装个 Python」变成「点一下就好」✓
|
|
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 } 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'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/uvfetch.mjs —— 「机器上连 uv 和 Python 都没有」时,**插件自己把 uv 弄到手**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* 为什么值得单独做:uv 是**单个可执行文件**(Windows 上 ~32MB 的 uv.exe ✓),
|
|
5
|
+
* **解压就能用、不用管理员、不用进 PATH** ✓,而且它还能自己把 Python 拉下来 ✓
|
|
6
|
+
* ⇒ 只要拿到它,"首次安装"就从「先去装个 Python」变成「点一下就好」✓
|
|
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 } 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'
|
|
24
|
+
|
|
25
|
+
/** 本机 Linux 是不是 musl libc(glibc 系统的 report header 会带 glibcVersionRuntime) */
|
|
26
|
+
function isMusl() {
|
|
27
|
+
if (process.platform !== 'linux') return false
|
|
28
|
+
try {
|
|
29
|
+
const v = process.report?.getReport?.()?.header?.glibcVersionRuntime
|
|
30
|
+
if (typeof v === 'string' && v.length) return false
|
|
31
|
+
} catch { /* 拿不到就继续,下面按动态链接器文件名判断 */ }
|
|
32
|
+
try {
|
|
33
|
+
return existsSync('/lib/ld-musl-aarch64.so.1') || existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-i386.so.1')
|
|
34
|
+
} catch { return false }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 按 platform + arch 挑 uv 官方 release 的资产名(按优先级排序的候选列表)。
|
|
39
|
+
* 之前这里写死 x86_64(linux)/ aarch64(darwin)/ x86_64(win32)⇒ 别的架构会下错二进制,
|
|
40
|
+
* 解压后版本检查跑不起来(比如 arm64 机器拿到 x86-64 的包)。现在按 arch 选,linux 再把
|
|
41
|
+
* glibc/musl 各列一个候选,靠下面的版本检查兜底。
|
|
42
|
+
*/
|
|
43
|
+
function uvAssetCandidates() {
|
|
44
|
+
const p = process.platform
|
|
45
|
+
const a = process.arch
|
|
46
|
+
if (p === 'win32') {
|
|
47
|
+
if (a === 'arm64') return { bin: 'uv.exe', names: ['uv-aarch64-pc-windows-msvc.zip'] }
|
|
48
|
+
if (a === 'ia32') return { bin: 'uv.exe', names: ['uv-i686-pc-windows-msvc.zip'] }
|
|
49
|
+
return { bin: 'uv.exe', names: ['uv-x86_64-pc-windows-msvc.zip'] }
|
|
50
|
+
}
|
|
51
|
+
if (p === 'darwin') {
|
|
52
|
+
return a === 'arm64'
|
|
53
|
+
? { bin: 'uv', names: ['uv-aarch64-apple-darwin.tar.gz'] }
|
|
54
|
+
: { bin: 'uv', names: ['uv-x86_64-apple-darwin.tar.gz'] }
|
|
55
|
+
}
|
|
56
|
+
// linux(及其余落到这行的 unix);只列 uv 官方出资产的架构
|
|
57
|
+
const arch = {
|
|
58
|
+
x64: 'x86_64',
|
|
59
|
+
arm64: 'aarch64',
|
|
60
|
+
arm: 'armv7',
|
|
61
|
+
ia32: 'i686',
|
|
62
|
+
ppc64: 'powerpc64le',
|
|
63
|
+
s390x: 's390x',
|
|
64
|
+
riscv64: 'riscv64gc',
|
|
65
|
+
}[a]
|
|
66
|
+
if (!arch) return null
|
|
67
|
+
// armv7 的硬浮点是 gnueabihf / musleabihf,其余是 gnu / musl
|
|
68
|
+
const hf = arch === 'armv7' ? 'eabihf' : ''
|
|
69
|
+
const libcs = isMusl() ? ['musl', 'gnu'] : ['gnu', 'musl']
|
|
70
|
+
return { bin: 'uv', names: libcs.map((l) => `uv-${arch}-unknown-linux-${l}${hf}.tar.gz`) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 算一个文件的 sha256 */
|
|
74
|
+
function sha256Of(file) {
|
|
75
|
+
return createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 取"官方期望的 sha256":
|
|
80
|
+
* · PyPI wheel:JSON 里自带 `digests.sha256` ✓
|
|
81
|
+
* · GitHub 资产:同目录下有一份 `<文件名>.sha256` ✓(代理会一起转 ✓)
|
|
82
|
+
* 取不到就返回 null(那时只记一句"未校验",不拦着)
|
|
83
|
+
*/
|
|
84
|
+
async function expectedSha(source) {
|
|
85
|
+
if (source.sha) return String(source.sha).toLowerCase()
|
|
86
|
+
try {
|
|
87
|
+
const r = await fetch(source.url + '.sha256')
|
|
88
|
+
if (!r.ok) return null
|
|
89
|
+
const m = /([0-9a-f]{64})/i.exec(await r.text())
|
|
90
|
+
return m ? m[1].toLowerCase() : null
|
|
91
|
+
} catch { return null }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 在目录树里按文件名找可执行文件(递归) */
|
|
95
|
+
export function findBin(name, dir) {
|
|
96
|
+
let entries = []
|
|
97
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return null }
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
const p = join(dir, e.name)
|
|
100
|
+
if (e.isDirectory()) { const hit = findBin(name, p); if (hit) return hit }
|
|
101
|
+
else if (e.name === name) return p
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 解压一个压缩包并找出目标文件(tar 优先 —— Windows 自带那份能读 zip) */
|
|
107
|
+
export function extractArchive(binName, archive, into) {
|
|
108
|
+
const attempts = [['tar', ['-xf', archive, '-C', into]], ['unzip', ['-o', archive, '-d', into]]]
|
|
109
|
+
for (const [cmd, args] of attempts) {
|
|
110
|
+
try {
|
|
111
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' })
|
|
112
|
+
if (r.status === 0) { const hit = findBin(binName, into); if (hit) return hit }
|
|
113
|
+
} catch { /* 换下一个工具 */ }
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 确保家目录里有一份可用的 uv,返回它的路径(拿不到就返回 null)。
|
|
120
|
+
* @param {{ dir: string, log?: (m: string) => void }} opts
|
|
121
|
+
*/
|
|
122
|
+
export async function ensureUv({ dir, log = () => {} }) {
|
|
123
|
+
const a = uvAssetCandidates()
|
|
124
|
+
if (!a) { log(`这台机器的平台/架构没有对应的 uv 官方资产(${process.platform} ${process.arch}),请手动放一份 uv 到 bin/`); return null }
|
|
125
|
+
const target = join(dir, a.bin)
|
|
126
|
+
if (existsSync(target)) return target
|
|
127
|
+
|
|
128
|
+
mkdirSync(dir, { recursive: true })
|
|
129
|
+
const tmp = join(dir, '_tmp')
|
|
130
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
131
|
+
mkdirSync(tmp, { recursive: true })
|
|
132
|
+
|
|
133
|
+
// 源按顺序试:每个候选资产名 ×(直连 → 两个国内能用的 GitHub 加速代理)
|
|
134
|
+
// → 最后兜底是 PyPI 镜像上的 uv wheel(win32 才有,且只有 x64 的 wheel)
|
|
135
|
+
const sources = a.names.flatMap((name) => {
|
|
136
|
+
const asset = `https://github.com/astral-sh/uv/releases/latest/download/${name}`
|
|
137
|
+
return [
|
|
138
|
+
{ label: `GitHub 直连(${name})`, url: asset, file: join(tmp, name) },
|
|
139
|
+
{ label: `GitHub 加速代理 gh-proxy.com(${name})`, url: `https://gh-proxy.com/${asset}`, file: join(tmp, name) },
|
|
140
|
+
{ label: `GitHub 加速代理 ghproxy.net(${name})`, url: `https://ghproxy.net/${asset}`, file: join(tmp, name) },
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
if (process.platform === 'win32') {
|
|
144
|
+
try {
|
|
145
|
+
const r = await fetch('https://pypi.tuna.tsinghua.edu.cn/pypi/uv/json')
|
|
146
|
+
const j = await r.json()
|
|
147
|
+
// ⚠️ PyPI 的 urls 顺序**不保证最新在前** ⇒ 必须按版本号自己排序挑最新(踩过:取到了 0.2.33 的老版本 ✗)
|
|
148
|
+
const verOf = (name) => { const m = /-(\d+\.\d+\.\d+(?:[.-][0-9A-Za-z.]+)?)-py3-none-win_amd64\.whl$/.exec(String(name)); return m ? m[1] : '0.0.0' }
|
|
149
|
+
const cmpVer = (a, b) => {
|
|
150
|
+
const A = verOf(a).split(/[.-]/).map(Number), B = verOf(b).split(/[.-]/).map(Number)
|
|
151
|
+
for (let i = 0; i < 4; i++) { if ((A[i] || 0) !== (B[i] || 0)) return (B[i] || 0) - (A[i] || 0) }
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
154
|
+
const w = (j.urls || [])
|
|
155
|
+
.filter((u) => /win_amd64\.whl$/.test(String(u.filename)))
|
|
156
|
+
.sort((x, y) => cmpVer(x.filename, y.filename))[0]
|
|
157
|
+
if (w?.url) {
|
|
158
|
+
sources.push({ label: 'PyPI 清华镜像(uv wheel)', url: w.url, file: join(tmp, String(w.filename)), sha: w.digests?.sha256 })
|
|
159
|
+
log(` (备选源已就绪:${w.filename})`)
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
log(` 列 PyPI 版本时出错(不影响,继续试别的源):${e?.message ?? e}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const s of sources) {
|
|
167
|
+
try {
|
|
168
|
+
// 每个候选都从干净的解压目录开始,别让上一个候选的残留文件被 findBin 误判
|
|
169
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
170
|
+
mkdirSync(tmp, { recursive: true })
|
|
171
|
+
log(`正在下载 uv —— ${s.label}`)
|
|
172
|
+
const res = await fetch(s.url)
|
|
173
|
+
if (!res.ok) { log(` → 返回 ${res.status},换下一个源`); continue }
|
|
174
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(s.file))
|
|
175
|
+
const mb = (statSync(s.file).size / 1048576).toFixed(1)
|
|
176
|
+
log(` → 下载完成 ${mb} MB`)
|
|
177
|
+
// ⭐ 完整性校验:下载下来是要**被执行**的,必须跟官方 sha256 对上(防的是第三方代理动过包 ✗)
|
|
178
|
+
const want = await expectedSha(s)
|
|
179
|
+
if (want) {
|
|
180
|
+
const gotSha = sha256Of(s.file)
|
|
181
|
+
if (gotSha !== want) {
|
|
182
|
+
log(` → ⚠️ sha256 校验不过(期望 ${want.slice(0, 12)}… 实际 ${gotSha.slice(0, 12)}…)⇒ 丢弃这个源`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
log(' → 官方 sha256 校验通过 ✓')
|
|
186
|
+
} else {
|
|
187
|
+
log(' → 没取到官方 sha256,跳过校验(下载来自 HTTPS 的官方地址时风险很小)')
|
|
188
|
+
}
|
|
189
|
+
log(' → 开始解压')
|
|
190
|
+
const hit = extractArchive(a.bin, s.file, tmp)
|
|
191
|
+
if (!hit) { log(' → 解压后没找到可执行文件,换下一个源'); continue }
|
|
192
|
+
copyFileSync(hit, target)
|
|
193
|
+
try { chmodSync(target, 0o755) } catch { /* Windows 上不需要 */ }
|
|
194
|
+
// ⭐ 版本检查:这个二进制得能**在这台机器上跑**——架构/ABI 不匹配(比如把 x86 的包下到了 arm64)会跑不起来 ✗
|
|
195
|
+
let runs = false
|
|
196
|
+
try {
|
|
197
|
+
const r = spawnSync(target, ['--version'], { stdio: 'ignore' })
|
|
198
|
+
runs = r.status === 0
|
|
199
|
+
log(' → 版本检查:' + (runs ? '能跑 ✓' : '跑不起来 ✗'))
|
|
200
|
+
} catch { runs = false }
|
|
201
|
+
if (!runs) {
|
|
202
|
+
log(' → 架构/ABI 不匹配,换下一个候选')
|
|
203
|
+
try { rmSync(target, { force: true }) } catch { /* 删不掉也不拦着 */ }
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
207
|
+
log(` → uv 已就位:${target}`)
|
|
208
|
+
return target
|
|
209
|
+
} catch (e) {
|
|
210
|
+
log(` → ${s.label} 失败:${e?.message ?? e}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
215
|
+
log('所有源都拿不到 uv(大概是网络到不了 GitHub / PyPI 镜像)')
|
|
216
|
+
return null
|
|
217
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bili-scroll",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "DSH 插件:每天替你刷一轮 B站 —— 读字幕挑出值得的、写一份日报,值得的就 @ 你",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./package.json": "./package.json"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
+
"profile-template",
|
|
25
26
|
"index.js",
|
|
26
27
|
"client.js",
|
|
27
28
|
"cordis.patch.yml",
|