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.
Files changed (174) hide show
  1. package/dist/cli.js +292 -125
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/system/operations.test.ts +54 -0
  96. package/template/apps/server/src/system/operations.ts +23 -1
  97. package/template/apps/server/src/system/seed.test.ts +4 -0
  98. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  99. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  100. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  101. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  102. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  103. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  104. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  105. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  106. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  107. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  108. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  109. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  110. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  111. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  112. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  113. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  114. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  115. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  116. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  117. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  118. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  119. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  120. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  121. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  122. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  123. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  124. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  125. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  126. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  127. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  128. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  129. package/template/apps/web/src/router/error-navigation.ts +22 -0
  130. package/template/apps/web/src/router/index.ts +12 -23
  131. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  132. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  133. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  134. package/template/apps/web/src/services/operations-api.ts +28 -0
  135. package/template/docker-compose.agent-debug.yml +1 -1
  136. package/template/docs/architecture/ai-runtime.md +2 -1
  137. package/template/docs/architecture/module-host.md +3 -0
  138. package/template/docs/platform/agent-run-attestation.md +65 -0
  139. package/template/docs/platform/agent-runtime-observability.md +135 -0
  140. package/template/docs/platform/agent-runtime.md +47 -3
  141. package/template/docs/platform/operations-overview.md +13 -0
  142. package/template/docs/platform/platform-authentication.md +4 -3
  143. package/template/packages/config/package.json +1 -1
  144. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  145. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  146. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  147. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  148. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  149. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  150. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  151. package/template/packages/database/package.json +1 -1
  152. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  153. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  154. package/template/packages/jobs/package.json +1 -1
  155. package/template/packages/modules/package.json +1 -1
  156. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  157. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  158. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  159. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  160. package/template/packages/modules/src/system-features.ts +8 -0
  161. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  162. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  163. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  164. package/template/packages/modules/src/system-modules.ts +13 -0
  165. package/template/packages/modules/src/system-permissions.ts +2 -0
  166. package/template/packages/server-sdk/index.ts +11 -0
  167. package/template/packages/server-sdk/package.json +2 -1
  168. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  169. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  170. package/template/packages/server-sdk/src/module-host.ts +5 -0
  171. package/template/packages/shared/index.ts +3 -0
  172. package/template/packages/shared/package.json +1 -1
  173. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  174. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,172 @@
