This commit is contained in:
eric
2026-03-26 01:59:04 -05:00
parent aba40c8cb7
commit 3bc1599763
12 changed files with 629 additions and 72 deletions

View File

@@ -6,22 +6,28 @@ function joinUrl(base: string, path: string): string {
return `${b}${p}`
}
export type ApiFetchInit = RequestInit & { pbToken?: string | null }
export async function apiFetch(
bind: BindPayload | null,
path: string,
init?: RequestInit,
init?: ApiFetchInit,
): Promise<Response> {
if (!bind) {
return Promise.reject(new Error('尚未绑定 PC请先在「配对」扫码'))
}
const headers = new Headers(init?.headers)
const { pbToken, ...rest } = init ?? {}
const headers = new Headers(rest.headers)
if (bind.token?.trim()) {
headers.set('Authorization', `Bearer ${bind.token.trim()}`)
}
return fetch(joinUrl(bind.baseUrl, path), { ...init, headers })
if (pbToken?.trim()) {
headers.set('X-PB-Token', pbToken.trim())
}
return fetch(joinUrl(bind.baseUrl, path), { ...rest, headers })
}
export async function apiJson<T>(bind: BindPayload | null, path: string, init?: RequestInit): Promise<T> {
export async function apiJson<T>(bind: BindPayload | null, path: string, init?: ApiFetchInit): Promise<T> {
const res = await apiFetch(bind, path, init)
if (!res.ok) {
const t = await res.text()

29
lib/authStorage.ts Normal file
View File

@@ -0,0 +1,29 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import type { PbUserRecord } from './pocketbase'
const KEY = 'pb_auth_session_v1'
export type StoredPbSession = {
token: string
record: PbUserRecord
}
export async function loadPbSession(): Promise<StoredPbSession | null> {
try {
const raw = await AsyncStorage.getItem(KEY)
if (!raw) return null
const j = JSON.parse(raw) as StoredPbSession
if (!j?.token || !j?.record?.id) return null
return j
} catch {
return null
}
}
export async function savePbSession(s: StoredPbSession): Promise<void> {
await AsyncStorage.setItem(KEY, JSON.stringify(s))
}
export async function clearPbSession(): Promise<void> {
await AsyncStorage.removeItem(KEY)
}

21
lib/liveTelemetry.ts Normal file
View File

@@ -0,0 +1,21 @@
import { apiFetch } from '@/lib/api'
import type { BindPayload } from '@/lib/types'
import { pbCreateLiveSession } from '@/lib/pocketbase'
export async function reportLiveControlEvent(
bind: BindPayload | null,
pbToken: string | null,
userId: string | undefined,
process: string,
event: 'start' | 'restart' | 'stop',
): Promise<void> {
if (!bind || !pbToken?.trim() || !userId) return
let machine: Record<string, unknown> = {}
try {
const r = await apiFetch(bind, '/host/machine_summary', { pbToken: pbToken.trim() })
if (r.ok) machine = (await r.json()) as Record<string, unknown>
} catch {
/* 忽略 */
}
await pbCreateLiveSession(pbToken.trim(), userId, { process, event, machine })
}

99
lib/pocketbase.ts Normal file
View File

@@ -0,0 +1,99 @@
/** PocketBase REST与 payjsapi 同源实例 https://pocketbase.hackrobot.cn */
export const POCKETBASE_URL = 'https://pocketbase.hackrobot.cn'
export type PbUserRecord = {
id: string
email?: string
name?: string
live_allowed?: boolean
[k: string]: unknown
}
export type PbAuthResponse = {
token: string
record: PbUserRecord
}
function joinPb(path: string): string {
const b = POCKETBASE_URL.replace(/\/$/, '')
const p = path.startsWith('/') ? path : `/${path}`
return `${b}${p}`
}
export async function pbAuthWithPassword(email: string, password: string): Promise<PbAuthResponse> {
const res = await fetch(joinPb('/api/collections/users/auth-with-password'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: email.trim(), password }),
})
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
if (!res.ok) {
const msg =
typeof data.message === 'string'
? data.message
: typeof data.data === 'object' && data.data && typeof (data.data as { message?: string }).message === 'string'
? (data.data as { message: string }).message
: `登录失败 (${res.status})`
throw new Error(msg)
}
const token = String(data.token ?? '')
const record = data.record as PbUserRecord | undefined
if (!token || !record?.id) throw new Error('登录响应异常')
return { token, record }
}
export async function pbAuthRefresh(token: string): Promise<PbAuthResponse> {
const res = await fetch(joinPb('/api/collections/users/auth-refresh'), {
method: 'POST',
headers: { Authorization: `Bearer ${token.trim()}` },
})
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
if (!res.ok) {
throw new Error(typeof data.message === 'string' ? data.message : `刷新失败 (${res.status})`)
}
const newToken = String(data.token ?? '')
const record = data.record as PbUserRecord | undefined
if (!newToken || !record?.id) throw new Error('刷新响应异常')
return { token: newToken, record }
}
export function isLiveAllowed(record: PbUserRecord | null | undefined): boolean {
if (!record) return false
return record.live_allowed !== false
}
export type LiveSessionPayload = {
process: string
event: 'start' | 'restart' | 'stop'
machine: Record<string, unknown>
}
/** 需在 PocketBase 新建集合 `live_sessions`,见项目内说明 */
export async function pbCreateLiveSession(
token: string,
userId: string,
payload: LiveSessionPayload,
): Promise<void> {
const res = await fetch(joinPb('/api/collections/live_sessions/records'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token.trim()}`,
},
body: JSON.stringify({
user: userId,
process: payload.process,
event: payload.event,
machine: payload.machine,
}),
})
if (!res.ok) {
const t = await res.text()
if (res.status === 404) {
console.warn('[live_sessions] 集合未创建或规则未放行:', t)
return
}
console.warn('[live_sessions] 写入失败:', res.status, t)
}
}