create-windy 0.4.0 → 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 (194) hide show
  1. package/dist/cli.js +263 -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/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/system/operations.test.ts +54 -0
  100. package/template/apps/server/src/system/operations.ts +23 -1
  101. package/template/apps/server/src/system/seed.test.ts +4 -0
  102. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  103. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  104. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  105. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  106. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  107. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  108. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  109. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  110. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  111. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  112. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  113. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  114. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  115. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  116. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  117. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  118. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  119. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  120. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  121. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  122. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  123. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  124. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  125. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  126. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  127. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  128. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  129. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  130. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  131. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  132. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  133. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  134. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  135. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  136. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  137. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  138. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  139. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  140. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  141. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  142. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  143. package/template/apps/web/src/router/error-navigation.ts +22 -0
  144. package/template/apps/web/src/router/index.ts +12 -23
  145. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  146. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  147. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  148. package/template/apps/web/src/services/operations-api.ts +28 -0
  149. package/template/docker-compose.agent-debug.yml +1 -1
  150. package/template/docs/architecture/ai-runtime.md +2 -1
  151. package/template/docs/architecture/module-host.md +3 -0
  152. package/template/docs/platform/agent-run-attestation.md +65 -0
  153. package/template/docs/platform/agent-runtime-observability.md +135 -0
  154. package/template/docs/platform/agent-runtime.md +47 -3
  155. package/template/docs/platform/operations-overview.md +13 -0
  156. package/template/docs/platform/platform-authentication.md +4 -3
  157. package/template/docs/platform/platform-shell-settings.md +15 -10
  158. package/template/docs/platform/web-system-crud.md +1 -1
  159. package/template/packages/config/package.json +1 -1
  160. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  161. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  162. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  163. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  164. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  165. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  166. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  167. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  168. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  169. package/template/packages/database/package.json +1 -1
  170. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  171. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  172. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  173. package/template/packages/jobs/package.json +1 -1
  174. package/template/packages/modules/package.json +1 -1
  175. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  176. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  177. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  178. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  179. package/template/packages/modules/src/system-features.ts +8 -0
  180. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  181. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  182. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  183. package/template/packages/modules/src/system-modules.ts +13 -0
  184. package/template/packages/modules/src/system-permissions.ts +2 -0
  185. package/template/packages/server-sdk/index.ts +11 -0
  186. package/template/packages/server-sdk/package.json +2 -1
  187. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  188. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  189. package/template/packages/server-sdk/src/module-host.ts +5 -0
  190. package/template/packages/shared/index.ts +3 -0
  191. package/template/packages/shared/package.json +1 -1
  192. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  193. package/template/packages/shared/src/platform-settings.ts +17 -0
  194. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,129 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { RefreshCwIcon } from "@lucide/vue";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+ import AiRuntimeActivityChart from "./components/ai-runtime/AiRuntimeActivityChart.vue";
