From 662e4087e93796e4a65cfc589986df391eaefa4c Mon Sep 17 00:00:00 2001 From: HUIDADA <1439078547@qq.com> Date: Wed, 23 Sep 2026 16:53:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(enhance):=20=E5=8F=96=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=94=9A=E5=AE=9A=20dock=20=E4=BF=AE=E5=A4=8D=E6=81=92?= =?UTF-8?q?=E8=B5=B0=E5=85=9C=E5=BA=95;handler=20=E6=96=B0=E5=A2=9E=20enha?= =?UTF-8?q?nce=5Fconfig.json=20=E7=83=AD=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:前端 currentModel() 全局搜 [data-model-current-value] 并按「offsetParent 可见+ 最靠下」挑节点。ZCode 输入框工具栏在 fixed 定位容器内(offsetParent 恒为 null)被可见 过滤整批误杀,后台残留的设置/工作流面板节点反而更靠下被取走,导致 modelValue/Label 恒为空 → 主进程四档解析全空 → ④兜底选中供应商表第一个可用供应商的第一个模型。 实测表现:报错模型不是用户所选(如选 glm-5.3-flash 却发出 deepseek-v4-flash-0731), 若该供应商 key 失效则 404 not supported by any configured account in this group, 且 404 被 classify 误判为「接口路径不对」。 修复: - currentModel() 先锚定 composer dock 再找模型按钮;dock 命中时不再因 offsetParent=null 丢弃(fixed 容器该属性本就恒为 null);全局兜底排除 workflow-run-settings-model 面板 - handler 新增⓪热配置档(最高优先级):每次点击热读 /enhance_config.json, 支持自定义 providerId/modelId/maxTokens/temperature 及思考强度 (openai: reasoning_effort;anthropic: thinking.budget_tokens 自动抬高 max_tokens), 改文件即生效、无需重启;留空则行为与旧版完全一致 - classify 正则补 not supported,404 模型类错误不再误报路径问题 验证:patcher 语法/模板标记校验、zcode-enhance-prompt.js node --check、 tests/test_patcher.py 234 例(唯一失败项为 run.cmd 环境固有,与改动无关, 已用 stash 对照原版代码确认);实测「默认-永久」key 发 glm-5.3-flash 200, 失效 key 复现 404 逐字吻合。 --- .../zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md | 80 +++++++++++++++++++ .../scripts/zcode-enhance-prompt.js | 34 +++++--- .../zcode-tokenspeed/scripts/zcode_patcher.py | 29 +++++-- 3 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 skills/zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md diff --git a/skills/zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md b/skills/zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md new file mode 100644 index 0000000..5161cbe --- /dev/null +++ b/skills/zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md @@ -0,0 +1,80 @@ +# 增强提示词:根因分析与自定义方案(0.6.3 → 热配置版) + +> 结论先行:`增强失败:HTTP 404/400 ... Model ... not supported/unavailable` 的根因是 +> **前端取不到界面所选模型 → 恒走兜底 → 兜底恰好选「默认-临时」(供应商表第 0 位)的第一个模型 +> `deepseek-v4-flash-0731` → 而该供应商的 key 在中转站已失效**。 +> 请求实际到达了中转站(它能返回结构化错误),是中转站路由层直接拒绝、未转发上游, +> 所以渠道日志里看不到记录。代码取值逻辑、本地配置、进程本身都不是直接元凶。 + +## 一、根因链(已逐层实测验证) + +1. **前端取模型稳定失灵**:`currentModel()` 全局搜 `[data-model-current-value]` 并按 + 「offsetParent 可见 + 最靠下」挑节点。但 ZCode 输入框工具栏位于 **fixed 定位容器**内 + (fixed 元素 `offsetParent` 恒为 null,被可见过滤整批误杀),后台又常驻设置/工作流面板的 + 隐藏节点(它们反而「更靠下」),于是 `modelValue/modelLabel` 恒为空或取错 → 无论怎么切模型、 + 收不收起下拉都一样。实测 asar:属性本身挂在主界面模型按钮上(`data-chat-toolbar-popover-trigger`), + 问题纯在取值启发式。 +2. **主进程四档解析全空 → ④兜底**:兜底排序把内置供应商排最后,取**第一个可用自定义供应商** + 的第一个模型 = 「默认-临时」的 `deepseek-v4-flash-0731`。 +3. **「默认-临时」key 已失效**:实测该 key 发 `deepseek-v4-flash-0731` 与 `glm-5.3` 均 404 + `not supported by any configured account in this group`;「默认-永久」key 发同模型 200。 + 第一轮的 `HTTP 400 Model is unavailable` 是同一兜底路径撞上临时 key 的坏渠道(间歇性), + 后来该 key 彻底失效表现为 404。两次报错同根。 +4. **tip 误判**:classify 正则未覆盖 `is not supported`,404 落入「接口路径可能不对」——已修复。 + +## 二、修复内容(本次已改,重打 asar + 重启一次后生效) + +| 文件 | 修改 | +|---|---| +| `scripts/zcode-enhance-prompt.js` | `currentModel()` 先锚定 composer dock 再找模型按钮;dock 命中时不再因 `offsetParent=null` 丢弃;全局兜底排除 `workflow-run-settings-model` 面板节点 | +| `scripts/zcode_patcher.py` `_ENHANCE_HANDLER` | ① 新增 **⓪热配置档**(最高优先级,读 `enhance_config.json`);② body 参数全部热读(`maxTokens`/`temperature`);③ 思考强度:openai 协议加 `reasoning_effort`,anthropic 协议加 `thinking.budget_tokens`(自动抬高 max_tokens);④ classify 正则补 `not supported`,404 模型错误不再误报「路径问题」 | + +向后兼容:`enhance_config.json` 不存在或 `providerId/modelId` 留空 = 与旧行为完全一致。 + +## 三、自定义增强的供应商 / 模型 / 思考强度(改完即生效,零重启) + +编辑 `~/.zcode/v2/enhance_config.json`(handler 每次点击都重新读取): + +```jsonc +{ + "providerId": "ff20a569-dfb4-4721-9749-072008d84504", // 供应商 id(provider_config.json → providerRules[].providerId) + "modelId": "glm-5.3-flash", // 发给中转站的模型名(可不在此供应商的模型表里) + "reasoningEffort": "medium", // openai 协议: reasoning_effort;留空不发 + "thinkingBudget": 0, // anthropic 协议: thinking budget_tokens(1024-32768);0 不发 + "maxTokens": 2048, // 输出上限(anthropic 开思考时会自动 ≥ budget+1024) + "temperature": 0.3 // openai 协议温度;anthropic 不发温度 +} +``` + +- **换供应商/模型**:改 `providerId` + `modelId`,下一次点增强立即生效。 +- **思考强度**:openai-compatible 中转 → `reasoningEffort`(low/medium/high,按中转站支持的值); + 也可以直接把 `modelId` 换成中转站的思考变体名。anthropic 协议 → `thinkingBudget`。 +- **查 providerId**:跑 `python enhance_doctor.py`(scripts 目录),第 4 节直接显示命中供应商的 id; + 或看 `provider_config.json`。 +- **诊断**:增强失败后控制台看 `window.__zenhanceDiag.lastRequest / lastResult`(含 `how=config/ref/fallback` + 与 `tried` 列表),一眼定位走的是哪一档。 + +## 四、生效路径(重要边界) + +| 改动 | 生效方式 | +|---|---| +| `enhance_config.json`(供应商/模型/思考强度/参数) | **零重启**:下次点击即生效 | +| 本次代码修改(前端脚本 + handler) | 需**一次性**:完全退出 ZCode → 跑 `python zcode_patcher.py --enhance-prompt`(或等看护自动写入)→ 重启 ZCode;之后永远只改配置、不再重启 | + +> 无法做到「改 asar 内已注入的代码还不重启」:主进程 handler 在启动时注册进内存; +> 但本方案把所有可调项都收敛进热读配置,重启只有一次,以后不再需要。 + +## 五、临时止血(不改代码、零重启,今天即可用) + +任选其一(推荐第 1 种,客户端自己写文件最安全): + +1. ZCode 设置里**删除或停用「默认-临时」供应商**(其 key 已失效)→ 兜底自动落到「默认-永久」; +2. 在设置里把「默认-永久」的模型列表把 `glm-5.3-flash` 调到第一位(兜底取列表第一个); +3. 手改 `provider_config.json`(先关闭设置页,避免写冲突):删掉「默认-临时」条目,或把它 + `personalModelIds` 清空(④兜底会 `continue` 跳过无模型的供应商)。 + +## 六、验证与回滚 + +- 验证:重启后输入文字点增强 → 成功则 toast 显示 `已用「glm-5.3-flash」增强`; + 失败则看 `window.__zenhanceDiag.lastResult.tried` 首项应为 `config:...`。 +- 回滚:`python zcode_patcher.py --revert --enhance-prompt`;删除 `enhance_config.json` 即回到纯界面选择行为。 diff --git a/skills/zcode-tokenspeed/scripts/zcode-enhance-prompt.js b/skills/zcode-tokenspeed/scripts/zcode-enhance-prompt.js index 88dee90..2d26d3a 100644 --- a/skills/zcode-tokenspeed/scripts/zcode-enhance-prompt.js +++ b/skills/zcode-tokenspeed/scripts/zcode-enhance-prompt.js @@ -186,25 +186,35 @@ } } - /** 当前选中的模型:优先读模型按钮上的 data-model-current-value(形如 providerId/modelId), - * 读不到就退化为该按钮的显示文案(主进程会按 modelId / name 反查配置)。 - * 注意:同一页面可能有多个带该属性的节点(弹窗、工作流设置面板等),必须挑 - * **可见的、最靠下的**那个 —— 否则会把后台面板里的模型当成本会话选的模型, - * 这正是「本机能润色、别人报 Model is unavailable」的一个直接来源。 */ + /** 当前选中的模型:模型按钮就挂在 composer 工具栏上,因此先锚定 dock 再找 + * [data-model-current-value](形如 providerId/modelId)。 + * ★ 修复:旧实现全局搜 + 「最靠下」启发式会稳定失灵——输入框工具栏位于 + * fixed 定位容器内,offsetParent 恒为 null 被「可见过滤」整批误杀;而后台 + * 残留的设置/工作流面板节点反而「更靠下」被取走(或整池为空),于是 + * mv/ml 恒为空 → 主进程四档解析全空 → 掉进兜底档,把请求发给供应商表里 + * 第一个可用供应商的第一个模型。典型表现:报错里的模型不是你选的那个。 */ function currentModel() { let value = "", label = ""; try { - const all = Array.from(document.querySelectorAll("[data-model-current-value]")); - if (all.length) { - const vis = all.filter((e) => e.offsetParent != null); - const pool = vis.length ? vis : all; - pool.sort((a, b) => b.getBoundingClientRect().top - a.getBoundingClientRect().top); - const el = pool[0]; + const dock = findDock(); + let pool = []; + if (dock) pool = Array.from(dock.querySelectorAll("[data-model-current-value]")); + if (!pool.length) { + // dock 内没有(极端布局)才退回全局,但排除工作流运行设置等面板的残留节点 + pool = Array.from(document.querySelectorAll("[data-model-current-value]")) + .filter((e) => !e.closest("[data-testid='workflow-run-settings-model']")); + } + if (pool.length) { + const vis = pool.filter((e) => e.offsetParent != null); + // dock 命中时不再因 offsetParent=null 丢弃:fixed 容器里该属性本就恒为 null + const usablePool = (dock && pool.length) ? pool : (vis.length ? vis : pool); + usablePool.sort((a, b) => b.getBoundingClientRect().top - a.getBoundingClientRect().top); + const el = usablePool[0]; value = String(el.getAttribute("data-model-current-value") || "").trim(); // 显示名取该节点里最长的可见文本(模型名 + 可能的连接方式后缀) const t = String(el.textContent || "").replace(/\s+/g, " ").trim(); label = t; - diag.modelCandidates = all.length; + diag.modelCandidates = pool.length; } } catch (err) { /* ignore */ } return { value, label }; diff --git a/skills/zcode-tokenspeed/scripts/zcode_patcher.py b/skills/zcode-tokenspeed/scripts/zcode_patcher.py index 0b94ebf..54c1c74 100644 --- a/skills/zcode-tokenspeed/scripts/zcode_patcher.py +++ b/skills/zcode-tokenspeed/scripts/zcode_patcher.py @@ -1437,6 +1437,9 @@ def _enhance_main_block(ipc_alias: str) -> bytes: // 因此这里先把 provider_config.json 归一化成统一的候选表,必要时再用 config.json 补缺。 // ============================================================ function rd(p){try{return JSON.parse(n.readFileSync(p,"utf-8"))}catch(_){return null}} +// ⓪ 增强专用热配置 /enhance_config.json:每次点击都重新读取,改文件即生效、无需重启。 +// providerId/modelId 留空或文件不存在时,行为与旧版完全一致(用界面当前选中的模型)。 +let cfg=rd(r.join(root,"enhance_config.json"))||{}; let srcTried=[]; // ① provider_config.json(权威)→ 归一化 let pcCands=[]; @@ -1494,6 +1497,13 @@ def _enhance_main_block(ipc_alias: str) -> bytes: tried.push(n+":"+pid+"/"+mid+(usable((byId[pid]||{}).p)?"":"(跳过:不可用)")); if(!byId[pid]||!usable(byId[pid].p))return null; return{pid:pid,mid:mid,p:byId[pid].p,how:n,name:(byId[pid].name||"")}} +// ⓪ 用户在 enhance_config.json 里显式指定的供应商+模型 —— 最高优先级。 +// 只要求供应商可用(key/baseURL 齐全),不校验模型表:允许发表里没有、 +// 但中转站实际支持的模型名(也是自定义思考强度模型变体的入口)。 +if(cfg&&String(cfg.providerId||"").trim()&&String(cfg.modelId||"").trim()){ +let cpid=String(cfg.providerId).trim(),cmid=String(cfg.modelId).trim(); +let cp=byId[cpid]; +if(cp&&usable(cp.p)){pick=cand(cpid,cmid,"config")}} // ① 界面直接给的 ref(providerId/modelId)——最准 if(mv){let k=mv.indexOf("/"); if(k>0){let pid=mv.slice(0,k),mid=mv.slice(k+1); @@ -1545,23 +1555,32 @@ def _enhance_main_block(ipc_alias: str) -> bytes: let sys=__SYS__,tpl=__TPL__; let user=tpl.split("{input}").join(text); let cs=[],body; +// 热配置参数:maxTokens / temperature / 思考强度(reasoningEffort 或 thinkingBudget) +let cfgMax=Math.max(256,Number(cfg.maxTokens)||2048); +let cfgTemp=(cfg.temperature==null||isNaN(Number(cfg.temperature)))?0.3:Number(cfg.temperature); +let re=String(cfg.reasoningEffort||"").trim().toLowerCase(); +let tb=Number(cfg.thinkingBudget)||0; let headers={"Content-Type":"application/json","Accept":"application/json",Authorization:"Bearer "+k,"x-api-key":k}; if(kind==="anthropic"){ cs.push((u.endsWith("/v1")?u:u+"/v1")+"/messages"); -body={model:pick.mid,max_tokens:2048,system:sys,messages:[{role:"user",content:user}]}} +body={model:pick.mid,max_tokens:cfgMax,system:sys,messages:[{role:"user",content:user}]}; +if(re||tb>0){let budget=Math.max(1024,Math.min(32768,tb||8192)); +body.thinking={type:"enabled",budget_tokens:budget}; +body.max_tokens=Math.max(cfgMax,budget+1024)}} else{ cs.push((u.endsWith("/v1")?u:u+"/v1")+"/chat/completions"); cs.push(u+"/chat/completions"); -body={model:pick.mid,stream:!1,temperature:0.3,max_tokens:2048, -messages:[{role:"system",content:sys},{role:"user",content:user}]}} +body={model:pick.mid,stream:!1,temperature:cfgTemp,max_tokens:cfgMax, +messages:[{role:"system",content:sys},{role:"user",content:user}]}; +if(re)body.reasoning_effort=re} let payload=JSON.stringify(body); // —— 错误分类 + 重试策略 —— // retryable: 换地址重试 / 退避重试有意义(网络抖动、超时、限流、供应商 5xx) // permanent: 请求本身或账号的问题,重试无意义,但要做「友好归因」 function classify(code,status,msg){ let m=String(msg||"").toLowerCase(); -if(/model is unavailable|model_not_found|not found|unknown model|no such model|not available/.test(m)) -return{kind:"model",retry:!1,tip:"界面所选模型在该供应商处不存在或未开通。请检查模型名称与套餐,或在设置里换一个模型"}; +if(/model is unavailable|model_not_found|not found|unknown model|no such model|not available|not supported/.test(m)) +return{kind:"model",retry:!1,tip:"该供应商不支持此模型或未开通(实测可用 enhance_config.json 指定其它供应商/模型)。请检查模型名称与套餐,或换一个供应商"}; if(/insufficient|balance|quota|exceeded|充值|余额/.test(m)) return{kind:"quota",retry:!1,tip:"额度或余额不足,请充值或切换供应商"}; if(/invalid[_ ]?api[_ ]?key|unauthorized|authentication|token|鉴权|令牌/.test(m)||status===401||status===403)