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,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ calculateCpuMonitor,
4
+ createGpuMonitorProbe,
5
+ type CpuSample,
6
+ } from "./system-resource-monitoring.js";
7
+
8
+ describe("系统 CPU 与 GPU 监控", () => {
9
+ test("CPU 占用按宿主机时间和逻辑核心归一化进程时间计算", () => {
10
+ const previous: CpuSample = {
11
+ hostIdleMilliseconds: 2_000,
12
+ hostTotalMilliseconds: 10_000,
13
+ processMicroseconds: 1_000_000,
14
+ observedAtMicroseconds: 5_000_000,
15
+ logicalCores: 4,
16
+ loadAverage1m: 1,
17
+ };
18
+ const snapshot = calculateCpuMonitor(previous, {
19
+ hostIdleMilliseconds: 2_400,
20
+ hostTotalMilliseconds: 11_000,
21
+ processMicroseconds: 1_800_000,
22
+ observedAtMicroseconds: 6_000_000,
23
+ logicalCores: 4,
24
+ loadAverage1m: 1.5,
25
+ });
26
+
27
+ expect(snapshot).toMatchObject({
28
+ hostUsagePercent: 60,
29
+ processUsagePercent: 20,
30
+ logicalCores: 4,
31
+ loadAverage1m: 1.5,
32
+ });
33
+ });
34
+
35
+ test("NVIDIA 指标关联设备利用率与 AI 计算进程显存", async () => {
36
+ const probe = createGpuMonitorProbe(async (args) =>
37
+ args[0]?.startsWith("--query-gpu=")
38
+ ? {
39
+ exitCode: 0,
40
+ stdout: '0, GPU-abc, "NVIDIA RTX 5000", 75, 8192, 32768, 62\n',
41
+ }
42
+ : {
43
+ exitCode: 0,
44
+ stdout: 'GPU-abc, 4321, "python ai-service.py", 7168\n',
45
+ },
46
+ );
47
+
48
+ await expect(probe()).resolves.toMatchObject({
49
+ status: "available",
50
+ devices: [
51
+ {
52
+ index: 0,
53
+ name: "NVIDIA RTX 5000",
54
+ usagePercent: 75,
55
+ memoryUsagePercent: 25,
56
+ temperatureCelsius: 62,
57
+ processes: [
58
+ {
59
+ pid: 4321,
60
+ name: "python ai-service.py",
61
+ usedMemoryBytes: 7_168 * 1_024 ** 2,
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ });
67
+ });
68
+
69
+ test("没有 nvidia-smi 时返回可解释的不可探测状态", async () => {
70
+ const probe = createGpuMonitorProbe(async () => ({
71
+ exitCode: null,
72
+ stdout: "",
73
+ missing: true,
74
+ }));
75
+
76
+ await expect(probe()).resolves.toMatchObject({
77
+ status: "not-detected",
78
+ provider: "NVIDIA",
79
+ devices: [],
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,300 @@
1
+ import { cpus, loadavg } from "node:os";
2
+
3
+ export interface CpuMonitorSnapshot {
4
+ status: "available";
5
+ logicalCores: number;
6
+ hostUsagePercent: number | null;
7
+ processUsagePercent: number | null;
8
+ loadAverage1m: number | null;
9
+ message?: string;
10
+ }
11
+
12
+ export interface GpuProcessSnapshot {
13
+ pid: number;
14
+ name: string;
15
+ usedMemoryBytes: number | null;
16
+ }
17
+
18
+ export interface GpuDeviceSnapshot {
19
+ index: number;
20
+ uuid: string;
21
+ name: string;
22
+ usagePercent: number | null;
23
+ memoryUsedBytes: number | null;
24
+ memoryTotalBytes: number | null;
25
+ memoryUsagePercent: number | null;
26
+ temperatureCelsius: number | null;
27
+ processes: GpuProcessSnapshot[];
28
+ }
29
+
30
+ export interface GpuMonitorSnapshot {
31
+ status: "available" | "not-detected" | "unavailable";
32
+ provider: "NVIDIA";
33
+ devices: GpuDeviceSnapshot[];
34
+ message: string;
35
+ }
36
+
37
+ export type CpuMonitorProbe = () => Promise<CpuMonitorSnapshot>;
38
+ export type GpuMonitorProbe = () => Promise<GpuMonitorSnapshot>;
39
+
40
+ export interface CpuSample {
41
+ hostIdleMilliseconds: number;
42
+ hostTotalMilliseconds: number;
43
+ processMicroseconds: number;
44
+ observedAtMicroseconds: number;
45
+ logicalCores: number;
46
+ loadAverage1m: number | null;
47
+ }
48
+
49
+ export function createCpuMonitorProbe(
50
+ readSample: () => CpuSample = systemCpuSample,
51
+ ): CpuMonitorProbe {
52
+ let previous = readSample();
53
+ return async () => {
54
+ const current = readSample();
55
+ const snapshot = calculateCpuMonitor(previous, current);
56
+ previous = current;
57
+ return snapshot;
58
+ };
59
+ }
60
+
61
+ export function calculateCpuMonitor(
62
+ previous: CpuSample,
63
+ current: CpuSample,
64
+ ): CpuMonitorSnapshot {
65
+ const totalDelta =
66
+ current.hostTotalMilliseconds - previous.hostTotalMilliseconds;
67
+ const idleDelta =
68
+ current.hostIdleMilliseconds - previous.hostIdleMilliseconds;
69
+ const elapsedMicroseconds =
70
+ current.observedAtMicroseconds - previous.observedAtMicroseconds;
71
+ const processDelta =
72
+ current.processMicroseconds - previous.processMicroseconds;
73
+ const hostUsagePercent =
74
+ totalDelta > 0 ? percent(1 - Math.max(0, idleDelta) / totalDelta) : null;
75
+ const processUsagePercent =
76
+ elapsedMicroseconds > 0 && current.logicalCores > 0
77
+ ? percent(
78
+ Math.max(0, processDelta) /
79
+ elapsedMicroseconds /
80
+ current.logicalCores,
81
+ )
82
+ : null;
83
+ return {
84
+ status: "available",
85
+ logicalCores: current.logicalCores,
86
+ hostUsagePercent,
87
+ processUsagePercent,
88
+ loadAverage1m: current.loadAverage1m,
89
+ message:
90
+ hostUsagePercent === null || processUsagePercent === null
91
+ ? "等待下一次刷新以计算 CPU 占用。"
92
+ : undefined,
93
+ };
94
+ }
95
+
96
+ interface CommandResult {
97
+ exitCode: number | null;
98
+ stdout: string;
99
+ missing?: boolean;
100
+ timedOut?: boolean;
101
+ }
102
+
103
+ export type NvidiaSmiRunner = (args: string[]) => Promise<CommandResult>;
104
+
105
+ export function createGpuMonitorProbe(
106
+ run: NvidiaSmiRunner = runNvidiaSmi,
107
+ ): GpuMonitorProbe {
108
+ return async () => {
109
+ const deviceResult = await run([
110
+ "--query-gpu=index,uuid,name,utilization.gpu,memory.used,memory.total,temperature.gpu",
111
+ "--format=csv,noheader,nounits",
112
+ ]);
113
+ if (deviceResult.missing) {
114
+ return {
115
+ status: "not-detected",
116
+ provider: "NVIDIA",
117
+ devices: [],
118
+ message:
119
+ "未检测到 nvidia-smi;当前仅支持 NVIDIA GPU,容器部署还需挂载 GPU 设备和驱动能力。",
120
+ };
121
+ }
122
+ if (deviceResult.exitCode !== 0) {
123
+ return {
124
+ status: "unavailable",
125
+ provider: "NVIDIA",
126
+ devices: [],
127
+ message: deviceResult.timedOut
128
+ ? "NVIDIA GPU 指标采集超时。"
129
+ : "nvidia-smi 查询失败,无法读取 GPU 指标。",
130
+ };
131
+ }
132
+ const devices = parseGpuDevices(deviceResult.stdout);
133
+ if (!devices.length) {
134
+ return {
135
+ status: "not-detected",
136
+ provider: "NVIDIA",
137
+ devices: [],
138
+ message: "nvidia-smi 未返回可用的 NVIDIA GPU。",
139
+ };
140
+ }
141
+ const processResult = await run([
142
+ "--query-compute-apps=gpu_uuid,pid,process_name,used_gpu_memory",
143
+ "--format=csv,noheader,nounits",
144
+ ]);
145
+ if (processResult.exitCode === 0) {
146
+ attachGpuProcesses(devices, processResult.stdout);
147
+ }
148
+ return {
149
+ status: "available",
150
+ provider: "NVIDIA",
151
+ devices,
152
+ message:
153
+ processResult.exitCode === 0
154
+ ? "GPU 利用率为设备采样值;进程列表展示计算进程的显存占用。"
155
+ : "GPU 设备指标可用,但计算进程明细暂不可用。",
156
+ };
157
+ };
158
+ }
159
+
160
+ function systemCpuSample(): CpuSample {
161
+ const cores = cpus();
162
+ const totals = cores.reduce(
163
+ (result, core) => {
164
+ const values = Object.values(core.times);
165
+ result.idle += core.times.idle;
166
+ result.total += values.reduce((total, value) => total + value, 0);
167
+ return result;
168
+ },
169
+ { idle: 0, total: 0 },
170
+ );
171
+ const processUsage = process.cpuUsage();
172
+ return {
173
+ hostIdleMilliseconds: totals.idle,
174
+ hostTotalMilliseconds: totals.total,
175
+ processMicroseconds: processUsage.user + processUsage.system,
176
+ observedAtMicroseconds: Number(process.hrtime.bigint() / 1_000n),
177
+ logicalCores: cores.length,
178
+ loadAverage1m: process.platform === "win32" ? null : loadavg()[0] || 0,
179
+ };
180
+ }
181
+
182
+ function percent(ratio: number): number {
183
+ return Number((Math.min(1, Math.max(0, ratio)) * 100).toFixed(2));
184
+ }
185
+
186
+ async function runNvidiaSmi(args: string[]): Promise<CommandResult> {
187
+ if (!Bun.which("nvidia-smi")) {
188
+ return { exitCode: null, stdout: "", missing: true };
189
+ }
190
+ const child = Bun.spawn(["nvidia-smi", ...args], {
191
+ stdout: "pipe",
192
+ stderr: "ignore",
193
+ });
194
+ let timeout: ReturnType<typeof setTimeout> | undefined;
195
+ const result = await Promise.race([
196
+ child.exited.then((exitCode) => ({ exitCode, timedOut: false })),
197
+ new Promise<{ exitCode: null; timedOut: true }>((resolve) => {
198
+ timeout = setTimeout(
199
+ () => resolve({ exitCode: null, timedOut: true }),
200
+ 1_500,
201
+ );
202
+ }),
203
+ ]);
204
+ if (timeout) clearTimeout(timeout);
205
+ if (result.timedOut) {
206
+ child.kill();
207
+ await child.exited;
208
+ return { exitCode: null, stdout: "", timedOut: true };
209
+ }
210
+ return {
211
+ exitCode: result.exitCode,
212
+ stdout: await new Response(child.stdout).text(),
213
+ };
214
+ }
215
+
216
+ function parseGpuDevices(output: string): GpuDeviceSnapshot[] {
217
+ return output
218
+ .split(/\r?\n/)
219
+ .filter(Boolean)
220
+ .flatMap((line) => {
221
+ const values = parseCsvLine(line);
222
+ if (values.length !== 7) return [];
223
+ const memoryUsedMiB = metric(values[4]);
224
+ const memoryTotalMiB = metric(values[5]);
225
+ return [
226
+ {
227
+ index: metric(values[0]) ?? 0,
228
+ uuid: values[1] || "",
229
+ name: values[2] || "NVIDIA GPU",
230
+ usagePercent: metric(values[3]),
231
+ memoryUsedBytes: mebibytes(memoryUsedMiB),
232
+ memoryTotalBytes: mebibytes(memoryTotalMiB),
233
+ memoryUsagePercent:
234
+ memoryUsedMiB !== null &&
235
+ memoryTotalMiB !== null &&
236
+ memoryTotalMiB > 0
237
+ ? percent(memoryUsedMiB / memoryTotalMiB)
238
+ : null,
239
+ temperatureCelsius: metric(values[6]),
240
+ processes: [],
241
+ },
242
+ ];
243
+ });
244
+ }
245
+
246
+ function attachGpuProcesses(
247
+ devices: GpuDeviceSnapshot[],
248
+ output: string,
249
+ ): void {
250
+ const byUuid = new Map(devices.map((device) => [device.uuid, device]));
251
+ for (const line of output.split(/\r?\n/).filter(Boolean)) {
252
+ const values = parseCsvLine(line);
253
+ const device = byUuid.get(values[0] || "");
254
+ if (!device || values.length !== 4) continue;
255
+ const pid = metric(values[1]);
256
+ if (pid === null) continue;
257
+ device.processes.push({
258
+ pid,
259
+ name: values[2] || "未知进程",
260
+ usedMemoryBytes: mebibytes(metric(values[3])),
261
+ });
262
+ }
263
+ }
264
+
265
+ function metric(value: string | undefined): number | null {
266
+ const normalized = value?.trim();
267
+ if (!normalized || normalized === "N/A" || normalized === "[Not Supported]") {
268
+ return null;
269
+ }
270
+ const parsed = Number(normalized);
271
+ return Number.isFinite(parsed) ? parsed : null;
272
+ }
273
+
274
+ function mebibytes(value: number | null): number | null {
275
+ return value === null ? null : value * 1_024 ** 2;
276
+ }
277
+
278
+ function parseCsvLine(line: string): string[] {
279
+ const values: string[] = [];
280
+ let value = "";
281
+ let quoted = false;
282
+ for (let index = 0; index < line.length; index += 1) {
283
+ const character = line[index]!;
284
+ if (character === '"') {
285
+ if (quoted && line[index + 1] === '"') {
286
+ value += '"';
287
+ index += 1;
288
+ } else {
289
+ quoted = !quoted;
290
+ }
291
+ } else if (character === "," && !quoted) {
292
+ values.push(value.trim());
293
+ value = "";
294
+ } else {
295
+ value += character;
296
+ }
297
+ }
298
+ values.push(value.trim());
299
+ return values;
300
+ }
@@ -30,6 +30,8 @@ describe("全局水印策略状态", () => {
30
30
  density: "high",
31
31
  contentFields: ["custom"],
32
32
  customContent: "全局审计",
33
+ opacityPercent: 12,
34
+ fontSize: 18,
33
35
  });
34
36
  const { useWatermarkSettings } = await import("./useWatermarkSettings");
35
37
  const watermark = useWatermarkSettings(shallowRef(undefined));
@@ -42,6 +44,8 @@ describe("全局水印策略状态", () => {
42
44
  density: "high",
43
45
  contentFields: ["custom"],
44
46
  customContent: "全局审计",
47
+ opacityPercent: 12,
48
+ fontSize: 18,
45
49
  });
46
50
  expect(localStorageRead).not.toHaveBeenCalled();
47
51
  expect(localStorageWrite).not.toHaveBeenCalled();
@@ -54,6 +58,8 @@ describe("全局水印策略状态", () => {
54
58
  density: "medium",
55
59
  contentFields: ["username", "displayName"],
56
60
  customContent: "",
61
+ opacityPercent: 8,
62
+ fontSize: 14,
57
63
  });
58
64
  api.update.mockResolvedValue({
59
65
  enabled: true,
@@ -61,6 +67,8 @@ describe("全局水印策略状态", () => {
61
67
  density: "high",
62
68
  contentFields: ["username", "custom"],
63
69
  customContent: " 内部资料 ",
70
+ opacityPercent: 16,
71
+ fontSize: 20,
64
72
  });
65
73
  const { useWatermarkSettings } = await import("./useWatermarkSettings");
66
74
  const actor = shallowRef({
@@ -80,6 +88,8 @@ describe("全局水印策略状态", () => {
80
88
  density: "high",
81
89
  contentFields: ["username", "custom"],
82
90
  customContent: "内部资料",
91
+ opacityPercent: 16,
92
+ fontSize: 20,
83
93
  });
84
94
 
85
95
  expect(api.update).toHaveBeenCalledWith({
@@ -88,6 +98,8 @@ describe("全局水印策略状态", () => {
88
98
  density: "high",
89
99
  contentFields: ["username", "custom"],
90
100
  customContent: "内部资料",
101
+ opacityPercent: 16,
102
+ fontSize: 20,
91
103
  });
92
104
  expect(second.policy.value).toEqual({
93
105
  enabled: true,
@@ -95,6 +107,8 @@ describe("全局水印策略状态", () => {
95
107
  density: "high",
96
108
  contentFields: ["username", "custom"],
97
109
  customContent: "内部资料",
110
+ opacityPercent: 16,
111
+ fontSize: 20,
98
112
  });
99
113
  });
100
114
  });
@@ -3,6 +3,8 @@ import {
3
3
  DEFAULT_PLATFORM_WATERMARK_POLICY,
4
4
  WATERMARK_CONTENT_FIELDS,
5
5
  WATERMARK_DENSITIES,
6
+ WATERMARK_FONT_SIZE_RANGE,
7
+ WATERMARK_OPACITY_RANGE,
6
8
  type PlatformWatermarkPolicy,
7
9
  type WatermarkContentField,
8
10
  type WatermarkDensity,
@@ -16,8 +18,6 @@ const DENSITY_GAPS: Record<
16
18
  medium: { horizontal: 72, vertical: 48 },
17
19
  high: { horizontal: 36, vertical: 24 },
18
20
  };
19
- const FONT_SIZE = 14;
20
- const LINE_HEIGHT = 22;
21
21
  const ROTATION_RADIANS = (22 * Math.PI) / 180;
22
22
  const MAX_LINE_WEIGHT = 32;
23
23
 
@@ -58,15 +58,26 @@ export function normalizeWatermarkPolicy(
58
58
  ),
59
59
  ),
60
60
  ];
61
+ const enabled = Boolean(value.enabled && value.businessPagesEnabled);
61
62
  return {
62
- enabled: value.enabled,
63
- businessPagesEnabled: value.businessPagesEnabled,
63
+ enabled,
64
+ businessPagesEnabled: enabled,
64
65
  density,
65
66
  contentFields:
66
67
  contentFields.length > 0
67
68
  ? contentFields
68
69
  : [...DEFAULT_PLATFORM_WATERMARK_POLICY.contentFields],
69
70
  customContent: value.customContent.trim().slice(0, 80),
71
+ opacityPercent: boundedInteger(
72
+ value.opacityPercent,
73
+ WATERMARK_OPACITY_RANGE,
74
+ DEFAULT_PLATFORM_WATERMARK_POLICY.opacityPercent,
75
+ ),
76
+ fontSize: boundedInteger(
77
+ value.fontSize,
78
+ WATERMARK_FONT_SIZE_RANGE,
79
+ DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
80
+ ),
70
81
  };
71
82
  }
72
83
 
@@ -78,10 +89,19 @@ export interface WatermarkPattern {
78
89
  export function createWatermarkPattern(
79
90
  content: string,
80
91
  density: WatermarkDensity,
92
+ fontSize: number,
81
93
  ): WatermarkPattern {
94
+ const safeFontSize = boundedInteger(
95
+ fontSize,
96
+ WATERMARK_FONT_SIZE_RANGE,
97
+ DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
98
+ );
99
+ const lineHeight = Math.ceil(safeFontSize * 1.57);
82
100
  const lines = wrapWatermarkContent(content);
83
- const textWidth = Math.max(...lines.map(estimatedTextWidth));
84
- const textHeight = lines.length * LINE_HEIGHT;
101
+ const textWidth = Math.max(
102
+ ...lines.map((line) => estimatedTextWidth(line, safeFontSize)),
103
+ );
104
+ const textHeight = lines.length * lineHeight;
85
105
  const rotatedWidth =
86
106
  textWidth * Math.cos(ROTATION_RADIANS) +
87
107
  textHeight * Math.sin(ROTATION_RADIANS);
@@ -91,17 +111,17 @@ export function createWatermarkPattern(
91
111
  const gap = DENSITY_GAPS[density];
92
112
  const width = Math.ceil(rotatedWidth + gap.horizontal);
93
113
  const height = Math.ceil(rotatedHeight + gap.vertical);
94
- const firstLineY = (height - textHeight) / 2 + FONT_SIZE;
114
+ const firstLineY = (height - textHeight) / 2 + safeFontSize;
95
115
  const tspans = lines
96
116
  .map(
97
117
  (line, index) =>
98
- `<tspan x="${width / 2}" y="${firstLineY + index * LINE_HEIGHT}">${escapeXml(line)}</tspan>`,
118
+ `<tspan x="${width / 2}" y="${firstLineY + index * lineHeight}">${escapeXml(line)}</tspan>`,
99
119
  )
100
120
  .join("");
101
121
  const svg =
102
122
  `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
103
123
  `<g transform="rotate(-22 ${width / 2} ${height / 2})">` +
104
- `<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${FONT_SIZE}" font-weight="500" fill="black">${tspans}</text>` +
124
+ `<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${safeFontSize}" font-weight="500" fill="black">${tspans}</text>` +
105
125
  "</g></svg>";
106
126
  return {
107
127
  image: `url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
@@ -125,8 +145,8 @@ function wrapWatermarkContent(content: string): string[] {
125
145
  return lines.length > 0 ? lines : [""];
126
146
  }
127
147
 
128
- function estimatedTextWidth(value: string): number {
129
- return Math.max(1, textWeight(value)) * (FONT_SIZE * 0.56);
148
+ function estimatedTextWidth(value: string, fontSize: number): number {
149
+ return Math.max(1, textWeight(value)) * (fontSize * 0.56);
130
150
  }
131
151
 
132
152
  function textWeight(value: string): number {
@@ -145,3 +165,12 @@ function escapeXml(value: string): string {
145
165
  .replaceAll('"', "&quot;")
146
166
  .replaceAll("'", "&apos;");
147
167
  }
168
+
169
+ function boundedInteger(
170
+ value: number,
171
+ range: { readonly min: number; readonly max: number },
172
+ fallback: number,
173
+ ): number {
174
+ if (!Number.isFinite(value)) return fallback;
175
+ return Math.min(range.max, Math.max(range.min, Math.round(value)));
176
+ }
@@ -73,8 +73,8 @@ describe("水印设置", () => {
73
73
  test("高密度缩小平铺单元且长内容自动换行以避免相邻水印重叠", () => {
74
74
  const content =
75
75
  "zhangsan · 张三 · 13800138000 · zs@example.com · 内部资料请勿外传";
76
- const low = createWatermarkPattern(content, "low");
77
- const high = createWatermarkPattern(content, "high");
76
+ const low = createWatermarkPattern(content, "low", 14);
77
+ const high = createWatermarkPattern(content, "high", 14);
78
78
  const dimensions = (value: typeof low) =>
79
79
  value.size.match(/\d+/g)!.map(Number);
80
80
 
@@ -86,21 +86,34 @@ describe("水印设置", () => {
86
86
  ).toBeGreaterThan(1);
87
87
  });
88
88
 
89
- test("服务端策略内容会被规范化后渲染", () => {
89
+ test("字体大小会改变平铺尺寸并写入实际 SVG", () => {
90
+ const small = createWatermarkPattern("zhangsan · 张三", "medium", 12);
91
+ const large = createWatermarkPattern("zhangsan · 张三", "medium", 32);
92
+ const width = (value: typeof small) => Number(value.size.match(/\d+/)?.[0]);
93
+
94
+ expect(width(large)).toBeGreaterThan(width(small));
95
+ expect(decodeURIComponent(large.image)).toContain('font-size="32"');
96
+ });
97
+
98
+ test("服务端策略内容会被规范化后渲染且旧范围开关合并到总开关", () => {
90
99
  expect(
91
100
  normalizeWatermarkPolicy({
92
- enabled: false,
93
- businessPagesEnabled: true,
101
+ enabled: true,
102
+ businessPagesEnabled: false,
94
103
  density: "high",
95
104
  contentFields: ["phone", "phone"],
96
105
  customContent: ` ${"x".repeat(100)} `,
106
+ opacityPercent: 99,
107
+ fontSize: 2,
97
108
  }),
98
109
  ).toEqual({
99
110
  enabled: false,
100
- businessPagesEnabled: true,
111
+ businessPagesEnabled: false,
101
112
  density: "high",
102
113
  contentFields: ["phone"],
103
114
  customContent: "x".repeat(80),
115
+ opacityPercent: 30,
116
+ fontSize: 12,
104
117
  });
105
118
  });
106
119
  });
@@ -193,7 +193,12 @@ function findMenu(menus: MenuNode[], key: string): MenuNode | undefined {
193
193
  >
194
194
  <div
195
195
  data-testid="sidebar-brand-logo"
196
- class="flex size-8 max-w-full shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary text-primary-foreground [&>svg]:size-5 group-data-[collapsible=icon]:size-4 group-data-[collapsible=icon]:rounded-sm group-data-[collapsible=icon]:[&>svg]:size-4"
196
+ class="flex size-8 max-w-full shrink-0 items-center justify-center overflow-hidden rounded-lg [&>svg]:size-5 group-data-[collapsible=icon]:size-4 group-data-[collapsible=icon]:rounded-sm group-data-[collapsible=icon]:[&>svg]:size-4"
197
+ :class="
198
+ platform.branding.value.logoUrl
199
+ ? 'bg-transparent text-sidebar-foreground'
200
+ : 'bg-primary text-primary-foreground'
201
+ "
197
202
  >
198
203
  <img
199
204
  v-if="platform.branding.value.logoUrl"
@@ -96,6 +96,8 @@ describe("AppSidebar", () => {
96
96
  expect(brandLink.classes()).toContain("group-data-[collapsible=icon]:p-0");
97
97
  expect(logo.classes()).toContain("group-data-[collapsible=icon]:size-4");
98
98
  expect(logo.classes()).toContain("overflow-hidden");
99
+ expect(logo.classes()).toContain("bg-transparent");
100
+ expect(logo.classes()).not.toContain("bg-primary");
99
101
  wrapper.unmount();
100
102
  });
101
103
 
@@ -17,6 +17,8 @@ const state = vi.hoisted(() => {
17
17
  density: "medium",
18
18
  contentFields: ["username", "displayName"],
19
19
  customContent: "",
20
+ opacityPercent: 8,
21
+ fontSize: 14,
20
22
  },
21
23
  };
22
24
 
@@ -31,17 +31,16 @@ const allowed = computed(() => {
31
31
  props.scope === "admin" ? "system.watermark.read" : undefined,
32
32
  }).allowed;
33
33
  });
34
- const scopeEnabled = computed(
35
- () => props.scope === "admin" || watermark.policy.value.businessPagesEnabled,
36
- );
37
34
  const patternStyle = computed<StyleValue>(() => {
38
35
  const pattern = createWatermarkPattern(
39
36
  watermark.content.value,
40
37
  watermark.policy.value.density,
38
+ watermark.policy.value.fontSize,
41
39
  );
42
40
  return {
43
41
  "--watermark-image": pattern.image,
44
42
  "--watermark-size": pattern.size,
43
+ opacity: watermark.policy.value.opacityPercent / 100,
45
44
  };
46
45
  });
47
46
  </script>
@@ -50,12 +49,9 @@ const patternStyle = computed<StyleValue>(() => {
50
49
  <Teleport to="body">
51
50
  <div
52
51
  v-if="
53
- allowed &&
54
- scopeEnabled &&
55
- watermark.policy.value.enabled &&
56
- watermark.content.value
52
+ allowed && watermark.policy.value.enabled && watermark.content.value
57
53
  "
58
- class="global-watermark pointer-events-none fixed inset-0 isolate z-[60] transform-gpu bg-foreground opacity-[0.075] dark:opacity-[0.11]"
54
+ class="global-watermark pointer-events-none fixed inset-0 isolate z-[60] transform-gpu bg-foreground"
59
55
  :style="patternStyle"
60
56
  aria-hidden="true"
61
57
  data-testid="global-watermark"