diff --git a/.gitignore b/.gitignore index 6966dad..f79730e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,13 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -.kilocode \ No newline at end of file +.kilocode + +# Docs +Docs/ + +# AI +.claude/ +.gemini/ +.codex/ +.cursor/ \ No newline at end of file diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index dff6386..d387ffd 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008'; -const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3000'; +import { getInternalApiBaseUrl } from '@/lib/apiBase'; /** * OAuth 回调处理 (Linux.do SSO) @@ -9,6 +8,9 @@ const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3 * 转发给后端 API 完成认证,然后重定向到前端 */ export async function GET(request: NextRequest) { + const API_BASE_URL = getInternalApiBaseUrl(); + const FRONTEND_URL = request.nextUrl.origin; + const searchParams = request.nextUrl.searchParams; const code = searchParams.get('code'); const state = searchParams.get('state'); @@ -94,4 +96,4 @@ export async function GET(request: NextRequest) { new URL('/auth?error=oauth_callback_failed', FRONTEND_URL) ); } -} \ No newline at end of file +} diff --git a/app/api/auth/github/callback/route.ts b/app/api/auth/github/callback/route.ts index 1427ed2..f4ae89d 100644 --- a/app/api/auth/github/callback/route.ts +++ b/app/api/auth/github/callback/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008'; -const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3000'; +import { getInternalApiBaseUrl } from '@/lib/apiBase'; /** * GitHub OAuth 回调处理 @@ -9,6 +8,9 @@ const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3 * 转发给后端 API 完成认证,然后重定向到前端 */ export async function GET(request: NextRequest) { + const API_BASE_URL = getInternalApiBaseUrl(); + const FRONTEND_URL = request.nextUrl.origin; + const searchParams = request.nextUrl.searchParams; const code = searchParams.get('code'); const state = searchParams.get('state'); @@ -101,4 +103,4 @@ export async function GET(request: NextRequest) { new URL(`/auth?error=${encodeURIComponent(errorMessage)}`, FRONTEND_URL) ); } -} \ No newline at end of file +} diff --git a/app/dashboard/accounts/page.tsx b/app/dashboard/accounts/page.tsx index 0c4cf9e..550506c 100644 --- a/app/dashboard/accounts/page.tsx +++ b/app/dashboard/accounts/page.tsx @@ -65,7 +65,7 @@ export default function AccountsPage() { const toasterRef = useRef(null); const [accounts, setAccounts] = useState([]); const [kiroAccounts, setKiroAccounts] = useState([]); - const [kiroBalances, setKiroBalances] = useState>({}); + const [kiroBalances, setKiroBalances] = useState>({}); const [hasBeta, setHasBeta] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); @@ -132,7 +132,7 @@ export default function AccountsPage() { setKiroAccounts(kiroData); // 加载每个Kiro账号的余额 - const balances: Record = {}; + const balances: Record = {}; await Promise.all( kiroData.map(async (account) => { try { @@ -305,7 +305,7 @@ export default function AccountsPage() { }); }; - const handleDeleteKiro = (accountId: number) => { + const handleDeleteKiro = (accountId: string) => { showConfirmDialog({ title: '删除账号', description: '确定要删除这个 Kiro 账号吗?此操作无法撤销。', @@ -1283,4 +1283,4 @@ export default function AccountsPage() { ); -} \ No newline at end of file +} diff --git a/app/dashboard/settings/page.tsx b/app/dashboard/settings/page.tsx index a8f61ba..f81d907 100644 --- a/app/dashboard/settings/page.tsx +++ b/app/dashboard/settings/page.tsx @@ -20,6 +20,7 @@ import { MorphingSquare } from '@/components/ui/morphing-square'; import { cn } from '@/lib/utils'; import Toaster, { ToasterRef } from '@/components/ui/toast'; import { Badge as Badge1 } from '@/components/ui/badge-1'; +import { getPublicApiBaseUrl } from '@/lib/apiBase'; export default function SettingsPage() { const toasterRef = useRef(null); @@ -37,7 +38,13 @@ export default function SettingsPage() { const [selectedConfigType, setSelectedConfigType] = useState<'antigravity' | 'kiro'>('antigravity'); const [keyName, setKeyName] = useState(''); - const apiEndpoint = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008'; + const [apiEndpoint, setApiEndpoint] = useState(() => getPublicApiBaseUrl()); + + useEffect(() => { + const base = getPublicApiBaseUrl(); + if (/^https?:\/\//i.test(base)) return; + setApiEndpoint(`${window.location.origin}${base}`); + }, []); const loadAPIKeys = async () => { try { @@ -565,4 +572,4 @@ export default function SettingsPage() { ); -} \ No newline at end of file +} diff --git a/components/add-account-drawer.tsx b/components/add-account-drawer.tsx index 90d519e..61abf31 100644 --- a/components/add-account-drawer.tsx +++ b/components/add-account-drawer.tsx @@ -1,11 +1,12 @@ 'use client'; import { useState, useRef, useEffect } from 'react'; -import { getOAuthAuthorizeUrl, submitOAuthCallback, getKiroOAuthAuthorizeUrl, getCurrentUser, pollKiroOAuthStatus } from '@/lib/api'; +import { createKiroAccount, getOAuthAuthorizeUrl, submitOAuthCallback, getKiroOAuthAuthorizeUrl, getCurrentUser, pollKiroOAuthStatus } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Button as StatefulButton } from '@/components/ui/stateful-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; import { Drawer, DrawerContent, @@ -34,6 +35,12 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr const [provider, setProvider] = useState<'Google' | 'Github' | ''>(''); // Kiro OAuth提供商 const [accountType, setAccountType] = useState<0 | 1>(0); // 0=专属, 1=共享 const [loginMethod, setLoginMethod] = useState<'antihook' | 'manual' | ''>(''); // Antigravity 登录方式 + const [kiroLoginMethod, setKiroLoginMethod] = useState<'oauth' | 'refresh_token' | ''>(''); + const [kiroImportAuthMethod, setKiroImportAuthMethod] = useState<'Social' | 'IdC'>('Social'); + const [kiroImportRefreshToken, setKiroImportRefreshToken] = useState(''); + const [kiroImportClientId, setKiroImportClientId] = useState(''); + const [kiroImportClientSecret, setKiroImportClientSecret] = useState(''); + const [kiroImportAccountName, setKiroImportAccountName] = useState(''); const [oauthUrl, setOauthUrl] = useState(''); const [oauthState, setOauthState] = useState(''); // Kiro OAuth state const [callbackUrl, setCallbackUrl] = useState(''); @@ -86,7 +93,7 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr } // Kiro需要选择OAuth提供商 if (platform === 'kiro') { - setStep('provider'); + setStep('method'); } else { setStep('type'); } @@ -107,6 +114,25 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr setStep('method'); } else { // Kiro 账号直接进入授权 + if (kiroLoginMethod === 'refresh_token') { + setOauthUrl(''); + setOauthState(''); + setCountdown(600); + setIsWaitingAuth(false); + setStep('authorize'); + return; + } + + if (kiroLoginMethod !== 'oauth') { + toasterRef.current?.show({ + title: '选择方式', + message: '请选择添加方式', + variant: 'warning', + position: 'top-right', + }); + return; + } + try { const result = await getKiroOAuthAuthorizeUrl(provider as 'Google' | 'Github', accountType); setOauthUrl(result.data.auth_url); @@ -127,6 +153,26 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr } } } else if (step === 'method') { + if (platform === 'kiro') { + if (!kiroLoginMethod) { + toasterRef.current?.show({ + title: '选择方式', + message: '请选择添加方式', + variant: 'warning', + position: 'top-right', + }); + return; + } + + if (kiroLoginMethod === 'oauth') { + setStep('provider'); + return; + } + + setStep('type'); + return; + } + if (!loginMethod) { toasterRef.current?.show({ title: '选择登录方式', @@ -169,17 +215,39 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr const handleBack = () => { if (step === 'provider') { - setStep('platform'); + setStep('method'); } else if (step === 'type') { if (platform === 'kiro') { - setStep('provider'); + if (kiroLoginMethod === 'oauth') { + setStep('provider'); + } else { + setStep('method'); + } } else { setStep('platform'); } } else if (step === 'method') { - setStep('type'); - setLoginMethod(''); + if (platform === 'antigravity') { + setStep('type'); + setLoginMethod(''); + } else { + setStep('platform'); + setKiroLoginMethod(''); + } } else if (step === 'authorize') { + if (platform === 'kiro') { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + if (pollTimerRef.current) { + clearInterval(pollTimerRef.current); + pollTimerRef.current = null; + } + setIsWaitingAuth(false); + setCountdown(600); + } + if (platform === 'antigravity') { setStep('method'); } else { @@ -342,6 +410,64 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr } }; + const handleImportKiroAccount = async () => { + const refreshToken = kiroImportRefreshToken.trim(); + if (!refreshToken) { + toasterRef.current?.show({ + title: '输入错误', + message: '请粘贴 refresh_token', + variant: 'warning', + position: 'top-right', + }); + return; + } + + const accountName = kiroImportAccountName.trim(); + const clientId = kiroImportClientId.trim(); + const clientSecret = kiroImportClientSecret.trim(); + + if (kiroImportAuthMethod === 'IdC' && (!clientId || !clientSecret)) { + toasterRef.current?.show({ + title: '输入错误', + message: 'IdC 方式需要 client_id 和 client_secret', + variant: 'warning', + position: 'top-right', + }); + return; + } + + try { + await createKiroAccount({ + refresh_token: refreshToken, + auth_method: kiroImportAuthMethod, + account_name: accountName || undefined, + client_id: kiroImportAuthMethod === 'IdC' ? clientId : undefined, + client_secret: kiroImportAuthMethod === 'IdC' ? clientSecret : undefined, + is_shared: accountType, + }); + + toasterRef.current?.show({ + title: '添加成功', + message: 'Kiro 账号已导入', + variant: 'success', + position: 'top-right', + }); + + window.dispatchEvent(new CustomEvent('accountAdded')); + onOpenChange(false); + resetState(); + onSuccess?.(); + } catch (err) { + toasterRef.current?.show({ + title: '导入失败', + message: err instanceof Error ? err.message : '导入 Kiro 账号失败', + variant: 'error', + position: 'top-right', + }); + throw err; + } + }; + const resetState = () => { // 清除所有定时器 if (timerRef.current) { @@ -358,6 +484,12 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr setProvider(''); setAccountType(0); setLoginMethod(''); + setKiroLoginMethod(''); + setKiroImportAuthMethod('Social'); + setKiroImportRefreshToken(''); + setKiroImportClientId(''); + setKiroImportClientSecret(''); + setKiroImportAccountName(''); setOauthUrl(''); setOauthState(''); setCallbackUrl(''); @@ -671,6 +803,60 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr )} {/* 步骤 4: 选择登录方式 (仅Antigravity) */} + {step === 'method' && platform === 'kiro' && ( +
+

+ 选择添加方式 +

+ +
+ + + +
+
+ )} + {step === 'method' && platform === 'antigravity' && (

@@ -740,7 +926,124 @@ export function AddAccountDrawer({ open, onOpenChange, onSuccess }: AddAccountDr {/* 步骤 5: OAuth 授权 */} {step === 'authorize' && (

- {platform === 'kiro' ? ( + {platform === 'kiro' && kiroLoginMethod === 'refresh_token' ? ( + <> +
+ +

+ 粘贴 refresh_token 后,服务端会校验并自动拉取账号信息。 +

+
+ +
+ + setKiroImportAccountName(e.target.value)} + className="h-12" + /> +
+ +
+ +
+ + + +
+
+ + {kiroImportAuthMethod === 'IdC' && ( +
+
+ + setKiroImportClientId(e.target.value)} + className="h-12 font-mono text-sm" + autoComplete="off" + /> +
+ +
+ + setKiroImportClientSecret(e.target.value)} + className="h-12 font-mono text-sm" + autoComplete="off" + type="password" + /> +
+
+ )} + +
+ +