diff --git a/internal/controlplane/api/accounts_operations.go b/internal/controlplane/api/accounts_operations.go index c572ab2..5fc62b1 100644 --- a/internal/controlplane/api/accounts_operations.go +++ b/internal/controlplane/api/accounts_operations.go @@ -71,13 +71,7 @@ func RegisterAccountRoutes(app *fiber.App, store *accountdomain.Store, runtimeSt return c.JSON(accounts) }) - app.Get("/api/phase-a/accounts/:id", func(c fiber.Ctx) error { - account, err := store.GetAccount(c.Context(), c.Params("id")) - if err != nil { - return phaseAError(c, err) - } - return c.JSON(account) - }) + app.Get("/api/phase-a/accounts/:id", accountDetailPayload(store, runtimeStore)) app.Post("/api/phase-a/accounts/:id/pause", func(c fiber.Ctx) error { unlock, err := lockAccountResources(c.Context(), runtimeStore, c.Params("id")) @@ -290,3 +284,37 @@ func phaseAError(c fiber.Ctx, err error) error { func PhaseAError(c fiber.Ctx, err error) error { return phaseAError(c, err) } + +// accountEnvironmentView 账号详情/出口重绑共用:账号即环境下内联展示的环境视图(schedule_block_reason 取值同 envView)。 +func accountEnvironmentView(environment hub.EnvironmentContext) map[string]any { + scheduleStatus, scheduleBlockReason := environmentScheduleReadiness(environment) + return map[string]any{ + "alias": environment.Alias, "name": environment.Name, "gateway": environment.Gateway, + "binding_version": environment.BindingVersion, "runtime_id": environment.RuntimeID, + "runtime_cleanup_pending": environment.RuntimeCleanupPending, + "network_exit_id": environment.Exit.ID, "network_exit_health": environment.Exit.HealthStatus, + "schedule_status": scheduleStatus, "schedule_block_reason": scheduleBlockReason, + } +} + +// accountDetailPayload 账号详情:环境信息内联在响应中(账号即环境),前端无需单独请求 /browsers。 +func accountDetailPayload(store *accountdomain.Store, runtimeStore HubStore) fiber.Handler { + return func(c fiber.Ctx) error { + account, err := store.GetAccount(c.Context(), c.Params("id")) + if err != nil { + return phaseAError(c, err) + } + payload := map[string]any{ + "id": account.ID, "name": account.Name, "platform": account.Platform, + "platform_account_key": account.PlatformAccountKey, "tags": account.Tags, + "authorization_status": account.AuthorizationStatus, "runtime_status": account.RuntimeStatus, + "version": account.Version, + } + if runtimeStore != nil { + if environment, envErr := runtimeStore.GetEnvironmentContextForAccount(c.Context(), account.ID); envErr == nil { + payload["environment"] = accountEnvironmentView(environment) + } + } + return c.JSON(payload) + } +} diff --git a/internal/controlplane/api/environments.go b/internal/controlplane/api/environments.go index 4304ff5..6f2bf21 100644 --- a/internal/controlplane/api/environments.go +++ b/internal/controlplane/api/environments.go @@ -1975,7 +1975,7 @@ func rebindBrowser(store HubStore, probe NetworkExitProbe, resolve func(hub.Netw if err := finish("succeeded", "environment_rebound", after); err != nil { return hubError(c, err) } - return c.JSON(after) + return c.JSON(map[string]any{"account_id": after.AccountID, "environment": accountEnvironmentView(after)}) } func deleteBrowser(store HubStore) fiber.Handler { diff --git a/web/src/components/accounts/RebindEnvironment.tsx b/web/src/components/accounts/RebindEnvironment.tsx new file mode 100644 index 0000000..cc95314 --- /dev/null +++ b/web/src/components/accounts/RebindEnvironment.tsx @@ -0,0 +1,79 @@ +// 账号环境出口重绑:账号即环境下唯一的换绑入口(环境跟随账号,不再有独立环境页)。 +// 仅列举检测通过的出口;重绑走 POST /network-exits/:id/rebind(后端联动账号环境),成功后由父级刷新。 +import { useEffect, useMemo, useState } from 'react'; +import { Button, Flex, Select, Typography } from 'antd'; +import { getList } from '@/services/api'; +import { jsonOptions, request } from '@/requestErrorConfig'; + +interface RebindEnvironmentProps { + binding: any; + disabled?: boolean; + onRebound: () => void | Promise; + onError: (error: unknown) => void; +} + +export default function RebindEnvironment({ binding, disabled, onRebound, onError }: RebindEnvironmentProps) { + const [exits, setExits] = useState([]); + const [exitsError, setExitsError] = useState(false); + const [exitID, setExitID] = useState(''); + const [busy, setBusy] = useState(false); + + useEffect(() => { + let cancelled = false; + getList({ resource: 'network-exits' }) + .then((result) => { + if (!cancelled) setExits((result.data ?? []).filter((item: any) => item.health_status === 'healthy')); + }) + .catch(() => { + if (!cancelled) setExitsError(true); + }); + return () => { + cancelled = true; + }; + }, []); + + // 直连(当前状态)或任一健康出口;已在用的出口不重复出现在选项里。 + const options = useMemo(() => { + const choices = [{ value: '', label: '当前机器直连' }]; + for (const exit of exits) { + if (exit.id === binding.network_exit_id) continue; + choices.push({ value: exit.id, label: `${exit.id}${exit.observed_public_ip ? ` · ${exit.observed_public_ip}` : ''}` }); + } + return choices; + }, [exits, binding.network_exit_id]); + + const unchanged = exitID === (binding.network_exit_id ?? ''); + + async function rebind() { + setBusy(true); + try { + await request(`/network-exits/${encodeURIComponent(binding.network_exit_id ?? '')}/rebind`, jsonOptions('POST', { network_exit_id: exitID })); + await onRebound(); + } catch (reason) { + onError(reason); + } finally { + setBusy(false); + } + } + + return ( + + + 切换出口 + - - - - - - - - - - ({ value: item.id, label: `${item.id} · ${item.platform}` }))} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ value: field, label: field }))} /> - - - - ) }, - ]} - /> - - - - - - - - ); -} diff --git a/web/src/pages/creator/settings/index.tsx b/web/src/pages/creator/settings/index.tsx index 0495380..30ea84f 100644 --- a/web/src/pages/creator/settings/index.tsx +++ b/web/src/pages/creator/settings/index.tsx @@ -24,9 +24,6 @@ const editableFields = [ 'ai_provider', 'ai_model', 'ai_configured', - 'transcription_provider', - 'transcription_model', - 'transcription_configured', ]; const editableSettings = (value: any) => Object.fromEntries(editableFields.map((field) => [field, value[field]])); @@ -128,22 +125,6 @@ export default function Page() { ) }, - { - key: 'transcription', - label: '转写服务', - children: ( -
- - - - - - - - 已完成转写配置审批 - -
- ) }, ]} />
diff --git a/web/src/pages/network-exits/index.tsx b/web/src/pages/network-exits/index.tsx index fcfbcce..2227a57 100644 --- a/web/src/pages/network-exits/index.tsx +++ b/web/src/pages/network-exits/index.tsx @@ -105,7 +105,7 @@ function ExitFormModal({ export default function Page() { const [exits, setExits] = useState([]); - const [browsers, setBrowsers] = useState([]); + const [accounts, setAccounts] = useState([]); const [bindingsError, setBindingsError] = useState(null); const [pending, setPending] = useState(true); const [error, setError] = useState(null); @@ -130,8 +130,8 @@ export default function Page() { const loadBindings = useCallback(async () => { try { - const result = await getList({ resource: 'browsers' }); - setBrowsers(result.data ?? []); + const result = await getList({ resource: 'accounts' }); + setAccounts(result.data ?? []); setBindingsError(null); } catch (loadError) { setBindingsError(loadError); @@ -145,12 +145,12 @@ export default function Page() { const accountsByExit = useMemo(() => { const map = new Map(); - for (const runtime of browsers) { - if (!runtime.network_exit_id || !runtime.account_id) continue; - map.set(runtime.network_exit_id, [...(map.get(runtime.network_exit_id) || []), runtime.account_id]); + for (const account of accounts) { + if (!account.network_exit_id) continue; + map.set(account.network_exit_id, [...(map.get(account.network_exit_id) || []), account.name || account.id]); } return map; - }, [browsers]); + }, [accounts]); async function createExit(values: ExitFormValues): Promise { setBusy('create'); diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 3434bd4..24242d4 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -3,7 +3,6 @@ import { AUTH_STORAGE_KEY, RequestError, jsonOptions, request, unauthorized } from '@/requestErrorConfig'; export const resourcePaths = { - browsers: '/browsers', gateways: '/gateways', accounts: '/phase-a/accounts', 'network-exits': '/network-exits', @@ -56,7 +55,7 @@ export async function getList({ resource, page, pageSize, filters = {} }; } -const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules']; +const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules']; export async function getOne(resource: Resource, id: string | number): Promise { if (!GETONE_ALLOWED.includes(resource)) throw new Error(`${resource} 不支持 getOne`); @@ -68,7 +67,6 @@ export async function create(resource: Resource, variables: unknown): Promise { - if (resource === 'browsers') throw new Error('browsers 不支持 update'); await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, jsonOptions('PUT', variables)); } @@ -79,17 +77,6 @@ export async function remove(resource: Resource, id: string | number): Promise { - const targets: Record = { - start: [`/browsers/${encodeURIComponent(alias)}/start`, 'POST'], - stop: [`/browsers/${encodeURIComponent(alias)}/stop`, 'POST'], - recycle: [`/browsers/${encodeURIComponent(alias)}`, 'DELETE'], - }; - const target = targets[action]; - if (!target) throw new Error(`未知运行环境操作: ${action}`); - await request(target[0], { method: target[1] }); -} - export async function accountAction(id: string, action: 'pause' | 'resume'): Promise { if (action !== 'pause' && action !== 'resume') throw new Error(`未知账号操作: ${action}`); await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: 'POST' }); diff --git a/web/src/utils/helpers.ts b/web/src/utils/helpers.ts index 72144b0..d62d943 100644 --- a/web/src/utils/helpers.ts +++ b/web/src/utils/helpers.ts @@ -77,14 +77,14 @@ export function accountReadiness(account: any, binding: any, bindingError: any = if (bindingError) return { label: '环境状态未知', reason: 'environment_unavailable', canResume: false, ready: false }; if (!binding) return { label: '未绑定运行环境', reason: 'binding_missing', canResume: false, ready: false }; let blocked = ''; - if (binding.cleanup_pending) { + if (binding.runtime_cleanup_pending) { blocked = 'runtime_stop_pending'; } else if (binding.network_exit_id && binding.network_exit_health !== 'healthy') { blocked = 'network_exit_unhealthy'; } if (blocked) return { label: readinessReasonText[blocked], reason: blocked, canResume: false, ready: false }; if (account.runtime_status === 'paused') { - const stopped = !binding.runtime_instance_id; + const stopped = !binding.runtime_id; return { label: stopped ? '资源就绪,可恢复' : '等待运行环境停止', reason: stopped ? '' : 'runtime_stop_pending', canResume: stopped, ready: false }; } const schedule = binding.schedule_block_reason || ''; diff --git a/web/src/utils/metadata.tsx b/web/src/utils/metadata.tsx index e5ebf9d..7d3384e 100644 --- a/web/src/utils/metadata.tsx +++ b/web/src/utils/metadata.tsx @@ -7,7 +7,6 @@ import { CommentOutlined, SettingOutlined, ProfileOutlined, - LaptopOutlined, ClusterOutlined, GatewayOutlined, DashboardOutlined, @@ -26,9 +25,6 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s { test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' }, { test: (p) => p === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' }, { test: (p) => p.startsWith('/creator/competitors/'), title: '竞品账号详情', subtitle: '查看竞品画像与作品列表,点击作品可追溯指标统计。' }, - { test: (p) => p === '/browsers', title: '运行环境', subtitle: '启动、停止并回收隔离的指纹浏览器运行环境。' }, - { test: (p) => p === '/browsers/new', title: '创建运行环境', subtitle: '为已授权且暂停的账号绑定网关、指纹与网络出口。' }, - { test: (p) => p.startsWith('/browsers/'), title: '运行环境详情', subtitle: '查看环境状态、账号绑定和运行实例信息。' }, { test: (p) => p === '/network-exits', title: '网络出口', subtitle: '维护固定出口、健康观测与账号绑定。' }, { test: (p) => p.startsWith('/network-exits/'), title: '网络出口详情', subtitle: '查看出口配置、健康状态和绑定关系。' }, { test: (p) => p === '/gateways', title: '网关管理', subtitle: '注册并检查宿主机 native browser gateway。' }, @@ -51,7 +47,6 @@ export const menu: MenuDataItem[] = [ { name: '资源', children: [ - { path: '/browsers', name: '运行环境', icon: }, { path: '/network-exits', name: '网络出口', icon: }, { path: '/gateways', name: '网关管理', icon: }, ], diff --git a/web/tests/table-fixed-header.test.cjs b/web/tests/table-fixed-header.test.cjs index 5139030..41500d9 100644 --- a/web/tests/table-fixed-header.test.cjs +++ b/web/tests/table-fixed-header.test.cjs @@ -8,7 +8,6 @@ const files = [ 'src/components/accounts/MonitoringAccountList.tsx', 'src/components/accounts/AccountManagementList.tsx', 'src/pages/accounts/import.tsx', - 'src/pages/browsers/index.tsx', 'src/pages/network-exits/index.tsx', 'src/pages/gateways/index.tsx', ];