create-windy 0.8.1 → 0.8.3
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 +52 -34
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/src/southwind_agent_server/attempt_context.py +9 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +5 -2
- package/template/apps/agent-server/src/southwind_agent_server/provider_output_guidance.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +16 -5
- package/template/apps/agent-server/tests/test_structured_logging.py +37 -0
- package/template/apps/agent-server/tests/test_tool_structured_output.py +347 -0
- package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
- package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
- package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
- package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
- package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
- package/template/apps/server/src/agent/operation-catalog.ts +135 -0
- package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
- package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
- package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
- package/template/apps/server/src/agent/run-route-response.ts +35 -0
- package/template/apps/server/src/agent/run-routes.ts +46 -43
- package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
- package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
- package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
- package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
- package/template/apps/server/src/agent/session-projection-view.ts +19 -16
- package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
- package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
- package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
- package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
- package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
- package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
- package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
- package/template/apps/server/src/agent/tool-catalog.ts +44 -0
- package/template/apps/server/src/agent/tool-registry.ts +45 -14
- package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
- package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
- package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
- package/template/apps/server/src/agent/workbench-facade.ts +339 -0
- package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
- package/template/apps/server/src/agent/workbench-routes.ts +97 -0
- package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
- package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
- package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
- package/template/apps/server/src/agent/workbench-sse.ts +50 -0
- package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
- package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
- package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
- package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
- package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
- package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
- package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
- package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
- package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
- package/template/apps/web/src/router/index.ts +4 -0
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/agent-admin-api.ts +155 -0
- package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
- package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
- package/template/docs/architecture/agent-harness.md +4 -0
- package/template/docs/architecture/log-governance-implementation.md +20 -3
- package/template/docs/architecture/log-governance-plan.md +2 -2
- package/template/docs/platform/agent-runtime.md +18 -0
- package/template/packages/modules/src/ai-operation-composition.ts +9 -0
- package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/system-admin-routes.ts +16 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
- package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
- package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
- package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
- package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
- package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
- package/template/packages/modules/src/system-audit-actions.ts +25 -0
- package/template/packages/modules/src/system-features.ts +19 -0
- package/template/packages/modules/src/system-module-catalog.ts +14 -12
- package/template/packages/modules/src/system-module-ownership.ts +12 -0
- package/template/packages/modules/src/system-modules.ts +2 -0
- package/template/packages/modules/src/system-permissions.ts +5 -0
- package/template/packages/server-sdk/index.ts +2 -0
- package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
- package/template/packages/shared/index.ts +2 -0
- package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
- package/template/packages/shared/src/agent-workbench.ts +100 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { RouterLink } from "vue-router";
|
|
4
|
+
import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
|
|
3
5
|
import { RefreshCwIcon } from "@lucide/vue";
|
|
4
6
|
import { Button } from "@/components/ui/button";
|
|
5
7
|
import AgentSessionDetail from "./components/agent-session/AgentSessionDetail.vue";
|
|
@@ -7,6 +9,12 @@ import AgentSessionList from "./components/agent-session/AgentSessionList.vue";
|
|
|
7
9
|
import { useAgentSessions } from "./composables/useAgentSessions";
|
|
8
10
|
|
|
9
11
|
const sessionState = useAgentSessions();
|
|
12
|
+
const access = useAccessSnapshot();
|
|
13
|
+
const canOpenWorkbench = computed(() =>
|
|
14
|
+
access.snapshot.value?.actor.permissionKeys.includes(
|
|
15
|
+
"system.agent-debug.read",
|
|
16
|
+
),
|
|
17
|
+
);
|
|
10
18
|
const selectedSession = computed(() =>
|
|
11
19
|
sessionState.sessions.value.find(
|
|
12
20
|
({ sessionId }) => sessionId === sessionState.selectedSessionId.value,
|
|
@@ -30,14 +38,27 @@ const refreshing = computed(
|
|
|
30
38
|
查看长期 Session、公开事件摘要、模型 Context 来源和受控 Memory 投影。
|
|
31
39
|
</p>
|
|
32
40
|
</div>
|
|
33
|
-
<
|
|
34
|
-
variant="outline"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
<div class="flex gap-2">
|
|
42
|
+
<Button v-if="canOpenWorkbench" variant="outline" as-child
|
|
43
|
+
><RouterLink
|
|
44
|
+
:to="{
|
|
45
|
+
path: '/admin/system/agent-debug',
|
|
46
|
+
query: selectedSession
|
|
47
|
+
? { sessionId: selectedSession.sessionId }
|
|
48
|
+
: {},
|
|
49
|
+
}"
|
|
50
|
+
>统一会话回放</RouterLink
|
|
51
|
+
></Button
|
|
52
|
+
>
|
|
53
|
+
<Button
|
|
54
|
+
variant="outline"
|
|
55
|
+
:disabled="refreshing"
|
|
56
|
+
@click="sessionState.refresh"
|
|
57
|
+
>
|
|
58
|
+
<RefreshCwIcon data-icon="inline-start" />
|
|
59
|
+
刷新
|
|
60
|
+
</Button>
|
|
61
|
+
</div>
|
|
41
62
|
</header>
|
|
42
63
|
|
|
43
64
|
<div
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
|
|
3
|
+
import type { AgentAdminToolCatalogItem } from "@southwind-ai/shared";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
|
|
6
|
+
import { listAdminTools } from "@/services/agent-admin-api";
|
|
7
|
+
import ToolCatalogPanel from "./components/agent-workbench/ToolCatalogPanel.vue";
|
|
8
|
+
import ToolRuleDetail from "./components/agent-workbench/ToolRuleDetail.vue";
|
|
9
|
+
const tools = shallowRef<readonly AgentAdminToolCatalogItem[]>([]);
|
|
10
|
+
const selected = shallowRef("");
|
|
11
|
+
const loading = shallowRef(false);
|
|
12
|
+
const error = shallowRef("");
|
|
13
|
+
const access = useAccessSnapshot();
|
|
14
|
+
const selectedTool = computed(() =>
|
|
15
|
+
tools.value.find((tool) => tool.key === selected.value),
|
|
16
|
+
);
|
|
17
|
+
let controller: AbortController | undefined;
|
|
18
|
+
async function refresh() {
|
|
19
|
+
controller?.abort();
|
|
20
|
+
const current = new AbortController();
|
|
21
|
+
controller = current;
|
|
22
|
+
tools.value = [];
|
|
23
|
+
loading.value = true;
|
|
24
|
+
error.value = "";
|
|
25
|
+
try {
|
|
26
|
+
const result = await listAdminTools(current.signal);
|
|
27
|
+
if (current.signal.aborted || controller !== current) return;
|
|
28
|
+
tools.value = result;
|
|
29
|
+
if (!result.some((tool) => tool.key === selected.value))
|
|
30
|
+
selected.value = result[0]?.key ?? "";
|
|
31
|
+
} catch (failure) {
|
|
32
|
+
if (!current.signal.aborted)
|
|
33
|
+
error.value =
|
|
34
|
+
failure instanceof Error ? failure.message : "工具目录加载失败";
|
|
35
|
+
} finally {
|
|
36
|
+
if (controller === current) loading.value = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
watch(access.fingerprint, () => {
|
|
40
|
+
controller?.abort();
|
|
41
|
+
tools.value = [];
|
|
42
|
+
selected.value = "";
|
|
43
|
+
error.value = "权限发生变化,请手动刷新目录。";
|
|
44
|
+
});
|
|
45
|
+
onMounted(refresh);
|
|
46
|
+
onBeforeUnmount(() => controller?.abort());
|
|
47
|
+
</script>
|
|
48
|
+
<template>
|
|
49
|
+
<div class="space-y-5">
|
|
50
|
+
<header class="flex items-start justify-between gap-3">
|
|
51
|
+
<div>
|
|
52
|
+
<h1 class="text-2xl font-semibold">工具目录与规则</h1>
|
|
53
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
54
|
+
查看已装配平台与业务工具的注册规则和当前调用条件。
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
<Button variant="outline" :disabled="loading" @click="refresh">{{
|
|
58
|
+
loading ? "加载中…" : "刷新"
|
|
59
|
+
}}</Button>
|
|
60
|
+
</header>
|
|
61
|
+
<p
|
|
62
|
+
v-if="error"
|
|
63
|
+
role="alert"
|
|
64
|
+
class="rounded-md border border-destructive/30 p-4 text-sm text-destructive"
|
|
65
|
+
>
|
|
66
|
+
{{ error }}
|
|
67
|
+
</p>
|
|
68
|
+
<div class="grid items-start gap-4 lg:grid-cols-[22rem_minmax(0,1fr)]">
|
|
69
|
+
<ToolCatalogPanel
|
|
70
|
+
:tools="tools"
|
|
71
|
+
:selected="selected"
|
|
72
|
+
@select="selected = $event"
|
|
73
|
+
/><ToolRuleDetail :tool="selectedTool" />
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, shallowRef } from "vue";
|
|
3
|
+
import { onBeforeRouteLeave, useRoute } from "vue-router";
|
|
4
|
+
import type {
|
|
5
|
+
AgentWorkbenchRecordingSummaryV1,
|
|
6
|
+
JsonValue,
|
|
7
|
+
} from "@southwind-ai/shared";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
|
|
10
|
+
import { useAgentWorkbench } from "../../composables/useAgentWorkbench";
|
|
11
|
+
import TestRunComposer from "./TestRunComposer.vue";
|
|
12
|
+
import WorkbenchSessionList from "./WorkbenchSessionList.vue";
|
|
13
|
+
import SessionReplayTimeline from "./SessionReplayTimeline.vue";
|
|
14
|
+
import DraftLeaveDialog from "./DraftLeaveDialog.vue";
|
|
15
|
+
import { formatJson } from "./catalog-presenter";
|
|
16
|
+
const state = useAgentWorkbench();
|
|
17
|
+
const confirmation = useUnsavedChangesConfirmation();
|
|
18
|
+
const route = useRoute();
|
|
19
|
+
const operationKey = shallowRef("");
|
|
20
|
+
const input = shallowRef("{}");
|
|
21
|
+
const baseline = shallowRef("{}");
|
|
22
|
+
const inputError = shallowRef("");
|
|
23
|
+
const mode = shallowRef<"history" | "test">("history");
|
|
24
|
+
const dirty = computed(() => input.value !== baseline.value);
|
|
25
|
+
function confirmChanges() {
|
|
26
|
+
return dirty.value
|
|
27
|
+
? confirmation.request(async () => {
|
|
28
|
+
input.value = baseline.value;
|
|
29
|
+
return true;
|
|
30
|
+
})
|
|
31
|
+
: Promise.resolve(true);
|
|
32
|
+
}
|
|
33
|
+
async function select(session: AgentWorkbenchRecordingSummaryV1) {
|
|
34
|
+
if (!(await confirmChanges())) return;
|
|
35
|
+
mode.value = "history";
|
|
36
|
+
await state.select(session);
|
|
37
|
+
}
|
|
38
|
+
async function changeOperation(key: string) {
|
|
39
|
+
if (!(await confirmChanges())) return;
|
|
40
|
+
operationKey.value = key;
|
|
41
|
+
input.value = "{}";
|
|
42
|
+
baseline.value = "{}";
|
|
43
|
+
inputError.value = "";
|
|
44
|
+
}
|
|
45
|
+
async function switchMode(next: "history" | "test") {
|
|
46
|
+
if (!(await confirmChanges())) return;
|
|
47
|
+
mode.value = next;
|
|
48
|
+
}
|
|
49
|
+
async function newSession() {
|
|
50
|
+
if (!(await confirmChanges())) return;
|
|
51
|
+
await state.select();
|
|
52
|
+
mode.value = "test";
|
|
53
|
+
}
|
|
54
|
+
async function refresh() {
|
|
55
|
+
if (!(await confirmChanges())) return;
|
|
56
|
+
await state.refresh();
|
|
57
|
+
}
|
|
58
|
+
async function start() {
|
|
59
|
+
inputError.value = "";
|
|
60
|
+
let parsed: JsonValue;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(input.value) as JsonValue;
|
|
63
|
+
} catch {
|
|
64
|
+
inputError.value = "输入必须是有效 JSON,请按 Operation Schema 填写。";
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const operation = state.operations.value.find(
|
|
68
|
+
(operation) => operation.key === operationKey.value,
|
|
69
|
+
);
|
|
70
|
+
if (!operation?.selectable) {
|
|
71
|
+
inputError.value = "当前操作不可调用,请刷新授权状态。";
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
new TextEncoder().encode(input.value).length >
|
|
76
|
+
operation.limits.maxInputBytes
|
|
77
|
+
) {
|
|
78
|
+
inputError.value = "测试输入超出此操作大小限制。";
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (await state.start(operationKey.value, parsed))
|
|
82
|
+
baseline.value = input.value;
|
|
83
|
+
}
|
|
84
|
+
function beforeUnload(event: BeforeUnloadEvent) {
|
|
85
|
+
if (dirty.value) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.returnValue = "";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
onBeforeRouteLeave(confirmChanges);
|
|
91
|
+
onMounted(() => window.addEventListener("beforeunload", beforeUnload));
|
|
92
|
+
onBeforeUnmount(() => window.removeEventListener("beforeunload", beforeUnload));
|
|
93
|
+
// Deep links select a recording only after the user explicitly refreshes / the initial list loads.
|
|
94
|
+
const linkedSession = computed(() =>
|
|
95
|
+
typeof route.query.sessionId === "string" ? route.query.sessionId : "",
|
|
96
|
+
);
|
|
97
|
+
</script>
|
|
98
|
+
<template>
|
|
99
|
+
<section class="space-y-4">
|
|
100
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
101
|
+
<Button
|
|
102
|
+
variant="outline"
|
|
103
|
+
:disabled="state.loading.value"
|
|
104
|
+
@click="refresh"
|
|
105
|
+
>{{ state.loading.value ? "加载中…" : "刷新会话与规则" }}</Button
|
|
106
|
+
><Button
|
|
107
|
+
v-if="state.canManage.value"
|
|
108
|
+
variant="outline"
|
|
109
|
+
:disabled="state.starting.value"
|
|
110
|
+
@click="newSession"
|
|
111
|
+
>新建测试会话</Button
|
|
112
|
+
><span v-if="state.running.value" class="text-xs text-muted-foreground"
|
|
113
|
+
>离开只断开查看,不会自动取消运行。</span
|
|
114
|
+
>
|
|
115
|
+
</div>
|
|
116
|
+
<p
|
|
117
|
+
v-if="state.error.value"
|
|
118
|
+
role="alert"
|
|
119
|
+
class="rounded-md border border-destructive/30 p-4 text-sm text-destructive"
|
|
120
|
+
>
|
|
121
|
+
{{ state.error.value }}
|
|
122
|
+
</p>
|
|
123
|
+
<Button
|
|
124
|
+
v-if="
|
|
125
|
+
linkedSession &&
|
|
126
|
+
!state.selected.value &&
|
|
127
|
+
state.sessions.value.find(
|
|
128
|
+
(session) => session.sessionId === linkedSession,
|
|
129
|
+
)
|
|
130
|
+
"
|
|
131
|
+
variant="outline"
|
|
132
|
+
@click="
|
|
133
|
+
select(
|
|
134
|
+
state.sessions.value.find(
|
|
135
|
+
(session) => session.sessionId === linkedSession,
|
|
136
|
+
)!,
|
|
137
|
+
)
|
|
138
|
+
"
|
|
139
|
+
>打开链接中的会话 {{ linkedSession }}</Button
|
|
140
|
+
>
|
|
141
|
+
<div class="grid items-start gap-4 lg:grid-cols-[20rem_minmax(0,1fr)]">
|
|
142
|
+
<WorkbenchSessionList
|
|
143
|
+
:sessions="state.sessions.value"
|
|
144
|
+
:selected="state.selected.value"
|
|
145
|
+
@select="select"
|
|
146
|
+
/>
|
|
147
|
+
<div class="min-w-0 space-y-4">
|
|
148
|
+
<header class="space-y-2">
|
|
149
|
+
<h2 class="text-lg font-medium">
|
|
150
|
+
{{
|
|
151
|
+
state.selected.value?.title || "选择业务会话,或新建受治理测试"
|
|
152
|
+
}}
|
|
153
|
+
</h2>
|
|
154
|
+
<div class="flex gap-2" role="group" aria-label="工作台模式">
|
|
155
|
+
<Button
|
|
156
|
+
:variant="mode === 'history' ? 'default' : 'outline'"
|
|
157
|
+
:aria-pressed="mode === 'history'"
|
|
158
|
+
@click="switchMode('history')"
|
|
159
|
+
>历史只读回放</Button
|
|
160
|
+
><Button
|
|
161
|
+
v-if="state.canManage.value"
|
|
162
|
+
:variant="mode === 'test' ? 'default' : 'outline'"
|
|
163
|
+
:aria-pressed="mode === 'test'"
|
|
164
|
+
@click="switchMode('test')"
|
|
165
|
+
>另起真实运行</Button
|
|
166
|
+
>
|
|
167
|
+
</div>
|
|
168
|
+
</header>
|
|
169
|
+
<template v-if="mode === 'test'">
|
|
170
|
+
<p
|
|
171
|
+
v-if="state.selected.value?.source === 'business'"
|
|
172
|
+
class="text-sm text-muted-foreground"
|
|
173
|
+
>
|
|
174
|
+
业务历史只读。开始测试会创建独立 Session,并重新计算当前授权。
|
|
175
|
+
</p>
|
|
176
|
+
<TestRunComposer
|
|
177
|
+
:operations="state.operations.value"
|
|
178
|
+
:operation-key="operationKey"
|
|
179
|
+
:input="input"
|
|
180
|
+
:starting="state.starting.value"
|
|
181
|
+
:running="state.running.value"
|
|
182
|
+
:can-manage="state.canManage.value"
|
|
183
|
+
@operation="changeOperation"
|
|
184
|
+
@update:input="input = $event"
|
|
185
|
+
@start="start"
|
|
186
|
+
/>
|
|
187
|
+
<p v-if="inputError" role="alert" class="text-sm text-destructive">
|
|
188
|
+
{{ inputError }}
|
|
189
|
+
</p>
|
|
190
|
+
</template>
|
|
191
|
+
<section
|
|
192
|
+
v-if="state.liveRun.value"
|
|
193
|
+
class="space-y-3 rounded-lg border bg-card p-4"
|
|
194
|
+
aria-label="实时测试运行"
|
|
195
|
+
>
|
|
196
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
197
|
+
<p class="break-all text-sm">
|
|
198
|
+
{{ state.liveRun.value.runId }} · {{ state.liveRun.value.status }}
|
|
199
|
+
</p>
|
|
200
|
+
<Button
|
|
201
|
+
v-if="state.running.value"
|
|
202
|
+
variant="destructive"
|
|
203
|
+
:disabled="state.cancelling.value"
|
|
204
|
+
@click="state.cancel"
|
|
205
|
+
>{{ state.cancelling.value ? "请求取消中…" : "取消运行" }}</Button
|
|
206
|
+
>
|
|
207
|
+
</div>
|
|
208
|
+
<ol class="space-y-2">
|
|
209
|
+
<li
|
|
210
|
+
v-for="event in state.liveEvents.value"
|
|
211
|
+
:key="event.sequence"
|
|
212
|
+
class="rounded-md bg-muted p-2 text-xs"
|
|
213
|
+
>
|
|
214
|
+
#{{ event.sequence }} · {{ event.type }} · {{ event.summary }}
|
|
215
|
+
</li>
|
|
216
|
+
</ol>
|
|
217
|
+
<div v-if="state.output.value !== undefined">
|
|
218
|
+
<h3 class="mb-2 text-sm font-medium">受控最终结果</h3>
|
|
219
|
+
<pre
|
|
220
|
+
class="max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs"
|
|
221
|
+
>{{ formatJson(state.output.value) }}</pre>
|
|
222
|
+
</div>
|
|
223
|
+
</section>
|
|
224
|
+
<SessionReplayTimeline
|
|
225
|
+
:key="`${state.selected.value?.source}:${state.selected.value?.sessionId}`"
|
|
226
|
+
:recording="state.recording.value"
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<DraftLeaveDialog
|
|
231
|
+
:open="confirmation.open.value"
|
|
232
|
+
@cancel="confirmation.cancel"
|
|
233
|
+
@discard="confirmation.save"
|
|
234
|
+
/>
|
|
235
|
+
</section>
|
|
236
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
defineProps<{ open: boolean }>();
|
|
12
|
+
defineEmits<{ cancel: []; discard: [] }>();
|
|
13
|
+
</script>
|
|
14
|
+
<template>
|
|
15
|
+
<Dialog :open="open"
|
|
16
|
+
><DialogContent
|
|
17
|
+
:show-close-button="false"
|
|
18
|
+
@escape-key-down="(event) => event.preventDefault()"
|
|
19
|
+
@interact-outside="(event) => event.preventDefault()"
|
|
20
|
+
><DialogHeader
|
|
21
|
+
><DialogTitle>测试输入尚未提交</DialogTitle
|
|
22
|
+
><DialogDescription
|
|
23
|
+
>切换会话或离开将丢弃当前输入。取消可继续编辑;放弃输入不会创建任何运行。</DialogDescription
|
|
24
|
+
></DialogHeader
|
|
25
|
+
><DialogFooter
|
|
26
|
+
><Button variant="outline" @click="$emit('cancel')"
|
|
27
|
+
>取消并继续编辑</Button
|
|
28
|
+
><Button variant="destructive" @click="$emit('discard')"
|
|
29
|
+
>放弃输入并继续</Button
|
|
30
|
+
></DialogFooter
|
|
31
|
+
></DialogContent
|
|
32
|
+
></Dialog
|
|
33
|
+
>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
AgentWorkbenchEventV1,
|
|
4
|
+
AgentWorkbenchRecordingV1,
|
|
5
|
+
} from "@southwind-ai/shared";
|
|
6
|
+
import { computed } from "vue";
|
|
7
|
+
import { formatJson } from "./catalog-presenter";
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
event?: AgentWorkbenchEventV1;
|
|
10
|
+
recording?: AgentWorkbenchRecordingV1;
|
|
11
|
+
events?: readonly AgentWorkbenchEventV1[];
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
}>();
|
|
14
|
+
defineEmits<{ seek: [index: number] }>();
|
|
15
|
+
const run = computed(() =>
|
|
16
|
+
props.recording?.runs.find((run) => run.runId === props.event?.runId),
|
|
17
|
+
);
|
|
18
|
+
const workflow = computed(() =>
|
|
19
|
+
props.recording?.workflows.find(
|
|
20
|
+
(workflow) =>
|
|
21
|
+
workflow.workflowRunId === props.event?.workflowRunId ||
|
|
22
|
+
Boolean(
|
|
23
|
+
props.event?.runId &&
|
|
24
|
+
workflow.nodes.some((node) => node.agentRunId === props.event?.runId),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
function target(runId?: string, nodeRunId?: string) {
|
|
29
|
+
const events = props.events ?? props.recording?.timeline ?? [];
|
|
30
|
+
if (nodeRunId) {
|
|
31
|
+
const nodeIndex = events.findIndex(
|
|
32
|
+
(event) => event.nodeRunId === nodeRunId,
|
|
33
|
+
);
|
|
34
|
+
if (nodeIndex >= 0) return nodeIndex;
|
|
35
|
+
}
|
|
36
|
+
return runId ? events.findIndex((event) => event.runId === runId) : -1;
|
|
37
|
+
}
|
|
38
|
+
function approvalTarget(workItemId: string) {
|
|
39
|
+
return (props.events ?? []).findIndex(
|
|
40
|
+
(event) => event.workItemId === workItemId,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
function toolTarget(type: string) {
|
|
44
|
+
if (!props.event?.toolCallId) return -1;
|
|
45
|
+
return (props.events ?? []).findIndex(
|
|
46
|
+
(event) =>
|
|
47
|
+
event.toolCallId === props.event?.toolCallId && event.type === type,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
<template>
|
|
52
|
+
<section
|
|
53
|
+
class="min-w-0 space-y-3 rounded-lg border bg-card p-4"
|
|
54
|
+
aria-label="调用关联详情"
|
|
55
|
+
>
|
|
56
|
+
<h3 class="font-medium">调用与关联结果</h3>
|
|
57
|
+
<p v-if="!event" class="text-sm text-muted-foreground">
|
|
58
|
+
选择事件查看模型、工具、工作流与结果关联。
|
|
59
|
+
</p>
|
|
60
|
+
<template v-else>
|
|
61
|
+
<p class="text-sm">{{ event.summary }}</p>
|
|
62
|
+
<dl class="grid grid-cols-[6rem_minmax(0,1fr)] gap-2 text-xs">
|
|
63
|
+
<dt>所属 Session</dt>
|
|
64
|
+
<dd class="break-all">{{ run?.sessionId || sessionId || "未记录" }}</dd>
|
|
65
|
+
<dt>事件</dt>
|
|
66
|
+
<dd>{{ event.sequence }} · {{ event.type }}</dd>
|
|
67
|
+
<dt>Turn / Step</dt>
|
|
68
|
+
<dd class="break-all">
|
|
69
|
+
{{ event.turnId || "未记录" }} / {{ event.stepId || "未记录" }}
|
|
70
|
+
</dd>
|
|
71
|
+
<dt>Run</dt>
|
|
72
|
+
<dd class="break-all">{{ event.runId || "未关联" }}</dd>
|
|
73
|
+
<dt>模型 Attempt</dt>
|
|
74
|
+
<dd class="break-all">
|
|
75
|
+
{{ event.modelAttemptId || "未记录"
|
|
76
|
+
}}<span v-if="event.attemptSequence">
|
|
77
|
+
· 第 {{ event.attemptSequence }} 次</span
|
|
78
|
+
>
|
|
79
|
+
</dd>
|
|
80
|
+
<dt>Tool</dt>
|
|
81
|
+
<dd class="break-all">
|
|
82
|
+
{{ event.toolKey || "未关联" }} · {{ event.toolCallId || "—" }}
|
|
83
|
+
</dd>
|
|
84
|
+
<dt>审批 / 产物</dt>
|
|
85
|
+
<dd class="break-all">
|
|
86
|
+
{{ event.workItemId || "—" }} / {{ event.artifactId || "—" }}
|
|
87
|
+
</dd>
|
|
88
|
+
<dt>结果状态</dt>
|
|
89
|
+
<dd>{{ event.outcome || run?.status || "未记录" }}</dd>
|
|
90
|
+
<template v-if="event.usage"
|
|
91
|
+
><dt>Usage</dt>
|
|
92
|
+
<dd>
|
|
93
|
+
输入 {{ event.usage.inputTokens ?? "未记录" }} / 输出
|
|
94
|
+
{{ event.usage.outputTokens ?? "未记录" }} tokens;费用
|
|
95
|
+
{{ event.usage.costMicros ?? "未记录" }} micros
|
|
96
|
+
</dd></template
|
|
97
|
+
>
|
|
98
|
+
</dl>
|
|
99
|
+
<div v-if="event.toolCallId" class="flex gap-3 text-xs">
|
|
100
|
+
<button
|
|
101
|
+
v-if="toolTarget('tool.requested') >= 0"
|
|
102
|
+
type="button"
|
|
103
|
+
class="text-primary underline"
|
|
104
|
+
@click="$emit('seek', toolTarget('tool.requested'))"
|
|
105
|
+
>
|
|
106
|
+
定位工具请求
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
v-if="toolTarget('tool.completed') >= 0"
|
|
110
|
+
type="button"
|
|
111
|
+
class="text-primary underline"
|
|
112
|
+
@click="$emit('seek', toolTarget('tool.completed'))"
|
|
113
|
+
>
|
|
114
|
+
定位工具结果
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
<div v-if="run">
|
|
118
|
+
<p class="text-sm">{{ run.operationKey }} · {{ run.status }}</p>
|
|
119
|
+
<p
|
|
120
|
+
v-if="run.correlationStatus !== 'complete'"
|
|
121
|
+
class="text-xs text-muted-foreground"
|
|
122
|
+
>
|
|
123
|
+
部分运行关联:{{ run.missing.join("、") }}
|
|
124
|
+
</p>
|
|
125
|
+
<pre
|
|
126
|
+
v-if="run.result?.output !== undefined"
|
|
127
|
+
class="max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs"
|
|
128
|
+
>{{ formatJson(run.result.output) }}</pre>
|
|
129
|
+
</div>
|
|
130
|
+
<div v-if="workflow" class="space-y-2 border-t pt-3">
|
|
131
|
+
<h4 class="text-sm font-medium">
|
|
132
|
+
工作流 {{ workflow.title }} · {{ workflow.status }}
|
|
133
|
+
</h4>
|
|
134
|
+
<div class="space-y-2">
|
|
135
|
+
<div
|
|
136
|
+
v-for="node in workflow.nodes"
|
|
137
|
+
:key="node.nodeRunId"
|
|
138
|
+
class="rounded-md border p-2 text-xs"
|
|
139
|
+
>
|
|
140
|
+
<p>
|
|
141
|
+
{{ node.nodeId }} · {{ node.type }} · {{ node.status }} · 尝试
|
|
142
|
+
{{ node.attempt }}
|
|
143
|
+
</p>
|
|
144
|
+
<p v-if="node.workItemId" class="break-all">
|
|
145
|
+
审批任务:{{ node.workItemId }}
|
|
146
|
+
</p>
|
|
147
|
+
<button
|
|
148
|
+
v-if="node.workItemId && approvalTarget(node.workItemId) >= 0"
|
|
149
|
+
type="button"
|
|
150
|
+
class="mt-1 mr-3 text-primary underline"
|
|
151
|
+
@click="$emit('seek', approvalTarget(node.workItemId))"
|
|
152
|
+
>
|
|
153
|
+
定位审批记录
|
|
154
|
+
</button>
|
|
155
|
+
<p v-if="node.selectedBranch">分支:{{ node.selectedBranch }}</p>
|
|
156
|
+
<p v-if="node.artifactIds.length">
|
|
157
|
+
产物:{{ node.artifactIds.join("、") }}
|
|
158
|
+
</p>
|
|
159
|
+
<button
|
|
160
|
+
v-if="target(node.agentRunId, node.nodeRunId) >= 0"
|
|
161
|
+
type="button"
|
|
162
|
+
class="mt-1 text-primary underline"
|
|
163
|
+
@click="$emit('seek', target(node.agentRunId, node.nodeRunId))"
|
|
164
|
+
>
|
|
165
|
+
定位节点 / Run 的记录
|
|
166
|
+
</button>
|
|
167
|
+
<p v-else class="mt-1 text-muted-foreground">
|
|
168
|
+
缺少此节点的可回放事件
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
</section>
|
|
175
|
+
</template>
|