diff --git a/web/hwlab-cloud-web/scripts/dist-contract.test.ts b/web/hwlab-cloud-web/scripts/dist-contract.test.ts index bdb51621..3c730f83 100644 --- a/web/hwlab-cloud-web/scripts/dist-contract.test.ts +++ b/web/hwlab-cloud-web/scripts/dist-contract.test.ts @@ -7,7 +7,9 @@ import { fileURLToPath } from "node:url"; import { assertCloudWebDistFresh, buildCloudWebDist, cloudWebDistAliasFiles, inspectCloudWebDistFreshness } from "./dist-contract.ts"; -test("Cloud Web dist build emits Vue assets and route aliases", async () => { +const DIST_CONTRACT_TEST_TIMEOUT_MS = 60_000; + +test("Cloud Web dist build emits Vue assets and route aliases", { timeout: DIST_CONTRACT_TEST_TIMEOUT_MS }, async () => { const rootDir = makeCloudWebFixture(); try { const distDir = await buildCloudWebDist(rootDir); @@ -34,7 +36,7 @@ test("Cloud Web dist build emits Vue assets and route aliases", async () => { } }); -test("Cloud Web dist freshness reports stale generated app asset", async () => { +test("Cloud Web dist freshness reports stale generated app asset", { timeout: DIST_CONTRACT_TEST_TIMEOUT_MS }, async () => { const rootDir = makeCloudWebFixture(); try { await buildCloudWebDist(rootDir); diff --git a/web/hwlab-cloud-web/scripts/workbench-provider-profile.test.ts b/web/hwlab-cloud-web/scripts/workbench-provider-profile.test.ts new file mode 100644 index 00000000..d261011c --- /dev/null +++ b/web/hwlab-cloud-web/scripts/workbench-provider-profile.test.ts @@ -0,0 +1,14 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { providerProfileOptionsFromPayload } from "../src/stores/workbench-session.ts"; + +test("Workbench provider catalog marks missing selected profile as unavailable", () => { + const options = providerProfileOptionsFromPayload({ profiles: [{ profile: "dsflash-go", label: "DeepSeek V4 Flash", configured: true }] }, "codex"); + + const available = options.find((option) => option.value === "dsflash-go"); + const missingSelected = options.find((option) => option.value === "codex"); + + assert.equal(available?.configured, true); + assert.equal(missingSelected?.configured, false); +}); diff --git a/web/hwlab-cloud-web/src/api/workbench.ts b/web/hwlab-cloud-web/src/api/workbench.ts index 6b5a78f2..61535ae8 100644 --- a/web/hwlab-cloud-web/src/api/workbench.ts +++ b/web/hwlab-cloud-web/src/api/workbench.ts @@ -63,6 +63,7 @@ export interface WorkbenchLaunchContext { mdtodoRootRef?: string | null; hwpodWorkspaceArgs?: string | null; contextFingerprint?: string | null; + providerProfile?: string | null; capabilities?: Record; executionContext?: WorkbenchLaunchExecutionContext | null; bodyPreview?: string | null; diff --git a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue index 3243636f..aab75033 100644 --- a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue +++ b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue @@ -23,6 +23,7 @@ import { useMdtodoTaskMutation } from "@/composables/mdtodo/useMdtodoTaskMutatio import { useMdtodoReportPreview } from "@/composables/mdtodo/useMdtodoReportPreview"; import { useMdtodoWorkbenchLaunch } from "@/composables/mdtodo/useMdtodoWorkbenchLaunch"; import { fileDisplayName } from "@/composables/mdtodo/useMdtodoDisplay"; +import { useWorkbenchStore } from "@/stores/workbench"; const router = useRouter(); const route = useRoute(); @@ -40,6 +41,7 @@ const taskData = useMdtodoTaskData(); const mutation = useMdtodoTaskMutation(); const report = useMdtodoReportPreview(); const launch = useMdtodoWorkbenchLaunch(router); +const workbench = useWorkbenchStore(); // Selection state (URL is authority; these mirror the URL) const selectedSourceId = ref(null); @@ -54,6 +56,8 @@ const taskPaneCollapsed = ref(false); const taskPaneWidth = ref(26); const reportPaneCollapsed = ref(false); const reportPaneWidth = ref(50); +const providerOptionsReady = ref(false); +const providerOptionsError = ref(null); // Dialog visibility const showInfo = ref(false); @@ -109,7 +113,22 @@ const selectedTaskBodyHtml = computed(() => report.markdownToHtml(selectedTaskBo const reportPreviewHtml = computed(() => report.markdownToHtml(report.reportPreview.value?.content ?? "")); const selectedFileName = computed(() => selectedFile.value ? fileDisplayName(selectedFile.value) : selectedTask.value?.fileRef || "-"); const reportPaneOpen = computed(() => Boolean(report.reportPreview.value)); -const workbenchLaunchEnabled = computed(() => launch.isLaunchEnabled(source.navigation.value)); +const selectedProviderValue = computed(() => String(workbench.providerProfile || "").trim()); +const selectedProviderOption = computed(() => workbench.providerOptions.find((option) => option.value === selectedProviderValue.value) ?? null); +const providerLaunchBlocker = computed(() => { + if (!providerOptionsReady.value) return "模型通道校验中"; + if (providerOptionsError.value) return providerOptionsError.value; + if (!selectedProviderValue.value) return "模型通道未选择"; + const option = selectedProviderOption.value; + if (option?.configured === false) return `模型通道 ${option.label || option.value} 未配置`; + return null; +}); +const workbenchLaunchCapabilityEnabled = computed(() => launch.isLaunchEnabled(source.navigation.value)); +const workbenchLaunchBlocker = computed(() => { + if (!workbenchLaunchCapabilityEnabled.value) return "Workbench Launch capability unavailable"; + return providerLaunchBlocker.value; +}); +const workbenchLaunchEnabled = computed(() => !workbenchLaunchBlocker.value); const launchButtonDisabled = computed(() => !selectedTask.value || !workbenchLaunchEnabled.value || launch.launchLoading.value); const selectedSourceCanReindex = computed(() => Boolean(selectedSourceId.value && !taskData.taskLoading.value && !source.sourceReindexLoading.value)); function reportLinksOnly(links: MdtodoTaskLinkRecord[]): MdtodoTaskLinkRecord[] { @@ -131,7 +150,25 @@ function projectedReportLink(task: MdtodoTaskRecord, linkCount: number): MdtodoT }; } -onMounted(() => void loadPage()); +onMounted(() => { + void refreshMdtodoProviderOptions(); + void loadPage(); +}); + +async function refreshMdtodoProviderOptions(): Promise { + providerOptionsReady.value = false; + providerOptionsError.value = null; + const ok = await workbench.refreshProviderOptions(); + providerOptionsReady.value = true; + if (!ok) { + providerOptionsError.value = "模型通道目录不可用"; + return; + } + const current = selectedProviderOption.value; + if (current?.configured !== false) return; + const fallback = workbench.providerOptions.find((option) => option.configured !== false); + if (fallback) workbench.setProviderProfile(fallback.value); +} watch(selectedSourceId, async (sourceId) => { if (!sourceId || loading.value || selection.applyingRouteSelection.value) return; @@ -353,7 +390,7 @@ function closeReport(): void { async function launchWorkbench(): Promise { const task = selectedTask.value; if (!task || launchButtonDisabled.value) return; - const result = await launch.launchTask(task, selectedTaskBody.value, selectedTaskLinks.value, selectedFileName.value, source.navigation.value); + const result = await launch.launchTask(task, selectedTaskBody.value, selectedTaskLinks.value, selectedFileName.value, source.navigation.value, selectedProviderValue.value); if (result) { await taskData.loadLinks(task.taskRef); await launch.navigateToWorkbench(result); @@ -478,7 +515,11 @@ function setError(err: unknown): void { :launch-loading="launch.launchLoading.value" :launch-error="launch.launchError.value" :launch-enabled="workbenchLaunchEnabled" + :launch-blocker="workbenchLaunchBlocker" + :provider-profile="workbench.providerProfile" + :provider-options="workbench.providerOptions" :navigation="source.navigation.value" + @update:provider-profile="workbench.setProviderProfile($event)" @update:edit-title="editTitle = $event" @update:edit-status="editStatus = $event" @update:edit-body="editBody = $event" diff --git a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue index a1fb8f3f..2b2021cd 100644 --- a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue +++ b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue @@ -4,6 +4,8 @@