feat: add mainland-reachable desktop distribution

This commit is contained in:
2026-08-16 23:00:54 +08:00
parent e8eeb4ac72
commit a3e1f11e85
42 changed files with 3661 additions and 191 deletions

View File

@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import test from 'node:test'
const nextTurn = () => new Promise(resolve => setImmediate(resolve))
function element(hidden = false) {
return {
disabled: false,
hidden,
textContent: '',
listeners: new Map(),
addEventListener(name, listener) {
this.listeners.set(name, listener)
},
}
}
test('startup waits for an available signed shell update decision', async () => {
const elements = new Map([
['status', element()],
['spinner', element()],
['retry', element(true)],
['detail', element(true)],
['update-panel', element(true)],
['update-notes', element()],
['install-update', element()],
['skip-update', element()],
])
const invokes = []
globalThis.document = {
readyState: 'complete',
getElementById(id) {
return elements.get(id)
},
}
globalThis.window = {
__TAURI__: {
event: { async listen() {} },
core: {
async invoke(command) {
invokes.push(command)
if (command === 'check_shell_update') {
return { version: '1.2.3', notes: 'signed release' }
}
return null
},
},
},
}
await import(new URL('../ui/app.js?update-test', import.meta.url))
await nextTurn()
assert.deepEqual(invokes, ['check_shell_update'])
assert.equal(elements.get('status').textContent, '发现壳更新 1.2.3')
assert.equal(elements.get('update-notes').textContent, 'signed release')
assert.equal(elements.get('update-panel').hidden, false)
await elements.get('skip-update').listeners.get('click')()
await nextTurn()
assert.deepEqual(invokes, ['check_shell_update', 'skip_shell_update'])
assert.equal(elements.get('update-panel').hidden, true)
})

View File

@@ -9,6 +9,12 @@ ROOT = Path(__file__).resolve().parents[1]
class VisionPluginFilesTests(unittest.TestCase):
def test_client_registers_system_settings_slots(self):
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text(
encoding="utf-8"
)
self.assertIn("settings.section", client)
self.assertNotIn("settings.plugins.tab", client)
self.assertIn("modlens-vision", client)
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text(
encoding="utf-8"
)
@@ -36,6 +42,40 @@ class VisionPluginFilesTests(unittest.TestCase):
self.assertIn("example: 'haiku'", host)
class VoicePluginFilesTests(unittest.TestCase):
def test_client_registers_composer_and_settings(self):
client = (ROOT / "plugins" / "dsh-desktop-voice" / "client.js").read_text(
encoding="utf-8"
)
host = (ROOT / "plugins" / "dsh-desktop-voice" / "index.js").read_text(
encoding="utf-8"
)
pkg = (ROOT / "plugins" / "dsh-desktop-voice" / "package.json").read_text(
encoding="utf-8"
)
self.assertIn("settings.section", client)
self.assertIn("conversation.input.right", client)
self.assertIn("dsh-desktop-voice", client)
self.assertIn("Ctrl+E", client)
self.assertIn("dictationMode", client)
self.assertIn("whisper-1", host)
self.assertIn("/dsh-desktop/voice", host)
self.assertIn("/dsh-desktop/voice/transcribe", host)
self.assertIn("/dsh-desktop/voice/model", host)
self.assertIn("SenseVoice", client)
self.assertIn("sherpa-onnx@", host)
self.assertNotIn("SpeechRecognition", client)
self.assertNotIn("engine === 'browser'", client)
self.assertIn("engine === 'openai'", client)
self.assertIn("cfg.engine === 'openai'", host)
self.assertFalse(
(ROOT / "plugins" / "dsh-desktop-voice" / "model.int8.onnx").exists()
)
self.assertIn("https://api.openai.com/v1", host)
self.assertIn("@deepseek-ai/dsh-client-ui-conversation", pkg)
self.assertIn("@deepseek-ai/dsh-client-ui-settings", pkg)
class ClipboardIngestTests(unittest.TestCase):
def test_inject_delivers_images_as_paste_not_only_drop(self):
ingest = (ROOT / "ui" / "inject" / "ingest.js").read_text(encoding="utf-8")
@@ -67,6 +107,9 @@ class BundledAttributionTests(unittest.TestCase):
self.assertIn("3.16.6", text)
self.assertIn("ffb845c5480adc953392a6db6f8a98ede621174b", text)
self.assertIn("dsh-desktop-vision", text)
self.assertIn("dsh-desktop-voice", text)
self.assertIn("https://github.com/dsh-market/dsh-market", text)
self.assertIn("1.9.0", text)
self.assertIn("dsh-plugin", readme)
self.assertIn("带上眼睛", readme)
self.assertIn("+8%", readme)
@@ -74,6 +117,7 @@ class BundledAttributionTests(unittest.TestCase):
self.assertTrue(
(ROOT / "docs" / "licenses" / "dsh-anchored-standard.NOTICE").is_file()
)
self.assertTrue((ROOT / "docs" / "licenses" / "dshmarket.LICENSE").is_file())
for name in ("splash.png", "session.png", "vision.png", "menu.png"):
self.assertTrue((ROOT / "docs" / "screenshots" / name).is_file())

