feat(mobile): bind device identities and bound voice usage in 0.1.4 dev

This commit is contained in:
yilsem committed 2026-09-12 12:11:18 +03:00
1 parent ac6687181e
commit 5f75dcc99a
24 files changed
+909 -47

No files matched your search

+32 -9
View File
@@ -1,11 +1,13 @@
import { AccountClient, openPhoneConnection } from './transport.mjs'
import { VoiceSession } from './voice.mjs'
import { DEFAULT_VOICE_LIMITS, voiceLimits, usageLabel } from './voice-usage.mjs'
import { taskState } from './behavior.mjs'
import { parseInvite } from './invite.mjs'
import { InviteScanner } from './qr.mjs'
import { InviteScanner, drawQr } from './qr.mjs'
import { createDeviceIdentity } from './peer-identity.mjs'
const $ = id => document.getElementById(id)
if (globalThis.Capacitor?.isNativePlatform()) globalThis.Capacitor.registerPlugin('MobileSecrets')
let credentials = {}, account, connection, selected, approval, voice, pendingTask, usageTimer, voiceGeneration = 0
let credentials = {}, account, connection, selected, approval, voice, pendingTask, voiceGeneration = 0
let connectedDesktopId = null
let connectionEpoch = 0, selectionEpoch = 0
const status = text => { $('status').textContent = text }
@@ -17,9 +19,13 @@ const scanner = new InviteScanner($('qr-video'), async raw => {
const result = await account.call(`/v1/aiturk/mobile/invites/${invite.inviteId}/claim`, {
secret: invite.secret, display_name: $('device-name').value.trim() })
if (account !== targetAccount || epoch !== connectionEpoch) return
const identity = await createDeviceIdentity(result.device_id)
if (account !== targetAccount || epoch !== connectionEpoch) return
Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token,
pairingId: result.pairing_id, preferredDesktopId: invite.desktopId })
pairingId: result.pairing_id, preferredDesktopId: invite.desktopId,
desktopFingerprints: { [invite.desktopId]: invite.desktopFingerprint }, localFingerprint: identity.fingerprint })
await saveCredentials()
showPhoneIdentity()
$('pair-code').textContent = `Bilgisayarda bekleyen telefonu getirip şu kodu onaylayın: ${result.user_code}`
status('QR okundu. Bilgisayardaki açık telefon onayı bekleniyor.')
})
@@ -28,6 +34,12 @@ action('scan-invite', async () => { if (!account || credentials.deviceId) throw
action('stop-scan', async () => scanner.stop())
$('qr-file').onchange = async event => { try { await scanner.image(event.target.files[0]) } catch (error) { status(error.message) } finally { event.target.value = '' } }
async function saveCredentials() { if (native()) await native().save({ value: JSON.stringify(credentials) }) }
function showPhoneIdentity() {
$('phone-identity').value = credentials.localFingerprint ? JSON.stringify({ kind: 'aiturk-phone-identity', version: 1,
deviceId: credentials.deviceId, pairingId: credentials.pairingId, fingerprint: credentials.localFingerprint }) : ''
$('phone-identity-qr').hidden = !$('phone-identity').value
if ($('phone-identity').value) drawQr($('phone-identity-qr'), $('phone-identity').value)
}
function addText(container, role, text) { const p = document.createElement('p'); p.textContent = `${role}: ${text}`; $(container).append(p); while ($(container).children.length > 150) $(container).firstChild.remove() }
function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } }
function rpc(action, params) { if (!connection) throw new Error('Önce bilgisayara bağlanın.'); return connection.requests.call(action, params) }
@@ -76,6 +88,7 @@ action('logout', async () => {
if (token?.startsWith('tsml_')) await new AccountClient({ accountToken: token }).call('/v1/aiturk/mobile-auth/logout', {})
connectionEpoch++; stopVoice(); connection?.close(); connection = null; selected = null; account = null
credentials = {}; pendingTask = null; await saveCredentials()
showPhoneIdentity()
$('work').hidden = true; $('login-link').hidden = true; $('login-code').textContent = ''; $('pair-code').textContent = ''
status('Bu uygulamanın oturumu kapatıldı. IDE’de başlamış iş kendi çalışma kurallarıyla devam edebilir.')
})
@@ -87,8 +100,11 @@ action('pair', async () => {
if (!credentials.accountToken) throw new Error('Önce AITURK hesabınıza giriş yapın.')
account = new AccountClient(credentials)
const result = await account.call('/v1/aiturk/pairings', { display_name: $('device-name').value.trim(), platform: 'android' })
Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id })
const identity = await createDeviceIdentity(result.device_id)
Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id,
localFingerprint: identity.fingerprint })
await saveCredentials()
showPhoneIdentity()
$('pair-code').textContent = `Eşleştirme kimliği:\n${result.pairing_id}\n\nKod: ${result.user_code}\nSon geçerlilik: ${result.expires_at}`
status('Bilgisayardaki açık onay bekleniyor.')
})
@@ -130,14 +146,18 @@ action('select-session', async () => {
for (const [id, choice] of [['allow', 'once'], ['deny', 'deny']]) action(id, async () => { const target = requireSession(); await rpc('approval.respond', { sessionId: target.id, requestId: approval?.requestId, choice }); setApproval(null); status('Yanıt gerçek IDE onayına iletildi.') })
action('save-google', async () => { const key = $('google-key').value.trim(); $('google-key').value = ''; await rpc('google.key.save', { key }); status('Google anahtarı bilgisayarın güvenli kasasına kaydedildi.') })
action('delete-google', async () => { stopVoice(); await rpc('google.key.delete'); status('Google anahtarı bilgisayar kasasından silindi.') })
function stopVoice() { voiceGeneration++; voice?.stop(); voice = null; clearInterval(usageTimer); $('voice-status').textContent = 'Mikrofon kapalı. IDE işi devam edebilir.' }
function stopVoice() { voiceGeneration++; voice?.stop(); voice = null; $('voice-status').textContent = 'Mikrofon kapalı. IDE işi devam edebilir.' }
async function startVoice(microphone) {
if (!$('consent').checked) throw new Error('Önce Google veri ve ücret bilgisini okuyup kabul edin.')
const limits = voiceLimits({ minutes: $('voice-minutes').value, usd: $('voice-budget').value })
// Only non-secret preferences; account and API credentials remain in the
// native vault. No usage history or transcripts are stored here.
try { localStorage.setItem('aiturk.voice-limits', JSON.stringify(limits)) } catch {}
const target = { ...requireSession() }, targetConnection = connection, desktopId = connectedDesktopId
stopVoice(); const generation = voiceGeneration; const token = await rpc('voice.token')
if (generation !== voiceGeneration || document.hidden) return
const started = Date.now()
voice = new VoiceSession({ ...token,
voice = new VoiceSession({ ...token, limits,
onUsage: usage => { if (generation === voiceGeneration) $('usage').textContent = usageLabel(usage, usage.seconds) },
onText: (role, text) => addText('discussion', role, text),
onDraft: (text, intent) => { $('task').value = text; $('intent').value = intent; updatePreview(); status('Asistan görev taslağını hazırladı. Henüz IDE’ye gönderilmedi.') },
onSubmit: async (text, intent) => {
@@ -148,8 +168,7 @@ async function startVoice(microphone) {
pendingTask = { taskId: crypto.randomUUID(), sessionId: target.id, model: target.model, desktopId, intent, text }
return deliver()
},
onError: status, onState: text => { $('voice-status').textContent = text; if (voice?.stopped) clearInterval(usageTimer) } })
usageTimer = setInterval(() => { $('usage').textContent = `Bu uygulamanın ölçtüğü görüşme süresi: ${Math.floor((Date.now() - started) / 1000)} sn. Google kotası/faturası değildir.` }, 1000)
onError: status, onState: text => { $('voice-status').textContent = text } })
await voice.start(microphone)
}
action('voice-start', () => startVoice(true)); action('voice-text', () => startVoice(false))
@@ -189,4 +208,8 @@ action('disconnect', async () => { connectionEpoch++; stopVoice(); connection?.c
action('revoke', async () => { const data = await account.call('/v1/aiturk/mobile/devices'); const self = data.devices.find(d => d.self); await account.call(`/v1/aiturk/devices/${self.id}/revoke`, { expected_version: self.version }); stopVoice(); connection?.close(); connection = null; credentials = {}; await saveCredentials(); account = null; $('work').hidden = true; status('Bu telefonun erişimi iptal edildi.') })
document.addEventListener('visibilitychange', () => { if (document.hidden) { stopVoice(); scanner.stop() } })
window.addEventListener('pagehide', () => { stopVoice(); scanner.stop(); connection?.close() })
let savedLimits = DEFAULT_VOICE_LIMITS
try { const raw = localStorage.getItem('aiturk.voice-limits'); if (raw) savedLimits = voiceLimits(JSON.parse(raw)) } catch {}
$('voice-minutes').value = savedLimits.minutes; $('voice-budget').value = savedLimits.usd
try { const saved = await native()?.load(); if (saved?.value) { credentials = JSON.parse(saved.value); if (credentials.accountToken) { account = new AccountClient(credentials); status('Kayıtlı cihaz bulundu. Bilgisayarları yenileyerek bağlanın.') } } } catch { status('Android güvenli kasası açılamadı. Hesabınızı yeniden eşleştirin.') }
showPhoneIdentity()