diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 2d2d37e..e6f43c0 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -307,27 +307,59 @@ const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], /** 自定义窗口 → 宿主会话桥(apply 时注入;不可用时 CustomPane 降级提示) */ let sessionBridge: { sessions: any; conversation: any; list: any; workspaces: any } | null = null -/** 宿主 API 客户端(apply 时从 connection 服务取;agentPresets 用于修复新会话继承已删预设的 bug) */ -let presetApi: { agentPresets?: any } | null = null +/** 宿主 API 客户端(apply 时从 connection 服务取;agentPresets/sessions 用于修复新会话继承失效模型的 bug) */ +let hostApi: { agentPresets?: any; sessions?: any } | null = null /** 新会话显式应用「部署默认预设」:宿主新会话座位同款逻辑(api.agentPresets.select,仅对 blank 会话生效)。 * 修复:用户删掉默认模型后,新建会话继承到失效预设,prompt 报 model-unavailable 导致窗口建不出来。 */ async function ensureSessionPreset(sessionId: string): Promise { - const api = presetApi?.agentPresets + const api = hostApi?.agentPresets if (!api || typeof api.list !== 'function' || typeof api.select !== 'function') return try { const listRes = await api.list({}) const presets = listRes?.result?.ok ? listRes?.result?.value?.presets : null if (!Array.isArray(presets) || presets.length === 0) return - const fallback = presets.find((p: any) => p?.isDefault)?.id ?? presets[0]?.id - if (!fallback) return - const selRes = await api.select({ sessionId, agentPreset: fallback }) - if (selRes?.result?.ok) { - try { sessionBridge?.sessions?.noteAgentPreset?.(sessionId, fallback) } catch {} + // 部署默认优先,若其本身也失效(如引用了被删的 provider),逐个尝试其余预设直到成功 + const ordered = [...presets] + const defIdx = ordered.findIndex((p: any) => p?.isDefault) + if (defIdx > 0) ordered.unshift(ordered.splice(defIdx, 1)[0]) + for (const preset of ordered) { + const pid = preset?.id + if (!pid) continue + try { + const selRes = await api.select({ sessionId, agentPreset: pid }) + if (selRes?.result?.ok) { + try { sessionBridge?.sessions?.noteAgentPreset?.(sessionId, pid) } catch {} + return + } + try { if (typeof console !== 'undefined' && console.warn) console.warn('[worktable] preset select failed:', pid, selRes?.result?.error?.message ?? '') } catch {} + } catch (err) { + try { if (typeof console !== 'undefined' && console.warn) console.warn('[worktable] preset select threw:', pid, String(err)) } catch {} + } } } catch { /* 修复失败不阻断主流程,错误仍按原路径提示 */ } } +/** 新会话模型选择修复(真正的根因修复):当前选择指向已删 provider(routable=false)时, + * 用 session.selectModel 切到模型目录里第一个可用 provider 的首个模型—— + * 该 API 同时把新选择存为默认,顺带修复后续所有新会话。 */ +async function ensureSessionModel(sessionId: string): Promise { + const api = hostApi?.sessions + if (!api || typeof api.models !== 'function' || typeof api.selectModel !== 'function') return + try { + const mRes = await api.models({ sessionId }) + if (!mRes?.result?.ok) return + const val = mRes.result.value + if (val?.routable === true) return + const groups = val?.groups + if (!Array.isArray(groups) || groups.length === 0) return + const g = groups[0] + const model = g?.models?.[0]?.id + if (!g?.id || !model) return + await api.selectModel({ sessionId, provider: g.id, model }) + } catch { /* 修复失败不阻断主流程,错误仍按原路径提示 */ } +} + /** 控制室(工作台自己)项目:固定 id、固定排项目列表第一位、不可删除 */ const CONSOLE_ID = 'wt-console' const CONSOLE_ICON = '🖥️' @@ -661,7 +693,8 @@ export async function createCustomSession(projectId: string, projectName: string if (workspaceId) createOpts = { workspaceId } else if (folder) createOpts = { cwd: folder } const sessionId = await b.sessions.create(createOpts) - await ensureSessionPreset(sessionId) // 新会话应用部署默认预设(修复继承已删模型 → model-unavailable) + await ensureSessionPreset(sessionId) // 新会话应用部署默认预设 + await ensureSessionModel(sessionId) // 修复继承失效 provider(selectModel 同步存默认,顺带修复后续新会话) markPluginSessionOpen(sessionId) // 插件发起的切换:不触发「切会话关项目」联动 try { await b.sessions.open?.(sessionId) } catch {} await promptIntoSession(sessionId, text) @@ -1454,7 +1487,8 @@ function buildCustomLayoutPrompt(req: string): string { if (workspaceId) createOpts = { workspaceId } else if (folder) createOpts = { cwd: folder } const sessionId = await b.sessions.create(createOpts) - await ensureSessionPreset(sessionId) // 同样修复:管理对话新建后首次发消息不再继承已删模型 + await ensureSessionPreset(sessionId) // 管理对话同样应用部署默认预设 + await ensureSessionModel(sessionId) // 管理对话同样修复失效 provider markPluginSessionOpen(sessionId) persistProjects((prev) => ({ ...prev, bindings: { ...prev.bindings, [CONSOLE_ID]: sessionId } })) setConsoleBind(null) @@ -2703,7 +2737,8 @@ export const inject = ['slots', 'locale', 'sessions', 'conversation', 'workspace export function apply(ctx: any) { // 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级) sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null, workspaces: ctx.workspaces ?? null } - try { presetApi = ctx.get?.('connection')?.api ?? null } catch { presetApi = null } + try { hostApi = ctx.get?.('connection')?.api ?? null } catch { hostApi = null } + try { (window as any).__dshHostApi = hostApi } catch {} try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces; (window as any).__dshBuildWindowTaskText = buildWindowTaskText; (window as any).__dshSyncSessionScope = () => syncSessionScope(sessionBridge?.list) } catch {} diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index 476f07f..ed3b8cd 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -1444,3 +1444,19 @@ - 验证(probe-batch16 阶段3 扩展):新建会话 agentPreset='standard'(部署默认预设已应用)✓; functional-diag 全 20 STEP ERROR_COUNT: 0。 - 备注:纯客户端改动,F5 即生效;未启用 agentPresets 的宿主自动跳过(无行为变化)。 + +## 补记(第十八轮:模型失效修复——真正根因在会话级模型选择) + +- 进展:上一轮预设修复后用户仍报 model-unavailable——端到端探测(probe-batch18:走工作台 + 新建流程后直接 session.prompt)实测 promptOk:false,证实预设应用并未重置会话级模型选择。 +- 根因(只读侦察宿主 api-proxy):新会话继承的是「默认模型选择」(provider/model,独立于 + agent 预设,defaults.saveDefaultModelSelection 持久化);用户删掉 openai provider 后, + 该选择 routable=false,session.prompt 在 turnAgentFor 处直接拒绝。会话级修复 API = + session.models(返回 current/routable/groups)+ session.selectModel。 +- 实现:hostApi 捕获(原 presetApi 更名,调试句柄 __dshHostApi);新增 ensureSessionModel: + session.models 查 routable——false 时取 groups[0] 首个可用模型 session.selectModel + (该 API 同时 saveDefaultModelSelection,把新选择存为默认 → 顺带修复后续所有新会话); + createCustomSession / bindConsoleNew 两处 preset 修复后追加调用。 +- 验证(probe-batch18 重跑):promptOk:true、promptErr:null(模型真实接受消息)✓; + functional-diag 全 20 STEP ERROR_COUNT: 0。 +- 备注:纯客户端改动,F5 即生效。 diff --git a/04_test/preset-inspect.cjs b/04_test/preset-inspect.cjs new file mode 100644 index 0000000..7e379ed --- /dev/null +++ b/04_test/preset-inspect.cjs @@ -0,0 +1,54 @@ +// preset-inspect.cjs — 检查宿主预设花名册与默认预设指向的 provider +const http = require('http'); +const { spawn } = require('child_process'); +const WebSocket = require('ws'); +const PORT = 9380; +const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [ + '--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', + '--window-size=1440,900', '--force-device-scale-factor=1', + '--remote-debugging-port=' + PORT, + '--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-preset1', + 'about:blank', +], { stdio: 'ignore' }); +const getJSON = (p) => new Promise((res, rej) => { + http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(e)}}) }).on('error', rej); +}); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +(async () => { + let list = null; + for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); } + const target = list.find((t) => t.type === 'page') || list[0]; + const ws = new WebSocket(target.webSocketDebuggerUrl); + let id = 0; const pending = new Map(); + const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); }); + ws.on('message', (raw) => { const m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }); + await new Promise((r) => ws.on('open', r)); + await send('Runtime.enable'); + await send('Page.enable'); + await send('Page.navigate', { url: 'http://127.0.0.1:3080/' }); + const evaluate = async (expr) => { + const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); + if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text }; + return r.result && r.result.result ? r.result.result.value : undefined; + }; + let ready = null; + for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshPresetApi"); if (ready === true) break; } + await sleep(1200); + const out = await evaluate(`(async function(){ + var out = {}; + try { + var api = window.__dshPresetApi; + out.apiCaptured = !!api; + out.agentPresetsPresent = !!(api && api.agentPresets); + var lr = api && api.agentPresets && await api.agentPresets.list({}); + out.listOk = lr && lr.result && lr.result.ok; + var presets = lr && lr.result && lr.result.ok ? (lr.result.value && lr.result.value.presets) : null; + out.count = Array.isArray(presets) ? presets.length : null; + out.rows = Array.isArray(presets) ? presets.map(function(p){ return { id: p.id, name: p.name, isDefault: !!p.isDefault, provider: p.provider || p.modelProvider || (p.composition && p.composition.provider) || null, keys: Object.keys(p).slice(0, 10) }; }) : null; + } catch (e) { out.err = String(e && e.stack || e); } + return out; + })()`); + console.log('RESULT ' + JSON.stringify(out)); + try { ws.close(); } catch {} + proc.kill(); +})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} }); diff --git a/04_test/probe-batch18.cjs b/04_test/probe-batch18.cjs new file mode 100644 index 0000000..0af49f4 --- /dev/null +++ b/04_test/probe-batch18.cjs @@ -0,0 +1,82 @@ +// probe-batch18.cjs — 端到端验证:新建并绑定 → 直接 prompt,确认无 model-unavailable +const http = require('http'); +const { spawn } = require('child_process'); +const WebSocket = require('ws'); +const PORT = 9381; +const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [ + '--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', + '--window-size=1440,900', '--force-device-scale-factor=1', + '--remote-debugging-port=' + PORT, + '--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-batch18', + 'about:blank', +], { stdio: 'ignore' }); +const getJSON = (p) => new Promise((res, rej) => { + http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(e)}}) }).on('error', rej); +}); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +(async () => { + let list = null; + for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); } + const target = list.find((t) => t.type === 'page') || list[0]; + const ws = new WebSocket(target.webSocketDebuggerUrl); + let id = 0; const pending = new Map(); + const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); }); + ws.on('message', (raw) => { const m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }); + await new Promise((r) => ws.on('open', r)); + await send('Runtime.enable'); + await send('Page.enable'); + await send('Page.addScriptToEvaluateOnNewDocument', { + source: "try{ if(location.origin==='http://127.0.0.1:3080'){ localStorage.setItem('dsh.worktable.view.v1', JSON.stringify({query:'',searchOpen:false,orderBy:'manual',dock:'footer',floatTop:null,sortMigratedV2:true})); localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify({order:[],lastUsed:{},hidden:[],nameOverrides:{},iconOverrides:{},shortcuts:[],layouts:[],bindings:{},folders:{},views:{},removed:[]})); } }catch(e){}", + }); + await send('Page.navigate', { url: 'http://127.0.0.1:3080/' }); + const evaluate = async (expr) => { + const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); + if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text }; + return r.result && r.result.result ? r.result.result.value : undefined; + }; + let ready = null; + for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshWorktable"); if (ready === true) break; } + await sleep(1500); + await evaluate("(function(){ var b=document.querySelector('button[title=\"Open sidebar\"],button[aria-label=\"Open sidebar\"]'); if(b){b.click(); return true} return false })()"); + await sleep(1000); + const out = await evaluate(`(async function(){ + var out = {}; + var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }; + var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); }; + var st = window.__dshWorktable.splitStore; + try { + // 控制室未绑定 → 点开 → 强制绑定 → 新建并绑定 + var card = vis('.dsh-wt_projects .dsh-wt_consoleEntry'); + card.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); + await sleep(600); + var bpop = vis('.dsh-wt_consoleBindPop'); + out.panelOpen = !!bpop; + var cb = bpop && bpop.querySelector('.dsh-wt_consoleCreateBtn'); + if (cb) { cb.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); } + await sleep(2500); + var saved = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')); + var sid = saved.bindings['wt-console']; + out.sid = sid; + out.specId = st.active && st.spec ? st.spec.id : null; + // 会话面 prompt 实测 + var svc = window.__dshSessions; + var snap = svc.list.getSnapshot(); + var row = snap.byId[sid]; + out.agentPreset = row ? (row.agentPreset || '(none)') : '(missing)'; + var face = svc.binding(sid).session; + out.hasPrompt = typeof face.prompt === 'function'; + var r1 = null; + try { + r1 = await face.prompt([{ type: 'text', text: '回复一个字:好' }], 'queue'); + out.promptOk = !!(r1 && r1.ok); + out.promptErr = r1 && !r1.ok ? (r1.error && (r1.error.code || r1.error.message)) : null; + } catch (e) { out.promptThrew = String(e).slice(0, 120); } + await sleep(2000); + st.close(); await sleep(200); + } catch (e) { out.err = String(e && e.stack || e); } + return out; + })()`); + console.log('RESULT ' + JSON.stringify(out)); + try { ws.close(); } catch {} + proc.kill(); +})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} }); diff --git a/AGENTS.md b/AGENTS.md index 9575d2d..8a22162 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,9 +33,11 @@ node --check lib/index.js 聊天窗恒在右侧;缩略图在 presetThumb() 加分支。 - **新会话预设修复**:新建会话(createCustomSession / bindConsoleNew)创建后调用 ensureSessionPreset——用宿主 api.agentPresets.list/select 显式应用「部署默认预设」 - (isDefault ?? 首个;select 仅对 blank 会话生效),修复用户删掉默认模型后新会话继承 - 失效预设、prompt 报 model-unavailable 建不出窗口的问题;失败静默、无 agentPresets - API 的宿主自动跳过。 + (isDefault ?? 首个,失败逐个尝试其余预设;select 仅对 blank 会话生效)。 +- **新会话模型修复(真根因)**:会话级模型选择独立于预设、随默认选择持久化——用户删掉 + provider 后新会话继承失效选择,prompt 报 model-unavailable。ensureSessionModel 用 + session.models 查 routable,false 时 session.selectModel 切到 groups[0] 首个可用模型 + (该 API 同时把新选择存为默认,顺带修复后续所有新会话)。两者都失败静默、缺 API 跳过。 - **对话绑定**:projects.v1.bindings = { 项目id → 会话id };打开项目时引擎自动 sessions.open(绑定会话)(openSplit / DOM 桥两处入口);未绑定/解绑 = 不切换。 - **项目×对话联动**:打开项目记录「打开前会话」(projectAttachRef.sessionId);项目打开期间切到