72
tests/test_release.py Normal file
View File

@@ -0,0 +1,72 @@
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "build-gitea-update.py"
class GiteaUpdateManifestTests(unittest.TestCase):
def test_normalizes_matrix_artifacts_and_embeds_signatures(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
artifacts = root / "artifacts"
output = root / "staged"
fixtures = {
"gitea-macos-arm64": ".app.tar.gz",
"gitea-macos-x64": ".app.tar.gz",
"gitea-linux": ".AppImage",
"gitea-windows": ".exe",
}
for matrix, suffix in fixtures.items():
bundle = artifacts / matrix / "target" / "release" / "bundle"
bundle.mkdir(parents=True)
artifact = bundle / f"DeepSeek Harness{suffix}"
artifact.write_bytes(matrix.encode())
artifact.with_name(artifact.name + ".sig").write_text(
f"signature-{matrix}\n", encoding="utf-8"
)
subprocess.run(
[
"python3",
str(SCRIPT),
"--artifacts",
str(artifacts),
"--output",
str(output),
"--version",
"1.2.3",
"--package-base-url",
"http://gitea.example/api/packages/u/generic/app",
"--pub-date",
"2026-08-16T00:00:00Z",
],
check=True,
)
manifest = json.loads((output / "latest.json").read_text(encoding="utf-8"))
self.assertEqual(manifest["version"], "1.2.3")
self.assertEqual(
set(manifest["platforms"]),
{
"darwin-aarch64-app",
"darwin-aarch64",
"darwin-x86_64-app",
"darwin-x86_64",
"linux-x86_64-appimage",
"linux-x86_64",
"windows-x86_64-nsis",
"windows-x86_64",
},
)
windows = manifest["platforms"]["windows-x86_64"]
self.assertEqual(windows["signature"], "signature-gitea-windows")
self.assertTrue(windows["url"].endswith("/1.2.3/dsh-easy-desktop_1.2.3_windows_x86_64.exe"))
if __name__ == "__main__":
unittest.main()

233
tests/voice_client.test.mjs Normal file
View File

@@ -0,0 +1,233 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import vm from 'node:vm'
const CLIENT = new URL('../plugins/dsh-desktop-voice/client.js', import.meta.url)
function loadVoiceClient({ confirmInstall = () => false, engine = 'sensevoice', holdInstall = false } = {}) {
let plugin
const registered = new Map()
const indicatorParts = {
'.label': { textContent: '' },
'.kbd': { textContent: '' },
'.stop': { hidden: false },
'.progress': { hidden: true, value: 0 },
'.percent': { hidden: true, textContent: '' },
}
const indicator = {
classList: { toggle() {} },
querySelector(selector) {
return indicatorParts[selector]
},
}
const window = {
__ModuleLoader__: {
load(definition) {
plugin = definition.factory((id) => {
if (id === 'react') return React
if (id === 'react/jsx-runtime') return jsx
throw new Error(`unexpected module: ${id}`)
})
},
},
addEventListener() {},
confirm: confirmInstall,
}
const document = {
querySelector() {
return {}
},
getElementById() {
return indicator
},
}
const jsx = {
jsx(type, props, key) {
return { type, props: props || {}, key }
},
jsxs(type, props, key) {
return { type, props: props || {}, key }
},
}
let hooks = []
let hookIndex = 0
const React = {
useState(initial) {
const index = hookIndex++
if (!(index in hooks)) hooks[index] = typeof initial === 'function' ? initial() : initial
return [hooks[index], (next) => {
hooks[index] = typeof next === 'function' ? next(hooks[index]) : next
}]
},
useRef(initial) {
const index = hookIndex++
if (!(index in hooks)) hooks[index] = { current: initial }
return hooks[index]
},
useEffect(effect, dependencies) {
const index = hookIndex++
const previous = hooks[index]
const changed = !dependencies || !previous || dependencies.some((value, i) => value !== previous[i])
hooks[index] = dependencies || null
if (changed) effect()
},
}
const requests = []
let micRequests = 0
const context = {
AudioContext: function AudioContext() {},
Blob,
DataView,
Float32Array,
MediaRecorder: undefined,
URL,
clearTimeout,
clearInterval,
console,
document,
encodeURIComponent,
fetch: async (url, options = {}) => {
requests.push({ url, method: options.method || 'GET' })
if (url === '/dsh-desktop/voice') {
return {
ok: true,
async json() {
return {
enabled: true,
engine,
dictationMode: 'toggle',
language: 'zh',
modelInstalled: false,
}
},
}
}
if (url === '/dsh-desktop/voice/model' && (!options.method || options.method === 'GET')) {
return {
ok: true,
async json() {
return { status: 'installing', percent: 42, stage: '正在下载 SenseVoice 模型…' }
},
}
}
if (url === '/dsh-desktop/voice/model' && options.method === 'POST') {
if (holdInstall) return new Promise(() => {})
return {
ok: true,
async json() {
return { installed: true }
},
}
}
throw new Error(`unexpected request: ${options.method || 'GET'} ${url}`)
},
navigator: {
language: 'zh-CN',
platform: 'Linux x86_64',
mediaDevices: {
getUserMedia() {
micRequests += 1
return new Promise(() => {})
},
},
},
setInterval,
setTimeout,
window,
}
vm.runInNewContext(readFileSync(CLIENT, 'utf8'), context, { filename: CLIENT.pathname })
plugin.apply({
slots: {
inject(_name, register) {
register()
},
register(meta, component) {
registered.set(meta.name, component)
},
},
})
const MicButton = registered.get('conversation.input.right')
assert.equal(typeof MicButton, 'function')
const VoiceSettings = registered.get('settings.section')
assert.equal(typeof VoiceSettings, 'function')
const props = {
inputActions: { setDraft() {} },
useInput() {
return ''
},
}
return {
indicatorParts,
requests,
get micRequests() {
return micRequests
},
async renderMicReady() {
hooks = []
hookIndex = 0
MicButton(props)
await new Promise((resolve) => setTimeout(resolve, 0))
hookIndex = 0
return MicButton(props)
},
async renderSettingsReady() {
hooks = []
hookIndex = 0
VoiceSettings({})
await new Promise((resolve) => setTimeout(resolve, 0))
hookIndex = 0
return VoiceSettings({})
},
}
}
test('microphone click offers to install a missing SenseVoice model', async () => {
let prompts = 0
const harness = loadVoiceClient({
confirmInstall(message) {
prompts += 1
assert.match(message, /SenseVoice/)
assert.match(message, /安装/)
return true
},
})
const button = await harness.renderMicReady()
button.props.onClick()
await new Promise((resolve) => setTimeout(resolve, 0))
assert.equal(prompts, 1)
assert.deepEqual(
harness.requests.filter((request) => request.method === 'POST'),
[{ url: '/dsh-desktop/voice/model', method: 'POST' }],
)
assert.equal(harness.micRequests, 1)
})
function textOf(node) {
if (node == null || typeof node === 'boolean') return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(textOf).join(' ')
return textOf(node.props && node.props.children)
}
test('OpenAI fields only render for the OpenAI engine', async () => {
const localSettings = textOf(await loadVoiceClient({ engine: 'sensevoice' }).renderSettingsReady())
assert.doesNotMatch(localSettings, /接口地址|API 密钥|Whisper 模型/)
const openAISettings = textOf(await loadVoiceClient({ engine: 'openai' }).renderSettingsReady())
assert.match(openAISettings, /接口地址/)
assert.match(openAISettings, /API 密钥/)
assert.match(openAISettings, /Whisper 模型/)
})
test('model installation renders live progress', async () => {
const harness = loadVoiceClient({ confirmInstall: () => true, holdInstall: true })
const button = await harness.renderMicReady()
button.props.onClick()
await new Promise((resolve) => setTimeout(resolve, 0))
assert.equal(harness.indicatorParts['.progress'].hidden, false)
assert.equal(harness.indicatorParts['.progress'].value, 42)
assert.equal(harness.indicatorParts['.percent'].textContent, '42%')
})

112
tests/voice_host.test.mjs Normal file
View File

@@ -0,0 +1,112 @@
import assert from 'node:assert/strict'
import { mkdirSync, mkdtempSync, rmSync, truncateSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
function response() {
return {
status: 0,
body: '',
writeHead(status) {
this.status = status
return this
},
end(body = '') {
this.body = body
},
}
}
function request(method, url) {
return {
method,
url,
headers: {},
async *[Symbol.asyncIterator]() {},
}
}
test('voice host reports and accepts an on-demand SenseVoice installation', async () => {
const home = mkdtempSync(join(tmpdir(), 'dsh-desktop-voice-test-'))
process.env.DSH_DESKTOP_VOICE_HOME = home
process.env.XDG_CONFIG_HOME = join(home, 'config')
const configDir = join(home, 'config', 'dsh-desktop')
mkdirSync(configDir, { recursive: true })
writeFileSync(join(configDir, 'voice.json'), JSON.stringify({ engine: 'auto' }))
try {
const routes = new Map()
const plugin = await import(`../plugins/dsh-desktop-voice/index.js?test=${Date.now()}`)
plugin.apply({
inject(_dependencies, load) {
load({
webServer: {
register(route) {
routes.set(route.path, route.handler)
},
},
})
},
})
const configResponse = response()
await routes.get('/dsh-desktop/voice')(request('GET', '/dsh-desktop/voice'), configResponse)
assert.equal(configResponse.status, 200)
const missing = JSON.parse(configResponse.body)
assert.equal(missing.engine, 'sensevoice')
assert.equal(missing.modelInstalled, false)
assert.equal(missing.modelStatus, 'missing')
assert.deepEqual(missing.options.engines.map((engine) => engine.id), ['sensevoice', 'openai'])
const missingProgressResponse = response()
await routes.get('/dsh-desktop/voice/model')(
request('GET', '/dsh-desktop/voice/model'),
missingProgressResponse,
)
assert.deepEqual(JSON.parse(missingProgressResponse.body), {
status: 'missing',
percent: 0,
stage: '',
})
const modelDir = join(home, 'sensevoice')
const packageDir = join(home, 'runtime', 'node_modules', 'sherpa-onnx')
mkdirSync(modelDir, { recursive: true })
mkdirSync(packageDir, { recursive: true })
writeFileSync(join(modelDir, 'model.int8.onnx'), '')
truncateSync(join(modelDir, 'model.int8.onnx'), 239233841)
writeFileSync(join(modelDir, 'tokens.txt'), '')
truncateSync(join(modelDir, 'tokens.txt'), 315894)
writeFileSync(
join(modelDir, 'installed.json'),
JSON.stringify({
modelVersion: '2365baeacb507f821a0c8120fcee3d484dba7a07',
runtimeVersion: '1.13.5',
}),
)
writeFileSync(join(packageDir, 'package.json'), JSON.stringify({ version: '1.13.5' }))
const installResponse = response()
await routes.get('/dsh-desktop/voice/model')(request('POST', '/dsh-desktop/voice/model'), installResponse)
assert.equal(installResponse.status, 200)
const installed = JSON.parse(installResponse.body)
assert.equal(installed.installed, true)
assert.equal(installed.modelInstalled, true)
assert.equal(installed.modelStatus, 'installed')
const installedProgressResponse = response()
await routes.get('/dsh-desktop/voice/model')(
request('GET', '/dsh-desktop/voice/model'),
installedProgressResponse,
)
assert.deepEqual(JSON.parse(installedProgressResponse.body), {
status: 'installed',
percent: 100,
stage: 'SenseVoice 已安装',
})
} finally {
delete process.env.XDG_CONFIG_HOME
delete process.env.DSH_DESKTOP_VOICE_HOME
rmSync(home, { recursive: true, force: true })
}
})