create-windy 0.8.2 → 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 +18 -1
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- 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/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>
|