{"record":{"id":"68b3b9c8be3e8d3a","repo":"BigPizzaV3/CodexPlusPlus","slug":"api-key-external","errorCode":null,"errorMessage":"请在设置中填写有效 API Key","messagePattern":"请在设置中填写有效 API Key","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/external-api.mjs","lineNumber":16,"sourceCode":"// OpenAI-compatible Chat Completions. Never persist credentials in tree checkpoints.\nexport function apiEndpoint(raw){\n  let url;try{url=new URL(String(raw).trim());}catch{throw Error('请输入完整的 HTTPS API 地址');}\n  if(url.protocol!=='https:'||url.username||url.password||url.search||url.hash)throw Error('API 地址须使用 HTTPS，且不含账号、密码、查询参数或片段');\n  const path=url.pathname.replace(/\\/+$/,'');\n  url.pathname=path.endsWith('/chat/completions')?path:(path||'/v1')+'/chat/completions';\n  return url.href;\n}\n\nexport function apiConfig(input){\n  const channel=input?.channel==='external'?'external':'native';\n  const value={channel,baseUrl:String(input?.baseUrl||'').trim(),model:String(input?.model||'').trim(),key:String(input?.key||'').trim(),remember:input?.remember===true,speed:input?.speed==='provider'?'provider':'fast',revision:input?.revision||crypto.randomUUID()};\n  if(channel==='external'){\n    value.endpoint=apiEndpoint(value.baseUrl);\n    if(!value.model||value.model.length>200)throw Error('请填写 API 的模型名称');\n    if(!value.key||/[\\r\\n]/.test(value.key))throw Error('请在设置中填写有效 API Key');\n  }\n  return value;\n}\n\nexport function storedApiConfig(config){\n  const {channel,baseUrl,model,remember,speed,revision}=config;\n  return {channel,baseUrl,model,remember,speed,revision,...remember?{key:config.key}:{}};\n}\n\nexport function apiError(error){\n  if(error?.name==='AbortError')return error;\n  if(error?.canvasApiLocal===true)return error;\n  const code=Number(error?.status??error?.responseStatus);\n  const hint={401:'密钥无效或已过期',403:'接口拒绝访问，请检查权限',404:'地址或模型不存在',408:'接口请求超时',413:'本批资料超过接口大小限制',429:'接口限流或额度不足'}[code];\n  // Provider messages can echo request contents and Authorization; never display them.\n  return Object.assign(Error(hint?`API ${code}：${hint}`:code>=400?`API 请求失败（HTTP ${code}），请检查服务状态`:'API 连接失败，请检查地址、网络及服务状态'),{retryable:!code||code===408||code===429||code>=500});\n}\n","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/external-api.mjs#L1-L34","documentation":"For the external channel, apiConfig() requires a non-empty API key that contains no CR/LF characters. Header injection via newlines is blocked, and an empty key would produce an unusable Authorization header. This error means the key field was blank or contained line breaks.","triggerScenarios":"apiConfig({channel:'external',...,key:''}) or key:'sk-abc\\n malicious-header: x'; thrown through readApiForm/config when the settings form has no key or a multiline paste.","commonSituations":"User forgets to paste the API key when switching to external channel; key copied with a trailing newline from a terminal or PDF; key field contains multiple lines from a bad paste; remember=false and the stored config has no key after reload.","solutions":["Paste the provider API key into the API Key field in settings (keys are never persisted in tree checkpoints unless 'remember' is enabled)","Strip whitespace/newlines from the pasted key before saving","Re-enter the key if the app was restarted with remember=false, since it is intentionally not stored","Get a new key from the provider if the old one is missing/revoked"],"exampleFix":"// before\nconst key = pastedKey; // 'sk-abc\\n'\n// after\nconst key = pastedKey.replace(/[\\r\\n]/g, '').trim();","handlingStrategy":"validation","validationCode":"const key = String(input?.key || '').trim();\nif (!key || /[\\r\\n]/.test(key)) throw new Error('请在设置中填写有效 API Key');","typeGuard":"function hasValidKey(c) { return typeof c?.key === 'string' && c.key.trim().length > 0 && !/[\\r\\n]/.test(c.key); }","tryCatchPattern":"let cfg;\ntry { cfg = apiConfig(formValues); } catch (e) { if (String(e).includes('API Key')) openSettingsAndFocusKeyField(); return; }","preventionTips":["Sanitize pasted keys with .replace(/[\\r\\n]/g,'').trim()","Prompt for the key immediately when the user switches to the external channel","Don't rely on remembered keys after restart when remember=false — check and re-prompt","Use single-line password inputs that strip newlines on paste"],"tags":["api-key","auth","validation"],"backgroundTag":"missing-api-key","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"}