13
+ import AiRuntimeFilters from "./components/ai-runtime/AiRuntimeFilters.vue";
14
+ import AiRuntimeRunDetailDialog from "./components/ai-runtime/AiRuntimeRunDetailDialog.vue";
15
+ import AiRuntimeRunTable from "./components/ai-runtime/AiRuntimeRunTable.vue";
16
+ import AiRuntimeSummaryCards from "./components/ai-runtime/AiRuntimeSummaryCards.vue";
17
+ import AiRuntimeTokenByModelChart from "./components/ai-runtime/AiRuntimeTokenByModelChart.vue";
18
+ import { useAiRuntimeReport } from "./composables/useAiRuntimeReport";
19
+
20
+ const report = useAiRuntimeReport();
21
+ const retentionDescription = computed(() => {
22
+ const summary = report.summary.value;
23
+ if (!summary) return "";
24
+ const available = summary.reportingAvailableFrom
25
+ ? `,现有事实从 ${new Date(summary.reportingAvailableFrom).toLocaleDateString("zh-CN")} 起`
26
+ : "";
27
+ return `报表保留 ${summary.retentionDays} 天${available}。未上报的 Provider 用量不会按 0 计算。`;
28
+ });
29
+ </script>
30
+
31
+ <template>
32
+ <div class="space-y-5">
33
+ <section
34
+ class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"
35
+ >
36
+ <div class="max-w-3xl space-y-2">
37
+ <h1 class="text-2xl font-semibold tracking-normal text-foreground">
38
+ AI Runtime
39
+ </h1>
40
+ <p class="text-sm leading-6 text-muted-foreground">
41
+ 查看 Agent Run 成功率、Provider 缓存、模型 Token
42
+ 用量和受治理的运行明细。 页面不展示 Prompt、回答正文或 Tool 参数。
43
+ </p>
44
+ </div>
45
+ <Button
46
+ variant="outline"
47
+ :disabled="report.loading.value"
48
+ @click="report.reload"
49
+ >
50
+ <RefreshCwIcon data-icon="inline-start" />
51
+ 刷新
52
+ </Button>
53
+ </section>
54
+
55
+ <AiRuntimeFilters
56
+ :range="report.range.value"
57
+ :actor-handle="report.actorHandle.value"
58
+ :actors="report.actors.value"
59
+ :disabled="report.loading.value"
60
+ @update:range="report.setRange"
61
+ @update:actor-handle="report.setActor"
62
+ @search:actors="report.searchActors"
63
+ />
64
+
65
+ <div
66
+ v-if="report.error.value"
67
+ class="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive"
68
+ >
69
+ {{ report.error.value }}
70
+ </div>
71
+
72
+ <template v-if="report.summary.value">
73
+ <AiRuntimeSummaryCards :summary="report.summary.value" />
74
+
75
+ <div class="grid gap-4 xl:grid-cols-2">
76
+ <Card>
77
+ <CardHeader>
78
+ <CardTitle>按模型统计总 Token</CardTitle>
79
+ <CardDescription>
80
+ 模型与 deployment 仅用于授权报表,不进入 Prometheus 高基数标签。
81
+ </CardDescription>
82
+ </CardHeader>
83
+ <CardContent>
84
+ <AiRuntimeTokenByModelChart
85
+ :models="report.summary.value.byModel"
86
+ />
87
+ </CardContent>
88
+ </Card>
89
+
90
+ <Card>
91
+ <CardHeader>
92
+ <CardTitle>按 UTC 日历日的 Agent Run</CardTitle>
93
+ <CardDescription>
94
+ 按 UTC 日历日聚合当前范围内的活动;取消和运行中不计入成功率分母。
95
+ </CardDescription>
96
+ </CardHeader>
97
+ <CardContent>
98
+ <AiRuntimeActivityChart :activity="report.summary.value.activity" />
99
+ </CardContent>
100
+ </Card>
101
+ </div>
102
+ </template>
103
+
104
+ <Card>
105
+ <CardHeader>
106
+ <CardTitle>运行明细</CardTitle>
107
+ <CardDescription>
108
+ {{ retentionDescription || "加载可查询的 Agent Run…" }}
109
+ </CardDescription>
110
+ </CardHeader>
111
+ <CardContent>
112
+ <AiRuntimeRunTable
113
+ :runs="report.runs.value"
114
+ :loading="report.loading.value"
115
+ @select="report.openRun"
116
+ @update:page="report.setPage"
117
+ @update:page-size="report.setPageSize"
118
+ />
119
+ </CardContent>
120
+ </Card>
121
+
122
+ <AiRuntimeRunDetailDialog
123
+ v-model:open="report.detailOpen.value"
124
+ :detail="report.detail.value"
125
+ :loading="report.detailLoading.value"
126
+ :error="report.detailError.value"
127
+ />
128
+ </div>
129
+ </template>
@@ -96,7 +96,7 @@ async function reload() {
96
96
  运维总览
97
97
  </h1>
98
98
  <p class="text-sm text-muted-foreground">
99
- 查看在线会话、应用与数据库资源、缓存状态和定时任务。
99
+ 查看在线会话、CPU/GPU、应用与数据库资源、缓存状态和定时任务。
100
100
  </p>
101
101
  </div>
102
102
  <Button variant="outline" :disabled="refreshing" @click="reloadAll">
@@ -117,6 +117,7 @@ async function reload() {
117
117
  :service="snapshot.serviceMonitor"
118
118
  :database="snapshot.databaseMonitor"
119
119
  :disk="snapshot.diskMonitor"
120
+ :gpu="snapshot.gpuMonitor"
120
121
  />
121
122
  <OperationOnlineUsers :users="snapshot.onlineUsers" />
122
123
  <OperationCacheMonitor :cache="snapshot.cacheMonitor" />
@@ -0,0 +1,60 @@
1
+ const COMPACT = new Intl.NumberFormat("zh-CN", {
2
+ notation: "compact",
3
+ maximumFractionDigits: 1,
4
+ });
5
+ const INTEGER = new Intl.NumberFormat("zh-CN");
6
+ const DATE_TIME = new Intl.DateTimeFormat("zh-CN", {
7
+ month: "2-digit",
8
+ day: "2-digit",
9
+ hour: "2-digit",
10
+ minute: "2-digit",
11
+ });
12
+
13
+ export function formatTokenCount(value: number | null | undefined) {
14
+ if (value === null || value === undefined) return "未上报";
15
+ return value >= 10_000 ? COMPACT.format(value) : INTEGER.format(value);
16
+ }
17
+
18
+ export function formatPercent(value: number | null | undefined) {
19
+ if (value === null || value === undefined) return "未上报";
20
+ return `${(value * 100).toFixed(value >= 0.995 ? 0 : 1)}%`;
21
+ }
22
+
23
+ export function formatCoverage(value: number) {
24
+ return `覆盖 ${formatPercent(value)}`;
25
+ }
26
+
27
+ export function formatDuration(value: number | null | undefined) {
28
+ if (value === null || value === undefined) return "—";
29
+ if (value < 1_000) return `${value} ms`;
30
+ if (value < 60_000) return `${(value / 1_000).toFixed(1)} s`;
31
+ return `${Math.floor(value / 60_000)}m ${Math.round(
32
+ (value % 60_000) / 1_000,
33
+ )}s`;
34
+ }
35
+
36
+ export function formatDateTime(value: string | undefined) {
37
+ if (!value) return "—";
38
+ const date = new Date(value);
39
+ return Number.isNaN(date.getTime()) ? value : DATE_TIME.format(date);
40
+ }
41
+
42
+ export function runStatusLabel(status: string) {
43
+ return (
44
+ {
45
+ completed: "成功",
46
+ failed: "失败",
47
+ cancelled: "已取消",
48
+ accepted: "已接受",
49
+ running: "运行中",
50
+ "awaiting-approval": "等待审批",
51
+ blocked: "已阻止",
52
+ abandoned: "已中止",
53
+ pending: "等待中",
54
+ }[status] || status
55
+ );
56
+ }
57
+
58
+ export function cacheStatusLabel(status: string) {
59
+ return { hit: "命中", miss: "未命中", unknown: "未上报" }[status] || status;
60
+ }
@@ -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