create-windy 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +292 -125
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -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
|
|
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
|
|
|
@@ -25,7 +25,12 @@ const developmentAuth = developmentAuthStatus();
|
|
|
25
25
|
<div class="w-full max-w-md space-y-4">
|
|
26
26
|
<div class="flex items-center justify-center gap-3">
|
|
27
27
|
<span
|
|
28
|
-
class="flex size-10 items-center justify-center rounded-xl
|
|
28
|
+
class="flex size-10 items-center justify-center rounded-xl"
|
|
29
|
+
:class="
|
|
30
|
+
platform.branding.value.logoUrl
|
|
31
|
+
? 'bg-transparent text-foreground'
|
|
32
|
+
: 'bg-primary text-primary-foreground'
|
|
33
|
+
"
|
|
29
34
|
>
|
|
30
35
|
<img
|
|
31
36
|
v-if="platform.branding.value.logoUrl"
|
|
@@ -13,6 +13,12 @@ const access = useAccessSnapshot();
|
|
|
13
13
|
const toast = useAppToast();
|
|
14
14
|
const softwareName = shallowRef("");
|
|
15
15
|
const logoUrl = shallowRef("");
|
|
16
|
+
const logoUrlInput = computed({
|
|
17
|
+
get: () => (logoUrl.value.startsWith("data:") ? "" : logoUrl.value),
|
|
18
|
+
set: (value: string) => {
|
|
19
|
+
logoUrl.value = value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
16
22
|
const description = shallowRef("");
|
|
17
23
|
const readingLogo = shallowRef(false);
|
|
18
24
|
const loaded = shallowRef(false);
|
|
@@ -145,11 +151,17 @@ defineExpose({
|
|
|
145
151
|
</Button>
|
|
146
152
|
</div>
|
|
147
153
|
<Input
|
|
148
|
-
v-model="
|
|
154
|
+
v-model="logoUrlInput"
|
|
149
155
|
:disabled="!canManage"
|
|
150
|
-
maxlength="
|
|
156
|
+
maxlength="2048"
|
|
151
157
|
placeholder="或填写 https://example.com/logo.png"
|
|
152
158
|
/>
|
|
159
|
+
<span
|
|
160
|
+
v-if="logoUrl.startsWith('data:')"
|
|
161
|
+
class="text-xs font-normal text-muted-foreground"
|
|
162
|
+
>
|
|
163
|
+
当前使用本地图片,编码内容不在输入框中显示。
|
|
164
|
+
</span>
|
|
153
165
|
<span class="text-xs font-normal text-muted-foreground">
|
|
154
166
|
本地文件支持 PNG、JPEG、WebP,最大
|
|
155
167
|
{{ maxLocalLogoBytes / 1024 }} KiB;也可使用 HTTPS 或站内路径。
|
|
@@ -159,7 +171,12 @@ defineExpose({
|
|
|
159
171
|
<p class="mb-3 text-xs font-medium text-muted-foreground">预览</p>
|
|
160
172
|
<div class="flex items-center gap-3">
|
|
161
173
|
<div
|
|
162
|
-
class="flex size-12 items-center justify-center overflow-hidden rounded-lg
|
|
174
|
+
class="flex size-12 items-center justify-center overflow-hidden rounded-lg"
|
|
175
|
+
:class="
|
|
176
|
+
logoUrl
|
|
177
|
+
? 'bg-transparent text-foreground'
|
|
178
|
+
: 'bg-primary text-primary-foreground'
|
|
179
|
+
"
|
|
163
180
|
>
|
|
164
181
|
<img
|
|
165
182
|
v-if="logoUrl"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { shallowRef } from "vue";
|
|
4
|
+
import BrandingSettingsSection from "./BrandingSettingsSection.vue";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
refresh: vi.fn(),
|
|
8
|
+
save: vi.fn(),
|
|
9
|
+
success: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/composables/useAccessSnapshot", () => ({
|
|
14
|
+
useAccessSnapshot: () => ({
|
|
15
|
+
snapshot: shallowRef({
|
|
16
|
+
actor: { permissionKeys: ["system.settings.manage"] },
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/composables/useAppToast", () => ({
|
|
22
|
+
useAppToast: () => ({ success: mocks.success, error: mocks.error }),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@/composables/usePlatformSettings", () => ({
|
|
26
|
+
usePlatformSettings: () => ({
|
|
27
|
+
refresh: mocks.refresh,
|
|
28
|
+
save: mocks.save,
|
|
29
|
+
saving: shallowRef(false),
|
|
30
|
+
error: shallowRef(""),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe("品牌基本信息", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mocks.refresh.mockReset();
|
|
37
|
+
mocks.save.mockReset();
|
|
38
|
+
mocks.success.mockReset();
|
|
39
|
+
mocks.error.mockReset();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("本地 Logo 保留预览但不在地址输入框暴露 Base64,且不添加底色", async () => {
|
|
43
|
+
const localLogo = "data:image/png;base64,AA==";
|
|
44
|
+
mocks.refresh.mockResolvedValue({
|
|
45
|
+
softwareName: "利剑",
|
|
46
|
+
logoUrl: localLogo,
|
|
47
|
+
description: "企业级 Dashboard 产品基座",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const wrapper = mount(BrandingSettingsSection);
|
|
51
|
+
await flushPromises();
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
(wrapper.get('input[placeholder^="或填写"]').element as HTMLInputElement)
|
|
55
|
+
.value,
|
|
56
|
+
).toBe("");
|
|
57
|
+
expect(wrapper.get('img[alt="Logo 预览"]').attributes("src")).toBe(
|
|
58
|
+
localLogo,
|
|
59
|
+
);
|
|
60
|
+
expect(wrapper.text()).toContain("编码内容不在输入框中显示");
|
|
61
|
+
const preview = wrapper.get('img[alt="Logo 预览"]').element.parentElement;
|
|
62
|
+
expect(preview?.classList.contains("bg-transparent")).toBe(true);
|
|
63
|
+
expect(preview?.classList.contains("bg-primary")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("外部或站内 Logo 地址仍可编辑", async () => {
|
|
67
|
+
mocks.refresh.mockResolvedValue({
|
|
68
|
+
softwareName: "Windy",
|
|
69
|
+
logoUrl: "/assets/logo.png",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const wrapper = mount(BrandingSettingsSection);
|
|
73
|
+
await flushPromises();
|
|
74
|
+
|
|
75
|
+
expect(
|
|
76
|
+
(wrapper.get('input[placeholder^="或填写"]').element as HTMLInputElement)
|
|
77
|
+
.value,
|
|
78
|
+
).toBe("/assets/logo.png");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -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
|
+
}
|