Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions skills/zcode-tokenspeed/ENHANCE_CUSTOM_PLAN.md
Original file line number Diff line number Diff line change
@@ -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` 即回到纯界面选择行为。
34 changes: 22 additions & 12 deletions skills/zcode-tokenspeed/scripts/zcode-enhance-prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
29 changes: 24 additions & 5 deletions skills/zcode-tokenspeed/scripts/zcode_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
// ⓪ 增强专用热配置 <root>/enhance_config.json:每次点击都重新读取,改文件即生效、无需重启。
// providerId/modelId 留空或文件不存在时,行为与旧版完全一致(用界面当前选中的模型)。
let cfg=rd(r.join(root,"enhance_config.json"))||{};
let srcTried=[];
// ① provider_config.json(权威)→ 归一化
let pcCands=[];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
Loading