feat: 界面全部改为中文
- 侧边栏导航:仪表盘、登录、内容浏览、发布、互动、API 测试、设置 - 7 个页面所有按钮、标签、提示、错误信息改为中文 - API 端点列表分类改为中文(登录、内容、发布、互动) - 组件内文本:展开/收起、复制、点赞、收藏、评论等 - 页面标题改为 Social MCP - 管理后台
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Social MCP - Admin Dashboard</title>
|
||||
<title>Social MCP - 管理后台</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>" />
|
||||
</head>
|
||||
<body class="bg-dark-bg text-dark-text">
|
||||
|
||||
@@ -25,7 +25,7 @@ export function CommentTree({ comments, depth = 0 }: Props) {
|
||||
</div>
|
||||
<p className="text-sm text-dark-text/90">{comment.content}</p>
|
||||
{comment.likeCount > 0 && (
|
||||
<span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} likes</span>
|
||||
<span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} 赞</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,15 +23,15 @@ export function FeedCard({ feed, onClick }: Props) {
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-dark-muted text-sm">
|
||||
No Cover
|
||||
无封面
|
||||
</div>
|
||||
)}
|
||||
{feed.type === 'video' && (
|
||||
<Badge variant="info" className="absolute top-2 right-2">Video</Badge>
|
||||
<Badge variant="info" className="absolute top-2 right-2">视频</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || 'Untitled'}</h3>
|
||||
<h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || '无标题'}</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{feed.user.avatar && (
|
||||
@@ -39,7 +39,7 @@ export function FeedCard({ feed, onClick }: Props) {
|
||||
)}
|
||||
<span className="text-xs text-dark-muted truncate">{feed.user.nickname}</span>
|
||||
</div>
|
||||
<span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} likes</span>
|
||||
<span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} 赞</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative ml-auto w-full max-w-2xl bg-dark-card border-l border-dark-border overflow-y-auto">
|
||||
<div className="sticky top-0 bg-dark-card border-b border-dark-border px-5 py-3 flex items-center justify-between z-10">
|
||||
<h3 className="font-semibold truncate">Feed Detail</h3>
|
||||
<h3 className="font-semibold truncate">笔记详情</h3>
|
||||
<button onClick={onClose} className="text-dark-muted hover:text-dark-text text-xl">×</button>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
||||
{/* Video */}
|
||||
{detail.videoUrl && (
|
||||
<div className="rounded-xl overflow-hidden bg-dark-bg p-4">
|
||||
<Badge variant="info">Video Note</Badge>
|
||||
<Badge variant="info">视频笔记</Badge>
|
||||
<p className="text-xs text-dark-muted mt-2 break-all">{detail.videoUrl}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -108,10 +108,10 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Likes', value: detail.likeCount },
|
||||
{ label: 'Collects', value: detail.collectCount },
|
||||
{ label: 'Comments', value: detail.commentCount },
|
||||
{ label: 'Shares', value: detail.shareCount },
|
||||
{ label: '点赞', value: detail.likeCount },
|
||||
{ label: '收藏', value: detail.collectCount },
|
||||
{ label: '评论', value: detail.commentCount },
|
||||
{ label: '分享', value: detail.shareCount },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
||||
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
|
||||
@@ -145,14 +145,14 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
||||
variant="ghost"
|
||||
onClick={() => void navigator.clipboard.writeText(detail.id)}
|
||||
>
|
||||
Copy Feed ID
|
||||
复制 Feed ID
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => void navigator.clipboard.writeText(detail.xsecToken)}
|
||||
>
|
||||
Copy Token
|
||||
复制 Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
||||
{detail.comments.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
|
||||
Comments ({detail.comments.length})
|
||||
评论 ({detail.comments.length})
|
||||
</h3>
|
||||
<CommentTree comments={detail.comments} />
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
export function FeedGrid({ feeds, loading, onSelect, emptyText = 'No feeds found' }: Props) {
|
||||
export function FeedGrid({ feeds, loading, onSelect, emptyText = '暂无内容' }: Props) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-12">
|
||||
|
||||
@@ -56,10 +56,10 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Follows', value: profile.follows },
|
||||
{ label: 'Fans', value: profile.fans },
|
||||
{ label: 'Interactions', value: profile.interaction },
|
||||
{ label: 'Notes', value: profile.feedCount },
|
||||
{ label: '关注', value: profile.follows },
|
||||
{ label: '粉丝', value: profile.fans },
|
||||
{ label: '获赞与收藏', value: profile.interaction },
|
||||
{ label: '笔记', value: profile.feedCount },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
||||
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
|
||||
@@ -72,7 +72,7 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
|
||||
{profile.feeds.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
|
||||
Recent Notes ({profile.feeds.length})
|
||||
最近笔记 ({profile.feeds.length})
|
||||
</h3>
|
||||
<FeedGrid feeds={profile.feeds} loading={false} onSelect={onFeedSelect} />
|
||||
</div>
|
||||
|
||||
@@ -20,17 +20,17 @@ export function Header() {
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
Token 未配置 — 点击前往 Settings
|
||||
Token 未配置 — 点击前往设置
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{health && (
|
||||
<Badge variant={health.healthy ? 'success' : 'danger'}>
|
||||
{health.healthy ? 'Healthy' : 'Unhealthy'}
|
||||
{health.healthy ? '正常' : '异常'}
|
||||
</Badge>
|
||||
)}
|
||||
{!health && <Badge variant="warning">Connecting...</Badge>}
|
||||
{!health && <Badge variant="warning">连接中...</Badge>}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -17,13 +17,13 @@ export function JsonViewer({ data, collapsed = false, maxHeight = '400px' }: Pro
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="text-xs text-dark-muted hover:text-dark-text"
|
||||
>
|
||||
{isCollapsed ? 'Expand' : 'Collapse'}
|
||||
{isCollapsed ? '展开' : '收起'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void navigator.clipboard.writeText(json)}
|
||||
className="text-xs text-dark-muted hover:text-dark-accent"
|
||||
>
|
||||
Copy
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
|
||||
+20
-20
@@ -1,25 +1,25 @@
|
||||
export const NAV_ITEMS = [
|
||||
{ path: '/', label: 'Dashboard', icon: 'dashboard' },
|
||||
{ path: '/login', label: 'Login', icon: 'login' },
|
||||
{ path: '/browser', label: 'Browser', icon: 'browser' },
|
||||
{ path: '/publish', label: 'Publish', icon: 'publish' },
|
||||
{ path: '/interactions', label: 'Interactions', icon: 'interactions' },
|
||||
{ path: '/api-tester', label: 'API Tester', icon: 'api' },
|
||||
{ path: '/settings', label: 'Settings', icon: 'settings' },
|
||||
{ path: '/', label: '仪表盘', icon: 'dashboard' },
|
||||
{ path: '/login', label: '登录', icon: 'login' },
|
||||
{ path: '/browser', label: '内容浏览', icon: 'browser' },
|
||||
{ path: '/publish', label: '发布', icon: 'publish' },
|
||||
{ path: '/interactions', label: '互动', icon: 'interactions' },
|
||||
{ path: '/api-tester', label: 'API 测试', icon: 'api' },
|
||||
{ path: '/settings', label: '设置', icon: 'settings' },
|
||||
] as const;
|
||||
|
||||
export const API_ENDPOINTS = [
|
||||
{ key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: 'Check Login Status', category: 'Login' },
|
||||
{ key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: 'Get Login QR Code', category: 'Login' },
|
||||
{ key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: 'Delete Cookies (Logout)', category: 'Login' },
|
||||
{ key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: 'List Feeds', category: 'Content' },
|
||||
{ key: 'search', method: 'POST', path: '/api/xhs/search', label: 'Search', category: 'Content', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } },
|
||||
{ key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: 'Feed Detail', category: 'Content', body: { feed_id: '', xsec_token: '', load_all_comments: false } },
|
||||
{ key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: 'User Profile', category: 'Content', body: { user_id: '', xsec_token: '' } },
|
||||
{ key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: 'Publish Image Note', category: 'Publish', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } },
|
||||
{ key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: 'Publish Video Note', category: 'Publish', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } },
|
||||
{ key: 'comment', method: 'POST', path: '/api/xhs/comment', label: 'Post Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '' } },
|
||||
{ key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: 'Reply Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } },
|
||||
{ key: 'like', method: 'POST', path: '/api/xhs/like', label: 'Like/Unlike', category: 'Interaction', body: { feed_id: '', xsec_token: '', unlike: false } },
|
||||
{ key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: 'Favorite/Unfavorite', category: 'Interaction', body: { feed_id: '', xsec_token: '', unfavorite: false } },
|
||||
{ key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: '检查登录状态', category: '登录' },
|
||||
{ key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: '获取登录二维码', category: '登录' },
|
||||
{ key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: '删除 Cookie(登出)', category: '登录' },
|
||||
{ key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: '获取推荐列表', category: '内容' },
|
||||
{ key: 'search', method: 'POST', path: '/api/xhs/search', label: '搜索', category: '内容', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } },
|
||||
{ key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: '笔记详情', category: '内容', body: { feed_id: '', xsec_token: '', load_all_comments: false } },
|
||||
{ key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: '用户主页', category: '内容', body: { user_id: '', xsec_token: '' } },
|
||||
{ key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: '发布图文笔记', category: '发布', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } },
|
||||
{ key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: '发布视频笔记', category: '发布', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } },
|
||||
{ key: 'comment', method: 'POST', path: '/api/xhs/comment', label: '发表评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '' } },
|
||||
{ key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: '回复评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } },
|
||||
{ key: 'like', method: 'POST', path: '/api/xhs/like', label: '点赞/取消', category: '互动', body: { feed_id: '', xsec_token: '', unlike: false } },
|
||||
{ key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: '收藏/取消', category: '互动', body: { feed_id: '', xsec_token: '', unfavorite: false } },
|
||||
] as const;
|
||||
|
||||
@@ -52,12 +52,12 @@ export function ApiTesterPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold">API Tester</h1>
|
||||
<h1 className="text-2xl font-bold">API 测试</h1>
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ key: 'rest', label: 'REST API' },
|
||||
{ key: 'mcp', label: 'MCP Tools' },
|
||||
{ key: 'mcp', label: 'MCP 工具' },
|
||||
]}
|
||||
active={mode}
|
||||
onChange={(k) => setMode(k as 'rest' | 'mcp')}
|
||||
@@ -87,11 +87,11 @@ export function ApiTesterPage() {
|
||||
{response !== null && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Response</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">响应</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{responseStatus && (
|
||||
<Badge variant={responseStatus === 'success' ? 'success' : 'danger'}>
|
||||
{responseStatus}
|
||||
{responseStatus === 'success' ? '成功' : '失败'}
|
||||
</Badge>
|
||||
)}
|
||||
{duration !== null && (
|
||||
@@ -108,11 +108,11 @@ export function ApiTesterPage() {
|
||||
<div>
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">History</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => setHistory([])}>Clear</Button>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">历史记录</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => setHistory([])}>清空</Button>
|
||||
</div>
|
||||
{history.length === 0 ? (
|
||||
<p className="text-sm text-dark-muted">No requests yet</p>
|
||||
<p className="text-sm text-dark-muted">暂无请求</p>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
{history.map((entry) => (
|
||||
@@ -136,7 +136,7 @@ export function ApiTesterPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 text-dark-muted">
|
||||
<Badge variant={entry.status === 'success' ? 'success' : 'danger'} className="text-[10px]">
|
||||
{entry.status}
|
||||
{entry.status === 'success' ? '成功' : '失败'}
|
||||
</Badge>
|
||||
<span>{entry.time}</span>
|
||||
<span>{entry.duration}ms</span>
|
||||
@@ -227,10 +227,10 @@ function RestPanel({
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST Request</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST 请求</h2>
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
label="Endpoint"
|
||||
label="端点"
|
||||
options={categories.flatMap((cat) => [
|
||||
{ value: `__cat_${cat}`, label: `── ${cat} ──` },
|
||||
...API_ENDPOINTS.filter((e) => e.category === cat).map((e) => ({
|
||||
@@ -253,7 +253,7 @@ function RestPanel({
|
||||
</div>
|
||||
{endpoint.method !== 'GET' && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm text-dark-muted">Request Body (JSON)</label>
|
||||
<label className="text-sm text-dark-muted">请求体 (JSON)</label>
|
||||
<textarea
|
||||
value={bodyText}
|
||||
onChange={(e) => setBodyText(e.target.value)}
|
||||
@@ -263,8 +263,8 @@ function RestPanel({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => void handleSend()} loading={loading}>Send Request</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}>Copy cURL</Button>
|
||||
<Button onClick={() => void handleSend()} loading={loading}>发送请求</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}>复制 cURL</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -375,20 +375,20 @@ function McpPanel({
|
||||
<>
|
||||
{/* Connection */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Connection</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP 连接</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={connState === 'connected' ? 'success' : connState === 'connecting' ? 'warning' : 'default'}>
|
||||
{connState}
|
||||
{connState === 'connected' ? '已连接' : connState === 'connecting' ? '连接中' : '未连接'}
|
||||
</Badge>
|
||||
{connState === 'disconnected' && (
|
||||
<Button size="sm" onClick={() => void handleConnect()} loading={connecting}>
|
||||
Connect
|
||||
连接
|
||||
</Button>
|
||||
)}
|
||||
{connState === 'connected' && (
|
||||
<>
|
||||
<span className="text-xs text-dark-muted">{tools.length} tools available</span>
|
||||
<Button size="sm" variant="ghost" onClick={handleDisconnect}>Disconnect</Button>
|
||||
<span className="text-xs text-dark-muted">{tools.length} 个工具可用</span>
|
||||
<Button size="sm" variant="ghost" onClick={handleDisconnect}>断开</Button>
|
||||
</>
|
||||
)}
|
||||
{connState === 'connecting' && <Spinner size="sm" />}
|
||||
@@ -398,10 +398,10 @@ function McpPanel({
|
||||
{/* Tool Call */}
|
||||
{connState === 'connected' && tools.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Tool Call</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP 工具调用</h2>
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
label="Tool"
|
||||
label="工具"
|
||||
options={tools.map((t) => ({ value: t.name, label: t.name }))}
|
||||
value={selectedTool}
|
||||
onChange={(e) => handleToolChange(e.target.value)}
|
||||
@@ -415,13 +415,13 @@ function McpPanel({
|
||||
)}
|
||||
{currentTool.inputSchema?.properties && (
|
||||
<div>
|
||||
<p className="text-dark-muted font-semibold mb-1">Parameters:</p>
|
||||
<p className="text-dark-muted font-semibold mb-1">参数:</p>
|
||||
{Object.entries(currentTool.inputSchema.properties).map(([key, prop]) => (
|
||||
<div key={key} className="flex gap-2 ml-2">
|
||||
<code className="text-dark-accent">{key}</code>
|
||||
<span className="text-dark-muted">
|
||||
{prop.type || ''}
|
||||
{currentTool.inputSchema?.required?.includes(key) ? ' (required)' : ' (optional)'}
|
||||
{currentTool.inputSchema?.required?.includes(key) ? ' (必填)' : ' (可选)'}
|
||||
{prop.description ? ` — ${prop.description}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -433,7 +433,7 @@ function McpPanel({
|
||||
|
||||
{/* Arguments editor */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm text-dark-muted">Arguments (JSON)</label>
|
||||
<label className="text-sm text-dark-muted">参数 (JSON)</label>
|
||||
<textarea
|
||||
value={argsText}
|
||||
onChange={(e) => setArgsText(e.target.value)}
|
||||
@@ -443,7 +443,7 @@ function McpPanel({
|
||||
</div>
|
||||
|
||||
<Button onClick={() => void handleCall()} loading={loading}>
|
||||
Call Tool
|
||||
调用工具
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -452,8 +452,8 @@ function McpPanel({
|
||||
{connState === 'disconnected' && (
|
||||
<Card>
|
||||
<div className="text-center py-8 text-dark-muted">
|
||||
<p>Connect to the MCP server to discover and test tools.</p>
|
||||
<p className="text-xs mt-2">This uses the same SSE + JSON-RPC protocol that AI clients use.</p>
|
||||
<p>连接到 MCP 服务器以发现和测试工具。</p>
|
||||
<p className="text-xs mt-2">使用与 AI 客户端相同的 SSE + JSON-RPC 协议。</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -43,10 +43,10 @@ export function BrowserPage() {
|
||||
if (res.success && res.data) {
|
||||
setFeeds(res.data);
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Failed to load feeds');
|
||||
toast('error', res.error?.message || '加载推荐失败');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('error', err instanceof Error ? err.message : 'Failed to load feeds');
|
||||
toast('error', err instanceof Error ? err.message : '加载推荐失败');
|
||||
} finally {
|
||||
setFeedsLoading(false);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function BrowserPage() {
|
||||
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!keyword.trim()) {
|
||||
toast('warning', 'Enter a keyword');
|
||||
toast('warning', '请输入关键词');
|
||||
return;
|
||||
}
|
||||
setSearchLoading(true);
|
||||
@@ -68,10 +68,10 @@ export function BrowserPage() {
|
||||
if (res.success && res.data) {
|
||||
setSearchResults(res.data);
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Search failed');
|
||||
toast('error', res.error?.message || '搜索失败');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('error', err instanceof Error ? err.message : 'Search failed');
|
||||
toast('error', err instanceof Error ? err.message : '搜索失败');
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
@@ -89,13 +89,13 @@ export function BrowserPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold">Content Browser</h1>
|
||||
<h1 className="text-2xl font-bold">内容浏览</h1>
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ key: 'explore', label: 'Explore' },
|
||||
{ key: 'search', label: 'Search' },
|
||||
{ key: 'user', label: 'User Profile' },
|
||||
{ key: 'explore', label: '探索' },
|
||||
{ key: 'search', label: '搜索' },
|
||||
{ key: 'user', label: '用户主页' },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={(k) => setTab(k as TabKey)}
|
||||
@@ -105,13 +105,13 @@ export function BrowserPage() {
|
||||
{tab === 'explore' && (
|
||||
<div className="space-y-4">
|
||||
<Button onClick={() => void handleExplore()} loading={feedsLoading}>
|
||||
Load Feed
|
||||
加载推荐
|
||||
</Button>
|
||||
<FeedGrid
|
||||
feeds={feeds}
|
||||
loading={feedsLoading}
|
||||
onSelect={handleFeedSelect}
|
||||
emptyText="Click 'Load Feed' to get recommended content"
|
||||
emptyText="点击「加载推荐」获取推荐内容"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -122,53 +122,53 @@ export function BrowserPage() {
|
||||
<div className="flex gap-3 items-end flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<Input
|
||||
label="Keyword"
|
||||
placeholder="Search xiaohongshu..."
|
||||
label="关键词"
|
||||
placeholder="搜索小红书..."
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
label="Sort"
|
||||
label="排序"
|
||||
options={[
|
||||
{ value: 'general', label: 'Default' },
|
||||
{ value: 'time_descending', label: 'Latest' },
|
||||
{ value: 'popularity_descending', label: 'Popular' },
|
||||
{ value: 'general', label: '默认' },
|
||||
{ value: 'time_descending', label: '最新' },
|
||||
{ value: 'popularity_descending', label: '热门' },
|
||||
]}
|
||||
value={sortFilter}
|
||||
onChange={(e) => setSortFilter(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
label="Type"
|
||||
label="类型"
|
||||
options={[
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'note', label: 'Notes' },
|
||||
{ value: 'video', label: 'Videos' },
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'note', label: '图文' },
|
||||
{ value: 'video', label: '视频' },
|
||||
]}
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
label="Time"
|
||||
label="时间"
|
||||
options={[
|
||||
{ value: 'all', label: 'Any time' },
|
||||
{ value: 'day', label: 'Past day' },
|
||||
{ value: 'week', label: 'Past week' },
|
||||
{ value: 'half_year', label: 'Past 6 months' },
|
||||
{ value: 'all', label: '不限' },
|
||||
{ value: 'day', label: '一天内' },
|
||||
{ value: 'week', label: '一周内' },
|
||||
{ value: 'half_year', label: '半年内' },
|
||||
]}
|
||||
value={timeFilter}
|
||||
onChange={(e) => setTimeFilter(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => void handleSearch()} loading={searchLoading}>
|
||||
Search
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<FeedGrid
|
||||
feeds={searchResults}
|
||||
loading={searchLoading}
|
||||
onSelect={handleFeedSelect}
|
||||
emptyText="Enter a keyword and search"
|
||||
emptyText="输入关键词进行搜索"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -178,11 +178,11 @@ export function BrowserPage() {
|
||||
<div className="space-y-4">
|
||||
{!userView && (
|
||||
<div className="text-center py-12 text-dark-muted">
|
||||
<p>Click on a user in a feed detail to view their profile</p>
|
||||
<p className="text-xs mt-2">Or enter user details manually:</p>
|
||||
<p>在笔记详情中点击用户查看主页</p>
|
||||
<p className="text-xs mt-2">或手动输入用户信息:</p>
|
||||
<div className="flex gap-3 items-end justify-center mt-4">
|
||||
<Input
|
||||
placeholder="User ID"
|
||||
placeholder="用户 ID"
|
||||
value={manualUserId}
|
||||
onChange={(e) => setManualUserId(e.target.value)}
|
||||
/>
|
||||
@@ -199,7 +199,7 @@ export function BrowserPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Load
|
||||
加载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +207,7 @@ export function BrowserPage() {
|
||||
{userView && userView.userId && userView.xsecToken && (
|
||||
<div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setUserView(null)} className="mb-4">
|
||||
← Back
|
||||
← 返回
|
||||
</Button>
|
||||
<UserCard
|
||||
userId={userView.userId}
|
||||
|
||||
@@ -15,9 +15,9 @@ export function DashboardPage() {
|
||||
return (
|
||||
<div className="space-y-6 max-w-5xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<h1 className="text-2xl font-bold">仪表盘</h1>
|
||||
<Button variant="ghost" size="sm" onClick={() => void refreshHealth()}>
|
||||
Refresh
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -25,24 +25,24 @@ export function DashboardPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Server Status */}
|
||||
<Card>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Server</div>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">服务器</div>
|
||||
{healthLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : health ? (
|
||||
<div className="space-y-1">
|
||||
<Badge variant={health.healthy ? 'success' : 'danger'}>
|
||||
{health.healthy ? 'Healthy' : 'Unhealthy'}
|
||||
{health.healthy ? '正常' : '异常'}
|
||||
</Badge>
|
||||
<p className="text-sm text-dark-muted">v{health.version}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Badge variant="danger">Offline</Badge>
|
||||
<Badge variant="danger">离线</Badge>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Uptime */}
|
||||
<Card>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Uptime</div>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">运行时间</div>
|
||||
{health ? (
|
||||
<p className="text-xl font-mono font-bold text-dark-text">{formatUptime(health.uptime)}</p>
|
||||
) : (
|
||||
@@ -52,13 +52,13 @@ export function DashboardPage() {
|
||||
|
||||
{/* Login Status */}
|
||||
<Card>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Xiaohongshu Login</div>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">小红书登录</div>
|
||||
{loginLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : loginStatus ? (
|
||||
<div className="space-y-1">
|
||||
<Badge variant={loginStatus.loggedIn ? 'success' : 'warning'}>
|
||||
{loginStatus.loggedIn ? 'Logged In' : 'Not Logged In'}
|
||||
{loginStatus.loggedIn ? '已登录' : '未登录'}
|
||||
</Badge>
|
||||
{loginStatus.username && (
|
||||
<p className="text-sm text-dark-muted">{loginStatus.username}</p>
|
||||
@@ -69,18 +69,18 @@ export function DashboardPage() {
|
||||
onClick={() => void refreshLogin()}
|
||||
className="text-xs text-dark-accent hover:underline"
|
||||
>
|
||||
Click to check
|
||||
点击检查
|
||||
</button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Memory */}
|
||||
<Card>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Memory</div>
|
||||
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">内存</div>
|
||||
{health ? (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xl font-mono font-bold text-dark-text">{health.memory.heapUsed} MB</p>
|
||||
<p className="text-xs text-dark-muted">of {health.memory.heapTotal} MB heap</p>
|
||||
<p className="text-xs text-dark-muted">共 {health.memory.heapTotal} MB 堆内存</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-dark-muted">-</p>
|
||||
@@ -91,13 +91,13 @@ export function DashboardPage() {
|
||||
{/* Plugin Health */}
|
||||
{health && Object.keys(health.plugins).length > 0 && (
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Plugins</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">插件</h2>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(health.plugins).map(([name, info]) => (
|
||||
<div key={name} className="flex items-center justify-between py-1">
|
||||
<span className="text-sm">{name}</span>
|
||||
<Badge variant={info.healthy ? 'success' : 'danger'}>
|
||||
{info.healthy ? 'Healthy' : info.message || 'Unhealthy'}
|
||||
{info.healthy ? '正常' : info.message || '异常'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
@@ -107,19 +107,19 @@ export function DashboardPage() {
|
||||
|
||||
{/* Quick actions */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">快捷操作</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/login')}>
|
||||
Manage Login
|
||||
管理登录
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/browser')}>
|
||||
Browse Content
|
||||
浏览内容
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/publish')}>
|
||||
Publish Note
|
||||
发布笔记
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/api-tester')}>
|
||||
API Tester
|
||||
API 测试
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -128,7 +128,7 @@ export function DashboardPage() {
|
||||
{health && (
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
|
||||
Raw Health Data
|
||||
原始健康数据
|
||||
</h2>
|
||||
<pre className="bg-dark-bg border border-dark-border rounded-lg p-4 text-xs text-dark-text overflow-auto font-mono max-h-64">
|
||||
{JSON.stringify(health, null, 2)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function InteractionsPage() {
|
||||
|
||||
const checkIds = () => {
|
||||
if (!feedId.trim() || !xsecToken.trim()) {
|
||||
toast('warning', 'Feed ID and xsec_token are required');
|
||||
toast('warning', 'Feed ID 和 xsec_token 为必填项');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -48,11 +48,11 @@ export function InteractionsPage() {
|
||||
setLoading(unlike ? 'unlike' : 'like');
|
||||
try {
|
||||
const res = await toggleLike(feedId, xsecToken, unlike);
|
||||
addLog(unlike ? 'Unlike' : 'Like', res);
|
||||
toast('success', unlike ? 'Unliked' : 'Liked');
|
||||
addLog(unlike ? '取消点赞' : '点赞', res);
|
||||
toast('success', unlike ? '已取消点赞' : '已点赞');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed';
|
||||
addLog(unlike ? 'Unlike' : 'Like', { error: msg });
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog(unlike ? '取消点赞' : '点赞', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@@ -64,11 +64,11 @@ export function InteractionsPage() {
|
||||
setLoading(unfavorite ? 'unfavorite' : 'favorite');
|
||||
try {
|
||||
const res = await toggleFavorite(feedId, xsecToken, unfavorite);
|
||||
addLog(unfavorite ? 'Unfavorite' : 'Favorite', res);
|
||||
toast('success', unfavorite ? 'Unfavorited' : 'Favorited');
|
||||
addLog(unfavorite ? '取消收藏' : '收藏', res);
|
||||
toast('success', unfavorite ? '已取消收藏' : '已收藏');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed';
|
||||
addLog(unfavorite ? 'Unfavorite' : 'Favorite', { error: msg });
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog(unfavorite ? '取消收藏' : '收藏', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@@ -77,18 +77,18 @@ export function InteractionsPage() {
|
||||
|
||||
const handleComment = useCallback(async () => {
|
||||
if (!checkIds() || !commentText.trim()) {
|
||||
toast('warning', 'Comment text is required');
|
||||
toast('warning', '评论内容为必填项');
|
||||
return;
|
||||
}
|
||||
setLoading('comment');
|
||||
try {
|
||||
const res = await postComment(feedId, xsecToken, commentText);
|
||||
addLog('Comment', res);
|
||||
toast('success', 'Comment posted');
|
||||
addLog('评论', res);
|
||||
toast('success', '评论已发布');
|
||||
setCommentText('');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed';
|
||||
addLog('Comment', { error: msg });
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog('评论', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@@ -97,7 +97,7 @@ export function InteractionsPage() {
|
||||
|
||||
const handleReply = useCallback(async () => {
|
||||
if (!checkIds() || !replyText.trim()) {
|
||||
toast('warning', 'Reply text is required');
|
||||
toast('warning', '回复内容为必填项');
|
||||
return;
|
||||
}
|
||||
setLoading('reply');
|
||||
@@ -109,12 +109,12 @@ export function InteractionsPage() {
|
||||
comment_id: replyCommentId || undefined,
|
||||
user_id: replyUserId || undefined,
|
||||
});
|
||||
addLog('Reply', res);
|
||||
toast('success', 'Reply posted');
|
||||
addLog('回复', res);
|
||||
toast('success', '回复已发布');
|
||||
setReplyText('');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed';
|
||||
addLog('Reply', { error: msg });
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog('回复', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@@ -123,11 +123,11 @@ export function InteractionsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<h1 className="text-2xl font-bold">Interactions</h1>
|
||||
<h1 className="text-2xl font-bold">互动</h1>
|
||||
|
||||
{/* Target */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Target Note</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">目标笔记</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Feed ID" value={feedId} onChange={(e) => setFeedId(e.target.value)} placeholder="Feed ID" />
|
||||
<Input label="xsec_token" value={xsecToken} onChange={(e) => setXsecToken(e.target.value)} placeholder="xsec_token" />
|
||||
@@ -136,45 +136,45 @@ export function InteractionsPage() {
|
||||
|
||||
{/* Like / Favorite */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">快捷操作</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button onClick={() => void handleLike(false)} loading={loading === 'like'} size="sm">
|
||||
Like
|
||||
点赞
|
||||
</Button>
|
||||
<Button onClick={() => void handleLike(true)} loading={loading === 'unlike'} variant="secondary" size="sm">
|
||||
Unlike
|
||||
取消点赞
|
||||
</Button>
|
||||
<Button onClick={() => void handleFavorite(false)} loading={loading === 'favorite'} size="sm">
|
||||
Favorite
|
||||
收藏
|
||||
</Button>
|
||||
<Button onClick={() => void handleFavorite(true)} loading={loading === 'unfavorite'} variant="secondary" size="sm">
|
||||
Unfavorite
|
||||
取消收藏
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Comment */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Post Comment</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">发表评论</h2>
|
||||
<div className="space-y-3">
|
||||
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="Write a comment..." />
|
||||
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="写评论..." />
|
||||
<Button onClick={() => void handleComment()} loading={loading === 'comment'} size="sm">
|
||||
Post Comment
|
||||
发表评论
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Reply */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Reply to Comment</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">回复评论</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Comment ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="Optional" />
|
||||
<Input label="User ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="Optional" />
|
||||
<Input label="评论 ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="可选" />
|
||||
<Input label="用户 ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="可选" />
|
||||
</div>
|
||||
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write a reply..." />
|
||||
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="写回复..." />
|
||||
<Button onClick={() => void handleReply()} loading={loading === 'reply'} size="sm">
|
||||
Post Reply
|
||||
发送回复
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -183,8 +183,8 @@ export function InteractionsPage() {
|
||||
{log.length > 0 && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Action Log</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => setLog([])}>Clear</Button>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">操作日志</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => setLog([])}>清空</Button>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{log.map((entry) => (
|
||||
|
||||
+25
-25
@@ -48,7 +48,7 @@ export function LoginPage() {
|
||||
const res = await getLoginQRCode();
|
||||
if (res.success && res.data) {
|
||||
if (res.data.alreadyLoggedIn) {
|
||||
toast('info', 'Already logged in!');
|
||||
toast('info', '已经登录!');
|
||||
void refreshStatus();
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function LoginPage() {
|
||||
if (cookieRes.success && cookieRes.data?.hasCookies) {
|
||||
stopPolling();
|
||||
setQrData(null);
|
||||
toast('success', 'Login successful!');
|
||||
toast('success', '登录成功!');
|
||||
void refreshStatus();
|
||||
}
|
||||
} catch {
|
||||
@@ -74,17 +74,17 @@ export function LoginPage() {
|
||||
if (prev <= 1) {
|
||||
stopPolling();
|
||||
setQrData(null);
|
||||
toast('warning', 'QR code expired');
|
||||
toast('warning', '二维码已过期');
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Failed to get QR code');
|
||||
toast('error', res.error?.message || '获取二维码失败');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('error', err instanceof Error ? err.message : 'Failed to get QR code');
|
||||
toast('error', err instanceof Error ? err.message : '获取二维码失败');
|
||||
} finally {
|
||||
setQrLoading(false);
|
||||
}
|
||||
@@ -95,13 +95,13 @@ export function LoginPage() {
|
||||
try {
|
||||
const res = await deleteCookies();
|
||||
if (res.success) {
|
||||
toast('success', 'Logged out successfully');
|
||||
toast('success', '已成功登出');
|
||||
void refreshStatus();
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Failed to logout');
|
||||
toast('error', res.error?.message || '登出失败');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('error', err instanceof Error ? err.message : 'Failed to logout');
|
||||
toast('error', err instanceof Error ? err.message : '登出失败');
|
||||
} finally {
|
||||
setLogoutLoading(false);
|
||||
}
|
||||
@@ -119,16 +119,16 @@ export function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<h1 className="text-2xl font-bold">Xiaohongshu Login</h1>
|
||||
<h1 className="text-2xl font-bold">小红书登录</h1>
|
||||
|
||||
{!token && (
|
||||
<Card className="border-dark-warning/30 bg-dark-warning/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-dark-warning">
|
||||
Bearer Token 未配置,API 请求将返回 401。请先在 Settings 中配置 Token。
|
||||
Bearer Token 未配置,API 请求将返回 401。请先在设置中配置 Token。
|
||||
</p>
|
||||
<Button size="sm" variant="secondary" onClick={() => navigate('/settings')}>
|
||||
前往 Settings
|
||||
前往设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -139,28 +139,28 @@ export function LoginPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-2">
|
||||
Current Status
|
||||
当前状态
|
||||
</h2>
|
||||
{statusLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : status ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={status.loggedIn ? 'success' : 'warning'}>
|
||||
{status.loggedIn ? 'Logged In' : 'Not Logged In'}
|
||||
{status.loggedIn ? '已登录' : '未登录'}
|
||||
</Badge>
|
||||
{status.username && <span className="text-sm">{status.username}</span>}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-dark-muted">Click Refresh to check login status</span>
|
||||
<span className="text-sm text-dark-muted">点击刷新检查登录状态</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => void refreshStatus()}>
|
||||
Refresh
|
||||
刷新
|
||||
</Button>
|
||||
{status?.loggedIn && (
|
||||
<Button variant="danger" size="sm" onClick={() => void handleLogout()} loading={logoutLoading}>
|
||||
Logout
|
||||
登出
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -170,14 +170,14 @@ export function LoginPage() {
|
||||
{/* QR Code Login */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">
|
||||
QR Code Login
|
||||
二维码登录
|
||||
</h2>
|
||||
|
||||
{!qrData && !qrLoading && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-dark-muted mb-4">Click the button to generate a QR code for login</p>
|
||||
<p className="text-dark-muted mb-4">点击按钮生成登录二维码</p>
|
||||
<Button onClick={() => void handleGetQR()} disabled={status?.loggedIn}>
|
||||
Get QR Code
|
||||
获取二维码
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -185,30 +185,30 @@ export function LoginPage() {
|
||||
{qrLoading && (
|
||||
<div className="flex flex-col items-center py-8 gap-3">
|
||||
<Spinner size="lg" />
|
||||
<p className="text-sm text-dark-muted">Generating QR code...</p>
|
||||
<p className="text-sm text-dark-muted">生成二维码中...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qrData && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="bg-white rounded-xl p-4">
|
||||
<img src={qrData} alt="Login QR Code" className="w-64 h-64" />
|
||||
<img src={qrData} alt="登录二维码" className="w-64 h-64" />
|
||||
</div>
|
||||
<p className="text-sm text-dark-muted">Scan with Xiaohongshu app to login</p>
|
||||
<p className="text-sm text-dark-muted">使用小红书 App 扫码登录</p>
|
||||
{polling && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm text-dark-accent">
|
||||
Waiting for scan... {formatCountdown(countdown)}
|
||||
等待扫码... {formatCountdown(countdown)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => void handleGetQR()}>
|
||||
Refresh QR
|
||||
刷新二维码
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={stopPolling}>
|
||||
Cancel
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function PublishPage() {
|
||||
|
||||
const handlePublishImage = useCallback(async () => {
|
||||
if (!imgTitle.trim() || !imgPaths.trim()) {
|
||||
toast('warning', 'Title and images are required');
|
||||
toast('warning', '标题和图片为必填项');
|
||||
return;
|
||||
}
|
||||
setImgLoading(true);
|
||||
@@ -52,12 +52,12 @@ export function PublishPage() {
|
||||
});
|
||||
setImgResult(res);
|
||||
if (res.success) {
|
||||
toast('success', 'Image note published!');
|
||||
toast('success', '图文笔记发布成功!');
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Publish failed');
|
||||
toast('error', res.error?.message || '发布失败');
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Publish failed';
|
||||
const msg = err instanceof Error ? err.message : '发布失败';
|
||||
toast('error', msg);
|
||||
setImgResult({ error: msg });
|
||||
} finally {
|
||||
@@ -67,7 +67,7 @@ export function PublishPage() {
|
||||
|
||||
const handlePublishVideo = useCallback(async () => {
|
||||
if (!vidTitle.trim() || !vidPath.trim()) {
|
||||
toast('warning', 'Title and video path are required');
|
||||
toast('warning', '标题和视频路径为必填项');
|
||||
return;
|
||||
}
|
||||
setVidLoading(true);
|
||||
@@ -83,12 +83,12 @@ export function PublishPage() {
|
||||
});
|
||||
setVidResult(res);
|
||||
if (res.success) {
|
||||
toast('success', 'Video note published!');
|
||||
toast('success', '视频笔记发布成功!');
|
||||
} else {
|
||||
toast('error', res.error?.message || 'Publish failed');
|
||||
toast('error', res.error?.message || '发布失败');
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Publish failed';
|
||||
const msg = err instanceof Error ? err.message : '发布失败';
|
||||
toast('error', msg);
|
||||
setVidResult({ error: msg });
|
||||
} finally {
|
||||
@@ -98,12 +98,12 @@ export function PublishPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-4">
|
||||
<h1 className="text-2xl font-bold">Publish Note</h1>
|
||||
<h1 className="text-2xl font-bold">发布笔记</h1>
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ key: 'image', label: 'Image Note' },
|
||||
{ key: 'video', label: 'Video Note' },
|
||||
{ key: 'image', label: '图文笔记' },
|
||||
{ key: 'video', label: '视频笔记' },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={setTab}
|
||||
@@ -112,28 +112,28 @@ export function PublishPage() {
|
||||
{tab === 'image' && (
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<Input label="Title" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="Note title" />
|
||||
<Textarea label="Content" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="Note body text" />
|
||||
<Textarea label="Image Paths (one per line)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg /path/to/image2.jpg" />
|
||||
<Input label="Tags (comma separated)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="travel, food" />
|
||||
<Input label="标题" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="笔记标题" />
|
||||
<Textarea label="正文" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="笔记正文" />
|
||||
<Textarea label="图片路径(每行一个)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg /path/to/image2.jpg" />
|
||||
<Input label="标签(逗号分隔)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="旅行, 美食" />
|
||||
<div className="flex gap-4 items-end">
|
||||
<Select
|
||||
label="Visibility"
|
||||
label="可见性"
|
||||
options={[
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'private', label: 'Private' },
|
||||
{ value: 'friends', label: 'Friends' },
|
||||
{ value: 'public', label: '公开' },
|
||||
{ value: 'private', label: '私密' },
|
||||
{ value: 'friends', label: '仅好友' },
|
||||
]}
|
||||
value={imgVisibility}
|
||||
onChange={(e) => setImgVisibility(e.target.value)}
|
||||
/>
|
||||
<label className="flex items-center gap-2 pb-2 cursor-pointer">
|
||||
<input type="checkbox" checked={imgOriginal} onChange={(e) => setImgOriginal(e.target.checked)} className="rounded" />
|
||||
<span className="text-sm text-dark-muted">Original content</span>
|
||||
<span className="text-sm text-dark-muted">原创声明</span>
|
||||
</label>
|
||||
</div>
|
||||
<Button onClick={() => void handlePublishImage()} loading={imgLoading}>
|
||||
Publish Image Note
|
||||
发布图文笔记
|
||||
</Button>
|
||||
{imgResult !== null && <JsonViewer data={imgResult} />}
|
||||
</div>
|
||||
@@ -143,22 +143,22 @@ export function PublishPage() {
|
||||
{tab === 'video' && (
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<Input label="Title" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="Note title" />
|
||||
<Textarea label="Content" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="Note body text" />
|
||||
<Input label="Video Path" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" />
|
||||
<Input label="Tags (comma separated)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="travel, vlog" />
|
||||
<Input label="标题" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="笔记标题" />
|
||||
<Textarea label="正文" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="笔记正文" />
|
||||
<Input label="视频路径" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" />
|
||||
<Input label="标签(逗号分隔)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="旅行, vlog" />
|
||||
<Select
|
||||
label="Visibility"
|
||||
label="可见性"
|
||||
options={[
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'private', label: 'Private' },
|
||||
{ value: 'friends', label: 'Friends' },
|
||||
{ value: 'public', label: '公开' },
|
||||
{ value: 'private', label: '私密' },
|
||||
{ value: 'friends', label: '仅好友' },
|
||||
]}
|
||||
value={vidVisibility}
|
||||
onChange={(e) => setVidVisibility(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => void handlePublishVideo()} loading={vidLoading}>
|
||||
Publish Video Note
|
||||
发布视频笔记
|
||||
</Button>
|
||||
{vidResult !== null && <JsonViewer data={vidResult} />}
|
||||
</div>
|
||||
|
||||
@@ -10,49 +10,49 @@ export function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<h1 className="text-2xl font-bold">Settings</h1>
|
||||
<h1 className="text-2xl font-bold">设置</h1>
|
||||
|
||||
{/* Server Connection */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Server Connection</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">服务器连接</h2>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Server URL"
|
||||
label="服务器地址"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
placeholder="Leave empty for same-origin (default)"
|
||||
placeholder="留空则使用同源地址(默认)"
|
||||
/>
|
||||
<p className="text-xs text-dark-muted">
|
||||
Leave empty when the dashboard is served by the same Express server.
|
||||
Set to e.g. <code className="text-dark-accent">http://192.168.1.100:3000</code> for remote servers.
|
||||
当 Dashboard 由同一个 Express 服务器提供时留空。设置为例如{' '}
|
||||
<code className="text-dark-accent">http://192.168.1.100:3000</code> 用于远程服务器。
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Authentication */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Authentication</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">认证</h2>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Bearer Token"
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="Enter your API token"
|
||||
placeholder="输入 API Token"
|
||||
/>
|
||||
<p className="text-xs text-dark-muted">
|
||||
Token from <code className="text-dark-accent">BEARER_TOKEN</code> environment variable or{' '}
|
||||
<code className="text-dark-accent">.social-mcp/bearer-token</code> file.
|
||||
Token 来自环境变量 <code className="text-dark-accent">BEARER_TOKEN</code> 或文件{' '}
|
||||
<code className="text-dark-accent">.social-mcp/bearer-token</code>。
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
toast('success', 'Settings saved');
|
||||
toast('success', '设置已保存');
|
||||
}}
|
||||
>
|
||||
Save
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -60,10 +60,10 @@ export function SettingsPage() {
|
||||
onClick={() => {
|
||||
setToken('');
|
||||
setServerUrl('');
|
||||
toast('info', 'Settings cleared');
|
||||
toast('info', '设置已清除');
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
全部清除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,11 +71,11 @@ export function SettingsPage() {
|
||||
|
||||
{/* About */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">About</h2>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">关于</h2>
|
||||
<div className="space-y-2 text-sm text-dark-muted">
|
||||
<p><span className="text-dark-text">Social MCP</span> — Multi-platform social media automation</p>
|
||||
<p>Version: 0.1.0</p>
|
||||
<p>Stack: React 19 + TypeScript + Tailwind CSS</p>
|
||||
<p><span className="text-dark-text">Social MCP</span> — 多平台社交媒体自动化</p>
|
||||
<p>版本:0.1.0</p>
|
||||
<p>技术栈:React 19 + TypeScript + Tailwind CSS</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user