create-windy 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +258 -121
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
cacheStatusLabel,
|
|
4
|
+
formatDuration,
|
|
5
|
+
formatPercent,
|
|
6
|
+
formatTokenCount,
|
|
7
|
+
runStatusLabel,
|
|
8
|
+
} from "./ai-runtime-presenter";
|
|
9
|
+
|
|
10
|
+
describe("AI Runtime 展示语义", () => {
|
|
11
|
+
test("未知计量不伪装成零", () => {
|
|
12
|
+
expect(formatTokenCount(null)).toBe("未上报");
|
|
13
|
+
expect(formatPercent(null)).toBe("未上报");
|
|
14
|
+
expect(formatTokenCount(0)).toBe("0");
|
|
15
|
+
expect(formatPercent(0)).toBe("0.0%");
|
|
16
|
+
expect(cacheStatusLabel("unknown")).toBe("未上报");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("耗时使用便于运维阅读的单位", () => {
|
|
20
|
+
expect(formatDuration(420)).toBe("420 ms");
|
|
21
|
+
expect(formatDuration(1_250)).toBe("1.3 s");
|
|
22
|
+
expect(formatDuration(61_000)).toBe("1m 1s");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("Run 与 Attempt 状态不向用户暴露内部英文枚举", () => {
|
|
26
|
+
expect(
|
|
27
|
+
[
|
|
28
|
+
"accepted",
|
|
29
|
+
"running",
|
|
30
|
+
"awaiting-approval",
|
|
31
|
+
"completed",
|
|
32
|
+
"failed",
|
|
33
|
+
"cancelled",
|
|
34
|
+
"blocked",
|
|
35
|
+
"abandoned",
|
|
36
|
+
].map(runStatusLabel),
|
|
37
|
+
).toEqual([
|
|
38
|
+
"已接受",
|
|
39
|
+
"运行中",
|
|
40
|
+
"等待审批",
|
|
41
|
+
"成功",
|
|
42
|
+
"失败",
|
|
43
|
+
"已取消",
|
|
44
|
+
"已阻止",
|
|
45
|
+
"已中止",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -2,8 +2,7 @@ export function localMinuteToIso(
|
|
|
2
2
|
value: string,
|
|
3
3
|
boundary: "start" | "end",
|
|
4
4
|
): string | undefined {
|
|
5
|
-
const match =
|
|
6
|
-
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value.trim());
|
|
5
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value.trim());
|
|
7
6
|
if (!match) return undefined;
|
|
8
7
|
const [, year, month, day, hour, minute] = match;
|
|
9
8
|
const local = new Date(
|
|
@@ -26,3 +25,36 @@ export function localMinuteToIso(
|
|
|
26
25
|
}
|
|
27
26
|
return local.toISOString();
|
|
28
27
|
}
|
|
28
|
+
|
|
29
|
+
export interface DefaultAuditTimeRange {
|
|
30
|
+
occurredAfterLocal: string;
|
|
31
|
+
occurredBeforeLocal: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultAuditTimeRange(
|
|
35
|
+
now: Date = new Date(),
|
|
36
|
+
): DefaultAuditTimeRange {
|
|
37
|
+
return {
|
|
38
|
+
occurredAfterLocal: toLocalMinute(
|
|
39
|
+
new Date(now.getTime() - 60 * 60 * 1_000),
|
|
40
|
+
),
|
|
41
|
+
occurredBeforeLocal: toLocalMinute(now),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function defaultAuditFilters(now: Date = new Date()) {
|
|
46
|
+
const range = defaultAuditTimeRange(now);
|
|
47
|
+
return {
|
|
48
|
+
occurredAfter: localMinuteToIso(range.occurredAfterLocal, "start"),
|
|
49
|
+
occurredBefore: localMinuteToIso(range.occurredBeforeLocal, "end"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toLocalMinute(value: Date): string {
|
|
54
|
+
const year = value.getFullYear();
|
|
55
|
+
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
56
|
+
const day = String(value.getDate()).padStart(2, "0");
|
|
57
|
+
const hour = String(value.getHours()).padStart(2, "0");
|
|
58
|
+
const minute = String(value.getMinutes()).padStart(2, "0");
|
|
59
|
+
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
60
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@/components/ui/select";
|
|
15
15
|
import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
|
|
16
16
|
import { listSystemResource, type SystemRecord } from "@/services/system-api";
|
|
17
|
-
import { localMinuteToIso } from "../audit-time-range";
|
|
17
|
+
import { defaultAuditTimeRange, localMinuteToIso } from "../audit-time-range";
|
|
18
18
|
|
|
19
19
|
export interface AuditLogFilterValue {
|
|
20
20
|
q?: string;
|
|
@@ -32,8 +32,9 @@ const emit = defineEmits<{
|
|
|
32
32
|
const keyword = shallowRef("");
|
|
33
33
|
const actorId = shallowRef("all");
|
|
34
34
|
const action = shallowRef("all");
|
|
35
|
-
const
|
|
36
|
-
const
|
|
35
|
+
const initialTimeRange = defaultAuditTimeRange();
|
|
36
|
+
const occurredAfterLocal = shallowRef(initialTimeRange.occurredAfterLocal);
|
|
37
|
+
const occurredBeforeLocal = shallowRef(initialTimeRange.occurredBeforeLocal);
|
|
37
38
|
const optionSource = shallowRef<SystemRecord[]>([]);
|
|
38
39
|
const includeScheduler = shallowRef(false);
|
|
39
40
|
const access = useAccessSnapshot();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
-
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
4
4
|
import { Select } from "@/components/ui/select";
|
|
5
5
|
import AuditLogFilters from "./AuditLogFilters.vue";
|
|
@@ -12,7 +12,32 @@ vi.mock("@/services/system-api", async () => ({
|
|
|
12
12
|
listSystemResource: api.list,
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
afterEach(() => vi.useRealTimers());
|
|
16
|
+
|
|
15
17
|
describe("审计日志筛选", () => {
|
|
18
|
+
test("默认显示最近一小时到当前分钟", async () => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.setSystemTime(new Date(2026, 6, 20, 10, 20, 30));
|
|
21
|
+
api.list.mockResolvedValue({
|
|
22
|
+
items: [],
|
|
23
|
+
total: 0,
|
|
24
|
+
page: 1,
|
|
25
|
+
pageSize: 100,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const wrapper = mount(AuditLogFilters);
|
|
29
|
+
await flushPromises();
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
(wrapper.get('[aria-label="审计开始时间"]').element as HTMLInputElement)
|
|
33
|
+
.value,
|
|
34
|
+
).toBe("2026-07-20T09:20");
|
|
35
|
+
expect(
|
|
36
|
+
(wrapper.get('[aria-label="审计结束时间"]').element as HTMLInputElement)
|
|
37
|
+
.value,
|
|
38
|
+
).toBe("2026-07-20T10:20");
|
|
39
|
+
});
|
|
40
|
+
|
|
16
41
|
test("提交关键字、操作者、动作和本地分钟范围,并原样设置导出选项", async () => {
|
|
17
42
|
api.list.mockResolvedValue({
|
|
18
43
|
items: [
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { OperationSnapshot } from "@/services/operations-api";
|
|
3
|
+
|
|
4
|
+
defineProps<{ gpu: OperationSnapshot["gpuMonitor"] }>();
|
|
5
|
+
|
|
6
|
+
function formatBytes(value: number | null) {
|
|
7
|
+
if (value === null) return "未知";
|
|
8
|
+
if (value < 1_024 ** 2) return `${(value / 1_024).toFixed(1)} KiB`;
|
|
9
|
+
if (value < 1_024 ** 3) return `${(value / 1_024 ** 2).toFixed(1)} MiB`;
|
|
10
|
+
return `${(value / 1_024 ** 3).toFixed(1)} GiB`;
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<section class="space-y-3">
|
|
16
|
+
<div>
|
|
17
|
+
<h3 class="text-sm font-medium">GPU 占用</h3>
|
|
18
|
+
<p class="text-xs text-muted-foreground">{{ gpu.message }}</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
v-if="gpu.status !== 'available'"
|
|
23
|
+
class="rounded-md border border-dashed p-3 text-sm text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
{{ gpu.status === "not-detected" ? "未检测到 GPU" : "GPU 指标不可用" }}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<article
|
|
29
|
+
v-for="device in gpu.devices"
|
|
30
|
+
:key="device.uuid"
|
|
31
|
+
class="space-y-3 rounded-lg border p-4"
|
|
32
|
+
>
|
|
33
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
34
|
+
<p class="font-medium">GPU {{ device.index }} · {{ device.name }}</p>
|
|
35
|
+
<p class="text-xs text-muted-foreground">
|
|
36
|
+
{{
|
|
37
|
+
device.temperatureCelsius === null
|
|
38
|
+
? "温度未知"
|
|
39
|
+
: `${device.temperatureCelsius} °C`
|
|
40
|
+
}}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
v-for="metric in [
|
|
46
|
+
{ label: 'GPU 利用率', value: device.usagePercent },
|
|
47
|
+
{ label: '显存占用率', value: device.memoryUsagePercent },
|
|
48
|
+
]"
|
|
49
|
+
:key="metric.label"
|
|
50
|
+
class="space-y-2"
|
|
51
|
+
>
|
|
52
|
+
<div class="flex justify-between gap-4 text-sm">
|
|
53
|
+
<span>{{ metric.label }}</span>
|
|
54
|
+
<span class="text-muted-foreground">
|
|
55
|
+
{{ metric.value === null ? "未知" : `${metric.value}%` }}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
role="progressbar"
|
|
60
|
+
:aria-label="`GPU ${device.index} ${metric.label}`"
|
|
61
|
+
aria-valuemin="0"
|
|
62
|
+
aria-valuemax="100"
|
|
63
|
+
:aria-valuenow="metric.value ?? 0"
|
|
64
|
+
class="h-3 overflow-hidden rounded-full bg-muted"
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
class="h-full rounded-full bg-primary transition-[width] duration-500"
|
|
68
|
+
:style="{ width: `${metric.value ?? 0}%` }"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<p class="text-xs text-muted-foreground">
|
|
74
|
+
显存 {{ formatBytes(device.memoryUsedBytes) }} /
|
|
75
|
+
{{ formatBytes(device.memoryTotalBytes) }}
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<div v-if="device.processes.length" class="space-y-2">
|
|
79
|
+
<p class="text-xs font-medium">计算进程</p>
|
|
80
|
+
<ul class="divide-y rounded-md border text-sm">
|
|
81
|
+
<li
|
|
82
|
+
v-for="item in device.processes"
|
|
83
|
+
:key="`${device.uuid}-${item.pid}`"
|
|
84
|
+
class="flex flex-wrap justify-between gap-2 px-3 py-2"
|
|
85
|
+
>
|
|
86
|
+
<span class="min-w-0 truncate">{{ item.name }}</span>
|
|
87
|
+
<span class="text-muted-foreground">
|
|
88
|
+
PID {{ item.pid }} · {{ formatBytes(item.usedMemoryBytes) }}
|
|
89
|
+
</span>
|
|
90
|
+
</li>
|
|
91
|
+
</ul>
|
|
92
|
+
</div>
|
|
93
|
+
<p v-else class="text-xs text-muted-foreground">
|
|
94
|
+
当前没有可见的 NVIDIA 计算进程。
|
|
95
|
+
</p>
|
|
96
|
+
</article>
|
|
97
|
+
</section>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import OperationGpuMonitor from "./OperationGpuMonitor.vue";
|
|
4
|
+
|
|
5
|
+
describe("GPU 监控面板", () => {
|
|
6
|
+
test("展示设备利用率、显存和 AI 计算进程", () => {
|
|
7
|
+
const wrapper = mount(OperationGpuMonitor, {
|
|
8
|
+
props: {
|
|
9
|
+
gpu: {
|
|
10
|
+
status: "available",
|
|
11
|
+
provider: "NVIDIA",
|
|
12
|
+
message: "GPU 利用率为设备采样值。",
|
|
13
|
+
devices: [
|
|
14
|
+
{
|
|
15
|
+
index: 0,
|
|
16
|
+
uuid: "GPU-abc",
|
|
17
|
+
name: "NVIDIA RTX 5000",
|
|
18
|
+
usagePercent: 75,
|
|
19
|
+
memoryUsedBytes: 8 * 1_024 ** 3,
|
|
20
|
+
memoryTotalBytes: 32 * 1_024 ** 3,
|
|
21
|
+
memoryUsagePercent: 25,
|
|
22
|
+
temperatureCelsius: 62,
|
|
23
|
+
processes: [
|
|
24
|
+
{
|
|
25
|
+
pid: 4321,
|
|
26
|
+
name: "python ai-service.py",
|
|
27
|
+
usedMemoryBytes: 7 * 1_024 ** 3,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(wrapper.text()).toContain("GPU 0 · NVIDIA RTX 5000");
|
|
37
|
+
expect(wrapper.text()).toContain("python ai-service.py");
|
|
38
|
+
expect(wrapper.text()).toContain("PID 4321 · 7.0 GiB");
|
|
39
|
+
expect(
|
|
40
|
+
wrapper
|
|
41
|
+
.get('[aria-label="GPU 0 GPU 利用率"]')
|
|
42
|
+
.attributes("aria-valuenow"),
|
|
43
|
+
).toBe("75");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("无 NVIDIA 能力时显示真实不可探测状态", () => {
|
|
47
|
+
const wrapper = mount(OperationGpuMonitor, {
|
|
48
|
+
props: {
|
|
49
|
+
gpu: {
|
|
50
|
+
status: "not-detected",
|
|
51
|
+
provider: "NVIDIA",
|
|
52
|
+
devices: [],
|
|
53
|
+
message: "未检测到 nvidia-smi。",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(wrapper.text()).toContain("未检测到 GPU");
|
|
59
|
+
expect(wrapper.text()).toContain("未检测到 nvidia-smi");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
import type { OperationSnapshot } from "@/services/operations-api";
|
|
11
11
|
import { formatDateTime } from "@/lib/date-time";
|
|
12
12
|
import DiskUsageChart from "./DiskUsageChart.vue";
|
|
13
|
+
import OperationGpuMonitor from "./OperationGpuMonitor.vue";
|
|
13
14
|
|
|
14
15
|
defineProps<{
|
|
15
16
|
service: OperationSnapshot["serviceMonitor"];
|
|
16
17
|
database: OperationSnapshot["databaseMonitor"];
|
|
17
18
|
disk: OperationSnapshot["diskMonitor"];
|
|
19
|
+
gpu: OperationSnapshot["gpuMonitor"];
|
|
18
20
|
}>();
|
|
19
21
|
|
|
20
22
|
function formatBytes(value?: number) {
|
|
@@ -83,6 +85,57 @@ function formatBytes(value?: number) {
|
|
|
83
85
|
</div>
|
|
84
86
|
</section>
|
|
85
87
|
|
|
88
|
+
<section class="space-y-3">
|
|
89
|
+
<div>
|
|
90
|
+
<h3 class="text-sm font-medium">CPU 占用</h3>
|
|
91
|
+
<p class="text-xs text-muted-foreground">
|
|
92
|
+
{{ service.cpu.logicalCores }} 个逻辑核心
|
|
93
|
+
<template v-if="service.cpu.loadAverage1m !== null">
|
|
94
|
+
· 1 分钟负载 {{ service.cpu.loadAverage1m }}
|
|
95
|
+
</template>
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
v-for="metric in [
|
|
100
|
+
{
|
|
101
|
+
label: '宿主机 CPU',
|
|
102
|
+
value: service.cpu.hostUsagePercent,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: 'Bun Server 进程 CPU',
|
|
106
|
+
value: service.cpu.processUsagePercent,
|
|
107
|
+
},
|
|
108
|
+
]"
|
|
109
|
+
:key="metric.label"
|
|
110
|
+
class="space-y-2"
|
|
111
|
+
>
|
|
112
|
+
<div class="flex justify-between gap-4 text-sm">
|
|
113
|
+
<span class="font-medium">{{ metric.label }}</span>
|
|
114
|
+
<span class="text-muted-foreground">
|
|
115
|
+
{{ metric.value === null ? "采集中" : `${metric.value}%` }}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div
|
|
119
|
+
role="progressbar"
|
|
120
|
+
:aria-label="metric.label"
|
|
121
|
+
aria-valuemin="0"
|
|
122
|
+
aria-valuemax="100"
|
|
123
|
+
:aria-valuenow="metric.value ?? 0"
|
|
124
|
+
class="h-3 overflow-hidden rounded-full bg-muted"
|
|
125
|
+
>
|
|
126
|
+
<div
|
|
127
|
+
class="h-full rounded-full bg-primary transition-[width] duration-500"
|
|
128
|
+
:style="{ width: `${metric.value ?? 0}%` }"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<p v-if="service.cpu.message" class="text-xs text-muted-foreground">
|
|
133
|
+
{{ service.cpu.message }}
|
|
134
|
+
</p>
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<OperationGpuMonitor :gpu="gpu" />
|
|
138
|
+
|
|
86
139
|
<section class="space-y-3">
|
|
87
140
|
<div>
|
|
88
141
|
<h3 class="text-sm font-medium">磁盘占用</h3>
|
|
@@ -17,6 +17,19 @@ describe("服务监控面板", () => {
|
|
|
17
17
|
totalBytes: 10_000,
|
|
18
18
|
usagePercent: 20.48,
|
|
19
19
|
},
|
|
20
|
+
cpu: {
|
|
21
|
+
status: "available",
|
|
22
|
+
logicalCores: 16,
|
|
23
|
+
hostUsagePercent: 42.5,
|
|
24
|
+
processUsagePercent: 3.25,
|
|
25
|
+
loadAverage1m: 2.1,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
gpu: {
|
|
29
|
+
status: "not-detected",
|
|
30
|
+
provider: "NVIDIA",
|
|
31
|
+
devices: [],
|
|
32
|
+
message: "未检测到 nvidia-smi。",
|
|
20
33
|
},
|
|
21
34
|
database: {
|
|
22
35
|
status: "healthy",
|
|
@@ -46,7 +59,9 @@ describe("服务监控面板", () => {
|
|
|
46
59
|
},
|
|
47
60
|
},
|
|
48
61
|
},
|
|
49
|
-
global: {
|
|
62
|
+
global: {
|
|
63
|
+
stubs: { DiskUsageChart: true, OperationGpuMonitor: true },
|
|
64
|
+
},
|
|
50
65
|
});
|
|
51
66
|
|
|
52
67
|
expect(wrapper.text()).toContain("PostgreSQL 16.3");
|
|
@@ -60,5 +75,9 @@ describe("服务监控面板", () => {
|
|
|
60
75
|
expect(wrapper.text()).toContain("宿主机总容量 未知");
|
|
61
76
|
expect(wrapper.text()).toContain("容器未配置宿主机文件系统挂载");
|
|
62
77
|
expect(wrapper.text()).toContain("每 60 秒刷新");
|
|
78
|
+
expect(wrapper.text()).toContain("16 个逻辑核心");
|
|
79
|
+
expect(
|
|
80
|
+
wrapper.get('[aria-label="宿主机 CPU"]').attributes("aria-valuenow"),
|
|
81
|
+
).toBe("42.5");
|
|
63
82
|
});
|
|
64
83
|
});
|
|
@@ -32,7 +32,10 @@ vi.mock("@/services/auth-api", () => ({
|
|
|
32
32
|
loadPasswordPolicy: api.loadPasswordPolicy,
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
|
-
afterEach(() =>
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
36
39
|
|
|
37
40
|
describe("SystemResourcePage", () => {
|
|
38
41
|
api.loadPasswordPolicy.mockResolvedValue({
|
|
@@ -212,6 +215,8 @@ describe("SystemResourcePage", () => {
|
|
|
212
215
|
|
|
213
216
|
// @windy-module system.audit begin
|
|
214
217
|
test("审计页使用操作者、动作和时间筛选条件请求", async () => {
|
|
218
|
+
vi.useFakeTimers();
|
|
219
|
+
vi.setSystemTime(new Date(2026, 6, 20, 10, 20, 30));
|
|
215
220
|
api.list.mockResolvedValue({ items: [], total: 0, page: 1, pageSize: 10 });
|
|
216
221
|
const wrapper = mount(SystemResourcePage, {
|
|
217
222
|
props: { config: systemResourceConfigs.auditLogs },
|
|
@@ -227,6 +232,14 @@ describe("SystemResourcePage", () => {
|
|
|
227
232
|
});
|
|
228
233
|
await flushPromises();
|
|
229
234
|
|
|
235
|
+
expect(api.list).toHaveBeenCalledWith(
|
|
236
|
+
"audit-logs",
|
|
237
|
+
expect.objectContaining({
|
|
238
|
+
occurredAfter: new Date(2026, 6, 20, 9, 20, 0, 0).toISOString(),
|
|
239
|
+
occurredBefore: new Date(2026, 6, 20, 10, 20, 59, 999).toISOString(),
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
230
243
|
await wrapper.get('[data-testid="audit-filter"]').trigger("click");
|
|
231
244
|
await flushPromises();
|
|
232
245
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Chart } from "@antv/g2";
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
onMounted,
|
|
7
|
+
useTemplateRef,
|
|
8
|
+
watch,
|
|
9
|
+
} from "vue";
|
|
10
|
+
import type { AiRuntimeActivity } from "@/services/ai-runtime-api";
|
|
11
|
+
import { formatTokenCount } from "../../ai-runtime-presenter";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{ activity: readonly AiRuntimeActivity[] }>();
|
|
14
|
+
const container = useTemplateRef<HTMLDivElement>("container");
|
|
15
|
+
const hasData = computed(() => props.activity.some(({ runs }) => runs > 0));
|
|
16
|
+
let chart: Chart | undefined;
|
|
17
|
+
|
|
18
|
+
onMounted(renderChart);
|
|
19
|
+
watch(() => props.activity, renderChart);
|
|
20
|
+
onBeforeUnmount(destroyChart);
|
|
21
|
+
|
|
22
|
+
async function renderChart() {
|
|
23
|
+
destroyChart();
|
|
24
|
+
if (!container.value || !hasData.value) return;
|
|
25
|
+
chart = new Chart({ container: container.value, autoFit: true, height: 300 });
|
|
26
|
+
chart.options({
|
|
27
|
+
type: "view",
|
|
28
|
+
data: props.activity,
|
|
29
|
+
children: [
|
|
30
|
+
{
|
|
31
|
+
type: "interval",
|
|
32
|
+
encode: { x: "date", y: "runs" },
|
|
33
|
+
style: { fill: "#0f766e", fillOpacity: 0.2 },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "line",
|
|
37
|
+
encode: { x: "date", y: "runs" },
|
|
38
|
+
style: { stroke: "#0f766e", lineWidth: 2 },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "point",
|
|
42
|
+
encode: { x: "date", y: "runs" },
|
|
43
|
+
style: { fill: "#0f766e", r: 2.5 },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
axis: {
|
|
47
|
+
x: { title: false, labelAutoRotate: false },
|
|
48
|
+
y: { title: "Run" },
|
|
49
|
+
},
|
|
50
|
+
tooltip: {
|
|
51
|
+
items: [
|
|
52
|
+
{ field: "runs", name: "Run" },
|
|
53
|
+
{
|
|
54
|
+
field: "totalTokens",
|
|
55
|
+
name: "Token",
|
|
56
|
+
valueFormatter: formatTokenCount,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
animate: false,
|
|
61
|
+
});
|
|
62
|
+
await chart.render();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function destroyChart() {
|
|
66
|
+
chart?.destroy();
|
|
67
|
+
chart = undefined;
|
|
68
|
+
container.value?.replaceChildren();
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div
|
|
74
|
+
v-if="hasData"
|
|
75
|
+
ref="container"
|
|
76
|
+
class="h-[300px] w-full"
|
|
77
|
+
aria-label="按 UTC 日历日聚合的 Agent Run 活动趋势图"
|
|
78
|
+
/>
|
|
79
|
+
<p
|
|
80
|
+
v-else
|
|
81
|
+
class="grid h-[300px] place-items-center text-sm text-muted-foreground"
|
|
82
|
+
>
|
|
83
|
+
当前范围内还没有 Agent Run。
|
|
84
|
+
</p>
|
|
85
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import AiRuntimeActivityChart from "./AiRuntimeActivityChart.vue";
|
|
4
|
+
import AiRuntimeTokenByModelChart from "./AiRuntimeTokenByModelChart.vue";
|
|
5
|
+
|
|
6
|
+
const chart = vi.hoisted(() => ({
|
|
7
|
+
destroy: vi.fn(),
|
|
8
|
+
options: vi.fn(),
|
|
9
|
+
render: vi.fn(async () => undefined),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@antv/g2", () => ({
|
|
13
|
+
Chart: class {
|
|
14
|
+
destroy = chart.destroy;
|
|
15
|
+
options = chart.options;
|
|
16
|
+
render = chart.render;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("AI Runtime 图表生命周期", () => {
|
|
25
|
+
test("活动图从有数据切为空数据时销毁旧实例", async () => {
|
|
26
|
+
const wrapper = mount(AiRuntimeActivityChart, {
|
|
27
|
+
props: {
|
|
28
|
+
activity: [
|
|
29
|
+
{
|
|
30
|
+
date: "2026-07-31",
|
|
31
|
+
runs: 1,
|
|
32
|
+
completed: 1,
|
|
33
|
+
failed: 0,
|
|
34
|
+
totalTokens: 10,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
await flushPromises();
|
|
40
|
+
|
|
41
|
+
await wrapper.setProps({ activity: [] });
|
|
42
|
+
await flushPromises();
|
|
43
|
+
|
|
44
|
+
expect(chart.render).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(chart.destroy).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(wrapper.text()).toContain("还没有 Agent Run");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("模型 Token 图从有数据切为空数据时销毁旧实例", async () => {
|
|
50
|
+
const wrapper = mount(AiRuntimeTokenByModelChart, {
|
|
51
|
+
props: {
|
|
52
|
+
models: [
|
|
53
|
+
{
|
|
54
|
+
deploymentKey: "ai.openai-compatible",
|
|
55
|
+
model: "qwen",
|
|
56
|
+
attempts: 1,
|
|
57
|
+
inputTokens: 10,
|
|
58
|
+
outputTokens: 5,
|
|
59
|
+
cachedInputTokens: 0,
|
|
60
|
+
reasoningTokens: 0,
|
|
61
|
+
totalTokens: 15,
|
|
62
|
+
unknownUsageCount: 0,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
await flushPromises();
|
|
68
|
+
|
|
69
|
+
expect(chart.options).toHaveBeenCalledWith(
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
encode: { x: "model", y: "totalTokens" },
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(chart.options.mock.calls[0]?.[0]).not.toHaveProperty("transform");
|
|
75
|
+
await wrapper.setProps({ models: [] });
|
|
76
|
+
await flushPromises();
|
|
77
|
+
|
|
78
|
+
expect(chart.render).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(chart.destroy).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(wrapper.text()).toContain("没有已上报");
|
|
81
|
+
});
|
|
82
|
+
});
|