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: '暂无自有账号。' }}
/>