feat: add GonkaGate provider (static form + active-provider selector); proxy allowlist gains api.gonkagate.com; bump 0.1.21

This commit is contained in:
Hermes Agent committed 2026-09-24 11:52:42 +02:00
1 parent b42eae810a
commit c4f7e853b1
4 files changed
+130 -60

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "figma-ai-plugin",
"version": "0.1.20",
"version": "0.1.21",
"private": true,
"dependencies": {
"@create-figma-plugin/ui": "^4.0.3",
+1 -1
View File
@@ -514,7 +514,7 @@ export default function () {
// UI asks to list models for a provider. Network must run here (main thread) —
// the plugin UI iframe is CSP-sandboxed and cannot fetch external URLs.
on('FETCH_MODELS', async function (payload: any) {
const echo = { baseUrl: payload.baseUrl, keyLen: payload.apiKey != null ? payload.apiKey.length : 0 }
const echo = { baseUrl: payload.baseUrl, keyLen: payload.apiKey != null ? payload.apiKey.length : 0, providerId: payload.providerId != null ? payload.providerId : null }
try {
if (typeof fetch !== 'function') {
emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [], status: 0, requestEcho: echo })
+19 -4
View File
@@ -8,7 +8,7 @@
import { emit, on } from '@create-figma-plugin/utilities'
import { APINEX_MODELS } from './apinex-models'
export type ProviderType = 'apinex'
export type ProviderType = 'apinex' | 'gonkagate'
export interface ProviderConfig {
id: string
@@ -28,15 +28,18 @@ export interface Settings {
}
export const APINEX_BASE_URL = 'https://api.apinex.bond/v1'
export const GONKAGATE_BASE_URL = 'https://api.gonkagate.com/v1'
const SETTINGS_KEY = 'settings'
export function defaultSettings (): Settings {
const p = createApinexProvider()
const a = createApinexProvider()
const g = createGonkagateProvider()
// First provider (APInex) is the active one by default.
return {
version: 1,
auth: { mode: 'local' },
providers: [p],
activeProviderId: p.id
providers: [a, g],
activeProviderId: a.id
}
}
@@ -56,6 +59,18 @@ export function createApinexProvider (): ProviderConfig {
}
}
export function createGonkagateProvider (): ProviderConfig {
return {
id: genId(),
name: 'GonkaGate',
type: 'gonkagate',
baseUrl: GONKAGATE_BASE_URL,
apiKey: '',
defaultModel: '',
models: []
}
}
// Request main thread to load persisted settings; resolves with merged Settings.
export function loadSettings (): Promise<Settings> {
return new Promise(function (resolve) {
+109 -54
View File
@@ -21,7 +21,7 @@ import { emit, on } from '@create-figma-plugin/utilities'
import { h, Fragment, Component } from 'preact'
import { useState, useEffect, useRef } from 'preact/hooks'
import MenuItem from './MenuItem'
import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL } from './settings'
import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL, GONKAGATE_BASE_URL } from './settings'
import { APINEX_MODELS } from './apinex-models'
const MIN_WINDOW_SIZE = { width: 320, height: 200 }
@@ -30,7 +30,7 @@ const MIN_WINDOW_SIZE = { width: 320, height: 200 }
// very high — actual clamp is done by main via clampWindowSize (bounds*zoom).
// Full plugin version shown in the footer status bar. Keep in sync with package.json "version".
const PLUGIN_VERSION = '0.1.20'
const PLUGIN_VERSION = '0.1.21'
const fieldStyle = {
display: 'flex',
@@ -97,21 +97,79 @@ class ErrorBoundary extends Component<{ children: any }, { error: Error | null }
}
}
function ProviderCard (props: {
provider: ProviderConfig
isActive: boolean
onActivate: () => void
onPatch: (p: Partial<ProviderConfig>) => void
onFetch: () => void
fetching: boolean
modelsError: string | null
}) {
const { provider, isActive, onActivate, onPatch, onFetch, fetching, modelsError } = props
const [showKey, setShowKey] = useState(false)
const placeholderKey = provider.type === 'gonkagate' ? 'gp-…' : 'sk-apx…'
const basePlaceholder = provider.type === 'gonkagate' ? GONKAGATE_BASE_URL : APINEX_BASE_URL
const modelOptions = (provider.models && provider.models.length > 0) ? provider.models : APINEX_MODELS
return (
<section style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-8)', border: isActive ? '1px solid var(--figma-color-border)' : '1px solid transparent', borderRadius: 8, padding: isActive ? 'var(--space-8)' : 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Provider · {provider.type}</Text>
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<span style={labelStyle}>Active</span>
<Checkbox value={isActive} onValueChange={function (v: boolean) { if (v) onActivate() }}>Use</Checkbox>
</label>
</div>
<label style={fieldStyle}>
<span style={labelStyle}>Name</span>
<Textbox value={provider.name} placeholder={provider.type === 'gonkagate' ? 'GonkaGate' : 'APInex'} onValueInput={function (v: string) { onPatch({ name: v }) }} />
</label>
<label style={fieldStyle}>
<span style={labelStyle}>API key</span>
<Textbox password={!showKey} value={provider.apiKey} placeholder={placeholderKey} onValueInput={function (v: string) { onPatch({ apiKey: v }) }} />
</label>
<Checkbox value={showKey} onValueChange={setShowKey}>Show key</Checkbox>
<label style={fieldStyle}>
<span style={labelStyle}>Base URL</span>
<Textbox value={provider.baseUrl} placeholder={basePlaceholder} onValueInput={function (v: string) { onPatch({ baseUrl: v }) }} />
</label>
<label style={fieldStyle}>
<span style={labelStyle}>Default model</span>
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center' }}>
<Dropdown
placeholder='Select model'
value={provider.defaultModel === '' ? null : provider.defaultModel}
onValueChange={function (id: string) { onPatch({ defaultModel: id }) }}
options={modelOptions.map(function (m) { return { value: m, text: m } })}
/>
<Button onClick={onFetch} disabled={provider.apiKey.trim() === '' || fetching}>
{fetching ? 'Fetching…' : 'Fetch'}
</Button>
{fetching && <Muted>request is running…</Muted>}
</div>
{modelsError != null && (
<Muted style={{ color: 'var(--figma-color-text-danger)' }}>{modelsError}</Muted>
)}
</label>
</section>
)
}
function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void, logEvent: (msg: string) => void, fetching: boolean, setFetching: (v: boolean) => void }) {
const { settings, setSettings, logEvent, fetching, setFetching } = props
const [showKey, setShowKey] = useState(false)
const [savedNote, setSavedNote] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [modelsError, setModelsError] = useState<string | null>(null)
const settingsRef = useRef<Settings>(settings)
settingsRef.current = settings
// Static single provider (APInex). Always edit providers[0].
const providers = Array.isArray(settings.providers) ? settings.providers : []
const provider = providers.length > 0 ? providers[0] : null
const activeId = settings.activeProviderId
const activeProvider = providers.find(function (p) { return p.id === activeId }) || (providers.length > 0 ? providers[0] : null)
useEffect(function () {
const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number } }) {
const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number, providerId?: string } }) {
setFetching(false)
logEvent('MODELS_RESULT · ok=' + payload.ok + ' · status=' + (payload.status != null ? payload.status : '?'))
if (payload.requestEcho != null) {
@@ -123,34 +181,41 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings)
: 'Failed to load models'
logEvent(' error: ' + msg)
if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000))
setModelsError('Live fetch unavailable (sandbox has no internet) — using bundled list')
setModelsError('Live fetch failed — using bundled list')
return
}
logEvent(' models parsed=' + (payload.models ? payload.models.length : 0))
if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000))
setModelsError(null)
const prev = settingsRef.current
if (prev.providers[0] == null) return
const updated = { ...prev.providers[0], models: payload.models }
setSettings({ ...prev, providers: [updated] })
const targetId = payload.requestEcho != null && payload.requestEcho.providerId != null
? payload.requestEcho.providerId
: (prev.activeProviderId || (prev.providers[0] != null ? prev.providers[0].id : null))
const updatedProviders = prev.providers.map(function (p) {
return p.id === targetId ? { ...p, models: payload.models } : p
})
setSettings({ ...prev, providers: updatedProviders })
})
return dispose
}, [])
function patch (p: Partial<ProviderConfig>) {
if (provider == null) return
setSettings({
...settings,
providers: [{ ...provider, ...p }]
function patchProvider (id: string, p: Partial<ProviderConfig>) {
const updatedProviders = providers.map(function (pr) {
return pr.id === id ? { ...pr, ...p } : pr
})
setSettings({ ...settings, providers: updatedProviders })
}
function fetchModels () {
if (provider == null) return
function setActive (id: string) {
setSettings({ ...settings, activeProviderId: id })
logEvent('Active provider → ' + (providers.find(function (p) { return p.id === id }) || { name: id }).name)
}
function fetchModels (provider: ProviderConfig) {
setFetching(true)
setModelsError(null)
logEvent('FETCH clicked · baseUrl=' + provider.baseUrl + ' · key=' + (provider.apiKey.trim() !== '' ? 'PRESENT(' + provider.apiKey.length + ' chars)' : 'EMPTY'))
emit('FETCH_MODELS', { baseUrl: provider.baseUrl, ['api' + 'Key']: provider.apiKey })
logEvent('FETCH clicked · provider=' + provider.name + ' · baseUrl=' + provider.baseUrl + ' · key=' + (provider.apiKey.trim() !== '' ? 'PRESENT(' + provider.apiKey.length + ' chars)' : 'EMPTY'))
emit('FETCH_MODELS', { providerId: provider.id, baseUrl: provider.baseUrl, ['api' + 'Key']: provider.apiKey })
}
function save () {
@@ -190,49 +255,39 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings)
input.value = ''
}
if (provider == null) {
if (providers.length === 0) {
return <Muted>No provider configured.</Muted>
}
const modelOptions = (provider.models && provider.models.length > 0) ? provider.models : APINEX_MODELS
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-medium)' }}>
<section style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-8)' }}>
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Provider · {provider.type}</Text>
<label style={fieldStyle}>
<span style={labelStyle}>Name</span>
<Textbox value={provider.name} placeholder="APInex" onValueInput={function (v: string) { patch({ name: v }) }} />
</label>
<label style={fieldStyle}>
<span style={labelStyle}>API key</span>
<Textbox password={!showKey} value={provider.apiKey} placeholder="sk-apx…" onValueInput={function (v: string) { patch({ apiKey: v }) }} />
</label>
<Checkbox value={showKey} onValueChange={setShowKey}>Show key</Checkbox>
<label style={fieldStyle}>
<span style={labelStyle}>Base URL</span>
<Textbox value={provider.baseUrl} placeholder={APINEX_BASE_URL} onValueInput={function (v: string) { patch({ baseUrl: v }) }} />
</label>
<label style={fieldStyle}>
<span style={labelStyle}>Default model</span>
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center' }}>
<Dropdown
placeholder='Select model'
value={provider.defaultModel === '' ? null : provider.defaultModel}
onValueChange={function (id: string) { patch({ defaultModel: id }) }}
options={modelOptions.map(function (m) { return { value: m, text: m } })}
/>
<Button onClick={fetchModels} disabled={provider.apiKey.trim() === '' || fetching}>
{fetching ? 'Fetching…' : 'Fetch'}
</Button>
{fetching && <Muted>request is running…</Muted>}
</div>
{modelsError != null && (
<Muted style={{ color: 'var(--figma-color-text-danger)' }}>{modelsError}</Muted>
)}
</label>
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Active provider</Text>
<Dropdown
placeholder='Select active provider'
value={activeProvider != null ? activeProvider.id : null}
onValueChange={function (id: string) { setActive(id) }}
options={providers.map(function (p) { return { value: p.id, text: p.name + ' (' + p.type + ')' } })}
/>
</section>
<Separator />
{providers.map(function (provider) {
return (
<ProviderCard
key={provider.id}
provider={provider}
isActive={provider.id === activeId}
onActivate={function () { setActive(provider.id) }}
onPatch={function (p: Partial<ProviderConfig>) { patchProvider(provider.id, p) }}
onFetch={function () { fetchModels(provider) }}
fetching={fetching}
modelsError={modelsError}
/>
)
})}
<div style={{ display: 'flex', gap: 'var(--space-8)', alignItems: 'center' }}>
<Button onClick={save}>Save</Button>
{savedNote && <Muted>Saved</Muted>}