Files
agent-fox/packages/web/src/components/PasswordRevealPrompt.tsx
YANG JIANKUAN 95198e6a07 feat: 完善 MCP 接入体验与协议层稳定性
接入体验:
- 客户端选择器覆盖 7 个客户端(Claude Desktop/Code、Cursor、Cline、
  Windsurf、Codex、GitHub Copilot),自动生成对应配置
- API Key 一键含 key 复制(密码验证后写入剪贴板)
- 公开 /mcp/:projectId/info + Test connection 按钮
- 项目新增 Usage 标签(每日柱状图 + 工具调用分布)
- 导入完成后引导跳转到 MCP 配置

协议层稳定性:
- session 超时清理(5 分钟扫描 / 30 分钟 idle 关闭)
- 错误响应标准化为 { error: { code, message } }
- 调用日志捕获错误信息 + 4KB payload 截断
- McpCallLog 增加 errorMessage 字段(migration)

抽出可复用模块:PasswordRevealPrompt 组件、BarChart 组件、
trends helper(daysWindowStart + getDailyTrends)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:52:47 +08:00

77 lines
2.6 KiB
TypeScript

import { useState } from 'react';
import { apiFetch } from '../lib/api';
import { useI18n } from '../lib/i18n';
import { useAuth } from '../lib/auth';
type Props = {
open: boolean;
promptText: string;
onCancel: () => void;
onReveal: (apiKey: string) => void;
onSetPasswordClick: () => void;
};
export default function PasswordRevealPrompt({ open, promptText, onCancel, onReveal, onSetPasswordClick }: Props) {
const { t } = useI18n();
const { user } = useAuth();
const hasPassword = user?.hasPassword !== false;
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
if (!open) return null;
const submit = async () => {
setLoading(true);
setError('');
try {
const data = await apiFetch<{ apiKey: string }>('/auth/api-key/reveal', {
method: 'POST', body: JSON.stringify({ password }),
});
onReveal(data.apiKey);
setPassword('');
} catch (err) {
setError(err instanceof Error ? err.message : 'Verification failed');
} finally {
setLoading(false);
}
};
if (!hasPassword) {
return (
<div className="p-3.5 rounded-lg border border-border-default bg-bg-primary space-y-2 animate-fade-in">
<p className="text-[13px] text-text-secondary">{t('dashboard.settings.setPasswordToReveal')}</p>
<div className="flex gap-2">
<button onClick={onSetPasswordClick} className="btn-primary text-[13px] py-1.5">
{t('dashboard.settings.setPasswordAction')}
</button>
<button onClick={onCancel} className="btn-ghost text-[13px] py-1.5">{t('common.cancel')}</button>
</div>
</div>
);
}
return (
<div className="p-3.5 rounded-lg border border-border-default bg-bg-primary space-y-2 animate-fade-in">
<p className="text-[13px] text-text-secondary">{promptText}</p>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && password) submit(); }}
className="input-base"
placeholder={t('dashboard.settings.currentPassword')}
autoFocus
/>
{error && <p className="text-[12px] text-danger">{error}</p>}
<div className="flex gap-2">
<button onClick={submit} disabled={loading || !password} className="btn-primary text-[13px] py-1.5">
{loading ? t('dashboard.settings.verifying') : t('common.confirm')}
</button>
<button onClick={onCancel} className="btn-ghost text-[13px] py-1.5">{t('common.cancel')}</button>
</div>
</div>
);
}