mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-13 06:22:49 +00:00
Merge pull request #799 from kaitranntt/kai/feat/gemini-power-user-mode
feat: extend power user mode to Gemini dashboard auth
This commit is contained in:
@@ -173,7 +173,7 @@ export interface CLIProxyLoggingConfig {
|
||||
* Controls high-risk flow safeguards for supported providers.
|
||||
*/
|
||||
export interface CLIProxySafetyConfig {
|
||||
/** Allow skipping AGY responsibility acknowledgement flow (default: false) */
|
||||
/** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */
|
||||
antigravity_ack_bypass?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
|
||||
// ==================== Auth Tokens ====================
|
||||
|
||||
/**
|
||||
* GET /api/settings/auth/antigravity-risk - Get AGY responsibility bypass setting
|
||||
* GET /api/settings/auth/antigravity-risk - Get shared power user bypass setting
|
||||
*/
|
||||
router.get('/auth/antigravity-risk', (req: Request, res: Response): void => {
|
||||
if (!requireSensitiveLocalAccess(req, res)) return;
|
||||
@@ -636,12 +636,12 @@ router.get('/auth/antigravity-risk', (req: Request, res: Response): void => {
|
||||
antigravityAckBypass: config.cliproxy?.safety?.antigravity_ack_bypass === true,
|
||||
});
|
||||
} catch (error) {
|
||||
respondInternalError(res, error, 'Failed to load Antigravity power user mode.');
|
||||
respondInternalError(res, error, 'Failed to load power user mode.');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/settings/auth/antigravity-risk - Update AGY responsibility bypass setting
|
||||
* PUT /api/settings/auth/antigravity-risk - Update shared power user bypass setting
|
||||
*/
|
||||
router.put('/auth/antigravity-risk', (req: Request, res: Response): void => {
|
||||
if (!requireSensitiveLocalAccess(req, res)) return;
|
||||
|
||||
@@ -60,6 +60,11 @@ function normalizeRiskPhrase(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
||||
}
|
||||
|
||||
interface PowerUserModeSyncOptions {
|
||||
pendingMessage?: string | null;
|
||||
disabledMessage?: string | null;
|
||||
}
|
||||
|
||||
export function AddAccountDialog({
|
||||
open,
|
||||
onClose,
|
||||
@@ -73,18 +78,21 @@ export function AddAccountDialog({
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [riskAcknowledgementText, setRiskAcknowledgementText] = useState('');
|
||||
const [agyRiskChecklist, setAgyRiskChecklist] = useState(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||
const [agyAckBypassEnabled, setAgyAckBypassEnabled] = useState(false);
|
||||
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(false);
|
||||
const [powerUserModeEnabled, setPowerUserModeEnabled] = useState(false);
|
||||
const [powerUserModeLoading, setPowerUserModeLoading] = useState(false);
|
||||
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
||||
const { t } = useTranslation();
|
||||
const wasAuthenticatingRef = useRef(false);
|
||||
const powerUserModeRequestIdRef = useRef(0);
|
||||
const powerUserModeLoadErrorShownRef = useRef(false);
|
||||
const authFlow = useCliproxyAuthFlow();
|
||||
const kiroImportMutation = useKiroImport();
|
||||
|
||||
const isKiro = provider === 'kiro';
|
||||
const requiresSafetyAcknowledgement = provider === 'gemini';
|
||||
const requiresAgyResponsibilityFlow = provider === 'agy' && !agyAckBypassEnabled;
|
||||
const isAgyBypassStatePending = provider === 'agy' && agyAckBypassLoading;
|
||||
const supportsPowerUserMode = provider === 'agy' || provider === 'gemini';
|
||||
const requiresGeminiSafetyAcknowledgement = provider === 'gemini' && !powerUserModeEnabled;
|
||||
const requiresAgyResponsibilityFlow = provider === 'agy' && !powerUserModeEnabled;
|
||||
const isPowerUserModePending = supportsPowerUserMode && powerUserModeLoading;
|
||||
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
|
||||
const isGeminiRiskAcknowledged = normalizeRiskPhrase(riskAcknowledgementText) === RISK_ACK_PHRASE;
|
||||
const defaultDeviceCode = isDeviceCodeProvider(provider);
|
||||
@@ -94,15 +102,62 @@ export function AddAccountDialog({
|
||||
const nicknameTrimmed = nickname.trim();
|
||||
const errorMessage = localError || authFlow.error;
|
||||
|
||||
const fetchAgyBypassState = useCallback(async (): Promise<boolean> => {
|
||||
const fetchPowerUserModeState = useCallback(async (): Promise<boolean> => {
|
||||
const response = await fetch('/api/settings/auth/antigravity-risk');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load Antigravity power user setting');
|
||||
throw new Error('Failed to load power user mode setting');
|
||||
}
|
||||
const data = (await response.json()) as { antigravityAckBypass?: boolean };
|
||||
return data.antigravityAckBypass === true;
|
||||
}, []);
|
||||
|
||||
const syncPowerUserModeState = useCallback(
|
||||
async ({ pendingMessage = null, disabledMessage = null }: PowerUserModeSyncOptions = {}) => {
|
||||
const requestId = ++powerUserModeRequestIdRef.current;
|
||||
setPowerUserModeLoading(true);
|
||||
|
||||
if (pendingMessage !== null) {
|
||||
setLocalError(pendingMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
const enabled = await fetchPowerUserModeState();
|
||||
if (powerUserModeRequestIdRef.current !== requestId) {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
setPowerUserModeEnabled(enabled);
|
||||
|
||||
if (disabledMessage) {
|
||||
setLocalError(enabled ? null : disabledMessage);
|
||||
} else if (pendingMessage !== null) {
|
||||
setLocalError(null);
|
||||
}
|
||||
|
||||
return enabled;
|
||||
} catch {
|
||||
if (powerUserModeRequestIdRef.current !== requestId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setPowerUserModeEnabled(false);
|
||||
setLocalError(disabledMessage ?? t('addAccountDialog.powerUserLoadFailed'));
|
||||
|
||||
if (!powerUserModeLoadErrorShownRef.current) {
|
||||
powerUserModeLoadErrorShownRef.current = true;
|
||||
toast.error(t('addAccountDialog.powerUserLoadFailed'));
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (powerUserModeRequestIdRef.current === requestId) {
|
||||
setPowerUserModeLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchPowerUserModeState, t]
|
||||
);
|
||||
|
||||
const resetAndClose = () => {
|
||||
setNickname('');
|
||||
setCallbackUrl('');
|
||||
@@ -110,9 +165,11 @@ export function AddAccountDialog({
|
||||
setLocalError(null);
|
||||
setRiskAcknowledgementText('');
|
||||
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||
setAgyAckBypassEnabled(false);
|
||||
setAgyAckBypassLoading(false);
|
||||
setPowerUserModeEnabled(false);
|
||||
setPowerUserModeLoading(false);
|
||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||
powerUserModeRequestIdRef.current += 1;
|
||||
powerUserModeLoadErrorShownRef.current = false;
|
||||
wasAuthenticatingRef.current = false;
|
||||
onClose();
|
||||
};
|
||||
@@ -126,41 +183,24 @@ export function AddAccountDialog({
|
||||
}, [provider, open]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
return () => {
|
||||
powerUserModeRequestIdRef.current += 1;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!open || provider !== 'agy') {
|
||||
setAgyAckBypassEnabled(false);
|
||||
setAgyAckBypassLoading(false);
|
||||
useEffect(() => {
|
||||
if (!open || !supportsPowerUserMode) {
|
||||
powerUserModeRequestIdRef.current += 1;
|
||||
setPowerUserModeEnabled(false);
|
||||
setPowerUserModeLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadAgyBypassState = async () => {
|
||||
try {
|
||||
setAgyAckBypassLoading(true);
|
||||
const enabled = await fetchAgyBypassState();
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassEnabled(enabled);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassEnabled(false);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAgyBypassState();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fetchAgyBypassState, open, provider]);
|
||||
void syncPowerUserModeState();
|
||||
}, [open, provider, supportsPowerUserMode, syncPowerUserModeState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || provider !== 'agy' || !authFlow.error || !agyAckBypassEnabled) {
|
||||
if (!open || provider !== 'agy' || !authFlow.error || !powerUserModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,34 +211,11 @@ export function AddAccountDialog({
|
||||
normalizedError.includes('responsibility checklist');
|
||||
if (!ackRequired) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const syncBypassState = async () => {
|
||||
try {
|
||||
setAgyAckBypassLoading(true);
|
||||
const enabled = await fetchAgyBypassState();
|
||||
if (cancelled) return;
|
||||
setAgyAckBypassEnabled(enabled);
|
||||
if (!enabled) {
|
||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
||||
}
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setAgyAckBypassEnabled(false);
|
||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void syncBypassState();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [agyAckBypassEnabled, authFlow.error, fetchAgyBypassState, open, provider]);
|
||||
void syncPowerUserModeState({
|
||||
pendingMessage: t('addAccountDialog.powerUserLoading'),
|
||||
disabledMessage: t('addAccountDialog.powerUserUnavailableRetry'),
|
||||
});
|
||||
}, [authFlow.error, open, powerUserModeEnabled, provider, syncPowerUserModeState, t]);
|
||||
|
||||
// When authFlow completes successfully (polling detected success), apply preset and close
|
||||
useEffect(() => {
|
||||
@@ -248,8 +265,8 @@ export function AddAccountDialog({
|
||||
* - Authorization code providers use /start-url and polling.
|
||||
*/
|
||||
const handleAuthenticate = () => {
|
||||
if (isAgyBypassStatePending) {
|
||||
setLocalError('Loading Antigravity safety settings. Please wait a moment and retry.');
|
||||
if (isPowerUserModePending) {
|
||||
setLocalError(t('addAccountDialog.powerUserLoading'));
|
||||
return;
|
||||
}
|
||||
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
||||
@@ -258,7 +275,7 @@ export function AddAccountDialog({
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requiresSafetyAcknowledgement && !isGeminiRiskAcknowledged) {
|
||||
if (requiresGeminiSafetyAcknowledgement && !isGeminiRiskAcknowledged) {
|
||||
setLocalError(
|
||||
`Type "${RISK_ACK_PHRASE}" to acknowledge the account safety warning before authenticating this provider.`
|
||||
);
|
||||
@@ -336,7 +353,7 @@ export function AddAccountDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
{provider === 'agy' && agyAckBypassEnabled && !showAuthUI && (
|
||||
{supportsPowerUserMode && powerUserModeEnabled && !showAuthUI && (
|
||||
<div className="rounded-lg border border-amber-400/35 bg-amber-50/70 p-3 text-xs text-amber-900 dark:border-amber-800/60 dark:bg-amber-950/25 dark:text-amber-100">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 font-semibold">
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
@@ -346,7 +363,7 @@ export function AddAccountDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requiresSafetyAcknowledgement && !showAuthUI && (
|
||||
{requiresGeminiSafetyAcknowledgement && !showAuthUI && (
|
||||
<AccountSafetyWarningCard
|
||||
showAcknowledgement
|
||||
acknowledgementPhrase={RISK_ACK_PHRASE}
|
||||
@@ -541,9 +558,9 @@ export function AddAccountDialog({
|
||||
onClick={handleAuthenticate}
|
||||
disabled={
|
||||
isPending ||
|
||||
isAgyBypassStatePending ||
|
||||
isPowerUserModePending ||
|
||||
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
|
||||
(requiresSafetyAcknowledgement && !isGeminiRiskAcknowledged)
|
||||
(requiresGeminiSafetyAcknowledgement && !isGeminiRiskAcknowledged)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
|
||||
+67
-45
@@ -483,7 +483,13 @@ const resources = {
|
||||
descOauth: 'Click Authenticate to get an OAuth URL. Open it in any browser to sign in.',
|
||||
powerUserEnabled: 'Power user mode enabled',
|
||||
powerUserSkipped:
|
||||
'AGY responsibility checklist is skipped from Settings > Proxy. You accept full responsibility for OAuth/account risk.',
|
||||
'Settings > Proxy power user mode is skipping the AGY responsibility checklist and Gemini dashboard risk phrase. You accept full responsibility for OAuth/account risk.',
|
||||
powerUserLoadFailed:
|
||||
'Failed to load power user mode settings. Check Settings > Proxy and try again.',
|
||||
powerUserLoading:
|
||||
'Loading power user safety settings. Please wait a moment and retry.',
|
||||
powerUserUnavailableRetry:
|
||||
'Power user mode is unavailable. Complete the required provider safety step and retry.',
|
||||
authMethod: 'Auth Method',
|
||||
selectKiroAuthMethod: 'Select Kiro auth method',
|
||||
nicknameRequired: 'Nickname (required)',
|
||||
@@ -732,13 +738,13 @@ const resources = {
|
||||
viewDocs: 'View documentation',
|
||||
},
|
||||
settingsProxy: {
|
||||
failedLoadAgyMode: 'Failed to load AGY power user mode',
|
||||
failedUpdateAgyMode: 'Failed to update AGY power user mode',
|
||||
failedVerifyAgyMode: 'Failed to verify AGY power user mode persistence',
|
||||
failedLoadAgyMode: 'Failed to load power user mode',
|
||||
failedUpdateAgyMode: 'Failed to update power user mode',
|
||||
failedVerifyAgyMode: 'Failed to verify power user mode persistence',
|
||||
notPersistedAgyMode:
|
||||
'AGY power user mode was not persisted. Config may have been modified by another process.',
|
||||
agyModeEnabled: 'AGY power user mode enabled.',
|
||||
agyModeDisabled: 'AGY power user mode disabled.',
|
||||
'Power user mode was not persisted. Config may have been modified by another process.',
|
||||
agyModeEnabled: 'Power user mode enabled.',
|
||||
agyModeDisabled: 'Power user mode disabled.',
|
||||
typePhraseToContinue: 'Type "{{value}}" to continue.',
|
||||
description: 'Configure local or remote {{backend}} connection for proxy-based profiles',
|
||||
backendPlus: 'CLIProxy Plus',
|
||||
@@ -758,21 +764,22 @@ const resources = {
|
||||
'Existing Kiro/Copilot variants will not work with CLIProxyAPI. Switch to CLIProxyAPIPlus or remove those variants.',
|
||||
safety: 'Safety',
|
||||
agyModeTitle: 'Antigravity Power User Mode',
|
||||
agyModeDesc: 'Skip AGY responsibility checklist in Add Account and `ccs agy` flows.',
|
||||
agyModeDesc:
|
||||
'Skip the AGY responsibility checklist and the Gemini dashboard typed acknowledgement.',
|
||||
agyWarning:
|
||||
'Use only if you fully understand the OAuth suspension/ban risk pattern (#509). CCS cannot assume responsibility for account loss.',
|
||||
finalConfirm: 'Final confirmation required',
|
||||
finalConfirmDesc:
|
||||
'Enabling this will skip AGY safety checkpoints in both dashboard and CLI. Review issue #509 and type the exact phrase to proceed.',
|
||||
'Enabling this will skip AGY safety checkpoints and the Gemini dashboard risk phrase gate. Review issue #509 and type the exact phrase to proceed.',
|
||||
step1: 'Step 1',
|
||||
readIssue: 'Read issue #509',
|
||||
step2: 'Step 2',
|
||||
typePrefix: 'Type',
|
||||
typeSuffix: 'to enable.',
|
||||
typePhraseAria: 'Type I ACCEPT RISK to enable Antigravity power user mode',
|
||||
typePhraseAria: 'Type I ACCEPT RISK to enable power user mode',
|
||||
exactPhrase: 'Exact phrase required.',
|
||||
enableAgyMode: 'Enable Power User Mode',
|
||||
toggleAgyMode: 'Toggle AGY power user mode',
|
||||
toggleAgyMode: 'Toggle power user mode',
|
||||
fallbackSettings: 'Fallback Settings',
|
||||
enableFallback: 'Enable fallback to local',
|
||||
enableFallbackDesc: 'Use local proxy if remote is unreachable',
|
||||
@@ -1665,7 +1672,11 @@ const resources = {
|
||||
descDeviceCode: '点击认证后,验证码将显示,请在提供商网站输入。',
|
||||
descOauth: '点击认证获取 OAuth URL,在任意浏览器中打开即可登录。',
|
||||
powerUserEnabled: '已启用高级用户模式',
|
||||
powerUserSkipped: '已在设置 > 代理 中跳过 AGY 责任清单,您需自行承担 OAuth/账号风险。',
|
||||
powerUserSkipped:
|
||||
'设置 > 代理 中的高级用户模式会跳过 AGY 责任确认清单和 Gemini Dashboard 的风险短语。OAuth / 账号风险需自行承担。',
|
||||
powerUserLoadFailed: '加载高级用户模式设置失败。请检查“设置 > 代理”后重试。',
|
||||
powerUserLoading: '正在加载高级用户安全设置。请稍候后重试。',
|
||||
powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。',
|
||||
authMethod: '认证方式',
|
||||
selectKiroAuthMethod: '选择 Kiro 认证方式',
|
||||
nicknameRequired: '昵称(必填)',
|
||||
@@ -1904,12 +1915,12 @@ const resources = {
|
||||
viewDocs: '查看文档',
|
||||
},
|
||||
settingsProxy: {
|
||||
failedLoadAgyMode: '加载 AGY 高级模式失败',
|
||||
failedUpdateAgyMode: '更新 AGY 高级模式失败',
|
||||
failedVerifyAgyMode: '校验 AGY 高级模式持久化失败',
|
||||
notPersistedAgyMode: 'AGY 高级模式未成功持久化,配置可能被其他进程修改。',
|
||||
agyModeEnabled: 'AGY 高级模式已启用。',
|
||||
agyModeDisabled: 'AGY 高级模式已关闭。',
|
||||
failedLoadAgyMode: '加载高级模式失败',
|
||||
failedUpdateAgyMode: '更新高级模式失败',
|
||||
failedVerifyAgyMode: '校验高级模式持久化失败',
|
||||
notPersistedAgyMode: '高级模式未成功持久化,配置可能被其他进程修改。',
|
||||
agyModeEnabled: '高级模式已启用。',
|
||||
agyModeDisabled: '高级模式已关闭。',
|
||||
typePhraseToContinue: '请输入“{{value}}”后继续。',
|
||||
description: '为基于代理的配置设置本地或远程 {{backend}} 连接',
|
||||
backendPlus: 'CLIProxy Plus',
|
||||
@@ -1929,20 +1940,20 @@ const resources = {
|
||||
'现有 Kiro/Copilot 变体与 CLIProxyAPI 不兼容。请切换到 CLIProxyAPIPlus 或移除这些变体。',
|
||||
safety: '安全',
|
||||
agyModeTitle: 'Antigravity 高级模式',
|
||||
agyModeDesc: '在 Add Account 与 `ccs agy` 流程中跳过 AGY 责任确认清单。',
|
||||
agyModeDesc: '跳过 AGY 责任确认清单,以及 Gemini Dashboard 中输入风险短语的步骤。',
|
||||
agyWarning: '仅在你充分理解 OAuth 封禁风险模式(#509)后使用。CCS 不对账号损失承担责任。',
|
||||
finalConfirm: '需要最终确认',
|
||||
finalConfirmDesc:
|
||||
'启用后会跳过 Dashboard 与 CLI 的 AGY 安全检查。请先阅读 issue #509,并输入精确短语后继续。',
|
||||
'启用后会跳过 Dashboard / CLI 中的 AGY 安全检查,以及 Gemini Dashboard 的风险短语确认。请先阅读 issue #509,并输入精确短语后继续。',
|
||||
step1: '步骤 1',
|
||||
readIssue: '阅读 issue #509',
|
||||
step2: '步骤 2',
|
||||
typePrefix: '输入',
|
||||
typeSuffix: '以启用。',
|
||||
typePhraseAria: '输入 I ACCEPT RISK 以启用 Antigravity 高级模式',
|
||||
typePhraseAria: '输入 I ACCEPT RISK 以启用高级模式',
|
||||
exactPhrase: '必须输入完全一致的短语。',
|
||||
enableAgyMode: '启用高级模式',
|
||||
toggleAgyMode: '切换 AGY 高级模式',
|
||||
toggleAgyMode: '切换高级模式',
|
||||
fallbackSettings: '回退设置',
|
||||
enableFallback: '启用本地回退',
|
||||
enableFallbackDesc: '远程不可达时使用本地代理',
|
||||
@@ -2859,7 +2870,13 @@ const resources = {
|
||||
'Nhấp vào Xác thực để nhận URL OAuth. Mở nó trong bất kỳ trình duyệt nào để đăng nhập.',
|
||||
powerUserEnabled: 'Đã bật chế độ power user',
|
||||
powerUserSkipped:
|
||||
'Danh sách kiểm tra trách nhiệm của AGY bị bỏ qua khỏi Cài đặt > Proxy. Bạn chấp nhận hoàn toàn trách nhiệm về rủi ro tài khoản/OAuth.',
|
||||
'Chế độ power user trong Cài đặt > Proxy đang bỏ qua danh sách kiểm tra trách nhiệm AGY và bước nhập cụm từ rủi ro của Gemini trên dashboard. Bạn tự chịu hoàn toàn rủi ro OAuth/tài khoản.',
|
||||
powerUserLoadFailed:
|
||||
'Không thể tải cài đặt chế độ power user. Hãy kiểm tra Cài đặt > Proxy rồi thử lại.',
|
||||
powerUserLoading:
|
||||
'Đang tải cài đặt an toàn cho chế độ power user. Vui lòng đợi một chút rồi thử lại.',
|
||||
powerUserUnavailableRetry:
|
||||
'Chế độ power user hiện không khả dụng. Hãy hoàn tất bước an toàn bắt buộc của nhà cung cấp rồi thử lại.',
|
||||
authMethod: 'Phương thức xác thực',
|
||||
selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro',
|
||||
nicknameRequired: 'Biệt danh (bắt buộc)',
|
||||
@@ -3111,13 +3128,13 @@ const resources = {
|
||||
viewDocs: 'Xem tài liệu',
|
||||
},
|
||||
settingsProxy: {
|
||||
failedLoadAgyMode: 'Không thể tải chế độ power user AGY',
|
||||
failedUpdateAgyMode: 'Không thể cập nhật chế độ power user AGY',
|
||||
failedVerifyAgyMode: 'Không thể xác minh trạng thái lưu của chế độ power user AGY',
|
||||
failedLoadAgyMode: 'Không thể tải chế độ power user',
|
||||
failedUpdateAgyMode: 'Không thể cập nhật chế độ power user',
|
||||
failedVerifyAgyMode: 'Không thể xác minh trạng thái lưu của chế độ power user',
|
||||
notPersistedAgyMode:
|
||||
'Chế độ power user AGY chưa được lưu bền vững. Cấu hình có thể đã bị tiến trình khác sửa.',
|
||||
agyModeEnabled: 'Đã bật chế độ power user AGY.',
|
||||
agyModeDisabled: 'Đã tắt chế độ power user AGY.',
|
||||
'Chế độ power user chưa được lưu bền vững. Cấu hình có thể đã bị tiến trình khác sửa.',
|
||||
agyModeEnabled: 'Đã bật chế độ power user.',
|
||||
agyModeDisabled: 'Đã tắt chế độ power user.',
|
||||
typePhraseToContinue: 'Nhập "{{value}}" để tiếp tục.',
|
||||
description:
|
||||
'Định cấu hình kết nối {{backend}} cục bộ hoặc từ xa cho cấu hình dựa trên proxy',
|
||||
@@ -3140,21 +3157,21 @@ const resources = {
|
||||
safety: 'An toàn',
|
||||
agyModeTitle: 'Chế độ power user Antigravity',
|
||||
agyModeDesc:
|
||||
'Bỏ qua danh sách kiểm tra trách nhiệm AGY trong quy trình Thêm tài khoản và `ccs agy`.',
|
||||
'Bỏ qua danh sách kiểm tra trách nhiệm AGY và bước nhập cụm từ xác nhận của Gemini trên dashboard.',
|
||||
agyWarning:
|
||||
'Chỉ bật nếu bạn hiểu rõ rủi ro bị đình chỉ/cấm OAuth (#509). CCS không chịu trách nhiệm khi mất tài khoản.',
|
||||
finalConfirm: 'Xác nhận cuối cùng',
|
||||
finalConfirmDesc:
|
||||
'Việc bật tính năng này sẽ bỏ qua các điểm kiểm tra an toàn AGY trong cả bảng điều khiển và CLI. Xem lại vấn đề #509 và nhập cụm từ chính xác để tiếp tục.',
|
||||
'Việc bật tính năng này sẽ bỏ qua các điểm kiểm tra an toàn AGY và bước nhập cụm từ rủi ro của Gemini trên dashboard. Xem lại vấn đề #509 và nhập cụm từ chính xác để tiếp tục.',
|
||||
step1: 'Bước 1',
|
||||
readIssue: 'Đọc vấn đề #509',
|
||||
step2: 'Bước 2',
|
||||
typePrefix: 'Nhập',
|
||||
typeSuffix: 'để kích hoạt.',
|
||||
typePhraseAria: 'Nhập I ACCEPT RISK để bật chế độ power user Antigravity',
|
||||
typePhraseAria: 'Nhập I ACCEPT RISK để bật chế độ power user',
|
||||
exactPhrase: 'Phải nhập chính xác cụm từ.',
|
||||
enableAgyMode: 'Bật chế độ power user',
|
||||
toggleAgyMode: 'Chuyển đổi chế độ power user AGY',
|
||||
toggleAgyMode: 'Chuyển đổi chế độ power user',
|
||||
fallbackSettings: 'Cài đặt dự phòng',
|
||||
enableFallback: 'Cho phép dự phòng về cục bộ',
|
||||
enableFallbackDesc: 'Sử dụng proxy cục bộ nếu không thể truy cập được từ xa',
|
||||
@@ -4083,7 +4100,13 @@ const resources = {
|
||||
'認証をクリックすると OAuth URL を取得します。任意のブラウザーで開いてサインインしてください。',
|
||||
powerUserEnabled: '上級者モードが有効です',
|
||||
powerUserSkipped:
|
||||
'設定 > プロキシで AGY の責任確認チェックをスキップしています。OAuth / アカウントに関するリスクはすべて自己責任となります。',
|
||||
'設定 > プロキシのパワーユーザーモードにより、AGY の責任確認チェックと Gemini ダッシュボードのリスク文言入力をスキップしています。OAuth / アカウントに関するリスクはすべて自己責任です。',
|
||||
powerUserLoadFailed:
|
||||
'パワーユーザーモード設定を読み込めませんでした。設定 > プロキシを確認してから再試行してください。',
|
||||
powerUserLoading:
|
||||
'パワーユーザーモードの安全設定を読み込み中です。少し待ってから再試行してください。',
|
||||
powerUserUnavailableRetry:
|
||||
'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。',
|
||||
authMethod: '認証方法',
|
||||
selectKiroAuthMethod: 'Kiro の認証方法を選択',
|
||||
nicknameRequired: 'ニックネーム(必須)',
|
||||
@@ -4339,13 +4362,13 @@ const resources = {
|
||||
viewDocs: 'ドキュメントを見る',
|
||||
},
|
||||
settingsProxy: {
|
||||
failedLoadAgyMode: 'AGY パワーユーザーモードの読み込みに失敗しました',
|
||||
failedUpdateAgyMode: 'AGY パワーユーザーモードの更新に失敗しました',
|
||||
failedVerifyAgyMode: 'AGY パワーユーザーモードの永続化確認に失敗しました',
|
||||
failedLoadAgyMode: 'パワーユーザーモードの読み込みに失敗しました',
|
||||
failedUpdateAgyMode: 'パワーユーザーモードの更新に失敗しました',
|
||||
failedVerifyAgyMode: 'パワーユーザーモードの永続化確認に失敗しました',
|
||||
notPersistedAgyMode:
|
||||
'AGY パワーユーザーモードが保存されませんでした。別のプロセスが設定を変更した可能性があります。',
|
||||
agyModeEnabled: 'AGY パワーユーザーモードを有効にしました。',
|
||||
agyModeDisabled: 'AGY パワーユーザーモードを無効にしました。',
|
||||
'パワーユーザーモードが保存されませんでした。別のプロセスが設定を変更した可能性があります。',
|
||||
agyModeEnabled: 'パワーユーザーモードを有効にしました。',
|
||||
agyModeDisabled: 'パワーユーザーモードを無効にしました。',
|
||||
typePhraseToContinue: '続行するには「{{value}}」と入力してください。',
|
||||
description:
|
||||
'プロキシベースのプロファイル用に、ローカルまたはリモートの {{backend}} 接続を設定します',
|
||||
@@ -4368,22 +4391,21 @@ const resources = {
|
||||
safety: '安全設定',
|
||||
agyModeTitle: 'Antigravity パワーユーザーモード',
|
||||
agyModeDesc:
|
||||
'アカウント追加画面と `ccs agy` フローで、AGY の責任確認チェックをスキップします。',
|
||||
'アカウント追加画面の AGY 責任確認チェックと、Gemini ダッシュボードの入力確認をスキップします。',
|
||||
agyWarning:
|
||||
'OAuth の停止/BAN リスクパターン (#509) を十分理解している場合にのみ使用してください。CCS はアカウント喪失の責任を負いません。',
|
||||
finalConfirm: '最終確認',
|
||||
finalConfirmDesc:
|
||||
'有効にすると、ダッシュボードと CLI の両方で AGY の安全チェックをスキップします。続行する前に issue #509 を確認し、正確なフレーズを入力してください。',
|
||||
'有効にすると、ダッシュボード / CLI の AGY 安全チェックと Gemini ダッシュボードのリスク文言確認をスキップします。続行する前に issue #509 を確認し、正確なフレーズを入力してください。',
|
||||
step1: 'ステップ 1',
|
||||
readIssue: 'Issue #509 を読む',
|
||||
step2: 'ステップ 2',
|
||||
typePrefix: '有効にするには',
|
||||
typeSuffix: 'と入力してください。',
|
||||
typePhraseAria:
|
||||
'Antigravity パワーユーザーモードを有効にするには I ACCEPT RISK と入力してください',
|
||||
typePhraseAria: 'パワーユーザーモードを有効にするには I ACCEPT RISK と入力してください',
|
||||
exactPhrase: '完全一致で入力してください。',
|
||||
enableAgyMode: 'パワーユーザーモードを有効にする',
|
||||
toggleAgyMode: 'AGY パワーユーザーモードを切り替え',
|
||||
toggleAgyMode: 'パワーユーザーモードを切り替え',
|
||||
fallbackSettings: 'フォールバック設定',
|
||||
enableFallback: 'ローカルへのフォールバックを有効化',
|
||||
enableFallbackDesc: 'リモートに接続できない場合はローカルプロキシを使用します',
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
||||
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const authMocks = vi.hoisted(() => ({
|
||||
startAuth: vi.fn(),
|
||||
cancelAuth: vi.fn(),
|
||||
submitCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
const kiroImportMocks = vi.hoisted(() => ({
|
||||
mutate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-cliproxy-auth-flow', () => ({
|
||||
useCliproxyAuthFlow: () => ({
|
||||
provider: null,
|
||||
isAuthenticating: false,
|
||||
error: null,
|
||||
authUrl: null,
|
||||
oauthState: null,
|
||||
isSubmittingCallback: false,
|
||||
isDeviceCodeFlow: false,
|
||||
startAuth: authMocks.startAuth,
|
||||
cancelAuth: authMocks.cancelAuth,
|
||||
submitCallback: authMocks.submitCallback,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-cliproxy', () => ({
|
||||
useKiroImport: () => ({
|
||||
isPending: false,
|
||||
mutate: kiroImportMocks.mutate,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function createJsonResponse(body: Record<string, unknown>): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('AddAccountDialog power user mode', () => {
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
|
||||
beforeEach(async () => {
|
||||
await i18n.changeLanguage('en');
|
||||
vi.clearAllMocks();
|
||||
fetchMock.mockReset();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('skips the Gemini typed acknowledgement when power user mode is enabled', async () => {
|
||||
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: true }));
|
||||
|
||||
render(
|
||||
<AddAccountDialog open onClose={vi.fn()} provider="gemini" displayName="Gemini" />
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||
);
|
||||
|
||||
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||
await waitFor(() => expect(authenticateButton).toBeEnabled());
|
||||
|
||||
expect(screen.getByText('Power user mode enabled')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Type exact phrase to continue/i)).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(authenticateButton);
|
||||
|
||||
expect(authMocks.startAuth).toHaveBeenCalledWith(
|
||||
'gemini',
|
||||
expect.objectContaining({
|
||||
riskAcknowledgement: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the Gemini typed acknowledgement when power user mode is disabled', async () => {
|
||||
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: false }));
|
||||
|
||||
render(
|
||||
<AddAccountDialog open onClose={vi.fn()} provider="gemini" displayName="Gemini" />
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||
);
|
||||
|
||||
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||
await waitFor(() => expect(authenticateButton).toBeDisabled());
|
||||
|
||||
expect(screen.getByText(/Type exact phrase to continue/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces a power user mode fetch failure and fails closed for Gemini', async () => {
|
||||
fetchMock.mockRejectedValue(new Error('network down'));
|
||||
|
||||
render(
|
||||
<AddAccountDialog open onClose={vi.fn()} provider="gemini" displayName="Gemini" />
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'Failed to load power user mode settings. Check Settings > Proxy and try again.'
|
||||
)
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Failed to load power user mode settings. Check Settings > Proxy and try again.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/Type exact phrase to continue/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Authenticate' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('skips the AGY responsibility checklist when power user mode is enabled', async () => {
|
||||
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: true }));
|
||||
|
||||
render(<AddAccountDialog open onClose={vi.fn()} provider="agy" displayName="AGY" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||
);
|
||||
|
||||
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||
await waitFor(() => expect(authenticateButton).toBeEnabled());
|
||||
|
||||
expect(screen.getByText('Power user mode enabled')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Step 1: I reviewed issue #509/i)).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(authenticateButton);
|
||||
|
||||
expect(authMocks.startAuth).toHaveBeenCalledWith(
|
||||
'agy',
|
||||
expect.objectContaining({
|
||||
riskAcknowledgement: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the AGY responsibility checklist when power user mode is disabled', async () => {
|
||||
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: false }));
|
||||
|
||||
render(<AddAccountDialog open onClose={vi.fn()} provider="agy" displayName="AGY" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||
);
|
||||
|
||||
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||
await waitFor(() => expect(authenticateButton).toBeDisabled());
|
||||
|
||||
expect(screen.getByText(/Step 1: I reviewed issue #509/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user