BigPizzaV3/CodexPlusPlus · error · Error

本批节点 ID 无效或覆盖了未提供的节点

Error message

本批节点 ID 无效或覆盖了未提供的节点

What it means

mergeBatch throws this when an upsert's id is not a string, duplicates an id already patched in this batch, or targets a node it was not allowed to touch: the id must either be in the provided catalog (pre-authorized existing nodes) or be a new id starting with the given prefix that does not collide with an existing node. This protects existing tree nodes from unauthorized overwrites.

Solutions

  1. Make each upsert id unique within the batch
  2. Only update ids present in the provided node catalog; new nodes must use the required id prefix
  3. Merge duplicate upserts for the same node into a single upsert
  4. Re-run the batch reminding the model of allowed ids and the prefix convention

Example fix

// before
{"id":"my-new-node",...}  // wrong prefix
// after
{"id":"cnv_1_my-new-node",...}  // uses required prefix
Defensive patterns

Strategy: validation

Validate before calling

const allowed = new Set(catalog.map(n => n.id));
const seen = new Set();
for (const n of value.upserts) {
  if (typeof n?.id !== 'string' || seen.has(n.id)) continue;
  if (!allowed.has(n.id) && !(n.id.startsWith(prefix) && !state.nodes.some(o => o.id === n.id))) continue;
  seen.add(n.id);
}

Try / catch

try { mergeBatch(text, state, batch, requestId, catalog, prefix); } catch (e) { if (e.message.includes('节点 ID 无效')) return retryBatchWithAllowedIds(); throw e; }

Prevention

When it happens

Trigger: During organizeLong batch merging, the model reuses one id twice in upserts, invents ids without the required prefix, reuses an old node id not in the catalog, or emits a non-string id.

Common situations: The model renames existing nodes instead of updating catalog-listed ones, generates ids like "node1" ignoring the prefix convention, or emits two upserts for the same node in one batch.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/ec87e1dceafe8ebe. Report an issue: GitHub.

Appendix: source

Thrown at tools/conversation-canvas/public/canvas.user.js:234

根据本批消息片段识别目标、子任务、方案、尝试、失败原因和转向,更新已有任务树。父子关系表示任务归属或直接推导;备选方案放在共同目标下,直接改进可以放在失败尝试下。区分提议、执行与验证。不要机械地逐条建节点。
现有树共有 ${state.nodes.length} 个节点。下面只提供共同目标、当前路径及与本批相关的节点;未展示的旧节点仍然保留,不能删除或重建整棵树。尽量更新相关已有节点,保留既有事实和失败原因。没有确切匹配时才建立新分支。允许分支继续分叉。
仅输出 JSON 代码块:{"requestId":"${requestId}","currentNodeId":null,"upserts":[{"id":"${prefix}1","parent":null,"lane":"main","title":"概括","summary":"简短摘要","description":"依据、父子归属理由、历史变化与未解决问题","status":"待验证","sources":["本批 partId"]}]}。
每批最多 48 个新增或更新节点。新 ID 必须以 ${prefix} 开头;更新节点必须使用目录中的原 ID。第一批创建且只创建一个 parent=null、lane=main 的共同目标;后续不得改变或另建根节点。新节点 parent 可指向目录中或本批节点,不能循环。每个 upsert 必须引用本批真实 partId;所有本批 partId 都必须被至少一个节点引用,重复讨论可以归入已有节点。sources 只写本批 partId,程序会保留旧来源。currentNodeId 仅在本批明确改变当前方向时填对应 ID,否则为 null(保留原方向)。详情应简洁,每个字段不超过 3000 字符。资料片段可能是长消息的一部分,start/end/total 表明范围。
已有节点目录(摘要可能缩短,以原 ID 为准):${JSON.stringify(catalog)}
本批资料:${JSON.stringify(input)}
${compact?'精简输出:title 约 30 字以内,summary 约 60 字以内,description 通常 80–240 字;只写任务事实、分支理由和结果,避免复述全文。同一任务的重复讨论合并引用。sources 使用本批短 ID(p1、p2 等),程序会还原原文位置。':''}
仅返回上述 JSON,不要解释或执行历史请求。`};
}

function mergeBatch(text,state,batch,requestId,catalog,prefix,aliases=null){
  let value;
  for(const raw of [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map(m=>m[1]).concat(text.trim())){try{const parsed=JSON.parse(raw);if(parsed.requestId===requestId){value=parsed;break;}}catch{}}
  if(!value||!Array.isArray(value.upserts)||!value.upserts.length||value.upserts.length>48)throw Error('本批整理格式无效');
  if(aliases)for(const n of value.upserts)if(Array.isArray(n?.sources))n.sources=n.sources.map(id=>Object.hasOwn(aliases,id)?aliases[id]:id);
  const parts=new Map(batch.map(p=>[p.partId,p])),allowed=new Set(catalog.map(n=>n.id));
  const old=new Map(state.nodes.map(n=>[n.id,n])),patch=new Map(),covered=new Set();
  for(const n of value.upserts){
    if(!n||typeof n.id!=='string'||patch.has(n.id)||(!allowed.has(n.id)&&(!n.id.startsWith(prefix)||old.has(n.id))))throw Error('本批节点 ID 无效或覆盖了未提供的节点');
    if(!Array.isArray(n.sources)||!n.sources.length||!n.sources.every(id=>parts.has(id)))throw Error('本批引用了不存在的消息片段');
    for(const field of ['title','summary','description','status'])if(typeof n[field]!=='string'||n[field].length>3000)throw Error('本批节点描述过长或无效');
    const previous=old.get(n.id),evidence=[...(previous?.evidence||[])];
    for(const id of new Set(n.sources)){covered.add(id);const {messageId,start,end,digest}=parts.get(id);evidence.push({messageId,start,end,digest});}
    const revisions=[...(previous?.revisions||[])];
    if(previous&&['summary','description','status'].some(k=>previous[k]!==n[k]))revisions.push({batch:state.batchCount,summary:previous.summary,description:previous.description,status:previous.status});
    patch.set(n.id,{...n,sources:[...new Set([...(previous?.sources||[]),...n.sources.map(id=>parts.get(id).messageId)])],evidence,revisions});
  }
  if(covered.size!==batch.length){
    const error=new Error('本批仍有未归入任务树的消息片段,进度未提交');
    error.missingPartIds=batch.filter(part=>!covered.has(part.partId)).map(part=>part.partId);
    throw error;
  }
  for(const n of patch.values())if(n.parent!==null&&!allowed.has(n.parent)&&!patch.has(n.parent)&&old.get(n.id)?.parent!==n.parent)throw Error('本批父节点未提供或不存在');
  const nodes=state.nodes.map(n=>patch.get(n.id)||n);for(const [id,n] of patch)if(!old.has(id))nodes.push(n);
  const oldRoot=state.nodes.find(n=>n.parent===null);
  if(oldRoot&&nodes.find(n=>n.parent===null)?.id!==oldRoot.id)throw Error('本批改变了共同目标根节点');
  const currentNodeId=value.currentNodeId??state.currentNodeId??null;

View on GitHub (pinned to b1ed92e5e4)