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.
Files changed (86) hide show
  1. package/dist/cli.js +18 -1
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
  5. package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
  6. package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
  7. package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
  8. package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
  9. package/template/apps/server/src/agent/operation-catalog.ts +135 -0
  10. package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
  11. package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
  12. package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
  13. package/template/apps/server/src/agent/run-route-response.ts +35 -0
  14. package/template/apps/server/src/agent/run-routes.ts +46 -43
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
  16. package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
  17. package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
  18. package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
  19. package/template/apps/server/src/agent/session-projection-view.ts +19 -16
  20. package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
  21. package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
  22. package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
  23. package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
  24. package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
  25. package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
  26. package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
  27. package/template/apps/server/src/agent/tool-catalog.ts +44 -0
  28. package/template/apps/server/src/agent/tool-registry.ts +45 -14
  29. package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
  30. package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
  31. package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
  32. package/template/apps/server/src/agent/workbench-facade.ts +339 -0
  33. package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
  34. package/template/apps/server/src/agent/workbench-routes.ts +97 -0
  35. package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
  36. package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
  37. package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
  38. package/template/apps/server/src/agent/workbench-sse.ts +50 -0
  39. package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
  40. package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
  41. package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
  42. package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
  43. package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
  44. package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
  45. package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
  46. package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
  47. package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
  48. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
  49. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
  50. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
  51. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
  52. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
  53. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
  54. package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
  55. package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
  56. package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
  57. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
  58. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
  59. package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
  60. package/template/apps/web/src/router/index.ts +4 -0
  61. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  62. package/template/apps/web/src/services/agent-admin-api.ts +155 -0
  63. package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
  64. package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
  65. package/template/docs/architecture/agent-harness.md +4 -0
  66. package/template/packages/modules/src/ai-operation-composition.ts +9 -0
  67. package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
  68. package/template/packages/modules/src/manifest.ts +2 -0
  69. package/template/packages/modules/src/system-admin-routes.ts +16 -0
  70. package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
  71. package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
  72. package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
  73. package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
  74. package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
  75. package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
  76. package/template/packages/modules/src/system-audit-actions.ts +25 -0
  77. package/template/packages/modules/src/system-features.ts +19 -0
  78. package/template/packages/modules/src/system-module-catalog.ts +14 -12
  79. package/template/packages/modules/src/system-module-ownership.ts +12 -0
  80. package/template/packages/modules/src/system-modules.ts +2 -0
  81. package/template/packages/modules/src/system-permissions.ts +5 -0
  82. package/template/packages/server-sdk/index.ts +2 -0
  83. package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
  84. package/template/packages/shared/index.ts +2 -0
  85. package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
  86. 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
- <Button
34
- variant="outline"
35
- :disabled="refreshing"
36
- @click="sessionState.refresh"
37
- >
38
- <RefreshCwIcon data-icon="inline-start" />
39
- 刷新
40
- </Button>
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>