接入体验:
- 客户端选择器覆盖 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>
77 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|