fix: resolve 6 remaining defects (P2-18, P2-21, P3-04, P3-05, P3-06, P3-02)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- P2-18: TOTP QR code local generation via qrcode lib (no external service) - P2-21: Suspend foreign LLM providers (OpenAI/Anthropic/Gemini) for early stage - P3-04: get_progress() now calculates actual percentage from completed/total steps - P3-05: saveSaaSSession calls now have .catch() error logging - P3-06: SaaS relay chatStream passes session_key/agent_id to backend - P3-02: Whiteboard unification plan document created Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import { useSaaSStore } from '../../store/saasStore';
|
||||
import { Shield, ShieldCheck, ShieldOff, Copy, Check, Loader2, AlertCircle, X } from 'lucide-react';
|
||||
|
||||
@@ -103,14 +104,8 @@ export function TOTPSettings() {
|
||||
使用 Google Authenticator / Authy 扫描下方二维码,然后输入验证码完成绑定。
|
||||
</p>
|
||||
|
||||
{/* QR Code */}
|
||||
<div className="flex flex-col items-center gap-3 py-2">
|
||||
<img
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(totpSetupData.otpauth_uri)}&size=200x200`}
|
||||
alt="TOTP QR Code"
|
||||
className="w-48 h-48 border border-gray-200 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
{/* QR Code — P2-18: Generated locally, no external service */}
|
||||
<LocalQRCode data={totpSetupData.otpauth_uri} />
|
||||
|
||||
{/* Manual secret */}
|
||||
<div>
|
||||
@@ -283,3 +278,40 @@ export function TOTPSettings() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** P2-18: Local QR code generation — no external service, no secret leakage */
|
||||
function LocalQRCode({ data }: { data: string }) {
|
||||
const [src, setSrc] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
QRCode.toDataURL(data, { width: 200, margin: 1, color: { dark: '#000', light: '#fff' } })
|
||||
.then((url) => { if (!cancelled) setSrc(url); })
|
||||
.catch((e) => { if (!cancelled) setError(String(e)); });
|
||||
return () => { cancelled = true; };
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 py-2 text-red-500 text-xs">
|
||||
<AlertCircle className="w-8 h-8" />
|
||||
<span>QR 生成失败,请使用下方密钥手动输入</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div className="w-48 h-48 flex items-center justify-center border border-gray-200 rounded-lg">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 py-2">
|
||||
<img src={src} alt="TOTP QR Code" className="w-48 h-48 border border-gray-200 rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,18 +38,20 @@ interface EmbeddingProvider {
|
||||
|
||||
// 可用的 Provider 列表
|
||||
// 注意: Coding Plan 是专为编程助手设计的优惠套餐,使用专用端点
|
||||
// P2-21: 外国模型 (OpenAI, Anthropic, Gemini) 暂停支持,标记为 suspended
|
||||
const AVAILABLE_PROVIDERS = [
|
||||
// === Coding Plan 专用端点 (推荐用于编程场景) ===
|
||||
{ id: 'kimi-coding', name: 'Kimi Coding Plan', baseUrl: 'https://api.kimi.com/coding/v1' },
|
||||
{ id: 'qwen-coding', name: '百炼 Coding Plan', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1' },
|
||||
{ id: 'zhipu-coding', name: '智谱 GLM Coding Plan', baseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4' },
|
||||
// === 标准 API 端点 ===
|
||||
// === 标准 API 端点 (国内) ===
|
||||
{ id: 'kimi', name: 'Kimi (标准 API)', baseUrl: 'https://api.moonshot.cn/v1' },
|
||||
{ id: 'zhipu', name: '智谱 (标准 API)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' },
|
||||
{ id: 'qwen', name: '百炼/通义千问 (标准)', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
|
||||
{ id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' },
|
||||
{ id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' },
|
||||
{ id: 'anthropic', name: 'Anthropic', baseUrl: 'https://api.anthropic.com' },
|
||||
// === 暂停支持 (P2-21: 前期不使用非国内大模型) ===
|
||||
{ id: 'openai', name: 'OpenAI (暂停支持)', baseUrl: 'https://api.openai.com/v1', suspended: true },
|
||||
{ id: 'anthropic', name: 'Anthropic (暂停支持)', baseUrl: 'https://api.anthropic.com', suspended: true },
|
||||
{ id: 'custom', name: '自定义', baseUrl: '' },
|
||||
];
|
||||
|
||||
@@ -663,7 +665,7 @@ export function ModelsAPI() {
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
{AVAILABLE_PROVIDERS.map((p) => (
|
||||
{AVAILABLE_PROVIDERS.filter((p) => !(p as any).suspended).map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -119,6 +119,9 @@ export function createSaaSRelayGatewayClient(
|
||||
stream: true,
|
||||
};
|
||||
|
||||
// P3-06: Pass sessionKey/agentId to relay for session continuity
|
||||
if (opts?.sessionKey) body['session_key'] = opts.sessionKey;
|
||||
if (opts?.agentId) body['agent_id'] = opts.agentId;
|
||||
if (opts?.thinking_enabled) body['thinking_enabled'] = true;
|
||||
if (opts?.reasoning_effort) body['reasoning_effort'] = opts.reasoning_effort;
|
||||
|
||||
|
||||
@@ -209,7 +209,9 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
account: loginData.account,
|
||||
saasUrl: normalizedUrl,
|
||||
};
|
||||
saveSaaSSession(sessionData); // async — fire and forget (non-blocking)
|
||||
saveSaaSSession(sessionData).catch((e) =>
|
||||
log.warn('Failed to persist SaaS session after login', { error: e })
|
||||
);
|
||||
saveConnectionMode('saas');
|
||||
|
||||
set({
|
||||
@@ -309,7 +311,9 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
account: loginData.account,
|
||||
saasUrl: normalizedUrl,
|
||||
};
|
||||
saveSaaSSession(sessionData);
|
||||
saveSaaSSession(sessionData).catch((e) =>
|
||||
log.warn('Failed to persist SaaS session after TOTP login', { error: e })
|
||||
);
|
||||
saveConnectionMode('saas');
|
||||
|
||||
set({
|
||||
@@ -374,7 +378,9 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
account: registerData.account,
|
||||
saasUrl: normalizedUrl,
|
||||
};
|
||||
saveSaaSSession(sessionData);
|
||||
saveSaaSSession(sessionData).catch((e) =>
|
||||
log.warn('Failed to persist SaaS session after register', { error: e })
|
||||
);
|
||||
saveConnectionMode('saas');
|
||||
|
||||
set({
|
||||
@@ -784,7 +790,9 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
await saasClient.verifyTotp(code);
|
||||
const account = await saasClient.me();
|
||||
const { saasUrl } = get();
|
||||
saveSaaSSession({ token: null, account, saasUrl }); // Token in saasClient memory only
|
||||
saveSaaSSession({ token: null, account, saasUrl }).catch((e) =>
|
||||
log.warn('Failed to persist SaaS session after verifyTotp', { error: e })
|
||||
); // Token in saasClient memory only
|
||||
set({ totpSetupData: null, isLoading: false, account });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof SaaSApiError ? err.message
|
||||
@@ -800,7 +808,9 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
await saasClient.disableTotp(password);
|
||||
const account = await saasClient.me();
|
||||
const { saasUrl } = get();
|
||||
saveSaaSSession({ token: null, account, saasUrl });
|
||||
saveSaaSSession({ token: null, account, saasUrl }).catch((e) =>
|
||||
log.warn('Failed to persist SaaS session after disableTotp', { error: e })
|
||||
);
|
||||
set({ isLoading: false, account });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof SaaSApiError ? err.message
|
||||
|
||||
Reference in New Issue
Block a user