create-windy 0.6.9 → 0.6.11
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/README.md +4 -1
- package/dist/cli.js +865 -305
- package/package.json +1 -1
- package/template/.windy-template.json +3 -3
- package/template/README.md +4 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +19 -0
- package/template/apps/agent-server/tests/test_multimodal.py +30 -0
- package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
- package/template/apps/agent-server/tests/test_report_routes.py +86 -0
- package/template/apps/agent-server/tests/test_report_store.py +62 -1
- package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
- package/template/apps/server/src/agent/run-audit.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +1 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -2
- package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
- package/template/apps/server/src/agent/run-output-schema.ts +15 -0
- package/template/apps/server/src/agent/run-release.ts +11 -0
- package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
- package/template/apps/server/src/agent/vision-input.ts +37 -2
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +10 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +1 -0
- package/template/apps/server/src/license/pairing-http.ts +138 -0
- package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
- package/template/apps/server/src/license/pairing-routes.ts +257 -0
- package/template/apps/server/src/license/pairing-service.test.ts +320 -0
- package/template/apps/server/src/license/pairing-service.ts +321 -0
- package/template/apps/server/src/license/restriction.test.ts +11 -0
- package/template/apps/server/src/license/restriction.ts +5 -0
- package/template/apps/server/src/license/routes.ts +8 -1
- package/template/apps/server/src/license/runtime-service.ts +20 -0
- package/template/apps/web/package.json +6 -4
- package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
- package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
- package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
- package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
- package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +3 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +43 -31
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +15 -4
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.vue +48 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +2 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts +70 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +29 -2
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.webtest.ts +56 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +7 -10
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +115 -2
- package/template/apps/web/src/services/license-api.ts +60 -0
- package/template/apps/web/src/style.webtest.ts +5 -6
- package/template/docs/architecture/ai-runtime.md +5 -0
- package/template/docs/license/license-pairing-activation.md +67 -0
- package/template/docs/operations/incremental-delivery.md +6 -6
- package/template/docs/platform/agent-runtime-observability.md +6 -0
- package/template/docs/platform/agent-supervisor-output-contract.md +97 -0
- package/template/packages/ai-client/package.json +1 -1
- package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
- package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-ui-catalog.ts +5 -1
- package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
- package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
- package/template/packages/modules/src/system-api-control-plane.ts +37 -0
- package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
- package/template/packages/modules/src/system-module-catalog.ts +8 -18
- package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
- package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
- package/template/packages/modules/src/system-modules.ts +2 -0
- package/template/packages/server-sdk/index.ts +5 -0
- package/template/packages/server-sdk/package.json +3 -1
- package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +19 -0
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { shallowRef } from "vue";
|
|
2
|
+
import { computed, shallowRef } from "vue";
|
|
3
3
|
import { SearchIcon } from "@lucide/vue";
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@/components/ui/select";
|
|
5
13
|
import type { AiRuntimeActor, AiRuntimeRange } from "@/services/ai-runtime-api";
|
|
6
14
|
|
|
7
|
-
defineProps<{
|
|
15
|
+
const props = defineProps<{
|
|
8
16
|
range: AiRuntimeRange;
|
|
9
17
|
actorHandle?: string;
|
|
10
18
|
actors: readonly AiRuntimeActor[];
|
|
@@ -17,6 +25,12 @@ const emit = defineEmits<{
|
|
|
17
25
|
"search:actors": [value: string];
|
|
18
26
|
}>();
|
|
19
27
|
const actorSearch = shallowRef("");
|
|
28
|
+
const allActorsValue = "__all_actors__";
|
|
29
|
+
const selectedActor = computed({
|
|
30
|
+
get: () => props.actorHandle ?? allActorsValue,
|
|
31
|
+
set: (value: string) =>
|
|
32
|
+
emit("update:actorHandle", value === allActorsValue ? undefined : value),
|
|
33
|
+
});
|
|
20
34
|
|
|
21
35
|
const ranges: readonly { value: AiRuntimeRange; label: string }[] = [
|
|
22
36
|
{ value: "day", label: "近 24 小时" },
|
|
@@ -24,11 +38,6 @@ const ranges: readonly { value: AiRuntimeRange; label: string }[] = [
|
|
|
24
38
|
{ value: "retained", label: "保留期" },
|
|
25
39
|
];
|
|
26
40
|
|
|
27
|
-
function selectActor(event: Event) {
|
|
28
|
-
const value = (event.target as HTMLSelectElement).value;
|
|
29
|
-
emit("update:actorHandle", value || undefined);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
41
|
function searchActors() {
|
|
33
42
|
emit("search:actors", actorSearch.value.trim());
|
|
34
43
|
}
|
|
@@ -38,11 +47,14 @@ function searchActors() {
|
|
|
38
47
|
<div
|
|
39
48
|
class="flex flex-col gap-3 rounded-lg border bg-card p-3 md:flex-row md:items-center md:justify-between"
|
|
40
49
|
>
|
|
41
|
-
<div
|
|
50
|
+
<div
|
|
51
|
+
class="inline-flex min-h-9 w-fit items-center gap-1 rounded-md bg-muted p-0.5"
|
|
52
|
+
>
|
|
42
53
|
<Button
|
|
43
54
|
v-for="item in ranges"
|
|
44
55
|
:key="item.value"
|
|
45
56
|
size="sm"
|
|
57
|
+
class="h-8 px-3"
|
|
46
58
|
:variant="range === item.value ? 'secondary' : 'ghost'"
|
|
47
59
|
:disabled="disabled"
|
|
48
60
|
@click="emit('update:range', item.value)"
|
|
@@ -51,19 +63,19 @@ function searchActors() {
|
|
|
51
63
|
</Button>
|
|
52
64
|
</div>
|
|
53
65
|
|
|
54
|
-
<div class="flex flex-col gap-2 sm:flex-row sm:items-
|
|
55
|
-
<label class="
|
|
66
|
+
<div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-end">
|
|
67
|
+
<label class="grid gap-1 text-sm text-muted-foreground">
|
|
56
68
|
查找
|
|
57
|
-
<span class="flex">
|
|
58
|
-
<
|
|
69
|
+
<span class="flex min-w-0">
|
|
70
|
+
<Input
|
|
59
71
|
v-model="actorSearch"
|
|
60
|
-
class="h-9 w-
|
|
72
|
+
class="h-9 w-full rounded-r-none border-r-0 focus-visible:z-10 sm:w-44"
|
|
61
73
|
placeholder="姓名或账号"
|
|
62
74
|
:disabled="disabled"
|
|
63
75
|
@keyup.enter="searchActors"
|
|
64
76
|
/>
|
|
65
77
|
<Button
|
|
66
|
-
class="rounded-l-none"
|
|
78
|
+
class="h-9 w-9 shrink-0 rounded-l-none"
|
|
67
79
|
size="icon"
|
|
68
80
|
variant="outline"
|
|
69
81
|
:disabled="disabled"
|
|
@@ -74,24 +86,24 @@ function searchActors() {
|
|
|
74
86
|
</Button>
|
|
75
87
|
</span>
|
|
76
88
|
</label>
|
|
77
|
-
<label class="
|
|
89
|
+
<label class="grid gap-1 text-sm text-muted-foreground">
|
|
78
90
|
操作者
|
|
79
|
-
<
|
|
80
|
-
class="h-9
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</
|
|
94
|
-
</
|
|
91
|
+
<Select v-model="selectedActor" :disabled="disabled">
|
|
92
|
+
<SelectTrigger class="h-9 w-full sm:w-48" aria-label="筛选操作者">
|
|
93
|
+
<SelectValue placeholder="全部操作者" />
|
|
94
|
+
</SelectTrigger>
|
|
95
|
+
<SelectContent>
|
|
96
|
+
<SelectItem :value="allActorsValue">全部操作者</SelectItem>
|
|
97
|
+
<SelectItem
|
|
98
|
+
v-for="actor in actors"
|
|
99
|
+
:key="actor.handle"
|
|
100
|
+
:value="actor.handle"
|
|
101
|
+
>
|
|
102
|
+
{{ actor.displayName
|
|
103
|
+
}}{{ actor.username ? `(${actor.username})` : "" }}
|
|
104
|
+
</SelectItem>
|
|
105
|
+
</SelectContent>
|
|
106
|
+
</Select>
|
|
95
107
|
</label>
|
|
96
108
|
</div>
|
|
97
109
|
</div>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { mount } from "@vue/test-utils";
|
|
2
|
-
import { describe, expect, test } from "vitest";
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
3
|
+
import { Select } from "@/components/ui/select";
|
|
3
4
|
import AiRuntimeFilters from "./AiRuntimeFilters.vue";
|
|
4
5
|
|
|
6
|
+
afterEach(() => document.body.replaceChildren());
|
|
7
|
+
|
|
5
8
|
describe("AI Runtime 操作者筛选", () => {
|
|
6
|
-
test("
|
|
9
|
+
test("按姓名搜索后通过设计系统 Select 选择服务端返回的操作者", async () => {
|
|
7
10
|
const wrapper = mount(AiRuntimeFilters, {
|
|
8
11
|
props: {
|
|
9
12
|
range: "day",
|
|
@@ -19,9 +22,17 @@ describe("AI Runtime 操作者筛选", () => {
|
|
|
19
22
|
|
|
20
23
|
await wrapper.get('input[placeholder="姓名或账号"]').setValue("张三");
|
|
21
24
|
await wrapper.get('button[aria-label="搜索操作者"]').trigger("click");
|
|
22
|
-
|
|
25
|
+
wrapper.findComponent(Select).vm.$emit("update:modelValue", "actor_opaque");
|
|
26
|
+
await flushPromises();
|
|
23
27
|
|
|
24
28
|
expect(wrapper.emitted("search:actors")).toEqual([["张三"]]);
|
|
25
29
|
expect(wrapper.emitted("update:actorHandle")).toEqual([["actor_opaque"]]);
|
|
30
|
+
expect(wrapper.find("select").exists()).toBe(false);
|
|
31
|
+
expect(
|
|
32
|
+
wrapper.get('[data-slot="select-trigger"]').attributes("aria-label"),
|
|
33
|
+
).toBe("筛选操作者");
|
|
34
|
+
expect(wrapper.get('[data-slot="select-trigger"]').attributes("role")).toBe(
|
|
35
|
+
"combobox",
|
|
36
|
+
);
|
|
26
37
|
});
|
|
27
38
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import type { AiRuntimeModelUsage } from "@/services/ai-runtime-api";
|
|
4
|
+
import { formatTokenCount } from "../../ai-runtime-presenter";
|
|
5
|
+
|
|
6
|
+
defineProps<{ models: readonly AiRuntimeModelUsage[] }>();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<section aria-labelledby="ai-runtime-model-usage-title" class="space-y-2">
|
|
11
|
+
<div>
|
|
12
|
+
<h3 id="ai-runtime-model-usage-title" class="text-sm font-medium">
|
|
13
|
+
当前范围的模型与 deployment
|
|
14
|
+
</h3>
|
|
15
|
+
<p class="text-xs text-muted-foreground">
|
|
16
|
+
模型名优先采用 Provider 回报;未回报时显示请求模型。Token 未上报不会按 0
|
|
17
|
+
处理。
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<ul
|
|
21
|
+
v-if="models.length"
|
|
22
|
+
class="flex flex-wrap gap-2"
|
|
23
|
+
aria-label="当前筛选范围内的模型 Token 用量"
|
|
24
|
+
>
|
|
25
|
+
<li
|
|
26
|
+
v-for="item in models"
|
|
27
|
+
:key="`${item.deploymentKey}:${item.model}`"
|
|
28
|
+
class="flex min-w-0 items-start gap-1.5 rounded-md border bg-muted/30 px-2 py-1 text-xs"
|
|
29
|
+
>
|
|
30
|
+
<Badge
|
|
31
|
+
variant="secondary"
|
|
32
|
+
class="h-auto whitespace-normal break-all py-0.5 font-medium"
|
|
33
|
+
>
|
|
34
|
+
{{ item.model }}
|
|
35
|
+
</Badge>
|
|
36
|
+
<span class="min-w-0 break-all text-muted-foreground">
|
|
37
|
+
{{ item.deploymentKey }}
|
|
38
|
+
</span>
|
|
39
|
+
<span class="whitespace-nowrap tabular-nums text-foreground">
|
|
40
|
+
{{ formatTokenCount(item.totalTokens) }} Token
|
|
41
|
+
</span>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
<p v-else class="text-sm text-muted-foreground">
|
|
45
|
+
当前范围内没有已上报的模型事实。
|
|
46
|
+
</p>
|
|
47
|
+
</section>
|
|
48
|
+
</template>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeModelUsageList from "./AiRuntimeModelUsageList.vue";
|
|
4
|
+
|
|
5
|
+
describe("AI Runtime 模型用量列表", () => {
|
|
6
|
+
test("模型、deployment 和 Token 在图表外始终可读", () => {
|
|
7
|
+
const wrapper = mount(AiRuntimeModelUsageList, {
|
|
8
|
+
props: {
|
|
9
|
+
models: [
|
|
10
|
+
{
|
|
11
|
+
deploymentKey: "ai.primary",
|
|
12
|
+
model: "provider-model-v2",
|
|
13
|
+
attempts: 2,
|
|
14
|
+
inputTokens: 10,
|
|
15
|
+
outputTokens: 5,
|
|
16
|
+
cachedInputTokens: null,
|
|
17
|
+
reasoningTokens: null,
|
|
18
|
+
totalTokens: 15,
|
|
19
|
+
unknownUsageCount: 0,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(wrapper.text()).toContain("provider-model-v2");
|
|
26
|
+
expect(wrapper.text()).toContain("ai.primary");
|
|
27
|
+
expect(wrapper.text()).toContain("15 Token");
|
|
28
|
+
expect(
|
|
29
|
+
wrapper.find("[aria-label='当前筛选范围内的模型 Token 用量']").exists(),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue
CHANGED
|
@@ -127,7 +127,8 @@ const open = defineModel<boolean>("open", { required: true });
|
|
|
127
127
|
{{ attempt.reportedModel || attempt.requestedModel }}
|
|
128
128
|
</div>
|
|
129
129
|
<div class="text-xs text-muted-foreground">
|
|
130
|
-
{{ attempt.deploymentKey }}
|
|
130
|
+
{{ attempt.deploymentKey }} ·
|
|
131
|
+
{{ attempt.reportedModel ? "Provider 回报" : "请求模型" }}
|
|
131
132
|
</div>
|
|
132
133
|
</TableCell>
|
|
133
134
|
<TableCell>{{ runStatusLabel(attempt.outcome) }}</TableCell>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeRunDetailDialog from "./AiRuntimeRunDetailDialog.vue";
|
|
4
|
+
|
|
5
|
+
afterEach(() => document.body.replaceChildren());
|
|
6
|
+
|
|
7
|
+
describe("AI Runtime Run 详情", () => {
|
|
8
|
+
test("按 Attempt sequence 保留 deployment、请求模型和 Provider 回报模型", async () => {
|
|
9
|
+
const wrapper = mount(AiRuntimeRunDetailDialog, {
|
|
10
|
+
attachTo: document.body,
|
|
11
|
+
props: {
|
|
12
|
+
open: true,
|
|
13
|
+
detail: {
|
|
14
|
+
runId: "run_1",
|
|
15
|
+
operationKey: "business.answer",
|
|
16
|
+
actor: null,
|
|
17
|
+
status: "completed",
|
|
18
|
+
attempts: 2,
|
|
19
|
+
modelDeployments: [],
|
|
20
|
+
totalTokens: 12,
|
|
21
|
+
cacheStatus: "unknown",
|
|
22
|
+
releaseId: "release_1",
|
|
23
|
+
traceId: "trace_1",
|
|
24
|
+
attemptItems: [
|
|
25
|
+
{
|
|
26
|
+
sequence: 1,
|
|
27
|
+
deploymentKey: "ai.primary",
|
|
28
|
+
requestedModel: "requested-a",
|
|
29
|
+
reportedModel: "provider-a",
|
|
30
|
+
outcome: "completed",
|
|
31
|
+
inputTokens: null,
|
|
32
|
+
outputTokens: null,
|
|
33
|
+
cachedInputTokens: null,
|
|
34
|
+
reasoningTokens: null,
|
|
35
|
+
totalTokens: 10,
|
|
36
|
+
cacheStatus: "unknown",
|
|
37
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
sequence: 2,
|
|
41
|
+
deploymentKey: "ai.fallback",
|
|
42
|
+
requestedModel: "requested-b",
|
|
43
|
+
reportedModel: null,
|
|
44
|
+
outcome: "failed",
|
|
45
|
+
inputTokens: null,
|
|
46
|
+
outputTokens: null,
|
|
47
|
+
cachedInputTokens: null,
|
|
48
|
+
reasoningTokens: null,
|
|
49
|
+
totalTokens: null,
|
|
50
|
+
cacheStatus: "unknown",
|
|
51
|
+
startedAt: "2026-07-31T00:00:01.000Z",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
await flushPromises();
|
|
58
|
+
|
|
59
|
+
const rows = [...document.querySelectorAll("tbody tr")];
|
|
60
|
+
expect(rows.map((row) => row.textContent)).toEqual(
|
|
61
|
+
expect.arrayContaining([
|
|
62
|
+
expect.stringContaining("provider-a"),
|
|
63
|
+
expect.stringContaining("requested-b"),
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
expect(rows[0]?.textContent).toContain("ai.primary · Provider 回报");
|
|
67
|
+
expect(rows[1]?.textContent).toContain("ai.fallback · 请求模型");
|
|
68
|
+
wrapper.unmount();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TableRow,
|
|
12
12
|
} from "@/components/ui/table";
|
|
13
13
|
import type {
|
|
14
|
+
AiRuntimeModelDeployment,
|
|
14
15
|
AiRuntimePage,
|
|
15
16
|
AiRuntimeRunItem,
|
|
16
17
|
} from "@/services/ai-runtime-api";
|
|
@@ -40,11 +41,15 @@ function statusVariant(status: string) {
|
|
|
40
41
|
? "secondary"
|
|
41
42
|
: "outline";
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
function modelDeploymentLabel(item: AiRuntimeModelDeployment) {
|
|
46
|
+
return `${item.model} · ${item.deploymentKey}`;
|
|
47
|
+
}
|
|
43
48
|
</script>
|
|
44
49
|
|
|
45
50
|
<template>
|
|
46
51
|
<div class="space-y-3">
|
|
47
|
-
<div class="overflow-
|
|
52
|
+
<div class="overflow-x-auto rounded-md border">
|
|
48
53
|
<Table>
|
|
49
54
|
<TableHeader>
|
|
50
55
|
<TableRow>
|
|
@@ -105,7 +110,29 @@ function statusVariant(status: string) {
|
|
|
105
110
|
{{ runStatusLabel(run.status) }}
|
|
106
111
|
</Badge>
|
|
107
112
|
</TableCell>
|
|
108
|
-
<TableCell
|
|
113
|
+
<TableCell class="min-w-64">
|
|
114
|
+
<ul
|
|
115
|
+
v-if="run.modelDeployments.length"
|
|
116
|
+
class="flex flex-wrap gap-1"
|
|
117
|
+
:aria-label="`Run ${run.runId} 使用的模型与 deployment`"
|
|
118
|
+
>
|
|
119
|
+
<li
|
|
120
|
+
v-for="item in run.modelDeployments"
|
|
121
|
+
:key="`${item.deploymentKey}:${item.model}`"
|
|
122
|
+
>
|
|
123
|
+
<Badge
|
|
124
|
+
variant="outline"
|
|
125
|
+
class="h-auto max-w-60 whitespace-normal break-all py-0.5 text-left leading-4"
|
|
126
|
+
>
|
|
127
|
+
{{ modelDeploymentLabel(item) }}
|
|
128
|
+
<span v-if="item.attemptCount > 1" class="ml-1">
|
|
129
|
+
×{{ item.attemptCount }}
|
|
130
|
+
</span>
|
|
131
|
+
</Badge>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
<span v-else class="text-muted-foreground">未上报</span>
|
|
135
|
+
</TableCell>
|
|
109
136
|
<TableCell class="text-right tabular-nums">
|
|
110
137
|
{{ formatTokenCount(run.totalTokens) }}
|
|
111
138
|
</TableCell>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeRunTable from "./AiRuntimeRunTable.vue";
|
|
4
|
+
|
|
5
|
+
describe("AI Runtime Run 列表", () => {
|
|
6
|
+
test("完整展示多个模型与 deployment 组合,而不是折叠成首项", () => {
|
|
7
|
+
const wrapper = mount(AiRuntimeRunTable, {
|
|
8
|
+
props: {
|
|
9
|
+
runs: {
|
|
10
|
+
items: [
|
|
11
|
+
{
|
|
12
|
+
runId: "run_1",
|
|
13
|
+
operationKey: "business.answer",
|
|
14
|
+
actor: null,
|
|
15
|
+
status: "completed",
|
|
16
|
+
attempts: 3,
|
|
17
|
+
modelDeployments: [
|
|
18
|
+
{
|
|
19
|
+
deploymentKey: "ai.primary",
|
|
20
|
+
model: "provider-a",
|
|
21
|
+
attemptCount: 2,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
deploymentKey: "ai.fallback",
|
|
25
|
+
model: "requested-b",
|
|
26
|
+
attemptCount: 1,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
totalTokens: 12,
|
|
30
|
+
cacheStatus: "unknown",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
runId: "run_2",
|
|
34
|
+
operationKey: "business.answer",
|
|
35
|
+
actor: null,
|
|
36
|
+
status: "failed",
|
|
37
|
+
attempts: 0,
|
|
38
|
+
modelDeployments: [],
|
|
39
|
+
totalTokens: null,
|
|
40
|
+
cacheStatus: "unknown",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
total: 2,
|
|
44
|
+
page: 1,
|
|
45
|
+
pageSize: 20,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(wrapper.text()).toContain("provider-a · ai.primary");
|
|
51
|
+
expect(wrapper.text()).toContain("×2");
|
|
52
|
+
expect(wrapper.text()).toContain("requested-b · ai.fallback");
|
|
53
|
+
expect(wrapper.text()).toContain("未上报");
|
|
54
|
+
expect(wrapper.find(".overflow-x-auto").exists()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AiRuntimeActorSummary,
|
|
4
4
|
AiRuntimeAttemptDetail,
|
|
5
5
|
AiRuntimeReportSummary,
|
|
6
|
+
AiRuntimeRunModelDeployment,
|
|
6
7
|
AiRuntimeRunDetail as RuntimeRunDetail,
|
|
7
8
|
AiRuntimeRunListItem,
|
|
8
9
|
PageResult,
|
|
@@ -18,6 +19,7 @@ export interface AiRuntimeReportQuery {
|
|
|
18
19
|
export type AiRuntimeSummary = AiRuntimeReportSummary;
|
|
19
20
|
export type AiRuntimeModelUsage = AiRuntimeReportSummary["byModel"][number];
|
|
20
21
|
export type AiRuntimeActivity = AiRuntimeReportSummary["activity"][number];
|
|
22
|
+
export type AiRuntimeModelDeployment = AiRuntimeRunModelDeployment;
|
|
21
23
|
|
|
22
24
|
export interface AiRuntimeRunItem {
|
|
23
25
|
readonly runId: string;
|
|
@@ -28,7 +30,7 @@ export interface AiRuntimeRunItem {
|
|
|
28
30
|
readonly finishedAt?: string;
|
|
29
31
|
readonly durationMs?: number;
|
|
30
32
|
readonly attempts: number;
|
|
31
|
-
readonly
|
|
33
|
+
readonly modelDeployments: readonly AiRuntimeModelDeployment[];
|
|
32
34
|
readonly totalTokens: number | null;
|
|
33
35
|
readonly cacheStatus: "hit" | "miss" | "unknown";
|
|
34
36
|
readonly traceId?: string;
|
|
@@ -39,7 +41,7 @@ export interface AiRuntimeAttempt {
|
|
|
39
41
|
readonly sequence: number;
|
|
40
42
|
readonly deploymentKey: string;
|
|
41
43
|
readonly requestedModel: string;
|
|
42
|
-
readonly reportedModel
|
|
44
|
+
readonly reportedModel: string | null;
|
|
43
45
|
readonly outcome: string;
|
|
44
46
|
readonly inputTokens: number | null;
|
|
45
47
|
readonly outputTokens: number | null;
|
|
@@ -119,7 +121,7 @@ function runItem(value: AiRuntimeRunListItem): AiRuntimeRunItem {
|
|
|
119
121
|
...(value.finishedAt ? { finishedAt: value.finishedAt } : {}),
|
|
120
122
|
...(value.durationMs === null ? {} : { durationMs: value.durationMs }),
|
|
121
123
|
attempts: value.attemptCount,
|
|
122
|
-
|
|
124
|
+
modelDeployments: value.modelDeployments,
|
|
123
125
|
totalTokens: value.usage.totalTokens,
|
|
124
126
|
cacheStatus: value.cacheStatus,
|
|
125
127
|
};
|
|
@@ -139,7 +141,8 @@ function attemptItem(value: AiRuntimeAttemptDetail): AiRuntimeAttempt {
|
|
|
139
141
|
return {
|
|
140
142
|
sequence: value.sequence,
|
|
141
143
|
deploymentKey: value.deploymentKey,
|
|
142
|
-
requestedModel: value.
|
|
144
|
+
requestedModel: value.requestedModel,
|
|
145
|
+
reportedModel: value.reportedModel,
|
|
143
146
|
outcome: value.outcome,
|
|
144
147
|
inputTokens: value.inputTokens,
|
|
145
148
|
outputTokens: value.outputTokens,
|
|
@@ -156,9 +159,3 @@ function attemptItem(value: AiRuntimeAttemptDetail): AiRuntimeAttempt {
|
|
|
156
159
|
...(value.finishedAt ? { finishedAt: value.finishedAt } : {}),
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
|
-
|
|
160
|
-
function modelLabel(models: readonly string[]) {
|
|
161
|
-
if (models.length === 0) return undefined;
|
|
162
|
-
if (models.length === 1) return models[0];
|
|
163
|
-
return `${models[0]} 等 ${models.length} 个`;
|
|
164
|
-
}
|
|
@@ -25,6 +25,18 @@ describe("AI Runtime 报表 API", () => {
|
|
|
25
25
|
finishedAt: null,
|
|
26
26
|
durationMs: null,
|
|
27
27
|
attemptCount: 2,
|
|
28
|
+
modelDeployments: [
|
|
29
|
+
{
|
|
30
|
+
deploymentKey: "ai.primary",
|
|
31
|
+
model: "provider-a",
|
|
32
|
+
attemptCount: 1,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
deploymentKey: "ai.fallback",
|
|
36
|
+
model: "requested-b",
|
|
37
|
+
attemptCount: 1,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
28
40
|
models: ["gpt-a", "gpt-b"],
|
|
29
41
|
cacheStatus: "unknown",
|
|
30
42
|
usage: {
|
|
@@ -62,7 +74,18 @@ describe("AI Runtime 报表 API", () => {
|
|
|
62
74
|
);
|
|
63
75
|
expect(page.items[0]).toMatchObject({
|
|
64
76
|
attempts: 2,
|
|
65
|
-
|
|
77
|
+
modelDeployments: [
|
|
78
|
+
{
|
|
79
|
+
deploymentKey: "ai.primary",
|
|
80
|
+
model: "provider-a",
|
|
81
|
+
attemptCount: 1,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
deploymentKey: "ai.fallback",
|
|
85
|
+
model: "requested-b",
|
|
86
|
+
attemptCount: 1,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
66
89
|
totalTokens: null,
|
|
67
90
|
cacheStatus: "unknown",
|
|
68
91
|
});
|
|
@@ -79,6 +102,7 @@ describe("AI Runtime 报表 API", () => {
|
|
|
79
102
|
finishedAt: null,
|
|
80
103
|
durationMs: null,
|
|
81
104
|
attemptCount: 0,
|
|
105
|
+
modelDeployments: [],
|
|
82
106
|
models: [],
|
|
83
107
|
cacheStatus: "unknown",
|
|
84
108
|
usage: {
|
|
@@ -91,7 +115,30 @@ describe("AI Runtime 报表 API", () => {
|
|
|
91
115
|
releaseId: "release_1",
|
|
92
116
|
traceId: "trace_1",
|
|
93
117
|
safeErrorCode: null,
|
|
94
|
-
attempts: [
|
|
118
|
+
attempts: [
|
|
119
|
+
{
|
|
120
|
+
attemptId: "attempt_1",
|
|
121
|
+
sequence: 1,
|
|
122
|
+
toolRound: 1,
|
|
123
|
+
deploymentKey: "ai.primary",
|
|
124
|
+
requestedModel: "requested-model",
|
|
125
|
+
reportedModel: "provider-model",
|
|
126
|
+
model: "provider-model",
|
|
127
|
+
outcome: "completed",
|
|
128
|
+
cacheStatus: "unknown",
|
|
129
|
+
usageSource: "provider-reported",
|
|
130
|
+
inputTokens: null,
|
|
131
|
+
outputTokens: null,
|
|
132
|
+
cachedInputTokens: null,
|
|
133
|
+
reasoningTokens: null,
|
|
134
|
+
totalTokens: null,
|
|
135
|
+
firstTokenLatencyMs: null,
|
|
136
|
+
durationMs: null,
|
|
137
|
+
safeErrorCode: null,
|
|
138
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
139
|
+
finishedAt: null,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
95
142
|
})
|
|
96
143
|
.mockResolvedValueOnce({ items: [], total: 0, page: 1, pageSize: 50 });
|
|
97
144
|
|
|
@@ -108,4 +155,70 @@ describe("AI Runtime 报表 API", () => {
|
|
|
108
155
|
"/system/ai-runtime/actors?page=1&pageSize=50&q=%E5%BC%A0+%E4%B8%89",
|
|
109
156
|
);
|
|
110
157
|
});
|
|
158
|
+
|
|
159
|
+
test("详情保留 Provider 回报与请求模型的原始区别", async () => {
|
|
160
|
+
mocks.apiRequest.mockResolvedValueOnce({
|
|
161
|
+
runId: "run_1",
|
|
162
|
+
operationKey: "customer.answer",
|
|
163
|
+
actor: null,
|
|
164
|
+
status: "completed",
|
|
165
|
+
startedAt: null,
|
|
166
|
+
finishedAt: null,
|
|
167
|
+
durationMs: null,
|
|
168
|
+
attemptCount: 1,
|
|
169
|
+
modelDeployments: [
|
|
170
|
+
{
|
|
171
|
+
deploymentKey: "ai.primary",
|
|
172
|
+
model: "provider-model",
|
|
173
|
+
attemptCount: 1,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
models: ["provider-model"],
|
|
177
|
+
cacheStatus: "unknown",
|
|
178
|
+
usage: {
|
|
179
|
+
inputTokens: null,
|
|
180
|
+
outputTokens: null,
|
|
181
|
+
cachedInputTokens: null,
|
|
182
|
+
reasoningTokens: null,
|
|
183
|
+
totalTokens: null,
|
|
184
|
+
},
|
|
185
|
+
releaseId: "release_1",
|
|
186
|
+
traceId: "trace_1",
|
|
187
|
+
safeErrorCode: null,
|
|
188
|
+
attempts: [
|
|
189
|
+
{
|
|
190
|
+
attemptId: "attempt_1",
|
|
191
|
+
sequence: 1,
|
|
192
|
+
toolRound: 1,
|
|
193
|
+
deploymentKey: "ai.primary",
|
|
194
|
+
requestedModel: "requested-model",
|
|
195
|
+
reportedModel: "provider-model",
|
|
196
|
+
model: "provider-model",
|
|
197
|
+
outcome: "completed",
|
|
198
|
+
cacheStatus: "unknown",
|
|
199
|
+
usageSource: "provider-reported",
|
|
200
|
+
inputTokens: null,
|
|
201
|
+
outputTokens: null,
|
|
202
|
+
cachedInputTokens: null,
|
|
203
|
+
reasoningTokens: null,
|
|
204
|
+
totalTokens: null,
|
|
205
|
+
firstTokenLatencyMs: null,
|
|
206
|
+
durationMs: null,
|
|
207
|
+
safeErrorCode: null,
|
|
208
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
209
|
+
finishedAt: null,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const detail = await getAiRuntimeRun("run_1");
|
|
215
|
+
|
|
216
|
+
expect(detail.attemptItems).toEqual([
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
deploymentKey: "ai.primary",
|
|
219
|
+
requestedModel: "requested-model",
|
|
220
|
+
reportedModel: "provider-model",
|
|
221
|
+
}),
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
111
224
|
});
|