feat(aiturk): one-click sign-in for AITURK IDE (1.0.0-beta.14)

The onboarding and provider settings get a Sign in button: the IDE shows a
short code, opens the turkservis.online approval page, and after approval
saves the TurkServis provider itself and hands a separate member key to the
AITURK CLI over stdin (aiturk login --key-stdin --keep-other-account).
Keys never reach the renderer, logs, process arguments or URLs. Pasting an
API key remains as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-03 15:13:23 +03:00
1 parent fe0a45a84f
commit 25d8c18c10
13 files changed
+1938 -15

No files matched your search

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { AiturkLogin } from '@/components/aiturk-login'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
@@ -80,7 +81,10 @@ function toPayload(form: EndpointForm, models?: string[]): CustomEndpointUpdate
export function CustomEndpointsSettings({ onConfigSaved, onMainModelChanged }: CustomEndpointsSettingsProps) {
const { t } = useI18n()
const c = useAiturkCopy().custom
const aiturkCopy = useAiturkCopy()
const c = aiturkCopy.custom
const loginCopy = aiturkCopy.login
const loginLabel = loginCopy.settingsSignIn
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
@@ -129,6 +133,31 @@ export function CustomEndpointsSettings({ onConfigSaved, onMainModelChanged }: C
}
}, [])
// One-click sign-in saved the TurkServis provider in the main process:
// reload the list and tell the app its default model changed.
async function handleAiturkConnected() {
try {
const data = await getCustomEndpoints()
setEndpoints(data.endpoints)
const current = data.endpoints.find(endpoint => endpoint.id === 'turkservis')
if (current) {
setForm(formFromEndpoint(current))
setDiscoveredModels(current.models)
if (current.is_current) {
onMainModelChanged?.(current.id, current.model)
}
}
triggerHaptic('success')
onConfigSaved?.()
notify({ kind: 'success', message: loginCopy.connected })
} catch (err) {
notifyError(err, c.loadFailed)
}
}
async function handleSave() {
try {
setSaving(true)
@@ -298,10 +327,13 @@ export function CustomEndpointsSettings({ onConfigSaved, onMainModelChanged }: C
</section>
<section>
<Button className="mb-3" onClick={() => {
setForm({ ...EMPTY_FORM, name: 'TurkServis · AITURK', baseUrl: AITURK_API_BASE_URL })
setDiscoveredModels([])
}} variant="outline">{c.preset}</Button>
<div className="mb-3 flex flex-wrap items-start gap-2">
<AiturkLogin label={loginLabel} onConnected={handleAiturkConnected} />
<Button onClick={() => {
setForm({ ...EMPTY_FORM, name: 'TurkServis · AITURK', baseUrl: AITURK_API_BASE_URL })
setDiscoveredModels([])
}} variant="outline">{c.preset}</Button>
</div>
<SectionHeading icon={Plus} title={form.id ? c.edit : c.add} />
<div className="grid gap-3 rounded-md border border-border/50 p-3">
<div className="grid gap-3 sm:grid-cols-2">
@@ -0,0 +1,331 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import type { AiturkCliStatus } from '@/global'
import { useAiturkCopy } from '@/i18n/aiturk'
import { aiturkLoginScope } from '@/lib/aiturk-provider'
import { Loader2 } from '@/lib/icons'
import { notify } from '@/store/notifications'
const POLL_INTERVAL_MS = 3000
type Phase = 'idle' | 'starting' | 'waiting' | 'question' | 'replacing'
interface Waiting {
userCode: string
verificationUri: string
}
type LoginCopy = ReturnType<typeof useAiturkCopy>
function errorCode(error: unknown): string {
// The main process only ever throws fixed codes ("aiturk-login:<code>").
return /aiturk-login:([a-z-]+)/.exec(error instanceof Error ? error.message : String(error))?.[1] ?? ''
}
function errorText(code: string, copy: LoginCopy): string {
switch (code) {
case 'rate-limited':
return copy.login.rateLimited
case 'browser-failed':
return copy.login.browserFailed
case 'no-models':
return copy.noModels
case 'provider-failed':
case 'invalid-response':
return copy.failed
default:
return copy.login.unavailable
}
}
function cliText(cli: AiturkCliStatus, copy: LoginCopy): string {
switch (cli) {
case 'ok':
return copy.login.cliOk
case 'outdated':
return copy.login.cliOutdated
case 'failed':
case 'unknown-account':
return copy.login.cliFailed
default:
// 'missing' stays silent; 'other-account' is asked as a question.
return ''
}
}
/**
* One-click TurkServis sign-in, shared by onboarding and Settings.
*
* The API keys never reach this component: the main process shows us only the
* short code, then reports `connected` once it has saved the provider and
* handed the AITURK CLI its own key.
*/
export function useAiturkLogin(onConnected: () => Promise<void> | void) {
const copy = useAiturkCopy()
const [phase, setPhase] = useState<Phase>('idle')
const [waiting, setWaiting] = useState<null | Waiting>(null)
const [error, setError] = useState('')
const [info, setInfo] = useState('')
const live = useRef(false)
const mounted = useRef(true)
const onConnectedRef = useRef(onConnected)
const copyRef = useRef(copy)
onConnectedRef.current = onConnected
copyRef.current = copy
// eslint-disable-next-line no-restricted-syntax -- mount flag + in-flight login flag, not an atom mirror
useEffect(() => {
mounted.current = true
return () => {
mounted.current = false
// Leaving mid-login: revoke the code and drop anything main still holds.
if (live.current) {
live.current = false
void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {})
}
}
}, [])
const finish = useCallback(async (cli: AiturkCliStatus) => {
live.current = false
const text = cliText(cli, copyRef.current)
if (mounted.current) {
setWaiting(null)
setPhase('idle')
setInfo(text)
}
try {
await onConnectedRef.current()
} catch {
// The provider is saved; a failed refresh is the caller's concern.
}
// Onboarding closes this panel as soon as it is connected; keep the CLI
// note visible as a toast in that case.
if (!mounted.current && text) {
notify({ kind: cli === 'ok' ? 'success' : 'info', message: text })
}
}, [])
// eslint-disable-next-line no-restricted-syntax -- clears the in-flight login flag when polling ends
useEffect(() => {
if (phase !== 'waiting') {
return
}
let stopped = false
let timer: ReturnType<typeof setTimeout> | undefined
const tick = async () => {
const bridge = window.hermesDesktop?.aiturkLogin
if (stopped || !bridge) {
return
}
let result
try {
result = await bridge.poll()
} catch {
result = { state: 'pending' as const }
}
if (stopped) {
return
}
if (result.state === 'pending') {
timer = setTimeout(() => void tick(), POLL_INTERVAL_MS)
return
}
if (result.state === 'connected') {
if (result.cli === 'other-account') {
setWaiting(null)
setPhase('question')
return
}
await finish(result.cli)
return
}
live.current = false
setWaiting(null)
setPhase('idle')
setError(result.state === 'expired' ? copyRef.current.login.expired : errorText(result.error, copyRef.current))
}
timer = setTimeout(() => void tick(), POLL_INTERVAL_MS)
return () => {
stopped = true
clearTimeout(timer)
}
}, [phase, finish])
const start = useCallback(async () => {
const bridge = window.hermesDesktop?.aiturkLogin
setError('')
setInfo('')
if (!bridge) {
setError(copyRef.current.login.unsupported)
return
}
setPhase('starting')
try {
const started = await bridge.start(aiturkLoginScope())
if (!mounted.current) {
void bridge.cancel().catch(() => {})
return
}
live.current = true
setWaiting({ userCode: started.userCode, verificationUri: started.verificationUri })
setPhase('waiting')
} catch (err) {
if (mounted.current) {
setPhase('idle')
setError(errorText(errorCode(err), copyRef.current))
}
}
}, [])
const cancel = useCallback(() => {
live.current = false
setWaiting(null)
setPhase('idle')
void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {})
}, [])
const reopen = useCallback(() => {
if (waiting) {
void window.hermesDesktop?.openExternal(waiting.verificationUri).catch(() => {})
}
}, [waiting])
const replaceCli = useCallback(async () => {
setPhase('replacing')
let cli: AiturkCliStatus = 'failed'
try {
cli = (await window.hermesDesktop?.aiturkLogin?.overwriteCli())?.cli ?? 'failed'
} catch {
cli = 'failed'
}
await finish(cli === 'other-account' ? 'failed' : cli)
}, [finish])
const keepCli = useCallback(async () => {
void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {})
await finish('missing')
}, [finish])
return { phase, waiting, error, info, start, cancel, reopen, replaceCli, keepCli }
}
interface AiturkLoginProps {
/** Runs once the TurkServis provider is saved (refresh lists, advance onboarding…). */
onConnected: () => Promise<void> | void
/** Idle button label; defaults to "Giriş yap". */
label?: string
variant?: 'default' | 'outline'
disabled?: boolean
}
export function AiturkLogin({ onConnected, label, variant = 'default', disabled }: AiturkLoginProps) {
const copy = useAiturkCopy()
const login = useAiturkLogin(onConnected)
if (login.phase === 'waiting' && login.waiting) {
return (
<div className="grid gap-3" data-testid="aiturk-login-code">
<div>
<div className="text-xs text-muted-foreground">{copy.login.codeLabel}</div>
<output aria-label={copy.login.codeLabel} className="block font-mono text-3xl font-semibold tracking-[0.3em]">
{login.waiting.userCode}
</output>
</div>
<p className="text-sm text-muted-foreground">{copy.login.confirmHint}</p>
<div className="flex flex-wrap gap-2">
<Button onClick={login.reopen} size="sm" variant="outline">
{copy.login.reopen}
</Button>
<Button onClick={login.cancel} size="sm" variant="ghost">
{copy.login.cancel}
</Button>
</div>
</div>
)
}
if (login.phase === 'question' || login.phase === 'replacing') {
const replacing = login.phase === 'replacing'
return (
<div className="grid gap-3" role="group">
<p className="text-sm">{copy.login.cliOtherAccount}</p>
<div className="flex flex-wrap gap-2">
<Button disabled={replacing} onClick={() => void login.replaceCli()} size="sm">
{replacing ? <Loader2 className="animate-spin" /> : null}
{copy.login.cliReplace}
</Button>
<Button disabled={replacing} onClick={() => void login.keepCli()} size="sm" variant="outline">
{copy.login.cliKeep}
</Button>
</div>
</div>
)
}
const starting = login.phase === 'starting'
return (
<div className="grid gap-2">
<div>
<Button disabled={disabled || starting} onClick={() => void login.start()} variant={variant}>
{starting ? <Loader2 className="animate-spin" /> : null}
{starting ? copy.login.starting : (label ?? copy.login.signIn)}
</Button>
</div>
{login.error ? (
<p className="text-sm text-destructive" role="alert">
{login.error}
</p>
) : null}
{login.info ? (
<p className="text-sm text-muted-foreground" role="status">
{login.info}
</p>
) : null}
</div>
)
}
@@ -1,4 +1,4 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { saveCustomEndpoint, validateCustomEndpoint } from '@/hermes'
@@ -9,7 +9,12 @@ import { AiturkProvider } from './aiturk-provider'
vi.mock('@/hermes', () => ({ saveCustomEndpoint: vi.fn(), validateCustomEndpoint: vi.fn() }))
vi.mock('./flow', () => ({ DocsLink: ({ children, href }: { children: React.ReactNode; href: string }) => <a href={href}>{children}</a> }))
afterEach(() => { cleanup(); vi.resetAllMocks() })
afterEach(() => {
cleanup()
vi.resetAllMocks()
vi.useRealTimers()
delete (window as { hermesDesktop?: unknown }).hermesDesktop
})
function setup() {
const onConnected = vi.fn().mockResolvedValue(undefined)
@@ -17,6 +22,7 @@ function setup() {
const input = screen.getByLabelText('TurkServis API anahtarı') as HTMLInputElement
fireEvent.change(input, { target: { value: 'test-key-never-real' } })
fireEvent.click(screen.getByRole('button', { name: 'Bağlan' }))
return { input, onConnected }
}
@@ -47,3 +53,163 @@ describe('TurkServis provider setup', () => {
expect(saveCustomEndpoint).not.toHaveBeenCalled()
})
})
const URI = 'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e'
function setupLogin(polls: unknown[]) {
vi.useFakeTimers()
const bridge = {
start: vi.fn().mockResolvedValue({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: Date.now() + 600_000 }),
poll: vi.fn(async () => polls.shift() ?? { state: 'pending' }),
cancel: vi.fn().mockResolvedValue({ ok: true }),
overwriteCli: vi.fn().mockResolvedValue({ cli: 'ok' })
}
const openExternal = vi.fn().mockResolvedValue(undefined)
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { aiturkLogin: bridge, openExternal }
const onConnected = vi.fn().mockResolvedValue(undefined)
render(<I18nProvider configClient={null}><AiturkProvider onConnected={onConnected} /></I18nProvider>)
return { bridge, onConnected, openExternal }
}
const click = async (name: string) => {
await act(async () => {
fireEvent.click(screen.getByRole('button', { name }))
await Promise.resolve()
})
}
const tick = () =>
act(async () => {
await vi.advanceTimersByTimeAsync(3000)
})
describe('TurkServis one-click sign-in', () => {
it('shows the code, lets the user reopen the page and polls every 3 seconds', async () => {
const { bridge, onConnected, openExternal } = setupLogin([{ state: 'pending' }, { state: 'pending' }])
await click('Giriş yap')
expect(bridge.start).toHaveBeenCalledOnce()
expect(screen.getByLabelText('Giriş kodu').textContent).toBe('ABCD2345')
expect(screen.getByText('Tarayıcıda açılan sayfadaki kod bununla aynıysa onaylayın.')).toBeTruthy()
expect(bridge.poll).not.toHaveBeenCalled()
await tick()
expect(bridge.poll).toHaveBeenCalledTimes(1)
await tick()
expect(bridge.poll).toHaveBeenCalledTimes(2)
await click('Sayfayı yeniden aç')
expect(openExternal).toHaveBeenCalledWith(URI)
expect(onConnected).not.toHaveBeenCalled()
// No key ever reaches the renderer through this flow.
expect(saveCustomEndpoint).not.toHaveBeenCalled()
expect(validateCustomEndpoint).not.toHaveBeenCalled()
})
it('calls onConnected once the main process reports the provider connected', async () => {
const { bridge, onConnected } = setupLogin([{ state: 'pending' }, { state: 'connected', cli: 'ok' }])
await click('Giriş yap')
await tick()
expect(onConnected).not.toHaveBeenCalled()
await tick()
expect(onConnected).toHaveBeenCalledOnce()
expect(screen.getByRole('status').textContent).toBe('AITURK CLI de bu hesapla hazır.')
expect(screen.queryByLabelText('Giriş kodu')).toBeNull()
await tick()
expect(bridge.poll).toHaveBeenCalledTimes(2)
})
it.each([
['missing', ''],
['outdated', 'AITURK CLI güncellenince kendiliğinden bağlanması için yeniden giriş yapın.'],
['failed', 'AITURK CLI bağlanamadı; terminalde `aiturk login` çalıştırın.'],
['unknown-account', 'AITURK CLI bağlanamadı; terminalde `aiturk login` çalıştırın.']
])('connects and explains CLI result %s', async (cli, text) => {
const { onConnected } = setupLogin([{ state: 'connected', cli }])
await click('Giriş yap')
await tick()
expect(onConnected).toHaveBeenCalledOnce()
expect(screen.queryByRole('status')?.textContent ?? '').toBe(text)
})
it('asks before replacing another CLI account, then replaces on confirm', async () => {
const { bridge, onConnected } = setupLogin([{ state: 'connected', cli: 'other-account' }])
await click('Giriş yap')
await tick()
expect(screen.getByText('AITURK CLI’de başka bir hesap kayıtlı. Bu hesapla değiştirilsin mi?')).toBeTruthy()
expect(onConnected).not.toHaveBeenCalled()
expect(bridge.overwriteCli).not.toHaveBeenCalled()
await click('Değiştir')
expect(bridge.overwriteCli).toHaveBeenCalledOnce()
expect(onConnected).toHaveBeenCalledOnce()
expect(screen.getByRole('status').textContent).toBe('AITURK CLI de bu hesapla hazır.')
})
it('leaves the other CLI account alone when the user says so', async () => {
const { bridge, onConnected } = setupLogin([{ state: 'connected', cli: 'other-account' }])
await click('Giriş yap')
await tick()
await click('Olduğu gibi bırak')
expect(bridge.overwriteCli).not.toHaveBeenCalled()
expect(bridge.cancel).toHaveBeenCalledOnce()
expect(onConnected).toHaveBeenCalledOnce()
})
it('cancels on Vazgeç and returns to the sign-in button', async () => {
const { bridge, onConnected } = setupLogin([])
await click('Giriş yap')
await click('Vazgeç')
expect(bridge.cancel).toHaveBeenCalledOnce()
expect(screen.getByRole('button', { name: 'Giriş yap' })).toBeTruthy()
await tick()
expect(bridge.poll).not.toHaveBeenCalled()
expect(onConnected).not.toHaveBeenCalled()
})
it('reports an expired code and a failed start without connecting', async () => {
const { bridge, onConnected } = setupLogin([{ state: 'expired' }])
await click('Giriş yap')
await tick()
expect(screen.getByRole('alert').textContent).toBe('Giriş kodunun süresi doldu. Yeniden deneyin.')
bridge.start.mockRejectedValueOnce(
new Error("Error invoking remote method 'aiturk:login:start': AiturkLoginError: aiturk-login:rate-limited")
)
await click('Giriş yap')
expect(screen.getByRole('alert').textContent).toBe('Çok fazla deneme yapıldı. Biraz sonra yeniden deneyin.')
expect(onConnected).not.toHaveBeenCalled()
})
it('revokes a pending login when the panel goes away', async () => {
const { bridge } = setupLogin([])
await click('Giriş yap')
cleanup()
expect(bridge.cancel).toHaveBeenCalledOnce()
})
it('still connects with a pasted API key', async () => {
vi.mocked(validateCustomEndpoint).mockResolvedValue({ ok: true, reachable: true, models: ['account/model-a'], message: '' })
vi.mocked(saveCustomEndpoint).mockResolvedValue({ id: 'turkservis', endpoints: [], current: { base_url: 'https://ai.turkservis.online/v1', model: 'account/model-a', provider: 'turkservis' }, ok: true })
const { bridge, onConnected } = setupLogin([])
vi.useRealTimers()
expect(screen.getByText('ya da API anahtarı yapıştırın')).toBeTruthy()
fireEvent.change(screen.getByLabelText('TurkServis API anahtarı'), { target: { value: 'test-key-never-real' } })
fireEvent.click(screen.getByRole('button', { name: 'Bağlan' }))
await waitFor(() => expect(onConnected).toHaveBeenCalledOnce())
expect(saveCustomEndpoint).toHaveBeenCalledWith(expect.objectContaining({ api_key: 'test-key-never-real', model: 'account/model-a' }))
expect(bridge.start).not.toHaveBeenCalled()
})
})
@@ -1,5 +1,6 @@
import { useState } from 'react'
import { AiturkLogin } from '@/components/aiturk-login'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { saveCustomEndpoint, validateCustomEndpoint } from '@/hermes'
@@ -18,18 +19,22 @@ export function AiturkProvider({ onConnected }: { onConnected: () => Promise<voi
const [error, setError] = useState('')
async function connect() {
if (busy || !key.trim()) return
if (busy || !key.trim()) {return}
setBusy(true)
setError('')
try {
// Discovery goes through the connected backend's existing credential
// boundary. Do not ship a shared key or hard-code a model catalog.
const checked = await validateCustomEndpoint(aiturkEndpoint(key))
if (!checked.ok) throw new Error(checked.message || copy.failed)
if (!checked.ok) {throw new Error(checked.message || copy.failed)}
const model = checked.models[0]
if (!model) throw new Error(copy.noModels)
if (!model) {throw new Error(copy.noModels)}
const saved = await saveCustomEndpoint(aiturkEndpoint(key, model, checked.models))
if (saved.ok === false) throw new Error(copy.failed)
if (saved.ok === false) {throw new Error(copy.failed)}
setKey('')
await onConnected()
} catch (err) {
@@ -45,16 +50,18 @@ export function AiturkProvider({ onConnected }: { onConnected: () => Promise<voi
<h3 className="font-semibold">TurkServis · AITURK</h3>
<p className="mt-1 text-sm text-muted-foreground">{copy.pitch}</p>
</div>
<AiturkLogin disabled={busy} onConnected={onConnected} />
<div className="text-xs text-muted-foreground">{copy.login.orPasteKey}</div>
<label className="grid gap-2 text-sm">
{copy.key}
<Input autoComplete="off" disabled={busy} onChange={e => setKey(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) void connect() }}
onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) {void connect()} }}
placeholder={copy.key} type="password" value={key} />
</label>
{error ? <p className="text-sm text-destructive" role="alert">{error}</p> : null}
<div className="flex flex-wrap items-center justify-between gap-3">
<DocsLink href={AITURK_ACCOUNT_URL}>{copy.getKey}</DocsLink>
<Button disabled={busy || !key.trim()} onClick={() => void connect()}>
<Button disabled={busy || !key.trim()} onClick={() => void connect()} variant="outline">
{busy ? <Loader2 className="animate-spin" /> : null}{busy ? t.common.connecting : t.common.connect}
</Button>
</div>
+22
View File
@@ -323,6 +323,14 @@ declare global {
setDisableF12?: (blocked: boolean) => void
setPreviewShortcutActive?: (active: boolean) => void
openExternal: (url: string) => Promise<void>
/** One-click TurkServis sign-in (electron/aiturk-login.ts). API keys stay
* in the main process; only the short code and a status come back. */
aiturkLogin?: {
start: (scope?: { connectionId?: string; profile?: string }) => Promise<AiturkLoginStart>
poll: () => Promise<AiturkLoginPoll>
cancel: () => Promise<{ ok: true }>
overwriteCli: () => Promise<{ cli: AiturkCliStatus }>
}
/** One-shot loopback callback listener for MCP OAuth against remote
* backends (electron/mcp-oauth-callback-ipc.ts): bind on THIS machine,
* pass redirectUri as client_redirect_uri to mcp.servers.oauth.start,
@@ -1212,6 +1220,20 @@ export type DesktopBootstrapEvent =
docsUrl: string
}
export type AiturkCliStatus = 'ok' | 'missing' | 'outdated' | 'other-account' | 'unknown-account' | 'failed'
export interface AiturkLoginStart {
userCode: string
verificationUri: string
expiresAt: number
}
export type AiturkLoginPoll =
| { state: 'pending' }
| { state: 'expired' }
| { state: 'connected'; cli: AiturkCliStatus }
| { state: 'error'; error: string }
export interface HermesApiRequest {
path: string
method?: string
+44
View File
@@ -30,6 +30,28 @@ const en = {
getKey: 'My account and API keys',
noModels: 'No models are available for this key.',
failed: 'Could not connect to TurkServis.',
login: {
signIn: 'Sign in',
settingsSignIn: 'Sign in to AITURK',
starting: 'Starting sign-in…',
codeLabel: 'Sign-in code',
confirmHint: 'Approve if the code on the page opened in your browser matches this one.',
reopen: 'Reopen the page',
cancel: 'Cancel',
orPasteKey: 'or paste an API key',
connected: 'Your TurkServis account is connected.',
expired: 'The sign-in code expired. Try again.',
rateLimited: 'Too many attempts. Try again in a little while.',
unavailable: 'Could not start sign-in. Check your internet connection and try again.',
browserFailed: 'Could not open the browser. Try again.',
unsupported: 'One-click sign-in is not available here. Paste an API key instead.',
cliOk: 'AITURK CLI is ready with this account too.',
cliOutdated: 'After AITURK CLI is updated, sign in again to connect it automatically.',
cliOtherAccount: 'AITURK CLI has a different account saved. Replace it with this account?',
cliReplace: 'Replace',
cliKeep: 'Leave as is',
cliFailed: 'AITURK CLI could not be connected; run `aiturk login` in a terminal.'
},
custom: {
title: 'Custom endpoints',
active: 'Active',
@@ -79,6 +101,28 @@ const tr: typeof en = {
getKey: 'Hesabım ve API anahtarlarım',
noModels: 'Bu anahtarla kullanılabilecek model bulunamadı.',
failed: 'TurkServis bağlantısı kurulamadı.',
login: {
signIn: 'Giriş yap',
settingsSignIn: 'AITURK’e giriş yap',
starting: 'Giriş başlatılıyor…',
codeLabel: 'Giriş kodu',
confirmHint: 'Tarayıcıda açılan sayfadaki kod bununla aynıysa onaylayın.',
reopen: 'Sayfayı yeniden aç',
cancel: 'Vazgeç',
orPasteKey: 'ya da API anahtarı yapıştırın',
connected: 'TurkServis hesabınız bağlandı.',
expired: 'Giriş kodunun süresi doldu. Yeniden deneyin.',
rateLimited: 'Çok fazla deneme yapıldı. Biraz sonra yeniden deneyin.',
unavailable: 'Giriş başlatılamadı. İnternet bağlantınızı kontrol edip yeniden deneyin.',
browserFailed: 'Tarayıcı açılamadı. Yeniden deneyin.',
unsupported: 'Tek tıkla giriş burada kullanılamıyor. API anahtarı yapıştırın.',
cliOk: 'AITURK CLI de bu hesapla hazır.',
cliOutdated: 'AITURK CLI güncellenince kendiliğinden bağlanması için yeniden giriş yapın.',
cliOtherAccount: 'AITURK CLI’de başka bir hesap kayıtlı. Bu hesapla değiştirilsin mi?',
cliReplace: 'Değiştir',
cliKeep: 'Olduğu gibi bırak',
cliFailed: 'AITURK CLI bağlanamadı; terminalde `aiturk login` çalıştırın.'
},
custom: {
title: 'Özel API bağlantıları',
active: 'Etkin',
+7
View File
@@ -1,8 +1,15 @@
import { connectionScoped, profileScoped } from '@/api/client'
import type { CustomEndpointUpdate } from '@/types/hermes'
export const AITURK_API_BASE_URL = 'https://ai.turkservis.online/v1'
export const AITURK_ACCOUNT_URL = 'https://turkservis.online/panel'
/** Where the main process should save the provider after a one-click sign-in:
* the same (connection, profile) the renderer's saveCustomEndpoint targets. */
export function aiturkLoginScope(): { connectionId?: string; profile?: string } {
return { ...connectionScoped(), ...profileScoped() }
}
export function aiturkEndpoint(apiKey: string, model = '', models?: string[]): CustomEndpointUpdate {
return {
id: 'turkservis',