feat: improve agent management and speed up tests

This commit is contained in:
justme committed 2026-10-06 15:48:46 +08:00
1 parent 134c06d605
commit 193fc3a95c
28 files changed
+419 -57

No files matched your search

+111 -1
View File
@@ -1,5 +1,5 @@
import { Check, Copy, Moon, Plug, Sun } from 'lucide-react'
import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
import { Component, createContext, useCallback, useContext, useEffect, useRef, useState, type ErrorInfo, type ReactNode } from 'react'
import { ApiError } from '../api/client'
import { getBase, getToken, normalizeBase, setSource } from '../api/source'
import { useTheme } from '../hooks'
@@ -230,6 +230,116 @@ export function LoadingBar({ active }: { active: boolean }) {
) : null
}
export interface CommandTab {
id: string
label: string
command: string
}
/** 按浏览器所在的系统挑一个默认的标签(Windows / macOS / 其他当 Linux)。 */
export function guessPlatform(): 'windows' | 'macos' | 'linux' {
const text = `${navigator.platform} ${navigator.userAgent}`.toLowerCase()
return text.includes('win') ? 'windows' : text.includes('mac') ? 'macos' : 'linux'
}
/**
* 带标签切换的命令框(curl / npm / bun 那样):上面一排下划线标签,下面是等宽的命令,右侧一个复制图标。
* 记住上次选的标签(只是浏览器里的小方便)。
*/
export function CommandTabs({ tabs, initial, storageKey }: { tabs: CommandTab[]; initial?: string; storageKey?: string }) {
const [active, setActive] = useState(() => {
let saved: string | null = null
try {
saved = storageKey ? localStorage.getItem(storageKey) : null
} catch {
// 浏览器禁用存储时只是不记住
}
return [saved, initial].find((id) => id && tabs.some((t) => t.id === id)) ?? tabs[0]?.id ?? ''
})
const [done, setDone] = useState(false)
const tab = tabs.find((t) => t.id === active) ?? tabs[0]
if (!tab) return null
const pick = (id: string) => {
setActive(id)
try {
if (storageKey) localStorage.setItem(storageKey, id)
} catch {
// 同上
}
}
return (
<div className="cmd-tabs">
<div className="cmd-tablist" role="tablist">
{tabs.map((t) => (
<button key={t.id} type="button" role="tab" aria-selected={t.id === tab.id} className={t.id === tab.id ? 'on' : ''} onClick={() => pick(t.id)}>
{t.label}
</button>
))}
</div>
<div className="cmd-body">
<code>{tab.command}</code>
<button
type="button"
className="cmd-copy"
aria-label="复制命令"
title="复制命令"
onClick={async () => {
try {
await navigator.clipboard.writeText(tab.command)
setDone(true)
window.setTimeout(() => setDone(false), 1500)
} catch {
// 没有剪贴板权限时用户可以手动选中
}
}}
>
{done ? <Check size={16} /> : <Copy size={16} />}
</button>
</div>
</div>
)
}
/**
* 页面出错时显示提示,而不是整页白屏:说明出了什么错,可以重试(重新渲染这一块)或刷新整个页面。
* 常见原因是前端和后端版本对不上(后端还没升级到新接口)。
*/
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
state: { error: Error | null } = { error: null }
static getDerivedStateFromError(error: Error) {
return { error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(error, info.componentStack)
}
render() {
const { error } = this.state
if (!error) return this.props.children
return (
<div className="login">
<div className="card stack" style={{ maxWidth: 520 }}>
<div>
<h1>页面出错了</h1>
<p className="muted">这一页没能显示出来。如果刚升级过,多半是前端和后端的版本对不上:先把后端升级到对应的版本,再刷新。</p>
</div>
<pre className="code">{error.message}</pre>
<div className="row" style={{ gap: 8 }}>
<button type="button" className="btn primary" onClick={() => this.setState({ error: null })}>
重试
</button>
<button type="button" className="btn" onClick={() => location.reload()}>
刷新页面
</button>
</div>
</div>
</div>
)
}
}
/** 数据源设置:默认连同源的后端;填了地址就连别的后端(跨域用 API 令牌,后端要放行这个前端的来源)。 */
export function SourceButton({ tokenLabel = '只读令牌(站点私有时才需要)' }: { tokenLabel?: string }) {
const [open, setOpen] = useState(false)