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,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
|
+
}
|