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.
Files changed (174) hide show
  1. package/dist/cli.js +258 -121
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/system/operations.test.ts +54 -0
  96. package/template/apps/server/src/system/operations.ts +23 -1
  97. package/template/apps/server/src/system/seed.test.ts +4 -0
  98. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  99. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  100. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  101. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  102. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  103. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  104. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  105. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  106. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  107. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  108. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  109. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  110. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  111. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  112. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  113. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  114. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  115. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  116. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  117. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  118. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  119. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  120. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  121. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  122. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  123. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  124. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  125. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  126. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  127. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  128. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  129. package/template/apps/web/src/router/error-navigation.ts +22 -0
  130. package/template/apps/web/src/router/index.ts +12 -23
  131. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  132. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  133. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  134. package/template/apps/web/src/services/operations-api.ts +28 -0
  135. package/template/docker-compose.agent-debug.yml +1 -1
  136. package/template/docs/architecture/ai-runtime.md +2 -1
  137. package/template/docs/architecture/module-host.md +3 -0
  138. package/template/docs/platform/agent-run-attestation.md +65 -0
  139. package/template/docs/platform/agent-runtime-observability.md +135 -0
  140. package/template/docs/platform/agent-runtime.md +47 -3
  141. package/template/docs/platform/operations-overview.md +13 -0
  142. package/template/docs/platform/platform-authentication.md +4 -3
  143. package/template/packages/config/package.json +1 -1
  144. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  145. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  146. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  147. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  148. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  149. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  150. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  151. package/template/packages/database/package.json +1 -1
  152. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  153. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  154. package/template/packages/jobs/package.json +1 -1
  155. package/template/packages/modules/package.json +1 -1
  156. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  157. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  158. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  159. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  160. package/template/packages/modules/src/system-features.ts +8 -0
  161. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  162. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  163. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  164. package/template/packages/modules/src/system-modules.ts +13 -0
  165. package/template/packages/modules/src/system-permissions.ts +2 -0
  166. package/template/packages/server-sdk/index.ts +11 -0
  167. package/template/packages/server-sdk/package.json +2 -1
  168. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  169. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  170. package/template/packages/server-sdk/src/module-host.ts +5 -0
  171. package/template/packages/shared/index.ts +3 -0
  172. package/template/packages/shared/package.json +1 -1
  173. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  174. 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 occurredAfterLocal = shallowRef("");
36
- const occurredBeforeLocal = shallowRef("");
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: { stubs: { DiskUsageChart: true } },
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(() => vi.clearAllMocks());
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
+ });