feat(agents): 完善恢复费用与云资源交互

This commit is contained in:
brother7 committed 2026-09-10 22:29:24 +08:00
1 parent f8f39f39de
commit 303f26294e
24 files changed
+1170 -127

No files matched your search

+35 -17
View File
@@ -4,12 +4,16 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { usePendingCloudInput } from './CloudPending';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import type { CloudAccess as Access, CloudApplication, CloudCost, CloudKey } from '../../../shared/cloud-agents';
import type { CloudAccess as Access, CloudApplication, CloudKey } from '../../../shared/cloud-agents';
import { CloudBudgetEditor, CloudCosts } from './CloudCosts';
import { CloudLifecycle } from './CloudLifecycle';
import type { CloudAgentDraft } from '../../../shared/cloud-agents';
import type { CloudAgentOperations } from '../../../shared/cloud-agents';
import { cloudStatus } from './CloudChat';
const errorText = (e: unknown) => e instanceof Error ? e.message : '操作失败,请重试';
export function CloudAccessPanel({ slug, revision, onPublished }: { slug: string; revision: number; onPublished: () => void }) {
export function CloudAccessPanel({ slug, revision, onPublished, onChanged }: { slug: string; revision: number; onPublished: () => void; onChanged?: (draft: CloudAgentDraft) => void }) {
const [access, setAccess] = useState<Access | null>(null);
const [costs, setCosts] = useState<CloudCost[]>([]);
const [query, setQuery] = useState('');
const [users, setUsers] = useState<{ account_id: string; display_name: string; username: string }[]>([]);
const [name, setName] = useState('');
@@ -21,12 +25,9 @@ export function CloudAccessPanel({ slug, revision, onPublished }: { slug: string
const applicationOperation = useRef<{ operation_id: string; name: string } | null>(null);
const alive = useRef(true);
const refresh = useCallback(async () => {
const results = await Promise.allSettled([cloudAgentsApi.call('access', { slug }), cloudAgentsApi.call('costs', { slug })]);
const result = await cloudAgentsApi.call('access', { slug });
if (!alive.current) return;
if (results[0].status === 'fulfilled') setAccess(results[0].value);
if (results[1].status === 'fulfilled') setCosts(results[1].value.items);
const failure = results.find(result => result.status === 'rejected');
if (failure?.status === 'rejected') throw failure.reason;
setAccess(result);
}, [slug]);
useEffect(() => {
alive.current = true;
@@ -98,15 +99,9 @@ export function CloudAccessPanel({ slug, revision, onPublished }: { slug: string
<p className="mt-3 text-xs leading-6 text-muted-foreground">相同请求标识与输入可重试。通过返回的 request_id 查询排队状态,通过 run_id 读取运行结果或订阅事件;这些请求继续使用同一应用凭据。</p>
</details>}
</section>
<section className="space-y-4"><h2 className="font-medium">最近费用</h2>
<p className="text-sm text-muted-foreground">最近 100 次模型调用,仅显示费用归属。调用者的对话与文件不会在这里展示。</p>
{!costs.length ? <p className="text-sm text-muted-foreground">还没有模型调用费用</p> : <div className="overflow-x-auto"><table className="w-full text-left text-sm tabular-nums">
<thead><tr className="border-b border-border"><th className="p-3 font-medium">时间</th><th className="p-3 font-medium">调用来源</th><th className="p-3 font-medium">状态</th><th className="p-3 font-medium">实际点数</th></tr></thead>
<tbody>{costs.map(cost => <tr key={cost.id} className="border-b border-border/50"><td className="p-3">{new Date(cost.created_at).toLocaleString()}</td>
<td className="p-3"><p>{cost.context.caller_kind === 'application' ? '应用' : '个人账号'} · v{cost.context.version}</p><p className="text-xs text-muted-foreground">{cost.context.caller_id}</p></td>
<td className="p-3">{({ reserved: '已预占', dispatched: '待结算', pending_review: '待核对用量', settled: '已结算', released: '已释放', failed: '失败' } as Record<string, string>)[cost.status] ?? cost.status}</td><td className="p-3">{cost.actual_points === null ? '待结算' : cost.actual_points}</td></tr>)}</tbody>
</table></div>}
</section>
<CloudBudgetEditor slug={slug} />
<CloudCosts slug={slug} applications={access?.applications ?? []} />
<CloudLifecycle slug={slug} revision={revision} versions={access?.versions.map(v => v.version) ?? []} onChanged={draft => { onChanged?.(draft); void refresh().catch(e => setError(errorText(e))); }} />
{access && <details className="text-sm"><summary className="cursor-pointer">发布历史({access.versions.length})</summary>
{access.versions.map(v => <p key={v.version} className="mt-3 tabular-nums">版本 {v.version} · 草稿修订 {v.draft_revision} · {new Date(v.created_at).toLocaleString()}</p>)}</details>}
</div>;
@@ -140,5 +135,28 @@ function ApplicationKeys({ application, onUpdated }: { application: CloudApplica
<div className="flex gap-2"><Button variant="outline" onClick={() => void navigator.clipboard.writeText(secret).catch(() => setError('复制失败,请手动选择复制'))}>复制凭据</Button>
<Button variant="ghost" onClick={() => setSecret('')}>已保存,隐藏</Button></div></div>}
{error && <p role="alert" className="text-destructive">{error}</p>}
<ApplicationCalls applicationId={application.id} />
</div>;
}
function ApplicationCalls({ applicationId }: { applicationId: string }) {
const [page, setPage] = useState<CloudAgentOperations['applicationCalls']['output'] | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const read = async (offset = 0) => {
setBusy(true); setError('');
try { const result = await cloudAgentsApi.call('applicationCalls', { application_id: applicationId, offset });
setPage(previous => ({ ...result, items: offset && previous ? [...previous.items, ...result.items] : result.items })); }
catch (e) { setError(errorText(e)); } finally { setBusy(false); }
};
return <details onToggle={e => { if (e.currentTarget.open && !page) void read(); }}>
<summary className="min-h-8 cursor-pointer">应用调用状态</summary>
<p className="mt-2 text-xs text-muted-foreground">包括尚未产生模型费用的请求。只显示状态与版本,不展示应用的会话内容。</p>
{error && <p role="alert" className="mt-2 text-destructive">{error}</p>}
{page && !page.items.length && <p className="mt-3 text-muted-foreground">此应用还没有调用记录。</p>}
{page?.items.map(item => <div key={item.request_id} className="mt-3 border-t pt-2 text-xs"><p>{new Date(item.created_at).toLocaleString()} · v{item.version} · {cloudStatus(item.run_status ?? item.request_status)}</p>
<p className="mt-1 break-all text-muted-foreground">请求 {item.request_id}{item.run_id && ' · 运行 ' + item.run_id}</p></div>)}
<div className="mt-3 flex gap-2"><Button variant="ghost" size="sm" disabled={busy} onClick={() => void read()}>刷新调用状态</Button>
{page?.next_offset != null && <Button variant="outline" size="sm" disabled={busy} onClick={() => void read(page.next_offset!)}>加载更多调用</Button>}</div>
</details>;
}