{"record":{"id":"68fe2c51b2fa5d6f","repo":"BigPizzaV3/CodexPlusPlus","slug":"result-message-codex","errorCode":null,"errorMessage":"result?.message||'Codex++ 会话读取失败'","messagePattern":"result\\?\\.message\\|\\|'Codex\\+\\+ 会话读取失败'","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":87,"sourceCode":"    rows.push({...row,childCount:children.length,active:view.activePath.has(row.node.id)});\n    if(!collapsed.has(row.node.id))for(let i=children.length-1;i>=0;i--)stack.push({node:children[i],depth:row.depth+1});\n  }\n  return {rows,pending:view.pending};\n}\n\n\nfunction treeMarkup(nodes,currentNodeId,collapsed=new Set(),selected=null,options={}){\n  const esc=s=>String(s??'').replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n  const view=visibleTreeRows(nodes,currentNodeId,collapsed),offset=options.offset||0,pendingOffset=options.pendingOffset||0;\n  const card=n=>`<button class=\"node ${selected===n.id?'selected':''} ${n.id===currentNodeId?'current':''}\" data-node=\"${esc(n.id)}\"><small>${esc(n.status)} · ${n.summaryKind==='原文摘录'?'原文摘录':'模型归纳'}${n.id===currentNodeId?' · 当前推进':''}</small><strong>${esc(n.title)}</strong><p>${esc(n.summary)}</p></button>`;\n  const tree=view.rows.length?`<ul class=\"task-tree\" aria-label=\"模型识别的任务树\">${view.rows.slice(offset,offset+100).map(({node:n,depth,childCount,active})=>`<li class=\"task-branch ${active?'active-path':''}\" style=\"margin-left:${Math.min(depth,8)*14}px\"><div class=\"tree-row\">${childCount?`<button class=\"tree-toggle\" data-collapse=\"${esc(n.id)}\" aria-label=\"${collapsed.has(n.id)?'展开':'收起'} ${esc(n.title)}\" aria-expanded=\"${!collapsed.has(n.id)}\">${collapsed.has(n.id)?'▸':'▾'}</button>`:'<span class=\"tree-leaf\" aria-hidden=\"true\">·</span>'}${card(n)}</div>${depth>8?`<small>第 ${depth+1} 层 · 详情中可查看完整路径</small>`:''}</li>`).join('')}</ul>`:'<p class=\"note\">点击「整理脉络」，由 Codex 识别目标、方案与多层尝试。</p>';\n  const pending=view.pending.length?`<details class=\"pending-messages\" ${options.pendingOpen?'open':''}><summary>待整理消息 · ${view.pending.length} 条</summary><p class=\"note\">任务归属将在下一次整理时识别。</p><div class=\"pending-list\">${view.pending.slice(pendingOffset,pendingOffset+20).map(card).join('')}</div><div class=\"tree-actions\"><button data-pending-page=\"${Math.max(0,pendingOffset-20)}\" ${pendingOffset===0?'disabled':''}>前 20 条</button><span>${pendingOffset+1}–${Math.min(pendingOffset+20,view.pending.length)}</span><button data-pending-page=\"${pendingOffset+20}\" ${pendingOffset+20>=view.pending.length?'disabled':''}>后 20 条</button></div></details>`:'';\n  return tree+pending;\n}\n\n\nfunction graphFromExport(result, threadId, annotations={nodes:[]}) {\n  if(result?.status!=='ok')throw new Error(result?.message||'Codex++ 会话读取失败');\n  if(result.kind!=='codex-rollout'||result.session_id!==threadId||typeof result.content!=='string')throw new Error('会话接口返回了不匹配的数据');\n  const messages=[],seen=new Set();\n  let declaredId=null;\n  // Ignore an unfinished JSONL tail; the next refresh will read the completed line.\n  const lines=result.content.slice(0,result.content.lastIndexOf('\\n')+1).split('\\n');\n  for(let i=0;i<lines.length;i++){\n    if(!lines[i].trim())continue;\n    let r;try{r=JSON.parse(lines[i]);}catch{continue;}\n    if(r.type==='session_meta')declaredId=r.payload?.id;\n    const m=parseMessage(r,`message-${threadId}-line-${i}`);\n    if(m&&!seen.has(m.id)){seen.add(m.id);messages.push(m);}\n  }\n  if(declaredId&&declaredId!==threadId)throw new Error('记录中的任务 ID 与当前任务不一致');\n  return {threadId,title:annotations.title||'当前对话',messages,...buildGraph(messages,annotations)};\n}\n\nfunction organizationPrompt(messages, requestId) {\n  const history=messages.map(({id,role,text})=>({id,role,text}));","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L69-L105","documentation":"graphFromExport() is the entry point that turns a Codex++ /api/session export into the canvas graph. It first verifies result.status === 'ok'; on any other status it throws with the backend-provided message, defaulting to the generic Chinese string 'Codex++ 会话读取失败'. This surfaces transport/backend-level failures (session read errors) distinctly from the subsequent shape checks (kind/session_id/content).","triggerScenarios":"Calling graphFromExport (via sync) with a result object whose status is 'error'/'failed'/undefined — e.g. the userscript's GM fetch to the Codex++ session API returned an error payload, timed out, or the backend failed to read the rollout file for threadId.","commonSituations":"Codex++ server not running or the session API path changed after an upgrade; the rollout file for the given threadId was deleted or is unreadable; response was HTML/JSON of a different shape so status is undefined; stale threadId passed after a session reset.","solutions":["Verify the Codex++ backend/session API is running and the response is JSON with status:'ok' (inspect the raw result in console).","Re-check the threadId passed to graphFromExport — refresh the session list and retry with a valid id.","Upgrade or re-sync canvas.user.js with the backend so the expected export contract matches.","Improve the throw to include result.status and threadId for faster diagnosis of which case fired."],"exampleFix":"// before\nif(result?.status!=='ok')throw new Error(result?.message||'Codex++ 会话读取失败');\n// after\nif(result?.status!=='ok')throw new Error(result?.message||`Codex++ 会话读取失败（status=${result?.status ?? '无响应'}，thread=${threadId}）`);","handlingStrategy":"type-guard","validationCode":"const isOkExport = (r, threadId) => r?.status === 'ok' && r?.kind === 'codex-rollout' && r?.session_id === threadId && typeof r?.content === 'string';\nif (!isOkExport(result, threadId)) { view.showError('会话导出不可用，请刷新后重试'); return; }","typeGuard":"function isValidExport(r) {\n  return r != null && typeof r === 'object' && r.status === 'ok' && r.kind === 'codex-rollout' && typeof r.content === 'string';\n}","tryCatchPattern":"try {\n  const graph = graphFromExport(result, threadId, annotations);\n} catch (e) {\n  if (String(e).includes('会话读取失败')) {\n    console.error('session export failed', { threadId, result });\n    view.showError('无法读取 Codex++ 会话，请确认 Codex++ 正在运行并重试。');\n  } else throw e;\n}","preventionTips":["Validate the export shape (status/kind/session_id/content) before calling graphFromExport.","Confirm the Codex++ backend is reachable and the session API contract matches the userscript version.","Log the raw result object when the guard fails to distinguish transport vs shape failures.","Re-fetch/refresh the session list rather than reusing a possibly stale threadId."],"tags":["userscript","api-response","session","error-handling"],"backgroundTag":"api-error-response","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}