create-windy 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/cli.js +258 -121
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/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,27 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { AiRuntimeActorHandleCodec } from "./actor-handle.js";
3
+
4
+ describe("AI Runtime Actor handle", () => {
5
+ test("签发的 handle 不包含主键且可由同一密钥验证", () => {
6
+ const codec = new AiRuntimeActorHandleCodec("internal-token-for-tests");
7
+ const first = codec.issue("user-sensitive-id");
8
+ const second = codec.issue("user-sensitive-id");
9
+
10
+ expect(first).toStartWith("actor_");
11
+ expect(first).not.toContain("user-sensitive-id");
12
+ expect(first).not.toBe(second);
13
+ expect(codec.verify(first)).toBe("user-sensitive-id");
14
+ expect(codec.verify(second)).toBe("user-sensitive-id");
15
+ });
16
+
17
+ test("篡改或更换密钥后 fail closed", () => {
18
+ const codec = new AiRuntimeActorHandleCodec("internal-token-for-tests");
19
+ const handle = codec.issue("user-1");
20
+
21
+ expect(codec.verify(`${handle.slice(0, -1)}A`)).toBeUndefined();
22
+ expect(
23
+ new AiRuntimeActorHandleCodec("different-internal-token").verify(handle),
24
+ ).toBeUndefined();
25
+ expect(codec.verify("user-1")).toBeUndefined();
26
+ });
27
+ });
@@ -0,0 +1,81 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHmac,
5
+ randomBytes,
6
+ } from "node:crypto";
7
+
8
+ const VERSION = 1;
9
+ const PREFIX = "actor_";
10
+ const IV_BYTES = 12;
11
+ const TAG_BYTES = 16;
12
+ const AAD = Buffer.from("windy.ai-runtime.actor-handle.v1", "utf8");
13
+ const MAX_ACTOR_ID_BYTES = 128;
14
+
15
+ export class AiRuntimeActorHandleCodec {
16
+ readonly #key: Buffer;
17
+
18
+ constructor(secret: string) {
19
+ if (secret.trim().length < 16) {
20
+ throw new Error("AI Runtime Actor handle secret 至少需要 16 个字符");
21
+ }
22
+ this.#key = createHmac("sha256", secret)
23
+ .update("windy.ai-runtime.actor-handle.key.v1")
24
+ .digest();
25
+ }
26
+
27
+ issue(actorId: string): string {
28
+ const plaintext = Buffer.from(actorId, "utf8");
29
+ if (plaintext.length === 0 || plaintext.length > MAX_ACTOR_ID_BYTES) {
30
+ throw new Error("AI Runtime Actor ID 长度无效");
31
+ }
32
+ const iv = randomBytes(IV_BYTES);
33
+ const cipher = createCipheriv("aes-256-gcm", this.#key, iv);
34
+ cipher.setAAD(AAD);
35
+ const ciphertext = Buffer.concat([
36
+ cipher.update(plaintext),
37
+ cipher.final(),
38
+ ]);
39
+ return `${PREFIX}${Buffer.concat([
40
+ Buffer.from([VERSION]),
41
+ iv,
42
+ cipher.getAuthTag(),
43
+ ciphertext,
44
+ ]).toString("base64url")}`;
45
+ }
46
+
47
+ verify(handle: string): string | undefined {
48
+ if (!handle.startsWith(PREFIX)) return undefined;
49
+ try {
50
+ const encoded = handle.slice(PREFIX.length);
51
+ const payload = Buffer.from(encoded, "base64url");
52
+ if (payload.toString("base64url") !== encoded) return undefined;
53
+ if (
54
+ payload.length <= 1 + IV_BYTES + TAG_BYTES ||
55
+ payload[0] !== VERSION
56
+ ) {
57
+ return undefined;
58
+ }
59
+ const ivStart = 1;
60
+ const tagStart = ivStart + IV_BYTES;
61
+ const ciphertextStart = tagStart + TAG_BYTES;
62
+ const decipher = createDecipheriv(
63
+ "aes-256-gcm",
64
+ this.#key,
65
+ payload.subarray(ivStart, tagStart),
66
+ );
67
+ decipher.setAAD(AAD);
68
+ decipher.setAuthTag(payload.subarray(tagStart, ciphertextStart));
69
+ const plaintext = Buffer.concat([
70
+ decipher.update(payload.subarray(ciphertextStart)),
71
+ decipher.final(),
72
+ ]);
73
+ if (plaintext.length === 0 || plaintext.length > MAX_ACTOR_ID_BYTES) {
74
+ return undefined;
75
+ }
76
+ return plaintext.toString("utf8");
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,91 @@
1
+ import type {
2
+ AiRuntimeActorPage,
3
+ AiRuntimeActorSummary,
4
+ AiRuntimeReportRange,
5
+ AiRuntimeReportSummary,
6
+ AiRuntimeRunDetail,
7
+ AiRuntimeRunListItem,
8
+ AiRuntimeRunPage,
9
+ } from "@southwind-ai/shared";
10
+ import type { RequestActor } from "../guards.js";
11
+
12
+ export interface AiRuntimeReportQuery {
13
+ range: AiRuntimeReportRange;
14
+ actorId?: string;
15
+ }
16
+
17
+ export interface PublicAiRuntimeReportQuery {
18
+ range: AiRuntimeReportRange;
19
+ actorHandle?: string;
20
+ }
21
+
22
+ export interface AiRuntimeReportScope {
23
+ mode: "all" | "bounded" | "none";
24
+ actorIds: readonly string[];
25
+ departmentIds: readonly string[];
26
+ }
27
+
28
+ export interface InternalAiRuntimeRunListItem extends Omit<
29
+ AiRuntimeRunListItem,
30
+ "actor"
31
+ > {
32
+ actorId: string | null;
33
+ departmentId: string | null;
34
+ }
35
+
36
+ export interface InternalAiRuntimeRunDetail extends Omit<
37
+ AiRuntimeRunDetail,
38
+ "actor"
39
+ > {
40
+ actorId: string | null;
41
+ departmentId: string | null;
42
+ }
43
+
44
+ export interface InternalAiRuntimeRunPage {
45
+ items: InternalAiRuntimeRunListItem[];
46
+ total: number;
47
+ page: number;
48
+ pageSize: number;
49
+ }
50
+
51
+ export interface AiRuntimeReportRemotePort {
52
+ summary(
53
+ query: AiRuntimeReportQuery,
54
+ scope: AiRuntimeReportScope,
55
+ signal?: AbortSignal,
56
+ ): Promise<AiRuntimeReportSummary>;
57
+ runs(
58
+ query: AiRuntimeReportQuery & { page: number; pageSize: number },
59
+ scope: AiRuntimeReportScope,
60
+ signal?: AbortSignal,
61
+ ): Promise<InternalAiRuntimeRunPage>;
62
+ run(
63
+ runId: string,
64
+ signal?: AbortSignal,
65
+ ): Promise<InternalAiRuntimeRunDetail | undefined>;
66
+ }
67
+
68
+ export interface AiRuntimeActorDirectory {
69
+ list(
70
+ query: { q?: string; page: number; pageSize: number },
71
+ actor: RequestActor,
72
+ ): Promise<AiRuntimeActorPage>;
73
+ resolve(
74
+ actorIds: readonly string[],
75
+ actor: RequestActor,
76
+ ): Promise<Map<string, AiRuntimeActorSummary>>;
77
+ resolveHandle(
78
+ handle: string,
79
+ actor: RequestActor,
80
+ ): Promise<string | undefined>;
81
+ }
82
+
83
+ export type {
84
+ AiRuntimeActorPage,
85
+ AiRuntimeActorSummary,
86
+ AiRuntimeReportRange,
87
+ AiRuntimeReportSummary,
88
+ AiRuntimeRunDetail,
89
+ AiRuntimeRunListItem,
90
+ AiRuntimeRunPage,
91
+ };
@@ -0,0 +1,212 @@
1
+ import type {
2
+ AiRuntimeAttemptDetail,
3
+ AiRuntimeReportSummary,
4
+ AiRuntimeRunUsage,
5
+ } from "@southwind-ai/shared";
6
+ import type {
7
+ InternalAiRuntimeRunDetail,
8
+ InternalAiRuntimeRunListItem,
9
+ InternalAiRuntimeRunPage,
10
+ } from "./contracts.js";
11
+
12
+ export function parseReportSummary(value: unknown): AiRuntimeReportSummary {
13
+ const root = record(value);
14
+ const runs = record(root.runs);
15
+ const cache = record(root.cache);
16
+ const tokens = record(root.tokens);
17
+ return {
18
+ generatedAt: text(root.generatedAt),
19
+ reportingAvailableFrom: nullableText(root.reportingAvailableFrom),
20
+ retentionDays: integer(root.retentionDays),
21
+ runs: {
22
+ total: integer(runs.total),
23
+ completed: integer(runs.completed),
24
+ failed: integer(runs.failed),
25
+ cancelled: integer(runs.cancelled),
26
+ inProgress: integer(runs.inProgress),
27
+ successRate: nullableNumber(runs.successRate),
28
+ },
29
+ cache: {
30
+ hit: integer(cache.hit),
31
+ miss: integer(cache.miss),
32
+ unknown: integer(cache.unknown),
33
+ hitRate: nullableNumber(cache.hitRate),
34
+ tokenRate: nullableNumber(cache.tokenRate),
35
+ coverageRate: number(cache.coverageRate),
36
+ },
37
+ tokens: {
38
+ input: nullableInteger(tokens.input),
39
+ output: nullableInteger(tokens.output),
40
+ cachedInput: nullableInteger(tokens.cachedInput),
41
+ reasoning: nullableInteger(tokens.reasoning),
42
+ total: nullableInteger(tokens.total),
43
+ coverageRate: number(tokens.coverageRate),
44
+ },
45
+ byModel: array(root.byModel).map((item) => {
46
+ const model = record(item);
47
+ return {
48
+ deploymentKey: text(model.deploymentKey),
49
+ model: text(model.model),
50
+ attempts: integer(model.attempts),
51
+ inputTokens: nullableInteger(model.inputTokens),
52
+ outputTokens: nullableInteger(model.outputTokens),
53
+ cachedInputTokens: nullableInteger(model.cachedInputTokens),
54
+ reasoningTokens: nullableInteger(model.reasoningTokens),
55
+ totalTokens: nullableInteger(model.totalTokens),
56
+ unknownUsageCount: integer(model.unknownUsageCount),
57
+ };
58
+ }),
59
+ activity: array(root.activity).map((item) => {
60
+ const point = record(item);
61
+ return {
62
+ date: text(point.date),
63
+ runs: integer(point.runs),
64
+ completed: integer(point.completed),
65
+ failed: integer(point.failed),
66
+ totalTokens: nullableInteger(point.totalTokens),
67
+ };
68
+ }),
69
+ };
70
+ }
71
+
72
+ export function parseReportRunPage(value: unknown): InternalAiRuntimeRunPage {
73
+ const root = record(value);
74
+ return {
75
+ items: array(root.items).map(parseReportRun),
76
+ total: integer(root.total),
77
+ page: integer(root.page),
78
+ pageSize: integer(root.pageSize),
79
+ };
80
+ }
81
+
82
+ export function parseReportRunDetail(
83
+ value: unknown,
84
+ ): InternalAiRuntimeRunDetail {
85
+ const root = record(value);
86
+ return {
87
+ ...parseReportRun(root),
88
+ releaseId: text(root.releaseId),
89
+ traceId: text(root.traceId),
90
+ safeErrorCode: nullableText(root.safeErrorCode),
91
+ attempts: array(root.attempts).map(parseAttempt),
92
+ };
93
+ }
94
+
95
+ function parseReportRun(value: unknown): InternalAiRuntimeRunListItem {
96
+ const item = record(value);
97
+ return {
98
+ runId: text(item.runId),
99
+ operationKey: text(item.operationKey),
100
+ actorId: nullableText(item.actorId),
101
+ departmentId: nullableText(item.departmentId),
102
+ status: oneOf(item.status, [
103
+ "accepted",
104
+ "running",
105
+ "awaiting-approval",
106
+ "completed",
107
+ "failed",
108
+ "cancelled",
109
+ ]),
110
+ startedAt: nullableText(item.startedAt),
111
+ finishedAt: nullableText(item.finishedAt),
112
+ durationMs: nullableInteger(item.durationMs),
113
+ attemptCount: integer(item.attemptCount),
114
+ models: array(item.models).map(text),
115
+ cacheStatus: oneOf(item.cacheStatus, ["hit", "miss", "unknown"]),
116
+ usage: parseUsage(item.usage),
117
+ };
118
+ }
119
+
120
+ function parseAttempt(value: unknown): AiRuntimeAttemptDetail {
121
+ const item = record(value);
122
+ return {
123
+ attemptId: text(item.attemptId),
124
+ sequence: integer(item.sequence),
125
+ toolRound: integer(item.toolRound),
126
+ deploymentKey: text(item.deploymentKey),
127
+ model: text(item.model),
128
+ outcome: oneOf(item.outcome, [
129
+ "running",
130
+ "completed",
131
+ "failed",
132
+ "cancelled",
133
+ "blocked",
134
+ "abandoned",
135
+ ]),
136
+ cacheStatus: oneOf(item.cacheStatus, ["hit", "miss", "unknown"]),
137
+ usageSource: oneOf(item.usageSource, [
138
+ "provider-reported",
139
+ "estimated",
140
+ "unknown",
141
+ ]),
142
+ ...parseUsage(item),
143
+ firstTokenLatencyMs: nullableInteger(item.firstTokenLatencyMs),
144
+ durationMs: nullableInteger(item.durationMs),
145
+ safeErrorCode: nullableText(item.safeErrorCode),
146
+ startedAt: text(item.startedAt),
147
+ finishedAt: nullableText(item.finishedAt),
148
+ };
149
+ }
150
+
151
+ function parseUsage(value: unknown): AiRuntimeRunUsage {
152
+ const usage = record(value);
153
+ return {
154
+ inputTokens: nullableInteger(usage.inputTokens),
155
+ outputTokens: nullableInteger(usage.outputTokens),
156
+ cachedInputTokens: nullableInteger(usage.cachedInputTokens),
157
+ reasoningTokens: nullableInteger(usage.reasoningTokens),
158
+ totalTokens: nullableInteger(usage.totalTokens),
159
+ };
160
+ }
161
+
162
+ function record(value: unknown): Record<string, unknown> {
163
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
164
+ return value as Record<string, unknown>;
165
+ }
166
+
167
+ function array(value: unknown): unknown[] {
168
+ if (!Array.isArray(value)) invalid();
169
+ return value;
170
+ }
171
+
172
+ function text(value: unknown): string {
173
+ if (typeof value !== "string" || !value.trim()) invalid();
174
+ return value;
175
+ }
176
+
177
+ function nullableText(value: unknown): string | null {
178
+ return value === null ? null : text(value);
179
+ }
180
+
181
+ function number(value: unknown): number {
182
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
183
+ invalid();
184
+ }
185
+ return value;
186
+ }
187
+
188
+ function nullableNumber(value: unknown): number | null {
189
+ return value === null ? null : number(value);
190
+ }
191
+
192
+ function integer(value: unknown): number {
193
+ const result = number(value);
194
+ if (!Number.isSafeInteger(result)) invalid();
195
+ return result;
196
+ }
197
+
198
+ function nullableInteger(value: unknown): number | null {
199
+ return value === null ? null : integer(value);
200
+ }
201
+
202
+ function oneOf<const T extends string>(
203
+ value: unknown,
204
+ values: readonly T[],
205
+ ): T {
206
+ if (typeof value !== "string" || !values.includes(value as T)) invalid();
207
+ return value as T;
208
+ }
209
+
210
+ function invalid(): never {
211
+ throw new Error("Agent Runtime 报表返回协议无效");
212
+ }
@@ -0,0 +1,138 @@
1
+ import type {
2
+ AiRuntimeReportQuery,
3
+ AiRuntimeReportRemotePort,
4
+ AiRuntimeReportScope,
5
+ } from "./contracts.js";
6
+ import {
7
+ parseReportRunDetail,
8
+ parseReportRunPage,
9
+ parseReportSummary,
10
+ } from "./remote-parser.js";
11
+
12
+ export interface RemoteAiRuntimeReportOptions {
13
+ baseUrl: string;
14
+ internalToken: string;
15
+ timeoutMs?: number;
16
+ maxResponseBytes?: number;
17
+ }
18
+
19
+ export class RemoteAiRuntimeReportPort implements AiRuntimeReportRemotePort {
20
+ readonly #baseUrl: string;
21
+ readonly #timeoutMs: number;
22
+ readonly #maxResponseBytes: number;
23
+
24
+ constructor(private readonly options: RemoteAiRuntimeReportOptions) {
25
+ this.#baseUrl = normalizeInternalUrl(options.baseUrl);
26
+ this.#timeoutMs = options.timeoutMs ?? 10_000;
27
+ this.#maxResponseBytes = options.maxResponseBytes ?? 2 * 1024 * 1024;
28
+ if (!options.internalToken.trim()) {
29
+ throw new Error("Agent Server 内部 Token 不能为空");
30
+ }
31
+ }
32
+
33
+ async summary(
34
+ query: AiRuntimeReportQuery,
35
+ scope: AiRuntimeReportScope,
36
+ signal?: AbortSignal,
37
+ ) {
38
+ return parseReportSummary(
39
+ await this.get(
40
+ `/api/internal/ai-runtime-report/summary?${params(query, scope)}`,
41
+ signal,
42
+ ),
43
+ );
44
+ }
45
+
46
+ async runs(
47
+ query: AiRuntimeReportQuery & { page: number; pageSize: number },
48
+ scope: AiRuntimeReportScope,
49
+ signal?: AbortSignal,
50
+ ) {
51
+ const queryParams = params(query, scope);
52
+ queryParams.set("page", String(query.page));
53
+ queryParams.set("pageSize", String(query.pageSize));
54
+ return parseReportRunPage(
55
+ await this.get(
56
+ `/api/internal/ai-runtime-report/runs?${queryParams}`,
57
+ signal,
58
+ ),
59
+ );
60
+ }
61
+
62
+ async run(runId: string, signal?: AbortSignal) {
63
+ const response = await this.request(
64
+ `/api/internal/ai-runtime-report/runs/${encodeURIComponent(runId)}`,
65
+ signal,
66
+ );
67
+ if (response.status === 404) return undefined;
68
+ if (!response.ok) throw remoteFailure(response.status);
69
+ return parseReportRunDetail(
70
+ await boundedJson(response, this.#maxResponseBytes),
71
+ );
72
+ }
73
+
74
+ async get(path: string, signal?: AbortSignal): Promise<unknown> {
75
+ const response = await this.request(path, signal);
76
+ if (!response.ok) throw remoteFailure(response.status);
77
+ return boundedJson(response, this.#maxResponseBytes);
78
+ }
79
+
80
+ request(path: string, signal?: AbortSignal) {
81
+ return fetch(new URL(path, this.#baseUrl), {
82
+ headers: { authorization: `Bearer ${this.options.internalToken}` },
83
+ signal: signal
84
+ ? AbortSignal.any([signal, AbortSignal.timeout(this.#timeoutMs)])
85
+ : AbortSignal.timeout(this.#timeoutMs),
86
+ });
87
+ }
88
+ }
89
+
90
+ function params(
91
+ query: AiRuntimeReportQuery,
92
+ scope: AiRuntimeReportScope,
93
+ ): URLSearchParams {
94
+ const result = new URLSearchParams({
95
+ range: query.range,
96
+ scopeMode: scope.mode,
97
+ });
98
+ if (query.actorId) result.set("actorId", query.actorId);
99
+ for (const actorId of scope.actorIds) result.append("scopeActorId", actorId);
100
+ for (const departmentId of scope.departmentIds) {
101
+ result.append("scopeDepartmentId", departmentId);
102
+ }
103
+ return result;
104
+ }
105
+
106
+ async function boundedJson(
107
+ response: Response,
108
+ maximum: number,
109
+ ): Promise<unknown> {
110
+ const declared = Number(response.headers.get("content-length"));
111
+ if (Number.isFinite(declared) && declared > maximum) throw responseTooLarge();
112
+ const body = await response.text();
113
+ if (Buffer.byteLength(body) > maximum) throw responseTooLarge();
114
+ try {
115
+ return JSON.parse(body) as unknown;
116
+ } catch {
117
+ throw new Error("Agent Runtime 报表返回协议无效");
118
+ }
119
+ }
120
+
121
+ function remoteFailure(status: number): Error {
122
+ return new Error(`Agent Runtime 报表请求失败(HTTP ${status})`);
123
+ }
124
+
125
+ function responseTooLarge(): Error {
126
+ return new Error("Agent Runtime 报表响应超过安全上限");
127
+ }
128
+
129
+ function normalizeInternalUrl(raw: string): string {
130
+ const url = new URL(raw);
131
+ if (!["http:", "https:"].includes(url.protocol)) {
132
+ throw new Error("Agent Server URL 只允许 HTTP 或 HTTPS");
133
+ }
134
+ if (url.username || url.password || url.search || url.hash) {
135
+ throw new Error("Agent Server URL 不允许凭据、查询串或片段");
136
+ }
137
+ return url.toString();
138
+ }