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.
Files changed (95) hide show
  1. package/dist/cli.js +52 -34
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/src/southwind_agent_server/attempt_context.py +9 -1
  5. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +5 -2
  6. package/template/apps/agent-server/src/southwind_agent_server/provider_output_guidance.py +34 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +16 -5
  8. package/template/apps/agent-server/tests/test_structured_logging.py +37 -0
  9. package/template/apps/agent-server/tests/test_tool_structured_output.py +347 -0
  10. package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
  11. package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
  12. package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
  13. package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
  14. package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
  15. package/template/apps/server/src/agent/operation-catalog.ts +135 -0
  16. package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
  17. package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
  18. package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
  19. package/template/apps/server/src/agent/run-route-response.ts +35 -0
  20. package/template/apps/server/src/agent/run-routes.ts +46 -43
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
  22. package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
  23. package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
  24. package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
  25. package/template/apps/server/src/agent/session-projection-view.ts +19 -16
  26. package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
  27. package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
  28. package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
  29. package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
  30. package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
  31. package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
  32. package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
  33. package/template/apps/server/src/agent/tool-catalog.ts +44 -0
  34. package/template/apps/server/src/agent/tool-registry.ts +45 -14
  35. package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
  36. package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
  37. package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
  38. package/template/apps/server/src/agent/workbench-facade.ts +339 -0
  39. package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
  40. package/template/apps/server/src/agent/workbench-routes.ts +97 -0
  41. package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
  42. package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
  43. package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
  44. package/template/apps/server/src/agent/workbench-sse.ts +50 -0
  45. package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
  46. package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
  47. package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
  48. package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
  49. package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
  50. package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
  51. package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
  52. package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
  53. package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
  54. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
  55. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
  56. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
  57. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
  58. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
  59. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
  60. package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
  61. package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
  62. package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
  63. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
  64. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
  65. package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
  66. package/template/apps/web/src/router/index.ts +4 -0
  67. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  68. package/template/apps/web/src/services/agent-admin-api.ts +155 -0
  69. package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
  70. package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
  71. package/template/docs/architecture/agent-harness.md +4 -0
  72. package/template/docs/architecture/log-governance-implementation.md +20 -3
  73. package/template/docs/architecture/log-governance-plan.md +2 -2
  74. package/template/docs/platform/agent-runtime.md +18 -0
  75. package/template/packages/modules/src/ai-operation-composition.ts +9 -0
  76. package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
  77. package/template/packages/modules/src/manifest.ts +2 -0
  78. package/template/packages/modules/src/system-admin-routes.ts +16 -0
  79. package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
  80. package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
  81. package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
  82. package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
  83. package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
  84. package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
  85. package/template/packages/modules/src/system-audit-actions.ts +25 -0
  86. package/template/packages/modules/src/system-features.ts +19 -0
  87. package/template/packages/modules/src/system-module-catalog.ts +14 -12
  88. package/template/packages/modules/src/system-module-ownership.ts +12 -0
  89. package/template/packages/modules/src/system-modules.ts +2 -0
  90. package/template/packages/modules/src/system-permissions.ts +5 -0
  91. package/template/packages/server-sdk/index.ts +2 -0
  92. package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
  93. package/template/packages/shared/index.ts +2 -0
  94. package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
  95. package/template/packages/shared/src/agent-workbench.ts +100 -0
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import type { AgentAdminToolCatalogItem } from "@southwind-ai/shared";
3
+ import { availabilityLabel, formatJson } from "./catalog-presenter";
4
+ defineProps<{ tool?: AgentAdminToolCatalogItem }>();
5
+ </script>
6
+ <template>
7
+ <section
8
+ class="min-w-0 rounded-lg border bg-card p-5"
9
+ aria-label="工具规则详情"
10
+ >
11
+ <p v-if="!tool" class="text-sm text-muted-foreground">
12
+ 选择工具查看规则与 Schema。
13
+ </p>
14
+ <template v-else>
15
+ <h2 class="text-lg font-semibold">{{ tool.label }}</h2>
16
+ <p class="mt-1 break-all font-mono text-xs text-muted-foreground">
17
+ {{ tool.key }}
18
+ </p>
19
+ <p class="mt-3 text-sm">{{ tool.description }}</p>
20
+ <p class="mt-3 rounded-md bg-muted p-3 text-sm">
21
+ {{ availabilityLabel(tool.availabilityReason) }}
22
+ </p>
23
+ <dl class="my-5 grid grid-cols-[7rem_minmax(0,1fr)] gap-2 text-sm">
24
+ <dt class="text-muted-foreground">所属模块</dt>
25
+ <dd>{{ tool.module }}</dd>
26
+ <dt class="text-muted-foreground">Feature / 权限</dt>
27
+ <dd class="break-all">
28
+ {{ tool.featureKey }} / {{ tool.permissionKey }}
29
+ </dd>
30
+ <dt class="text-muted-foreground">Scope / 执行</dt>
31
+ <dd>{{ tool.scopeMode }} / {{ tool.executionMode }}</dd>
32
+ <dt class="text-muted-foreground">动作 / 分类</dt>
33
+ <dd>{{ tool.operation }} / {{ tool.dataClassification }}</dd>
34
+ <dt class="text-muted-foreground">审批</dt>
35
+ <dd>
36
+ {{ tool.approval === "required" ? "需要审批" : "不需要审批"
37
+ }}<span v-if="tool.approvalRoles?.length">
38
+ ·
39
+ {{
40
+ tool.approvalRoles
41
+ .map((role) => role.name ?? role.code)
42
+ .join("、")
43
+ }}</span
44
+ ><span v-if="tool.approvalTimeoutSeconds">
45
+ · {{ tool.approvalTimeoutSeconds }} 秒</span
46
+ >
47
+ </dd>
48
+ <dt class="text-muted-foreground">幂等策略</dt>
49
+ <dd>{{ tool.idempotency }}</dd>
50
+ <dt class="text-muted-foreground">执行限制</dt>
51
+ <dd>
52
+ {{ tool.limits.maxDurationSeconds }} 秒;输入
53
+ {{ tool.limits.maxInputBytes }} 字节;输出
54
+ {{ tool.limits.maxOutputBytes }} 字节
55
+ </dd>
56
+ <dt class="text-muted-foreground">审计动作</dt>
57
+ <dd class="break-all">{{ tool.auditAction }}</dd>
58
+ <dt class="text-muted-foreground">允许的 Operations</dt>
59
+ <dd class="break-all">
60
+ {{ tool.allowedOperationKeys.join("、") || "没有已注册操作允许调用" }}
61
+ </dd>
62
+ </dl>
63
+ <details open class="mb-3">
64
+ <summary class="cursor-pointer text-sm font-medium">
65
+ 输入 Schema · {{ tool.inputSchemaKey }}
66
+ </summary>
67
+ <pre
68
+ class="mt-2 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs"
69
+ >{{ formatJson(tool.inputSchema) }}</pre>
70
+ </details>
71
+ <details open>
72
+ <summary class="cursor-pointer text-sm font-medium">
73
+ 输出 Schema · {{ tool.outputSchemaKey }}
74
+ </summary>
75
+ <pre
76
+ class="mt-2 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs"
77
+ >{{ formatJson(tool.outputSchema) }}</pre>
78
+ </details>
79
+ </template>
80
+ </section>
81
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { computed, shallowRef } from "vue";
3
+ import type { AgentWorkbenchRecordingSummaryV1 } from "@southwind-ai/shared";
4
+ import { Input } from "@/components/ui/input";
5
+ const props = defineProps<{
6
+ sessions: readonly AgentWorkbenchRecordingSummaryV1[];
7
+ selected?: AgentWorkbenchRecordingSummaryV1;
8
+ }>();
9
+ defineEmits<{ select: [session: AgentWorkbenchRecordingSummaryV1] }>();
10
+ const query = shallowRef("");
11
+ const filtered = computed(() =>
12
+ props.sessions.filter((session) =>
13
+ `${session.title} ${session.sessionId} ${session.operationKey ?? ""}`
14
+ .toLocaleLowerCase()
15
+ .includes(query.value.toLocaleLowerCase()),
16
+ ),
17
+ );
18
+ </script>
19
+ <template>
20
+ <aside
21
+ class="space-y-3 rounded-lg border bg-card p-4"
22
+ aria-label="业务与测试会话"
23
+ >
24
+ <h2 class="font-medium">会话记录</h2>
25
+ <Input
26
+ v-model="query"
27
+ aria-label="搜索会话"
28
+ placeholder="名称、Session 或 Operation"
29
+ />
30
+ <div class="max-h-[65vh] space-y-2 overflow-y-auto">
31
+ <button
32
+ v-for="session in filtered"
33
+ :key="`${session.source}:${session.sessionId}`"
34
+ type="button"
35
+ :aria-pressed="
36
+ selected?.sessionId === session.sessionId &&
37
+ selected?.source === session.source
38
+ "
39
+ class="w-full rounded-md border p-3 text-left hover:bg-muted aria-pressed:border-primary aria-pressed:bg-primary/5"
40
+ @click="$emit('select', session)"
41
+ >
42
+ <span class="block text-sm font-medium">{{ session.title }}</span
43
+ ><span class="block break-all text-xs text-muted-foreground"
44
+ >{{
45
+ session.source === "business" ? "业务会话" : "Session 事件日志"
46
+ }}
47
+ · {{ session.sessionId }}</span
48
+ ><span class="block text-xs text-muted-foreground">{{
49
+ session.updatedAt
50
+ }}</span>
51
+ </button>
52
+ <p v-if="!filtered.length" class="py-5 text-sm text-muted-foreground">
53
+ 暂无当前授权可查看的会话。
54
+ </p>
55
+ </div>
56
+ </aside>
57
+ </template>
@@ -0,0 +1,20 @@
1
+ import type { AgentAdminAvailabilityReason } from "@southwind-ai/shared";
2
+ const reasons: Record<AgentAdminAvailabilityReason, string> = {
3
+ "internal-operation": "仅供服务端内部编排",
4
+ allowed: "当前具备调用条件(仍需运行时授权与审批)",
5
+ "permission-denied": "缺少执行权限",
6
+ "feature-unavailable": "功能未启用",
7
+ "license-unavailable": "许可不可用",
8
+ "scope-unavailable": "当前数据范围不满足要求",
9
+ "runtime-unavailable": "运行服务不可用",
10
+ "workflow-only": "仅能通过受治理工作流调用",
11
+ "unsupported-operation": "操作类型不支持直接调试",
12
+ "handler-unavailable": "工具实现未装配",
13
+ "governance-unavailable": "治理服务未就绪",
14
+ };
15
+ export function availabilityLabel(reason: AgentAdminAvailabilityReason) {
16
+ return reasons[reason];
17
+ }
18
+ export function formatJson(value: unknown) {
19
+ return JSON.stringify(value, null, 2);
20
+ }
@@ -0,0 +1,94 @@
1
+ import type {
2
+ AgentWorkbenchEventV1,
3
+ AgentWorkbenchRecordingV1,
4
+ } from "@southwind-ai/shared";
5
+ export interface ReplayEntry {
6
+ readonly event: AgentWorkbenchEventV1;
7
+ readonly ledger: string;
8
+ readonly sessionId?: string;
9
+ }
10
+ /** Ledger-local sequence is retained. Expanded children do not imply a global clock. */
11
+ export function replayEntries(
12
+ recording?: AgentWorkbenchRecordingV1,
13
+ ): readonly ReplayEntry[] {
14
+ if (!recording) return [];
15
+ const result: ReplayEntry[] = [];
16
+ const runs = new Set<string>();
17
+ const workflows = new Set<string>();
18
+ const segments = recording.segments ?? [
19
+ {
20
+ sessionId: recording.sessionId,
21
+ throughSequence: recording.timeline.at(-1)?.sequence ?? 0,
22
+ timeline: recording.timeline,
23
+ },
24
+ ];
25
+ for (const segment of segments) {
26
+ for (const event of segment.timeline) {
27
+ result.push({
28
+ event,
29
+ ledger: `Session ${segment.sessionId}`,
30
+ sessionId: segment.sessionId,
31
+ });
32
+ if (event.runId && !runs.has(event.runId)) {
33
+ const run = recording.runs.find(
34
+ (run) =>
35
+ run.runId === event.runId &&
36
+ (!run.sessionId || run.sessionId === segment.sessionId),
37
+ );
38
+ if (run) {
39
+ runs.add(run.runId);
40
+ result.push(
41
+ ...run.events.map((event) => ({
42
+ event,
43
+ ledger: `Run ${run.runId}`,
44
+ sessionId: run.sessionId ?? segment.sessionId,
45
+ })),
46
+ );
47
+ }
48
+ }
49
+ for (const workflow of recording.workflows) {
50
+ if (
51
+ workflows.has(workflow.workflowRunId) ||
52
+ (workflow.sessionId && workflow.sessionId !== segment.sessionId)
53
+ )
54
+ continue;
55
+ if (
56
+ workflow.workflowRunId !== event.workflowRunId &&
57
+ (!event.runId || workflow.parentRunId !== event.runId) &&
58
+ !workflow.nodes.some(
59
+ (node) => node.agentRunId && node.agentRunId === event.runId,
60
+ )
61
+ )
62
+ continue;
63
+ workflows.add(workflow.workflowRunId);
64
+ result.push(
65
+ ...workflow.events.map((event) => ({
66
+ event,
67
+ ledger: `Workflow ${workflow.workflowRunId}`,
68
+ sessionId: workflow.sessionId ?? segment.sessionId,
69
+ })),
70
+ );
71
+ }
72
+ }
73
+ }
74
+ // Unlinked facts remain explicitly separate; they are never assigned invented Turn IDs.
75
+ for (const run of recording.runs)
76
+ if (!runs.has(run.runId))
77
+ result.push(
78
+ ...run.events.map((event) => ({
79
+ event,
80
+ ledger: `独立 Run ${run.runId}(Session 关联缺失)`,
81
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
82
+ })),
83
+ );
84
+ for (const workflow of recording.workflows)
85
+ if (!workflows.has(workflow.workflowRunId))
86
+ result.push(
87
+ ...workflow.events.map((event) => ({
88
+ event,
89
+ ledger: `独立 Workflow ${workflow.workflowRunId}(Session 关联缺失)`,
90
+ ...(workflow.sessionId ? { sessionId: workflow.sessionId } : {}),
91
+ })),
92
+ );
93
+ return result;
94
+ }
@@ -0,0 +1,298 @@
1
+ import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
2
+ import type {
3
+ AgentAdminOperationCatalogItem,
4
+ AgentWorkbenchEventV1,
5
+ AgentWorkbenchRecordingSummaryV1,
6
+ AgentWorkbenchRecordingV1,
7
+ JsonValue,
8
+ } from "@southwind-ai/shared";
9
+ import { useAuthSession } from "@/composables/useAuthSession";
10
+ import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
11
+ import {
12
+ cancelTestRun,
13
+ createTestRun,
14
+ createTestSession,
15
+ getTestRunResult,
16
+ getWorkbenchRecording,
17
+ listAdminOperations,
18
+ listWorkbenchRecordings,
19
+ streamTestRun,
20
+ } from "@/services/agent-admin-api";
21
+ export function useAgentWorkbench() {
22
+ const access = useAccessSnapshot();
23
+ const auth = useAuthSession();
24
+ const sessions = shallowRef<readonly AgentWorkbenchRecordingSummaryV1[]>([]);
25
+ const operations = shallowRef<readonly AgentAdminOperationCatalogItem[]>([]);
26
+ const selected = shallowRef<AgentWorkbenchRecordingSummaryV1>();
27
+ const recording = shallowRef<AgentWorkbenchRecordingV1>();
28
+ const liveEvents = shallowRef<readonly AgentWorkbenchEventV1[]>([]);
29
+ const liveRun = shallowRef<{
30
+ runId: string;
31
+ operationKey: string;
32
+ sessionId: string;
33
+ status: string;
34
+ }>();
35
+ const output = shallowRef<JsonValue>();
36
+ const error = shallowRef("");
37
+ const loading = shallowRef(false);
38
+ const starting = shallowRef(false);
39
+ const cancelling = shallowRef(false);
40
+ let listController: AbortController | undefined;
41
+ let detailController: AbortController | undefined;
42
+ let runController: AbortController | undefined;
43
+ let cancelController: AbortController | undefined;
44
+ const testSessions = new Set<string>();
45
+ const canManage = computed(() =>
46
+ Boolean(
47
+ access.snapshot.value?.actor.permissionKeys.includes(
48
+ "system.agent-debug.manage",
49
+ ),
50
+ ),
51
+ );
52
+ const running = computed(() =>
53
+ Boolean(
54
+ liveRun.value &&
55
+ !["completed", "failed", "cancelled"].includes(liveRun.value.status),
56
+ ),
57
+ );
58
+ function clear() {
59
+ listController?.abort();
60
+ detailController?.abort();
61
+ runController?.abort();
62
+ cancelController?.abort();
63
+ listController = undefined;
64
+ detailController = undefined;
65
+ runController = undefined;
66
+ cancelController = undefined;
67
+ testSessions.clear();
68
+ loading.value = false;
69
+ starting.value = false;
70
+ cancelling.value = false;
71
+ sessions.value = [];
72
+ operations.value = [];
73
+ selected.value = undefined;
74
+ recording.value = undefined;
75
+ liveEvents.value = [];
76
+ liveRun.value = undefined;
77
+ output.value = undefined;
78
+ }
79
+ watch([access.fingerprint, () => auth.actor.value?.id], () => {
80
+ clear();
81
+ error.value = "当前身份或授权发生变化,旧记录已隐藏,请手动刷新。";
82
+ });
83
+ onMounted(refresh);
84
+ onBeforeUnmount(() => {
85
+ listController?.abort();
86
+ detailController?.abort();
87
+ runController?.abort();
88
+ cancelController?.abort();
89
+ });
90
+ async function refresh() {
91
+ listController?.abort();
92
+ const controller = new AbortController();
93
+ listController = controller;
94
+ error.value = "";
95
+ loading.value = true;
96
+ const results = await Promise.allSettled([
97
+ listWorkbenchRecordings("harness", controller.signal),
98
+ listWorkbenchRecordings("business", controller.signal),
99
+ listAdminOperations(controller.signal),
100
+ ]);
101
+ if (controller.signal.aborted || listController !== controller) return;
102
+ const [harness, business, operation] = results;
103
+ sessions.value = [
104
+ ...(harness.status === "fulfilled" ? harness.value.items : []),
105
+ ...(business.status === "fulfilled" ? business.value.items : []),
106
+ ];
107
+ operations.value = operation.status === "fulfilled" ? operation.value : [];
108
+ const failures = results
109
+ .filter((result) => result.status === "rejected")
110
+ .map((result) => message(result.reason));
111
+ if (failures.length) error.value = failures.join(";");
112
+ loading.value = false;
113
+ if (selected.value) await select(selected.value);
114
+ }
115
+ async function select(session?: AgentWorkbenchRecordingSummaryV1) {
116
+ detailController?.abort();
117
+ const preserveLive = Boolean(
118
+ session &&
119
+ session.source === "harness" &&
120
+ liveRun.value?.sessionId === session.sessionId,
121
+ );
122
+ if (!preserveLive) {
123
+ runController?.abort();
124
+ cancelController?.abort();
125
+ runController = undefined;
126
+ cancelController = undefined;
127
+ starting.value = false;
128
+ cancelling.value = false;
129
+ liveEvents.value = [];
130
+ liveRun.value = undefined;
131
+ output.value = undefined;
132
+ }
133
+ selected.value = session;
134
+ recording.value = undefined;
135
+ error.value = "";
136
+ if (!session) return;
137
+ const controller = new AbortController();
138
+ detailController = controller;
139
+ loading.value = true;
140
+ try {
141
+ const result = await getWorkbenchRecording(
142
+ session.sessionId,
143
+ session.source,
144
+ controller.signal,
145
+ );
146
+ if (!controller.signal.aborted && detailController === controller)
147
+ recording.value = result;
148
+ } catch (failure) {
149
+ if (!controller.signal.aborted) error.value = message(failure);
150
+ } finally {
151
+ if (detailController === controller) loading.value = false;
152
+ }
153
+ }
154
+ async function start(
155
+ operationKey: string,
156
+ input: JsonValue,
157
+ ): Promise<boolean> {
158
+ if (!canManage.value || starting.value || running.value) return false;
159
+ starting.value = true;
160
+ error.value = "";
161
+ runController?.abort();
162
+ const controller = new AbortController();
163
+ runController = controller;
164
+ try {
165
+ let session = selected.value;
166
+ if (
167
+ !session ||
168
+ session.source !== "harness" ||
169
+ !testSessions.has(session.sessionId)
170
+ ) {
171
+ const created = await createTestSession(controller.signal);
172
+ if (controller.signal.aborted || runController !== controller)
173
+ return false;
174
+ testSessions.add(created.sessionId);
175
+ session = {
176
+ source: "harness",
177
+ sessionId: created.sessionId,
178
+ title: "调试测试会话",
179
+ updatedAt: new Date().toISOString(),
180
+ };
181
+ selected.value = session;
182
+ recording.value = undefined;
183
+ sessions.value = [session, ...sessions.value];
184
+ }
185
+ const created = await createTestRun(
186
+ operationKey,
187
+ input,
188
+ session.sessionId,
189
+ controller.signal,
190
+ );
191
+ if (controller.signal.aborted || runController !== controller)
192
+ return false;
193
+ liveEvents.value = [];
194
+ output.value = undefined;
195
+ liveRun.value = {
196
+ ...created,
197
+ operationKey,
198
+ sessionId: session.sessionId,
199
+ };
200
+ void follow(liveRun.value, controller);
201
+ return true;
202
+ } catch (failure) {
203
+ if (!controller.signal.aborted) error.value = message(failure);
204
+ return false;
205
+ } finally {
206
+ if (runController === controller) starting.value = false;
207
+ }
208
+ }
209
+ async function follow(
210
+ run: NonNullable<typeof liveRun.value>,
211
+ controller: AbortController,
212
+ ) {
213
+ try {
214
+ await streamTestRun(
215
+ run.runId,
216
+ run.operationKey,
217
+ run.sessionId,
218
+ (event) => {
219
+ if (controller.signal.aborted || runController !== controller) return;
220
+ if (
221
+ !liveEvents.value.some(
222
+ (current) => current.sequence === event.sequence,
223
+ )
224
+ )
225
+ liveEvents.value = [...liveEvents.value, event];
226
+ const status = event.type.startsWith("run.")
227
+ ? event.type.slice(4)
228
+ : undefined;
229
+ if (status && ["completed", "failed", "cancelled"].includes(status))
230
+ liveRun.value = { ...run, status };
231
+ },
232
+ controller.signal,
233
+ );
234
+ if (controller.signal.aborted || runController !== controller) return;
235
+ const result = await getTestRunResult(
236
+ run.runId,
237
+ run.operationKey,
238
+ run.sessionId,
239
+ controller.signal,
240
+ );
241
+ if (controller.signal.aborted || runController !== controller) return;
242
+ output.value = result.output;
243
+ liveRun.value = { ...run, status: result.status };
244
+ const history = await getWorkbenchRecording(
245
+ run.sessionId,
246
+ "harness",
247
+ controller.signal,
248
+ );
249
+ if (!controller.signal.aborted) recording.value = history;
250
+ } catch (failure) {
251
+ if (!controller.signal.aborted)
252
+ error.value = `${message(failure)}。运行未自动重试,请手动刷新会话。`;
253
+ }
254
+ }
255
+ async function cancel() {
256
+ const run = liveRun.value;
257
+ if (!run || cancelling.value) return;
258
+ cancelling.value = true;
259
+ cancelController?.abort();
260
+ const controller = new AbortController();
261
+ cancelController = controller;
262
+ try {
263
+ await cancelTestRun(
264
+ run.runId,
265
+ run.operationKey,
266
+ run.sessionId,
267
+ controller.signal,
268
+ );
269
+ } catch (failure) {
270
+ if (!controller.signal.aborted && cancelController === controller)
271
+ error.value = message(failure);
272
+ } finally {
273
+ if (cancelController === controller) cancelling.value = false;
274
+ }
275
+ }
276
+ return {
277
+ sessions,
278
+ operations,
279
+ selected,
280
+ recording,
281
+ liveEvents,
282
+ liveRun,
283
+ output,
284
+ error,
285
+ loading,
286
+ starting,
287
+ cancelling,
288
+ running,
289
+ canManage,
290
+ refresh,
291
+ select,
292
+ start,
293
+ cancel,
294
+ };
295
+ }
296
+ function message(error: unknown) {
297
+ return error instanceof Error ? error.message : "会话服务不可用";
298
+ }