From 0580663f6e8d1f511d6e22ed79bd80923b43f1b0 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 20:10:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A9=E6=89=8B=E6=A0=8F=E8=BF=9E=E7=9D=80?= =?UTF-8?q?=E9=80=89=E4=B8=A4=E6=AC=A1=E3=80=81=E4=B8=A4=E6=AC=A1=E9=83=BD?= =?UTF-8?q?=E6=B2=A1=E5=AD=98=E4=B8=8A=E6=97=B6=E9=80=80=E5=9B=9E=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E5=AD=98=E7=9D=80=E7=9A=84=E8=AE=BE=E5=AE=9A?= =?UTF-8?q?=EF=BC=9A=E4=BB=A5=E5=89=8D=E9=80=80=E5=9B=9E=E7=9A=84=E6=98=AF?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E9=80=89=E6=97=B6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E7=9A=84=E4=B9=90=E8=A7=82=E5=80=BC=EF=BC=88=E4=BB=8E=E6=B2=A1?= =?UTF-8?q?=E5=AD=98=E4=B8=8A=EF=BC=89=EF=BC=8C=E5=8E=9F=E5=9B=A0=E5=92=8C?= =?UTF-8?q?=E9=BD=BF=E8=BD=AE=E7=BA=A2=E7=82=B9=E4=B8=80=E8=B5=B7=E6=B6=88?= =?UTF-8?q?=E5=A4=B1=EF=BC=9B=E7=8E=B0=E5=9C=A8=E8=AE=B0=E4=B8=8B=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E6=9C=80=E5=90=8E=E4=B8=80=E6=AC=A1=E8=AE=A4?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=E8=AE=BE=E5=AE=9A=EF=BC=8C=E6=B2=A1=E5=AD=98?= =?UTF-8?q?=E4=B8=8A=E6=97=B6=E9=80=80=E5=9B=9E=E5=AE=83=E5=B9=B6=E5=86=8D?= =?UTF-8?q?=E5=8F=96=E4=B8=80=E6=AC=A1=E4=BB=A5=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E4=B8=BA=E5=87=86=E3=80=82check-studio=20=E7=94=A8=E6=94=BE?= =?UTF-8?q?=E8=A1=8C=E9=97=B8=E9=97=A8=E9=80=A0=E5=87=BA=E5=85=88=E5=8F=91?= =?UTF-8?q?=E7=9A=84=E9=82=A3=E6=AC=A1=E8=BF=98=E6=B2=A1=E5=9B=9E=E6=9D=A5?= =?UTF-8?q?=E5=B0=B1=E5=8F=88=E9=80=89=E4=BA=86=E4=B8=80=E6=AC=A1=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/run/Composer.tsx | 21 +++++++++++++++++---- scripts/check-studio.mjs | 35 ++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/frontend/src/run/Composer.tsx b/frontend/src/run/Composer.tsx index 569da602..acbebd3c 100644 --- a/frontend/src/run/Composer.tsx +++ b/frontend/src/run/Composer.tsx @@ -49,6 +49,8 @@ export function Composer({ hero, onFocusChange }: { const [saveFailed, setSaveFailed] = useState(false) // 只认最后一次选择的回包:连着选两次时,先发的那次晚回来也不能盖掉后一次 const picks = useRef(0) + // 服务端最后一次认下的设定:没存上时退回它。不能退回选之前显示的样子——连着选几次时,那可能是前一次还没存上的乐观值 + const confirmed = useRef(null) const [useBase, setUseBase] = useState(true) const ref = useRef(null) @@ -59,7 +61,10 @@ export function Composer({ hero, onFocusChange }: { let alive = true setSaveFailed(false) const seq = picks.current - void api.copilot.getModel().then((m) => { if (alive && seq === picks.current) setSaved(m) }).catch(() => undefined) + void api.copilot.getModel().then((m) => { + confirmed.current = m + if (alive && seq === picks.current) setSaved(m) + }).catch(() => undefined) return () => { alive = false } // eslint-disable-next-line react-hooks/exhaustive-deps }, [opts]) @@ -94,11 +99,19 @@ export function Composer({ hero, onFocusChange }: { // 哪个模型由服务端说了算,回来之前不写「当前」 setSaved({ configured: !!value, provider: null, model: value || null, effective_provider: null, effective_model: value || null, error: null, panel_error: null }) - void api.copilot.setModel({ model: value || null }).then((m) => { if (seq === picks.current) setSaved(m) }).catch(() => { + void api.copilot.setModel({ model: value || null }).then((m) => { + confirmed.current = m + if (seq === picks.current) setSaved(m) + }).catch(() => { if (seq !== picks.current) return - // 没存上:服务端存着的还是原来那一组,退回选之前的样子(原因、齿轮上的红点照旧),并说明保存失败 - setSaved(before) + // 没存上:服务端存着的还是最后一次认下的那一组,退回它(原因、齿轮上的红点照旧),并说明保存失败;再取一次,以服务端 + // 现在存着的为准(先发的那次可能晚到地存上了) + setSaved(confirmed.current ?? before) setSaveFailed(true) + void api.copilot.getModel().then((m) => { + confirmed.current = m + if (seq === picks.current) setSaved(m) + }).catch(() => undefined) }) } diff --git a/scripts/check-studio.mjs b/scripts/check-studio.mjs index 7a741992..ae0ec595 100644 --- a/scripts/check-studio.mjs +++ b/scripts/check-studio.mjs @@ -2666,15 +2666,17 @@ await section('点名的模型接入:检查器模型一栏说清留空用哪 // 那样看着一切正常,发出去还会带上模型、绕过存着的接入 const BROKEN = { configured: true, provider: 'demo-gone', model: 'big-model', effective_provider: null, effective_model: null, error: ASSISTANT_BROKEN, panel_error: ASSISTANT_PANEL } - const copilot = { saved: BROKEN, puts: [], fail: false } + // gate 是一个 Promise 时,保存要等它放行才答复:用来造「先发的那次还没回来就又选了一次」 + const copilot = { saved: BROKEN, puts: [], fail: false, gate: null } const before = async (page) => { const json = (route, body, status = 200) => route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) }) await page.route(/\/api\/providers(\?.*)?$/, (route) => (route.request().method() === 'GET' ? json(route, MODEL_PROVIDERS) : route.fallback())) await page.route(/\/api\/workflows\/validate$/, (route) => json(route, modelLint(route.request().postDataJSON()?.graph ?? {}))) - await page.route(/\/api\/copilot\/model$/, (route) => { + await page.route(/\/api\/copilot\/model$/, async (route) => { if (route.request().method() === 'PUT') { const body = route.request().postDataJSON() copilot.puts.push(body) + if (copilot.gate) await copilot.gate if (copilot.fail) return json(route, { detail: '数据库已锁定,请稍后重试' }, 500) copilot.saved = body.model ? { configured: true, provider: null, model: body.model, effective_provider: 'demo-main', effective_model: body.model, error: null, panel_error: null } @@ -2767,6 +2769,24 @@ await section('点名的模型接入:检查器模型一栏说清留空用哪 check('……下拉退回存着的那一项,原因和齿轮上的红点照旧', await pickedText() === 'big-model(模型接入「demo-gone」)' && await page.locator('[data-copilot-model-error]').innerText().catch(() => '') === ASSISTANT_PANEL && await page.locator('[data-copilot-model-broken]').count() === 1, await pickedText()) + // 连着选两次、两次都没存上(先发的那次还没回来就又选了一次):退回服务端存着的那一项,不是第一次选的、从没存上的 + // 模型;原因、齿轮上的红点照旧,并说明没改成 + let release + copilot.gate = new Promise((resolve) => { release = resolve }) + const sent = copilot.puts.length + await select.selectOption('small-model') + await until(() => copilot.puts.length > sent, 3000) + await select.selectOption('') + await until(() => copilot.puts.length > sent + 1, 3000) + release() + copilot.gate = null + await page.locator('[data-copilot-model-save-failed]').waitFor({ timeout: 3000 }).catch(() => {}) + await page.waitForTimeout(300) + check('连着选两次都没存上:下拉退回服务端存着的那一项,原因、红点照旧,说明没改成', copilot.puts.length === sent + 2 + && await pickedText() === 'big-model(模型接入「demo-gone」)' + && await page.locator('[data-copilot-model-error]').innerText().catch(() => '') === ASSISTANT_PANEL + && await page.locator('[data-copilot-model-broken]').count() === 1 + && (await page.locator('[data-copilot-model-save-failed]').innerText().catch(() => '')) === '保存失败,助手使用的模型未更改', await pickedText()) // 收起再展开:重新取回的就是服务端现在存着的,上一次「保存失败」的说明不再留着,原因照旧 await gear.click() await gear.click() @@ -2776,10 +2796,11 @@ await section('点名的模型接入:检查器模型一栏说清留空用哪 && await page.locator('[data-copilot-model-error]').innerText().catch(() => '') === ASSISTANT_PANEL) copilot.fail = false + const n0 = copilot.puts.length await select.selectOption('') - await until(() => copilot.puts.length > 1, 3000) - check('重新选「跟随默认」算一次改动:存成不指定接入和模型', copilot.puts.length === 2 && copilot.puts[1].model === null - && !copilot.puts[1].provider, JSON.stringify(copilot.puts)) + await until(() => copilot.puts.length > n0, 3000) + check('重新选「跟随默认」算一次改动:存成不指定接入和模型', copilot.puts.length === n0 + 1 && copilot.puts[n0].model === null + && !copilot.puts[n0].provider, JSON.stringify(copilot.puts)) await page.waitForFunction(() => !document.querySelector('[data-copilot-model-error]'), null, { timeout: 3000 }).catch(() => {}) check('……原因、保存失败的说明、齿轮上的标记一起消失,「跟随默认」写出当前用的模型', await page.locator('[data-copilot-model-error]').count() === 0 && await page.locator('[data-copilot-model-save-failed]').count() === 0 @@ -2787,9 +2808,9 @@ await section('点名的模型接入:检查器模型一栏说清留空用哪 && await followText() === '跟随默认(当前:big-model)', await followText()) // 选了一个模型:服务端回来的「当前」就是这个模型,不能写到「跟随默认」上(跟随默认用的是 demo-main 的 big-model) await select.selectOption('small-model') - await until(() => copilot.puts.length > 2, 3000) + await until(() => copilot.puts.length > n0 + 1, 3000) await page.waitForTimeout(200) - check('选了具体模型:「跟随默认」不写「当前」', copilot.puts[2]?.model === 'small-model' && await followText() === '跟随默认' + check('选了具体模型:「跟随默认」不写「当前」', copilot.puts[n0 + 1]?.model === 'small-model' && await followText() === '跟随默认' && await select.inputValue() === 'small-model', await followText()) check('没有运行时报错', errors.length === 0, errors.slice(0, 2).join(' | ')) await ctx.close()