mirror of
https://github.com/TommyFang2077/dsh-desktop.git
synced 2026-08-17 09:06:36 +08:00
release: v0.1.2
- bundle dshmarket 1.10.1; drop the duplicate vision settings section (modlens card is the single config surface) - embed the updater signing public key in every build (Makefile / CI / Flatpak); in-app updates now actually run - structured release notes per version: docs/release-notes/v<version>.md feeds GitHub, Gitea and the in-app update panel - gate releases on the notes file; Gitea publish reads/patches the same body
This commit is contained in:
@@ -1,209 +0,0 @@
|
||||
window.__ModuleLoader__.load({
|
||||
id: 'dsh-desktop-vision',
|
||||
factory: (require) => {
|
||||
var module = { exports: {} }
|
||||
var exports = module.exports
|
||||
var React = require('react')
|
||||
var jsx = require('react/jsx-runtime')
|
||||
|
||||
var css = [
|
||||
'.dshdv{width:100%;max-width:640px;display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary)}',
|
||||
'.dshdv h3{margin:0;font-size:15px;font-weight:600;line-height:22px}',
|
||||
'.dshdv p{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}',
|
||||
'.dshdv label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
||||
'.dshdv .head{display:flex;align-items:center;justify-content:space-between;gap:12px}',
|
||||
'.dshdv a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-size:12px;line-height:18px}',
|
||||
'.dshdv a:hover{text-decoration:underline}',
|
||||
'.dshdv input,.dshdv select{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit}',
|
||||
'.dshdv input:focus,.dshdv select:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}',
|
||||
'.dshdv button{align-self:flex-start;height:32px;padding:0 14px;border:0;border-radius:8px;background:var(--dsw-alias-state-business-primary);color:#fff;font:inherit;cursor:pointer}',
|
||||
'.dshdv button:disabled{opacity:.55;cursor:default}',
|
||||
'.dshdv .ok{color:var(--dsw-alias-state-success-primary)}',
|
||||
'.dshdv .err{color:var(--dsw-alias-state-error-primary)}',
|
||||
].join('')
|
||||
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined') return
|
||||
if (document.querySelector('style[data-plugin-css="dsh-desktop-vision"]')) return
|
||||
var tag = document.createElement('style')
|
||||
tag.dataset.pluginCss = 'dsh-desktop-vision'
|
||||
tag.textContent = css
|
||||
document.head.appendChild(tag)
|
||||
}
|
||||
|
||||
function remote(provider) {
|
||||
var q = provider ? '?provider=' + encodeURIComponent(provider) : ''
|
||||
return fetch('/dsh-desktop/modlens' + q).then(function (res) {
|
||||
if (!res.ok) throw new Error('load failed ' + res.status)
|
||||
return res.json()
|
||||
})
|
||||
}
|
||||
|
||||
function VisionSettings() {
|
||||
ensureCss()
|
||||
var state = React.useState({ status: 'loading' })
|
||||
var snap = state[0]
|
||||
var setSnap = state[1]
|
||||
React.useEffect(function () {
|
||||
var live = true
|
||||
remote()
|
||||
.then(function (form) {
|
||||
if (live) setSnap({ status: 'ready', form: form, message: '' })
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (live) setSnap({ status: 'error', message: String(error.message || error) })
|
||||
})
|
||||
return function () {
|
||||
live = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
function patch(field, value) {
|
||||
setSnap(function (cur) {
|
||||
if (cur.status !== 'ready') return cur
|
||||
return { status: 'ready', form: Object.assign({}, cur.form, { [field]: value }), message: '' }
|
||||
})
|
||||
}
|
||||
|
||||
function onProvider(id) {
|
||||
remote(id)
|
||||
.then(function (form) {
|
||||
setSnap({ status: 'ready', form: form, message: '' })
|
||||
})
|
||||
.catch(function () {
|
||||
patch('provider', id)
|
||||
})
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (snap.status !== 'ready' || snap.saving) return
|
||||
setSnap(Object.assign({}, snap, { saving: true, message: '' }))
|
||||
fetch('/dsh-desktop/modlens', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(snap.form),
|
||||
})
|
||||
.then(function (res) {
|
||||
return res.json().then(function (body) {
|
||||
if (!res.ok) throw new Error(body.error || 'save failed')
|
||||
return body
|
||||
})
|
||||
})
|
||||
.then(function (form) {
|
||||
setSnap({ status: 'ready', form: form, message: '已保存', saving: false })
|
||||
})
|
||||
.catch(function (error) {
|
||||
setSnap(Object.assign({}, snap, { saving: false, message: String(error.message || error) }))
|
||||
})
|
||||
}
|
||||
|
||||
if (snap.status === 'loading') {
|
||||
return jsx.jsx('div', { className: 'dshdv', children: jsx.jsx('p', { children: '正在读取 ModLens 配置…' }) })
|
||||
}
|
||||
if (snap.status === 'error') {
|
||||
return jsx.jsx('div', { className: 'dshdv', children: jsx.jsx('p', { className: 'err', children: snap.message }) })
|
||||
}
|
||||
var form = snap.form
|
||||
var remoteEngine = form.provider === 'openai' || form.provider === 'gemini-api' || form.provider === 'anthropic'
|
||||
var options = form.options || []
|
||||
var current = options.filter(function (item) { return item.id === form.provider })[0]
|
||||
var apiUrl = (current && current.apiUrl) || form.apiUrl || ''
|
||||
var example = (current && current.example) || form.example || ''
|
||||
var getApi = apiUrl
|
||||
? jsx.jsx('a', { href: apiUrl, children: '获取 API' })
|
||||
: null
|
||||
return jsx.jsxs('div', {
|
||||
className: 'dshdv',
|
||||
children: [
|
||||
jsx.jsx('h3', { children: 'ModLens 视觉模型' }),
|
||||
jsx.jsx('p', {
|
||||
children:
|
||||
'纯文本对话模型读图时使用这里的引擎。已声明视觉能力的模型(如 Qwen)不会走这条桥。',
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'引擎',
|
||||
jsx.jsx('select', {
|
||||
value: form.provider,
|
||||
onChange: function (event) {
|
||||
onProvider(event.target.value)
|
||||
},
|
||||
children: options.map(function (item) {
|
||||
return jsx.jsx('option', { value: item.id, children: item.label }, item.id)
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'接口地址',
|
||||
jsx.jsx('input', {
|
||||
value: form.baseUrl || '',
|
||||
disabled: !remoteEngine,
|
||||
placeholder: form.officialBaseUrl || '',
|
||||
onChange: function (event) {
|
||||
patch('baseUrl', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
jsx.jsxs('span', { className: 'head', children: ['API 密钥', getApi] }),
|
||||
jsx.jsx('input', {
|
||||
type: 'password',
|
||||
value: form.apiKey || '',
|
||||
disabled: !remoteEngine,
|
||||
onChange: function (event) {
|
||||
patch('apiKey', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'视觉模型',
|
||||
jsx.jsx('input', {
|
||||
value: form.model || '',
|
||||
placeholder: example ? '例如 ' + example : '',
|
||||
onChange: function (event) {
|
||||
patch('model', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsx('button', {
|
||||
type: 'button',
|
||||
disabled: !!snap.saving,
|
||||
onClick: save,
|
||||
children: snap.saving ? '保存中…' : '保存',
|
||||
}),
|
||||
snap.message
|
||||
? jsx.jsx('p', {
|
||||
className: /失败|failed|error/i.test(snap.message) ? 'err' : 'ok',
|
||||
children: snap.message,
|
||||
})
|
||||
: null,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function apply(ctx) {
|
||||
ctx.slots.inject('settings.section', function () {
|
||||
return ctx.slots.register(
|
||||
{
|
||||
name: 'settings.section',
|
||||
id: 'modlens-vision',
|
||||
order: 12,
|
||||
label: '视觉模型',
|
||||
},
|
||||
VisionSettings,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
exports.apply = apply
|
||||
exports.inject = ['slots']
|
||||
return module.exports
|
||||
},
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
# Settings page + host route for the ModLens vision engine.
|
||||
- insert:
|
||||
- id: dsh-desktop-vision
|
||||
name: dsh-desktop-vision
|
||||
@@ -1,181 +0,0 @@
|
||||
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
export const name = 'dsh-desktop-vision'
|
||||
export const inject = []
|
||||
|
||||
const PROVIDERS = [
|
||||
{
|
||||
id: 'openai',
|
||||
label: 'OpenAI 兼容',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
apiUrl: 'https://platform.openai.com/api-keys',
|
||||
example: 'gpt-4o',
|
||||
},
|
||||
{
|
||||
id: 'gemini-api',
|
||||
label: 'Gemini API',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
apiUrl: 'https://aistudio.google.com/apikey',
|
||||
example: 'gemini-3.6-flash',
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
label: 'Anthropic API',
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
apiUrl: 'https://console.anthropic.com/settings/keys',
|
||||
example: 'claude-haiku-4-5-20251001',
|
||||
},
|
||||
{
|
||||
id: 'antigravity-cli',
|
||||
label: 'Antigravity CLI(免费)',
|
||||
baseUrl: '',
|
||||
apiUrl: 'https://antigravity.google/',
|
||||
example: 'gemini-3.6-flash-low',
|
||||
},
|
||||
{
|
||||
id: 'claude-cli',
|
||||
label: 'Claude Code 登录',
|
||||
baseUrl: '',
|
||||
apiUrl: 'https://code.claude.com',
|
||||
example: 'haiku',
|
||||
},
|
||||
]
|
||||
const PROVIDER_IDS = new Set(PROVIDERS.map((item) => item.id))
|
||||
|
||||
function providerOf(id) {
|
||||
return PROVIDERS.find((entry) => entry.id === id)
|
||||
}
|
||||
|
||||
function officialBaseUrl(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.baseUrl : ''
|
||||
}
|
||||
|
||||
function officialApiUrl(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.apiUrl : ''
|
||||
}
|
||||
|
||||
function officialExample(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.example : ''
|
||||
}
|
||||
const FORM_FIELDS = ['apiKey', 'baseUrl', 'model']
|
||||
|
||||
function configPath() {
|
||||
const home = process.env.MODLENS_HOME || join(homedir(), '.modlens')
|
||||
return join(home, 'config.json')
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(configPath(), 'utf8'))
|
||||
return data && typeof data === 'object' ? data : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function formOf(cfg, provider) {
|
||||
const id = PROVIDER_IDS.has(provider) ? provider : 'openai'
|
||||
const entry =
|
||||
cfg.providers && typeof cfg.providers === 'object' && typeof cfg.providers[id] === 'object'
|
||||
? cfg.providers[id]
|
||||
: {}
|
||||
return {
|
||||
provider: id,
|
||||
apiKey: String(entry.apiKey || ''),
|
||||
baseUrl: String(entry.baseUrl || officialBaseUrl(id)),
|
||||
officialBaseUrl: officialBaseUrl(id),
|
||||
apiUrl: officialApiUrl(id),
|
||||
example: officialExample(id),
|
||||
model: String(entry.model || ''),
|
||||
}
|
||||
}
|
||||
|
||||
function saveForm(values) {
|
||||
const provider = String(values.provider || 'openai').trim()
|
||||
if (!PROVIDER_IDS.has(provider)) {
|
||||
const error = new Error(`unknown provider: ${provider}`)
|
||||
error.status = 400
|
||||
throw error
|
||||
}
|
||||
const cfg = loadConfig()
|
||||
cfg.provider = provider
|
||||
if (!cfg.providers || typeof cfg.providers !== 'object') cfg.providers = {}
|
||||
if (!cfg.providers[provider] || typeof cfg.providers[provider] !== 'object') {
|
||||
cfg.providers[provider] = {}
|
||||
}
|
||||
const entry = cfg.providers[provider]
|
||||
for (const field of FORM_FIELDS) {
|
||||
const raw = String(values[field] || '').trim()
|
||||
if (raw) entry[field] = raw
|
||||
else delete entry[field]
|
||||
}
|
||||
const path = configPath()
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`, { encoding: 'utf8' })
|
||||
try {
|
||||
chmodSync(path, 0o600)
|
||||
} catch {
|
||||
// best-effort; some filesystems ignore mode
|
||||
}
|
||||
return formOf(cfg, provider)
|
||||
}
|
||||
|
||||
async function readJsonBody(req) {
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
||||
if (!raw) return {}
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
export function apply(ctx) {
|
||||
if (typeof ctx.inject !== 'function') return
|
||||
ctx.inject(['webServer'], (scope) => {
|
||||
try {
|
||||
scope.webServer.register({
|
||||
name: 'dsh-desktop-modlens',
|
||||
kind: 'exact',
|
||||
path: '/dsh-desktop/modlens',
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
if (req.method === 'GET') {
|
||||
const cfg = loadConfig()
|
||||
const provider = new URL(req.url, 'http://localhost').searchParams.get('provider')
|
||||
json(res, 200, {
|
||||
...formOf(cfg, provider || cfg.provider || 'openai'),
|
||||
options: PROVIDERS.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
officialBaseUrl: item.baseUrl,
|
||||
apiUrl: item.apiUrl,
|
||||
example: item.example,
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (req.method === 'PUT' || req.method === 'POST') {
|
||||
const body = await readJsonBody(req)
|
||||
json(res, 200, { ...saveForm(body), saved: true })
|
||||
return
|
||||
}
|
||||
res.writeHead(405).end()
|
||||
} catch (error) {
|
||||
json(res, error.status || 500, { error: String(error?.message || error) })
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`[dsh-desktop-vision] config route skipped: ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "dsh-desktop-vision",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-ui-settings"
|
||||
],
|
||||
"platform": "web",
|
||||
"immediately": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user