diff --git a/internal/controlplane/api/app_migrated_test.go b/internal/controlplane/api/app_migrated_test.go index 62b1d54..919645c 100644 --- a/internal/controlplane/api/app_migrated_test.go +++ b/internal/controlplane/api/app_migrated_test.go @@ -288,7 +288,14 @@ func TestCreatorFixtureRoutesPostgres(t *testing.T) { t.Fatal("competitor preview without browser unexpectedly succeeded") } - // 自有账号监控:列表带作品聚合统计(fixture 已有 1 个 owned 作品)。 + // 账号即环境:给 fixture-account 绑定网关环境(fixture-small 保持未绑定),验证 monitor-views 内联 environment。 + if _, err := hubStore.CreateGateway(ctx, "fixture-gateway", "http://127.0.0.1:1", ""); err != nil { + t.Fatal(err) + } + if _, _, err := hubStore.CreateBoundEnv(ctx, hub.Env{Alias: "fixture-account", Name: "fixture-account-env", Gateway: "fixture-gateway"}, "fixture-account", ""); err != nil { + t.Fatal(err) + } + // 自有账号监控:列表带作品聚合统计(fixture 已有 1 个 owned 作品)+ 内联运行环境视图。 viewResponse := do(app, http.MethodGet, "/api/creator/accounts/monitor-views", "", "operator", "unit-test-password") if viewResponse.Code != http.StatusOK { t.Fatalf("GET monitor views: %d %s", viewResponse.Code, viewResponse.Body.String()) @@ -297,6 +304,12 @@ func TestCreatorFixtureRoutesPostgres(t *testing.T) { ID string `json:"id"` WorkCount int64 `json:"work_count"` LatestPublishedAt *string `json:"latest_published_at"` + Environment *struct { + Gateway string `json:"gateway"` + Name string `json:"name"` + RuntimeID string `json:"runtime_id"` + NetworkExitID string `json:"network_exit_id"` + } `json:"environment"` } if err := json.Unmarshal(viewResponse.Body.Bytes(), &monitorViews); err != nil { t.Fatal(err) @@ -304,8 +317,16 @@ func TestCreatorFixtureRoutesPostgres(t *testing.T) { viewByAccount := map[string]bool{} for _, view := range monitorViews { viewByAccount[view.ID] = true - if view.ID == "fixture-account" && (view.WorkCount < 1 || view.LatestPublishedAt == nil) { - t.Fatalf("monitor view aggregates missing for fixture-account: %+v", view) + if view.ID == "fixture-account" { + if view.WorkCount < 1 || view.LatestPublishedAt == nil { + t.Fatalf("monitor view aggregates missing for fixture-account: %+v", view) + } + if view.Environment == nil || view.Environment.Gateway != "fixture-gateway" || view.Environment.Name != "fixture-account-env" { + t.Fatalf("monitor view environment missing for fixture-account: %+v", view) + } + } + if view.ID == "fixture-small" && view.Environment != nil { + t.Fatalf("unbound account should not inline environment: %+v", view) } } if !viewByAccount["fixture-account"] { diff --git a/internal/controlplane/api/creator.go b/internal/controlplane/api/creator.go index 0d74231..84f2536 100644 --- a/internal/controlplane/api/creator.go +++ b/internal/controlplane/api/creator.go @@ -27,6 +27,12 @@ import ( "github.com/sirupsen/logrus" ) +// monitorViewPayload monitor-views 响应体:视图字段平铺 + 可选内联环境视图(账号即环境)。 +type monitorViewPayload struct { + creator.AccountMonitorView + Environment map[string]any `json:"environment,omitempty"` +} + func creatorPageQuery(c fiber.Ctx) (page, pageSize int, enabled bool, err error) { pageValue, pageSizeValue := strings.TrimSpace(c.Query("page")), strings.TrimSpace(c.Query("page_size")) if pageValue == "" && pageSizeValue == "" { @@ -128,13 +134,24 @@ func registerCreatorWithServices(app *fiber.App, store *creator.Store, phaseASto } return c.JSON(profiles) }) - // 自有账号监控:列表带作品聚合统计。 + // 自有账号监控:列表带作品聚合统计;账号即环境——内联运行环境视图(详情页同款),未绑定不带该字段。 app.Get("/api/creator/accounts/monitor-views", func(c fiber.Ctx) error { views, err := store.ListAccountMonitorViews(c.Context()) if err != nil { return creatorError(c, err) } - return c.JSON(views) + payloads := make([]monitorViewPayload, 0, len(views)) + for _, view := range views { + payload := monitorViewPayload{AccountMonitorView: view} + if hubStore != nil { + // 每账号一次环境查询:账号即环境一一对应,列表量级数十,无需批量接口。 + if environment, envErr := hubStore.GetEnvironmentContextForAccount(c.Context(), view.ID); envErr == nil { + payload.Environment = accountEnvironmentView(environment) + } + } + payloads = append(payloads, payload) + } + return c.JSON(payloads) }) // 自有账号采集状态:作品/评论两条 checkpoint + 下次采集窗口。 app.Get("/api/creator/accounts/:id/collection-status", func(c fiber.Ctx) error { diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index 1306eed..fb5e446 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -9,16 +9,27 @@ import { Alert, App, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { remove, creatorUpdate, creatorGet, creatorAction } from '@/services/api'; -import { conflictMessage, dateTime } from '@/utils/helpers'; +import { accountReadiness, conflictMessage, dateTime } from '@/utils/helpers'; import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table'; // 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。 +interface EnvironmentView { + gateway?: string; + name?: string; + alias?: string; + runtime_id?: string; + runtime_cleanup_pending?: boolean; + network_exit_id?: string; + network_exit_health?: string; +} + interface Row { id: string; name: string; platform_account_key: string; tags?: string[]; runtime_status?: string; + environment?: EnvironmentView; work_count?: number; latest_published_at?: string | null; follower_count?: number | null; @@ -207,14 +218,42 @@ export default function AccountManagementList() { title: '标签', dataIndex: 'tags', render: (tags?: string[]) => (tags?.length ? {tags.map((tag) => {tag})} : 未设置) }, + { + title: '运行环境', + render: (_, account) => { + const env = account.environment; + if (!env) return 未绑定运行环境; + return ( + + + {env.name || env.alias || '—'} + + {env.runtime_cleanup_pending ? '停止待确认' : env.runtime_id ? '运行中' : '未启动'} + + + + 网关 {env.gateway || '—'} + + + {env.network_exit_id + ? `出口 ${env.network_exit_id} · ${env.network_exit_health === 'healthy' ? '健康' : env.network_exit_health || '状态未知'}` + : '机器直连'} + + + ); + } }, { title: '状态', dataIndex: 'status', - render: (_, account) => ( - - {account.runtime_status === 'active' ? '启用' : '暂停'} - - ) }, + render: (_, account) => { + // 与详情页同款三层判定(绑定/出口/账号),环境未绑定时退化为账号启停状态。 + const readiness = accountReadiness(account, account.environment ?? null); + return ( + + {readiness.label} + + ); + } }, { title: '操作', key: 'actions', @@ -265,7 +304,7 @@ export default function AccountManagementList() { loading={pending} sticky pagination={tablePagination()} - scroll={tableScroll(horizontal ? 1200 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} + scroll={tableScroll(horizontal ? 1320 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: '暂无自有账号。' }} />