1
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
+ import { parsePagination } from "../pagination.js";
3
+ import { findFeature } from "../route-guards.js";
4
+ import type { createServerRuntime } from "../runtime.js";
5
+ import { routeContext } from "../system/route-helpers.js";
6
+ import type { RouteApp } from "../system/route-types.js";
7
+ import { withPermission } from "../system/resource-route-support.js";
8
+ import {
9
+ AiRuntimeReportError,
10
+ type AiRuntimeReportService,
11
+ } from "./service.js";
12
+
13
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
14
+
15
+ export function registerAiRuntimeReportRoutes(
16
+ app: RouteApp,
17
+ service: AiRuntimeReportService,
18
+ runtime: ServerRuntime,
19
+ features: FeatureFlagDefinition[],
20
+ ): void {
21
+ const feature = findFeature(features, "system.ai-runtime");
22
+
23
+ app.get("/system/ai-runtime/summary", (rawContext) => {
24
+ const { guardContext, query } = routeContext(rawContext);
25
+ return permitted(runtime, guardContext, feature, () =>
26
+ safe(() => service.summary(reportQuery(query), guardContext)),
27
+ );
28
+ });
29
+
30
+ app.get("/system/ai-runtime/runs", (rawContext) => {
31
+ const { guardContext, query } = routeContext(rawContext);
32
+ return permitted(runtime, guardContext, feature, () =>
33
+ safe(async () => {
34
+ const pagination = parsePagination(query);
35
+ if (pagination instanceof Response) return pagination;
36
+ return service.runs(
37
+ { ...reportQuery(query), ...pagination },
38
+ guardContext,
39
+ );
40
+ }),
41
+ );
42
+ });
43
+
44
+ app.get("/system/ai-runtime/runs/:runId", (rawContext) => {
45
+ const { guardContext, params } = routeContext(rawContext);
46
+ return permitted(runtime, guardContext, feature, () =>
47
+ safe(() => service.run(runId(params.runId), guardContext)),
48
+ );
49
+ });
50
+
51
+ app.get("/system/ai-runtime/actors", (rawContext) => {
52
+ const { guardContext, query } = routeContext(rawContext);
53
+ return permitted(runtime, guardContext, feature, () =>
54
+ safe(async () => {
55
+ const pagination = parsePagination(query);
56
+ if (pagination instanceof Response) return pagination;
57
+ return service.actorPage(
58
+ {
59
+ ...pagination,
60
+ ...(keyword(query.q) ? { q: keyword(query.q) } : {}),
61
+ },
62
+ guardContext,
63
+ );
64
+ }),
65
+ );
66
+ });
67
+ }
68
+
69
+ function permitted(
70
+ runtime: ServerRuntime,
71
+ context: Parameters<typeof withPermission>[1],
72
+ feature: FeatureFlagDefinition | undefined,
73
+ handler: () => unknown,
74
+ ) {
75
+ return withPermission(
76
+ runtime,
77
+ context,
78
+ "GET",
79
+ "ai-runtime",
80
+ "read",
81
+ feature,
82
+ handler,
83
+ );
84
+ }
85
+
86
+ function reportQuery(query: Record<string, string | undefined>) {
87
+ if (query.actorId !== undefined) {
88
+ throw new AiRuntimeReportError(
89
+ "INVALID_AI_RUNTIME_QUERY",
90
+ "操作者筛选必须使用 opaque handle",
91
+ 400,
92
+ );
93
+ }
94
+ const range = query.range ?? "day";
95
+ if (!["day", "month", "retained"].includes(range)) {
96
+ throw new AiRuntimeReportError(
97
+ "INVALID_AI_RUNTIME_RANGE",
98
+ "报表时间范围无效",
99
+ 400,
100
+ );
101
+ }
102
+ const actorHandle = optionalActorHandle(query.actorHandle);
103
+ return {
104
+ range: range as "day" | "month" | "retained",
105
+ ...(actorHandle ? { actorHandle } : {}),
106
+ };
107
+ }
108
+
109
+ function optionalActorHandle(value: string | undefined): string | undefined {
110
+ if (value === undefined || value === "") return undefined;
111
+ if (!/^actor_[A-Za-z0-9_-]{30,220}$/.test(value)) {
112
+ throw new AiRuntimeReportError(
113
+ "INVALID_AI_RUNTIME_QUERY",
114
+ "操作者筛选无效",
115
+ 400,
116
+ );
117
+ }
118
+ return value;
119
+ }
120
+
121
+ function runId(value: string | undefined): string {
122
+ const result = optionalIdentifier(value, "Run ID");
123
+ if (!result) {
124
+ throw new AiRuntimeReportError(
125
+ "INVALID_AI_RUNTIME_RUN_ID",
126
+ "Run ID 无效",
127
+ 400,
128
+ );
129
+ }
130
+ return result;
131
+ }
132
+
133
+ function optionalIdentifier(
134
+ value: string | undefined,
135
+ label: string,
136
+ ): string | undefined {
137
+ if (value === undefined || value === "") return undefined;
138
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(value)) {
139
+ throw new AiRuntimeReportError(
140
+ "INVALID_AI_RUNTIME_QUERY",
141
+ `${label} 无效`,
142
+ 400,
143
+ );
144
+ }
145
+ return value;
146
+ }
147
+
148
+ function keyword(value: string | undefined): string | undefined {
149
+ const result = value?.trim();
150
+ if (result && result.length > 100) {
151
+ throw new AiRuntimeReportError(
152
+ "INVALID_AI_RUNTIME_QUERY",
153
+ "搜索关键词最多 100 个字符",
154
+ 400,
155
+ );
156
+ }
157
+ return result || undefined;
158
+ }
159
+
160
+ async function safe(handler: () => unknown): Promise<unknown> {
161
+ try {
162
+ return await handler();
163
+ } catch (error) {
164
+ if (error instanceof AiRuntimeReportError) {
165
+ return Response.json(
166
+ { error: error.code, message: error.message },
167
+ { status: error.status },
168
+ );
169
+ }
170
+ throw error;
171
+ }
172
+ }
@@ -0,0 +1,63 @@
1
+ import { isServerScopeContext } from "../data-access/context.js";
2
+ import { buildDataScopeQuerySpecification } from "../data-access/query-specification.js";
3
+ import type { RequestActor } from "../guards.js";
4
+ import type {
5
+ AiRuntimeReportScope,
6
+ InternalAiRuntimeRunListItem,
7
+ } from "./contracts.js";
8
+
9
+ export interface AiRuntimeDepartmentScope {
10
+ descendants(departmentId: string): Promise<readonly string[]>;
11
+ }
12
+
13
+ export async function resolveAiRuntimeReportScope(
14
+ actor: RequestActor,
15
+ departments: AiRuntimeDepartmentScope,
16
+ ): Promise<AiRuntimeReportScope> {
17
+ if (!isServerScopeContext(actor.scopeContext)) {
18
+ return { mode: "none", actorIds: [], departmentIds: [] };
19
+ }
20
+ const specification = await buildDataScopeQuerySpecification(
21
+ actor.scopeContext,
22
+ departments,
23
+ );
24
+ if (
25
+ specification.branches.some(
26
+ ({ kind }) => kind === "all" || kind === "tenant",
27
+ )
28
+ ) {
29
+ return { mode: "all", actorIds: [], departmentIds: [] };
30
+ }
31
+ const actorIds = specification.branches.flatMap((branch) =>
32
+ branch.kind === "owner" ? [branch.ownerId] : [],
33
+ );
34
+ const departmentIds = specification.branches.flatMap((branch) =>
35
+ branch.kind === "dimension" && branch.dimension === "departmentId"
36
+ ? [...branch.values]
37
+ : [],
38
+ );
39
+ const hasScope = actorIds.length > 0 || departmentIds.length > 0;
40
+ return {
41
+ mode: hasScope ? "bounded" : "none",
42
+ actorIds: unique(actorIds),
43
+ departmentIds: unique(departmentIds),
44
+ };
45
+ }
46
+
47
+ export function reportScopeAllows(
48
+ scope: AiRuntimeReportScope,
49
+ run: Pick<InternalAiRuntimeRunListItem, "actorId"> & {
50
+ departmentId?: string | null;
51
+ },
52
+ ): boolean {
53
+ if (scope.mode === "all") return true;
54
+ if (scope.mode === "none") return false;
55
+ return Boolean(
56
+ (run.actorId && scope.actorIds.includes(run.actorId)) ||
57
+ (run.departmentId && scope.departmentIds.includes(run.departmentId)),
58
+ );
59
+ }
60
+
61
+ function unique(values: readonly string[]): string[] {
62
+ return [...new Set(values)].sort();
63
+ }
@@ -0,0 +1,271 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { featureKey } from "@southwind-ai/shared";
3
+ import { buildScopeContext } from "../data-access/context.js";
4
+ import { createRequestGuardContext } from "../guards.js";
5
+ import { createServerRuntime } from "../runtime.js";
6
+ import { FakeRouteApp } from "../system/route-test-app.js";
7
+ import type {
8
+ AiRuntimeActorDirectory,
9
+ AiRuntimeReportRemotePort,
10
+ AiRuntimeReportScope,
11
+ InternalAiRuntimeRunDetail,
12
+ } from "./contracts.js";
13
+ import { registerAiRuntimeReportRoutes } from "./routes.js";
14
+ import { AiRuntimeReportError, AiRuntimeReportService } from "./service.js";
15
+
16
+ const feature = {
17
+ key: featureKey("system.ai-runtime"),
18
+ label: "AI Runtime 报表",
19
+ module: "system.agent",
20
+ visible: "visible" as const,
21
+ enabled: true,
22
+ stage: "stable" as const,
23
+ };
24
+
25
+ describe("AI Runtime report service", () => {
26
+ test("部门范围详情越权统一返回 404", async () => {
27
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
28
+ const remote = new FakeRemote();
29
+ remote.detail = runDetail("department-other");
30
+ const service = createService(runtime, remote);
31
+
32
+ await expect(service.run("run_1", context(true))).rejects.toEqual(
33
+ new AiRuntimeReportError(
34
+ "AI_RUNTIME_RUN_NOT_FOUND",
35
+ "Agent Run 不存在",
36
+ 404,
37
+ ),
38
+ );
39
+ });
40
+
41
+ test("详情授权后剥离内部 actorId 与 departmentId 并记录审计", async () => {
42
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
43
+ const remote = new FakeRemote();
44
+ remote.detail = runDetail("department-a");
45
+ const service = createService(runtime, remote);
46
+
47
+ const result = await service.run("run_1", context(true));
48
+
49
+ expect(result.actor).toEqual({
50
+ handle: "actor_opaque_value",
51
+ displayName: "用户 A",
52
+ username: "user-a",
53
+ });
54
+ expect(result).not.toHaveProperty("actorId");
55
+ expect(result).not.toHaveProperty("departmentId");
56
+ expect(runtime.auditSink.list().at(-1)?.action).toBe(
57
+ "agent.runtime.run.read",
58
+ );
59
+ });
60
+
61
+ test("缺少权限时 API fail closed 且不调用远端", async () => {
62
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
63
+ const remote = new FakeRemote();
64
+ const service = createService(runtime, remote);
65
+ const app = new FakeRouteApp();
66
+ registerAiRuntimeReportRoutes(app, service, runtime, [feature]);
67
+
68
+ const result = (await app.invoke("GET", "/system/ai-runtime/summary", {
69
+ guardContext: context(false),
70
+ params: {},
71
+ query: { range: "day" },
72
+ })) as Response;
73
+
74
+ expect(result.status).toBe(403);
75
+ expect(remote.summaryCalls).toBe(0);
76
+ });
77
+
78
+ test("浏览器只能用可验证 handle 筛选,远端仍接收内部 actorId", async () => {
79
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
80
+ const remote = new FakeRemote();
81
+ const service = createService(runtime, remote);
82
+
83
+ await service.summary(
84
+ { range: "day", actorHandle: "actor_opaque_value" },
85
+ context(true),
86
+ );
87
+
88
+ expect(remote.summaryQuery).toEqual({
89
+ range: "day",
90
+ actorId: "user-a",
91
+ });
92
+ });
93
+
94
+ test("旧 actorId query 被 API 拒绝", async () => {
95
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
96
+ const remote = new FakeRemote();
97
+ const service = createService(runtime, remote);
98
+ const app = new FakeRouteApp();
99
+ registerAiRuntimeReportRoutes(app, service, runtime, [feature]);
100
+
101
+ const result = (await app.invoke("GET", "/system/ai-runtime/summary", {
102
+ guardContext: context(true),
103
+ params: {},
104
+ query: { range: "day", actorId: "user-a" },
105
+ })) as Response;
106
+
107
+ expect(result.status).toBe(400);
108
+ expect(remote.summaryCalls).toBe(0);
109
+ });
110
+
111
+ test("无法验证或越出范围的 handle fail closed", async () => {
112
+ const runtime = createServerRuntime({}, { featureCatalog: [feature] });
113
+ const remote = new FakeRemote();
114
+ const service = createService(runtime, remote);
115
+
116
+ await expect(
117
+ service.summary(
118
+ { range: "day", actorHandle: "actor_out_of_scope" },
119
+ context(true),
120
+ ),
121
+ ).rejects.toEqual(
122
+ new AiRuntimeReportError(
123
+ "INVALID_AI_RUNTIME_ACTOR",
124
+ "操作者筛选无效或已超出当前数据范围",
125
+ 400,
126
+ ),
127
+ );
128
+ expect(remote.summaryCalls).toBe(0);
129
+ });
130
+ });
131
+
132
+ class FakeRemote implements AiRuntimeReportRemotePort {
133
+ detail?: InternalAiRuntimeRunDetail;
134
+ summaryCalls = 0;
135
+ summaryQuery?: { range: "day" | "month" | "retained"; actorId?: string };
136
+
137
+ async summary(
138
+ query: { range: "day" | "month" | "retained"; actorId?: string },
139
+ _scope: AiRuntimeReportScope,
140
+ ) {
141
+ this.summaryCalls += 1;
142
+ this.summaryQuery = query;
143
+ return {
144
+ generatedAt: "2026-07-31T00:00:00.000Z",
145
+ reportingAvailableFrom: null,
146
+ retentionDays: 90,
147
+ runs: {
148
+ total: 0,
149
+ completed: 0,
150
+ failed: 0,
151
+ cancelled: 0,
152
+ inProgress: 0,
153
+ successRate: null,
154
+ },
155
+ cache: {
156
+ hit: 0,
157
+ miss: 0,
158
+ unknown: 0,
159
+ hitRate: null,
160
+ tokenRate: null,
161
+ coverageRate: 0,
162
+ },
163
+ tokens: {
164
+ input: 0,
165
+ output: 0,
166
+ cachedInput: 0,
167
+ reasoning: 0,
168
+ total: 0,
169
+ coverageRate: 0,
170
+ },
171
+ byModel: [],
172
+ activity: [],
173
+ };
174
+ }
175
+
176
+ async runs(
177
+ _query: {
178
+ range: "day" | "month" | "retained";
179
+ actorId?: string;
180
+ page: number;
181
+ pageSize: number;
182
+ },
183
+ _scope: AiRuntimeReportScope,
184
+ ) {
185
+ return { items: [], total: 0, page: 1, pageSize: 20 };
186
+ }
187
+
188
+ async run() {
189
+ return this.detail;
190
+ }
191
+ }
192
+
193
+ const actors: AiRuntimeActorDirectory = {
194
+ async list(query) {
195
+ return { items: [], total: 0, ...query };
196
+ },
197
+ async resolve(actorIds) {
198
+ return new Map(
199
+ actorIds.map((id) => [
200
+ id,
201
+ {
202
+ handle: "actor_opaque_value",
203
+ displayName: "用户 A",
204
+ username: "user-a",
205
+ },
206
+ ]),
207
+ );
208
+ },
209
+ async resolveHandle(handle) {
210
+ return handle === "actor_opaque_value" ? "user-a" : undefined;
211
+ },
212
+ };
213
+
214
+ function createService(
215
+ runtime: ReturnType<typeof createServerRuntime>,
216
+ remote: AiRuntimeReportRemotePort,
217
+ ) {
218
+ return new AiRuntimeReportService(
219
+ remote,
220
+ actors,
221
+ { descendants: async (id) => [id] },
222
+ runtime,
223
+ );
224
+ }
225
+
226
+ function context(permitted: boolean) {
227
+ return createRequestGuardContext(new Request("http://localhost"), {
228
+ resolveActor: () => ({
229
+ id: "reviewer",
230
+ type: "user",
231
+ name: "审计员",
232
+ roleCodes: ["reviewer"],
233
+ permissionKeys: permitted ? ["system.ai-runtime.read"] : [],
234
+ scopeContext: buildScopeContext("reviewer", "department-a", [
235
+ {
236
+ id: "role-reviewer",
237
+ code: "reviewer",
238
+ dataScope: "department",
239
+ departmentIds: [],
240
+ },
241
+ ]),
242
+ }),
243
+ });
244
+ }
245
+
246
+ function runDetail(departmentId: string): InternalAiRuntimeRunDetail {
247
+ return {
248
+ runId: "run_1",
249
+ operationKey: "business.answer",
250
+ actorId: "user-a",
251
+ departmentId,
252
+ status: "completed",
253
+ startedAt: "2026-07-31T00:00:00.000Z",
254
+ finishedAt: "2026-07-31T00:00:01.000Z",
255
+ durationMs: 1_000,
256
+ attemptCount: 0,
257
+ models: [],
258
+ cacheStatus: "unknown",
259
+ usage: {
260
+ inputTokens: null,
261
+ outputTokens: null,
262
+ cachedInputTokens: null,
263
+ reasoningTokens: null,
264
+ totalTokens: null,
265
+ },
266
+ releaseId: "release_1",
267
+ traceId: "trace_1",
268
+ safeErrorCode: null,
269
+ attempts: [],
270
+ };
271
+ }
@@ -0,0 +1,206 @@
1
+ import type {
2
+ AiRuntimeActorPage,
3
+ AiRuntimeReportSummary,
4
+ AiRuntimeRunDetail,
5
+ AiRuntimeRunListItem,
6
+ AiRuntimeRunPage,
7
+ } from "@southwind-ai/shared";
8
+ import type { RequestGuardContext } from "../guards.js";
9
+ import { resourceEvent } from "../system/route-helpers.js";
10
+ import type { createServerRuntime } from "../runtime.js";
11
+ import type {
12
+ AiRuntimeActorDirectory,
13
+ AiRuntimeReportQuery,
14
+ AiRuntimeReportRemotePort,
15
+ InternalAiRuntimeRunDetail,
16
+ InternalAiRuntimeRunListItem,
17
+ PublicAiRuntimeReportQuery,
18
+ } from "./contracts.js";
19
+ import {
20
+ reportScopeAllows,
21
+ resolveAiRuntimeReportScope,
22
+ type AiRuntimeDepartmentScope,
23
+ } from "./scope.js";
24
+
25
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
26
+
27
+ export class AiRuntimeReportService {
28
+ constructor(
29
+ private readonly remote: AiRuntimeReportRemotePort,
30
+ private readonly actors: AiRuntimeActorDirectory,
31
+ private readonly departments: AiRuntimeDepartmentScope,
32
+ private readonly runtime: ServerRuntime,
33
+ ) {}
34
+
35
+ async summary(
36
+ query: PublicAiRuntimeReportQuery,
37
+ context: RequestGuardContext,
38
+ ): Promise<AiRuntimeReportSummary> {
39
+ const [scope, remoteQuery] = await Promise.all([
40
+ resolveAiRuntimeReportScope(context.actor, this.departments),
41
+ this.remoteQuery(query, context),
42
+ ]);
43
+ const result = await this.remote.summary(remoteQuery, scope);
44
+ await this.auditReport(context, query, "summary", {
45
+ resultCount: result.runs.total,
46
+ });
47
+ return result;
48
+ }
49
+
50
+ async runs(
51
+ query: PublicAiRuntimeReportQuery & { page: number; pageSize: number },
52
+ context: RequestGuardContext,
53
+ ): Promise<AiRuntimeRunPage> {
54
+ const [scope, remoteQuery] = await Promise.all([
55
+ resolveAiRuntimeReportScope(context.actor, this.departments),
56
+ this.remoteQuery(query, context),
57
+ ]);
58
+ const result = await this.remote.runs(
59
+ { ...remoteQuery, page: query.page, pageSize: query.pageSize },
60
+ scope,
61
+ );
62
+ const items = await this.projectRuns(result.items, context);
63
+ await this.auditReport(context, query, "runs", {
64
+ page: result.page,
65
+ pageSize: result.pageSize,
66
+ resultCount: items.length,
67
+ total: result.total,
68
+ });
69
+ return { ...result, items };
70
+ }
71
+
72
+ async run(
73
+ runId: string,
74
+ context: RequestGuardContext,
75
+ ): Promise<AiRuntimeRunDetail> {
76
+ const [scope, raw] = await Promise.all([
77
+ resolveAiRuntimeReportScope(context.actor, this.departments),
78
+ this.remote.run(runId),
79
+ ]);
80
+ if (!raw || !reportScopeAllows(scope, raw)) {
81
+ throw new AiRuntimeReportError(
82
+ "AI_RUNTIME_RUN_NOT_FOUND",
83
+ "Agent Run 不存在",
84
+ 404,
85
+ );
86
+ }
87
+ const item = await this.projectRunDetail(raw, context);
88
+ await this.runtime.auditSink.recordCriticalEvent(
89
+ resourceEvent(
90
+ context,
91
+ "agent.runtime.run.read",
92
+ "agent-runtime-run",
93
+ runId,
94
+ {
95
+ operationKey: raw.operationKey,
96
+ attemptCount: raw.attemptCount,
97
+ },
98
+ ),
99
+ );
100
+ return item;
101
+ }
102
+
103
+ async actorPage(
104
+ query: { q?: string; page: number; pageSize: number },
105
+ context: RequestGuardContext,
106
+ ): Promise<AiRuntimeActorPage> {
107
+ const result = await this.actors.list(query, context.actor);
108
+ await this.auditReport(context, { range: "retained" }, "actors", {
109
+ page: result.page,
110
+ pageSize: result.pageSize,
111
+ resultCount: result.items.length,
112
+ total: result.total,
113
+ });
114
+ return result;
115
+ }
116
+
117
+ async projectRuns(
118
+ values: InternalAiRuntimeRunListItem[],
119
+ context: RequestGuardContext,
120
+ ): Promise<AiRuntimeRunListItem[]> {
121
+ const actorMap = await this.actors.resolve(
122
+ values.flatMap(({ actorId }) => (actorId ? [actorId] : [])),
123
+ context.actor,
124
+ );
125
+ return values.map((value) => projectRun(value, actorMap));
126
+ }
127
+
128
+ async projectRunDetail(
129
+ value: InternalAiRuntimeRunDetail,
130
+ context: RequestGuardContext,
131
+ ): Promise<AiRuntimeRunDetail> {
132
+ const actorMap = await this.actors.resolve(
133
+ value.actorId ? [value.actorId] : [],
134
+ context.actor,
135
+ );
136
+ const { actorId, departmentId: _departmentId, ...publicValue } = value;
137
+ return {
138
+ ...publicValue,
139
+ actor: actorId ? (actorMap.get(actorId) ?? null) : null,
140
+ };
141
+ }
142
+
143
+ async auditReport(
144
+ context: RequestGuardContext,
145
+ query: PublicAiRuntimeReportQuery,
146
+ view: string,
147
+ metadata: Record<string, unknown>,
148
+ ): Promise<void> {
149
+ await this.runtime.auditSink.recordCriticalEvent(
150
+ resourceEvent(
151
+ context,
152
+ "agent.runtime.report.read",
153
+ "agent-runtime-report",
154
+ view,
155
+ {
156
+ range: query.range,
157
+ actorFilterApplied: Boolean(query.actorHandle),
158
+ ...metadata,
159
+ },
160
+ ),
161
+ );
162
+ }
163
+
164
+ private async remoteQuery(
165
+ query: PublicAiRuntimeReportQuery,
166
+ context: RequestGuardContext,
167
+ ): Promise<AiRuntimeReportQuery> {
168
+ if (!query.actorHandle) return { range: query.range };
169
+ const actorId = await this.actors.resolveHandle(
170
+ query.actorHandle,
171
+ context.actor,
172
+ );
173
+ if (!actorId) {
174
+ throw new AiRuntimeReportError(
175
+ "INVALID_AI_RUNTIME_ACTOR",
176
+ "操作者筛选无效或已超出当前数据范围",
177
+ 400,
178
+ );
179
+ }
180
+ return { range: query.range, actorId };
181
+ }
182
+ }
183
+
184
+ export class AiRuntimeReportError extends Error {
185
+ constructor(
186
+ readonly code: string,
187
+ message: string,
188
+ readonly status: number,
189
+ ) {
190
+ super(message);
191
+ }
192
+ }
193
+
194
+ function projectRun(
195
+ value: InternalAiRuntimeRunListItem,
196
+ actors: Map<
197
+ string,
198
+ { handle: string; displayName: string; username?: string }
199
+ >,
200
+ ): AiRuntimeRunListItem {
201
+ const { actorId, departmentId: _departmentId, ...publicValue } = value;
202
+ return {
203
+ ...publicValue,
204
+ actor: actorId ? (actors.get(actorId) ?? null) : null,
205
+ };
206
+ }