chore: prepare GoodBuddy 0.8.3
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions

This commit is contained in:
lofyer
2026-08-07 01:56:32 +08:00
parent b8fc7bc86e
commit 4100911c34
23 changed files with 1779 additions and 404 deletions
+16 -14
View File
@@ -19,7 +19,8 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
family: 'sensevoice',
quantization: 'int8',
repositoryUrl:
'https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17',
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-sense-voice-zh-en-ja-ko-yue',
license: {
name: '模型仓库自定义许可(Model License',
notice:
@@ -33,9 +34,9 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
role: 'model',
download: {
url:
'https://huggingface.co/csukuangfj/' +
'sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/' +
'resolve/6a65851692da9706cbddfac66ea9b96ebb1dee21/' +
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-sense-voice-zh-en-ja-ko-yue/' +
'resolve/73eca47697f980daa3d16112404174b6b950b514/' +
'model.int8.onnx',
size: 239_233_841,
sha256:
@@ -47,9 +48,9 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
role: 'tokens',
download: {
url:
'https://huggingface.co/csukuangfj/' +
'sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/' +
'resolve/86f7114c4a83bcba0c609dd8d8dfa730d967ade9/' +
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-sense-voice-zh-en-ja-ko-yue/' +
'resolve/73eca47697f980daa3d16112404174b6b950b514/' +
'tokens.txt',
size: 315_894,
sha256:
@@ -67,7 +68,8 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
family: 'whisper',
quantization: 'int8',
repositoryUrl:
'https://huggingface.co/csukuangfj/sherpa-onnx-whisper-tiny',
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-whisper-tiny',
license: {
name: 'MIT License',
notice:
@@ -81,9 +83,9 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
role: 'encoder',
download: {
url:
'https://huggingface.co/csukuangfj/' +
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-whisper-tiny/resolve/' +
'65176e2deb88badc814a94058666cadccc29b61c/' +
'33a655645234f82ce833cf27b689d9c2212e693f/' +
'tiny-encoder.int8.onnx',
size: 12_937_772,
sha256:
@@ -95,9 +97,9 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
role: 'decoder',
download: {
url:
'https://huggingface.co/csukuangfj/' +
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-whisper-tiny/resolve/' +
'65176e2deb88badc814a94058666cadccc29b61c/' +
'33a655645234f82ce833cf27b689d9c2212e693f/' +
'tiny-decoder.int8.onnx',
size: 89_855_401,
sha256:
@@ -109,9 +111,9 @@ export const SPEECH_MODEL_CATALOG: readonly SpeechModelCatalogEntry[] =
role: 'tokens',
download: {
url:
'https://huggingface.co/csukuangfj/' +
'https://modelscope.cn/models/pengzhendong/' +
'sherpa-onnx-whisper-tiny/resolve/' +
'65176e2deb88badc814a94058666cadccc29b61c/' +
'33a655645234f82ce833cf27b689d9c2212e693f/' +
'tiny-tokens.txt',
size: 816_730,
sha256:
+70 -17
View File
@@ -55,7 +55,8 @@ function downloadableCatalog(
languages: ['中文'],
family: 'whisper',
quantization: 'int8',
repositoryUrl: 'https://huggingface.co/example/download-test-model',
repositoryUrl:
'https://modelscope.cn/models/example/download-test-model',
license: {
name: 'MIT License',
notice: 'Test-only model metadata.',
@@ -68,8 +69,8 @@ function downloadableCatalog(
role: 'model',
download: {
url:
'https://huggingface.co/example/download-test-model/' +
'resolve/revision/model.onnx',
'https://modelscope.cn/models/example/download-test-model/' +
`resolve/${'a'.repeat(40)}/model.onnx`,
size: modelBytes.byteLength,
sha256: sha256(modelBytes)
}
@@ -79,8 +80,8 @@ function downloadableCatalog(
role: 'tokens',
download: {
url:
'https://huggingface.co/example/download-test-model/' +
'resolve/revision/tokens.txt',
'https://modelscope.cn/models/example/download-test-model/' +
`resolve/${'a'.repeat(40)}/tokens.txt`,
size: tokenBytes.byteLength,
sha256: sha256(tokenBytes)
}
@@ -124,6 +125,16 @@ describe('speech model catalog', () => {
'tiny-decoder.int8.onnx',
'tiny-tokens.txt'
])
for (const entry of SPEECH_MODEL_CATALOG) {
expect(entry.repositoryUrl).toMatch(
/^https:\/\/modelscope\.cn\/models\//u
)
for (const file of entry.files) {
expect(file.download?.url).toMatch(
/^https:\/\/modelscope\.cn\/models\/[^/]+\/[^/]+\/resolve\/[a-f0-9]{40}\/[^/]+$/u
)
}
}
})
})
@@ -168,7 +179,9 @@ describe('SpeechModelManager downloads', () => {
})
expect(transport).toHaveBeenCalledTimes(2)
for (const [input, init] of transport.mock.calls) {
expect(String(input)).toMatch(/^https:\/\/huggingface\.co\//u)
expect(String(input)).toMatch(
/^https:\/\/modelscope\.cn\/models\//u
)
expect(init).toMatchObject({
method: 'GET',
redirect: 'manual',
@@ -203,26 +216,66 @@ describe('SpeechModelManager downloads', () => {
})
})
it('rejects untrusted redirects and bad digests without installing', async () => {
it('accepts arbitrary HTTP hosts and cross-host redirects', async () => {
const userData = await temporaryDirectory()
const modelBytes = new TextEncoder().encode('expected')
const catalog = downloadableCatalog(modelBytes)
const tokenBytes = new TextEncoder().encode('tokens')
const catalog = downloadableCatalog(modelBytes).map((entry) => ({
...entry,
files: entry.files.map((file) => ({
...file,
download: file.download
? {
...file.download,
url: file.download.url.replace(
'https://modelscope.cn',
'http://models.internal.example'
)
}
: undefined
}))
}))
const transport = vi.fn<typeof fetch>(async (input) => {
const url = new URL(String(input))
if (url.hostname === 'models.internal.example') {
return new Response(null, {
status: 302,
headers: {
location: `https://cdn.example.net${url.pathname}`
}
})
}
const bytes = url.pathname.endsWith('model.onnx')
? modelBytes
: tokenBytes
return new Response(bytes, {
headers: { 'content-length': String(bytes.byteLength) }
})
})
const redirected = new SpeechModelManager({
userDataDirectory: userData,
catalog,
fetch: vi.fn<typeof fetch>(async () =>
new Response(null, {
status: 302,
headers: {
location: 'https://attacker.invalid/model.onnx'
}
})
)
fetch: transport
})
await expect(
redirected.install('download-test-model')
).rejects.toThrow('允许的 Hugging Face HTTPS')
).resolves.toMatchObject({ id: 'download-test-model' })
expect(transport).toHaveBeenCalledTimes(4)
expect(
transport.mock.calls.map(([input]) => new URL(String(input)).hostname)
).toEqual([
'models.internal.example',
'cdn.example.net',
'models.internal.example',
'cdn.example.net'
])
})
it('rejects bad digests without installing', async () => {
const userData = await temporaryDirectory()
const modelBytes = new TextEncoder().encode('expected')
const catalog = downloadableCatalog(modelBytes)
const badDigest = new SpeechModelManager({
userDataDirectory: userData,
catalog,
+3 -17
View File
@@ -32,17 +32,6 @@ const MANIFEST_FILE_NAME = 'manifest.json'
const SELECTION_FILE_NAME = '.selection.json'
const PARTIAL_SUFFIX = '.partial'
const SPEECH_MODEL_ALLOWED_DOWNLOAD_HOSTS = new Set([
'huggingface.co',
'cdn-lfs.huggingface.co',
'cdn-lfs-us-1.huggingface.co',
'cdn-lfs-eu-1.huggingface.co',
'cdn-lfs.hf.co',
'cdn-lfs-us-1.hf.co',
'cdn-lfs-eu-1.hf.co',
'cas-bridge.xethub.hf.co'
])
const selectionSchema = z
.object({
selectedModelId: speechModelIdSchema.nullable()
@@ -110,13 +99,10 @@ function safeChild(parent: string, name: string): string {
function validateDownloadUrl(value: string): URL {
const url = new URL(value)
if (
url.protocol !== 'https:' ||
url.username ||
url.password ||
url.hash ||
!SPEECH_MODEL_ALLOWED_DOWNLOAD_HOSTS.has(url.hostname.toLowerCase())
url.protocol !== 'http:' &&
url.protocol !== 'https:'
) {
throw new Error('模型下载地址必须是允许的 Hugging Face HTTPS 地址')
throw new Error('模型下载地址必须使用 HTTP 或 HTTPS')
}
return url
}