From a77b2f160703a658f179f604eb1ce460fabd605d Mon Sep 17 00:00:00 2001 From: magicalastrogy Date: Mon, 19 Oct 2026 07:29:17 +0800 Subject: [PATCH 1/4] chore: upgrade pi-ai to 1.3.0 and align provider adapters --- artifact/bundle.js | 2 +- artifact/bundle.js.map | 2 +- doc/pi_multiprovider_integration.md | 14 +++-- package.json | 2 +- src/function/update/model_list.ts | 4 +- .../update/pi/google_proxy_adapter.ts | 63 ++----------------- src/function/update/pi/pi_gateway.ts | 3 + src/function/update/pi/provider_target.ts | 35 ++++++----- src/panel/open_source_licenses.ts | 4 +- tests/model_list.test.ts | 6 +- tests/pi_google_proxy_adapter.test.ts | 9 ++- tests/pi_provider_catalog_contract.test.ts | 62 ++++++++++++++++++ tests/pi_provider_registry.test.ts | 35 ++++++----- tests/pi_runtime.test.ts | 4 +- yarn.lock | 30 ++++----- 15 files changed, 155 insertions(+), 120 deletions(-) diff --git a/artifact/bundle.js b/artifact/bundle.js index 5c4d80d..b0918c3 100644 --- a/artifact/bundle.js +++ b/artifact/bundle.js @@ -1,3 +1,3 @@ /*! For license information please see bundle.js.LICENSE.txt */ -import{GoogleGenAI as e}from'https://testingcf.jsdelivr.net/npm/@google/genai@2.21.0/+esm';import{calculateCost as t,clampThinkingLevel as n,collapseSystemMessages as o,createAssistantMessageEventStream as r,createModels as a,createProvider as i,getCurrentSystemPrompt as s,getCurrentTools as l}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/+esm';import{ANT_LING_MODELS as c}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/ant-ling.models/+esm';import{ANTHROPIC_MODELS as u}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/anthropic.models/+esm';import{BASETEN_MODELS as p}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/baseten.models/+esm';import{CEREBRAS_MODELS as d}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/cerebras.models/+esm';import{DEEPSEEK_MODELS as m}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/deepseek.models/+esm';import{FIREWORKS_MODELS as h}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/fireworks.models/+esm';import{GITHUB_COPILOT_MODELS as f}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/github-copilot.models/+esm';import{GOOGLE_MODELS as g}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/google.models/+esm';import{GROQ_MODELS as v}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/groq.models/+esm';import{HUGGINGFACE_MODELS as b}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/huggingface.models/+esm';import{KIMI_CODING_MODELS as y}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/kimi-coding.models/+esm';import{MINIMAX_MODELS as w}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/minimax.models/+esm';import{MINIMAX_CN_MODELS as C}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/minimax-cn.models/+esm';import{MISTRAL_MODELS as A}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/mistral.models/+esm';import{MOONSHOTAI_MODELS as k}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/moonshotai.models/+esm';import{MOONSHOTAI_CN_MODELS as x}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/moonshotai-cn.models/+esm';import{NVIDIA_MODELS as N}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/nvidia.models/+esm';import{OPENAI_MODELS as S}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/openai.models/+esm';import{OPENAI_CODEX_MODELS as I}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/openai-codex.models/+esm';import{OPENCODE_MODELS as E}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/opencode.models/+esm';import{OPENCODE_GO_MODELS as T}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/opencode-go.models/+esm';import{OPENROUTER_MODELS as P}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/openrouter.models/+esm';import{QWEN_TOKEN_PLAN_MODELS as M}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/qwen-token-plan.models/+esm';import{QWEN_TOKEN_PLAN_CN_MODELS as O}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/qwen-token-plan-cn.models/+esm';import{QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS as B}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/qwen-token-plan-individual.models/+esm';import{TOGETHER_MODELS as V}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/together.models/+esm';import{VERCEL_AI_GATEWAY_MODELS as U}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/vercel-ai-gateway.models/+esm';import{XAI_MODELS as R}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/xai.models/+esm';import{XIAOMI_MODELS as j}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/xiaomi.models/+esm';import{XIAOMI_TOKEN_PLAN_AMS_MODELS as L}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/xiaomi-token-plan-ams.models/+esm';import{XIAOMI_TOKEN_PLAN_CN_MODELS as F}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/xiaomi-token-plan-cn.models/+esm';import{XIAOMI_TOKEN_PLAN_SGP_MODELS as W}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/xiaomi-token-plan-sgp.models/+esm';import{ZAI_MODELS as D}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/zai.models/+esm';import{ZAI_CODING_CN_MODELS as G}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/providers/zai-coding-cn.models/+esm';import{openAIResponsesApi as q}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/openai-responses.lazy/+esm';import{openAICompletionsApi as H}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/openai-completions.lazy/+esm';import{anthropicMessagesApi as Y}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/anthropic-messages.lazy/+esm';import{googleGenerativeAIApi as Z}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/google-generative-ai.lazy/+esm';import{mistralConversationsApi as J}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/mistral-conversations.lazy/+esm';import{openAICodexResponsesApi as X}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/openai-codex-responses.lazy/+esm';import{convertMessages as K,convertTools as Q,isThinkingPart as ee,mapStopReasonString as te,resolveGoogleFunctionCallingMode as ne,resolveGoogleThinkingLevel as oe,retainThoughtSignature as re,retryGoogleRequest as ae,supportsGoogleStrictToolSampling as ie}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/google-shared/+esm';import{buildBaseOptions as se}from'https://testingcf.jsdelivr.net/npm/@earendil-works/pi-ai@1.0.0/api/simple-options/+esm';import{AST_NODE_PROPS_KEYS as le,CORE_ERROR_CODES_EXTEND_POINT as ce,CORE_WARN_CODES_EXTEND_POINT as ue,DATETIME_FORMAT_OPTIONS_KEYS as pe,DEFAULT_LOCALE as de,MISSING_RESOLVE_VALUE as me,NOT_REOSLVED as he,NUMBER_FORMAT_OPTIONS_KEYS as fe,clearDateTimeFormat as ge,clearNumberFormat as ve,compile as be,createCompileError as ye,createCoreContext as _e,datetime as we,fallbackWithLocaleChain as Ce,getFallbackContext as Ae,isMessageAST as ke,isMessageFunction as xe,number as Ne,parseDateTimeArgs as Se,parseNumberArgs as Ie,parseTranslateArgs as Ee,registerLocaleFallbacker as Te,registerMessageCompiler as Pe,registerMessageResolver as Me,resolveValue as Oe,setFallbackContext as Be,translate as ze,updateFallbackLocale as Ve}from'https://testingcf.jsdelivr.net/npm/@intlify/core-base/+esm';import{assign as Ue,create as Re,deepCopy as je,hasOwn as Le,inBrowser as Fe,isArray as We,isBoolean as De,isEmptyObject as Ge,isFunction as qe,isNumber as $e,isObject as He,isPlainObject as Ye,isRegExp as Ze,isString as Je,makeSymbol as Xe}from'https://testingcf.jsdelivr.net/npm/@intlify/shared/+esm';import{jsonrepair as Ke}from'https://testingcf.jsdelivr.net/npm/jsonrepair/+esm';import*as Qe from'https://testingcf.jsdelivr.net/npm/compare-versions/+esm';import*as et from'https://testingcf.jsdelivr.net/npm/json5/+esm';import*as tt from'https://testingcf.jsdelivr.net/npm/klona/+esm';import*as nt from'https://testingcf.jsdelivr.net/npm/mathjs/+esm';var ot={9345(e,t,n){n.r(t),n.d(t,{default:()=>s});var o=n(1354),r=n.n(o),a=n(6314),i=n.n(a)()(r());i.push([e.id,'.mvu-button-wrap[data-v-543ceaa4]{display:flex;flex-wrap:wrap;gap:0.5rem 0.6rem;align-items:center}.mvu-button-wrap[data-v-543ceaa4] .menu_button{box-sizing:border-box;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;padding:0.35rem 0.6rem;min-height:unset;height:2.05rem;line-height:1.1}\n','',{version:3,sources:['webpack://./src/panel/Button.vue'],names:[],mappings:'AAoCA,kCACI,YAAa,CACb,cAAe,CACf,iBAAkB,CAClB,kBACJ,CAEA,+CACI,qBAAsB,CACtB,eAAgB,CAChB,kBAAmB,CACnB,eAAgB,CAChB,sBAAuB,CACvB,0BAA2B,CAC3B,sBAAuB,CACvB,gBAAiB,CACjB,cAAe,CACf,eACJ',sourcesContent:['\n\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-character-override__metadata[data-v-ac919d4c]{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:0.25rem 0.7rem;margin:0;padding:0.35rem 0.55rem;border-radius:8px;background-color:rgba(0,0,0,0.08)}.mvu-character-override__metadata dt[data-v-ac919d4c]{font-weight:600;opacity:0.82}.mvu-character-override__metadata dd[data-v-ac919d4c]{min-width:0;margin:0;overflow-wrap:anywhere}.mvu-character-override__fieldset[data-v-ac919d4c]{min-inline-size:0;margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:0.45rem}.mvu-character-override__fieldset[data-v-ac919d4c]:disabled{opacity:0.6}.mvu-character-override__group[data-v-ac919d4c]{display:flex;flex-direction:column;gap:0.3rem;padding-top:0.1rem}.mvu-character-override__group-title[data-v-ac919d4c]{padding:0 0.15rem}.mvu-character-override__select-field.mvu-field[data-v-ac919d4c]{flex-flow:row nowrap;align-items:center;gap:0.4rem;padding:0.3rem 0.5rem}.mvu-character-override__select-field>.text_pole[data-v-ac919d4c]{flex:1 1 auto;width:auto;min-width:0;margin:0}.mvu-character-override__regex-error[data-v-ac919d4c]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/CharacterOverride.vue\"],\"names\":[],\"mappings\":\"AAsMA,mDACI,YAAa,CACb,+CAAiD,CACjD,kBAAmB,CACnB,QAAS,CACT,uBAAwB,CACxB,iBAAkB,CAClB,iCACJ,CAEA,sDACI,eAAgB,CAChB,YACJ,CAEA,sDACI,WAAY,CACZ,QAAS,CACT,sBACJ,CAEA,mDACI,iBAAkB,CAClB,QAAS,CACT,SAAU,CACV,QAAS,CACT,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,4DACI,WACJ,CAEA,gDACI,YAAa,CACb,qBAAsB,CACtB,UAAW,CACX,kBACJ,CAEA,sDACI,iBACJ,CAEA,iEACI,oBAAqB,CACrB,kBAAmB,CACnB,UAAW,CACX,qBACJ,CAEA,kEACI,aAAc,CACd,UAAW,CACX,WAAY,CACZ,QACJ,CAEA,sDACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-license-table-wrap[data-v-67402f54]{max-height:18rem;overflow:auto;border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:6px}.mvu-license-table[data-v-67402f54]{width:100%;border-collapse:collapse;font-size:0.9em}.mvu-license-table th[data-v-67402f54],.mvu-license-table td[data-v-67402f54]{padding:0.35rem 0.5rem;text-align:start;vertical-align:top;border-bottom:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,0.45))}.mvu-license-table th[data-v-67402f54]:last-child,.mvu-license-table td[data-v-67402f54]:last-child{width:7.5rem;white-space:nowrap}.mvu-license-table tbody tr:last-child td[data-v-67402f54]{border-bottom:0}.mvu-license-table a[data-v-67402f54]{overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Compatibility.vue\"],\"names\":[],\"mappings\":\"AA0GA,yCACI,gBAAiB,CACjB,aAAc,CACd,8DAAmE,CACnE,iBACJ,CAEA,oCACI,UAAW,CACX,wBAAyB,CACzB,eACJ,CAEA,8EAEI,sBAAuB,CACvB,gBAAiB,CACjB,kBAAmB,CACnB,wEACJ,CAEA,oGAEI,YAAa,CACb,kBACJ,CAEA,2DACI,eACJ,CAEA,sCACI,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.inline-drawer-content[data-v-7a58cca8]{flex-direction:column;gap:0.75rem;padding-top:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Panel.vue\"],\"names\":[],\"mappings\":\"AAmCA,wCACI,qBAAsB,CACtB,WAAY,CACZ,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-details[data-v-3a7b912a]{border:1px dashed var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.5rem 0.7rem;background-color:rgba(0,0,0,0.06);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 70%,transparent)}.mvu-details__summary[data-v-3a7b912a]{cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;opacity:0.95}.mvu-details__title-suffix[data-v-3a7b912a]{display:inline-flex;align-items:center;margin-inline-start:0.35rem;vertical-align:middle}.mvu-details__content[data-v-3a7b912a]{margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Detail.vue\"],\"names\":[],\"mappings\":\"AAwBA,8BACI,+DAAoE,CACpE,kBAAmB,CACnB,qBAAsB,CACtB,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,cAAe,CACf,wBAAiB,CAAjB,gBAAiB,CACjB,eAAgB,CAChB,YACJ,CAEA,4CACI,mBAAoB,CACpB,kBAAmB,CACnB,2BAA4B,CAC5B,qBACJ,CAEA,uCACI,iBAAkB,CAClB,YAAa,CACb,qBAAsB,CACtB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field[data-v-1bd30ada]{padding:0.45rem 0.6rem;gap:0.25rem;border:1px solid color-mix(in srgb,var(--SmartThemeBorderColor,rgba(45,45,45,1)) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 55%,transparent)}.mvu-field__label[data-v-1bd30ada]{display:inline-flex;align-items:center;gap:0.35rem;font-weight:600;opacity:0.95}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Field.vue\"],\"names\":[],\"mappings\":\"AAeA,4BACI,sBAAuB,CACvB,WAAY,CACZ,iGAAwG,CACxG,kBAAmB,CACnB,mGAKJ,CAEA,mCACI,mBAAoB,CACpB,kBAAmB,CACnB,WAAY,CACZ,eAAgB,CAChB,YACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-help-icon[data-v-7a641daa]{cursor:pointer}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/HelpIcon.vue\"],\"names\":[],\"mappings\":\"AAoBA,gCACI,cACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-model-select[data-v-7b6ca08a]{display:flex;flex-direction:column;gap:0.5rem}.mvu-model-select__row[data-v-7b6ca08a]{width:100%}.mvu-model-select__row--controls[data-v-7b6ca08a]{display:grid;grid-template-columns:1fr auto;gap:0.5rem;align-items:center}.mvu-model-select__btn[data-v-7b6ca08a]{white-space:nowrap;text-align:left;padding:0.35rem 0.6rem;min-height:unset;height:2.05rem;line-height:1.1}@media (max-width:520px){.mvu-model-select__row--controls[data-v-7b6ca08a]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/ModelSelect.vue\"],\"names\":[],\"mappings\":\"AA4JA,mCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,wCACI,UACJ,CAEA,kDACI,YAAa,CACb,8BAA+B,CAC/B,UAAW,CACX,kBACJ,CAEA,wCACI,kBAAmB,CACnB,eAAgB,CAChB,sBAAuB,CACvB,gBAAiB,CACjB,cAAe,CACf,eACJ,CAEA,yBACI,kDACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-override-badge[data-v-14bdab4e]{display:inline-flex;flex:0 1 auto;align-items:center;max-width:100%;padding:0.08rem 0.38rem;border:1px solid currentColor;border-radius:999px;font-size:calc(var(--mainFontSize,1rem) * 0.76);font-weight:600;line-height:1.25;text-align:center;overflow-wrap:anywhere}.mvu-override-badge--override[data-v-14bdab4e]{color:var(--SmartThemeEmColor,#d39e00);background-color:color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 12%,transparent)}.mvu-override-badge--additive[data-v-14bdab4e]{color:var(--SmartThemeQuoteColor,#6ba7ff);background-color:color-mix(in srgb,var(--SmartThemeQuoteColor,#6ba7ff) 12%,transparent)}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/OverrideBadge.vue\"],\"names\":[],\"mappings\":\"AAuBA,qCACI,mBAAoB,CACpB,aAAc,CACd,kBAAmB,CACnB,cAAe,CACf,uBAAwB,CACxB,6BAA8B,CAC9B,mBAAoB,CACpB,+CAAiD,CACjD,eAAgB,CAChB,gBAAiB,CACjB,iBAAkB,CAClB,sBACJ,CAEA,+CACI,sCAAwC,CACxC,oFACJ,CAEA,+CACI,yCAA2C,CAC3C,uFACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-range-number[data-v-48562df7]{display:grid;grid-template-columns:1fr 7.5rem;gap:0.5rem;align-items:center}.mvu-range-number__range[data-v-48562df7]{width:100%}.mvu-range-number__number[data-v-48562df7]{text-align:left;padding-top:0.3rem;padding-bottom:0.3rem;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 33%,transparent)}@media (max-width:420px){.mvu-range-number[data-v-48562df7]{grid-template-columns:1fr 6.5rem}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/RangeNumber.vue\"],\"names\":[],\"mappings\":\"AA2CA,mCACI,YAAa,CACb,gCAAiC,CACjC,UAAW,CACX,kBACJ,CAEA,0CACI,UACJ,CAEA,2CACI,eAAgB,CAChB,kBAAmB,CACnB,qBAAsB,CACtB,mGAKJ,CAEA,yBACI,mCACI,gCACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-section[data-v-ab897094]{border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.6rem 0.75rem;gap:0.45rem;background-color:rgba(0,0,0,0.08);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 75%,transparent)}.mvu-section__content[data-v-ab897094]{gap:0.5rem}.mvu-section__title[data-v-ab897094]{display:flex;flex-wrap:wrap;align-items:center;gap:0.3rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Section.vue\"],\"names\":[],\"mappings\":\"AAmBA,8BACI,8DAAmE,CACnE,kBAAmB,CACnB,sBAAuB,CACvB,WAAY,CACZ,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,UACJ,CAEA,qCACI,YAAa,CACb,cAAe,CACf,kBAAmB,CACnB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-warning[data-v-58f80c4e]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-58f80c4e]{line-height:1}.mvu-warning__text[data-v-58f80c4e]{word-break:break-word}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Method.vue\"],\"names\":[],\"mappings\":\"AA4CA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-note[data-v-7ae42438]{padding:0 0.6rem 0.45rem;opacity:0.8;font-size:calc(var(--mainFontSize,1rem) * 0.9)}.mvu-regex-error[data-v-7ae42438]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;word-break:break-word}.mvu-regex-actions[data-v-7ae42438]{display:flex;padding:0 0.6rem 0.45rem}.mvu-regex-actions__button[data-v-7ae42438]{min-height:2rem;white-space:normal}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Prompt.vue\"],\"names\":[],\"mappings\":\"AA0MA,2BACI,wBAAyB,CACzB,WAAY,CACZ,8CACJ,CAEA,kCACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,qBACJ,CAEA,oCACI,YAAa,CACb,wBACJ,CAEA,4CACI,eAAgB,CAChB,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field-grid[data-v-0edef090]{display:flex;flex-direction:column;gap:0.5rem}.mvu-api-profile-controls[data-v-0edef090]{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem}.mvu-api-profile-actions[data-v-0edef090]{display:flex;flex-wrap:wrap;gap:0.5rem}.mvu-note[data-v-0edef090]{opacity:0.85;color:var(--SmartThemeEmColor,inherit)}.mvu-pi-proxy-option[data-v-0edef090]{padding:0 0.6rem}.mvu-warning[data-v-0edef090]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-0edef090]{line-height:1}.mvu-warning__text[data-v-0edef090]{word-break:break-word}.mvu-pi-advanced-textarea[data-v-0edef090]{width:100%;min-height:5rem;resize:vertical;font-family:var(--monoFontFamily,monospace);white-space:pre}.mvu-field-error[data-v-0edef090]{color:var(--SmartThemeQuoteColor,#ff6b6b);line-height:1.35;word-break:break-word}.mvu-oauth-status[data-v-0edef090]{display:flex;flex-direction:column;gap:0.25rem}.mvu-link-button[data-v-0edef090]{display:inline-flex;align-items:center;text-decoration:none}@media (max-width:520px){.mvu-api-profile-controls[data-v-0edef090]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Source.vue\"],\"names\":[],\"mappings\":\"AA0wDA,iCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,2CACI,YAAa,CACb,6BAA8B,CAC9B,UACJ,CAEA,0CACI,YAAa,CACb,cAAe,CACf,UACJ,CAEA,2BACI,YAAa,CACb,sCACJ,CAEA,sCACI,gBACJ,CAEA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ,CAEA,2CACI,UAAW,CACX,eAAgB,CAChB,eAAgB,CAChB,2CAA6C,CAC7C,eACJ,CAEA,kCACI,yCAA2C,CAC3C,gBAAiB,CACjB,qBACJ,CAEA,mCACI,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,kCACI,mBAAoB,CACpB,kBAAmB,CACnB,oBACJ,CAEA,yBACI,2CACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = [];\n\n // return the list of modules as css string\n list.toString = function toString() {\n return this.map(function (item) {\n var content = \"\";\n var needLayer = typeof item[5] !== \"undefined\";\n if (item[4]) {\n content += \"@supports (\".concat(item[4], \") {\");\n }\n if (item[2]) {\n content += \"@media \".concat(item[2], \" {\");\n }\n if (needLayer) {\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\n }\n content += cssWithMappingToString(item);\n if (needLayer) {\n content += \"}\";\n }\n if (item[2]) {\n content += \"}\";\n }\n if (item[4]) {\n content += \"}\";\n }\n return content;\n }).join(\"\");\n };\n\n // import a list of modules into the list\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === \"string\") {\n modules = [[null, modules, undefined]];\n }\n var alreadyImportedModules = {};\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n if (typeof layer !== \"undefined\") {\n if (typeof item[5] === \"undefined\") {\n item[5] = layer;\n } else {\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\n item[5] = layer;\n }\n }\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\n item[2] = media;\n }\n }\n if (supports) {\n if (!item[4]) {\n item[4] = \"\".concat(supports);\n } else {\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\n item[4] = supports;\n }\n }\n list.push(item);\n }\n };\n return list;\n};","\"use strict\";\n\nmodule.exports = function (item) {\n var content = item[1];\n var cssMapping = item[3];\n if (!cssMapping) {\n return content;\n }\n if (typeof btoa === \"function\") {\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n var sourceMapping = \"/*# \".concat(data, \" */\");\n return [content].concat([sourceMapping]).join(\"\\n\");\n }\n return [content].join(\"\\n\");\n};","import { compileEntryCommentRegex } from '@/function/request/entry_comment_regex';\nimport { tr } from '@/i18n';\nimport { parseString } from '@util/common';\nimport { klona } from 'klona';\nimport * as z from 'zod';\n\nexport const CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME = '[config_override]';\n\n// loose 模式用于保留未来版本或其他扩展写入的未知配置字段。\nexport const CharacterSettingsOverrideSchema = z\n .object({\n 更新方式: z.enum(['随AI输出', '额外模型解析']).optional(),\n 额外模型解析配置: z\n .object({\n 启用自动请求: z.boolean().optional(),\n 世界书条目白名单正则: z.string().optional(),\n 世界书条目黑名单正则: z.string().optional(),\n })\n .loose()\n .optional(),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().optional(),\n sendas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .optional(),\n })\n .loose();\n\nexport type CharacterSettingsOverride = z.infer;\n\nexport type CharacterSettingsOverridePath =\n | '更新方式'\n | '额外模型解析配置.启用自动请求'\n | '额外模型解析配置.世界书条目白名单正则'\n | '额外模型解析配置.世界书条目黑名单正则'\n | '兼容性.更新到聊天变量'\n | '兼容性.sendas不视为user消息';\n\nexport type CharacterSettingsOverrideValue = string | boolean | undefined;\n\nexport function isCharacterSettingsOverrideEntryName(name: unknown): boolean {\n return (\n typeof name === 'string' &&\n name.trim().toLowerCase() === CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME\n );\n}\n\nexport function hasOwnCharacterSettingsOverride(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): boolean {\n return _.has(draft, path);\n}\n\nexport function getCharacterSettingsOverrideValue(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): CharacterSettingsOverrideValue {\n return _.get(draft, path) as CharacterSettingsOverrideValue;\n}\n\nfunction removeEmptyKnownValues(draft: CharacterSettingsOverride): CharacterSettingsOverride {\n // “继承全局配置”在文件中表示字段不存在,而不是保存空字符串或空对象。\n const result = klona(draft);\n const extra_model = result.额外模型解析配置;\n if (extra_model) {\n for (const key of ['世界书条目白名单正则', '世界书条目黑名单正则'] as const) {\n if (extra_model[key]?.trim() === '') {\n delete extra_model[key];\n }\n }\n if (Object.keys(extra_model).length === 0) {\n delete result.额外模型解析配置;\n }\n }\n if (result.兼容性 && Object.keys(result.兼容性).length === 0) {\n delete result.兼容性;\n }\n return result;\n}\n\nexport function normalizeCharacterSettingsOverride(\n draft: CharacterSettingsOverride\n): CharacterSettingsOverride {\n return CharacterSettingsOverrideSchema.parse(removeEmptyKnownValues(draft));\n}\n\nfunction makeAdditionalPropertiesExplicit(schema: unknown): unknown {\n // Zod 会用空对象表示允许附加属性;显式改为 true 便于其他 JSON Schema 工具识别。\n if (Array.isArray(schema)) {\n return schema.map(makeAdditionalPropertiesExplicit);\n }\n if (!_.isPlainObject(schema)) {\n return schema;\n }\n\n const result = Object.fromEntries(\n Object.entries(schema as Record).map(([key, value]) => [\n key,\n key === 'additionalProperties' && _.isPlainObject(value) && _.isEmpty(value)\n ? true\n : makeAdditionalPropertiesExplicit(value),\n ])\n );\n return result;\n}\n\nexport function getCharacterSettingsOverrideJsonSchema(): Record {\n const generated = z.toJSONSchema(CharacterSettingsOverrideSchema, {\n target: 'draft-2020-12',\n });\n return {\n ...(makeAdditionalPropertiesExplicit(generated) as Record),\n title: 'CharacterSettingsOverride',\n };\n}\n\nexport function parseCharacterSettingsOverrideContent(content: string): CharacterSettingsOverride {\n const document = JSON.parse(content) as unknown;\n if (!_.isPlainObject(document)) {\n throw new Error(tr('runtime.characterOverride.contentMustBeObject'));\n }\n\n // schema 仅供角色卡编辑器展示,不属于实际覆盖配置。\n const { schema: _embedded_schema, ...draft } = document as Record;\n return normalizeCharacterSettingsOverride(CharacterSettingsOverrideSchema.parse(draft));\n}\n\nexport function recoverCharacterSettingsOverridePassthrough(\n content: string\n): CharacterSettingsOverride {\n const document = parseString(content) as unknown;\n if (!_.isPlainObject(document)) {\n return {};\n }\n\n // 丢弃可能损坏的已知字段,只抢救能够安全原样写回的未知扩展字段。\n const recovered = klona(document) as Record;\n delete recovered.schema;\n delete recovered.更新方式;\n\n const remove_known_nested_fields = (key: '额外模型解析配置' | '兼容性', fields: string[]) => {\n const nested = recovered[key];\n if (!_.isPlainObject(nested)) {\n delete recovered[key];\n return;\n }\n const nested_record = nested as Record;\n fields.forEach(field => delete nested_record[field]);\n if (_.isEmpty(nested_record)) {\n delete recovered[key];\n }\n };\n remove_known_nested_fields('额外模型解析配置', [\n '启用自动请求',\n '世界书条目白名单正则',\n '世界书条目黑名单正则',\n ]);\n remove_known_nested_fields('兼容性', ['更新到聊天变量', 'sendas不视为user消息']);\n\n return CharacterSettingsOverrideSchema.parse(recovered);\n}\n\nexport function serializeCharacterSettingsOverride(draft: CharacterSettingsOverride): string {\n const normalized = normalizeCharacterSettingsOverride(draft);\n const { schema: _existing_schema, ...config } = normalized;\n // schema 固定放在最后,兼顾人工阅读和 SillyTavern 的编辑体验。\n const document = {\n ...config,\n schema: getCharacterSettingsOverrideJsonSchema(),\n };\n return JSON.stringify(document, null, 4);\n}\n\nexport function hasActiveCharacterSettingsOverride(draft: CharacterSettingsOverride): boolean {\n const ordinary_paths: CharacterSettingsOverridePath[] = [\n '更新方式',\n '额外模型解析配置.启用自动请求',\n '兼容性.更新到聊天变量',\n '兼容性.sendas不视为user消息',\n ];\n if (ordinary_paths.some(path => hasOwnCharacterSettingsOverride(draft, path))) {\n return true;\n }\n\n // 空白或无效正则不会参与过滤,也不应让界面显示为“覆盖中”。\n return (\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目白名单正则 ?? '').regex !==\n undefined ||\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目黑名单正则 ?? '').regex !==\n undefined\n );\n}\n","import { tr } from '@/i18n';\n\nexport type EntryCommentRegexCompileResult = {\n regex?: RegExp;\n error?: string;\n};\n\nexport type EntryCommentFilterSource = '用户全局配置' | '角色卡配置';\n\nexport type EntryCommentFilterResult = {\n lore: 'globalLore' | 'characterLore' | 'chatLore' | 'personaLore';\n world: string;\n comment: string;\n reason: '白名单' | '黑名单';\n sources: EntryCommentFilterSource[];\n};\n\nexport const ENTRY_COMMENT_FILTER_LOG_TITLE = tr('runtime.filter.logTitle');\n\nexport function logEntryCommentFilterResult(result: EntryCommentFilterResult[]) {\n console.log(\n ENTRY_COMMENT_FILTER_LOG_TITLE,\n result.map(entry => ({ ...entry }))\n );\n}\n\nfunction isEscaped(value: string, index: number): boolean {\n let backslash_count = 0;\n for (let i = index - 1; i >= 0 && value[i] === '\\\\'; i--) {\n backslash_count++;\n }\n return backslash_count % 2 === 1;\n}\n\nfunction findClosingSlash(value: string): number {\n for (let i = value.length - 1; i > 0; i--) {\n if (value[i] === '/' && !isEscaped(value, i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function compileEntryCommentRegex(value: string): EntryCommentRegexCompileResult {\n const trimmed = value.trim();\n if (trimmed === '') {\n return {};\n }\n\n try {\n if (trimmed.startsWith('/')) {\n const closing_slash = findClosingSlash(trimmed);\n if (closing_slash <= 0) {\n throw new Error(tr('runtime.regex.jsStyleSyntax'));\n }\n return {\n regex: new RegExp(\n trimmed.slice(1, closing_slash),\n trimmed.slice(closing_slash + 1)\n ),\n };\n }\n\n return { regex: new RegExp(trimmed) };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n}\n\nexport function testEntryCommentRegex(regex: RegExp, comment: string): boolean {\n regex.lastIndex = 0;\n return regex.test(comment);\n}\n","import { normalizePiThinkingLevel, type PiThinkingLevel } from './pi/thinking_setting';\nimport { tr } from '@/i18n';\nimport {\n getPiProviderTargetDefinition,\n resolvePiApiKeyScope,\n} from '@/function/update/pi/provider_target';\nimport { klona } from 'klona';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './pi/credential_refs';\n\nexport type ExtraModelApiProfileBackend = 'custom' | 'pi';\n\nexport type ExtraModelApiRequestFields = {\n 破限方案: '使用内置破限' | '使用当前预设' | '使用其他预设';\n 其他预设名称: string;\n 随机头部: boolean;\n 应答格式: '聊天消息' | '工具调用' | '格式化输出' | '格式化输出(v4兼容)';\n 关闭thinking: boolean;\n 兼容假流式: boolean;\n};\n\nconst API_REQUEST_FIELDS = [\n '破限方案',\n '其他预设名称',\n '随机头部',\n '应答格式',\n '关闭thinking',\n '兼容假流式',\n] as const satisfies readonly (keyof ExtraModelApiRequestFields)[];\n\n/** 提取已配置的请求选项,保留缺失字段,供方案保存和旧方案补全使用。 */\nfunction extractApiRequestFields(\n config: Partial\n): Partial {\n return Object.fromEntries(\n API_REQUEST_FIELDS.filter(field => config[field] !== undefined).map(field => [\n field,\n config[field],\n ])\n );\n}\n\nexport type ExtraModelPiConnectionFields = {\n [key: string]: unknown;\n /** 各服务商绑定的集中 OAuth 凭证编号;空映射表示明确未登录。 */\n credentialIds?: PiCredentialReferences;\n provider: string;\n api: string;\n authType: string;\n endpoint: string;\n useProxy: boolean;\n thinkingLevel?: PiThinkingLevel;\n model: string;\n contextWindow: number | string;\n customHeaders: string;\n customIncludeBody: string;\n customExcludeBody: string;\n};\n\nexport type ExtraModelPiSettings = ExtraModelPiConnectionFields & {\n credentials: Record;\n apiKeys?: Record;\n};\n\nexport type ExtraModelApiProfile = Partial & {\n [key: string]: unknown;\n 名称: string;\n backend?: ExtraModelApiProfileBackend;\n api地址: string;\n 密钥: string;\n 模型名称: string;\n pi?: ExtraModelPiConnectionFields;\n};\n\nexport type ExtraModelApiProfileFields = Partial & {\n 模型来源?: '与插头相同' | '自定义' | '更多';\n api地址: string;\n 密钥: string;\n customApiKey?: string;\n 模型名称: string;\n pi?: ExtraModelPiSettings;\n api方案列表: ExtraModelApiProfile[];\n 当前api方案: string;\n};\n\nexport const DEFAULT_EXTRA_MODEL_API_PROFILE_NAME = '默认';\n\n/** 统一方案名两端空白,避免查找、去重和保存使用不同的名称。 */\nfunction normalizeExtraModelApiProfileName(name: string): string {\n return name.trim();\n}\n\n/**\n * 将方案中的上下文窗口规范为非负整数,0 表示使用目录值。\n * 返回 undefined 表示配置无效,调用方不得将其当作目录默认值。\n */\nexport function normalizeExtraModelPiProfileContextWindow(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isInteger(value) && value >= 0 ? value : undefined;\n }\n if (typeof value !== 'string') {\n return undefined;\n }\n\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;\n}\n\n/** 根据当前模型来源确定方案保存的是自定义连接还是 Pi 连接。 */\nfunction getExtraModelApiProfileBackend(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileBackend {\n return config.模型来源 === '更多' ? 'pi' : 'custom';\n}\n\n/**\n * 生成脱离响应式代理的连接快照,并规范连接标识与上下文窗口。\n * 快照不包含 OAuth 凭证或按目标缓存的 API Key,避免方案复制这些共享状态。\n */\nfunction clonePiConnectionFields(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): ExtraModelPiConnectionFields | undefined {\n if (pi === undefined) {\n return undefined;\n }\n\n // Settings and profile entries normally arrive as Vue/Pinia reactive proxies. Native\n // structuredClone rejects proxies with DataCloneError, while klona traverses them into\n // detached plain objects and still preserves unknown forward-compatible fields.\n const cloned = klona(pi);\n delete cloned.credentials;\n delete cloned.apiKeys;\n if (pi.credentialIds !== undefined || pi.credentials !== undefined) {\n cloned.credentialIds = resolvePiCredentialReferences(\n pi,\n (pi.credentials as Record | undefined) ?? {}\n );\n }\n // Runtime resolution treats surrounding whitespace on connection identifiers as insignificant.\n // Persist the same canonical representation so profile validation, the Source form, dirty\n // checks, and runtime resolution cannot disagree about a target after import.\n for (const field of ['provider', 'api', 'authType', 'endpoint', 'model'] as const) {\n if (typeof cloned[field] === 'string') {\n cloned[field] = cloned[field].trim();\n }\n }\n // Profiles saved before this option existed, and malformed imported values, must retain the\n // direct-transport default rather than becoming enabled through truthiness coercion.\n cloned.useProxy = cloned.useProxy === true;\n cloned.thinkingLevel = normalizePiThinkingLevel(cloned.thinkingLevel);\n const context_window = normalizeExtraModelPiProfileContextWindow(cloned.contextWindow);\n if (context_window !== undefined) {\n cloned.contextWindow = context_window;\n }\n return cloned;\n}\n\n/** 仅为完整且受支持的 API Key 目标解析密钥缓存位置;无效连接返回空字符串。 */\nfunction resolvePiConnectionApiKeyScope(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): string {\n if (\n pi === undefined ||\n typeof pi.provider !== 'string' ||\n typeof pi.api !== 'string' ||\n typeof pi.authType !== 'string' ||\n typeof pi.endpoint !== 'string'\n ) {\n return '';\n }\n\n return resolvePiApiKeyScope(\n getPiProviderTargetDefinition(pi.provider.trim()),\n pi.api.trim(),\n pi.authType.trim(),\n pi.endpoint\n );\n}\n\n/** 检查导入快照是否具备全部连接字段及合法类型,避免用默认值补成可发送连接。 */\nfunction hasCompletePiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n pi !== undefined &&\n typeof pi.provider === 'string' &&\n typeof pi.api === 'string' &&\n typeof pi.authType === 'string' &&\n (pi.authType.trim() === 'api_key' || pi.authType.trim() === 'oauth') &&\n typeof pi.endpoint === 'string' &&\n typeof pi.useProxy === 'boolean' &&\n typeof pi.model === 'string' &&\n typeof pi.contextWindow === 'number' &&\n Number.isInteger(pi.contextWindow) &&\n pi.contextWindow >= 0 &&\n typeof pi.customHeaders === 'string' &&\n typeof pi.customIncludeBody === 'string' &&\n typeof pi.customExcludeBody === 'string'\n );\n}\n\n/** 在结构完整的基础上检查服务商、协议和模型是否已填写。 */\nfunction hasConfiguredPiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n hasCompletePiConnectionSnapshot(pi) &&\n pi.provider.trim().length > 0 &&\n pi.api.trim().length > 0 &&\n pi.model.trim().length > 0\n );\n}\n\n/** 判断连接能否归属到有效的 API Key 目标,作为保留方案密钥的条件。 */\nfunction hasValidPiApiKeyTarget(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): boolean {\n return hasConfiguredPiConnectionSnapshot(pi) && resolvePiConnectionApiKeyScope(pi) !== '';\n}\n\n/** 规范单个方案的名称、后端和连接快照,并清除不属于当前认证方式的密钥。 */\nfunction normalizeExtraModelApiProfile(profile: ExtraModelApiProfile): ExtraModelApiProfile {\n const cloned = klona(profile);\n delete cloned.customApiKey;\n cloned.名称 = normalizeExtraModelApiProfileName(cloned.名称);\n const backend: ExtraModelApiProfileBackend = cloned.backend === 'pi' ? 'pi' : 'custom';\n cloned.backend = backend;\n\n if (backend === 'pi' && cloned.pi !== undefined) {\n cloned.pi = clonePiConnectionFields(cloned.pi);\n } else {\n delete cloned.pi;\n }\n if (backend === 'pi') {\n // These legacy top-level fields belong to the hidden Custom source. Pi owns its endpoint\n // and model inside `pi`, so importing or saving them here must never overwrite that cache.\n cloned.api地址 = '';\n cloned.模型名称 = '';\n\n // A profile-level key has wire meaning only for a registry-backed, normalizable API-key\n // target. OAuth and malformed/future targets retain metadata but never a dormant secret.\n if (!hasValidPiApiKeyTarget(cloned.pi)) {\n cloned.密钥 = '';\n }\n }\n return cloned;\n}\n\n/** 规范方案列表,剔除空名称和重复名称,同名方案只保留首次出现的条目。 */\nfunction normalizeExtraModelApiProfileList(\n profiles: ExtraModelApiProfile[]\n): ExtraModelApiProfile[] {\n const names = new Set();\n const normalized_profiles: ExtraModelApiProfile[] = [];\n for (const profile of profiles) {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n if (normalized_profile.名称 === '' || names.has(normalized_profile.名称)) {\n continue;\n }\n names.add(normalized_profile.名称);\n normalized_profiles.push(normalized_profile);\n }\n return normalized_profiles;\n}\n\n/** 将方案连接合并到运行设置,同时保留当前凭证仓库和目标密钥缓存。 */\nfunction mergePiConnectionFields(\n current: ExtraModelPiSettings | undefined,\n profile: ExtraModelPiConnectionFields\n): ExtraModelPiSettings {\n const current_clone = current === undefined ? undefined : klona(current);\n const profile_clone = clonePiConnectionFields(profile)!;\n return {\n ...current_clone,\n ...profile_clone,\n credentialIds: resolvePiCredentialReferences(profile, current_clone?.credentials),\n credentials: klona(current_clone?.credentials ?? {}),\n apiKeys: klona(current_clone?.apiKeys ?? {}),\n } as ExtraModelPiSettings;\n}\n\n/** 清空可发送的连接字段,保留共享凭证和密钥缓存,等待用户重新配置。 */\nfunction clearPiConnectionFields(pi: ExtraModelPiSettings): ExtraModelPiSettings {\n const cloned = klona(pi);\n return {\n ...cloned,\n credentialIds: {},\n provider: '',\n api: '',\n authType: 'api_key',\n endpoint: '',\n useProxy: false,\n thinkingLevel: 'default',\n model: '',\n contextWindow: 0,\n credentials: klona(cloned.credentials),\n apiKeys: klona(cloned.apiKeys ?? {}),\n customHeaders: '',\n customIncludeBody: '',\n customExcludeBody: '',\n };\n}\n\n/** 从当前设置提取可保存的连接和请求选项;Pi 方案必须先通过完整性校验。 */\nexport function extractExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfile {\n const backend = getExtraModelApiProfileBackend(config);\n const pi_snapshot = backend === 'pi' ? clonePiConnectionFields(config.pi) : undefined;\n if (backend === 'pi' && !hasConfiguredPiConnectionSnapshot(pi_snapshot)) {\n throw new Error(tr('runtime.apiProfile.piConfigRequired'));\n }\n return {\n 名称:\n normalizeExtraModelApiProfileName(config.当前api方案) ||\n DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n backend,\n api地址: backend === 'pi' ? '' : config.api地址,\n 密钥: backend === 'pi' && !hasValidPiApiKeyTarget(pi_snapshot) ? '' : config.密钥,\n 模型名称: backend === 'pi' ? '' : config.模型名称,\n ...extractApiRequestFields(config),\n ...(backend === 'pi' && pi_snapshot !== undefined ? { pi: pi_snapshot } : {}),\n };\n}\n\n/**\n * 应用方案的连接及请求选项,并切换到相应模型来源。\n * 无效 Pi 快照会清空活动连接,不能借用其他来源的密钥继续发送。\n */\nexport function applyExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile: ExtraModelApiProfile\n): ExtraModelApiProfileFields {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const result: ExtraModelApiProfileFields = {\n ...config,\n ...extractApiRequestFields(normalized_profile),\n 模型来源: normalized_profile.backend === 'pi' ? '更多' : '自定义',\n 密钥: normalized_profile.密钥,\n 当前api方案: normalized_profile.名称,\n ...(normalized_profile.backend === 'custom'\n ? {\n api地址: normalized_profile.api地址,\n 模型名称: normalized_profile.模型名称,\n }\n : {}),\n };\n if (normalized_profile.backend === 'pi') {\n if (hasConfiguredPiConnectionSnapshot(normalized_profile.pi)) {\n result.pi = mergePiConnectionFields(config.pi, normalized_profile.pi);\n } else {\n // Never combine a malformed Pi profile's key with the previously active provider,\n // endpoint, or model. Preserve credentials, but disable the connection until the\n // user repairs and saves a complete snapshot.\n result.密钥 = '';\n if (config.pi !== undefined) {\n result.pi = clearPiConnectionFields(config.pi);\n }\n }\n }\n return result;\n}\n\n/** 按规范化名称新增或更新方案,更新时保留已有条目的其他字段。 */\nexport function upsertExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile: ExtraModelApiProfile\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile.名称);\n if (!normalized_name) {\n throw new Error(tr('runtime.apiProfile.nameRequired'));\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(profiles);\n const next_profile = {\n ...normalizeExtraModelApiProfile(profile),\n 名称: normalized_name,\n };\n const existing_index = normalized_profiles.findIndex(item => item.名称 === normalized_name);\n if (existing_index === -1) {\n return [...normalized_profiles, next_profile];\n }\n\n const next_profiles = [...normalized_profiles];\n next_profiles[existing_index] = normalizeExtraModelApiProfile({\n ...normalized_profiles[existing_index],\n ...next_profile,\n });\n return next_profiles;\n}\n\n/** 返回删除指定方案后的规范化列表,不直接修改原列表。 */\nexport function removeExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).filter(\n profile => profile.名称 !== normalized_name\n );\n}\n\n/** 按规范化名称检查方案是否存在,供保存和删除操作校验。 */\nexport function hasExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): boolean {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).some(\n profile => profile.名称 === normalized_name\n );\n}\n\n/** 比较活动方案与当前连接、密钥及请求选项,判断是否存在未保存修改。 */\nexport function isActiveExtraModelApiProfileDirty(config: ExtraModelApiProfileFields): boolean {\n const active_name = normalizeExtraModelApiProfileName(config.当前api方案);\n if (!active_name) {\n return false;\n }\n\n const profile = normalizeExtraModelApiProfileList(config.api方案列表).find(\n item => item.名称 === active_name\n );\n if (!profile) {\n return false;\n }\n\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const backend = getExtraModelApiProfileBackend(config);\n if (normalized_profile.backend !== backend || normalized_profile.密钥 !== config.密钥) {\n return true;\n }\n\n if (\n API_REQUEST_FIELDS.some(\n field =>\n normalized_profile[field] !== undefined &&\n normalized_profile[field] !== config[field]\n )\n ) {\n return true;\n }\n\n if (\n backend === 'custom' &&\n (normalized_profile.api地址 !== config.api地址 ||\n normalized_profile.模型名称 !== config.模型名称)\n ) {\n return true;\n }\n\n return (\n backend === 'pi' && !_.isEqual(normalized_profile.pi, clonePiConnectionFields(config.pi))\n );\n}\n\n/** 解除方案绑定并清空活动连接;使用 Pi 时保留隐藏的自定义连接字段。 */\nexport function clearUnboundExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileFields {\n const preserve_hidden_custom_fields = config.模型来源 === '更多';\n return {\n ...config,\n 当前api方案: '',\n api地址: preserve_hidden_custom_fields ? config.api地址 : '',\n 密钥: '',\n 模型名称: preserve_hidden_custom_fields ? config.模型名称 : '',\n ...(config.模型来源 === '更多' && config.pi !== undefined\n ? { pi: clearPiConnectionFields(config.pi) }\n : {}),\n };\n}\n\n/** 校正活动方案引用,使列表、当前名称和连接状态保持一致。 */\nexport function reconcileExtraModelApiProfileSelection(\n config: T\n): T {\n const normalized_config = {\n ...config,\n api方案列表: normalizeExtraModelApiProfileList(config.api方案列表),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n const active_name = normalized_config.当前api方案;\n if (!active_name) {\n return normalized_config;\n }\n\n if (hasExtraModelApiProfile(normalized_config.api方案列表, active_name)) {\n return normalized_config;\n }\n\n if (normalized_config.api方案列表.length === 0) {\n return {\n ...normalized_config,\n 当前api方案: '',\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...normalized_config,\n api方案列表: normalized_config.api方案列表,\n },\n normalized_config.api方案列表[0]\n ) as T;\n}\n\n/** 删除指定的活动方案,并同步清理方案选择及相关连接字段。 */\nexport function deleteActiveExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n const remaining = removeExtraModelApiProfile(config.api方案列表, profile_name);\n if (remaining.length === 0) {\n return {\n ...clearUnboundExtraModelApiProfileFields(config),\n api方案列表: [],\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: remaining,\n },\n remaining[0]\n );\n}\n\nexport type ExtraModelApiProfileDeletionConfirmation = 'discard_unsaved_changes' | 'delete_profile';\n\n/**\n * 有未保存修改时先确认丢弃,再确认删除,任一步取消都返回 null。\n * 删除前再次核对活动方案,避免等待弹窗期间误删用户新选中的方案。\n */\nexport async function deleteActiveExtraModelApiProfileWithConfirmation(\n config: ExtraModelApiProfileFields,\n profile_name: string,\n confirm: (confirmation: ExtraModelApiProfileDeletionConfirmation) => Promise\n): Promise {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n if (isActiveExtraModelApiProfileDirty(config) && !(await confirm('discard_unsaved_changes'))) {\n return null;\n }\n\n if (!(await confirm('delete_profile'))) {\n return null;\n }\n\n return deleteActiveExtraModelApiProfile(config, profile_name);\n}\n\n/**\n * 补全方案中缺失的请求选项,并校正方案选择与密钥归属。\n * 不完整的 Pi 快照保持不可发送,不能从遗留自定义字段推导出 Pi 连接。\n */\nexport function migrateExtraModelApiProfiles(config: T): T {\n let migrated = {\n ...config,\n ...(config.pi === undefined\n ? {}\n : {\n pi: {\n ...config.pi,\n credentialIds: resolvePiCredentialReferences(\n config.pi,\n config.pi.credentials\n ),\n },\n }),\n // Old profiles only stored connection fields. Copy the current request options into\n // every profile once; saved per-profile values must survive subsequent loads/imports.\n api方案列表: normalizeExtraModelApiProfileList(\n config.api方案列表.map(profile => ({\n ...profile,\n ...(profile.backend === 'pi' && profile.pi !== undefined\n ? {\n pi: {\n ...profile.pi,\n credentialIds: resolvePiCredentialReferences(\n profile.pi,\n config.pi?.credentials\n ),\n },\n }\n : {}),\n ...extractApiRequestFields(config),\n ...extractApiRequestFields(profile),\n }))\n ),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n\n // The shared root key has wire meaning only for a registry-backed, normalizable API-key target.\n // Clear stale imported values even when there is no active profile (and before legacy-profile\n // detection), while retaining the source-specific Custom/API-key caches and OAuth store.\n if (\n migrated.模型来源 === '更多' &&\n resolvePiConnectionApiKeyScope(migrated.pi) === '' &&\n migrated.密钥 !== ''\n ) {\n migrated = {\n ...migrated,\n 密钥: '',\n } as T;\n }\n\n const active_profile = migrated.api方案列表.find(\n profile => profile.名称 === migrated.当前api方案\n );\n if (active_profile?.backend === 'pi' && !hasConfiguredPiConnectionSnapshot(active_profile.pi)) {\n migrated = applyExtraModelApiProfile(migrated, active_profile) as T;\n }\n\n if (migrated.api方案列表.length === 0) {\n const has_legacy_custom_api =\n migrated.api地址.trim().length > 0 ||\n migrated.密钥.trim().length > 0 ||\n migrated.模型名称.trim().length > 0;\n if (has_legacy_custom_api) {\n // A newly selected but not yet configured Pi source may still carry legacy root\n // Custom fields. Do not turn those unrelated fields into a sendable Pi profile.\n if (\n getExtraModelApiProfileBackend(migrated) === 'pi' &&\n !hasConfiguredPiConnectionSnapshot(migrated.pi)\n ) {\n return reconcileExtraModelApiProfileSelection(migrated);\n }\n const profile = extractExtraModelApiProfileFields(migrated);\n migrated = {\n ...migrated,\n api方案列表: [\n {\n ...profile,\n 名称: DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n },\n ],\n 当前api方案: migrated.当前api方案 || DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n };\n }\n }\n\n return reconcileExtraModelApiProfileSelection(migrated);\n}\n\n/** 按名称查找并应用方案;不存在时抛出可展示的配置错误。 */\nexport function selectExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const profile = normalized_profiles.find(item => item.名称 === normalized_name);\n if (!profile) {\n throw new Error(\n tr('runtime.apiProfile.notFound', {\n name: normalized_name,\n })\n );\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: normalized_profiles,\n },\n profile\n );\n}\n\n/** 保存当前连接和请求选项,支持更新活动方案或指定名称,并防止覆盖其他同名方案。 */\nexport function saveCurrentExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name?: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name ?? config.当前api方案);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.selectOrEnterName'));\n }\n\n if (\n hasExtraModelApiProfile(config.api方案列表, target_name) &&\n normalizeExtraModelApiProfileName(config.当前api方案) !== target_name\n ) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const source_profile = normalized_profiles.find(\n item => item.名称 === normalizeExtraModelApiProfileName(config.当前api方案)\n );\n const normalized_source_profile =\n source_profile === undefined ? undefined : normalizeExtraModelApiProfile(source_profile);\n const profile: ExtraModelApiProfile = {\n ...normalized_source_profile,\n ...extractExtraModelApiProfileFields(config),\n 名称: target_name,\n };\n if (profile.backend !== 'pi') {\n delete profile.pi;\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: upsertExtraModelApiProfile(normalized_profiles, profile),\n },\n profile\n );\n}\n\n/** 校验新名称不为空且未占用,再将当前配置另存为独立方案。 */\nexport function saveAsNewExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.enterNewName'));\n }\n if (hasExtraModelApiProfile(config.api方案列表, target_name)) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n return saveCurrentExtraModelApiProfile(config, target_name);\n}\n","/** 兼容旧浏览器的 AbortSignal 静态接口;可选方法由运行时检测后补齐。 */\ntype AbortSignalConstructorWithPolyfills = {\n any?: (signals: Iterable) => AbortSignal;\n timeout?: (milliseconds: number) => AbortSignal;\n};\n\n/**\n * 为旧版酒馆浏览器补齐 Pi OAuth 刷新所需的 AbortSignal.timeout 和 any。\n * 仅在原生实现缺失时安装,并在取消后解除组合信号的监听。\n */\nexport function installPiAbortSignalPolyfills(): void {\n const constructor = AbortSignal as unknown as AbortSignalConstructorWithPolyfills;\n if (typeof constructor.timeout !== 'function') {\n Object.defineProperty(constructor, 'timeout', {\n configurable: true,\n writable: true,\n /** 创建到期自动取消的信号,拒绝负数和非有限超时时间。 */\n value(milliseconds: number): AbortSignal {\n if (!Number.isFinite(milliseconds) || milliseconds < 0) {\n throw new RangeError(\n 'AbortSignal timeout must be a non-negative finite number'\n );\n }\n const controller = new AbortController();\n setTimeout(\n () =>\n controller.abort(\n new DOMException('The operation timed out', 'TimeoutError')\n ),\n milliseconds\n );\n return controller.signal;\n },\n });\n }\n\n if (typeof constructor.any === 'function') {\n return;\n }\n\n Object.defineProperty(constructor, 'any', {\n configurable: true,\n writable: true,\n /** 合并多个取消信号,首次取消时保留原始原因并清理其他监听。 */\n value(signals: Iterable): AbortSignal {\n const controller = new AbortController();\n const listeners = new Map void>();\n /** 解除已注册的取消监听,避免组合信号结束后继续持有输入信号。 */\n const cleanup = () => {\n for (const [signal, listener] of listeners) {\n signal.removeEventListener('abort', listener);\n }\n listeners.clear();\n };\n /** 只转发首次取消原因,并统一释放组合信号的监听。 */\n const forward = (signal: AbortSignal) => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n cleanup();\n };\n\n for (const signal of signals) {\n if (listeners.has(signal)) {\n continue;\n }\n if (signal.aborted) {\n forward(signal);\n break;\n }\n /** 将当前输入信号的取消传递给组合信号。 */\n const listener = () => forward(signal);\n listeners.set(signal, listener);\n signal.addEventListener('abort', listener, { once: true });\n }\n return controller.signal;\n },\n });\n}\n","import type { ProviderHeaders } from '@earendil-works/pi-ai';\nimport YAML from 'yaml';\n\n/** 解析用户输入的 YAML 或 JSON;空输入表示未配置,语法错误只返回字段级提示。 */\nfunction parseYaml(value: string, label: string): unknown {\n if (!value.trim()) {\n return undefined;\n }\n try {\n return YAML.parse(value);\n } catch {\n throw new Error(`More source ${label} is not valid YAML or JSON`);\n }\n}\n\n/** 判断解析结果是否为非空、非数组对象,供请求配置的结构校验使用。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 解析自定义请求头,允许字符串或 null,并禁止覆盖由认证层管理的密钥头。 */\nexport function parsePiCustomHeaders(value: string): ProviderHeaders | undefined {\n const parsed = parseYaml(value, 'customHeaders');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customHeaders must be an object');\n }\n const headers: ProviderHeaders = {};\n for (const [name, header_value] of Object.entries(parsed)) {\n const normalized_name = name.trim();\n if (!normalized_name || (typeof header_value !== 'string' && header_value !== null)) {\n throw new Error('More source customHeaders values must be strings or null');\n }\n if (\n /^(authorization|proxy-authorization|x-api-key|x-goog-api-key)$/i.test(normalized_name)\n ) {\n throw new Error(\n `More source customHeaders cannot override authentication header '${normalized_name}'`\n );\n }\n headers[normalized_name] = header_value;\n }\n return headers;\n}\n\n/** 解析要合并到请求体的对象;空输入不产生覆盖。 */\nexport function parsePiCustomIncludeBody(value: string): Record | undefined {\n const parsed = parseYaml(value, 'customIncludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customIncludeBody must be an object');\n }\n return parsed;\n}\n\n/** 将数组或逗号、换行分隔的字段名规范为去重列表,拒绝空名称和非字符串。 */\nexport function parsePiCustomExcludeBody(value: string): string[] | undefined {\n const parsed = parseYaml(value, 'customExcludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n const fields =\n typeof parsed === 'string'\n ? parsed\n .split(/[\\n,]/)\n .map(item => item.trim())\n .filter(Boolean)\n : parsed;\n if (\n !Array.isArray(fields) ||\n fields.some(field => typeof field !== 'string' || !field.trim())\n ) {\n throw new Error('More source customExcludeBody must be an array of field names');\n }\n return [...new Set(fields.map(field => field.trim()))];\n}\n","import { getPendingPromptCaptureDiagnostics } from './prompt_capture';\n\nconst pi_request_controllers = new Map();\nconst pi_request_attempts = new Map();\n\nexport class PiRequestAbortedError extends Error {\n readonly generationId: string;\n\n /** 将取消原因与生成编号绑定,供重试策略和界面识别主动停止。 */\n constructor(generationId: string, reason?: unknown) {\n const detail =\n reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';\n super(\n detail\n ? `More source request '${generationId}' was aborted: ${detail}`\n : `More source request '${generationId}' was aborted`\n );\n this.name = 'PiRequestAbortedError';\n this.generationId = generationId;\n }\n}\n\n/** 识别 Pi 的主动取消错误,避免将其当作普通请求失败重试。 */\nexport function isPiRequestAbortedError(error: unknown): error is PiRequestAbortedError {\n return error instanceof PiRequestAbortedError;\n}\n\n/** 实际服务商请求的取消句柄,连接本次请求、用户停止和完整尝试的取消信号。 */\nexport type PiRequestControllerRegistration = {\n controller: AbortController;\n signal: AbortSignal;\n /** 移除本次登记和关联监听;释放本身不会取消请求。 */\n release: () => void;\n};\n\n/** 跨越提示词捕获与服务商请求的尝试句柄,用于记住两阶段之间发生的取消。 */\nexport type PiRequestAttemptRegistration = {\n signal: AbortSignal;\n /** 完整尝试结束后移除登记;调用多次不会影响同编号的新尝试。 */\n release: () => void;\n};\n\n/**\n * 为“提示词捕获 → 服务商请求”的完整尝试保留生成编号和取消信号。\n * 该记录在捕获结束后仍保留停止标记,防止间隙中的取消被后续请求注册绕过。\n */\nexport function beginPiRequestAttempt(generation_id: string): PiRequestAttemptRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_attempts.has(generation_id) || pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n pi_request_attempts.set(generation_id, controller);\n let released = false;\n /** 只释放本次尝试登记的记录,避免清理同编号的新实例。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n if (pi_request_attempts.get(generation_id) === controller) {\n pi_request_attempts.delete(generation_id);\n }\n };\n\n return { signal: controller.signal, release };\n}\n\n/** 登记实际请求控制器,并继承完整尝试已发生或随后发生的取消。 */\nexport function registerPiRequestController(\n generation_id: string,\n caller_signal?: AbortSignal\n): PiRequestControllerRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n const attempt_signal = pi_request_attempts.get(generation_id)?.signal;\n const abort_sources = [\n ...new Set(\n [attempt_signal, caller_signal].filter(\n (signal): signal is AbortSignal => signal !== undefined\n )\n ),\n ];\n const abort_listeners = abort_sources.map(signal => {\n /** 把调用方或尝试层信号的取消原因传给实际请求控制器。 */\n const forward_abort = () => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n };\n signal.addEventListener('abort', forward_abort, { once: true });\n if (signal.aborted) {\n forward_abort();\n }\n return { signal, forward_abort };\n });\n\n pi_request_controllers.set(generation_id, controller);\n let released = false;\n /** 解除上游取消监听,并只移除当前请求的登记项。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n for (const { signal, forward_abort } of abort_listeners) {\n signal.removeEventListener('abort', forward_abort);\n }\n if (pi_request_controllers.get(generation_id) === controller) {\n pi_request_controllers.delete(generation_id);\n }\n };\n\n return { controller, signal: controller.signal, release };\n}\n\n/** 在实际请求执行期间登记控制器,无论成功、失败还是取消都释放登记。 */\nexport async function withPiRequestController(\n generation_id: string,\n run: (signal: AbortSignal) => Promise,\n caller_signal?: AbortSignal\n): Promise {\n const registration = registerPiRequestController(generation_id, caller_signal);\n try {\n return await run(registration.signal);\n } finally {\n registration.release();\n }\n}\n\n/** 按生成编号取消 Pi 尝试和实际请求,保留可供后续注册检查的停止状态。 */\nexport function stopPiRequestById(generation_id: string, reason?: unknown): boolean {\n const controllers = [\n pi_request_attempts.get(generation_id),\n pi_request_controllers.get(generation_id),\n ];\n let stopped = false;\n for (const controller of controllers) {\n if (!controller || controller.signal.aborted) {\n continue;\n }\n controller.abort(reason ?? new PiRequestAbortedError(generation_id));\n stopped = true;\n }\n return stopped;\n}\n\n/** 同时停止同一编号的 Slash 捕获和 Pi 请求,覆盖生成链路的各个阶段。 */\nexport function stopExtraModelRequestById(generation_id: string, reason?: unknown): boolean {\n const slash_stopped = stopGenerationById(generation_id);\n const pi_stopped = stopPiRequestById(generation_id, reason);\n return slash_stopped || pi_stopped;\n}\n\n/** 取消所有仍活动的 Pi 尝试与请求,并返回受影响的生成编号数量。 */\nexport function stopAllPiRequests(reason?: unknown): number {\n const ids = [\n ...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()]),\n ].filter(id => {\n const attempt = pi_request_attempts.get(id);\n const runtime = pi_request_controllers.get(id);\n return (\n (attempt !== undefined && !attempt.signal.aborted) ||\n (runtime !== undefined && !runtime.signal.aborted)\n );\n });\n for (const id of ids) {\n stopPiRequestById(id, reason);\n }\n return ids.length;\n}\n\n/** 停止全部额外模型尝试,包括仍处于 Slash 提示词捕获阶段的请求。 */\nexport function stopAllExtraModelRequests(reason?: unknown): number {\n const ids = new Set([\n ...getPendingPromptCaptureDiagnostics().map(capture => capture.generationId),\n ...pi_request_attempts.keys(),\n ...pi_request_controllers.keys(),\n ]);\n let stopped = 0;\n for (const id of ids) {\n if (stopExtraModelRequestById(id, reason)) {\n stopped += 1;\n }\n }\n return stopped;\n}\n\n/** 返回当前登记的生成编号,供取消操作及生命周期诊断使用。 */\nexport function getActivePiRequestIds(): readonly string[] {\n return [...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()])];\n}\n\n/**\n * 脚本卸载时取消全部请求,但保留登记项直到各自结束。\n * 尝试记录仍充当停止标记,防止并发完成的捕获在卸载后发起新请求。\n */\nexport function clearPiRequestControllers(): void {\n stopAllExtraModelRequests(new Error('More source request registry disposed'));\n}\n","/** 连接与方案仅保存服务商到集中凭证记录的引用,不复制会轮换的 refresh token。 */\nexport type PiCredentialReferences = Record;\n\n/** 接受旧版服务商键和带服务商命名空间的新凭证编号,拒绝跨服务商引用。 */\nexport function isPiCredentialId(providerId: string, credentialId: string): boolean {\n return (\n credentialId === providerId ||\n credentialId.startsWith(`oauth:${encodeURIComponent(providerId)}:`)\n );\n}\n\n/** 规范显式引用;缺少字段的旧设置才从原 providerId 记录一次性继承登录态。 */\nexport function resolvePiCredentialReferences(\n pi: { credentialIds?: unknown },\n credentials: Record = {}\n): PiCredentialReferences {\n if (pi.credentialIds !== undefined) {\n if (\n typeof pi.credentialIds !== 'object' ||\n pi.credentialIds === null ||\n Array.isArray(pi.credentialIds)\n ) {\n return {};\n }\n return Object.fromEntries(\n Object.entries(pi.credentialIds).filter(\n ([providerId, id]) => typeof id === 'string' && isPiCredentialId(providerId, id)\n )\n );\n }\n return Object.fromEntries(\n Object.entries(credentials)\n .filter(\n ([id, value]) =>\n !id.startsWith('oauth:') &&\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'oauth'\n )\n .map(([providerId]) => [providerId, providerId])\n );\n}\n","import { useDataStore } from '@/store';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './credential_refs';\nimport type {\n AuthOperationOptions,\n Credential,\n CredentialInfo,\n CredentialStore,\n OAuthCredential,\n} from './pi_gateway';\n\n/** 按凭证编号排队执行的凭证操作,进入队列前不提前读取或修改凭证。 */\ntype CredentialTask = () => Promise;\n\n/** 复制 OAuth 凭证,避免调用方修改持久化对象本身。 */\nfunction cloneOAuthCredential(credential: OAuthCredential): OAuthCredential {\n return { ...credential };\n}\n\n/**\n * 从宽松保存的设置中读取符合 Pi 契约的 OAuth 凭证。\n * 未知字段可以保留,但缺失或无效的 access、refresh 和 expires 不进入运行时。\n */\nfunction readOAuthCredential(value: unknown): OAuthCredential | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return undefined;\n }\n\n const candidate = value as Record;\n if (\n candidate.type !== 'oauth' ||\n typeof candidate.access !== 'string' ||\n candidate.access.trim().length === 0 ||\n typeof candidate.refresh !== 'string' ||\n candidate.refresh.trim().length === 0 ||\n typeof candidate.expires !== 'number' ||\n !Number.isFinite(candidate.expires)\n ) {\n return undefined;\n }\n\n return cloneOAuthCredential(candidate as OAuthCredential);\n}\n\n/** 所有捕获同一设置仓库的适配器共用真实凭证编号的队列。 */\nconst storeChains = new WeakMap>>();\n/** 登录和解绑的代次按所属连接记录,阻止已经退出的待完成登录重新绑定。 */\nconst bindingGenerations = new WeakMap>();\n\ntype DataStore = ReturnType;\ntype CredentialStoreHooks = {\n beforeWrite?: (providerId: string, credentialId: string) => void;\n /** 返回 false 时仅解除绑定,保留其他方案仍使用的集中凭证。 */\n beforeDelete?: (providerId: string, credentialId: string | undefined) => boolean;\n};\n\n/** 在读取或写入凭证前检查调用方是否已取消操作。 */\nfunction throwIfAborted(options?: AuthOperationOptions): void {\n options?.signal?.throwIfAborted();\n}\n\n/** 取消时立即结束调用方等待,同时允许内部串行任务安全收尾。 */\nfunction raceWithAbort(task: Promise, signal?: AbortSignal): Promise {\n if (signal === undefined) {\n return task;\n }\n\n if (signal.aborted) {\n return Promise.reject(signal.reason);\n }\n return new Promise((resolve, reject) => {\n /** 解除取消监听并使用原始取消原因拒绝等待中的调用。 */\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort);\n reject(signal.reason);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n\n task.then(\n value => {\n signal.removeEventListener('abort', onAbort);\n resolve(value);\n },\n error => {\n signal.removeEventListener('abort', onAbort);\n reject(error);\n }\n );\n });\n}\n\n/**\n * 创建基于设置持久化的 OAuth 凭证仓库。\n * 同一凭证编号的修改和删除串行执行,写入前复查取消状态,避免刷新覆盖登出或新凭证。\n */\nexport function createPiCredentialStore(\n credentialIds?: PiCredentialReferences,\n dataStore: DataStore = useDataStore(),\n hooks: CredentialStoreHooks = {}\n): CredentialStore {\n const references = credentialIds === undefined ? undefined : { ...credentialIds };\n const getPersistedCredentials = () => dataStore.settings.额外模型解析配置.pi.credentials;\n const resolveId = (providerId: string) =>\n references === undefined ? providerId : references[providerId];\n const chains = storeChains.get(dataStore) ?? new Map>();\n storeChains.set(dataStore, chains);\n\n /** 按凭证编号串行安排凭证修改;前序失败不阻塞后续任务,取消后也保持队列顺序。 */\n const enqueue = (\n providerId: string,\n task: CredentialTask,\n options?: AuthOperationOptions\n ): Promise => {\n const previous = chains.get(providerId) ?? Promise.resolve();\n const queued = (async () => {\n throwIfAborted(options);\n await previous.catch(() => undefined);\n throwIfAborted(options);\n return task();\n })();\n const tail = queued.then(\n () => undefined,\n () => undefined\n );\n chains.set(providerId, tail);\n void tail.then(() => {\n if (chains.get(providerId) === tail) {\n chains.delete(providerId);\n }\n });\n\n return raceWithAbort(queued, options?.signal);\n };\n\n return {\n /** 读取并返回有效凭证的副本,忽略仓库中无法识别的记录。 */\n async read(\n providerId: string,\n options?: AuthOperationOptions\n ): Promise {\n throwIfAborted(options);\n const credentialId = resolveId(providerId);\n const credential =\n credentialId === undefined\n ? undefined\n : readOAuthCredential(getPersistedCredentials()[credentialId]);\n return credential === undefined ? undefined : cloneOAuthCredential(credential);\n },\n\n /** 列出有效 OAuth 凭证的服务商和类型,不向列表调用方暴露令牌内容。 */\n async list(options?: AuthOperationOptions): Promise {\n throwIfAborted(options);\n const ids =\n references ??\n Object.fromEntries(Object.keys(getPersistedCredentials()).map(id => [id, id]));\n return Object.entries(ids).flatMap(([providerId, credentialId]) =>\n readOAuthCredential(getPersistedCredentials()[credentialId]) === undefined\n ? []\n : [{ providerId, type: 'oauth' as const }]\n );\n },\n\n /** 在凭证队列内读取、更新并保存凭证,提交前再次检查取消信号。 */\n modify(\n providerId: string,\n fn: (current: Credential | undefined) => Promise,\n options?: AuthOperationOptions\n ): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.reject(new Error('No OAuth credential is bound to this connection'));\n }\n return enqueue(\n credentialId,\n async () => {\n const current = readOAuthCredential(getPersistedCredentials()[credentialId]);\n const next = await fn(\n current === undefined ? undefined : cloneOAuthCredential(current)\n );\n throwIfAborted(options);\n\n if (next === undefined) {\n return current === undefined ? undefined : cloneOAuthCredential(current);\n }\n\n const oauth = readOAuthCredential(next);\n if (oauth === undefined) {\n throw new TypeError(\n 'More source credential store only persists valid OAuth credentials'\n );\n }\n\n const persisted = cloneOAuthCredential(oauth);\n hooks.beforeWrite?.(providerId, credentialId);\n getPersistedCredentials()[credentialId] = persisted;\n return cloneOAuthCredential(persisted);\n },\n options\n );\n },\n\n /** 在同一凭证队列中删除凭证,与自动或手动刷新保持确定的先后顺序。 */\n delete(providerId: string, options?: AuthOperationOptions): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.resolve().then(() => {\n throwIfAborted(options);\n hooks.beforeDelete?.(providerId, undefined);\n });\n }\n return enqueue(\n credentialId,\n async () => {\n throwIfAborted(options);\n if (hooks.beforeDelete?.(providerId, credentialId) !== false) {\n delete getPersistedCredentials()[credentialId];\n }\n },\n options\n );\n },\n };\n}\n\n/** 捕获操作发起时所属的方案及活动连接,回调提交前复核原引用。 */\nfunction captureBinding(dataStore: DataStore) {\n const config = dataStore.settings.额外模型解析配置;\n const profileName = config.当前api方案;\n const profile = config.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const activeReferences = resolvePiCredentialReferences(config.pi, config.pi.credentials);\n const profileReferences =\n profile?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(profile.pi, config.pi.credentials);\n const source = config.模型来源;\n const provider = config.pi.provider;\n const authType = config.pi.authType;\n const generations = bindingGenerations.get(dataStore) ?? new Map();\n bindingGenerations.set(dataStore, generations);\n const scope = JSON.stringify([profileName, source, provider, authType]);\n const observedGenerations = new Map(generations);\n\n /** 仅修改发起操作时的所属方案;活动连接只有仍绑定原值时才同步更新。 */\n const replace = (providerId: string, nextId: string | undefined) => {\n const generationKey = `${scope}:${providerId}`;\n const generation = observedGenerations.get(generationKey) ?? 0;\n if ((generations.get(generationKey) ?? 0) !== generation) {\n throw new Error('The OAuth connection was changed by another authentication operation');\n }\n const current = dataStore.settings.额外模型解析配置;\n const target =\n profileReferences === undefined\n ? undefined\n : current.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const isActive =\n current.当前api方案 === profileName &&\n current.模型来源 === source &&\n current.pi.provider === provider &&\n current.pi.authType === authType;\n const currentRefs = resolvePiCredentialReferences(current.pi, current.pi.credentials);\n const targetRefs =\n target?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(target.pi, current.pi.credentials);\n if (isActive && currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The active OAuth credential changed while authentication was pending');\n }\n if (profileReferences !== undefined) {\n if (!target?.pi || targetRefs?.[providerId] !== profileReferences[providerId]) {\n throw new Error('The OAuth profile changed while authentication was pending');\n }\n } else if (!isActive || currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The OAuth connection changed while authentication was pending');\n }\n const updated = (refs: PiCredentialReferences) => {\n const result = { ...refs };\n if (nextId === undefined) delete result[providerId];\n else result[providerId] = nextId;\n return result;\n };\n generations.set(generationKey, generation + 1);\n if (target?.pi && targetRefs) target.pi.credentialIds = updated(targetRefs);\n if (isActive && currentRefs[providerId] === activeReferences[providerId]) {\n current.pi.credentialIds = updated(currentRefs);\n }\n };\n return { references: activeReferences, replace };\n}\n\n/** 是否仍有方案或活动连接引用该凭证;未迁移的旧方案也参与引用检查。 */\nfunction isCredentialReferenced(dataStore: DataStore, credentialId: string): boolean {\n const config = dataStore.settings.额外模型解析配置;\n const connections = [\n config.pi,\n ...config.api方案列表.flatMap(profile =>\n profile.backend === 'pi' && profile.pi ? [profile.pi] : []\n ),\n ];\n return connections.some(pi =>\n Object.values(resolvePiCredentialReferences(pi, config.pi.credentials)).includes(\n credentialId\n )\n );\n}\n\n/** 捕获当前连接的凭证引用;随后切换方案不会改变此对象读写的账号。 */\nexport function getPiCredentialStore(pi?: { credentialIds?: unknown }): CredentialStore {\n const dataStore = useDataStore();\n const settings = dataStore.settings.额外模型解析配置.pi;\n const references = resolvePiCredentialReferences(pi ?? settings, settings.credentials);\n return createPiCredentialStore(references, dataStore);\n}\n\n/** 捕获退出登录的所属方案,仅解绑该方案,最后一个引用消失后才删除集中凭证。 */\nexport function createPiOAuthLogoutStore(): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n return createPiCredentialStore(binding.references, dataStore, {\n beforeDelete(providerId, credentialId) {\n binding.replace(providerId, undefined);\n return credentialId !== undefined && !isCredentialReferenced(dataStore, credentialId);\n },\n });\n}\n\n/** 为登录结果分配全新记录,成功提交时才绑定原方案,避免覆盖其他账号或失败时丢失旧登录态。 */\nexport function createPiOAuthLoginStore(providerId: string, nonce: string): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n const id = `oauth:${encodeURIComponent(providerId)}:${nonce}`;\n return createPiCredentialStore({ [providerId]: id }, dataStore, {\n beforeWrite(writtenProvider, credentialId) {\n binding.replace(writtenProvider, credentialId);\n },\n });\n}\n","import {\n GoogleGenAI,\n type GenerateContentConfig,\n type GenerateContentParameters,\n type ThinkingConfig,\n} from '@google/genai';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n buildBaseOptions,\n calculateCost,\n clampThinkingLevel,\n collapseSystemMessages,\n convertGoogleMessages,\n convertGoogleTools,\n createAssistantMessageEventStream,\n getCurrentSystemPrompt,\n getCurrentTools,\n isGoogleThinkingPart,\n mapGoogleStopReason,\n resolveGoogleFunctionCallingMode,\n resolveGoogleThinkingLevel,\n retainGoogleThoughtSignature,\n retryGoogleRequest,\n supportsGoogleStrictToolSampling,\n type Api,\n type AssistantMessage,\n type AssistantMessageEventStream,\n type FetchFunction,\n type GoogleApiThinkingLevel,\n type Model,\n type ProviderHeaders,\n type ProviderStreams,\n type ResolvedGoogleThinkingLevel,\n type SimpleStreamOptions,\n type StreamOptions,\n type TextContent,\n type ThinkingBudgets,\n type ThinkingContent,\n type ToolCall,\n type TranscriptContext,\n} from './pi_gateway';\n\n/**\n * 当前锁定的 Google SDK 未公开实例级 fetch 选项,但内部 apiClient 保留了 HTTP 入口。\n * 只声明经过核对的最小结构;SDK 升级后若不再匹配,发送带凭证请求前即停止。\n * 该入口只替换当前实例的传输,不修改全局 fetch 或 SDK 原型。\n */\ntype InjectableGoogleClient = {\n apiClient?: {\n apiCall?: (\n url: string,\n init: RequestInit,\n retryOptions?: unknown,\n timeout?: number,\n abortSignal?: AbortSignal\n ) => Promise;\n };\n};\n\nconst GOOGLE_TRANSPORT_PROBE_KEY = 'mvu-google-transport-compatibility-check';\n\nexport class GoogleProxyAdapterCompatibilityError extends Error {\n readonly code = 'google_proxy_adapter_incompatible';\n readonly retryable = false;\n\n /** 将 SDK 传输注入点不兼容标记为不可重试错误,避免被包装成普通服务商失败。 */\n constructor() {\n super(\n 'The installed Google Generative AI SDK no longer exposes the audited per-client HTTP transport required by the SillyTavern proxy.'\n );\n this.name = 'GoogleProxyAdapterCompatibilityError';\n }\n}\n\n/** Google 自定义传输路径的请求选项,补齐通用流选项之外的工具选择和思考控制。 */\nexport interface GoogleProxyOptions extends StreamOptions {\n toolChoice?: 'auto' | 'none' | 'any';\n thinking?: {\n enabled: boolean;\n /** -1 由 Google 动态分配预算;0 关闭基于预算的思考。 */\n budgetTokens?: number;\n level?: GoogleApiThinkingLevel;\n };\n}\n\n/**\n * 仅替换单个 Google 客户端的 HTTP 调用实现,保留取消和超时信号。\n * 先验证当前 SDK 注入点;不修改全局 fetch 或客户端原型。\n */\nexport function installGoogleClientFetch(client: unknown, fetch_impl: FetchFunction): void {\n const api_client = (client as InjectableGoogleClient | null)?.apiClient;\n if (\n !api_client ||\n typeof api_client.apiCall !== 'function' ||\n api_client.apiCall.length !== 5\n ) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n\n installPiAbortSignalPolyfills();\n // GenAI 2.x moved abort/timeout out of RequestInit into apiCall arguments. Pi owns retries,\n // but the injected transport must preserve those signals through response-body consumption.\n /** 合并 RequestInit、SDK 参数和超时产生的信号,再交给本次请求的传输实现。 */\n const injected = (\n url: string,\n init: RequestInit,\n _retry_options?: unknown,\n timeout?: number,\n abort_signal?: AbortSignal\n ) => {\n const signals = [init.signal, abort_signal].filter(\n (signal): signal is AbortSignal => signal != null\n );\n if (timeout !== undefined && timeout > 0) {\n signals.push(AbortSignal.timeout(timeout));\n }\n if (signals.length === 0) {\n return fetch_impl(url, init);\n }\n const signal = AbortSignal.any(signals);\n signal.throwIfAborted();\n return fetch_impl(url, { ...init, signal });\n };\n try {\n api_client.apiCall = injected;\n } catch {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n if (api_client.apiCall !== injected) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n}\n\n/**\n * 在 Pi 懒加载流边界之前验证 SDK 注入点,保留明确的兼容错误。\n * 构造客户端与校验过程不发送网络请求。\n */\nexport function assertGoogleProxyAdapterCompatible(): void {\n const client = new GoogleGenAI({ apiKey: GOOGLE_TRANSPORT_PROBE_KEY });\n installGoogleClientFetch(client, async () => {\n throw new GoogleProxyAdapterCompatibilityError();\n });\n}\n\n/** 仅在请求指定了不同于全局 fetch 的传输实现时启用本地桥接。 */\nfunction shouldUseInjectedFetch(\n fetch_impl: FetchFunction | undefined\n): fetch_impl is FetchFunction {\n return fetch_impl !== undefined && fetch_impl !== globalThis.fetch;\n}\n\n/** 移除表示删除操作的 null 请求头,转换为 Google SDK 接受的字符串映射。 */\nfunction providerHeadersToRecord(\n headers: ProviderHeaders | undefined\n): Record | undefined {\n if (!headers) {\n return undefined;\n }\n const result: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== null) {\n result[key] = value;\n }\n }\n return Object.keys(result).length === 0 ? undefined : result;\n}\n\n/** 删除孤立的 UTF-16 代理码元,避免系统提示词编码失败。 */\nfunction sanitizeSurrogates(text: string): string {\n return text.replace(\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?): AssistantMessage {\n return {\n role: 'assistant',\n content: [],\n api: 'google-generative-ai' as Api,\n provider: model.provider,\n model: model.id,\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'pending',\n timestamp: Date.now(),\n };\n}\n\n/** 按本次模型端点和请求头创建 Google 客户端,并安装仅作用于该实例的 fetch。 */\nfunction createClient(\n model: Model<'google-generative-ai'>,\n api_key: string,\n options_headers: ProviderHeaders | undefined,\n fetch_impl: FetchFunction\n): GoogleGenAI {\n const http_options: {\n baseUrl?: string;\n apiVersion?: string;\n headers?: Record;\n } = {};\n if (model.baseUrl) {\n http_options.baseUrl = model.baseUrl;\n // Pi model base URLs already include their API version path.\n http_options.apiVersion = '';\n }\n const headers = providerHeadersToRecord({\n 'User-Agent': 'pi (browser)',\n ...model.headers,\n ...options_headers,\n });\n if (headers) {\n http_options.headers = headers;\n }\n\n const client = new GoogleGenAI({\n apiKey: api_key,\n httpOptions: Object.keys(http_options).length === 0 ? undefined : http_options,\n });\n installGoogleClientFetch(client, fetch_impl);\n return client;\n}\n\n/** 将 Pi 上下文、工具、采样和思考配置转换为 Google 生成参数,并保留取消信号。 */\nfunction buildParams(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions = {}\n): GenerateContentParameters {\n // Pi 1.x 将系统提示词和工具声明放进 transcript;Google 使用折叠后的系统消息。\n const normalized_context = collapseSystemMessages(context);\n const contents = convertGoogleMessages(model, normalized_context);\n const system_prompt = getCurrentSystemPrompt(normalized_context.messages);\n const tools = getCurrentTools(normalized_context.messages);\n const generation_config: GenerateContentConfig = {};\n if (options.temperature !== undefined) {\n generation_config.temperature = options.temperature;\n }\n if (options.maxTokens !== undefined) {\n generation_config.maxOutputTokens = options.maxTokens;\n }\n\n const supports_strict_mode = supportsGoogleStrictToolSampling(model.id);\n const function_calling_mode = tools.length\n ? resolveGoogleFunctionCallingMode(tools, options.toolChoice, supports_strict_mode)\n : undefined;\n const config: GenerateContentConfig = {\n ...(Object.keys(generation_config).length === 0 ? {} : generation_config),\n ...(system_prompt ? { systemInstruction: sanitizeSurrogates(system_prompt) } : {}),\n ...(tools.length ? { tools: convertGoogleTools(tools, false, supports_strict_mode) } : {}),\n ...(function_calling_mode === undefined\n ? {}\n : { toolConfig: { functionCallingConfig: { mode: function_calling_mode } } }),\n };\n\n if (options.thinking?.enabled && model.reasoning) {\n const thinking_config: ThinkingConfig = { includeThoughts: true };\n if (options.thinking.level !== undefined) {\n thinking_config.thinkingLevel = options.thinking\n .level as ThinkingConfig['thinkingLevel'];\n } else if (options.thinking.budgetTokens !== undefined) {\n thinking_config.thinkingBudget = options.thinking.budgetTokens;\n }\n config.thinkingConfig = thinking_config;\n } else if (model.reasoning && options.thinking && !options.thinking.enabled) {\n config.thinkingConfig = getDisabledThinkingConfig(model);\n }\n\n if (options.signal) {\n if (options.signal.aborted) {\n throw new Error('Request aborted');\n }\n config.abortSignal = options.signal;\n }\n\n return {\n model: model.id,\n contents,\n config,\n };\n}\n\nlet tool_call_counter = 0;\n\n/**\n * 通过注入的传输执行 Google 生成,并转换为 Pi 的文本、思考、工具和用量事件。\n * 客户端兼容性同步校验,避免初始化失败被吞为普通模型响应。\n */\nfunction streamWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const output = createOutput(model);\n const stream = createAssistantMessageEventStream();\n const api_key = options.apiKey;\n\n // Construct and verify the instance-local injection point synchronously. This cannot perform\n // network I/O, and preserves the compatibility error instead of converting it to provider data.\n const client = api_key ? createClient(model, api_key, options.headers, fetch_impl) : undefined;\n\n void (async () => {\n try {\n if (!api_key || !client) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n\n let params = buildParams(model, context, options);\n const next_params = await options.onPayload?.(params, model);\n if (next_params !== undefined) {\n params = next_params as GenerateContentParameters;\n }\n const google_stream = await retryGoogleRequest(\n () => client.models.generateContentStream(params),\n options\n );\n\n stream.push({ type: 'start', partial: output });\n let current_block: TextContent | ThinkingContent | null = null;\n const blocks = output.content;\n /** 取得当前输出块下标,使增量事件始终指向最新内容块。 */\n const block_index = () => blocks.length - 1;\n\n for await (const chunk of google_stream) {\n output.responseId ||= chunk.responseId;\n const candidate = chunk.candidates?.[0];\n if (candidate?.content?.parts) {\n for (const part of candidate.content.parts) {\n if (part.text !== undefined) {\n const is_thinking = isGoogleThinkingPart(part);\n if (\n !current_block ||\n (is_thinking && current_block.type !== 'thinking') ||\n (!is_thinking && current_block.type !== 'text')\n ) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n if (is_thinking) {\n current_block = {\n type: 'thinking',\n thinking: '',\n thinkingSignature: undefined,\n };\n output.content.push(current_block);\n stream.push({\n type: 'thinking_start',\n contentIndex: block_index(),\n partial: output,\n });\n } else {\n current_block = { type: 'text', text: '' };\n output.content.push(current_block);\n stream.push({\n type: 'text_start',\n contentIndex: block_index(),\n partial: output,\n });\n }\n }\n\n if (current_block.type === 'thinking') {\n current_block.thinking += part.text;\n current_block.thinkingSignature = retainGoogleThoughtSignature(\n current_block.thinkingSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'thinking_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n } else {\n current_block.text += part.text;\n current_block.textSignature = retainGoogleThoughtSignature(\n current_block.textSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'text_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n }\n }\n\n if (part.functionCall) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n current_block = null;\n }\n\n const provided_id = part.functionCall.id;\n const needs_new_id =\n !provided_id ||\n output.content.some(\n block => block.type === 'toolCall' && block.id === provided_id\n );\n const tool_call_id = needs_new_id\n ? `${part.functionCall.name}_${Date.now()}_${++tool_call_counter}`\n : provided_id;\n const tool_call: ToolCall = {\n type: 'toolCall',\n id: tool_call_id,\n name: part.functionCall.name || '',\n arguments: (part.functionCall.args as ToolCall['arguments']) ?? {},\n ...(part.thoughtSignature\n ? { thoughtSignature: part.thoughtSignature }\n : {}),\n };\n output.content.push(tool_call);\n stream.push({\n type: 'toolcall_start',\n contentIndex: block_index(),\n partial: output,\n });\n stream.push({\n type: 'toolcall_delta',\n contentIndex: block_index(),\n delta: JSON.stringify(tool_call.arguments),\n partial: output,\n });\n stream.push({\n type: 'toolcall_end',\n contentIndex: block_index(),\n toolCall: tool_call,\n partial: output,\n });\n }\n }\n }\n\n if (candidate?.finishReason) {\n output.rawStopReason = candidate.finishReason;\n output.stopReason = mapGoogleStopReason(candidate.finishReason);\n if (\n output.stopReason === 'stop' &&\n output.content.some(block => block.type === 'toolCall')\n ) {\n output.stopReason = 'toolUse';\n }\n }\n\n if (chunk.usageMetadata) {\n output.usage = {\n input:\n (chunk.usageMetadata.promptTokenCount || 0) -\n (chunk.usageMetadata.cachedContentTokenCount || 0),\n output:\n (chunk.usageMetadata.candidatesTokenCount || 0) +\n (chunk.usageMetadata.thoughtsTokenCount || 0),\n cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,\n cacheWrite: 0,\n reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,\n totalTokens: chunk.usageMetadata.totalTokenCount || 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n calculateCost(model, output.usage);\n }\n }\n\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n\n if (options.signal?.aborted) {\n throw new Error('Request was aborted');\n }\n if (output.stopReason === 'pending') {\n throw new Error('Google stream ended without a finish reason');\n }\n if (output.stopReason === 'aborted' || output.stopReason === 'error') {\n throw new Error(\n output.rawStopReason\n ? `Provider stopped with: ${output.rawStopReason}`\n : 'An unknown error occurred'\n );\n }\n\n stream.push({ type: 'done', reason: output.stopReason, message: output });\n stream.end();\n } catch (error) {\n for (const block of output.content) {\n if ('index' in block) {\n delete (block as { index?: number }).index;\n }\n }\n output.stopReason = options.signal?.aborted ? 'aborted' : 'error';\n output.errorMessage = formatGoogleError(error);\n stream.push({ type: 'error', reason: output.stopReason, error: output });\n stream.end();\n }\n })();\n\n return stream;\n}\n\n/** 识别使用思考等级配置的 Gemma 4 模型名称。 */\nfunction isGemma4Model(model: Model<'google-generative-ai'>): boolean {\n return /gemma-?4/.test(model.id.toLowerCase());\n}\n\n/** 识别 Gemini 3 Pro 系列,以使用其支持的思考等级范围。 */\nfunction isGemini3ProModel(model: Model<'google-generative-ai'>): boolean {\n return /gemini-3(?:\\.\\d+)?-pro/.test(model.id.toLowerCase());\n}\n\n/** 识别 Gemini 3 Flash 系列及对应 latest 别名。 */\nfunction isGemini3FlashModel(model: Model<'google-generative-ai'>): boolean {\n const id = model.id.toLowerCase();\n return (\n /gemini-3(?:\\.\\d+)?-flash/.test(id) ||\n id === 'gemini-flash-latest' ||\n id === 'gemini-flash-lite-latest'\n );\n}\n\n/** 为不同 Google 模型选择关闭或最低允许思考配置,避免发送不支持的零预算。 */\nfunction getDisabledThinkingConfig(model: Model<'google-generative-ai'>): ThinkingConfig {\n if (isGemini3ProModel(model)) {\n return { thinkingLevel: 'LOW' as ThinkingConfig['thinkingLevel'] };\n }\n if (isGemini3FlashModel(model) || isGemma4Model(model)) {\n return { thinkingLevel: 'MINIMAL' as ThinkingConfig['thinkingLevel'] };\n }\n return { thinkingBudget: 0 };\n}\n\n/** 把 Pi 思考等级映射到当前 Google 模型实际支持的等级。 */\nfunction getThinkingLevel(\n effort: ResolvedGoogleThinkingLevel,\n model: Model<'google-generative-ai'>\n): GoogleApiThinkingLevel {\n if (isGemini3ProModel(model)) {\n return effort === 'minimal' || effort === 'low' ? 'LOW' : 'HIGH';\n }\n if (isGemma4Model(model)) {\n return effort === 'minimal' || effort === 'low' ? 'MINIMAL' : 'HIGH';\n }\n switch (effort) {\n case 'minimal':\n return 'MINIMAL';\n case 'low':\n return 'LOW';\n case 'medium':\n return 'MEDIUM';\n case 'high':\n return 'HIGH';\n }\n}\n\n/** 优先使用自定义思考预算,否则采用相应模型的预算映射;未知模型交由服务端动态分配。 */\nfunction getGoogleBudget(\n model: Model<'google-generative-ai'>,\n level: ResolvedGoogleThinkingLevel,\n custom_budgets?: ThinkingBudgets\n): number {\n if (custom_budgets?.[level] !== undefined) {\n return custom_budgets[level];\n }\n if (model.id.includes('2.5-pro')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 32768 }[level];\n }\n if (model.id.includes('2.5-flash-lite')) {\n return { minimal: 512, low: 2048, medium: 8192, high: 24576 }[level];\n }\n if (model.id.includes('2.5-flash')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 24576 }[level];\n }\n return -1;\n}\n\n/** 将 Pi 简化选项解析为模型适用的思考等级或预算,再复用注入传输的流实现。 */\nfunction streamSimpleWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: SimpleStreamOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const api_key = options.apiKey;\n if (!api_key) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n const base: GoogleProxyOptions = {\n ...buildBaseOptions(model, context, options, api_key),\n toolChoice: options.toolChoice,\n };\n if (!options.reasoning) {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n\n const clamped_reasoning = clampThinkingLevel(model, options.reasoning);\n if (clamped_reasoning === 'off') {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);\n if (isGemini3ProModel(model) || isGemini3FlashModel(model) || isGemma4Model(model)) {\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: { enabled: true, level: getThinkingLevel(resolved_level, model) },\n },\n fetch_impl\n );\n }\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: {\n enabled: true,\n budgetTokens: getGoogleBudget(model, resolved_level, options.thinkingBudgets),\n },\n },\n fetch_impl\n );\n}\n\n/** 普通浏览器请求沿用 Pi 上游实现;具有独立 fetch 的代理或非流式请求走本地桥接。 */\nexport function createGoogleProxyAwareApi(upstream: ProviderStreams): ProviderStreams {\n return {\n /** 为完整流选项选择上游实现或带请求级传输的 Google 桥接。 */\n stream(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.stream(model, context, options);\n }\n return streamWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options as GoogleProxyOptions,\n options.fetch\n );\n },\n /** 为简化流选项选择上游实现或带请求级传输的 Google 桥接。 */\n streamSimple(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.streamSimple(model, context, options);\n }\n return streamSimpleWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options,\n options.fetch\n );\n },\n };\n}\n","import { type Api, type Model } from './pi_gateway';\nimport {\n getPiCatalogModels,\n getPiProviderDefinition,\n isPiCatalogModelApiCompatible,\n type PiAuthType,\n type PiProviderDefinition,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_registry';\nimport {\n getPiProviderApiBaseUrl,\n normalizePiApiBaseEndpoint,\n normalizePiTargetEndpoint,\n PiEndpointValidationError,\n} from './provider_target';\n\nexport { resolvePiApiKeyScope } from './provider_target';\n\n/** 设置中保存的 Pi 连接与模型选择,字符串取值仍需通过注册表校验。 */\nexport interface PiModelConfiguration {\n provider: string;\n api: string;\n authType: string;\n endpoint?: string;\n model: string;\n /** 0 或未填写表示使用目录窗口;正值覆盖目录,目录外模型必须显式填写。 */\n contextWindow?: number;\n}\n\n/** 模型解析边界的原始输入,接受 unknown 以校验旧设置和外部调用。 */\nexport interface ResolvePiModelInput {\n piConfig: PiModelConfiguration | unknown;\n /** 用户配置的最大回复 token 数,尚未应用模型目录的输出上限。 */\n maxTokens: number | unknown;\n apiKey?: string | unknown;\n}\n\n/** 连接、认证和模型预算校验的失败分类,供预检及本地化提示使用。 */\nexport type PiModelResolutionErrorCode =\n | 'invalid_config'\n | 'unknown_provider'\n | 'unsupported_api'\n | 'unsupported_auth'\n | 'missing_api_key'\n | 'invalid_endpoint'\n | 'custom_endpoint_not_allowed'\n | 'oauth_endpoint_not_allowed'\n | 'oauth_api_mismatch'\n | 'missing_model'\n | 'invalid_context_window'\n | 'missing_context_window'\n | 'invalid_max_tokens'\n | 'max_tokens_exceed_context';\n\nexport class PiModelResolutionError extends Error {\n /** 保留模型配置错误码,供预检和界面给出稳定的错误分类。 */\n constructor(\n public readonly code: PiModelResolutionErrorCode,\n message: string\n ) {\n super(message);\n this.name = 'PiModelResolutionError';\n }\n}\n\n/** 校验并规范化后的连接配置,保留目录依据与最终预算,供构造本次 Pi Model 使用。 */\nexport interface ValidatedPiConfiguration {\n definition: PiProviderDefinition;\n provider: PiProviderKey;\n api: PiWireApi;\n authType: PiAuthType;\n endpoint: string;\n modelId: string;\n /** 用户显式窗口;0 表示采用匹配目录项的上下文窗口。 */\n manualContextWindow: number;\n configuredMaxTokens: number;\n apiKey?: string;\n /** 仅在默认端点且协议兼容时使用,避免套用自定义端点的同名模型。 */\n catalogModel?: Model;\n effectiveContextWindow: number;\n /** 用户回复额度与适用目录输出上限的较小值。 */\n effectiveMaxTokens: number;\n}\n\n/** 交给运行时的模型解析结果,model 是本次请求独立的目录副本或动态元数据。 */\nexport interface ResolvedPiModel {\n definition: PiProviderDefinition;\n model: Model;\n /** 是否实际采用了匹配目录的元数据,而非仅命中同名模型。 */\n catalogHit: boolean;\n effectiveContextWindow: number;\n effectiveMaxTokens: number;\n authType: PiAuthType;\n apiKey?: string;\n}\n\n/** 判断输入是否为可读取配置字段的非空、非数组对象。 */\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 读取并规范必填字符串,缺失时抛出指定的模型配置错误。 */\nfunction requiredString(\n source: Record,\n field: string,\n errorCode: PiModelResolutionErrorCode,\n label: string\n): string {\n const value = source[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw new PiModelResolutionError(errorCode, `${label} must be a non-empty string`);\n }\n return value.trim();\n}\n\n/** 规范自定义端点并执行浏览器传输限制,将端点错误转换为模型配置错误。 */\nexport function normalizePiEndpoint(endpoint: string): string {\n try {\n return normalizePiTargetEndpoint(endpoint);\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n}\n\n/** 判断端点是否等同服务商的协议默认地址,空值与显式填写默认地址视为相同。 */\nexport function isPiDefaultProviderEndpoint(\n definition: PiProviderDefinition,\n endpoint: string,\n api: PiWireApi = definition.defaultApi\n): boolean {\n const requested = endpoint.trim();\n if (requested === '') {\n return true;\n }\n try {\n return (\n normalizePiApiBaseEndpoint(api, requested) ===\n normalizePiApiBaseEndpoint(api, getPiProviderApiBaseUrl(definition, api))\n );\n } catch {\n return false;\n }\n}\n\n/** 校验允许 0 哨兵值的配置整数,拒绝无效值隐式回退。 */\nfunction validateNonNegativeInteger(value: unknown, field: string): number {\n if (value === undefined) {\n return 0;\n }\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n `${field} must be a non-negative integer`\n );\n }\n return value;\n}\n\n/** 校验必须大于 0 的配置整数,供回复 token 等硬性预算使用。 */\nfunction validatePositiveInteger(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new PiModelResolutionError(\n 'invalid_max_tokens',\n `${field} must be a positive integer`\n );\n }\n return value;\n}\n\n/** 从所选服务商目录中查找模型,并检查目录项与当前协议是否兼容。 */\nfunction findCatalogModel(\n definition: PiProviderDefinition,\n modelId: string\n): Model | undefined {\n return getPiCatalogModels(definition.key).find(model => model.id === modelId);\n}\n\n/**\n * 校验服务商、协议、认证、端点、模型和 token 参数的组合。\n * 只使用适用于当前端点的目录元数据,不修改设置或共享目录对象。\n */\nexport function validatePiConfiguration(input: ResolvePiModelInput): ValidatedPiConfiguration {\n if (!isRecord(input.piConfig)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'More source configuration must be an object'\n );\n }\n\n const provider = requiredString(\n input.piConfig,\n 'provider',\n 'unknown_provider',\n 'More source provider'\n );\n const definition = getPiProviderDefinition(provider);\n if (!definition) {\n throw new PiModelResolutionError(\n 'unknown_provider',\n `Unknown More source provider: ${provider}`\n );\n }\n\n const api = requiredString(input.piConfig, 'api', 'unsupported_api', 'More source API');\n if (!definition.allowedApis.some(candidate => candidate === api)) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Provider ${definition.key} does not support API ${api}`\n );\n }\n\n const authType = requiredString(\n input.piConfig,\n 'authType',\n 'unsupported_auth',\n 'More source auth type'\n );\n if (!definition.allowedAuthTypes.some(candidate => candidate === authType)) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} does not support ${authType} authentication`\n );\n }\n\n const rawEndpoint = input.piConfig.endpoint;\n if (rawEndpoint !== undefined && typeof rawEndpoint !== 'string') {\n throw new PiModelResolutionError(\n 'invalid_endpoint',\n 'More source endpoint must be a string'\n );\n }\n const requestedEndpoint = (rawEndpoint ?? '').trim();\n\n if (authType === 'oauth') {\n if (!definition.oauth) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} has no OAuth registration`\n );\n }\n if (requestedEndpoint) {\n throw new PiModelResolutionError(\n 'oauth_endpoint_not_allowed',\n 'OAuth providers cannot use a custom endpoint'\n );\n }\n if (api !== definition.oauth.api) {\n throw new PiModelResolutionError(\n 'oauth_api_mismatch',\n `OAuth for ${definition.key} requires API ${definition.oauth.api}`\n );\n }\n } else if (requestedEndpoint && !definition.allowCustomEndpoint) {\n throw new PiModelResolutionError(\n 'custom_endpoint_not_allowed',\n `Provider ${definition.key} does not allow a custom endpoint`\n );\n }\n\n let endpoint: string;\n const defaultEndpoint = getPiProviderApiBaseUrl(definition, api as PiWireApi);\n try {\n endpoint = requestedEndpoint\n ? normalizePiApiBaseEndpoint(api as PiWireApi, requestedEndpoint)\n : defaultEndpoint;\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n const usesDefaultEndpoint = endpoint === normalizePiEndpoint(defaultEndpoint);\n const modelId = requiredString(input.piConfig, 'model', 'missing_model', 'More source model');\n const manualContextWindow = validateNonNegativeInteger(\n input.piConfig.contextWindow,\n 'More source contextWindow'\n );\n const configuredMaxTokens = validatePositiveInteger(input.maxTokens, 'maximum reply tokens');\n const catalogModelById = findCatalogModel(definition, modelId);\n const catalogModelMatchesApi =\n catalogModelById !== undefined &&\n isPiCatalogModelApiCompatible(definition, catalogModelById, api as PiWireApi);\n if (catalogModelById && !catalogModelMatchesApi && usesDefaultEndpoint) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Model ${modelId} is registered for ${catalogModelById.api}, not ${api}`\n );\n }\n // On a custom OpenAI-compatible endpoint the same identifier may refer to an entirely\n // different model. Require manual metadata and treat it as dynamic instead of replaying\n // catalog metadata from another wire API.\n const catalogModel =\n catalogModelMatchesApi && usesDefaultEndpoint ? catalogModelById : undefined;\n\n if (!catalogModel && manualContextWindow === 0) {\n throw new PiModelResolutionError(\n 'missing_context_window',\n `Model ${modelId} is not in the ${definition.key} catalog; contextWindow is required`\n );\n }\n\n const effectiveContextWindow =\n manualContextWindow > 0 ? manualContextWindow : catalogModel!.contextWindow;\n if (!Number.isInteger(effectiveContextWindow) || effectiveContextWindow <= 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n 'effective contextWindow must be a positive integer'\n );\n }\n if (configuredMaxTokens > effectiveContextWindow) {\n throw new PiModelResolutionError(\n 'max_tokens_exceed_context',\n 'maximum reply tokens must not exceed contextWindow'\n );\n }\n\n const effectiveMaxTokens = catalogModel\n ? Math.min(configuredMaxTokens, catalogModel.maxTokens)\n : configuredMaxTokens;\n\n let apiKey: string | undefined;\n if (authType === 'api_key') {\n if (typeof input.apiKey !== 'string' || input.apiKey.trim() === '') {\n throw new PiModelResolutionError(\n 'missing_api_key',\n `An API key is required for ${definition.key}`\n );\n }\n apiKey = input.apiKey.trim();\n }\n\n return {\n definition,\n provider: definition.providerId,\n api: api as PiWireApi,\n authType: authType as PiAuthType,\n endpoint,\n modelId,\n manualContextWindow,\n configuredMaxTokens,\n apiKey,\n catalogModel,\n effectiveContextWindow,\n effectiveMaxTokens,\n };\n}\n\n/** 为目录外或自定义端点模型构造独立元数据,使用显式窗口与保守能力默认值。 */\nfunction createDynamicModel(configuration: ValidatedPiConfiguration): Model {\n return {\n id: configuration.modelId,\n name: configuration.modelId,\n api: configuration.api,\n provider: configuration.provider,\n baseUrl: configuration.endpoint,\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: configuration.effectiveContextWindow,\n maxTokens: configuration.effectiveMaxTokens,\n };\n}\n\n/** 复制目录模型后应用本次端点和预算,防止请求修改共享目录元数据。 */\nfunction cloneCatalogModel(configuration: ValidatedPiConfiguration): Model {\n const original = configuration.catalogModel!;\n const model = structuredClone(original) as Model;\n\n model.api = configuration.api;\n model.provider = configuration.provider;\n model.baseUrl = configuration.endpoint;\n model.contextWindow = configuration.effectiveContextWindow;\n model.maxTokens = configuration.effectiveMaxTokens;\n\n // Compatibility metadata is API-shaped. Do not replay Responses flags into Completions.\n if (original.api !== configuration.api) {\n delete model.compat;\n }\n return model;\n}\n\n/** 在配置校验后选择目录模型或动态模型,并返回本次请求的有效预算及认证信息。 */\nexport function resolvePiModel(input: ResolvePiModelInput): ResolvedPiModel {\n const configuration = validatePiConfiguration(input);\n const catalogHit = configuration.catalogModel !== undefined;\n return {\n definition: configuration.definition,\n model: catalogHit ? cloneCatalogModel(configuration) : createDynamicModel(configuration),\n catalogHit,\n effectiveContextWindow: configuration.effectiveContextWindow,\n effectiveMaxTokens: configuration.effectiveMaxTokens,\n authType: configuration.authType,\n apiKey: configuration.apiKey,\n };\n}\n\n/** 从中文字段的额外模型设置中提取 Pi 连接和回复预算,再调用统一模型解析。 */\nexport function resolvePiModelFromExtraModelSettings(settings: unknown): ResolvedPiModel {\n if (!isRecord(settings)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'extra-model settings must be an object'\n );\n }\n return resolvePiModel({\n piConfig: settings.pi,\n maxTokens: settings['最大回复token数'],\n apiKey: settings['密钥'],\n });\n}\n","import {\n createPiOAuthLoginStore,\n createPiOAuthLogoutStore,\n getPiCredentialStore,\n} from './credential_store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport type { CredentialStore, ModelAuth, OAuthAuth, OAuthCredential } from './pi_gateway';\nimport { getPiProviderRegistration, type PiOAuthDefinition } from './provider_registry';\n\nconst DEFAULT_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst CLOSED_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst MAX_CLOSED_ATTEMPTS = 128;\nconst REFRESH_TIMEOUT_MS = 15_000;\nconst OPENAI_CODEX_PROVIDER_ID = 'openai-codex';\nconst ANTHROPIC_PROVIDER_ID = 'anthropic';\nconst OPENAI_ACCOUNT_CLAIM = 'https://api.openai.com/auth';\n\n/** 浏览器登录、回调校验、令牌交换及持久化的失败分类,供界面和取消逻辑使用。 */\nexport type PiOAuthErrorCode =\n | 'unsupported_provider'\n | 'browser_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_failed'\n | 'attempt_expired'\n | 'attempt_used'\n | 'cancelled'\n | 'browser_network'\n | 'token_http'\n | 'token_response'\n | 'account_id'\n | 'missing_credential'\n | 'credential_store';\n\nexport class PiOAuthError extends Error {\n readonly code: PiOAuthErrorCode;\n\n /** 为浏览器 OAuth 失败保留稳定错误码,供界面和取消逻辑分类处理。 */\n constructor(code: PiOAuthErrorCode, message: string) {\n super(message);\n this.name = 'PiOAuthError';\n this.code = code;\n }\n}\n\n/** 可展示给界面的登录尝试信息,不包含 PKCE verifier 或令牌。 */\nexport type PiOAuthAttemptView = {\n id: string;\n providerId: string;\n authorizationUrl: string;\n /** 本次回调可被接受的截止时间,使用 Unix 毫秒时间戳。 */\n expiresAt: number;\n};\n\n/** 对外展示的 OAuth 凭证摘要;loggedIn 表示已存有凭证,过期凭证仍可尝试刷新。 */\nexport type PiOAuthCredentialStatus = {\n loggedIn: boolean;\n type?: 'oauth';\n /** 已保存访问令牌的过期时间,使用 Unix 毫秒时间戳。 */\n expiresAt?: number;\n};\n\n/** 令牌请求需要的最小 fetch 契约,便于注入测试传输。 */\ntype FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise;\n\n/** 生成 state、PKCE verifier 和 SHA-256 challenge 所需的最小 Web Crypto 接口。 */\ntype CryptoLike = {\n getRandomValues(array: T): T;\n subtle: {\n digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise;\n };\n};\n\n/** 单次登录引用的只读服务商 OAuth 注册信息。 */\ntype OAuthMetadata = Readonly;\n\n/** 可替换的浏览器与存储依赖,未传入时使用页面环境和共享凭证仓库。 */\ntype PiOAuthDependencies = {\n fetch?: FetchLike;\n crypto?: CryptoLike;\n /** 返回 Unix 毫秒时间戳,用于登录有效期和令牌过期计算。 */\n now?: () => number;\n credentialStore?: CredentialStore;\n};\n\n/** 发起登录时的依赖、取消信号及回调等待期限。 */\nexport type BeginPiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n /** 从创建授权链接起计算的有效时长,单位为毫秒。 */\n attemptTtlMs?: number;\n};\n\n/** 完成回调校验和授权码交换时使用的依赖与取消信号。 */\nexport type CompletePiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n};\n\n/** 凭证查询、删除等操作共享的取消与存储配置。 */\nexport type PiOAuthOperationOptions = {\n signal?: AbortSignal;\n credentialStore?: CredentialStore;\n};\n\n/** 刷新令牌所需的操作选项,只额外依赖网络和时钟,不需要重新生成 PKCE。 */\nexport type RefreshPiOAuthOptions = PiOAuthOperationOptions &\n Pick;\n\n/** 构建 Pi OAuthAuth 桥接对象时注入的浏览器能力与登录有效时长。 */\nexport type BrowserOAuthAuthOptions = Pick & {\n attemptTtlMs?: number;\n};\n\n/** 仅保存在内存中的登录尝试,负责回调防重放、有效期和交换请求取消。 */\ntype PendingAttempt = {\n id: string;\n providerId: string;\n metadata: OAuthMetadata;\n /** 登录开始时捕获的所属方案仓库,回调不再解析当前界面。 */\n credentialStore: CredentialStore;\n /** PKCE 原始验证串,只在交换授权码时提交给令牌端点。 */\n verifier: string;\n /** 与回调严格匹配的随机 state,用于绑定本次授权请求。 */\n state: string;\n expiresAt: number;\n /** exchanging 表示授权码已被领取处理,阻止重复提交同一回调。 */\n phase: 'pending' | 'exchanging';\n controller: AbortController;\n timeout: ReturnType;\n /** 清理调用方取消监听,避免尝试结束后继续持有外部信号。 */\n detachCallerAbort?: () => void;\n};\n\n/** 短期保留已关闭尝试的原因,使迟到或重复回调得到明确错误。 */\ntype ClosedAttemptReason = 'used' | 'expired' | 'cancelled';\n\nconst pendingAttempts = new Map();\nconst closedAttempts = new Map();\n\n/** 构造带稳定错误码的 OAuth 错误。 */\nfunction oauthError(code: PiOAuthErrorCode, message: string): PiOAuthError {\n return new PiOAuthError(code, message);\n}\n\n/** 创建统一的登录取消错误,供各异步阶段使用。 */\nfunction cancellationError(): PiOAuthError {\n return oauthError('cancelled', 'More source OAuth login was cancelled.');\n}\n\n/** 将已取消信号转换为 OAuth 取消错误,阻止继续交换或写入令牌。 */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw cancellationError();\n }\n}\n\n/** 解析可注入的时钟,使登录有效期和刷新测试可以精确控制时间。 */\nfunction getNow(options?: Pick): () => number {\n return options?.now ?? Date.now;\n}\n\n/** 解析浏览器令牌请求使用的 fetch,缺少能力时返回明确的环境错误。 */\nfunction getFetch(options?: Pick): FetchLike {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires the browser Fetch API for token requests.'\n );\n }\n return fetchImpl.bind(globalThis) as FetchLike;\n}\n\n/** 确认 Web Crypto 提供随机数和 SHA-256,满足 OAuth state 与 PKCE 的生成要求。 */\nfunction getCrypto(options?: Pick): CryptoLike {\n const cryptoImpl = options?.crypto ?? (globalThis.crypto as CryptoLike | undefined);\n if (\n !cryptoImpl ||\n typeof cryptoImpl.getRandomValues !== 'function' ||\n typeof cryptoImpl.subtle?.digest !== 'function'\n ) {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires Web Crypto with SHA-256 support.'\n );\n }\n return cryptoImpl;\n}\n\n/** 优先使用注入的凭证仓库,否则复用运行时的共享仓库。 */\nfunction getCredentialStore(\n options?: Pick\n): CredentialStore {\n return options?.credentialStore ?? getPiCredentialStore();\n}\n\n/** 只解析已注册且支持浏览器登录的 OAuth 服务商,拒绝其他授权流程。 */\nfunction getOAuthMetadata(providerId: string): OAuthMetadata {\n const registration = getPiProviderRegistration(providerId);\n const metadata = registration?.oauth;\n if (!metadata || metadata.providerId !== providerId) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n if (providerId !== ANTHROPIC_PROVIDER_ID && providerId !== OPENAI_CODEX_PROVIDER_ID) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n return metadata;\n}\n\n/** 将随机字节或摘要编码为无填充的 Base64URL,供 PKCE 和 state 使用。 */\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return globalThis.btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\n/** 用密码学随机数生成指定字节长度的 Base64URL 标识。 */\nfunction randomBase64Url(cryptoImpl: CryptoLike, length: number): string {\n return bytesToBase64Url(cryptoImpl.getRandomValues(new Uint8Array(length)));\n}\n\n/** 生成仅保留在内存中的 verifier,并计算 S256 challenge。 */\nasync function createPkce(cryptoImpl: CryptoLike): Promise<{\n verifier: string;\n challenge: string;\n}> {\n const verifier = randomBase64Url(cryptoImpl, 32);\n const verifierBytes = Uint8Array.from(verifier, character => character.charCodeAt(0));\n const digest = await cryptoImpl.subtle.digest('SHA-256', verifierBytes);\n return {\n verifier,\n challenge: bytesToBase64Url(new Uint8Array(digest)),\n };\n}\n\n/** 按服务商元数据组装授权地址,附带回调地址、scope、PKCE 和 state。 */\nfunction createAuthorizationUrl(metadata: OAuthMetadata, challenge: string, state: string): string {\n const url = new URL(metadata.authorizeUrl);\n for (const [name, value] of Object.entries(metadata.authorizeParams)) {\n url.searchParams.set(name, value);\n }\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', metadata.clientId);\n url.searchParams.set('redirect_uri', metadata.redirectUri);\n url.searchParams.set('scope', metadata.scope);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n return url.toString();\n}\n\n/** 短期记录已关闭尝试的原因,并限制记录数量,使重复回调可得到准确错误。 */\nfunction rememberClosedAttempt(id: string, reason: ClosedAttemptReason): void {\n const now = Date.now();\n for (const [closedId, closed] of closedAttempts) {\n if (closed.removeAfter <= now) {\n closedAttempts.delete(closedId);\n }\n }\n while (closedAttempts.size >= MAX_CLOSED_ATTEMPTS) {\n const oldestId = closedAttempts.keys().next().value as string | undefined;\n if (oldestId === undefined) {\n break;\n }\n closedAttempts.delete(oldestId);\n }\n closedAttempts.set(id, { reason, removeAfter: now + CLOSED_ATTEMPT_TTL_MS });\n}\n\n/** 关闭当前登录尝试,释放计时器和监听,清除 verifier 与 state,并记录关闭原因。 */\nfunction closeAttempt(attempt: PendingAttempt, reason: ClosedAttemptReason, abort: boolean): void {\n if (pendingAttempts.get(attempt.id) !== attempt) {\n return;\n }\n pendingAttempts.delete(attempt.id);\n clearTimeout(attempt.timeout);\n attempt.detachCallerAbort?.();\n if (abort && !attempt.controller.signal.aborted) {\n attempt.controller.abort();\n }\n attempt.verifier = '';\n attempt.state = '';\n rememberClosedAttempt(attempt.id, reason);\n}\n\n/** 使超时尝试失效并取消进行中的令牌交换。 */\nfunction expireAttempt(attempt: PendingAttempt): void {\n closeAttempt(attempt, 'expired', true);\n}\n\n/** 将调用方取消传递给内部控制器,并返回解绑函数。 */\nfunction linkAbortSignal(signal: AbortSignal | undefined, controller: AbortController): () => void {\n if (!signal) {\n return () => undefined;\n }\n /** 将外部取消转发给当前令牌操作。 */\n const onAbort = () => controller.abort();\n if (signal.aborted) {\n onAbort();\n } else {\n signal.addEventListener('abort', onAbort, { once: true });\n }\n return () => signal.removeEventListener('abort', onAbort);\n}\n\n/** 根据已关闭原因区分过期、取消和回调重复使用。 */\nfunction errorForClosedAttempt(reason: ClosedAttemptReason): PiOAuthError {\n if (reason === 'expired') {\n return oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (reason === 'cancelled') {\n return cancellationError();\n }\n return oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n}\n\n/** 取得仍处于待回调阶段的有效尝试,拒绝过期、已消费或已取消的记录。 */\nfunction getPendingAttempt(attemptId: string, now: number): PendingAttempt {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n const closed = closedAttempts.get(attemptId);\n if (closed) {\n throw errorForClosedAttempt(closed.reason);\n }\n throw oauthError(\n 'attempt_used',\n 'No active More source OAuth login attempt matches this callback.'\n );\n }\n if (now >= attempt.expiresAt) {\n expireAttempt(attempt);\n throw oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (attempt.phase !== 'pending') {\n throw oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n }\n if (attempt.controller.signal.aborted) {\n closeAttempt(attempt, 'cancelled', false);\n throw cancellationError();\n }\n return attempt;\n}\n\n/** 通过地址与 state 校验后提取的授权码回调,不再携带完整 URL。 */\ntype ParsedCallback = { code: string; state: string };\n\n/** 补齐 URL 的默认端口,使回调地址校验不受显式端口写法影响。 */\nfunction normalizePort(url: URL): string {\n if (url.port) {\n return url.port;\n }\n return url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '';\n}\n\n/**\n * 校验完整回环回调地址的协议、主机、端口、路径和 state,再提取授权码。\n * 回调必须属于当前尝试;服务商返回的授权错误也在此转换为固定错误。\n */\nfunction parseAndValidateCallback(callbackUrl: string, attempt: PendingAttempt): ParsedCallback {\n let callback: URL;\n let redirect: URL;\n try {\n callback = new URL(callbackUrl.trim());\n redirect = new URL(attempt.metadata.redirectUri);\n } catch {\n throw oauthError(\n 'invalid_callback',\n 'Paste the complete loopback callback URL from the browser address bar.'\n );\n }\n\n const allowedHosts = new Set(\n attempt.metadata.allowedCallbackHosts.map(host => host.toLowerCase())\n );\n allowedHosts.add(redirect.hostname.toLowerCase());\n if (\n callback.protocol !== 'http:' ||\n callback.username !== '' ||\n callback.password !== '' ||\n callback.hash !== '' ||\n !allowedHosts.has(callback.hostname.toLowerCase()) ||\n normalizePort(callback) !== normalizePort(redirect) ||\n callback.pathname !== redirect.pathname\n ) {\n throw oauthError(\n 'invalid_callback',\n 'The More source OAuth callback does not match the registered loopback address.'\n );\n }\n\n const state = callback.searchParams.get('state') ?? '';\n if (!state || state !== attempt.state) {\n throw oauthError('state_mismatch', 'The More source OAuth callback state does not match.');\n }\n if (callback.searchParams.has('error')) {\n throw oauthError(\n 'authorization_failed',\n 'The OAuth provider did not authorize the More source login.'\n );\n }\n const code = callback.searchParams.get('code') ?? '';\n if (!code) {\n throw oauthError('invalid_callback', 'The More source OAuth callback is missing its code.');\n }\n return { code, state };\n}\n\n/** 综合信号和 OAuth 错误码识别取消,避免误报为网络或存储故障。 */\nfunction isCancellation(error: unknown, signal: AbortSignal): boolean {\n return signal.aborted || (error instanceof PiOAuthError && error.code === 'cancelled');\n}\n\n/** 区分主动取消与浏览器网络失败,不透传可能含敏感请求数据的底层错误。 */\nfunction normalizeFetchFailure(error: unknown, signal: AbortSignal): never {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n if (error instanceof TypeError) {\n throw oauthError(\n 'browser_network',\n 'The browser could not reach the More source OAuth token endpoint. Check network access and whether the provider allows browser CORS requests.'\n );\n }\n throw oauthError(\n 'browser_network',\n 'The More source OAuth token request failed in the browser.'\n );\n}\n\n/** 令牌交换的两种输入:首次登录使用授权码和 PKCE,续期使用刷新令牌。 */\ntype TokenGrant =\n | { type: 'authorization_code'; code: string; verifier: string; state: string }\n | { type: 'refresh_token'; refreshToken: string };\n\n/** 已校验并统一字段名的令牌响应,持久化前还需计算过期时间和补齐账号信息。 */\ntype TokenData = {\n access: string;\n refresh: string;\n /** 服务商返回的有效时长,单位为秒,并非绝对时间戳。 */\n expiresInSeconds: number;\n};\n\n/** 按授权码或刷新令牌两种 grant 生成令牌请求字段,并遵循服务商的 state 约定。 */\nfunction createTokenFields(metadata: OAuthMetadata, grant: TokenGrant): Record {\n const fields: Record = { ...metadata.tokenParams };\n fields.client_id = metadata.clientId;\n if (grant.type === 'authorization_code') {\n fields.grant_type = 'authorization_code';\n fields.code = grant.code;\n fields.code_verifier = grant.verifier;\n fields.redirect_uri = metadata.redirectUri;\n if (metadata.includeStateInTokenRequest) {\n fields.state = grant.state;\n }\n } else {\n fields.grant_type = 'refresh_token';\n fields.refresh_token = grant.refreshToken;\n }\n return fields;\n}\n\n/** 校验令牌接口状态和必需字段;刷新响应未返回 refresh_token 时保留原刷新令牌。 */\nasync function readTokenResponse(\n response: Response,\n fallbackRefreshToken?: string\n): Promise {\n if (!response.ok) {\n throw oauthError(\n 'token_http',\n `The More source OAuth token endpoint rejected the request (HTTP ${response.status}).`\n );\n }\n\n let body: unknown;\n try {\n body = JSON.parse(await response.text());\n } catch {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n if (!body || typeof body !== 'object') {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n const token = body as Record;\n const access = token.access_token;\n const refresh = token.refresh_token ?? fallbackRefreshToken;\n const expiresIn = token.expires_in;\n if (\n typeof access !== 'string' ||\n access.length === 0 ||\n typeof refresh !== 'string' ||\n refresh.length === 0 ||\n typeof expiresIn !== 'number' ||\n !Number.isFinite(expiresIn) ||\n expiresIn <= 0\n ) {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint response is missing required fields.'\n );\n }\n return { access, refresh, expiresInSeconds: expiresIn };\n}\n\n/** 按服务商要求用 JSON 或表单交换令牌,并在请求前后检查取消。 */\nasync function requestTokens(\n metadata: OAuthMetadata,\n grant: TokenGrant,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n const fields = createTokenFields(metadata, grant);\n const headers: Record = { Accept: 'application/json' };\n let body: string;\n if (metadata.exchangeKind === 'json') {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(fields);\n } else {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n body = new URLSearchParams(fields).toString();\n }\n\n let response: Response;\n try {\n response = await getFetch(options)(metadata.tokenUrl, {\n method: 'POST',\n headers,\n body,\n signal,\n });\n } catch (error) {\n normalizeFetchFailure(error, signal);\n }\n throwIfAborted(signal);\n return readTokenResponse(\n response!,\n grant.type === 'refresh_token' ? grant.refreshToken : undefined\n );\n}\n\n/** 解码 JWT 中的 Base64URL JSON 数据,兼容缺少 TextDecoder 的浏览器。 */\nfunction decodeBase64UrlJson(value: string): unknown {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const binary = globalThis.atob(padded);\n const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));\n const json =\n typeof globalThis.TextDecoder === 'function'\n ? new globalThis.TextDecoder().decode(bytes)\n : decodeURIComponent(\n [...bytes].map(byte => `%${byte.toString(16).padStart(2, '0')}`).join('')\n );\n return JSON.parse(json);\n}\n\n/** 从访问令牌的账号声明中读取 Codex 所需账号标识;缺失时拒绝建立凭证。 */\nfunction extractOpenAIAccountId(accessToken: string): string {\n try {\n const parts = accessToken.split('.');\n if (parts.length !== 3) {\n throw new Error('invalid JWT');\n }\n const payload = decodeBase64UrlJson(parts[1]);\n if (!payload || typeof payload !== 'object') {\n throw new Error('invalid JWT payload');\n }\n const authClaim = (payload as Record)[OPENAI_ACCOUNT_CLAIM];\n if (!authClaim || typeof authClaim !== 'object') {\n throw new Error('missing auth claim');\n }\n const accountId = (authClaim as Record).chatgpt_account_id;\n if (typeof accountId !== 'string' || accountId.length === 0) {\n throw new Error('missing account ID');\n }\n return accountId;\n } catch {\n throw oauthError(\n 'account_id',\n 'The OpenAI Codex OAuth token does not contain a ChatGPT account ID.'\n );\n }\n}\n\n/** 将令牌响应转换为 Pi OAuth 凭证,预留过期时间偏差,并补齐 Codex 账号标识。 */\nfunction credentialFromTokens(\n metadata: OAuthMetadata,\n token: TokenData,\n now: number\n): OAuthCredential {\n const credential: OAuthCredential = {\n type: 'oauth',\n access: token.access,\n refresh: token.refresh,\n expires: now + token.expiresInSeconds * 1000 - metadata.expirySkewMs,\n };\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n credential.accountId = extractOpenAIAccountId(token.access);\n }\n return credential;\n}\n\n/** 通过共享凭证队列保存登录结果,分别处理取消与存储失败。 */\nasync function persistCredential(\n providerId: string,\n credential: OAuthCredential,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n try {\n await getCredentialStore(options).modify(providerId, async () => credential, { signal });\n } catch (error) {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'More source OAuth login succeeded, but the credential could not be saved.'\n );\n }\n}\n\n/**\n * 创建带有效期的浏览器登录尝试,生成 PKCE 和 state,返回供界面打开的授权地址。\n * 秘密校验信息留在内存中,调用方取消或超时会关闭该尝试。\n */\nexport async function beginPiOAuth(\n providerId: string,\n options: BeginPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const metadata = getOAuthMetadata(providerId);\n const ttl = options.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new RangeError('More source OAuth attemptTtlMs must be a positive number.');\n }\n const cryptoImpl = getCrypto(options);\n const credentialStore =\n options.credentialStore ??\n createPiOAuthLoginStore(providerId, randomBase64Url(cryptoImpl, 18));\n const { verifier, challenge } = await createPkce(cryptoImpl);\n throwIfAborted(options.signal);\n\n const state = randomBase64Url(cryptoImpl, 32);\n const id = randomBase64Url(cryptoImpl, 18);\n const expiresAt = getNow(options)() + ttl;\n const controller = new AbortController();\n const attempt: PendingAttempt = {\n id,\n providerId,\n metadata,\n credentialStore,\n verifier,\n state,\n expiresAt,\n phase: 'pending' as const,\n controller,\n timeout: undefined as unknown as ReturnType,\n };\n attempt.timeout = setTimeout(() => expireAttempt(attempt), ttl);\n pendingAttempts.set(id, attempt);\n if (options.signal) {\n /** 关闭已取消的登录尝试,阻止随后到达的回调继续交换令牌。 */\n const onAbort = () => closeAttempt(attempt, 'cancelled', true);\n options.signal.addEventListener('abort', onAbort, { once: true });\n attempt.detachCallerAbort = () => options.signal?.removeEventListener('abort', onAbort);\n if (options.signal.aborted) {\n onAbort();\n throw cancellationError();\n }\n }\n\n return {\n id,\n providerId,\n authorizationUrl: createAuthorizationUrl(metadata, challenge, state),\n expiresAt,\n };\n}\n\n/**\n * 校验并一次性消费回调,交换授权码后保存凭证,最终清理尝试状态。\n * 重复回调或交换期间取消都不能产生第二次有效登录。\n */\nexport async function completePiOAuth(\n attemptId: string,\n callbackUrl: string,\n options: CompletePiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const now = getNow(options);\n const attempt = getPendingAttempt(attemptId, now());\n let parsed: ParsedCallback;\n try {\n parsed = parseAndValidateCallback(callbackUrl, attempt);\n } catch (error) {\n if (error instanceof PiOAuthError && error.code === 'authorization_failed') {\n closeAttempt(attempt, 'used', true);\n }\n throw error;\n }\n attempt.phase = 'exchanging';\n const detachCompleteAbort = linkAbortSignal(options.signal, attempt.controller);\n\n try {\n throwIfAborted(attempt.controller.signal);\n const token = await requestTokens(\n attempt.metadata,\n {\n type: 'authorization_code',\n code: parsed.code,\n verifier: attempt.verifier,\n state: parsed.state,\n },\n attempt.controller.signal,\n options\n );\n const credential = credentialFromTokens(attempt.metadata, token, now());\n await persistCredential(attempt.providerId, credential, attempt.controller.signal, {\n credentialStore: options.credentialStore ?? attempt.credentialStore,\n });\n closeAttempt(attempt, 'used', false);\n return credential;\n } catch (error) {\n const cancelled = isCancellation(error, attempt.controller.signal);\n closeAttempt(attempt, cancelled ? 'cancelled' : 'used', true);\n if (cancelled) {\n throw cancellationError();\n }\n throw error;\n } finally {\n detachCompleteAbort();\n }\n}\n\n/** 按尝试编号取消登录,清除回调校验状态并中止未完成的交换。 */\nexport function cancelPiOAuth(attemptId: string): boolean {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n return false;\n }\n closeAttempt(attempt, 'cancelled', true);\n return true;\n}\n\n/** 取消全部待完成的登录尝试,供界面卸载和脚本清理使用。 */\nexport function cancelAllPiOAuth(): void {\n for (const attempt of [...pendingAttempts.values()]) {\n closeAttempt(attempt, 'cancelled', true);\n }\n}\n\n/** 解除捕获方案的登录绑定;其他方案仍引用的集中凭证会继续保留。 */\nexport async function logoutPiOAuth(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n try {\n const store = options.credentialStore ?? createPiOAuthLogoutStore();\n await store.delete(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential could not be removed.'\n );\n }\n}\n\n/** 返回是否已登录及过期时间等界面状态,不暴露访问令牌和刷新令牌。 */\nexport async function getPiOAuthCredentialStatus(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n let credential;\n try {\n credential = await getCredentialStore(options).read(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential status could not be read.'\n );\n }\n if (!credential || credential.type !== 'oauth') {\n return { loggedIn: false };\n }\n return {\n loggedIn: true,\n type: 'oauth',\n expiresAt: credential.expires,\n };\n}\n\n/**\n * 由界面强制刷新现有 OAuth 凭证,与 Pi 自动刷新共用同一服务商修改锁。\n * 操作使用捕获的凭证仓库和取消信号,刷新完成后再提交新凭证。\n */\nexport async function refreshPiOAuth(\n providerId: string,\n options: RefreshPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const auth = getBrowserOAuthAuth(providerId, options);\n const store = getCredentialStore(options);\n installPiAbortSignalPolyfills();\n const signal = AbortSignal.any([\n ...(options.signal ? [options.signal] : []),\n AbortSignal.timeout(REFRESH_TIMEOUT_MS),\n ]);\n /** 生成缺少可刷新 OAuth 凭证的错误,供刷新入口统一报告。 */\n const missingCredential = () =>\n oauthError('missing_credential', 'Sign in before refreshing OAuth credentials.');\n try {\n const observed = await store.read(providerId, { signal });\n if (observed?.type !== 'oauth') {\n throw missingCredential();\n }\n const refreshed = await store.modify(\n providerId,\n async current => {\n throwIfAborted(signal);\n if (current?.type !== 'oauth') {\n throw missingCredential();\n }\n // An automatic refresh or another login may have completed while we queued.\n // Keep that result instead of reusing or rotating its token a second time.\n if (\n current.access !== observed.access ||\n current.refresh !== observed.refresh ||\n current.expires !== observed.expires\n ) {\n return undefined;\n }\n return auth.refresh(current, signal);\n },\n { signal }\n );\n if (refreshed?.type !== 'oauth') {\n throw missingCredential();\n }\n return { loggedIn: true, type: 'oauth', expiresAt: refreshed.expires };\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n if (signal.aborted) {\n throw oauthError('browser_network', 'The OAuth credential refresh timed out.');\n }\n if (error instanceof PiOAuthError) {\n throw error;\n }\n throw oauthError('credential_store', 'The refreshed OAuth credential could not be saved.');\n }\n}\n\n/**\n * 将浏览器授权、刷新和凭证转换接入 Pi 的 OAuthAuth 契约。\n * 登录通过授权地址与手动回调交互完成,不启动 Node 回环服务器。\n */\nfunction createBrowserOAuthAuth(\n metadata: OAuthMetadata,\n options: BrowserOAuthAuthOptions\n): OAuthAuth {\n const providerName =\n metadata.providerId === ANTHROPIC_PROVIDER_ID\n ? 'Anthropic (Claude Pro/Max)'\n : 'OpenAI (ChatGPT Plus/Pro)';\n return {\n name: providerName,\n isSubscription: true,\n /** 通过 Pi 认证交互展示授权地址、接收回调,并在结束时释放登录尝试。 */\n async login(interaction) {\n const attempt = await beginPiOAuth(metadata.providerId, {\n ...options,\n signal: interaction.signal,\n });\n interaction.notify({\n type: 'auth_url',\n url: attempt.authorizationUrl,\n instructions: 'Complete login, then paste the complete loopback callback URL here.',\n });\n let callbackUrl: string;\n try {\n callbackUrl = await interaction.prompt({\n type: 'manual_code',\n message: 'Paste the complete loopback callback URL:',\n placeholder: metadata.redirectUri.replace('localhost', '127.0.0.1'),\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n if (interaction.signal.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'invalid_callback',\n 'More source OAuth callback input was cancelled.'\n );\n }\n try {\n return await completePiOAuth(attempt.id, callbackUrl, {\n ...options,\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n throw error;\n }\n },\n /** 为 Pi 自动续期交换 refresh_token,保留请求的取消信号并更新过期时间。 */\n async refresh(credential, signal) {\n const token = await requestTokens(\n metadata,\n { type: 'refresh_token', refreshToken: credential.refresh },\n signal,\n options\n );\n return credentialFromTokens(metadata, token, getNow(options)());\n },\n /** 把持久化 OAuth 凭证转换为 Pi 请求认证信息,保留服务商需要的账号字段。 */\n async toAuth(credential): Promise {\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n extractOpenAIAccountId(credential.access);\n }\n return { apiKey: credential.access };\n },\n };\n}\n\n/** 按服务商解析浏览器 OAuth 实现,并传入可替换的请求和时钟依赖。 */\nexport function getBrowserOAuthAuth(\n providerId: string,\n options: BrowserOAuthAuthOptions = {}\n): OAuthAuth {\n return createBrowserOAuthAuth(getOAuthMetadata(providerId), options);\n}\n","import { uuidv4 } from '@util/common';\n\nexport const PI_PROMPT_CAPTURE_MODEL_PREFIX = 'mvu-pi-prompt-capture:';\nexport const PI_PROMPT_CAPTURE_API_URL =\n 'https://mvu-pi-prompt-capture.invalid/v1/chat/completions';\n\n/** 一次提示词捕获的诊断快照;只含状态和标记,不暴露完整提示词。 */\nexport type PromptCaptureDiagnostics = {\n generationId: string;\n /** 写入占位请求 model 字段的唯一标记,用于隔离并发生成事件。 */\n marker: string;\n markerMatched: boolean;\n captured: boolean;\n /** 占位生成是否已被成功停止,避免捕获后继续请求酒馆后端。 */\n stopSucceeded: boolean;\n /** 首次失败原因;清理阶段不会覆盖它。 */\n captureError?: string;\n};\n\n/** 从酒馆设置就绪事件复制出的最终消息及其所属生成编号。 */\nexport type CapturedPrompt = {\n generationId: string;\n messages: SillyTavern.SendingMessage[];\n};\n\n/** 捕获完成后的消息、诊断信息和接管回调的返回值。 */\nexport type PromptCaptureResult = PromptCaptureDiagnostics &\n CapturedPrompt & {\n result?: T;\n };\n\n/** 接管捕获结果并接收原生停止事件的回调,使 Pi 请求沿用酒馆的生成生命周期。 */\nexport type PromptCaptureOptions = {\n /** 在提示词就绪监听内等待此回调完成,期间保留酒馆原生停止按钮。 */\n onCaptured: (prompt: CapturedPrompt) => Promise;\n /** 仅转发用户停止当前生成的事件,不包含内部占位请求清理。 */\n onStopped: (generation_id: string) => void;\n};\n\n/** 进行中捕获的内部可变状态,messages 仅在成功复制提示词后存在。 */\ntype MutablePromptCaptureState = PromptCaptureDiagnostics & {\n messages?: SillyTavern.SendingMessage[];\n};\n\n/** 酒馆设置就绪事件中本适配层需要读取的字段,其余请求参数保持透明。 */\ntype SettingsReadyData = {\n messages: SillyTavern.SendingMessage[];\n model: string;\n [key: string]: unknown;\n};\n\n/** generate / generateRaw 的共同调用形状;捕获只依赖事件,不使用它们的返回结果。 */\ntype GenerateRunner = (config: TConfig) => Promise;\n\nconst pending_prompt_captures = new Map();\n\n/** 将非空生成编号编码为专用模型标记,用于从设置就绪事件中识别本次捕获。 */\nexport function encodePromptCaptureMarker(generation_id: string): string {\n if (generation_id.length === 0) {\n throw new Error('Prompt capture generation_id cannot be empty');\n }\n\n try {\n return `${PI_PROMPT_CAPTURE_MODEL_PREFIX}${encodeURIComponent(generation_id)}`;\n } catch (error) {\n throw new Error(`Prompt capture generation_id cannot be encoded: ${generation_id}`, {\n cause: error,\n });\n }\n}\n\n/** 只接受可规范往返编码的捕获标记,普通模型名或畸形标记返回 null。 */\nexport function decodePromptCaptureMarker(model: unknown): string | null {\n if (typeof model !== 'string' || !model.startsWith(PI_PROMPT_CAPTURE_MODEL_PREFIX)) {\n return null;\n }\n\n const encoded_generation_id = model.slice(PI_PROMPT_CAPTURE_MODEL_PREFIX.length);\n if (encoded_generation_id.length === 0) {\n return null;\n }\n\n try {\n const generation_id = decodeURIComponent(encoded_generation_id);\n if (\n generation_id.length === 0 ||\n encodeURIComponent(generation_id) !== encoded_generation_id\n ) {\n return null;\n }\n return generation_id;\n } catch {\n return null;\n }\n}\n\n/**\n * 将生成配置转换为专用捕获请求,移除业务工具、Schema、凭证和自定义覆盖。\n * 请求指向固定无效地址,并应在最终提示词捕获后被停止,避免误发到主模型。\n */\nexport function buildPromptCaptureConfig(\n config: TConfig,\n generation_id: string\n): TConfig {\n const marker = encodePromptCaptureMarker(generation_id);\n\n return {\n ...config,\n generation_id,\n should_stream: false,\n should_silence: config.should_silence ?? true,\n tools: [],\n tool_choice: 'none',\n json_schema: undefined,\n custom_api: {\n source: 'custom',\n apiurl: PI_PROMPT_CAPTURE_API_URL,\n key: '',\n model: marker,\n custom_include_body: {},\n custom_exclude_body: [],\n custom_include_headers: {},\n },\n } as TConfig;\n}\n\n/** 返回进行中捕获的诊断快照,不暴露完整提示词内容。 */\nexport function getPendingPromptCaptureDiagnostics(): PromptCaptureDiagnostics[] {\n return Array.from(pending_prompt_captures.values(), state => ({\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n ...(state.captureError === undefined ? {} : { captureError: state.captureError }),\n }));\n}\n\n/** 只有标记匹配、提示词复制成功且生成已停止,才认定捕获完整成功。 */\nfunction hasCompletedCapture(state: MutablePromptCaptureState): boolean {\n return state.markerMatched && state.captured && state.stopSucceeded;\n}\n\n/** 只记录首次捕获失败原因,避免清理阶段覆盖最有用的错误。 */\nfunction recordCaptureError(state: MutablePromptCaptureState, message: string): void {\n state.captureError ??= message;\n}\n\n/**\n * 在酒馆设置就绪事件的最后阶段复制最终提示词,并以生成编号隔离并发捕获。\n * 可等待 Pi 回调结束以维持原生停止按钮;随后停止占位请求,最终释放全部监听和登记。\n */\nexport async function capturePrompt(\n run: GenerateRunner,\n config: TConfig,\n options?: PromptCaptureOptions\n): Promise> {\n const generation_id = config.generation_id || uuidv4();\n const marker = encodePromptCaptureMarker(generation_id);\n\n if (pending_prompt_captures.has(generation_id)) {\n throw new Error(`Prompt capture generation_id is already pending: ${generation_id}`);\n }\n\n const state: MutablePromptCaptureState = {\n generationId: generation_id,\n marker,\n markerMatched: false,\n captured: false,\n stopSucceeded: false,\n };\n const capture_config = buildPromptCaptureConfig(config, generation_id);\n let captured_result: T | undefined;\n let callback_failed = false;\n let callback_error: unknown;\n let callback_started = false;\n let listening_for_stop = options !== undefined;\n /** 仅将当前生成编号的用户停止事件传给 Pi 回调,不影响其他并发请求。 */\n const stop_listener = (stopped_id?: string) => {\n if (listening_for_stop && stopped_id === generation_id) {\n options?.onStopped(generation_id);\n }\n };\n /** 关闭并移除停止监听,防止内部终止占位请求被误判为用户取消。 */\n const remove_stop_listener = () => {\n listening_for_stop = false;\n if (options) {\n eventRemoveListener(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n };\n\n /** 匹配当前捕获标记后复制最终提示词,等待接管回调,再阻止占位请求进入酒馆后端。 */\n const listener = async (generate_data: SettingsReadyData) => {\n const event_generation_id = decodePromptCaptureMarker(generate_data?.model);\n if (\n event_generation_id !== generation_id ||\n pending_prompt_captures.get(event_generation_id) !== state\n ) {\n return;\n }\n\n state.markerMatched = true;\n if (state.captureError !== undefined || state.captured) {\n return;\n }\n if (!Array.isArray(generate_data.messages)) {\n recordCaptureError(\n state,\n `Prompt capture event has no messages array: ${generation_id}`\n );\n return;\n }\n\n try {\n state.messages = structuredClone(generate_data.messages);\n } catch {\n recordCaptureError(state, `Failed to clone prompt capture messages: ${generation_id}`);\n return;\n }\n state.captured = true;\n if (options) {\n callback_started = true;\n try {\n // Settings-ready listeners are awaited by Slash. Keep generation (and its\n // native Stop button) active until the provider request has settled.\n captured_result = await options.onCaptured({\n generationId: generation_id,\n messages: state.messages,\n });\n } catch (error) {\n callback_failed = true;\n callback_error = error;\n }\n }\n // This stop only prevents the fixed capture request from reaching ST's backend.\n // It must not be mistaken for a user cancellation of the successful Pi request.\n remove_stop_listener();\n try {\n state.stopSucceeded = stopGenerationById(generation_id);\n } catch {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n return;\n }\n if (!state.stopSucceeded) {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n }\n };\n\n let subscription: { stop: () => void } | undefined;\n pending_prompt_captures.set(generation_id, state);\n\n try {\n if (options) {\n eventOn(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n subscription = eventMakeLast(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n\n let runner_failed = false;\n let runner_error: unknown;\n try {\n await run(capture_config);\n } catch (error) {\n runner_failed = true;\n runner_error = error;\n }\n\n if (state.captureError !== undefined) {\n throw new Error(state.captureError);\n }\n if (callback_failed) {\n throw callback_error;\n }\n if (runner_failed && !hasCompletedCapture(state)) {\n throw runner_error;\n }\n\n if (!hasCompletedCapture(state) || state.messages === undefined) {\n throw new Error(`Generation ended without a captured prompt: ${generation_id}`);\n }\n\n return {\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n messages: state.messages,\n ...(callback_started ? { result: captured_result } : {}),\n };\n } finally {\n remove_stop_listener();\n try {\n subscription?.stop();\n } finally {\n try {\n // Slash-Runner 4.9.1's EventOnReturn.stop() cannot reliably remove\n // its wrapped listener, so always remove the original listener too.\n eventRemoveListener(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n } finally {\n if (pending_prompt_captures.get(generation_id) === state) {\n pending_prompt_captures.delete(generation_id);\n }\n }\n }\n }\n}\n\n/** 使用当前预设的 generate 流程构造并捕获最终提示词。 */\nexport function captureGeneratePrompt(\n config: GenerateConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generate, config, options);\n}\n\n/** 使用显式顺序的 generateRaw 流程构造并捕获最终提示词。 */\nexport function captureGenerateRawPrompt(\n config: GenerateRawConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generateRaw, config, options);\n}\n","import { resolvePiThinkingLevel } from './thinking';\nimport { isPiThinkingEnabled } from './thinking_setting';\nimport {\n ANT_LING_MODELS,\n ANTHROPIC_MODELS,\n BASETEN_MODELS,\n CEREBRAS_MODELS,\n DEEPSEEK_MODELS,\n FIREWORKS_MODELS,\n GITHUB_COPILOT_MODELS,\n GOOGLE_MODELS,\n GROQ_MODELS,\n HUGGINGFACE_MODELS,\n KIMI_CODING_MODELS,\n MINIMAX_CN_MODELS,\n MINIMAX_MODELS,\n MISTRAL_MODELS,\n MOONSHOTAI_CN_MODELS,\n MOONSHOTAI_MODELS,\n NVIDIA_MODELS,\n OPENAI_CODEX_MODELS,\n OPENAI_MODELS,\n OPENCODE_GO_MODELS,\n OPENCODE_MODELS,\n OPENROUTER_MODELS,\n QWEN_TOKEN_PLAN_CN_MODELS,\n QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n QWEN_TOKEN_PLAN_MODELS,\n TOGETHER_MODELS,\n VERCEL_AI_GATEWAY_MODELS,\n XAI_MODELS,\n XIAOMI_MODELS,\n XIAOMI_TOKEN_PLAN_AMS_MODELS,\n XIAOMI_TOKEN_PLAN_CN_MODELS,\n XIAOMI_TOKEN_PLAN_SGP_MODELS,\n ZAI_CODING_CN_MODELS,\n ZAI_MODELS,\n anthropicMessagesApi,\n googleGenerativeAIApi,\n mistralConversationsApi,\n openAICodexResponsesApi,\n openAICompletionsApi,\n openAIResponsesApi,\n type Api,\n type Model,\n type ProviderStreams,\n} from './pi_gateway';\nimport { createGoogleProxyAwareApi } from './google_proxy_adapter';\nimport {\n getPiProviderApiBaseUrl,\n PI_PROVIDER_KEYS,\n PI_PROVIDER_TARGET_REGISTRY,\n type PiAuthType,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_target';\n\nexport {\n isPiCorsProxyRequired,\n PI_AUTH_TYPES,\n PI_PROVIDER_KEYS,\n PI_WIRE_APIS,\n shouldUsePiCorsProxy,\n} from './provider_target';\nexport type { PiAuthType, PiProviderKey, PiWireApi } from './provider_target';\n\n/**\n * Codex protocol level implemented by the pinned pi-ai adapter/catalog. Keep this tied to an\n * adapter upgrade rather than MVU's unrelated application version: the models endpoint uses it to\n * hide entries whose request shape the active adapter cannot yet support.\n */\nexport const OPENAI_CODEX_ADAPTER_CLIENT_VERSION = '0.144.0';\n\n/** 来源面板单个配置字段的呈现与编辑方式。 */\nexport type PiFieldMode = 'hidden' | 'readonly' | 'select' | 'editable';\n\n/** 服务商对应的面板字段规则,认证相关字段按所选认证方式显示。 */\nexport interface PiProviderFieldVisibility {\n api: PiFieldMode;\n authType: PiFieldMode;\n endpoint: PiFieldMode;\n apiKey: 'hidden' | 'when-api-key';\n oauth: 'hidden' | 'when-oauth';\n model: 'editable';\n contextWindow: 'editable';\n}\n\n/** 协议与模型组合可用的请求能力,供面板显隐和发送前参数校验共用。 */\nexport interface PiApiCapabilities {\n /** 是否支持 MVU 当前采用的 Pi 流式执行链路。 */\n streaming: boolean;\n tools: boolean;\n imageInput: boolean;\n /** 是否支持服务商原生 JSON Schema 输出约束。 */\n structuredOutput: boolean;\n /** 是否支持不指定 Schema 的原生 JSON 对象输出。 */\n jsonObjectOutput: boolean;\n temperature: boolean;\n /** temperature 可配置的闭区间,仅在支持该参数时生效。 */\n temperatureRange: readonly [minimum: number, maximum: number];\n /** 可直接传给当前协议的采样参数;未支持的配置在预检时拒绝。 */\n sampling: Readonly<{\n topP: boolean;\n topK: boolean;\n frequencyPenalty: boolean;\n presencePenalty: boolean;\n }>;\n}\n\n/** 令牌端点要求的请求正文编码:JSON 或 URL 编码表单。 */\nexport type PiOAuthExchangeKind = 'json' | 'form';\n\n/**\n * 来源面板与浏览器 OAuth 流程共享的公共客户端配置。\n * 每次登录独有的 PKCE verifier 和 state 保存在尝试状态中,不属于注册信息。\n */\nexport interface PiOAuthDefinition {\n providerId: PiProviderKey;\n api: PiWireApi;\n clientId: string;\n authorizeUrl: string;\n tokenUrl: string;\n redirectUri: string;\n /** 粘贴回调允许使用的回环主机名,端口和路径仍需与 redirectUri 匹配。 */\n allowedCallbackHosts: readonly ('localhost' | '127.0.0.1')[];\n scope: string;\n exchangeKind: PiOAuthExchangeKind;\n authorizeParams: Readonly>;\n tokenParams: Readonly>;\n /** 部分服务商要求交换授权码时同时提交本次 state。 */\n includeStateInTokenRequest: boolean;\n /** 持久化时从服务商有效期扣除的毫秒数,使刷新提前于实际过期。 */\n expirySkewMs: number;\n}\n\n/** 完整服务商注册信息,在轻量连接规则上加入能力表、界面配置和 OAuth 元数据。 */\nexport interface PiProviderDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n displayName: Readonly<{\n 'zh-CN': string;\n en: string;\n }>;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须经酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n /** 按协议登记的能力基线,查询具体模型时还会应用模型限制。 */\n apiCapabilities: Readonly>>>;\n fields: Readonly;\n oauth?: Readonly;\n}\n\n/** 按模型目录键索引的只读 Pi 元数据,不可直接修改为本次请求的配置。 */\ntype Catalog = Readonly>>;\n/** 创建协议流适配器的工厂,供 Provider 构建时选择实际协议实现。 */\ntype ApiLoader = () => ProviderStreams;\n\nconst CATALOGS: Readonly> = {\n 'ant-ling': ANT_LING_MODELS,\n anthropic: ANTHROPIC_MODELS,\n baseten: BASETEN_MODELS,\n cerebras: CEREBRAS_MODELS,\n deepseek: DEEPSEEK_MODELS,\n fireworks: FIREWORKS_MODELS,\n 'github-copilot': GITHUB_COPILOT_MODELS,\n google: GOOGLE_MODELS,\n groq: GROQ_MODELS,\n huggingface: HUGGINGFACE_MODELS,\n 'kimi-coding': KIMI_CODING_MODELS,\n minimax: MINIMAX_MODELS,\n 'minimax-cn': MINIMAX_CN_MODELS,\n mistral: MISTRAL_MODELS,\n moonshotai: MOONSHOTAI_MODELS,\n 'moonshotai-cn': MOONSHOTAI_CN_MODELS,\n nvidia: NVIDIA_MODELS,\n openai: OPENAI_MODELS,\n 'openai-codex': OPENAI_CODEX_MODELS,\n opencode: OPENCODE_MODELS,\n 'opencode-go': OPENCODE_GO_MODELS,\n openrouter: OPENROUTER_MODELS,\n 'qwen-token-plan': QWEN_TOKEN_PLAN_MODELS,\n 'qwen-token-plan-cn': QWEN_TOKEN_PLAN_CN_MODELS,\n 'qwen-token-plan-individual': QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n together: TOGETHER_MODELS,\n 'vercel-ai-gateway': VERCEL_AI_GATEWAY_MODELS,\n xai: XAI_MODELS,\n xiaomi: XIAOMI_MODELS,\n 'xiaomi-token-plan-ams': XIAOMI_TOKEN_PLAN_AMS_MODELS,\n 'xiaomi-token-plan-cn': XIAOMI_TOKEN_PLAN_CN_MODELS,\n 'xiaomi-token-plan-sgp': XIAOMI_TOKEN_PLAN_SGP_MODELS,\n zai: ZAI_MODELS,\n 'zai-coding-cn': ZAI_CODING_CN_MODELS,\n};\n\n/** Provider catalog entries that are known to have been shut down upstream. */\nconst GOOGLE_RETIRED_MODEL_IDS: ReadonlySet = new Set(['gemini-3.1-flash-lite-preview']);\n\nconst API_LOADERS: Readonly> = {\n 'openai-responses': openAIResponsesApi,\n 'openai-completions': openAICompletionsApi,\n 'openai-codex-responses': openAICodexResponsesApi,\n 'anthropic-messages': anthropicMessagesApi,\n 'google-generative-ai': () => createGoogleProxyAwareApi(googleGenerativeAIApi()),\n 'mistral-conversations': mistralConversationsApi,\n};\n\nconst TOP_P_ONLY = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TOP_P_TOP_K = Object.freeze({\n topP: true,\n topK: true,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst OPENAI_COMPLETIONS_SAMPLING = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: true,\n presencePenalty: true,\n});\n\nconst NO_SAMPLING = Object.freeze({\n topP: false,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TEMPERATURE_ZERO_TO_TWO = Object.freeze([0, 2] as const);\nconst TEMPERATURE_ZERO_TO_ONE = Object.freeze([0, 1] as const);\n\nconst OPENAI_RESPONSES_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_ONLY,\n});\n\nconst OPENAI_COMPLETIONS_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst CODEX_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: false,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: NO_SAMPLING,\n});\n\nconst ANTHROPIC_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: false,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: TOP_P_TOP_K,\n});\n\nconst GOOGLE_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_TOP_K,\n});\n\nconst MISTRAL_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst API_CAPABILITIES: Readonly>> = Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n 'openai-codex-responses': CODEX_CAPABILITIES,\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n 'mistral-conversations': MISTRAL_CAPABILITIES,\n});\n\nconst ANTHROPIC_OAUTH: Readonly = Object.freeze({\n providerId: 'anthropic',\n api: 'anthropic-messages',\n clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',\n authorizeUrl: 'https://claude.ai/oauth/authorize',\n tokenUrl: 'https://platform.claude.com/v1/oauth/token',\n redirectUri: 'http://localhost:53692/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',\n exchangeKind: 'json',\n authorizeParams: Object.freeze({ code: 'true' }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: true,\n expirySkewMs: 5 * 60 * 1000,\n});\n\nconst OPENAI_CODEX_OAUTH: Readonly = Object.freeze({\n providerId: 'openai-codex',\n api: 'openai-codex-responses',\n clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',\n authorizeUrl: 'https://auth.openai.com/oauth/authorize',\n tokenUrl: 'https://auth.openai.com/oauth/token',\n redirectUri: 'http://localhost:1455/auth/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'openid profile email offline_access',\n exchangeKind: 'form',\n authorizeParams: Object.freeze({\n id_token_add_organizations: 'true',\n codex_cli_simplified_flow: 'true',\n originator: 'pi',\n }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: false,\n expirySkewMs: 0,\n});\n\nconst EDITABLE_OPENAI_FIELDS: Readonly = Object.freeze({\n api: 'select',\n authType: 'readonly',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst OAUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'hidden',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MIXED_AUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'select',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst API_KEY_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MULTI_API_KEY_FIELDS: Readonly = Object.freeze({\n ...API_KEY_FIELDS,\n api: 'select',\n});\n\n/** 从轻量目标定义构造固定 API Key 服务商,补充展示信息、目录和协议能力。 */\nfunction fixedApiKeyProvider(\n key: PiProviderKey,\n name: string,\n chineseName = name\n): Readonly {\n const target = PI_PROVIDER_TARGET_REGISTRY[key];\n const apiCapabilities = Object.freeze(\n Object.fromEntries(target.allowedApis.map(api => [api, API_CAPABILITIES[api]]))\n ) as Readonly>>>;\n return Object.freeze({\n ...target,\n displayName: Object.freeze({ 'zh-CN': chineseName, en: name }),\n apiCapabilities,\n fields: target.allowedApis.length > 1 ? MULTI_API_KEY_FIELDS : API_KEY_FIELDS,\n });\n}\n\nexport const PI_PROVIDER_REGISTRY: Readonly> =\n Object.freeze({\n 'ant-ling': fixedApiKeyProvider('ant-ling', 'Ant Ling'),\n anthropic: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.anthropic,\n displayName: Object.freeze({ 'zh-CN': 'Anthropic', en: 'Anthropic' }),\n apiCapabilities: Object.freeze({\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n }),\n fields: MIXED_AUTH_FIELDS,\n oauth: ANTHROPIC_OAUTH,\n }),\n baseten: fixedApiKeyProvider('baseten', 'Baseten'),\n cerebras: fixedApiKeyProvider('cerebras', 'Cerebras'),\n deepseek: fixedApiKeyProvider('deepseek', 'DeepSeek'),\n fireworks: fixedApiKeyProvider('fireworks', 'Fireworks'),\n 'github-copilot': fixedApiKeyProvider('github-copilot', 'GitHub Copilot'),\n google: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.google,\n displayName: Object.freeze({ 'zh-CN': 'Google Gemini', en: 'Google Gemini' }),\n apiCapabilities: Object.freeze({\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n }),\n fields: API_KEY_FIELDS,\n }),\n groq: fixedApiKeyProvider('groq', 'Groq'),\n huggingface: fixedApiKeyProvider('huggingface', 'Hugging Face'),\n 'kimi-coding': fixedApiKeyProvider('kimi-coding', 'Kimi For Coding'),\n minimax: fixedApiKeyProvider('minimax', 'MiniMax'),\n 'minimax-cn': fixedApiKeyProvider('minimax-cn', 'MiniMax CN', 'MiniMax(中国)'),\n mistral: fixedApiKeyProvider('mistral', 'Mistral'),\n moonshotai: fixedApiKeyProvider('moonshotai', 'Moonshot AI'),\n 'moonshotai-cn': fixedApiKeyProvider(\n 'moonshotai-cn',\n 'Moonshot AI CN',\n 'Moonshot AI(中国)'\n ),\n nvidia: fixedApiKeyProvider('nvidia', 'NVIDIA'),\n openai: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.openai,\n displayName: Object.freeze({ 'zh-CN': 'OpenAI', en: 'OpenAI' }),\n apiCapabilities: Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n }),\n fields: EDITABLE_OPENAI_FIELDS,\n }),\n 'openai-codex': Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY['openai-codex'],\n displayName: Object.freeze({ 'zh-CN': 'OpenAI Codex', en: 'OpenAI Codex' }),\n apiCapabilities: Object.freeze({\n 'openai-codex-responses': CODEX_CAPABILITIES,\n }),\n fields: OAUTH_FIELDS,\n oauth: OPENAI_CODEX_OAUTH,\n }),\n opencode: fixedApiKeyProvider('opencode', 'OpenCode Zen'),\n 'opencode-go': fixedApiKeyProvider('opencode-go', 'OpenCode Go'),\n openrouter: fixedApiKeyProvider('openrouter', 'OpenRouter'),\n 'qwen-token-plan': fixedApiKeyProvider('qwen-token-plan', 'Qwen Token Plan'),\n 'qwen-token-plan-cn': fixedApiKeyProvider(\n 'qwen-token-plan-cn',\n 'Qwen Token Plan CN',\n 'Qwen Token Plan(中国)'\n ),\n 'qwen-token-plan-individual': fixedApiKeyProvider(\n 'qwen-token-plan-individual',\n 'Qwen Token Plan Individual'\n ),\n together: fixedApiKeyProvider('together', 'Together'),\n 'vercel-ai-gateway': fixedApiKeyProvider('vercel-ai-gateway', 'Vercel AI Gateway'),\n xai: fixedApiKeyProvider('xai', 'xAI'),\n xiaomi: fixedApiKeyProvider('xiaomi', 'Xiaomi'),\n 'xiaomi-token-plan-ams': fixedApiKeyProvider(\n 'xiaomi-token-plan-ams',\n 'Xiaomi Token Plan AMS'\n ),\n 'xiaomi-token-plan-cn': fixedApiKeyProvider(\n 'xiaomi-token-plan-cn',\n 'Xiaomi Token Plan CN',\n 'Xiaomi Token Plan(中国)'\n ),\n 'xiaomi-token-plan-sgp': fixedApiKeyProvider(\n 'xiaomi-token-plan-sgp',\n 'Xiaomi Token Plan SGP'\n ),\n zai: fixedApiKeyProvider('zai', 'Z.AI'),\n 'zai-coding-cn': fixedApiKeyProvider(\n 'zai-coding-cn',\n 'Z.AI Coding CN',\n 'Z.AI Coding(中国)'\n ),\n } satisfies Record);\n\n/** 按注册顺序返回支持的服务商定义,供来源界面生成选项。 */\nexport function listPiProviderDefinitions(): readonly PiProviderDefinition[] {\n return PI_PROVIDER_KEYS.map(key => PI_PROVIDER_REGISTRY[key]);\n}\n\n/** 按注册键读取服务商定义,拒绝命中对象原型上的同名属性。 */\nexport function getPiProviderDefinition(key: string): PiProviderDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_REGISTRY, key)\n ? PI_PROVIDER_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** Compatibility spelling used by the OAuth/UI integration. */\nexport const getPiProviderRegistration = getPiProviderDefinition;\n\n/** 取得服务商的固定模型目录,未知服务商返回空列表。 */\nexport function getPiCatalogModels(key: PiProviderKey | string): readonly Model[] {\n const definition = getPiProviderDefinition(key);\n if (!definition) {\n return [];\n }\n const models = Object.values(CATALOGS[definition.key]);\n return definition.key === 'google'\n ? models.filter(model => !GOOGLE_RETIRED_MODEL_IDS.has(model.id))\n : models;\n}\n\n/** 综合模型元数据和已知模型限制判断是否可发送 temperature。 */\nfunction modelSupportsTemperature(model: Model | undefined): boolean {\n const compat = model?.compat;\n return (\n compat === undefined ||\n !('supportsTemperature' in compat) ||\n compat.supportsTemperature !== false\n );\n}\n\n/**\n * Anthropic rejects every sampling control (temperature, top_p and top_k) for these catalog\n * models. Keep the explicit IDs alongside the provider registry so a missing compat flag cannot\n * accidentally make a newly published catalog entry permissive.\n */\nconst ANTHROPIC_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'claude-fable-5',\n 'claude-opus-4-7',\n 'claude-opus-4-8',\n 'claude-opus-5',\n 'claude-sonnet-5',\n]);\n\n/** Catalog models explicitly available through OpenAI Chat Completions as well as Responses. */\nconst OPENAI_CHAT_COMPLETIONS_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-4',\n 'gpt-4-turbo',\n 'gpt-4.1',\n 'gpt-4.1-mini',\n 'gpt-4.1-nano',\n 'gpt-4o',\n 'gpt-4o-2024-05-13',\n 'gpt-4o-2024-08-06',\n 'gpt-4o-2024-11-20',\n 'gpt-4o-mini',\n 'gpt-5',\n 'gpt-5-chat-latest',\n 'gpt-5-mini',\n 'gpt-5-nano',\n 'gpt-5.1',\n 'gpt-5.2',\n 'gpt-5.2-chat-latest',\n 'gpt-5.3-chat-latest',\n 'gpt-5.4',\n 'gpt-5.4-mini',\n 'gpt-5.4-nano',\n 'gpt-5.5',\n 'gpt-5.6-luna',\n 'gpt-5.6-sol',\n 'gpt-5.6-terra',\n 'o1',\n 'o3',\n 'o3-mini',\n 'o4-mini',\n]);\n\nconst OPENAI_NON_STREAMING_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-5.5-pro',\n 'gpt-realtime-2.1',\n 'o1-pro',\n 'o3-pro',\n]);\n\nconst GOOGLE_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'gemini-3.6-flash',\n 'gemini-3.7-flash',\n]);\n\n/** 验证固定目录模型是否适用于当前服务商协议,避免借用另一协议的能力元数据。 */\nexport function isPiCatalogModelApiCompatible(\n definition: PiProviderDefinition,\n model: Model,\n api: PiWireApi\n): boolean {\n if (model.api === api) {\n return true;\n }\n return (\n definition.key === 'openai' &&\n model.api === 'openai-responses' &&\n api === 'openai-completions' &&\n OPENAI_CHAT_COMPLETIONS_MODEL_IDS.has(model.id)\n );\n}\n\n/**\n * 重新查找并校验目录项,不直接信任调用方的 catalogHit 标记。\n * 只有确认匹配的元数据才能影响图片、流式和采样能力。\n */\nfunction verifiedCatalogModel(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean }\n): Model | undefined {\n const model = options.model;\n if (!options.catalogHit || !model) {\n return undefined;\n }\n\n const catalogModel = getPiCatalogModels(definition.key).find(\n candidate => candidate.id === model.id\n );\n if (\n !catalogModel ||\n catalogModel.provider !== definition.providerId ||\n model.provider !== definition.providerId ||\n !isPiCatalogModelApiCompatible(definition, catalogModel, api) ||\n (model.api !== api && model.api !== catalogModel.api) ||\n catalogModel.baseUrl !== getPiProviderApiBaseUrl(definition, api) ||\n model.baseUrl !== getPiProviderApiBaseUrl(definition, api)\n ) {\n return undefined;\n }\n\n return catalogModel;\n}\n\n/** 将服务商协议能力与可信目录模型限制合并,给出当前请求可用的能力。 */\nexport function resolvePiCapabilities(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean; thinkingLevel?: string }\n): Readonly | undefined {\n const registered = definition.apiCapabilities[api];\n if (!registered) {\n return undefined;\n }\n\n const catalogModel = verifiedCatalogModel(definition, api, options);\n const samplingModel = catalogModel ?? options.model;\n const thinkingEnabled = isPiThinkingEnabled(\n resolvePiThinkingLevel(options.thinkingLevel, catalogModel)\n );\n const anthropicSamplingAllowed =\n definition.key !== 'anthropic' ||\n catalogModel === undefined ||\n (samplingModel !== undefined &&\n modelSupportsTemperature(samplingModel) &&\n !ANTHROPIC_NO_SAMPLING_MODEL_IDS.has(samplingModel.id));\n // pi's Responses adapter selects `reasoning.effort: none` when the catalog explicitly\n // exposes it. Older GPT-5/o-series/pro models cannot turn reasoning off and reject\n // temperature/top_p. Dynamic OpenAI-compatible models remain API-level configurable.\n const openAISamplingAllowed =\n definition.key !== 'openai' ||\n !catalogModel?.reasoning ||\n catalogModel.thinkingLevelMap?.off === 'none';\n const googleSamplingAllowed =\n definition.key !== 'google' ||\n !catalogModel ||\n !GOOGLE_NO_SAMPLING_MODEL_IDS.has(catalogModel.id);\n const samplingAllowed =\n anthropicSamplingAllowed &&\n openAISamplingAllowed &&\n googleSamplingAllowed &&\n !(thinkingEnabled && (api === 'anthropic-messages' || definition.key === 'openai'));\n const streamingAllowed =\n definition.key !== 'openai' ||\n !catalogModel ||\n !OPENAI_NON_STREAMING_MODEL_IDS.has(catalogModel.id);\n\n return Object.freeze({\n ...registered,\n // These are adapter-level promises. Any narrower endpoint/model support is reported by\n // the provider request itself instead of being guessed from a pinned catalog.\n streaming: registered.streaming && streamingAllowed,\n tools: registered.tools,\n structuredOutput: registered.structuredOutput,\n jsonObjectOutput: registered.jsonObjectOutput,\n imageInput: registered.imageInput && catalogModel?.input.includes('image') === true,\n temperature:\n registered.temperature && modelSupportsTemperature(samplingModel) && samplingAllowed,\n sampling: samplingAllowed ? registered.sampling : NO_SAMPLING,\n });\n}\n\n/** 仅创建当前来源明确允许的协议懒加载适配器,避免引入无关服务商实现。 */\nexport function createPiApiImplementations(\n definitionOrKey: PiProviderDefinition | PiProviderKey | string\n): Partial> {\n const definition =\n typeof definitionOrKey === 'string'\n ? getPiProviderDefinition(definitionOrKey)\n : definitionOrKey;\n if (!definition) {\n return {};\n }\n\n return Object.fromEntries(\n definition.allowedApis.map(api => [api, API_LOADERS[api]()])\n ) as Partial>;\n}\n","export const PI_PROVIDER_KEYS = [\n 'ant-ling',\n 'anthropic',\n 'baseten',\n 'cerebras',\n 'deepseek',\n 'fireworks',\n 'github-copilot',\n 'google',\n 'groq',\n 'huggingface',\n 'kimi-coding',\n 'minimax',\n 'minimax-cn',\n 'mistral',\n 'moonshotai',\n 'moonshotai-cn',\n 'nvidia',\n 'openai',\n 'openai-codex',\n 'opencode',\n 'opencode-go',\n 'openrouter',\n 'qwen-token-plan',\n 'qwen-token-plan-cn',\n 'qwen-token-plan-individual',\n 'together',\n 'vercel-ai-gateway',\n 'xai',\n 'xiaomi',\n 'xiaomi-token-plan-ams',\n 'xiaomi-token-plan-cn',\n 'xiaomi-token-plan-sgp',\n 'zai',\n 'zai-coding-cn',\n] as const;\n/** MVU 当前注册的服务商标识,用于设置、连接规则与 Pi 模型目录之间的关联。 */\nexport type PiProviderKey = (typeof PI_PROVIDER_KEYS)[number];\n\nexport const PI_WIRE_APIS = [\n 'openai-responses',\n 'openai-completions',\n 'openai-codex-responses',\n 'anthropic-messages',\n 'google-generative-ai',\n 'mistral-conversations',\n] as const;\n/** 实际发送请求所用的协议,与服务商名称分开选择。 */\nexport type PiWireApi = (typeof PI_WIRE_APIS)[number];\n\n/** 标记必须使用流式传输的 Codex 账号接口,该接口没有 JSON 非流式模式。 */\nexport function isPiStreamingRequired(api: string): boolean {\n return api === 'openai-codex-responses';\n}\n\nexport const PI_AUTH_TYPES = ['api_key', 'oauth'] as const;\n/** Pi 连接的凭证来源:手填 API Key 或浏览器 OAuth 登录凭证。 */\nexport type PiAuthType = (typeof PI_AUTH_TYPES)[number];\n\n/** 不依赖 SDK 的连接规则,供设置迁移、凭证目标隔离和运行时校验共用。 */\nexport interface PiProviderTargetDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须通过酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n}\n\n/** 注册连接规则时允许省略代理列表,由定义函数补齐并冻结为空数组。 */\ntype PiProviderTargetDefinitionInput = Omit & {\n readonly corsProxyRequiredApis?: readonly PiWireApi[];\n};\n\nconst NO_CORS_PROXY_REQUIRED_APIS: readonly PiWireApi[] = Object.freeze([]);\n\n/** 补齐并冻结目标的代理策略,使共享注册信息不能被请求修改。 */\nfunction definePiProviderTarget(\n input: PiProviderTargetDefinitionInput\n): Readonly {\n return Object.freeze({\n ...input,\n corsProxyRequiredApis:\n input.corsProxyRequiredApis === undefined\n ? NO_CORS_PROXY_REQUIRED_APIS\n : Object.freeze([...input.corsProxyRequiredApis]),\n });\n}\n\n/**\n * Lightweight source of truth for provider/API/auth/endpoint compatibility. Keeping this separate\n * from catalogs and adapters lets settings migration validate credential targets without loading\n * the provider SDK boundary.\n */\nexport const PI_PROVIDER_TARGET_REGISTRY: Readonly<\n Record>\n> = Object.freeze({\n 'ant-ling': definePiProviderTarget({\n key: 'ant-ling',\n providerId: 'ant-ling',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.ant-ling.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n anthropic: definePiProviderTarget({\n key: 'anthropic',\n providerId: 'anthropic',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key', 'oauth'] as const),\n defaultBaseUrl: 'https://api.anthropic.com',\n allowCustomEndpoint: true,\n }),\n baseten: definePiProviderTarget({\n key: 'baseten',\n providerId: 'baseten',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://inference.baseten.co/v1',\n allowCustomEndpoint: false,\n }),\n cerebras: definePiProviderTarget({\n key: 'cerebras',\n providerId: 'cerebras',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.cerebras.ai/v1',\n allowCustomEndpoint: false,\n }),\n deepseek: definePiProviderTarget({\n key: 'deepseek',\n providerId: 'deepseek',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.deepseek.com',\n allowCustomEndpoint: false,\n }),\n fireworks: definePiProviderTarget({\n key: 'fireworks',\n providerId: 'fireworks',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.fireworks.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://api.fireworks.ai/inference',\n 'openai-completions': 'https://api.fireworks.ai/inference/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n 'github-copilot': definePiProviderTarget({\n key: 'github-copilot',\n providerId: 'github-copilot',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.individual.githubcopilot.com',\n corsProxyRequiredApis: ['anthropic-messages', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n google: definePiProviderTarget({\n key: 'google',\n providerId: 'google',\n defaultApi: 'google-generative-ai',\n allowedApis: Object.freeze(['google-generative-ai'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n allowCustomEndpoint: false,\n }),\n groq: definePiProviderTarget({\n key: 'groq',\n providerId: 'groq',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.groq.com/openai/v1',\n allowCustomEndpoint: false,\n }),\n huggingface: definePiProviderTarget({\n key: 'huggingface',\n providerId: 'huggingface',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://router.huggingface.co/v1',\n allowCustomEndpoint: false,\n }),\n 'kimi-coding': definePiProviderTarget({\n key: 'kimi-coding',\n providerId: 'kimi-coding',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.kimi.com/coding',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n minimax: definePiProviderTarget({\n key: 'minimax',\n providerId: 'minimax',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimax.io/anthropic',\n allowCustomEndpoint: false,\n }),\n 'minimax-cn': definePiProviderTarget({\n key: 'minimax-cn',\n providerId: 'minimax-cn',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimaxi.com/anthropic',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n mistral: definePiProviderTarget({\n key: 'mistral',\n providerId: 'mistral',\n defaultApi: 'mistral-conversations',\n allowedApis: Object.freeze(['mistral-conversations'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.mistral.ai',\n allowCustomEndpoint: false,\n }),\n moonshotai: definePiProviderTarget({\n key: 'moonshotai',\n providerId: 'moonshotai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'moonshotai-cn': definePiProviderTarget({\n key: 'moonshotai-cn',\n providerId: 'moonshotai-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.cn/v1',\n allowCustomEndpoint: false,\n }),\n nvidia: definePiProviderTarget({\n key: 'nvidia',\n providerId: 'nvidia',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://integrate.api.nvidia.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n openai: definePiProviderTarget({\n key: 'openai',\n providerId: 'openai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.openai.com/v1',\n allowCustomEndpoint: true,\n }),\n 'openai-codex': definePiProviderTarget({\n key: 'openai-codex',\n providerId: 'openai-codex',\n defaultApi: 'openai-codex-responses',\n allowedApis: Object.freeze(['openai-codex-responses'] as const),\n defaultAuthType: 'oauth',\n allowedAuthTypes: Object.freeze(['oauth'] as const),\n defaultBaseUrl: 'https://chatgpt.com/backend-api',\n corsProxyRequiredApis: ['openai-codex-responses'],\n allowCustomEndpoint: false,\n }),\n opencode: definePiProviderTarget({\n key: 'opencode',\n providerId: 'opencode',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/zen',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/zen',\n 'google-generative-ai': 'https://opencode.ai/zen/v1',\n 'openai-completions': 'https://opencode.ai/zen/v1',\n 'openai-responses': 'https://opencode.ai/zen/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n 'opencode-go': definePiProviderTarget({\n key: 'opencode-go',\n providerId: 'opencode-go',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/zen/go',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/zen/go',\n 'openai-completions': 'https://opencode.ai/zen/go/v1',\n 'openai-responses': 'https://opencode.ai/zen/go/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages', 'openai-completions', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n openrouter: definePiProviderTarget({\n key: 'openrouter',\n providerId: 'openrouter',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions', 'anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://openrouter.ai/api/v1',\n apiBaseUrls: Object.freeze({\n 'openai-completions': 'https://openrouter.ai/api/v1',\n 'anthropic-messages': 'https://openrouter.ai/api',\n }),\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan': definePiProviderTarget({\n key: 'qwen-token-plan',\n providerId: 'qwen-token-plan',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-cn': definePiProviderTarget({\n key: 'qwen-token-plan-cn',\n providerId: 'qwen-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-individual': definePiProviderTarget({\n key: 'qwen-token-plan-individual',\n providerId: 'qwen-token-plan-individual',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n together: definePiProviderTarget({\n key: 'together',\n providerId: 'together',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.together.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'vercel-ai-gateway': definePiProviderTarget({\n key: 'vercel-ai-gateway',\n providerId: 'vercel-ai-gateway',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://ai-gateway.vercel.sh',\n allowCustomEndpoint: false,\n }),\n xai: definePiProviderTarget({\n key: 'xai',\n providerId: 'xai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.x.ai/v1',\n allowCustomEndpoint: false,\n }),\n xiaomi: definePiProviderTarget({\n key: 'xiaomi',\n providerId: 'xiaomi',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-ams': definePiProviderTarget({\n key: 'xiaomi-token-plan-ams',\n providerId: 'xiaomi-token-plan-ams',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-ams.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-cn': definePiProviderTarget({\n key: 'xiaomi-token-plan-cn',\n providerId: 'xiaomi-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-cn.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-sgp': definePiProviderTarget({\n key: 'xiaomi-token-plan-sgp',\n providerId: 'xiaomi-token-plan-sgp',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n zai: definePiProviderTarget({\n key: 'zai',\n providerId: 'zai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.z.ai/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n 'zai-coding-cn': definePiProviderTarget({\n key: 'zai-coding-cn',\n providerId: 'zai-coding-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n});\n\n/** 读取轻量目标注册信息,供配置和密钥归属校验使用,无需加载 SDK 目录。 */\nexport function getPiProviderTargetDefinition(\n key: string\n): Readonly | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_TARGET_REGISTRY, key)\n ? PI_PROVIDER_TARGET_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** 判断指定服务商的内置协议端点是否必须经酒馆 CORS 代理访问。 */\nexport function isPiCorsProxyRequired(\n definition: Readonly | undefined,\n api: string\n): boolean {\n return definition?.corsProxyRequiredApis.includes(api as PiWireApi) ?? false;\n}\n\n/**\n * 内置端点遵循注册的代理策略,自定义端点遵循用户勾选状态。\n * 等价写法先规范化,避免把显式填写的默认地址误认作自定义目标。\n */\nexport function shouldUsePiCorsProxy(\n definition: Readonly | undefined,\n api: string,\n endpoint: string,\n customEndpointUseProxy: unknown\n): boolean {\n const explicit_endpoint = endpoint.trim();\n if (\n !definition ||\n explicit_endpoint === '' ||\n !definition.allowedApis.includes(api as PiWireApi)\n ) {\n return isPiCorsProxyRequired(definition, api);\n }\n try {\n const wire_api = api as PiWireApi;\n const configured_base = normalizePiApiBaseEndpoint(wire_api, explicit_endpoint);\n const registered_base = normalizePiApiBaseEndpoint(\n wire_api,\n getPiProviderApiBaseUrl(definition, wire_api)\n );\n if (configured_base === registered_base) {\n return isPiCorsProxyRequired(definition, api);\n }\n } catch {\n // Target validation reports malformed endpoints. Until then, keep the explicit checkbox\n // semantics so the UI does not mislabel an invalid value as a registered route.\n }\n return customEndpointUseProxy === true;\n}\n\n/** 优先读取协议专用基础地址,否则使用服务商默认地址。 */\nexport function getPiProviderApiBaseUrl(\n definition: Readonly,\n api: PiWireApi\n): string {\n return definition.apiBaseUrls?.[api] ?? definition.defaultBaseUrl;\n}\n\nexport class PiEndpointValidationError extends Error {\n /** 将端点格式或传输限制失败标记为独立的端点校验错误。 */\n constructor(message: string) {\n super(message);\n this.name = 'PiEndpointValidationError';\n }\n}\n\nconst HTTP_LOOPBACK_HOSTNAMES: ReadonlySet = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/** 规范端点地址,要求 HTTPS 或回环 HTTP,并拒绝内嵌凭证、查询参数和片段。 */\nexport function normalizePiTargetEndpoint(endpoint: string): string {\n let parsed: URL;\n try {\n parsed = new URL(endpoint.trim());\n } catch {\n throw new PiEndpointValidationError('More source endpoint must be a valid URL');\n }\n\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new PiEndpointValidationError('More source endpoint must use http or https');\n }\n if (\n parsed.protocol === 'http:' &&\n !HTTP_LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase())\n ) {\n throw new PiEndpointValidationError(\n 'More source HTTP endpoint must use localhost, 127.0.0.1, or [::1]'\n );\n }\n if (parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new PiEndpointValidationError(\n 'More source endpoint must not contain credentials, a query, or a fragment'\n );\n }\n\n const normalized_path = parsed.pathname.replace(/\\/+$/, '');\n return `${parsed.origin}${normalized_path}`;\n}\n\nconst PI_API_OPERATION_PATH_SUFFIXES: Partial> = {\n 'openai-responses': ['/responses'],\n 'openai-completions': ['/chat/completions', '/completions'],\n 'anthropic-messages': ['/v1/messages', '/messages'],\n};\n\n/**\n * 将安全端点转换为 SDK 需要的 API 基础地址。\n * 最多移除一次当前协议会自行追加的操作路径,避免重复拼接或过度裁剪。\n */\nexport function normalizePiApiBaseEndpoint(api: PiWireApi, endpoint: string): string {\n const normalized_endpoint = normalizePiTargetEndpoint(endpoint);\n const parsed = new URL(normalized_endpoint);\n const normalized_path_lower = parsed.pathname.toLowerCase();\n const operation_path = PI_API_OPERATION_PATH_SUFFIXES[api]?.find(suffix =>\n normalized_path_lower.endsWith(suffix)\n );\n if (!operation_path) {\n return normalized_endpoint;\n }\n\n const base_path = parsed.pathname.slice(0, -operation_path.length).replace(/\\/+$/, '');\n return normalizePiTargetEndpoint(`${parsed.origin}${base_path}`);\n}\n\n/**\n * 仅为完整、合法的 API Key 目标生成服务商与端点对应的缓存键。\n * 未知协议、OAuth、无效地址或不允许自定义端点时返回空值。\n */\nexport function resolvePiApiKeyScope(\n definition: Readonly | undefined,\n api: string,\n auth_type: string,\n endpoint: string\n): string {\n if (\n !definition ||\n auth_type !== 'api_key' ||\n !definition.allowedAuthTypes.includes('api_key') ||\n !definition.allowedApis.includes(api as PiWireApi) ||\n (endpoint.trim() !== '' && !definition.allowCustomEndpoint)\n ) {\n return '';\n }\n\n try {\n const effective_endpoint = normalizePiApiBaseEndpoint(\n api as PiWireApi,\n endpoint.trim() === ''\n ? getPiProviderApiBaseUrl(definition, api as PiWireApi)\n : endpoint\n );\n return `${definition.key}\\n${effective_endpoint}`;\n } catch {\n return '';\n }\n}\n","import type {\n Api,\n AssistantMessage,\n Context,\n ImageContent,\n Message,\n TextContent,\n ToolCall,\n ToolResultMessage,\n Usage,\n UserMessage,\n} from '@earendil-works/pi-ai';\n\n/** 需要按酒馆原始位置保留的系统消息,交由请求载荷 hook 在原位置恢复。 */\nexport type PiLateSystemMessage = {\n sourceIndex: number;\n /** 插在转换后第几条普通消息之前;等于 messages.length 表示位于末尾。 */\n beforeMessageIndex: number;\n text: string;\n};\n\n/** 上下文转换的内容保留及空消息诊断,不包含完整提示词。 */\nexport type PiContextAdapterDiagnostics = {\n /** 保留待处理的 system 数量;最终角色由协议适配层自动决定。 */\n preservedLateSystemCount: number;\n droppedEmptyMessageIndexes: number[];\n};\n\n/** 标准 Pi 上下文与必须一并传给载荷适配层的原生 system 消息。 */\nexport type PiContextAdapterResult = {\n context: Context;\n lateSystemMessages: readonly PiLateSystemMessage[];\n diagnostics: PiContextAdapterDiagnostics;\n};\n\n/** 提示词转换或原生 system 恢复失败的稳定分类。 */\nexport type PiContextAdapterErrorCode =\n | 'invalid-image'\n | 'invalid-tool-call'\n | 'system-payload-mismatch'\n | 'missing-tool-call'\n | 'unsupported-content';\n\nexport class PiContextAdapterError extends Error {\n /** 保留消息下标和错误类别,便于将上下文转换失败定位到原始提示词。 */\n constructor(\n message: string,\n readonly code: PiContextAdapterErrorCode,\n readonly sourceIndex: number\n ) {\n super(message);\n this.name = 'PiContextAdapterError';\n }\n}\n\n/** 酒馆捕获消息,加上历史工具结果可能携带的错误标记。 */\ntype SendingMessage = SillyTavern.SendingMessage & {\n name?: string;\n is_error?: boolean;\n isError?: boolean;\n};\n\n/** 酒馆多模态内容数组中的单个块。 */\ntype ContentBlock = NonNullable>[number];\n/** 当前转换层允许交给 Pi 的文本或图片输入。 */\ntype PiInputContent = TextContent | ImageContent;\n\nconst IMPORTED_ASSISTANT_API = 'sillytavern-import' as Api;\nconst IMPORTED_ASSISTANT_PROVIDER = 'sillytavern';\nconst IMPORTED_ASSISTANT_MODEL = 'prepared-prompt';\n\nconst SUPPORTED_IMAGE_MIME_TYPES = new Set(['image/gif', 'image/jpeg', 'image/png', 'image/webp']);\nconst MEBIBYTE = 1024 * 1024;\nconst MAX_DATA_URL_HEADER_CHARACTERS = 64;\nconst MAX_DECODED_IMAGE_HEADER_CHARACTERS = 768;\nconst MAX_ENCODED_IMAGE_HEADER_CHARACTERS = Math.ceil(MAX_DECODED_IMAGE_HEADER_CHARACTERS / 3) * 4;\n\nexport const PI_IMAGE_INPUT_LIMITS = Object.freeze({\n // Five MiB matches the strictest supported provider's common per-image cap.\n // The aggregate/count ceilings bound base64 copies on mobile browsers.\n maxDecodedBytesPerImage: 5 * MEBIBYTE,\n maxDecodedBytesPerContext: 16 * MEBIBYTE,\n maxImagesPerContext: 20,\n});\n\nconst MAX_ENCODED_CHARACTERS_PER_IMAGE =\n Math.ceil(PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / 3) * 4;\n\n/** 图片校验时提取的预算元数据,避免 token 预检重复解码图片。 */\nexport type PiImageMetadata = Readonly<{\n decodedBytes: number;\n dimensions?: Readonly<{\n width: number;\n height: number;\n }>;\n}>;\n\n/** 单次上下文转换累计使用的图片数量与解码字节数。 */\ntype ImageInputBudget = {\n decodedBytes: number;\n imageCount: number;\n};\n\nconst imageMetadata = new WeakMap();\n\n/** 读取转换时记录的图片大小和尺寸,供预算检查复用,避免再次解码。 */\nexport function getPiImageMetadata(image: ImageContent): PiImageMetadata | undefined {\n return imageMetadata.get(image);\n}\n\n/** 为导入的历史助手消息补齐零用量结构,避免把历史内容记为本次生成消耗。 */\nfunction makeZeroUsage(): Usage {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n}\n\n/** 转义消息名称中的 XML 特殊字符,防止名称破坏注入标记结构。 */\nfunction escapeXmlText(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');\n}\n\n/** 将有效的酒馆消息名称转换为可拼接的文本前缀。 */\nexport function formatSendingMessageName(name: string | undefined): string {\n if (typeof name !== 'string' || name.trim() === '') {\n return '';\n }\n return `${escapeXmlText(name)}`;\n}\n\n/** 识别仅含空白的内容,供严格校验和空消息处理使用。 */\nfunction isBlank(value: string): boolean {\n return value.trim().length === 0;\n}\n\n/** 仅接受普通对象或空原型对象,避免把类实例作为结构化消息内容。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 检查指定偏移处的文件头字节,用于验证声明的图片类型。 */\nfunction hasBytesAt(bytes: string, offset: number, expected: readonly number[]): boolean {\n return expected.every((value, index) => bytes.charCodeAt(offset + index) === value);\n}\n\n/** 核对 PNG、JPEG、GIF 或 WebP 文件签名,拒绝伪装为图片的数据。 */\nfunction hasRecognizableImageSignature(mimeType: string, bytes: string): boolean {\n if (mimeType === 'image/png') {\n return hasBytesAt(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n }\n if (mimeType === 'image/jpeg') {\n return hasBytesAt(bytes, 0, [0xff, 0xd8, 0xff]);\n }\n if (mimeType === 'image/gif') {\n return bytes.startsWith('GIF87a') || bytes.startsWith('GIF89a');\n }\n if (mimeType === 'image/webp') {\n return bytes.startsWith('RIFF') && bytes.slice(8, 12) === 'WEBP';\n }\n return false;\n}\n\n/** 判断字符是否属于标准 Base64 字母表,供图片载荷逐字符校验使用。 */\nfunction isBase64Character(code: number): boolean {\n return (\n (code >= 0x41 && code <= 0x5a) ||\n (code >= 0x61 && code <= 0x7a) ||\n (code >= 0x30 && code <= 0x39) ||\n code === 0x2b ||\n code === 0x2f\n );\n}\n\n/** 按大端读取图片头中的 16 位整数。 */\nfunction readUint16BigEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) * 0x100 + bytes.charCodeAt(offset + 1);\n}\n\n/** 按小端读取图片头中的 16 位整数。 */\nfunction readUint16LittleEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) + bytes.charCodeAt(offset + 1) * 0x100;\n}\n\n/** 按小端读取 WebP 图片头中的 24 位整数。 */\nfunction readUint24LittleEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) +\n bytes.charCodeAt(offset + 1) * 0x100 +\n bytes.charCodeAt(offset + 2) * 0x10000\n );\n}\n\n/** 按大端读取图片头中的 32 位整数,并保持无符号取值。 */\nfunction readUint32BigEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) * 0x1000000 +\n bytes.charCodeAt(offset + 1) * 0x10000 +\n bytes.charCodeAt(offset + 2) * 0x100 +\n bytes.charCodeAt(offset + 3)\n );\n}\n\nconst JPEG_START_OF_FRAME_MARKERS = new Set([\n 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,\n]);\n\n/** 扫描已解码的 JPEG 头部标记以提取尺寸;信息不足时不推测尺寸。 */\nfunction readJpegDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 4 || bytes.charCodeAt(0) !== 0xff || bytes.charCodeAt(1) !== 0xd8) {\n return undefined;\n }\n\n let offset = 2;\n while (offset + 3 < bytes.length) {\n if (bytes.charCodeAt(offset) !== 0xff) {\n offset += 1;\n continue;\n }\n while (offset < bytes.length && bytes.charCodeAt(offset) === 0xff) {\n offset += 1;\n }\n const marker = bytes.charCodeAt(offset);\n offset += 1;\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {\n continue;\n }\n if (marker === 0xd9 || marker === 0xda || offset + 1 >= bytes.length) {\n break;\n }\n const segmentLength = readUint16BigEndian(bytes, offset);\n if (segmentLength < 2 || offset + segmentLength > bytes.length) {\n break;\n }\n if (JPEG_START_OF_FRAME_MARKERS.has(marker) && segmentLength >= 7) {\n return {\n height: readUint16BigEndian(bytes, offset + 3),\n width: readUint16BigEndian(bytes, offset + 5),\n };\n }\n offset += segmentLength;\n }\n return undefined;\n}\n\n/** 按 WebP 头部编码类型提取尺寸,兼容其不同帧头布局。 */\nfunction readWebpDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 30 || !bytes.startsWith('RIFF') || bytes.slice(8, 12) !== 'WEBP') {\n return undefined;\n }\n\n const chunkType = bytes.slice(12, 16);\n if (chunkType === 'VP8X') {\n return {\n width: readUint24LittleEndian(bytes, 24) + 1,\n height: readUint24LittleEndian(bytes, 27) + 1,\n };\n }\n if (chunkType === 'VP8 ' && bytes.slice(23, 26) === '\\x9d\\x01\\x2a') {\n return {\n width: readUint16LittleEndian(bytes, 26) & 0x3fff,\n height: readUint16LittleEndian(bytes, 28) & 0x3fff,\n };\n }\n if (chunkType === 'VP8L' && bytes.charCodeAt(20) === 0x2f) {\n const b1 = bytes.charCodeAt(21);\n const b2 = bytes.charCodeAt(22);\n const b3 = bytes.charCodeAt(23);\n const b4 = bytes.charCodeAt(24);\n return {\n width: 1 + b1 + ((b2 & 0x3f) << 8),\n height: 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10),\n };\n }\n return undefined;\n}\n\n/** 根据已验证的图片类型分派尺寸读取,无法读取时返回 undefined。 */\nfunction readImageDimensions(bytes: string, mimeType: string): PiImageMetadata['dimensions'] {\n if (mimeType === 'image/png' && bytes.length >= 24) {\n return {\n width: readUint32BigEndian(bytes, 16),\n height: readUint32BigEndian(bytes, 20),\n };\n }\n if (mimeType === 'image/gif' && bytes.length >= 10) {\n return {\n width: readUint16LittleEndian(bytes, 6),\n height: readUint16LittleEndian(bytes, 8),\n };\n }\n if (mimeType === 'image/jpeg') {\n return readJpegDimensions(bytes);\n }\n if (mimeType === 'image/webp') {\n return readWebpDimensions(bytes);\n }\n return undefined;\n}\n\n/** 在受限的头部长度内查找 data URL 分隔符,避免对异常长头部做无界扫描。 */\nfunction findDataUrlSeparator(url: string): number {\n const maximumOffset = Math.min(url.length, MAX_DATA_URL_HEADER_CHARACTERS + 1);\n for (let offset = 5; offset < maximumOffset; offset++) {\n if (url.charCodeAt(offset) === 0x2c) {\n return offset;\n }\n }\n return -1;\n}\n\n/** 只解码识别图片类型和尺寸所需的有限头部,控制大图转换的内存开销。 */\nfunction decodeImageHeader(url: string, dataStart: number, encodedLength: number): string {\n const prefixLength = Math.min(encodedLength, MAX_ENCODED_IMAGE_HEADER_CHARACTERS);\n return atob(url.slice(dataStart, dataStart + prefixLength));\n}\n\n/**\n * 校验 data URL、Base64 编码、文件签名和图片预算,并记录元数据。\n * 仅接收受支持的内嵌图片,失败时携带原始消息下标,不自动下载远程地址。\n */\nfunction parseBase64Image(\n url: string,\n sourceIndex: number,\n imageBudget: ImageInputBudget\n): ImageContent {\n if (url.slice(0, 5).toLowerCase() !== 'data:') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含远程图片 URL;更多来源当前只接受 data URL 图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n\n if (url.length > MAX_DATA_URL_HEADER_CHARACTERS + 1 + MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const separator = findDataUrlSeparator(url);\n if (separator === -1) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const headerMatch = /^data:([^;,]+);base64$/i.exec(url.slice(0, separator));\n if (!headerMatch) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const [, rawMimeType] = headerMatch;\n const mimeType = rawMimeType.toLowerCase();\n if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片 MIME 类型不受支持;只接受 PNG、JPEG、GIF 或 WebP`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const dataStart = separator + 1;\n const encodedLength = url.length - dataStart;\n if (encodedLength > MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (encodedLength === 0 || encodedLength % 4 !== 0) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n let padding = 0;\n if (url.charCodeAt(url.length - 1) === 0x3d) {\n padding += 1;\n if (url.charCodeAt(url.length - 2) === 0x3d) {\n padding += 1;\n }\n }\n const decodedBytes = (encodedLength / 4) * 3 - padding;\n if (decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.imageCount + 1 > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求最多接受 ${PI_IMAGE_INPUT_LIMITS.maxImagesPerContext} 张图片`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.decodedBytes + decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求的图片总量超过 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const contentEnd = url.length - padding;\n for (let offset = dataStart; offset < contentEnd; offset++) {\n if (!isBase64Character(url.charCodeAt(offset))) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n for (let offset = contentEnd; offset < url.length; offset++) {\n if (url.charCodeAt(offset) !== 0x3d) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n\n let decodedHeader: string;\n try {\n decodedHeader = decodeImageHeader(url, dataStart, encodedLength);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无法解码的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n if (!hasRecognizableImageSignature(mimeType, decodedHeader)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片数据与声明的 MIME 类型不匹配或缺少有效文件头`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n imageBudget.imageCount += 1;\n imageBudget.decodedBytes += decodedBytes;\n\n const image: ImageContent = {\n type: 'image',\n data: url.slice(dataStart),\n mimeType,\n };\n const dimensions = readImageDimensions(decodedHeader, mimeType);\n imageMetadata.set(image, {\n decodedBytes,\n ...(dimensions === undefined ? {} : { dimensions }),\n });\n return image;\n}\n\n/** 将酒馆文本或图片块转换为 Pi 内容块,并执行对应的结构与图片校验。 */\nfunction convertContentBlock(\n block: ContentBlock,\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent {\n if (block.type === 'text') {\n return { type: 'text', text: block.text };\n }\n if (block.type === 'video_url') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含更多来源不支持的视频内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n if (block.type === 'image_url') {\n if (!allowImages) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法导入更多来源历史的图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return parseBase64Image(block.image_url.url, sourceIndex, imageBudget);\n }\n\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含未知内容块`,\n 'unsupported-content',\n sourceIndex\n );\n}\n\n/** 统一处理字符串和多块消息内容,逐块转换为 Pi 输入格式。 */\nfunction convertContent(\n content: SendingMessage['content'],\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent[] {\n if (typeof content === 'string') {\n return [{ type: 'text', text: content }];\n }\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map(block => convertContentBlock(block, sourceIndex, allowImages, imageBudget));\n}\n\n/** 判断内容块是否包含可发送的文本或图片。 */\nfunction contentHasValue(content: PiInputContent[]): boolean {\n return content.some(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 拼接文本内容块,供只允许纯文本的消息角色使用。 */\nfunction compactTextBlocks(content: PiInputContent[]): PiInputContent[] {\n return content.filter(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 提取 system 消息的纯文本,统一拒绝不支持的系统内容。 */\nfunction extractSystemText(message: SendingMessage, sourceIndex: number): string {\n const content = message.content;\n let text: string;\n if (typeof content === 'string') {\n text = content;\n } else if (Array.isArray(content)) {\n const textBlocks = content.map(block => {\n if (block.type !== 'text') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 system 消息包含非文本内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return block.text;\n });\n text = textBlocks.join('');\n } else {\n text = '';\n }\n\n const name = formatSendingMessageName(message.name);\n return [name, text].filter(value => value !== '').join('\\n');\n}\n\n/** 在消息正文前保留酒馆角色名,并避免生成空的名称前缀。 */\nfunction addNamePrefix(content: PiInputContent[], name: string | undefined): PiInputContent[] {\n const prefix = formatSendingMessageName(name);\n if (!prefix) {\n return content;\n }\n return [{ type: 'text', text: prefix }, ...content];\n}\n\n/** 将历史工具参数解析为普通对象,拒绝无效 JSON 或非对象参数。 */\nfunction parseToolArguments(argumentsValue: string, sourceIndex: number): ToolCall['arguments'] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(argumentsValue);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法解析的工具参数`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n if (!isPlainObject(parsed)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息的工具参数必须是 JSON object`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n // JSON.parse 已排除非 JSON 值,且上方校验保证参数是对象而非数组或标量。\n return parsed as ToolCall['arguments'];\n}\n\n/** 校验历史工具调用标识和参数,并转换为 Pi 工具调用块。 */\nfunction convertToolCalls(\n message: SendingMessage,\n sourceIndex: number,\n toolNamesById: Map\n): ToolCall[] {\n return (message.tool_calls ?? []).map(call => {\n if (!call.id || !call.function?.name) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无效工具调用`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n const existingName = toolNamesById.get(call.id);\n if (existingName && existingName !== call.function.name) {\n throw new PiContextAdapterError(\n `工具调用 ID ${call.id} 对应了多个工具名`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n toolNamesById.set(call.id, call.function.name);\n return {\n type: 'toolCall',\n id: call.id,\n name: call.function.name,\n arguments: parseToolArguments(call.function.arguments, sourceIndex),\n };\n });\n}\n\n/** 转换历史助手文本和工具调用,并补齐 Pi 要求的历史消息元数据。 */\nfunction convertAssistantMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): AssistantMessage {\n const textContent = compactTextBlocks(\n addNamePrefix(\n convertContent(message.content, sourceIndex, false, imageBudget),\n message.name\n )\n ) as TextContent[];\n const toolCalls = convertToolCalls(message, sourceIndex, toolNamesById);\n return {\n role: 'assistant',\n content: [...textContent, ...toolCalls],\n api: IMPORTED_ASSISTANT_API,\n provider: IMPORTED_ASSISTANT_PROVIDER,\n model: IMPORTED_ASSISTANT_MODEL,\n usage: makeZeroUsage(),\n stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',\n timestamp: now(),\n };\n}\n\n/** 将工具返回值关联到已知调用,保留工具名、错误标记及多模态结果。 */\nfunction convertToolResultMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): ToolResultMessage {\n const toolCallId = message.tool_call_id;\n if (!toolCallId) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息缺少 tool_call_id`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n const toolName = toolNamesById.get(toolCallId);\n if (!toolName) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息找不到对应的历史工具调用`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n return {\n role: 'toolResult',\n toolCallId,\n toolName,\n content: compactTextBlocks(convertContent(message.content, sourceIndex, true, imageBudget)),\n isError: message.is_error === true || message.isError === true,\n timestamp: now(),\n };\n}\n\n/** 所有来源统一丢弃空消息,并保留原始下标用于诊断。 */\nfunction recordEmptyMessage(sourceIndex: number, diagnostics: PiContextAdapterDiagnostics): void {\n diagnostics.droppedEmptyMessageIndexes.push(sourceIndex);\n}\n\n/**\n * 把酒馆最终提示词转换为 Pi 上下文,并返回转换诊断。\n * 前置 system 合并为系统提示词;后置 system 单独保留内容及原位置,由载荷 hook 恢复。\n * 同时校验工具关联和图片预算、清理空消息;渠道兼容由载荷适配层处理。\n * now 只供测试注入稳定时间,不提供渠道相关的转换策略开关。\n */\nexport function toPiContext(\n input: readonly SendingMessage[],\n now: () => number = Date.now\n): PiContextAdapterResult {\n const messages = Array.from(input);\n const diagnostics: PiContextAdapterDiagnostics = {\n preservedLateSystemCount: 0,\n droppedEmptyMessageIndexes: [],\n };\n\n let leadingSystemEnd = 0;\n while (leadingSystemEnd < messages.length && messages[leadingSystemEnd].role === 'system') {\n leadingSystemEnd++;\n }\n\n const systemPromptParts: string[] = [];\n for (let index = 0; index < leadingSystemEnd; index++) {\n const text = extractSystemText(messages[index], index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n systemPromptParts.push(text);\n }\n\n const lateSystemMessages: PiLateSystemMessage[] = [];\n const piMessages: Message[] = [];\n const toolNamesById = new Map();\n const imageBudget: ImageInputBudget = { decodedBytes: 0, imageCount: 0 };\n for (let index = leadingSystemEnd; index < messages.length; index++) {\n const message = messages[index];\n if (message.role === 'system') {\n const text = extractSystemText(message, index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n lateSystemMessages.push({\n sourceIndex: index,\n beforeMessageIndex: piMessages.length,\n text,\n });\n continue;\n }\n if (message.role === 'user') {\n const content = addNamePrefix(\n convertContent(message.content, index, true, imageBudget),\n message.name\n );\n if (!contentHasValue(content)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n const compactContent = compactTextBlocks(content);\n const userMessage: UserMessage = {\n role: 'user',\n content:\n compactContent.length === 1 && compactContent[0].type === 'text'\n ? compactContent[0].text\n : compactContent,\n timestamp: now(),\n };\n piMessages.push(userMessage);\n continue;\n }\n if (message.role === 'assistant') {\n const assistantMessage = convertAssistantMessage(\n message,\n index,\n now,\n toolNamesById,\n imageBudget\n );\n if (assistantMessage.content.length === 0) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n piMessages.push(assistantMessage);\n continue;\n }\n if (message.role === 'tool') {\n piMessages.push(\n convertToolResultMessage(message, index, now, toolNamesById, imageBudget)\n );\n continue;\n }\n throw new PiContextAdapterError(\n `第 ${index} 条消息包含未知角色`,\n 'unsupported-content',\n index\n );\n }\n diagnostics.droppedEmptyMessageIndexes.sort((left, right) => left - right);\n diagnostics.preservedLateSystemCount = lateSystemMessages.length;\n\n return {\n context: {\n ...(systemPromptParts.length > 0\n ? { systemPrompt: systemPromptParts.join('\\n\\n') }\n : {}),\n messages: piMessages,\n },\n lateSystemMessages,\n diagnostics,\n };\n}\n","import type { FetchFunction } from './pi_gateway';\nimport type { PiWireApi } from './provider_target';\n\n/** 尚待逐字段校验的协议 JSON 对象,用于非流式响应和本地合成的流事件。 */\ntype JsonObject = Record;\n\n/** 校验响应结构为非空、非数组对象。 */\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 拒绝不符合协议的非流式响应,不将可能包含提示词或凭证的响应正文写入错误。 */\nfunction invalidResponse(): never {\n // Do not include provider bodies: they can echo prompts, headers, or credentials.\n throw new Error('More source received an invalid non-streaming response');\n}\n\n/** 把单个协议事件编码为 SSE 文本,供 Pi 现有流解析器消费。 */\nfunction event(data: JsonObject): string {\n return `${typeof data.type === 'string' ? `event: ${data.type}\\n` : ''}data: ${JSON.stringify(data)}\\n\\n`;\n}\n\n/** 把 Chat Completions 完整响应转换为增量结束事件,补齐工具调用下标。 */\nfunction completionEvents(response: JsonObject): string {\n if (!Array.isArray(response.choices) || response.choices.length === 0) {\n return invalidResponse();\n }\n const choices = response.choices.map((choice: unknown, index) => {\n if (\n !isObject(choice) ||\n !isObject(choice.message) ||\n typeof choice.finish_reason !== 'string' ||\n !choice.finish_reason\n ) {\n return invalidResponse();\n }\n const delta = { ...choice.message };\n if (Array.isArray(delta.tool_calls)) {\n delta.tool_calls = delta.tool_calls.map((call: unknown, index) => {\n if (!isObject(call)) {\n return invalidResponse();\n }\n return { ...call, index };\n });\n }\n const { message: _message, ...rest } = choice;\n return { ...rest, index, delta };\n });\n return event({ ...response, object: 'chat.completion.chunk', choices }) + 'data: [DONE]\\n\\n';\n}\n\n/** 把 Responses 完整输出转换为输出项事件和终态事件,保留失败与截断状态。 */\nfunction responsesEvents(response: JsonObject): string {\n const status = response.status;\n if (!['completed', 'incomplete', 'failed', 'cancelled'].includes(String(status))) {\n return invalidResponse();\n }\n if (!Array.isArray(response.output)) {\n return invalidResponse();\n }\n return (\n response.output\n .map((item: unknown, output_index) => {\n if (!isObject(item)) {\n return invalidResponse();\n }\n return event({ type: 'response.output_item.done', output_index, item });\n })\n .join('') +\n event({\n type: `response.${status === 'cancelled' ? 'failed' : status}`,\n response,\n })\n );\n}\n\n/** 把 Anthropic 完整响应转换为消息和内容块事件,工具参数通过增量事件交给 Pi 解析。 */\nfunction anthropicEvents(response: JsonObject): string {\n if (!Array.isArray(response.content) || typeof response.stop_reason !== 'string') {\n return invalidResponse();\n }\n const usage = isObject(response.usage) ? response.usage : {};\n let result = event({\n type: 'message_start',\n message: { ...response, content: [], usage },\n });\n response.content.forEach((block: unknown, index) => {\n if (!isObject(block)) {\n return invalidResponse();\n }\n result += event({\n type: 'content_block_start',\n index,\n content_block: block.type === 'tool_use' ? { ...block, input: {} } : block,\n });\n // Pi finalizes tool arguments from the delta buffer, even when start has an input.\n if (block.type === 'tool_use') {\n result += event({\n type: 'content_block_delta',\n index,\n delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) },\n });\n }\n result += event({ type: 'content_block_stop', index });\n });\n return (\n result +\n event({\n type: 'message_delta',\n delta: {\n stop_reason: response.stop_reason,\n stop_sequence: response.stop_sequence,\n stop_details: response.stop_details,\n },\n usage,\n }) +\n event({ type: 'message_stop' })\n );\n}\n\n/** 按请求协议选择非流式响应转换器,拒绝错误对象和不支持的响应结构。 */\nfunction toEvents(api: PiWireApi, response: unknown): string {\n if (!isObject(response) || response.error) {\n return invalidResponse();\n }\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return completionEvents(response);\n case 'openai-responses':\n return responsesEvents(response);\n case 'anthropic-messages':\n return anthropicEvents(response);\n case 'google-generative-ai':\n if (!Array.isArray(response.candidates)) {\n return invalidResponse();\n }\n return event(response);\n default:\n return invalidResponse();\n }\n}\n\n/** 识别需要改写传输方式的生成接口,避免影响认证和模型列表等其他请求。 */\nfunction isGenerationUrl(api: PiWireApi, url: URL): boolean {\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return url.pathname.endsWith('/chat/completions');\n case 'openai-responses':\n return url.pathname.endsWith('/responses');\n case 'anthropic-messages':\n return url.pathname.endsWith('/messages');\n case 'google-generative-ai':\n return url.pathname.endsWith(':streamGenerateContent');\n default:\n return false;\n }\n}\n\n/**\n * 保留 Pi 的请求构造、认证和结果解析,只将支持的生成请求改为真正的非流式 HTTP。\n * 返回的 JSON 在本地转换为 Pi 可消费的事件流;Codex 的强制流式协议保持原样。\n */\nexport function createPiNonStreamingFetch(\n api: PiWireApi,\n fetch_impl?: FetchFunction\n): FetchFunction {\n return async (input, init) => {\n const send = fetch_impl ?? globalThis.fetch;\n const request = new Request(input, init);\n const url = new URL(request.url);\n if (request.method !== 'POST' || !isGenerationUrl(api, url)) {\n return send(input, init);\n }\n request.signal.throwIfAborted();\n const body: unknown = await request.json();\n if (!isObject(body)) {\n throw new Error('More source requires a JSON request body');\n }\n if (api === 'google-generative-ai') {\n url.pathname = url.pathname.replace(/:streamGenerateContent$/, ':generateContent');\n if (url.searchParams.get('alt') === 'sse') {\n url.searchParams.delete('alt');\n }\n } else {\n body.stream = false;\n delete body.stream_options;\n delete body.tool_stream;\n if (api === 'anthropic-messages' && Array.isArray(body.tools)) {\n body.tools.forEach((tool: unknown) => {\n if (isObject(tool)) {\n delete tool.eager_input_streaming;\n }\n });\n }\n }\n const headers = new Headers(request.headers);\n headers.set('accept', 'application/json');\n headers.delete('content-length');\n const response = await send(\n new Request(url, {\n ...init,\n method: request.method,\n headers,\n body: JSON.stringify(body),\n signal: request.signal,\n credentials: request.credentials,\n redirect: request.redirect,\n })\n );\n request.signal.throwIfAborted();\n // Keep provider HTTP errors intact for Pi's existing retry/error handling. Some gateways\n // ignore stream:false and still return SSE; their valid stream can be consumed as-is.\n if (!response.ok || response.headers.get('content-type')?.includes('text/event-stream')) {\n return response;\n }\n const data: unknown = await response.json();\n request.signal.throwIfAborted();\n const response_headers = new Headers(response.headers);\n response_headers.set('content-type', 'text/event-stream');\n response_headers.delete('content-length');\n response_headers.delete('content-encoding');\n return new Response(toEvents(api, data), {\n status: response.status,\n statusText: response.statusText,\n headers: response_headers,\n });\n };\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** 需要映射到服务商原生 JSON 输出配置的两种应答格式。 */\nexport type PiStructuredResponseFormat = '格式化输出' | '格式化输出(v4兼容)';\n\n/** MVU 的结构化输出 Schema,包含协议需要的名称及可选严格校验标记。 */\nexport type PiJsonSchema = {\n name: string;\n description?: string;\n value: Record;\n strict?: boolean;\n};\n\n/** 单次请求的原生载荷转换选项;消息角色由独立的 system 桥接层恢复。 */\nexport type PiPayloadTransformOptions = {\n api: Api;\n responseFormat?: PiStructuredResponseFormat;\n jsonSchema?: PiJsonSchema;\n customIncludeBody?: Record;\n customExcludeBody?: readonly string[];\n sampling?: {\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n };\n};\n\nconst PROTECTED_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'config',\n 'instructions',\n 'model',\n 'messages',\n 'input',\n 'contents',\n 'system',\n 'stream',\n 'tools',\n 'tool_choice',\n 'toolChoice',\n 'toolConfig',\n]);\n\nconst GOOGLE_PROTECTED_CONFIG_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'abortSignal',\n 'automaticFunctionCalling',\n 'httpOptions',\n 'responseJsonSchema',\n 'responseMimeType',\n 'responseSchema',\n 'systemInstruction',\n 'temperature',\n 'thinkingConfig',\n 'toolConfig',\n 'tools',\n 'topK',\n 'topP',\n]);\n\n/** 判断请求体或自定义配置是否为非空、非数组对象。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * 先删除再合并自定义字段,同时阻止修改受保护的请求字段。\n * 只浅拷贝原请求体以保留 AbortSignal 等浏览器对象,用户覆盖值单独深拷贝。\n */\nfunction applyCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n // Provider payloads may contain live browser objects (notably Google's AbortSignal).\n // A deep structuredClone turns those into plain objects and breaks cancellation. Only clone\n // the top-level payload here; user-supplied include values are cloned individually below.\n const result = { ...payload };\n for (const name of exclude) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot exclude protected field '${name}'`);\n }\n delete result[name];\n }\n for (const [name, value] of Object.entries(include)) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot override protected field '${name}'`);\n }\n result[name] = structuredClone(value);\n }\n return result;\n}\n\n/** 只接受 config.<字段> 形式的 Google 删除路径,避免歧义或删除整个生成配置。 */\nfunction googleConfigField(path: string): string {\n const match = /^config\\.([^.]+)$/.exec(path);\n if (!match) {\n throw new Error(\n `More source customExcludeBody for Google must use a direct 'config.' path, received '${path}'`\n );\n }\n return match[1];\n}\n\n/** 阻止自定义配置覆盖或删除由运行时管理的 Google 核心字段。 */\nfunction assertGoogleConfigFieldAllowed(field: string, operation: 'override' | 'exclude'): void {\n if (GOOGLE_PROTECTED_CONFIG_FIELDS.has(field)) {\n throw new Error(\n `More source custom body cannot ${operation} protected field 'config.${field}'`\n );\n }\n}\n\n/** 将 Google 自定义参数限制在 config 内,保留现有配置中的取消信号等对象。 */\nfunction applyGoogleCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n const existing_config = isPlainObject(payload.config) ? payload.config : {};\n const config = { ...existing_config };\n\n for (const path of exclude) {\n if (path === 'config' || PROTECTED_FIELDS.has(path)) {\n throw new Error(`More source custom body cannot exclude protected field '${path}'`);\n }\n const field = googleConfigField(path);\n assertGoogleConfigFieldAllowed(field, 'exclude');\n delete config[field];\n }\n\n for (const [name, value] of Object.entries(include)) {\n if (name !== 'config') {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(\n `More source custom body cannot override protected field '${name}'`\n );\n }\n throw new Error(\n `More source customIncludeBody for Google must place '${name}' inside the 'config' object`\n );\n }\n if (!isPlainObject(value)) {\n throw new Error(\n \"More source customIncludeBody for Google requires 'config' to be an object\"\n );\n }\n for (const [field, field_value] of Object.entries(value)) {\n assertGoogleConfigFieldAllowed(field, 'override');\n config[field] = structuredClone(field_value);\n }\n }\n\n return { ...payload, config };\n}\n\n/** 按协议写入原生 JSON Schema 或 JSON 对象输出配置,不支持的组合直接报错。 */\nfunction applyNativeStructuredOutput(\n payload: Record,\n api: Api,\n response_format: PiStructuredResponseFormat,\n schema: PiJsonSchema | undefined\n): Record {\n if (response_format === '格式化输出' && !schema) {\n throw new Error('More source structured output requires a JSON schema');\n }\n\n const is_json_schema = response_format === '格式化输出';\n if (api === 'openai-completions') {\n return {\n ...payload,\n response_format: is_json_schema\n ? {\n type: 'json_schema',\n json_schema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n if (api === 'openai-responses' || api === 'openai-codex-responses') {\n const existing_text = isPlainObject(payload.text) ? payload.text : {};\n return {\n ...payload,\n text: {\n ...existing_text,\n format: is_json_schema\n ? {\n type: 'json_schema',\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n }\n : { type: 'json_object' },\n },\n };\n }\n\n if (api === 'anthropic-messages') {\n if (!is_json_schema) {\n throw new Error(`More source API '${api}' does not support native JSON-object output`);\n }\n const existing_output_config = isPlainObject(payload.output_config)\n ? payload.output_config\n : {};\n return {\n ...payload,\n output_config: {\n ...existing_output_config,\n format: {\n type: 'json_schema',\n schema: schema!.value,\n },\n },\n };\n }\n\n if (api === 'google-generative-ai') {\n if (!is_json_schema) {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n },\n };\n }\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n responseJsonSchema: schema!.value,\n },\n };\n }\n\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n responseFormat: is_json_schema\n ? {\n type: 'json_schema',\n jsonSchema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schemaDefinition: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n throw new Error(`More source API '${api}' does not support native structured output`);\n}\n\n/** 将统一采样选项转换为各协议的字段名称和嵌套位置。 */\nfunction applyApiSampling(\n payload: Record,\n api: Api,\n sampling: PiPayloadTransformOptions['sampling']\n): Record {\n if (!sampling) {\n return payload;\n }\n if (api === 'openai-completions') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequency_penalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presence_penalty: sampling.presencePenalty }),\n };\n }\n if (api === 'openai-responses') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n };\n }\n if (api === 'anthropic-messages') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { top_k: sampling.topK }),\n };\n }\n if (api === 'google-generative-ai') {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { topK: sampling.topK }),\n },\n };\n }\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequencyPenalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presencePenalty: sampling.presencePenalty }),\n };\n }\n return payload;\n}\n\n/** 统一应用请求覆盖、采样和原生应答格式,并保留协议核心字段的控制权。 */\nexport function transformPiPayload(payload: unknown, options: PiPayloadTransformOptions): unknown {\n if (!isPlainObject(payload)) {\n throw new Error('More source provider payload must be an object');\n }\n let result = applyApiSampling(payload, options.api, options.sampling);\n result =\n options.api === 'google-generative-ai'\n ? applyGoogleCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n )\n : applyCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n );\n if (options.responseFormat) {\n result = applyNativeStructuredOutput(\n result,\n options.api,\n options.responseFormat,\n options.jsonSchema\n );\n }\n return result;\n}\n\n/** 将本轮转换选项绑定为 Pi 的 onPayload 回调。 */\nexport function createPiPayloadTransform(options: PiPayloadTransformOptions) {\n return (payload: unknown) => transformPiPayload(payload, options);\n}\n","import type { AssistantMessage, Tool, ToolCall } from '@earendil-works/pi-ai';\n\n/** 模型输出无法交回原有变量更新流程的原因,区分取消、截断和无效结果。 */\nexport type PiResultAdapterErrorCode =\n | 'aborted'\n | 'deferred'\n | 'empty-response'\n | 'invalid-tool-call'\n | 'length'\n | 'network'\n | 'provider-error';\n\nexport class PiResultAdapterError extends Error {\n /** 保留结果转换错误码,供运行时决定取消、重试及界面提示。 */\n constructor(\n message: string,\n readonly code: PiResultAdapterErrorCode\n ) {\n super(message);\n this.name = 'PiResultAdapterError';\n }\n}\n\n/** 将酒馆函数工具转为 Pi Tool 时附加的协议选项。 */\nexport type PiToolDefinitionOptions = {\n /** 服务商端的 Schema 或语法约束配置;false 显式关闭约束采样。 */\n constrainedSampling?: Tool['constrainedSampling'];\n};\n\n/** 仅接受普通或空原型对象,作为工具参数与 Schema 的结构边界。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 复制工具 Schema,无法复制时转换为工具定义错误,避免共享结构被修改。 */\nfunction cloneSchema(schema: Record): Record {\n try {\n return structuredClone(schema);\n } catch (cause) {\n throw new PiResultAdapterError(\n `工具参数 schema 无法复制:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n}\n\n/** 校验函数工具名称和对象 Schema,再转换为 Pi 工具定义及约束采样配置。 */\nexport function toPiToolDefinition(\n definition: ToolDefinition,\n options: PiToolDefinitionOptions = {}\n): Tool {\n if (definition?.type !== 'function' || !definition.function?.name?.trim()) {\n throw new PiResultAdapterError('工具定义缺少有效的 function name', 'invalid-tool-call');\n }\n\n const parameters = definition.function.parameters ?? {\n type: 'object',\n properties: {},\n };\n if (!isPlainObject(parameters) || parameters.type !== 'object') {\n throw new PiResultAdapterError(\n '工具参数 schema 的根节点必须是 object',\n 'invalid-tool-call'\n );\n }\n\n return {\n name: definition.function.name,\n description: definition.function.description ?? '',\n parameters: cloneSchema(parameters) as Tool['parameters'],\n ...(options.constrainedSampling === undefined\n ? {}\n : { constrainedSampling: options.constrainedSampling }),\n };\n}\n\nexport const toPiTool = toPiToolDefinition;\n\n/** 把 Pi 工具调用转换为酒馆结果结构,序列化参数并保留思考签名。 */\nfunction normalizeToolCall(call: ToolCall): GenerateToolCallResult['tool_calls'][number] {\n if (!call.id?.trim() || !call.name?.trim() || !isPlainObject(call.arguments)) {\n throw new PiResultAdapterError('更多来源返回了无效的工具调用', 'invalid-tool-call');\n }\n\n let argumentsValue: string;\n try {\n argumentsValue = JSON.stringify(call.arguments);\n } catch (cause) {\n throw new PiResultAdapterError(\n `更多来源工具参数无法序列化:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: argumentsValue,\n },\n ...(call.thoughtSignature ? { thought_signature: call.thoughtSignature } : {}),\n };\n}\n\n/** 只允许完整文本或工具调用结果继续转换,分别拒绝截断、取消、延迟和服务商失败。 */\nfunction assertSuccessfulStopReason(message: AssistantMessage): void {\n switch (message.stopReason) {\n case 'stop':\n case 'toolUse':\n return;\n case 'length':\n throw new PiResultAdapterError('更多来源回复因达到长度上限而被截断', 'length');\n case 'aborted':\n throw new PiResultAdapterError('更多来源请求已取消', 'aborted');\n case 'error': {\n // Provider errorMessage values are untrusted response data. They have been observed to\n // contain echoed Authorization headers/API keys, so retain only a coarse classification.\n const isNetworkError =\n typeof message.errorMessage === 'string' &&\n /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message.errorMessage\n );\n throw new PiResultAdapterError(\n isNetworkError\n ? 'The browser could not complete the More source request.'\n : 'More source request failed.',\n isNetworkError ? 'network' : 'provider-error'\n );\n }\n case 'pending':\n case 'deferred':\n throw new PiResultAdapterError(\n '更多来源回复尚未完成,当前不支持 deferred 结果',\n 'deferred'\n );\n default: {\n const exhaustive: never = message.stopReason;\n void exhaustive;\n throw new PiResultAdapterError(\n 'More source provider returned an unknown stop reason.',\n 'provider-error'\n );\n }\n }\n}\n\n/**\n * 提取助手文本及工具调用并返回 MVU 可消费的结果。\n * 空响应、仅有思考或工具结束却没有调用都视为协议失败,不写入聊天上下文。\n */\nexport function fromPiAssistantMessage(message: AssistantMessage): string | GenerateToolCallResult {\n assertSuccessfulStopReason(message);\n\n const text = message.content\n .filter(content => content.type === 'text')\n .map(content => content.text)\n .join('');\n const toolCalls = message.content\n .filter((content): content is ToolCall => content.type === 'toolCall')\n .map(normalizeToolCall);\n\n if (message.stopReason === 'toolUse' && toolCalls.length === 0) {\n throw new PiResultAdapterError(\n '更多来源以 toolUse 结束,但没有返回工具调用',\n 'invalid-tool-call'\n );\n }\n if (toolCalls.length > 0) {\n return {\n content: text,\n tool_calls: toolCalls,\n };\n }\n if (text.length === 0) {\n const hasThinking = message.content.some(content => content.type === 'thinking');\n throw new PiResultAdapterError(\n hasThinking ? '更多来源回复仅包含 thinking,没有业务内容' : '更多来源返回了空回复',\n 'empty-response'\n );\n }\n return text;\n}\n","import type { Context, ImageContent, Message } from '@earendil-works/pi-ai';\nimport {\n getPiImageMetadata,\n PI_IMAGE_INPUT_LIMITS,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 发送前的保守预算结果,用于解释输入是否超出模型窗口,不代表计费 token 数。 */\nexport type PiTokenPreflightResult = {\n estimatedInputTokens: number;\n /** 额外预留的安全余量,不包含回复 token。 */\n reservedTokens: number;\n /** 上下文窗口扣除回复额度及安全余量后允许的输入上限。 */\n maxInputTokens: number;\n};\n\nconst MIN_IMAGE_TOKEN_ESTIMATE = 1024;\nconst ESTIMATED_IMAGE_BYTES_PER_TOKEN = 64;\n// Common multimodal APIs estimate image tokens from tiled/pixel area. Using a\n// lower pixels-per-token ratio leaves deliberate cross-provider headroom.\nconst ESTIMATED_IMAGE_PIXELS_PER_TOKEN = 500;\n\n/**\n * 按 UTF-8 字节保守估算文本 token,兼顾中文与非基本平面字符。\n * 这是发送前的预算保护,不用于精确计费。\n */\nfunction estimateTextTokens(value: string): number {\n if (!value) {\n return 0;\n }\n // This is a deliberately conservative boundary guard, not billing-grade\n // tokenization. UTF-8 bytes avoid undercounting CJK and astral characters\n // while ASCII/JSON remains cheaper than non-ASCII text.\n let ascii_bytes = 0;\n let non_ascii_bytes = 0;\n for (const character of value) {\n const code_point = character.codePointAt(0)!;\n if (code_point <= 0x7f) {\n ascii_bytes += 1;\n } else if (code_point <= 0x7ff) {\n non_ascii_bytes += 2;\n } else if (code_point <= 0xffff) {\n non_ascii_bytes += 3;\n } else {\n non_ascii_bytes += 4;\n }\n }\n return Math.ceil(ascii_bytes / 3 + non_ascii_bytes / 2);\n}\n\n/** 根据 Base64 长度与填充估算解码字节数,避免为预算检查复制或解码整张图片。 */\nfunction estimateDecodedBase64Bytes(data: string): number {\n if (!data) {\n return 0;\n }\n // Adapter-produced blocks are canonical base64. Counting whitespace in an\n // external block overestimates safely and avoids an image-sized copy.\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding);\n}\n\n/** 优先使用上下文转换时记录的图片大小,外部内容块再按 Base64 长度估算。 */\nfunction getEstimatedImageBytes(image: ImageContent): number {\n return getPiImageMetadata(image)?.decodedBytes ?? estimateDecodedBase64Bytes(image.data);\n}\n\n/** 取图片最小预算、字节估算和像素面积估算中的最大值,保留跨服务商余量。 */\nfunction estimateImageTokens(image: ImageContent): number {\n const metadata = getPiImageMetadata(image);\n const estimated_bytes = getEstimatedImageBytes(image);\n const dimensions = metadata?.dimensions;\n\n let pixel_tokens = 0;\n if (\n dimensions !== undefined &&\n Number.isSafeInteger(dimensions.width) &&\n dimensions.width > 0 &&\n Number.isSafeInteger(dimensions.height) &&\n dimensions.height > 0\n ) {\n pixel_tokens = Math.ceil(\n (dimensions.width * dimensions.height) / ESTIMATED_IMAGE_PIXELS_PER_TOKEN\n );\n }\n\n return Math.max(\n MIN_IMAGE_TOKEN_ESTIMATE,\n Math.ceil(estimated_bytes / ESTIMATED_IMAGE_BYTES_PER_TOKEN),\n pixel_tokens\n );\n}\n\n/** 检查单图、总图片数量和累计解码大小,即使外部绕过上下文转换也执行预算限制。 */\nfunction assertPiImageInputBudget(context: Context): void {\n let image_count = 0;\n let decoded_bytes = 0;\n /** 累计单个图片块并立即检查各项图片限制,超限时停止遍历。 */\n const countImage = (image: ImageContent): void => {\n const image_bytes = getEstimatedImageBytes(image);\n if (image_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new Error('More source image exceeds the per-image input limit');\n }\n image_count += 1;\n decoded_bytes += image_bytes;\n if (image_count > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new Error('More source image count exceeds the per-context input limit');\n }\n if (decoded_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new Error('More source image total exceeds the per-context input limit');\n }\n };\n\n for (const message of context.messages) {\n if (message.role === 'user' && Array.isArray(message.content)) {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n } else if (message.role === 'toolResult') {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n }\n }\n}\n\n/** 按消息角色估算文本、思考、工具参数和图片,并计入消息结构开销。 */\nfunction estimateMessageTokens(message: Message): number {\n let tokens = 6;\n if (message.role === 'system') {\n tokens += estimateTextTokens(\n typeof message.content === 'string'\n ? message.content\n : message.content.map(block => block.text).join('')\n );\n for (const value of [message.sections, message.toolsAdded, message.toolsRemoved]) {\n if (value) tokens += estimateTextTokens(JSON.stringify(value));\n }\n return tokens;\n }\n if (message.role === 'user') {\n if (typeof message.content === 'string') {\n return tokens + estimateTextTokens(message.content);\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens;\n }\n if (message.role === 'assistant') {\n for (const block of message.content) {\n if (block.type === 'text') {\n tokens += estimateTextTokens(block.text);\n } else if (block.type === 'thinking') {\n tokens += estimateTextTokens(block.thinking);\n } else {\n tokens +=\n estimateTextTokens(block.name) +\n estimateTextTokens(JSON.stringify(block.arguments));\n }\n }\n return tokens;\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens + estimateTextTokens(message.toolName);\n}\n\n/** 汇总系统提示词、历史消息和工具定义的输入 token 估算。 */\nexport function estimatePiContextTokens(\n context: Context,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): number {\n let tokens = estimateTextTokens(context.systemPrompt ?? '');\n for (const message of context.messages) {\n tokens += estimateMessageTokens(message);\n }\n if (context.tools?.length) {\n tokens += estimateTextTokens(JSON.stringify(context.tools));\n }\n // 后置 system 尚未写入 Pi Context,但最终请求会恢复其文本和独立消息开销。\n for (const message of lateSystemMessages) {\n tokens += estimateTextTokens(message.text) + 8;\n }\n return Math.max(1, tokens);\n}\n\n/**\n * 预留回复 token 和安全余量后检查完整输入预算,不在此截断提示词。\n * 同时校验图片限制;超限错误携带估算明细,供界面解释原因。\n */\nexport function assertPiTokenBudget(\n context: Context,\n context_window: number,\n max_tokens: number,\n reserve_ratio = 0.07,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): PiTokenPreflightResult {\n if (!Number.isInteger(context_window) || context_window <= 0) {\n throw new Error('More source contextWindow must be a positive integer');\n }\n if (!Number.isInteger(max_tokens) || max_tokens <= 0) {\n throw new Error('More source maxTokens must be a positive integer');\n }\n if (max_tokens > context_window) {\n throw new Error('More source maxTokens must not exceed contextWindow');\n }\n assertPiImageInputBudget(context);\n\n const normalized_reserve_ratio = Math.min(0.1, Math.max(0.05, reserve_ratio));\n const reserved_tokens = Math.ceil(context_window * normalized_reserve_ratio);\n const max_input_tokens = context_window - max_tokens - reserved_tokens;\n const estimated_input_tokens = estimatePiContextTokens(context, lateSystemMessages);\n if (max_input_tokens <= 0 || estimated_input_tokens > max_input_tokens) {\n throw new Error(\n `More source prompt is too long: estimated ${estimated_input_tokens} input tokens, ` +\n `limit ${Math.max(0, max_input_tokens)} after reserving ${max_tokens} reply tokens ` +\n `and ${reserved_tokens} safety tokens`\n );\n }\n\n return {\n estimatedInputTokens: estimated_input_tokens,\n reservedTokens: reserved_tokens,\n maxInputTokens: max_input_tokens,\n };\n}\n","import type { Api, Context, Message, TextContent } from './pi_gateway';\nimport {\n PiContextAdapterError,\n type PiContextAdapterResult,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 一次请求的 system 恢复桥接;context 仅供 Pi 构造请求,restore 必须用于 onPayload。 */\nexport interface PiSystemMessageBridge {\n context: Context;\n restore: (payload: unknown) => unknown;\n /** Pi 会把 hook 异常转为普通服务商错误,保留原始分类供运行时取回。 */\n readonly failure: PiContextAdapterError | undefined;\n assertRestored: () => void;\n}\n\n/** 同一原始位置的一组 system,锚定到下一条实际消息,不额外伪造对话回合。 */\ntype SystemAnchor = {\n marker: string;\n messages: readonly PiLateSystemMessage[];\n};\n\n/** 对服务商 JSON 节点移除锚点后的结果;remove 仅用于锚点独占的临时文本块。 */\ntype CleanedNode = { value: unknown; anchors: SystemAnchor[]; remove?: boolean };\n\nlet nextAnchorNamespace = 0;\n\n/** 选用原始内容中不存在的请求级标记,防止用户文本被误识别为内部锚点。 */\nfunction containsMarker(value: unknown, prefix: string): boolean {\n if (typeof value === 'string') return value.includes(prefix);\n if (Array.isArray(value)) return value.some(item => containsMarker(item, prefix));\n if (value && typeof value === 'object') {\n return Object.values(value).some(item => containsMarker(item, prefix));\n }\n return false;\n}\n\n/** 在已有消息的首个文本块前加锚点;保留消息角色,避免打断 Pi 的工具调用关联。 */\nfunction anchorMessage(message: Message, marker: string, api: Api): Message {\n if (typeof message.content === 'string') {\n return { ...message, role: 'user', content: marker + message.content };\n }\n const content = [...message.content];\n const index = content.findIndex(block => block.type === 'text');\n let fallback = '';\n if (\n message.role === 'toolResult' &&\n !message.content\n .filter((block): block is TextContent => block.type === 'text')\n .map(block => block.text)\n .join('\\n')\n ) {\n const hasImages = message.content.some(block => block.type === 'image');\n // 锚点不能抑制 SDK 原本为无文本工具结果生成的占位说明。\n if (api === 'openai-completions' || api === 'mistral-conversations') {\n fallback = hasImages ? '(see attached image)' : '(no tool output)';\n } else if (!hasImages && ['openai-responses', 'openai-codex-responses'].includes(api)) {\n fallback = '(no tool output)';\n }\n }\n if (index === -1) {\n content.unshift({ type: 'text', text: marker + fallback });\n } else {\n const block = content[index] as TextContent;\n content[index] = { ...block, text: marker + block.text + fallback };\n }\n return { ...message, content } as Message;\n}\n\n/** 按实际协议和消息位置自动选择角色,不暴露额外的渠道配置。 */\nfunction systemMessageRole(\n context: Context,\n api: Api,\n message: PiLateSystemMessage\n): 'system' | 'user' {\n if (api === 'google-generative-ai') return 'user';\n if (api === 'anthropic-messages') {\n const previous = context.messages[message.beforeMessageIndex - 1];\n const next = context.messages[message.beforeMessageIndex];\n // toolResult 在 Anthropic 协议里是携带工具结果的 user 回合。\n return (previous?.role === 'user' || previous?.role === 'toolResult') &&\n (!next || next.role === 'assistant')\n ? 'system'\n : 'user';\n }\n return 'system';\n}\n\n/**\n * 保留普通消息角色,在服务商完成拆分/合并之后按内容锚点恢复 system;不符合协议约束时转为 user。\n * 锚点只进入待转换的文本,发送前必须恰好移除一次;不修改捕获输入、共享上下文或全局 fetch。\n */\nexport function createPiSystemMessageBridge(\n adapted: PiContextAdapterResult,\n api: Api\n): PiSystemMessageBridge {\n const { context, lateSystemMessages } = adapted;\n if (!lateSystemMessages.length) {\n return {\n context,\n restore: payload => payload,\n failure: undefined,\n assertRestored: () => {},\n };\n }\n\n let prefix: string;\n do {\n prefix = `__mvu_pi_system_anchor_${++nextAnchorNamespace}_`;\n } while (containsMarker(adapted, prefix));\n\n const groups = new Map();\n for (const message of lateSystemMessages) {\n const group = groups.get(message.beforeMessageIndex) ?? [];\n group.push(message);\n groups.set(message.beforeMessageIndex, group);\n }\n const anchors: SystemAnchor[] = [];\n const messages = context.messages.map((message, index) => {\n const group = groups.get(index);\n if (!group) return message;\n const anchor = { marker: `${prefix}${index}__`, messages: group };\n anchors.push(anchor);\n return anchorMessage(message, anchor.marker, api);\n });\n const tail = groups.get(context.messages.length) ?? [];\n const field =\n api === 'google-generative-ai'\n ? 'contents'\n : api === 'openai-responses' || api === 'openai-codex-responses'\n ? 'input'\n : 'messages';\n const hasUserFallback = lateSystemMessages.some(\n message => systemMessageRole(context, api, message) === 'user'\n );\n let failure: PiContextAdapterError | undefined;\n let applied = false;\n const restoredPayloads = new WeakSet();\n\n const mismatch = () =>\n new PiContextAdapterError(\n 'More source could not restore intermediate system messages in the provider payload.',\n 'system-payload-mismatch',\n lateSystemMessages[0].sourceIndex\n );\n const nativeMessages = (group: readonly PiLateSystemMessage[]) =>\n group.map(message =>\n api === 'google-generative-ai'\n ? { role: 'user', parts: [{ text: message.text }] }\n : { role: systemMessageRole(context, api, message), content: message.text }\n );\n\n /** 工具结果可能被 SDK 合并进同一 user;按工具结果块拆出插入边界,避免把指令提前到整组结果前。 */\n const splitToolResultGroups = (items: unknown[]): unknown[] =>\n items.flatMap(item => {\n if (!item || typeof item !== 'object' || !hasUserFallback) return [item];\n const message = item as Record;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const blocks = message[key];\n if (message.role !== 'user' || !Array.isArray(blocks)) return [item];\n const result: unknown[] = [];\n let current: unknown[] = [];\n for (const block of blocks) {\n const isToolResult =\n block &&\n typeof block === 'object' &&\n (block.type === 'tool_result' || block.functionResponse);\n if (isToolResult && current.length && containsMarker(block, prefix)) {\n result.push({ ...message, [key]: current });\n current = [];\n }\n current.push(block);\n }\n result.push({ ...message, [key]: current });\n return result;\n });\n\n /** 与酒馆兼容处理一致,user 回退后合并相邻 user,同时保留内容块、图片及工具结果的顺序。 */\n const mergeFallbackUsers = (items: unknown[]): unknown[] => {\n if (!hasUserFallback || (api !== 'anthropic-messages' && api !== 'google-generative-ai'))\n return items;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const asBlocks = (value: unknown) =>\n typeof value === 'string' ? [{ type: 'text', text: value }] : value;\n const merged: unknown[] = [];\n for (const item of items) {\n const current = item as Record;\n const previous = merged.at(-1) as Record | undefined;\n if (current?.role === 'user' && previous?.role === 'user') {\n const before = asBlocks(previous[key]);\n const after = asBlocks(current[key]);\n if (Array.isArray(before) && Array.isArray(after)) {\n merged[merged.length - 1] = { ...previous, [key]: [...before, ...after] };\n continue;\n }\n }\n merged.push(item);\n }\n return merged;\n };\n\n /** 只遍历原生消息字段;完整保留工具 ID、图片、缓存标记和其他服务商元数据。 */\n const clean = (value: unknown): CleanedNode => {\n if (typeof value === 'string') {\n if (!value.includes(prefix)) return { value, anchors: [] };\n const found: SystemAnchor[] = [];\n let text = value;\n for (const anchor of anchors) {\n const parts = text.split(anchor.marker);\n for (let index = 1; index < parts.length; index++) found.push(anchor);\n text = parts.join('');\n }\n return { value: text, anchors: found };\n }\n if (Array.isArray(value)) {\n const children = value.map(clean);\n return {\n value: children.filter(child => !child.remove).map(child => child.value),\n anchors: children.flatMap(child => child.anchors),\n };\n }\n if (!value || typeof value !== 'object') return { value, anchors: [] };\n const entries = Object.entries(value).map(([key, item]) => [key, clean(item)] as const);\n const found = entries.flatMap(([, child]) => child.anchors);\n if (!found.length) return { value, anchors: [] };\n const result = Object.fromEntries(entries.map(([key, child]) => [key, child.value]));\n const emptyAnchorBlock =\n result.text === '' &&\n (api === 'google-generative-ai' ||\n (typeof result.type === 'string' &&\n ['text', 'input_text', 'output_text'].includes(result.type)));\n // Responses 会把仅含工具调用的 assistant 上新增的锚点文本拆成单独消息,恢复后移除它。\n const emptyAnchorMessage =\n result.type === 'message' &&\n Array.isArray(result.content) &&\n result.content.length === 0;\n // Mistral 的纯工具助手消息省略 content,不能把临时定位块变成空内容数组。\n if (\n api === 'mistral-conversations' &&\n result.role === 'assistant' &&\n Array.isArray(result.content) &&\n !result.content.length\n ) {\n delete result.content;\n }\n return { value: result, anchors: found, remove: emptyAnchorBlock || emptyAnchorMessage };\n };\n\n return {\n context: { ...context, messages },\n get failure() {\n return failure;\n },\n restore(payload) {\n try {\n if (!payload || typeof payload !== 'object') throw mismatch();\n if (restoredPayloads.has(payload)) return payload;\n const original = payload as Record;\n const items = original[field];\n if (!Array.isArray(items)) throw mismatch();\n const seen = new Set();\n const restored: unknown[] = [];\n for (const item of splitToolResultGroups(items)) {\n const result = clean(item);\n if (result.anchors.length > 1) throw mismatch();\n const anchor = result.anchors[0];\n if (anchor) {\n if (seen.has(anchor) || anchors[seen.size] !== anchor) throw mismatch();\n seen.add(anchor);\n restored.push(...nativeMessages(anchor.messages));\n }\n if (!result.remove) restored.push(result.value);\n }\n if (seen.size !== anchors.length) throw mismatch();\n restored.push(...nativeMessages(tail));\n const result = { ...original, [field]: mergeFallbackUsers(restored) };\n if (containsMarker(result, prefix)) throw mismatch();\n restoredPayloads.add(result);\n applied = true;\n return result;\n } catch (error) {\n failure = error instanceof PiContextAdapterError ? error : mismatch();\n throw failure;\n }\n },\n assertRestored() {\n if (failure) throw failure;\n if (!applied) throw mismatch();\n },\n };\n}\n","import {\n normalizePiThinkingLevel,\n isPiThinkingEnabled,\n type PiThinkingLevel,\n} from './thinking_setting';\nimport { preparePiThinkingModel, resolvePiThinkingLevel } from './thinking';\nimport type { MvuSettings } from '@/store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n parsePiCustomExcludeBody,\n parsePiCustomHeaders,\n parsePiCustomIncludeBody,\n} from './config_parser';\nimport { PiContextAdapterError, toPiContext } from './context_adapter';\nimport { PiRequestAbortedError, registerPiRequestController } from './controller_registry';\nimport { getPiCredentialStore } from './credential_store';\nimport { assertGoogleProxyAdapterCompatible } from './google_proxy_adapter';\nimport {\n isPiDefaultProviderEndpoint,\n PiModelResolutionError,\n resolvePiModelFromExtraModelSettings,\n type ResolvedPiModel,\n} from './model_resolver';\nimport { getBrowserOAuthAuth } from './oauth';\nimport { createPiNonStreamingFetch } from './non_streaming_fetch';\nimport { isPiStreamingRequired } from './provider_target';\nimport { createPiPayloadTransform, transformPiPayload, type PiJsonSchema } from './payload';\nimport {\n createModels,\n createProvider,\n type Api,\n type ApiStreamOptions,\n type AssistantMessageEvent,\n type AuthContext,\n type CredentialStore,\n type FetchFunction,\n type SimpleStreamOptions,\n type ProviderAuth,\n type ProviderHeaders,\n type Tool,\n} from './pi_gateway';\nimport {\n createPiApiImplementations,\n resolvePiCapabilities,\n shouldUsePiCorsProxy,\n type PiApiCapabilities,\n type PiWireApi,\n} from './provider_registry';\nimport { fromPiAssistantMessage, PiResultAdapterError, toPiToolDefinition } from './result_adapter';\nimport { assertPiTokenBudget } from './token_preflight';\nimport { resolvePiToolChoice, type MvuToolChoice } from './tool_choice';\nimport { createPiSystemMessageBridge, type PiSystemMessageBridge } from './system_messages';\nimport {\n assertSillyTavernProxyAvailable,\n createSillyTavernProxyFetch,\n getSillyTavernProxyStatus,\n PiProxyUnavailableError,\n} from './sillytavern_proxy';\n\ninstallPiAbortSignalPolyfills();\n\nexport const PI_RUNTIME_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\n/** 额外解析允许使用的应答格式,供预检和原生载荷转换共同约束。 */\nexport type PiRuntimeResponseFormat = (typeof PI_RUNTIME_RESPONSE_FORMATS)[number];\n/** 一次额外模型请求读取的完整设置结构。 */\nexport type PiExtraModelSettings = MvuSettings['额外模型解析配置'];\n\n/** 运行时失败分类;配合 retryable 决定请求策略与界面提示。 */\nexport type PiRuntimeErrorCode =\n | 'invalid_configuration'\n | 'invalid_prompt'\n | 'missing_oauth_credential'\n | 'request_already_active'\n | 'unsupported_capability'\n | 'unsupported_image_input'\n | 'token_budget'\n | 'proxy_unavailable'\n | 'network'\n | 'provider'\n | 'protocol';\n\nexport class PiRuntimeError extends Error {\n /** 为运行时错误保存稳定类别和重试标记,供额外模型策略统一决策。 */\n constructor(\n readonly code: PiRuntimeErrorCode,\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'PiRuntimeError';\n }\n}\n\n/** 为串行和并发策略提供统一的不可重试错误判断。 */\nexport function isNonRetryablePiRuntimeError(error: unknown): boolean {\n return (\n (error instanceof PiRuntimeError && !error.retryable) ||\n error instanceof PiModelResolutionError ||\n error instanceof PiContextAdapterError\n );\n}\n\n/** 已通过服务商能力校验的采样选项,不含独立传入 SDK 的 temperature。 */\nexport type PiRuntimeSampling = Readonly<{\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n}>;\n\n/** 在重试批次开始前进行静态预检所需的设置、工具和可注入依赖。 */\nexport interface AssertPiRuntimeConfigurationInput {\n /** The whole persisted `额外模型解析配置` object. */\n settings: unknown;\n /** Defaults to `settings.应答格式`. */\n responseFormat?: unknown;\n /** Required by `格式化输出`; ignored by non-structured response modes. */\n jsonSchema?: PiJsonSchema;\n /** Required by `工具调用`; the MVU ToolDefinition constants can be passed directly. */\n tools?: readonly ToolDefinition[];\n /** Defaults to `required` in tool mode. */\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n signal?: AbortSignal;\n}\n\n/**\n * 重试前解析并冻结的连接、认证、能力与载荷配置。\n * 不包含提示词或请求级 system 锚点,可在同批次的每次尝试中复用。\n */\nexport interface PiRuntimePreflight {\n readonly resolution: ResolvedPiModel;\n readonly responseFormat: PiRuntimeResponseFormat;\n readonly capabilities: Readonly;\n readonly headers?: Readonly;\n readonly customIncludeBody?: Readonly>;\n readonly customExcludeBody?: readonly string[];\n readonly temperature?: number;\n readonly sampling: PiRuntimeSampling;\n readonly tools?: readonly Tool[];\n readonly toolChoice?: unknown;\n readonly jsonSchema?: PiJsonSchema;\n readonly credentialStore: CredentialStore;\n readonly fetch?: FetchFunction;\n readonly useCorsProxy: boolean;\n readonly streaming: boolean;\n readonly thinkingLevel: PiThinkingLevel;\n}\n\n/** 可选的 Pi 进度监听,运行时不会向此回调转发未经处理的错误事件。 */\nexport type PiRuntimeProgressCallback = (event: AssistantMessageEvent) => void | Promise;\n\n/** 执行单次请求的输入:预检快照、捕获消息、取消编号及可选进度监听。 */\nexport interface RunPiRequestInput {\n /** Supply this when configuration was validated once outside the retry loop. */\n preflight?: PiRuntimePreflight;\n /** Required only when `preflight` is omitted. */\n settings?: unknown;\n responseFormat?: unknown;\n jsonSchema?: PiJsonSchema;\n tools?: readonly ToolDefinition[];\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n messages: readonly SillyTavern.SendingMessage[];\n generationId: string;\n signal?: AbortSignal;\n onProgress?: PiRuntimeProgressCallback;\n}\n\n/** 设置根对象完成形状检查后、各字段尚未解析时的中间结构。 */\ntype ExtraModelSettingsRecord = Record & {\n pi?: unknown;\n};\n\nconst BROWSER_AUTH_CONTEXT: AuthContext = Object.freeze({\n env: async () => undefined,\n fileExists: async () => false,\n});\n\n/** 确认设置是非空、非数组对象,再读取请求字段。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 校验额外模型设置的根结构,结构错误在构造请求前失败。 */\nfunction requireSettings(value: unknown): ExtraModelSettingsRecord {\n if (!isPlainObject(value)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source extra-model settings must be an object'\n );\n }\n return value;\n}\n\n/** 优先采用本次显式应答格式,否则读取设置,并拒绝未知格式。 */\nfunction resolveResponseFormat(\n settings: ExtraModelSettingsRecord,\n override: unknown\n): PiRuntimeResponseFormat {\n const value = override ?? settings['应答格式'];\n if (\n typeof value !== 'string' ||\n !PI_RUNTIME_RESPONSE_FORMATS.some(candidate => candidate === value)\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source response format is missing or unsupported'\n );\n }\n return value as PiRuntimeResponseFormat;\n}\n\n/** 读取可选字符串配置;缺失返回空字符串,类型错误不能隐式转换。 */\nfunction optionalStringField(source: Record, name: string): string {\n const value = source[name];\n if (value === undefined) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new PiRuntimeError('invalid_configuration', `More source ${name} must be a string`);\n }\n return value;\n}\n\n/** 校验可选数值的范围、有限性及整数要求,缺失字段保持未设置。 */\nfunction optionalNumberField(\n source: Record,\n name: string,\n minimum: number,\n maximum: number,\n integer = false\n): number | undefined {\n const value = source[name];\n if (value === undefined) {\n return undefined;\n }\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n value < minimum ||\n value > maximum ||\n (integer && !Number.isInteger(value))\n ) {\n const kind = integer ? 'integer' : 'number';\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source ${name} must be a ${kind} between ${minimum} and ${maximum}`\n );\n }\n return value;\n}\n\n/** 仅解析当前模型支持的采样字段,并处理协议或模型特有的组合限制。 */\nfunction resolveSampling(\n settings: ExtraModelSettingsRecord,\n capabilities: Readonly,\n resolution: ResolvedPiModel\n): {\n temperature?: number;\n sampling: PiRuntimeSampling;\n} {\n let temperature = capabilities.temperature\n ? optionalNumberField(\n settings,\n '温度',\n capabilities.temperatureRange[0],\n capabilities.temperatureRange[1]\n )\n : undefined;\n let topP = capabilities.sampling.topP\n ? optionalNumberField(settings, 'top_p', 0, 1)\n : undefined;\n const topK = capabilities.sampling.topK\n ? optionalNumberField(settings, 'top_k', 0, 500, true)\n : undefined;\n const frequencyPenalty = capabilities.sampling.frequencyPenalty\n ? optionalNumberField(settings, '频率惩罚', -2, 2)\n : undefined;\n const presencePenalty = capabilities.sampling.presencePenalty\n ? optionalNumberField(settings, '存在惩罚', -2, 2)\n : undefined;\n if (\n resolution.definition.key === 'anthropic' &&\n isPiDefaultProviderEndpoint(resolution.definition, resolution.model.baseUrl) &&\n temperature !== undefined &&\n topP !== undefined\n ) {\n // Native Claude models require at most one of these samplers. MVU persists both\n // controls with 1 as their default, so omit the untouched one from the wire request.\n if (topP === 1) {\n topP = undefined;\n } else if (temperature === 1) {\n temperature = undefined;\n } else {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source Anthropic sampling requires temperature or top_p; reset one to 1.'\n );\n }\n }\n return {\n temperature,\n sampling: Object.freeze({\n ...(topP === undefined ? {} : { topP }),\n // zero is the existing \"unset\" value and is invalid for APIs that send top_k.\n ...(topK === undefined || topK === 0 ? {} : { topK }),\n ...(frequencyPenalty === undefined ? {} : { frequencyPenalty }),\n ...(presencePenalty === undefined ? {} : { presencePenalty }),\n }),\n };\n}\n\n/** 复制本次结构化输出 Schema,避免执行时修改调用方定义。 */\nfunction cloneJsonSchema(schema: PiJsonSchema | undefined): PiJsonSchema | undefined {\n return schema === undefined ? undefined : structuredClone(schema);\n}\n\n/** 解析当前模型的可信协议能力,注册信息缺失时终止配置预检。 */\nfunction capabilityFor(\n resolution: ResolvedPiModel,\n thinkingLevel: PiThinkingLevel\n): Readonly {\n const capability = resolvePiCapabilities(\n resolution.definition,\n resolution.model.api as PiWireApi,\n {\n model: resolution.model,\n catalogHit: resolution.catalogHit,\n thinkingLevel,\n }\n );\n if (!capability) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source API '${resolution.model.api}' has no registered runtime capability metadata`\n );\n }\n return capability;\n}\n\n/** 在捕获提示词前确认所选应答格式受到支持,避免重复发送必然失败的请求。 */\nfunction assertResponseCapability(\n responseFormat: PiRuntimeResponseFormat,\n capabilities: Readonly,\n api: Api\n): void {\n if (responseFormat === '工具调用' && !capabilities.tools) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support tool calls`\n );\n }\n const structuredOutputUnsupported =\n responseFormat === '格式化输出' && !capabilities.structuredOutput;\n const jsonObjectOutputUnsupported =\n responseFormat === '格式化输出(v4兼容)' && !capabilities.jsonObjectOutput;\n if (structuredOutputUnsupported || jsonObjectOutputUnsupported) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support native structured output`\n );\n }\n}\n\n/** 仅在工具调用模式校验并复制工具定义,其他模式不携带业务工具。 */\nfunction prepareTools(\n responseFormat: PiRuntimeResponseFormat,\n definitions: readonly ToolDefinition[] | undefined\n): readonly Tool[] | undefined {\n if (responseFormat !== '工具调用') {\n return undefined;\n }\n if (!definitions?.length) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source tool-call mode requires at least one tool definition'\n );\n }\n return Object.freeze(\n definitions.map(definition =>\n toPiToolDefinition(definition, {\n constrainedSampling: { type: 'json_schema', strict: 'prefer' },\n })\n )\n );\n}\n\n/** JSON Schema 模式要求显式结构定义,其他应答格式不继承残留 Schema。 */\nfunction prepareJsonSchema(\n responseFormat: PiRuntimeResponseFormat,\n schema: PiJsonSchema | undefined\n): PiJsonSchema | undefined {\n if (responseFormat === '格式化输出' && schema === undefined) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source structured output requires a JSON schema'\n );\n }\n return responseFormat === '格式化输出' ? cloneJsonSchema(schema) : undefined;\n}\n\n/** 用空载荷预演请求转换,提前发现受保护字段覆盖及格式配置错误。 */\nfunction validatePayloadConfiguration(preflight: {\n resolution: ResolvedPiModel;\n responseFormat: PiRuntimeResponseFormat;\n customIncludeBody?: Readonly>;\n customExcludeBody?: readonly string[];\n sampling: PiRuntimeSampling;\n jsonSchema?: PiJsonSchema;\n}): void {\n try {\n transformPiPayload(\n {},\n {\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n }\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source payload configuration is invalid'\n );\n }\n}\n\n/** 确认 OAuth 来源已有可用类型的凭证;到期刷新由后续 Pi 认证流程处理。 */\nasync function assertOAuthCredential(\n resolution: ResolvedPiModel,\n credentialStore: CredentialStore,\n signal?: AbortSignal\n): Promise {\n if (resolution.authType !== 'oauth') {\n return;\n }\n signal?.throwIfAborted();\n const credential = await credentialStore.read(resolution.definition.providerId, { signal });\n signal?.throwIfAborted();\n if (!credential || credential.type !== 'oauth') {\n throw new PiRuntimeError(\n 'missing_oauth_credential',\n `More source provider '${resolution.definition.providerId}' is not logged in`\n );\n }\n}\n\n/**\n * 一次性校验不依赖提示词的连接、认证、能力、预算参数和请求覆盖,并冻结本轮快照。\n * 应在串行或并发重试前调用,避免把固定配置错误误判为可重试的服务商失败。\n */\nexport async function assertPiRuntimeConfiguration(\n input: AssertPiRuntimeConfigurationInput\n): Promise {\n input.signal?.throwIfAborted();\n const settings = requireSettings(input.settings);\n const resolution = resolvePiModelFromExtraModelSettings(settings);\n const responseFormat = resolveResponseFormat(settings, input.responseFormat);\n const requestedThinkingLevel = normalizePiThinkingLevel(\n isPlainObject(settings.pi) ? settings.pi.thinkingLevel : undefined\n );\n resolution.model = preparePiThinkingModel(\n resolution.model,\n resolution.catalogHit,\n requestedThinkingLevel\n );\n // Validation, sampling and dispatch must all use the same effective level.\n const thinkingLevel = resolvePiThinkingLevel(requestedThinkingLevel, resolution.model);\n const capabilities = capabilityFor(resolution, thinkingLevel);\n const streaming =\n settings['兼容假流式'] === true || isPiStreamingRequired(resolution.model.api);\n if (streaming && !capabilities.streaming) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source model '${resolution.model.id}' does not support streaming requests`\n );\n }\n assertResponseCapability(responseFormat, capabilities, resolution.model.api);\n\n const piSettings = settings.pi;\n if (!isPlainObject(piSettings)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source connection settings must be an object'\n );\n }\n const useCorsProxy = shouldUsePiCorsProxy(\n resolution.definition,\n resolution.model.api,\n optionalStringField(piSettings, 'endpoint'),\n piSettings.useProxy\n );\n let headers: ProviderHeaders | undefined;\n let customIncludeBody: Record | undefined;\n let customExcludeBody: string[] | undefined;\n try {\n headers = parsePiCustomHeaders(optionalStringField(piSettings, 'customHeaders'));\n customIncludeBody = parsePiCustomIncludeBody(\n optionalStringField(piSettings, 'customIncludeBody')\n );\n customExcludeBody = parsePiCustomExcludeBody(\n optionalStringField(piSettings, 'customExcludeBody')\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error\n ? error.message\n : 'More source custom request configuration is invalid'\n );\n }\n const { temperature, sampling } = resolveSampling(settings, capabilities, resolution);\n let tools: readonly Tool[] | undefined;\n let toolChoice: unknown;\n try {\n tools = prepareTools(responseFormat, input.tools);\n toolChoice =\n responseFormat === '工具调用'\n ? resolvePiToolChoice(resolution.model.api, input.toolChoice ?? 'required')\n : undefined;\n } catch (error) {\n if (error instanceof PiRuntimeError) {\n throw error;\n }\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source tool configuration is invalid'\n );\n }\n if (isPiThinkingEnabled(thinkingLevel) && resolution.model.api === 'anthropic-messages') {\n if (toolChoice !== undefined && toolChoice !== 'auto' && toolChoice !== 'none') {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic thinking does not support forced tool calls. Use chat/structured output or turn thinking off.'\n );\n }\n if (\n !(\n resolution.model.compat &&\n 'forceAdaptiveThinking' in resolution.model.compat &&\n resolution.model.compat.forceAdaptiveThinking === true\n ) &&\n resolution.effectiveMaxTokens < 2048\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic budget-based thinking requires at least 2048 max reply tokens.'\n );\n }\n }\n const jsonSchema = prepareJsonSchema(responseFormat, input.jsonSchema);\n const credentialStore = input.credentialStore ?? getPiCredentialStore(piSettings);\n await assertOAuthCredential(resolution, credentialStore, input.signal);\n\n const preflight: PiRuntimePreflight = Object.freeze({\n resolution,\n responseFormat,\n capabilities,\n ...(headers === undefined ? {} : { headers: Object.freeze({ ...headers }) }),\n ...(customIncludeBody === undefined\n ? {}\n : { customIncludeBody: Object.freeze(structuredClone(customIncludeBody)) }),\n ...(customExcludeBody === undefined\n ? {}\n : { customExcludeBody: Object.freeze([...customExcludeBody]) }),\n temperature: capabilities.temperature ? temperature : undefined,\n sampling,\n tools,\n toolChoice,\n jsonSchema,\n credentialStore,\n fetch: input.fetch,\n useCorsProxy,\n streaming,\n thinkingLevel,\n });\n validatePayloadConfiguration(preflight);\n return preflight;\n}\n\n/** 只为本次解析出的模型和协议创建 Pi 服务商,并接入浏览器 OAuth 或请求密钥。 */\nfunction createRuntimeProvider(preflight: PiRuntimePreflight) {\n const { resolution } = preflight;\n let auth: ProviderAuth;\n if (resolution.authType === 'oauth') {\n auth = {\n oauth: getBrowserOAuthAuth(resolution.definition.providerId, {\n fetch: preflight.fetch,\n }),\n };\n } else {\n auth = {\n apiKey: {\n name: `${resolution.definition.displayName.en} API key`,\n /** 将本次设置提供的 API Key 转为 Pi 认证结果,解析前检查请求是否取消。 */\n async resolve({ credential, signal }) {\n signal.throwIfAborted();\n if (!credential?.key) {\n return undefined;\n }\n return {\n auth: { apiKey: credential.key },\n source: 'extra-model settings',\n };\n },\n },\n };\n }\n\n return createProvider({\n id: resolution.definition.providerId,\n name: resolution.definition.displayName.en,\n baseUrl: resolution.model.baseUrl,\n auth,\n models: [resolution.model],\n api: createPiApiImplementations(resolution.definition),\n });\n}\n\n/** 检查用户消息和工具返回值是否包含图片,供能力预检使用。 */\nfunction contextHasImages(context: ReturnType['context']): boolean {\n return context.messages.some(message => {\n if (message.role === 'user') {\n return (\n Array.isArray(message.content) &&\n message.content.some(content => content.type === 'image')\n );\n }\n if (message.role === 'toolResult') {\n return message.content.some(content => content.type === 'image');\n }\n return false;\n });\n}\n\n/** 提示词含图片时同时核对协议能力和模型输入类型,发送前拒绝不支持的组合。 */\nfunction assertImageCapability(\n preflight: PiRuntimePreflight,\n context: ReturnType['context']\n): void {\n if (!contextHasImages(context)) {\n return;\n }\n if (!preflight.capabilities.imageInput || !preflight.resolution.model.input.includes('image')) {\n throw new PiRuntimeError(\n 'unsupported_image_input',\n `More source model '${preflight.resolution.model.id}' does not support image input`\n );\n }\n}\n\n/** 仅保留需要写入原生载荷的两种格式化输出模式。 */\nfunction nativeStructuredFormat(\n responseFormat: PiRuntimeResponseFormat\n): '格式化输出' | '格式化输出(v4兼容)' | undefined {\n return responseFormat === '格式化输出' || responseFormat === '格式化输出(v4兼容)'\n ? responseFormat\n : undefined;\n}\n\n/**\n * 按本轮快照组合认证、采样、代理、非流式传输及载荷转换回调。\n * Codex 经代理时强制使用 SSE,所有请求层共享同一取消信号。\n */\nfunction createStreamOptions(\n preflight: PiRuntimePreflight,\n signal: AbortSignal,\n systemMessages: PiSystemMessageBridge\n): ApiStreamOptions {\n const providerFetch = preflight.useCorsProxy\n ? createSillyTavernProxyFetch({\n baseUrl: preflight.resolution.model.baseUrl,\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : preflight.fetch;\n return {\n signal,\n apiKey: preflight.resolution.apiKey,\n fetch: preflight.streaming\n ? providerFetch\n : createPiNonStreamingFetch(preflight.resolution.model.api as PiWireApi, providerFetch),\n headers: preflight.headers === undefined ? undefined : { ...preflight.headers },\n temperature: preflight.capabilities.temperature ? preflight.temperature : undefined,\n maxTokens: preflight.resolution.effectiveMaxTokens,\n ...(preflight.toolChoice === undefined ? {} : { toolChoice: preflight.toolChoice }),\n ...(preflight.useCorsProxy && preflight.resolution.model.api === 'openai-codex-responses'\n ? { transport: 'sse' as const }\n : {}),\n onPayload: payload =>\n systemMessages.restore(\n createPiPayloadTransform({\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n })(payload)\n ),\n };\n}\n\n/** 识别常见浏览器 fetch、网络和 CORS 失败,供运行时选择网络错误类别。 */\nfunction isNetworkFailure(error: unknown): boolean {\n const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';\n return /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message\n );\n}\n\n/** 通过稳定错误码识别 Google 注入点不兼容,避免直接导入适配器产生循环依赖。 */\nfunction isGoogleProxyAdapterCompatibilityFailure(error: unknown): boolean {\n // Keep the runtime boundary independent from the Google adapter module: provider registration\n // imports the runtime-facing gateway, so importing the adapter here would create a cycle.\n return (\n error instanceof Error &&\n (error as Error & { code?: unknown }).code === 'google_proxy_adapter_incompatible'\n );\n}\n\n/**\n * 将已知底层失败归类为运行时错误并保留重试语义。\n * 未知错误可能附带请求头或正文,只返回固定提示,不保留原始 cause。\n */\nfunction normalizeRuntimeFailure(error: unknown): Error {\n if (error instanceof PiRuntimeError || error instanceof PiContextAdapterError) {\n return error;\n }\n if (error instanceof PiProxyUnavailableError) {\n return new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n );\n }\n if (isGoogleProxyAdapterCompatibilityFailure(error)) {\n return new PiRuntimeError(\n 'protocol',\n 'The installed Google Generative AI SDK is incompatible with the browser proxy transport required by this build.'\n );\n }\n if (error instanceof PiResultAdapterError && error.code === 'aborted') {\n // The generation id is supplied by the request-level abort conversion below.\n return error;\n }\n if (\n (error instanceof PiResultAdapterError && error.code === 'network') ||\n isNetworkFailure(error)\n ) {\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n if (error instanceof PiResultAdapterError) {\n switch (error.code) {\n case 'provider-error':\n return new PiRuntimeError('provider', 'More source request failed.', true);\n case 'deferred':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned a deferred response, which is not supported.'\n );\n case 'empty-response':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned no usable response content.'\n );\n case 'invalid-tool-call':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned an invalid tool call.'\n );\n case 'length':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider response was truncated because it reached the output limit.'\n );\n case 'aborted':\n return error;\n case 'network':\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n }\n // SDK/fetch implementations may attach response bodies, headers, or request configuration to\n // arbitrary errors. Do not surface the unknown error or retain it as a cause.\n return new PiRuntimeError('provider', 'More source request failed.', true);\n}\n\n/** 要求非空生成编号,使取消登记与提示词捕获可以准确关联。 */\nfunction assertGenerationId(generationId: string): void {\n if (!generationId.trim()) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source request generationId must not be empty'\n );\n }\n}\n\n/**\n * 将捕获的酒馆提示词转换为 Pi 上下文,检查图片及 token 预算后执行所选服务商请求。\n * 统一处理代理检查、取消登记、错误分类和结果转换;返回值不自动追加到聊天或上下文。\n */\nexport async function runPiRequest(\n input: RunPiRequestInput\n): Promise {\n assertGenerationId(input.generationId);\n let preflight: PiRuntimePreflight;\n try {\n preflight =\n input.preflight ??\n (await assertPiRuntimeConfiguration({\n settings: input.settings,\n responseFormat: input.responseFormat,\n jsonSchema: input.jsonSchema,\n tools: input.tools,\n toolChoice: input.toolChoice,\n credentialStore: input.credentialStore,\n fetch: input.fetch,\n signal: input.signal,\n }));\n } catch (error) {\n if (input.signal?.aborted) {\n throw new PiRequestAbortedError(input.generationId, input.signal.reason ?? error);\n }\n throw error;\n }\n\n let registration: ReturnType;\n try {\n registration = registerPiRequestController(input.generationId, input.signal);\n } catch (error) {\n throw new PiRuntimeError(\n 'request_already_active',\n error instanceof Error ? error.message : 'More source request is already active'\n );\n }\n let systemMessages: PiSystemMessageBridge | undefined;\n try {\n if (registration.signal.aborted) {\n throw new PiRequestAbortedError(input.generationId, registration.signal.reason);\n }\n\n if (preflight.useCorsProxy) {\n try {\n await assertSillyTavernProxyAvailable({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n signal: registration.signal,\n force: true,\n });\n } catch (error) {\n throw normalizeRuntimeFailure(error);\n }\n }\n\n let adapted: ReturnType;\n try {\n adapted = toPiContext(input.messages);\n } catch (error) {\n if (error instanceof PiContextAdapterError) {\n throw new PiRuntimeError('invalid_prompt', error.message);\n }\n throw error;\n }\n const { context, lateSystemMessages } = adapted;\n if (preflight.tools !== undefined) {\n context.tools = [...preflight.tools];\n }\n assertImageCapability(preflight, context);\n systemMessages = createPiSystemMessageBridge(adapted, preflight.resolution.model.api);\n try {\n assertPiTokenBudget(\n context,\n preflight.resolution.effectiveContextWindow,\n preflight.resolution.effectiveMaxTokens,\n undefined,\n lateSystemMessages\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'token_budget',\n error instanceof Error ? error.message : 'More source token preflight failed'\n );\n }\n\n const models = createModels({\n credentials: preflight.credentialStore,\n authContext: BROWSER_AUTH_CONTEXT,\n });\n models.setProvider(createRuntimeProvider(preflight));\n const options = createStreamOptions(preflight, registration.signal, systemMessages);\n if (\n preflight.resolution.model.api === 'google-generative-ai' &&\n options.fetch &&\n options.fetch !== globalThis.fetch\n ) {\n // Pi wraps provider setup in lazyStream and would otherwise erase the compatibility\n // error code. Check the instance-only SDK seam before entering that boundary.\n assertGoogleProxyAdapterCompatible();\n }\n let stream;\n if (preflight.thinkingLevel === 'default') {\n stream = models.stream(preflight.resolution.model, systemMessages.context, options);\n } else {\n const model = {\n ...preflight.resolution.model,\n // The shared answer/thinking budget must stay inside the preflight reservation.\n maxTokens: preflight.resolution.effectiveMaxTokens,\n };\n // Pi 1.0's simple adapters forward native toolChoice unchanged, although the public\n // simple type only declares auto/none. Keep required/named choices from our resolver.\n // Real-adapter transport tests cover this compatibility boundary.\n const simpleOptions = {\n ...options,\n reasoning: preflight.thinkingLevel === 'off' ? undefined : preflight.thinkingLevel,\n } as SimpleStreamOptions;\n stream = models.streamSimple(model, systemMessages.context, simpleOptions);\n }\n\n for await (const event of stream) {\n // Provider error events can contain raw response bodies, request headers, or echoed\n // credentials. The normalized terminal error below is the public failure boundary.\n if (event.type !== 'error') {\n await input.onProgress?.(event);\n }\n }\n const message = await stream.result();\n if (registration.signal.aborted || message.stopReason === 'aborted') {\n throw new PiRequestAbortedError(\n input.generationId,\n registration.signal.aborted ? registration.signal.reason : undefined\n );\n }\n if (systemMessages.failure) throw systemMessages.failure;\n const result = fromPiAssistantMessage(message);\n systemMessages.assertRestored();\n return result;\n } catch (error) {\n const wasAborted =\n registration.signal.aborted ||\n error instanceof PiRequestAbortedError ||\n (error instanceof PiResultAdapterError && error.code === 'aborted');\n if (wasAborted) {\n throw error instanceof PiRequestAbortedError\n ? error\n : new PiRequestAbortedError(input.generationId, error);\n }\n const requestError = systemMessages?.failure ?? error;\n const proxy_status = preflight.useCorsProxy\n ? getSillyTavernProxyStatus({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : 'unchecked';\n const normalizedError =\n proxy_status === 'disabled' || proxy_status === 'unavailable'\n ? new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n )\n : normalizeRuntimeFailure(requestError);\n if (!registration.signal.aborted) {\n // Abort listeners must not observe the raw provider error either.\n registration.controller.abort(normalizedError);\n }\n throw normalizedError;\n } finally {\n registration.release();\n }\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** MVU 接受的工具选择规则;required/any 均表示必须调用工具,发送前按协议转换。 */\nexport type MvuToolChoice =\n | 'auto'\n | 'none'\n | 'required'\n | 'any'\n | { type: 'function'; function: { name: string } };\n\n/**\n * 把 MVU 工具选择转换为当前协议接受的形式。\n * 保留 auto/none,转换 required/any 与具名调用;不支持具名选择的协议提前拒绝。\n */\nexport function resolvePiToolChoice(api: Api, choice: MvuToolChoice | undefined): unknown {\n const normalized = choice ?? 'auto';\n if (normalized === 'auto' || normalized === 'none') {\n return normalized;\n }\n\n const named = typeof normalized === 'object' ? normalized.function.name.trim() : undefined;\n if (named !== undefined && !named) {\n throw new Error('More source named tool choice requires a tool name');\n }\n\n if (api === 'openai-completions') {\n return named\n ? { type: 'function', function: { name: named } }\n : normalized === 'any'\n ? 'required'\n : normalized;\n }\n if (api === 'openai-responses') {\n return named ? { type: 'function', name: named } : 'required';\n }\n if (api === 'openai-codex-responses') {\n if (named) {\n throw new Error(\n \"More source API 'openai-codex-responses' does not support a named tool choice\"\n );\n }\n return 'required';\n }\n if (api === 'anthropic-messages') {\n return named ? { type: 'tool', name: named } : 'any';\n }\n if (api === 'google-generative-ai') {\n if (named) {\n throw new Error(\n \"More source API 'google-generative-ai' does not support a named tool choice\"\n );\n }\n return 'any';\n }\n if (api === 'mistral-conversations') {\n return named ? { type: 'function', function: { name: named } } : normalized;\n }\n throw new Error(`More source API '${api}' does not support required tool choice`);\n}\n","import type { FetchFunction } from './pi_gateway';\n\nconst PROXY_DISABLED_MESSAGE =\n 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';\nconst PROXY_PROBE_BODY = 'mvu-st-cors-proxy-probe';\nconst PROXY_PROBE_TARGET = `data:text/plain,${PROXY_PROBE_BODY}`;\nconst PROXY_PROBE_TIMEOUT_MS = 5_000;\n\n/** 酒馆 CORS 代理的探测状态,区分未检查、检查中和三种已完成结果。 */\nexport type SillyTavernProxyStatus =\n | 'unchecked'\n | 'checking'\n | 'enabled'\n | 'disabled'\n | 'unavailable';\n\n/** 一次探测完成后可缓存的结果:已启用、明确关闭或当前不可用。 */\nexport type SillyTavernProxyTerminalStatus = Extract<\n SillyTavernProxyStatus,\n 'enabled' | 'disabled' | 'unavailable'\n>;\n\n/** 代理探测的环境与缓存选项,允许测试注入传输和酒馆源地址。 */\nexport interface SillyTavernProxyProbeOptions {\n /** 默认使用页面的 fetch 实现。 */\n fetch?: FetchFunction;\n /** 默认从页面基础地址或 location 解析酒馆源地址。 */\n origin?: string;\n /** 仅取消当前调用方的等待,不中止其他调用方共用的探测。 */\n signal?: AbortSignal;\n /** 忽略已缓存的完成结果,重新检查酒馆代理路由。 */\n force?: boolean;\n}\n\n/** 创建代理 fetch 所需的配置,限定携带凭证请求能够访问的目标范围。 */\nexport interface SillyTavernProxyFetchOptions extends SillyTavernProxyProbeOptions {\n /**\n * 服务商 API 基础地址;返回的 fetch 仅允许向同源、同路径或下级路径转发凭证。\n */\n baseUrl: string | URL;\n}\n\nexport class PiProxyUnavailableError extends Error {\n readonly code = 'proxy_unavailable';\n readonly retryable = false;\n\n /** 区分代理未启用和不可用,并标记为发送前应终止的配置类失败。 */\n constructor(readonly status: Exclude) {\n super(\n status === 'disabled'\n ? 'SillyTavern CORS proxy is not enabled'\n : 'SillyTavern CORS proxy is unavailable'\n );\n this.name = 'PiProxyUnavailableError';\n }\n}\n\n/** 与 Pi fetch 契约保持一致的请求目标,可为 URL、字符串或 Request。 */\ntype FetchInput = Parameters[0];\n\n/** 按 fetch 实现及酒馆源地址共享的探测记录,用 promise 合并同时发起的检查。 */\ntype ProbeEntry = {\n status: SillyTavernProxyStatus;\n promise?: Promise;\n};\n\nlet probe_cache = new WeakMap>();\n\n/** 读取注入或全局 fetch;浏览器缺少请求能力时返回 undefined。 */\nfunction resolveFetch(fetch_override?: FetchFunction): FetchFunction | undefined {\n const candidate = fetch_override ?? globalThis.fetch;\n return typeof candidate === 'function' ? candidate : undefined;\n}\n\n/** 从覆盖值或文档基础地址解析酒馆源地址,兼容 origin 为 null 的 srcdoc 脚本。 */\nfunction resolveOrigin(origin_override?: string): string | undefined {\n // Slash normally hosts scripts in srcdoc, where location.origin is \"null\" despite\n // inheriting the parent's origin. Use the same document base as relative browser fetch;\n // Slash's Blob mode also supplies a pointing to SillyTavern.\n const value = origin_override ?? globalThis.document?.baseURI ?? globalThis.location?.origin;\n if (!value) {\n return undefined;\n }\n try {\n const origin = new URL(value).origin;\n return origin === 'null' ? undefined : origin;\n } catch {\n return undefined;\n }\n}\n\n/** 按 fetch 实现与酒馆源地址隔离探测缓存,复用同一目标的进行中检查。 */\nfunction getProbeEntry(fetch_impl: FetchFunction, origin: string): ProbeEntry {\n let entries = probe_cache.get(fetch_impl);\n if (!entries) {\n entries = new Map();\n probe_cache.set(fetch_impl, entries);\n }\n let entry = entries.get(origin);\n if (!entry) {\n entry = { status: 'unchecked' };\n entries.set(origin, entry);\n }\n return entry;\n}\n\n/** 将完整目标地址编码进酒馆通用 CORS 代理路径。 */\nfunction proxyUrl(target: string): string {\n return `/proxy/${encodeURIComponent(target)}`;\n}\n\n/** 忽略首尾空白后精确核对探测正文,避免把其他页面误识别为代理结果。 */\nfunction exactResponseText(actual: string, expected: string): boolean {\n return actual.trim() === expected;\n}\n\n/** 通过本地 data URL 和超时限制探测代理,区分启用、关闭和暂时不可达。 */\nasync function performProbe(fetch_impl: FetchFunction): Promise {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROXY_PROBE_TIMEOUT_MS);\n try {\n // node-fetch resolves this data URL locally. Unlike a same-origin circular sentinel, it\n // remains reliable when SillyTavern is behind an HTTPS reverse proxy.\n const response = await fetch_impl(proxyUrl(PROXY_PROBE_TARGET), {\n method: 'GET',\n credentials: 'same-origin',\n cache: 'no-store',\n signal: controller.signal,\n });\n const body = await response.text();\n if (response.status === 200 && exactResponseText(body, PROXY_PROBE_BODY)) {\n return 'enabled';\n }\n if (response.status === 404 && exactResponseText(body, PROXY_DISABLED_MESSAGE)) {\n return 'disabled';\n }\n return 'unavailable';\n } catch {\n return 'unavailable';\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/** 保留信号原始取消原因,缺失时补齐标准 AbortError。 */\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n}\n\n/** 允许调用方取消对共享探测的等待,不中断其他调用方共用的探测请求。 */\nfunction waitForProbe(\n promise: Promise,\n signal?: AbortSignal\n): Promise {\n if (!signal) {\n return promise;\n }\n if (signal.aborted) {\n return Promise.reject(abortReason(signal));\n }\n return new Promise((resolve, reject) => {\n /** 解除当前等待者的监听并拒绝其 Promise,保留共享探测继续运行。 */\n const on_abort = () => {\n cleanup();\n reject(abortReason(signal));\n };\n /** 移除当前等待者的取消监听,避免探测结束后残留引用。 */\n const cleanup = () => signal.removeEventListener('abort', on_abort);\n signal.addEventListener('abort', on_abort, { once: true });\n promise.then(\n status => {\n cleanup();\n resolve(status);\n },\n error => {\n cleanup();\n reject(error);\n }\n );\n });\n}\n\n/** 只读取缓存中的代理状态,不触发新的网络探测。 */\nexport function getSillyTavernProxyStatus(\n options: SillyTavernProxyProbeOptions = {}\n): SillyTavernProxyStatus {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return 'unavailable';\n }\n return getProbeEntry(fetch_impl, origin).status;\n}\n\n/**\n * 合并并发探测并缓存确定的启用或关闭状态,暂时失败允许重试。\n * force 可重新核对服务端状态;单个等待者取消不影响共享探测。\n */\nexport function probeSillyTavernProxy(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return Promise.resolve('unavailable');\n }\n\n const entry = getProbeEntry(fetch_impl, origin);\n if (entry.promise) {\n return waitForProbe(entry.promise, options.signal);\n }\n if (!options.force && (entry.status === 'enabled' || entry.status === 'disabled')) {\n return waitForProbe(Promise.resolve(entry.status), options.signal);\n }\n\n entry.status = 'checking';\n entry.promise = performProbe(fetch_impl).then(status => {\n entry.status = status;\n entry.promise = undefined;\n return status;\n });\n return waitForProbe(entry.promise, options.signal);\n}\n\n/** 在发送带凭证的服务商请求前确认代理可用,不可用时立即报错。 */\nexport async function assertSillyTavernProxyAvailable(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const status = await probeSillyTavernProxy(options);\n if (status !== 'enabled') {\n throw new PiProxyUnavailableError(status);\n }\n}\n\n/** 要求不含内嵌凭证的绝对 HTTP(S) 地址,作为代理目标的第一层校验。 */\nfunction parseHttpUrl(value: string | URL, name: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL`);\n }\n if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL without credentials`);\n }\n return url;\n}\n\n/** 去除基础路径末尾斜杠,便于严格比较目标路径边界。 */\nfunction normalizeBasePath(pathname: string): string {\n return pathname === '/' ? '' : pathname.replace(/\\/+$/, '');\n}\n\n/** 限制请求必须位于配置服务商的同源基础路径内,避免凭证随代理流向其他目标。 */\nfunction assertTargetAllowed(target: URL, base: URL): void {\n const base_path = normalizeBasePath(base.pathname);\n const in_base_path =\n base_path === '' ||\n target.pathname === base_path ||\n target.pathname.startsWith(`${base_path}/`);\n if (target.origin !== base.origin || !in_base_path) {\n throw new TypeError('SillyTavern proxy target is outside the configured provider base URL');\n }\n}\n\n/** 识别 Request 输入,供后续按 fetch 语义合并 init 覆盖值。 */\nfunction requestFromInput(input: FetchInput): Request | undefined {\n return typeof Request !== 'undefined' && input instanceof Request ? input : undefined;\n}\n\n/** 统一取得字符串、URL 或 Request 输入对应的目标地址。 */\nfunction requestUrl(input: FetchInput, request: Request | undefined): string {\n return request?.url ?? input.toString();\n}\n\n/** 按 init 优先、Request 次之的顺序解析 HTTP 方法并统一大写。 */\nfunction requestMethod(request: Request | undefined, init?: RequestInit): string {\n return (init?.method ?? request?.method ?? 'GET').toUpperCase();\n}\n\n/** 遵循 fetch 的覆盖顺序解析本次请求头。 */\nfunction requestHeaders(request: Request | undefined, init?: RequestInit): HeadersInit | undefined {\n return init?.headers ?? request?.headers;\n}\n\n/** 保留 init 显式传入的取消信号或 null,否则继承 Request 的信号。 */\nfunction requestSignal(\n request: Request | undefined,\n init?: RequestInit\n): AbortSignal | null | undefined {\n if (init && 'signal' in init) {\n return init.signal;\n }\n return request?.signal;\n}\n\n/** 将可重放的请求体转换为文本,拒绝无法安全复用的流或其他载荷。 */\nasync function bodyToText(body: BodyInit): Promise {\n if (typeof body === 'string') {\n return body;\n }\n if (body instanceof URLSearchParams) {\n return body.toString();\n }\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return body.text();\n }\n if (body instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(body));\n }\n if (ArrayBuffer.isView(body)) {\n return new TextDecoder().decode(\n new Uint8Array(body.buffer, body.byteOffset, body.byteLength)\n );\n }\n throw new TypeError('SillyTavern proxy only supports replayable JSON request bodies');\n}\n\n/** 优先读取 init 请求体,必要时克隆 Request 读取,避免消费原始输入。 */\nasync function requestBodyText(\n request: Request | undefined,\n init?: RequestInit\n): Promise {\n if (init?.body !== undefined && init.body !== null) {\n return bodyToText(init.body);\n }\n if (init && 'body' in init && init.body === null) {\n return undefined;\n }\n if (request?.body !== null && request !== undefined) {\n return request.clone().text();\n }\n return undefined;\n}\n\n/** 从 Headers、键值数组或对象中按大小写不敏感名称查找请求头。 */\nfunction getHeader(headers: HeadersInit | undefined, wanted_name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const wanted = wanted_name.toLowerCase();\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(wanted_name) ?? undefined;\n }\n if (Array.isArray(headers)) {\n return headers.find(([name]) => name.toLowerCase() === wanted)?.[1];\n }\n const entry = Object.entries(headers).find(([name]) => name.toLowerCase() === wanted);\n return entry?.[1] === undefined ? undefined : String(entry[1]);\n}\n\n/** 确认带正文的代理请求使用受支持的方法、JSON 类型和合法 JSON 内容。 */\nfunction assertJsonBody(method: string, headers: HeadersInit | undefined, body?: string): void {\n if (body === undefined) {\n return;\n }\n if (!['POST', 'PUT', 'PATCH'].includes(method)) {\n throw new TypeError(`SillyTavern proxy does not forward request bodies for ${method}`);\n }\n const content_type = getHeader(headers, 'content-type') ?? '';\n if (!/^application\\/json(?:\\s*;|$)/i.test(content_type)) {\n throw new TypeError('SillyTavern proxy requires Content-Type: application/json');\n }\n try {\n JSON.parse(body);\n } catch {\n throw new TypeError('SillyTavern proxy request body must be valid JSON');\n }\n}\n\n/** 在准备和转发请求的边界检查取消状态,保留原始取消原因。 */\nfunction throwIfAborted(signal: AbortSignal | null | undefined): void {\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n }\n}\n\n/** 保留原始应答格式,仅遮盖上游可能在错误正文中回显的本次请求凭证。 */\nfunction redactProxyResponseBody(\n body: string,\n headers: HeadersInit | undefined,\n target: URL\n): string {\n const credentials = new Set();\n for (const name of ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key']) {\n const value = getHeader(headers, name)?.trim();\n if (value) {\n credentials.add(value);\n credentials.add(value.replace(/^Bearer\\s+/i, ''));\n }\n }\n for (const [name, value] of target.searchParams) {\n if (/^(?:key|api[_-]?key|(?:access[_-]?)?token)$/i.test(name) && value) {\n credentials.add(value);\n }\n }\n for (const credential of [...credentials].filter(Boolean).sort((a, b) => b.length - a.length)) {\n body = body.split(credential).join('[REDACTED]');\n }\n return body;\n}\n\n/** 异步读取克隆应答,在流结束或中断后输出 debug 日志,不占用 SDK 的正文。 */\nasync function debugProxyResponse(\n response: Response,\n target: URL,\n method: string,\n headers: HeadersInit | undefined,\n signal: AbortSignal | null | undefined\n): Promise {\n const copy = response.clone();\n const reader = copy.body?.getReader();\n let body = '';\n let complete = false;\n // 不等待 tee 分支的 cancel:其 Promise 可能要等 SDK 也结束读取才完成。\n const on_abort = () => {\n void reader?.cancel().catch(() => {});\n };\n try {\n if (reader) {\n const decoder = new TextDecoder();\n signal?.addEventListener('abort', on_abort, { once: true });\n if (signal?.aborted) {\n on_abort();\n }\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) {\n body += decoder.decode();\n complete = !signal?.aborted;\n break;\n }\n body += decoder.decode(chunk.value, { stream: true });\n }\n } else {\n body = await copy.text();\n complete = !signal?.aborted;\n }\n } catch {\n // 读取失败时仍记录已收到的部分;日志失败不能改变请求结果。\n } finally {\n signal?.removeEventListener('abort', on_abort);\n reader?.releaseLock();\n }\n console.debug(\n '[MVU Pi Proxy] raw response',\n { method, url: `${target.origin}${target.pathname}`, status: response.status, complete },\n redactProxyResponseBody(body, headers, target)\n );\n}\n\n/** 检查返回值是否为酒馆明确的代理关闭提示,不把普通上游 404 误判为代理关闭。 */\nasync function isDisabledProxyResponse(response: Response): Promise {\n if (response.status !== 404) {\n return false;\n }\n try {\n return exactResponseText(await response.clone().text(), PROXY_DISABLED_MESSAGE);\n } catch {\n return false;\n }\n}\n\n/** 将实际请求发现的代理终态写回对应探测缓存。 */\nfunction cacheTerminalStatus(\n fetch_impl: FetchFunction,\n origin: string,\n status: SillyTavernProxyTerminalStatus\n): void {\n const entry = getProbeEntry(fetch_impl, origin);\n entry.status = status;\n entry.promise = undefined;\n}\n\n/**\n * 创建受单一服务商基础地址约束的代理 fetch,转发认证头、JSON 正文和取消信号。\n * 发送前确认代理可用,目标越界或载荷不兼容时拒绝转发。\n */\nexport function createSillyTavernProxyFetch(options: SillyTavernProxyFetchOptions): FetchFunction {\n const base = parseHttpUrl(options.baseUrl, 'baseUrl');\n\n return async (input: FetchInput, init?: RequestInit): Promise => {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n throw new PiProxyUnavailableError('unavailable');\n }\n\n const request = requestFromInput(input);\n const target = parseHttpUrl(requestUrl(input, request), 'proxy target');\n assertTargetAllowed(target, base);\n\n const method = requestMethod(request, init);\n const headers = requestHeaders(request, init);\n const signal = requestSignal(request, init);\n throwIfAborted(signal);\n const body = await requestBodyText(request, init);\n assertJsonBody(method, headers, body);\n\n await assertSillyTavernProxyAvailable({\n fetch: fetch_impl,\n origin,\n ...(signal == null ? {} : { signal }),\n });\n throwIfAborted(signal);\n\n const response = await fetch_impl(proxyUrl(target.href), {\n method,\n ...(headers === undefined ? {} : { headers }),\n ...(body === undefined ? {} : { body }),\n ...(signal === undefined ? {} : { signal }),\n credentials: 'same-origin',\n });\n void debugProxyResponse(response, target, method, headers, signal).catch(() => {});\n if (await isDisabledProxyResponse(response)) {\n cacheTerminalStatus(fetch_impl, origin, 'disabled');\n throw new PiProxyUnavailableError('disabled');\n }\n return response;\n };\n}\n\n/** @internal 仅供测试清空模块级探测缓存,避免用例之间共享状态。 */\nexport function resetSillyTavernProxyStatusForTests(): void {\n probe_cache = new WeakMap();\n}\n","import { clampThinkingLevel, type Api, type Model } from './pi_gateway';\nimport { normalizePiThinkingLevel, type PiThinkingLevel } from './thinking_setting';\n\n/** 按可信目录或协议适配元数据解析生效等级;缺少元数据时保留显式选择。 */\nexport function resolvePiThinkingLevel(value: unknown, model?: Model): PiThinkingLevel {\n const level = normalizePiThinkingLevel(value);\n if (level === 'default' || level === 'off' || !model) return level;\n return model.reasoning ? clampThinkingLevel(model, level) : 'off';\n}\n\n/** 为动态模型补充显式启用信息,避免 SDK 再次把支持 effort 的协议降为 high。 */\nexport function preparePiThinkingModel(\n model: Model,\n catalogHit: boolean,\n level: PiThinkingLevel\n): Model {\n if (catalogHit || level === 'default') return model;\n const extendedEffort =\n (level === 'xhigh' || level === 'max') &&\n [\n 'openai-completions',\n 'openai-responses',\n 'openai-codex-responses',\n 'mistral-conversations',\n ].includes(model.api);\n return {\n ...model,\n reasoning: true,\n ...(extendedEffort ? { thinkingLevelMap: { xhigh: 'xhigh', max: 'max' } } : {}),\n };\n}\n","/** 默认值沿用原始协议行为;显式等级由 Pi 按模型能力映射。 */\nexport const PI_THINKING_LEVELS = [\n 'default',\n 'off',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n] as const;\nexport type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];\n\nexport function normalizePiThinkingLevel(value: unknown): PiThinkingLevel {\n return PI_THINKING_LEVELS.includes(value as PiThinkingLevel)\n ? (value as PiThinkingLevel)\n : 'default';\n}\n\nexport function isPiThinkingEnabled(value: unknown): boolean {\n const level = normalizePiThinkingLevel(value);\n return level !== 'default' && level !== 'off';\n}\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const commonMessages = defineMessages({\n 'common.enabled': {\n 'zh-CN': '启用',\n en: 'Enabled',\n },\n 'common.disabled': {\n 'zh-CN': '关闭',\n en: 'Disabled',\n },\n 'common.yes': {\n 'zh-CN': '是',\n en: 'Yes',\n },\n 'common.no': {\n 'zh-CN': '否',\n en: 'No',\n },\n 'common.confirm': {\n 'zh-CN': '确认',\n en: 'Confirm',\n },\n 'common.cancel': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const panelMessages = defineMessages({\n 'panel.title': {\n 'zh-CN': 'MVU 变量框架',\n en: 'MVU Variable Framework',\n },\n 'panel.version.section': {\n 'zh-CN': '当前版本',\n en: 'Current version',\n },\n 'panel.version.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n 'panel.notification.section': {\n 'zh-CN': '通知设置',\n en: 'Notifications',\n },\n 'panel.notification.frameworkLoaded': {\n 'zh-CN': 'MVU 框架加载成功时通知',\n en: 'Notify when the MVU framework loads successfully',\n },\n 'panel.notification.variablesInitialized': {\n 'zh-CN': '变量初始化成功时通知',\n en: 'Notify when variables are initialized successfully',\n },\n 'panel.notification.variableError': {\n 'zh-CN': '变量初始化/更新出错时通知',\n en: 'Notify when variable initialization or updates fail',\n },\n 'panel.notification.extraModelParsing': {\n 'zh-CN': '额外模型解析中通知',\n en: 'Notify while extra-model parsing is running',\n },\n 'panel.cleanup.section': {\n 'zh-CN': '自动清理变量',\n en: 'Automatic variable cleanup',\n },\n 'panel.cleanup.enable': {\n 'zh-CN': '启用自动清理变量',\n en: 'Enable automatic variable cleanup',\n },\n 'panel.cleanup.strategy': {\n 'zh-CN': '清理策略',\n en: 'Cleanup strategy',\n },\n 'panel.cleanup.snapshotInterval': {\n 'zh-CN': '快照保留间隔',\n en: 'Snapshot retention interval',\n },\n 'panel.cleanup.recentFloors': {\n 'zh-CN': '要保留变量的最近楼层数',\n en: 'Recent floors whose variables are retained',\n },\n 'panel.cleanup.restoreFloors': {\n 'zh-CN': '触发恢复变量的最近楼层数',\n en: 'Recent floors that trigger variable restoration',\n },\n 'panel.compatibility.section': {\n 'zh-CN': '兼容性',\n en: 'Compatibility',\n },\n 'panel.compatibility.updateChatVariables': {\n 'zh-CN': '变量更新到聊天变量',\n en: 'Write variable updates to chat variables',\n },\n 'panel.compatibility.updateChatVariablesHelp': {\n 'zh-CN':\n '启用后,所有变量更新结果也会输出到聊天变量中。如果部分老角色卡无法正常游玩,可以开启这个开关。',\n en: 'When enabled, every variable update is also written to chat variables. Enable this if some older character cards do not work correctly.',\n },\n 'panel.compatibility.showLegacy': {\n 'zh-CN': '显示老旧功能',\n en: 'Show legacy features',\n },\n 'panel.compatibility.sendasNotUser': {\n 'zh-CN': 'sendas 不视为 user 消息',\n en: 'Do not treat sendas as a user message',\n },\n 'panel.compatibility.nonblockingExtraAnalysis': {\n 'zh-CN': '额外模型解析不阻塞正文显示(实验性)',\n en: 'Show replies without waiting for extra model analysis (experimental)',\n },\n 'panel.compatibility.nonblockingExtraAnalysisHelp': {\n 'zh-CN':\n '默认关闭。开启后,角色卡的部分默认前端页面会立即显示,其他插件的额外分析流程也可同时执行,无需等待变量解析完成。仅对非工具调用的自动解析生效。',\n en: \"Off by default. When enabled, some of a character card's default frontend pages appear immediately, and other plugins can run their additional analysis at the same time, without waiting for variable analysis to finish. Applies only to automatic analysis without tool calling.\",\n },\n 'panel.compatibility.license': {\n 'zh-CN': 'LICENSE',\n en: 'LICENSE',\n },\n 'panel.compatibility.licenseIntro': {\n 'zh-CN': 'MVU 采用 MIT 许可证。以下是运行时使用或随扩展打包的第三方开源组件:',\n en: 'MVU is licensed under MIT. The following third-party components are used at runtime or bundled with the extension:',\n },\n 'panel.compatibility.licenseComponent': {\n 'zh-CN': '组件',\n en: 'Component',\n },\n 'panel.compatibility.licenseIdentifier': {\n 'zh-CN': '许可证',\n en: 'License',\n },\n 'panel.compatibility.licenseDetails': {\n 'zh-CN':\n '许可证标识来自当前依赖元数据;点击组件名称可查看项目,完整条款以各项目 LICENSE 为准。',\n en: 'License identifiers come from current dependency metadata. Select a component name to view its project; the project LICENSE contains the complete terms.',\n },\n 'panel.button.section': {\n 'zh-CN': '修复按钮',\n en: 'Repair actions',\n },\n 'panel.button.reprocessVariables': {\n 'zh-CN': '重新处理变量',\n en: 'Reprocess variables',\n },\n 'panel.button.reloadInitialVariables': {\n 'zh-CN': '重新读取初始变量',\n en: 'Reload initial variables',\n },\n 'panel.button.snapshotFloor': {\n 'zh-CN': '快照楼层',\n en: 'Snapshot floor',\n },\n 'panel.button.replayFloor': {\n 'zh-CN': '重演楼层',\n en: 'Replay floor',\n },\n 'panel.button.retryExtraModelParsing': {\n 'zh-CN': '重试额外模型解析',\n en: 'Retry extra-model parsing',\n },\n 'panel.button.incrementalRepairExtraModelParsing': {\n 'zh-CN': '增量校正额外模型解析',\n en: 'Incrementally repair extra-model parsing',\n },\n 'panel.button.clearOldFloorVariables': {\n 'zh-CN': '清除旧楼层变量',\n en: 'Clear variables from old floors',\n },\n 'panel.help.ariaLabel': {\n 'zh-CN': '帮助',\n en: 'Help',\n },\n 'panel.badge.override': {\n 'zh-CN': '角色卡覆盖',\n en: 'Character-card override',\n },\n 'panel.badge.overrideWithValue': {\n 'zh-CN': '角色卡覆盖:{value}',\n en: 'Character-card override: {value}',\n },\n 'panel.badge.additive': {\n 'zh-CN': '角色卡规则叠加',\n en: 'Character-card rule added',\n },\n 'panel.update.section': {\n 'zh-CN': '变量更新方式',\n en: 'Variable update method',\n },\n 'panel.update.method.aiOutput': {\n 'zh-CN': '随 AI 输出',\n en: 'Alongside AI output',\n },\n 'panel.update.method.extraModel': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.update.method.unsupportedWarning': {\n 'zh-CN':\n '世界书 [{worldbooks}] 未适配额外模型解析,视为 [mvu_plot] 条目(只会发给剧情 AI、不会发给变量更新 AI)。',\n en: 'World book [{worldbooks}] is not adapted for extra-model parsing and will be treated as an [mvu_plot] entry (sent only to the story AI, not the variable-update AI).',\n },\n 'panel.modelSelect.ariaLabel': {\n 'zh-CN': '模型列表',\n en: 'Model list',\n },\n 'panel.modelSelect.chooseFromList': {\n 'zh-CN': '(从列表选择)',\n en: '(Choose from list)',\n },\n 'panel.modelSelect.loading': {\n 'zh-CN': '获取中…',\n en: 'Fetching…',\n },\n 'panel.modelSelect.fetch': {\n 'zh-CN': '获取模型列表',\n en: 'Fetch model list',\n },\n 'panel.modelSelect.empty': {\n 'zh-CN': '模型列表为空或获取失败',\n en: 'The model list is empty or could not be fetched',\n },\n 'panel.modelSelect.fetchTitle': {\n 'zh-CN': '[MVU]获取模型列表',\n en: '[MVU] Fetch model list',\n },\n 'panel.modelSelect.fetchFailureTitle': {\n 'zh-CN': '[MVU]获取模型列表失败',\n en: '[MVU] Failed to fetch model list',\n },\n 'panel.prompt.apiOptionsMigrated': {\n 'zh-CN': '破限/应答格式等配置 已迁移到 API 方案,与方案配置绑定',\n en: 'Jailbreak, response format, and related settings have moved to API profiles and are saved with each profile.',\n },\n 'panel.source.pi.auth.accountLogin': {\n 'zh-CN': '账号登录',\n en: 'Account sign-in',\n },\n 'panel.source.pi.contextWindowHelp': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。请按服务商说明填写;数值过小会使较长的请求无法发送。',\n en: 'The total length the model can read and generate, in tokens. Enter the limit given by your provider; setting it too low prevents longer requests from being sent.',\n },\n 'panel.prompt.section': {\n 'zh-CN': '请求内容',\n en: 'Request content',\n },\n 'panel.prompt.jailbreakStrategy': {\n 'zh-CN': '破限方案',\n en: 'Jailbreak strategy',\n },\n 'panel.prompt.jailbreak.builtin': {\n 'zh-CN': '使用内置破限',\n en: 'Use built-in jailbreak',\n },\n 'panel.prompt.jailbreak.currentPreset': {\n 'zh-CN': '使用当前预设',\n en: 'Use current preset',\n },\n 'panel.prompt.jailbreak.otherPreset': {\n 'zh-CN': '使用其他预设',\n en: 'Use another preset',\n },\n 'panel.prompt.targetPreset': {\n 'zh-CN': '目标预设',\n en: 'Target preset',\n },\n 'panel.prompt.noSavedPreset': {\n 'zh-CN': '未检测到可用的已保存预设',\n en: 'No available saved presets detected',\n },\n 'panel.prompt.randomHeader': {\n 'zh-CN': '随机头部',\n en: 'Randomized header',\n },\n 'panel.prompt.randomHeaderHelp': {\n 'zh-CN':\n 'Gemini 系模型会记录破限头部,因此需要在头部增加随机数。如果你使用的不是 Gemini 系模型,请关闭这个功能,以免缓存失效。',\n en: 'Gemini-family models may record the jailbreak header, so a random number is added to it. Disable this for non-Gemini models to avoid invalidating the prompt cache.',\n },\n 'panel.prompt.responseFormat': {\n 'zh-CN': '应答格式',\n en: 'Response format',\n },\n 'panel.prompt.response.chatMessage': {\n 'zh-CN': '聊天消息',\n en: 'Chat message',\n },\n 'panel.prompt.response.toolCall': {\n 'zh-CN': '工具调用',\n en: 'Tool call',\n },\n 'panel.prompt.response.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.prompt.response.structuredV4': {\n 'zh-CN': '格式化输出(v4 兼容)',\n en: 'Structured output (v4 compatible)',\n },\n 'panel.prompt.disableThinking': {\n 'zh-CN': '关闭 thinking',\n en: 'Disable thinking',\n },\n 'panel.prompt.disableThinkingHelp': {\n 'zh-CN': '关闭后可避免部分空回复问题。',\n en: 'Disabling thinking can prevent some empty responses.',\n },\n 'panel.prompt.disable': {\n 'zh-CN': '关闭',\n en: 'Disable',\n },\n 'panel.prompt.fakeStreaming': {\n 'zh-CN': '兼容假流式',\n en: 'Pseudo-streaming compatibility',\n },\n 'panel.prompt.fakeStreamingHelp': {\n 'zh-CN':\n '默认关闭,等待模型一次返回完整结果。若渠道要求流式请求,或等待时经常超时,可尝试勾选。开启后仍会等回复完整后再更新变量。OpenAI Codex 只支持流式传输,会始终使用流式。',\n en: 'Disabled by default: wait for the complete response. Enable this if your provider requires streaming or requests often time out while waiting. Variables are still updated after the full response arrives. OpenAI Codex requires streaming and always uses it.',\n },\n 'panel.prompt.whitelist': {\n 'zh-CN': '世界书条目白名单正则',\n en: 'World-book entry whitelist regex',\n },\n 'panel.prompt.whitelistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段只保留 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing retains only world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.whitelistPlaceholder': {\n 'zh-CN': '角色{or}地点 或 /角色{or}地点/i',\n en: 'character{or}location or /character{or}location/i',\n },\n 'panel.prompt.blacklist': {\n 'zh-CN': '世界书条目黑名单正则',\n en: 'World-book entry blacklist regex',\n },\n 'panel.prompt.blacklistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段会排除 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing excludes world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.blacklistPlaceholder': {\n 'zh-CN': '临时{or}禁用 或 /临时{or}禁用/i',\n en: 'temporary{or}disabled or /temporary{or}disabled/i',\n },\n 'panel.prompt.regexInvalid': {\n 'zh-CN': '正则无效:{error}',\n en: 'Invalid regex: {error}',\n },\n 'panel.prompt.filtered.title': {\n 'zh-CN': '上次分析被筛选的条目',\n en: 'Entries filtered from the last analysis',\n },\n 'panel.prompt.filtered.empty': {\n 'zh-CN': '上次分析没有被黑/白名单筛选掉的条目。',\n en: 'No entries were filtered by the whitelist or blacklist in the last analysis.',\n },\n 'panel.prompt.filtered.entrySource': {\n 'zh-CN': '条目来源',\n en: 'Entry source',\n },\n 'panel.prompt.filtered.worldBook': {\n 'zh-CN': '世界书',\n en: 'World book',\n },\n 'panel.prompt.filtered.reason': {\n 'zh-CN': '原因',\n en: 'Reason',\n },\n 'panel.prompt.filtered.configSource': {\n 'zh-CN': '配置来源',\n en: 'Configuration source',\n },\n 'panel.prompt.filtered.comment': {\n 'zh-CN': '条目备注',\n en: 'Comment',\n },\n 'panel.prompt.filtered.globalLore': {\n 'zh-CN': '全局世界书',\n en: 'Global world book',\n },\n 'panel.prompt.filtered.characterLore': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.prompt.filtered.chatLore': {\n 'zh-CN': '聊天世界书',\n en: 'Chat world book',\n },\n 'panel.prompt.filtered.personaLore': {\n 'zh-CN': '用户世界书',\n en: 'Persona world book',\n },\n 'panel.prompt.filtered.whitelistReason': {\n 'zh-CN': '白名单',\n en: 'Whitelist',\n },\n 'panel.prompt.filtered.blacklistReason': {\n 'zh-CN': '黑名单',\n en: 'Blacklist',\n },\n 'panel.prompt.filtered.globalConfig': {\n 'zh-CN': '用户全局配置',\n en: 'User global settings',\n },\n 'panel.prompt.filtered.characterConfig': {\n 'zh-CN': '角色卡配置',\n en: 'Character-card settings',\n },\n 'panel.prompt.toolCallUnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'工具调用'\",\n en: \"[MVU] Cannot use 'Tool call'\",\n },\n 'panel.prompt.toolCallUnsupported': {\n 'zh-CN': '当前 API 源不支持工具调用,请换用支持 tools 的渠道模型或改用其他应答格式',\n en: 'The current API source does not support tool calls. Choose a provider/model that supports tools, or use another response format.',\n },\n 'panel.prompt.structuredV4UnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'格式化输出(v4兼容)'\",\n en: \"[MVU] Cannot use 'Structured output (v4 compatible)'\",\n },\n 'panel.prompt.structuredV4RequiresCustom': {\n 'zh-CN': '格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同',\n en: 'Structured output (v4 compatible) requires a custom extra-model source and cannot use the current connection.',\n },\n 'panel.request.section': {\n 'zh-CN': '请求策略',\n en: 'Request strategy',\n },\n 'panel.request.method': {\n 'zh-CN': '请求方式',\n en: 'Request method',\n },\n 'panel.request.sequential': {\n 'zh-CN': '依次请求,失败后重试',\n en: 'Request sequentially and retry on failure',\n },\n 'panel.request.parallel': {\n 'zh-CN': '同时请求多次',\n en: 'Send multiple requests in parallel',\n },\n 'panel.request.onceThenParallel': {\n 'zh-CN': '先请求一次,失败后再同时请求多次',\n en: 'Request once, then retry in parallel on failure',\n },\n 'panel.request.count': {\n 'zh-CN': '请求次数',\n en: 'Number of requests',\n },\n 'panel.request.auto': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.request.autoHelp': {\n 'zh-CN':\n '关闭后,AI 回复完成时不会自动触发额外模型解析;你需要主动点击“重试额外模型解析”按钮,才会执行解析并添加状态栏占位符 <StatusPlaceHolderImpl/>。',\n en: 'When disabled, extra-model parsing is not triggered automatically after the AI finishes replying. Click “Retry extra-model parsing” to run it and add the status-bar placeholder <StatusPlaceHolderImpl/>.',\n },\n 'panel.request.batchWarning': {\n 'zh-CN':\n '请升级酒馆助手到 4.4.3 或更高版本,否则批量请求功能可能让预设的“流式传输”设置失效',\n en: \"Upgrade Tavern Helper to version 4.4.3 or later. Otherwise, batch requests may override the preset's streaming setting.\",\n },\n 'panel.request.batchWarningTitle': {\n 'zh-CN': '[MVU]批量请求可能有问题',\n en: '[MVU] Batch requests may not work correctly',\n },\n 'panel.source.section': {\n 'zh-CN': '模型来源',\n en: 'Model source',\n },\n 'panel.source.sameAsConnection': {\n 'zh-CN': '与插头相同',\n en: 'Same as current connection',\n },\n 'panel.source.custom': {\n 'zh-CN': '自定义',\n en: 'Custom',\n },\n 'panel.source.more': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.profile.section': {\n 'zh-CN': 'API 方案',\n en: 'API profiles',\n },\n 'panel.source.profile.current': {\n 'zh-CN': '当前方案',\n en: 'Current profile',\n },\n 'panel.source.profile.ariaLabel': {\n 'zh-CN': 'API 方案',\n en: 'API profile',\n },\n 'panel.source.profile.manual': {\n 'zh-CN': '(手动编辑,未绑定方案)',\n en: '(Manual settings, not bound to a profile)',\n },\n 'panel.source.profile.newName': {\n 'zh-CN': '新方案名称',\n en: 'New profile name',\n },\n 'panel.source.profile.save': {\n 'zh-CN': '保存当前方案',\n en: 'Save current profile',\n },\n 'panel.source.profile.saveAs': {\n 'zh-CN': '另存为新方案',\n en: 'Save as new profile',\n },\n 'panel.source.profile.delete': {\n 'zh-CN': '删除当前方案',\n en: 'Delete current profile',\n },\n 'panel.source.profile.backendCustom': {\n 'zh-CN': '自定义 API',\n en: 'Custom API',\n },\n 'panel.source.profile.backendPi': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.apiAddress': {\n 'zh-CN': 'API 地址',\n en: 'API address',\n },\n 'panel.source.apiKey': {\n 'zh-CN': 'API 密钥',\n en: 'API key',\n },\n 'panel.source.apiKeyPlaceholder': {\n 'zh-CN': '留空表示无需密钥',\n en: 'Leave blank when no key is required',\n },\n 'panel.source.modelName': {\n 'zh-CN': '模型名称',\n en: 'Model name',\n },\n // “更多”来源的连接、协议能力和认证界面文案;底层 Pi 标识不作为产品名称展示。\n 'panel.source.pi.provider': {\n 'zh-CN': '来源',\n en: 'Provider',\n },\n 'panel.source.pi.endpoint': {\n 'zh-CN': 'API 基础地址',\n en: 'API base URL',\n },\n 'panel.source.pi.endpointDefault': {\n 'zh-CN': '留空使用默认地址:{endpoint}',\n en: 'Leave blank to use the default: {endpoint}',\n },\n 'panel.source.pi.endpointPlaceholder': {\n 'zh-CN': '请输入 API 基础地址',\n en: 'Enter an API base URL',\n },\n 'panel.source.pi.endpointHelp': {\n 'zh-CN':\n '填写服务商提供的 API 地址,留空使用默认地址。也可以直接粘贴完整的聊天或 Responses 接口地址,MVU 会自动处理末尾路径。',\n en: 'Enter the API URL supplied by your provider, or leave it blank to use the default. You can also paste a full chat or Responses URL; MVU adjusts its ending automatically.',\n },\n 'panel.source.pi.proxy.use': {\n 'zh-CN': '使用 Proxy',\n en: 'Use Proxy',\n },\n 'panel.source.pi.proxy.help': {\n 'zh-CN':\n '如果获取模型列表或生成请求因 CORS(浏览器跨域限制)失败,可以尝试勾选,通过 SillyTavern 转发请求。需要先在 SillyTavern 中开启 Proxy。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'If fetching models or generating a response fails because of CORS (browser cross-origin restrictions), try enabling this to forward requests through SillyTavern. Proxy must also be enabled in SillyTavern. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.proxy.notEnabled': {\n 'zh-CN': '当前来源需要 Proxy,但 SillyTavern 的 Proxy 未开启或无法连接。',\n en: 'This provider needs Proxy, but SillyTavern Proxy is disabled or unavailable.',\n },\n 'panel.source.pi.proxy.notEnabledHelp': {\n 'zh-CN':\n '打开 SillyTavern 的 config.yaml,将 enableCorsProxy 设为 true,保存后重启 SillyTavern。如果酒馆由他人托管,请联系管理员开启。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'Open SillyTavern config.yaml, set enableCorsProxy to true, save, and restart SillyTavern. If someone else hosts your server, ask its administrator to enable it. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.model': {\n 'zh-CN': '模型',\n en: 'Model',\n },\n 'panel.source.pi.catalogModel': {\n 'zh-CN': '模型目录',\n en: 'Model catalog',\n },\n 'panel.source.pi.customModel': {\n 'zh-CN': '(手动填写模型)',\n en: '(Enter a model manually)',\n },\n 'panel.source.pi.api.openaiResponses': {\n 'zh-CN': 'OpenAI Responses',\n en: 'OpenAI Responses',\n },\n 'panel.source.pi.api.openaiCompletions': {\n 'zh-CN': 'OpenAI Chat Completions',\n en: 'OpenAI Chat Completions',\n },\n 'panel.source.pi.api.openaiCodexResponses': {\n 'zh-CN': 'OpenAI Codex Responses',\n en: 'OpenAI Codex Responses',\n },\n 'panel.source.pi.api.anthropicMessages': {\n 'zh-CN': 'Anthropic Messages',\n en: 'Anthropic Messages',\n },\n 'panel.source.pi.api.googleGenerativeAi': {\n 'zh-CN': 'Google Generative AI',\n en: 'Google Generative AI',\n },\n 'panel.source.pi.api.mistralConversations': {\n 'zh-CN': 'Mistral Conversations',\n en: 'Mistral Conversations',\n },\n 'panel.source.pi.auth.apiKey': {\n 'zh-CN': 'API Key',\n en: 'API key',\n },\n 'panel.source.pi.capabilities': {\n 'zh-CN': '可尝试的功能:{capabilities}。具体是否可用,以你使用的模型和服务商为准。',\n en: 'Features you can try: {capabilities}. Availability depends on your model and provider.',\n },\n 'panel.source.pi.capability.tools': {\n 'zh-CN': '工具调用',\n en: 'tools',\n },\n 'panel.source.pi.capability.images': {\n 'zh-CN': '读取图片',\n en: 'Read images',\n },\n 'panel.source.pi.capability.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.source.pi.thinkingLevel': {\n 'zh-CN': '思考等级',\n en: 'Thinking level',\n },\n 'panel.source.pi.thinkingLevelHelp': {\n 'zh-CN':\n '默认保持现有请求行为;其他等级按模型能力映射,部分模型无法完全关闭思考。未知模型按协议转换所选等级;OpenAI/Mistral 的极高、最高会原样尝试,Google/预算式思考仍按协议映射。Anthropic 开启思考时不支持强制工具调用,旧模型至少需要 2048 回复 token。自定义请求体覆盖仍优先。',\n en: 'Default preserves existing requests. Other levels are mapped to model capabilities; some models cannot fully disable thinking. Unknown models use protocol-specific mappings: OpenAI/Mistral attempt xhigh/max unchanged, while Google and budget-based thinking use protocol mappings. Anthropic thinking cannot force tool calls; older models need at least 2048 reply tokens. Custom body overrides take precedence.',\n },\n 'panel.source.pi.thinking.default': {\n 'zh-CN': '默认(保持原行为)',\n en: 'Default (unchanged behavior)',\n },\n 'panel.source.pi.thinking.off': {\n 'zh-CN': '关闭 / 最低',\n en: 'Off / minimum',\n },\n 'panel.source.pi.thinking.minimal': {\n 'zh-CN': '极低',\n en: 'Minimal',\n },\n 'panel.source.pi.thinking.low': {\n 'zh-CN': '低',\n en: 'Low',\n },\n 'panel.source.pi.thinking.medium': {\n 'zh-CN': '中',\n en: 'Medium',\n },\n 'panel.source.pi.thinking.high': {\n 'zh-CN': '高',\n en: 'High',\n },\n 'panel.source.pi.thinking.xhigh': {\n 'zh-CN': '极高',\n en: 'Extra high',\n },\n 'panel.source.pi.thinking.max': {\n 'zh-CN': '最高',\n en: 'Maximum',\n },\n 'panel.source.pi.contextWindow': {\n 'zh-CN': '上下文窗口',\n en: 'Context window',\n },\n 'panel.source.pi.contextWindowCatalog': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。已自动填入 {value};如果服务商给出的限制不同,可手动修改,清空后恢复自动值。',\n en: 'The total length the model can read and generate, in tokens. Automatically set to {value}. Edit it if your provider gives a different limit; clear it to restore the automatic value.',\n },\n 'panel.source.pi.contextWindowOverride': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。当前使用你填写的值;清空后会尝试自动填写已知模型的限制。',\n en: 'The total length the model can read and generate, in tokens. Using your entered value; clear it to try the known model limit.',\n },\n 'panel.source.pi.contextWindowRequired': {\n 'zh-CN': '请输入正整数上下文窗口;模型目录未提供该值时必须手动填写。',\n en: 'Enter a positive integer context window. It is required when the model catalog has no value.',\n },\n 'panel.source.pi.error.unknownProvider': {\n 'zh-CN': '“更多”中的未知来源“{provider}”;请重新选择来源。',\n en: 'Unknown provider \"{provider}\" under More. Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedApi': {\n 'zh-CN': '已保存的来源“{provider}”不支持接口“{api}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support API \"{api}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedAuth': {\n 'zh-CN': '已保存的来源“{provider}”不支持认证方式“{auth}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support authentication type \"{auth}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedEndpoint': {\n 'zh-CN': '当前来源不支持自定义 API 地址,请重新选择来源或切换 API 方案。',\n en: 'This provider does not support a custom API URL. Select a provider again or switch API profiles.',\n },\n 'panel.source.pi.featureDisabled': {\n 'zh-CN':\n '此构建已关闭“更多”模型来源。已保存的“更多”配置仍会保留;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Saved More-source settings are preserved; select Same as current connection or Custom.',\n },\n 'panel.source.pi.maxTokensPositive': {\n 'zh-CN': '最大回复 token 必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'panel.source.pi.maxTokensExceedContext': {\n 'zh-CN': '最大回复 token 不能大于上下文窗口。',\n en: 'Maximum response tokens cannot exceed the context window.',\n },\n 'panel.source.pi.customHeaders': {\n 'zh-CN': '自定义请求头',\n en: 'Custom request headers',\n },\n 'panel.source.pi.customHeadersHelp': {\n 'zh-CN':\n '仅在服务商要求额外请求头时填写,格式为 YAML 或 JSON,例如 X-App: MVU。留空即可;API 密钥请填在上方的密钥栏。',\n en: 'Fill this in only if your provider requires extra headers. Use YAML or JSON, for example X-App: MVU. Otherwise leave it blank. Enter your API key in the key field above.',\n },\n 'panel.source.pi.customOverridesSwitchHelp': {\n 'zh-CN':\n '下方的自定义请求头、附加字段和排除字段会随 API 方案保存。手动切换来源或 API 地址时会清空这三项;切换方案会恢复该方案保存的内容。',\n en: 'Custom headers, additional fields, and excluded fields below are saved with the API profile. Manually changing the provider or API URL clears these three fields; switching profiles restores their saved values.',\n },\n 'panel.source.pi.customIncludeBody': {\n 'zh-CN': '请求体附加字段',\n en: 'Additional request body fields',\n },\n 'panel.source.pi.customIncludeBodyHelp': {\n 'zh-CN':\n '服务商要求额外参数时在这里填写,使用 YAML 或 JSON,例如 seed: 123。同名参数会采用这里的值。Google 参数请放在 config 下。不需要时留空;密钥、模型和流式开关请使用对应选项。',\n en: 'Add parameters required by your provider as YAML or JSON, for example seed: 123. These values replace matching parameters. Put Google parameters under config. Leave blank when unnecessary; use the dedicated controls for keys, models, and streaming.',\n },\n 'panel.source.pi.customExcludeBody': {\n 'zh-CN': '请求体排除字段',\n en: 'Excluded request body fields',\n },\n 'panel.source.pi.customExcludeBodyHelp': {\n 'zh-CN':\n '如果服务商提示某个参数不被支持,可在这里填写参数名,使请求不再携带它。多个名称用换行或逗号分隔,也支持 YAML 列表。Google 参数写为 config.<参数名>。不需要时留空。',\n en: 'If your provider rejects a parameter, enter its name here to leave it out of requests. Separate names with newlines or commas, or use a YAML list. For Google, use config.<parameter>. Otherwise leave blank.',\n },\n 'panel.source.pi.clearCustomField': {\n 'zh-CN': '清空',\n en: 'Clear',\n },\n 'panel.source.pi.oauth.section': {\n 'zh-CN': 'OAuth 登录',\n en: 'OAuth login',\n },\n 'panel.source.pi.oauth.status': {\n 'zh-CN': '状态',\n en: 'Status',\n },\n 'panel.source.pi.oauth.checking': {\n 'zh-CN': '检查中…',\n en: 'Checking…',\n },\n 'panel.source.pi.oauth.loggedIn': {\n 'zh-CN': '已登录',\n en: 'Signed in',\n },\n 'panel.source.pi.oauth.loggedOut': {\n 'zh-CN': '未登录',\n en: 'Not signed in',\n },\n 'panel.source.pi.oauth.expiresAt': {\n 'zh-CN': '凭据有效期:{time}',\n en: 'Credential expiry: {time}',\n },\n 'panel.source.pi.oauth.login': {\n 'zh-CN': '登录',\n en: 'Sign in',\n },\n 'panel.source.pi.oauth.refresh': {\n 'zh-CN': '刷新凭证',\n en: 'Refresh credentials',\n },\n 'panel.source.pi.oauth.refreshing': {\n 'zh-CN': '正在刷新凭证…',\n en: 'Refreshing credentials…',\n },\n 'panel.source.pi.oauth.refreshSucceeded': {\n 'zh-CN': '凭证已刷新。',\n en: 'Credentials refreshed.',\n },\n 'panel.source.pi.oauth.cancel': {\n 'zh-CN': '取消登录',\n en: 'Cancel sign-in',\n },\n 'panel.source.pi.oauth.logout': {\n 'zh-CN': '登出',\n en: 'Sign out',\n },\n 'panel.source.pi.oauth.authorizationUrl': {\n 'zh-CN': '授权链接',\n en: 'Authorization URL',\n },\n 'panel.source.pi.oauth.openAuthorization': {\n 'zh-CN': '打开授权页',\n en: 'Open authorization page',\n },\n 'panel.source.pi.oauth.copyAuthorization': {\n 'zh-CN': '复制授权链接',\n en: 'Copy authorization URL',\n },\n 'panel.source.pi.oauth.copySucceeded': {\n 'zh-CN': '已复制授权链接',\n en: 'Authorization URL copied',\n },\n 'panel.source.pi.oauth.copyFailed': {\n 'zh-CN': '无法复制授权链接,请手动选择并复制。',\n en: 'Could not copy the authorization URL. Select and copy it manually.',\n },\n 'panel.source.pi.oauth.callbackUrl': {\n 'zh-CN': '回调链接',\n en: 'Callback URL',\n },\n 'panel.source.pi.oauth.callbackHelp': {\n 'zh-CN':\n '完成授权后,如果跳转到的页面打不开,不影响登录。请复制浏览器地址栏中以 127.0.0.1 或 localhost 开头的完整链接,粘贴到这里,再点击“完成登录”。',\n en: 'After authorization, the redirected page may fail to open; you can still finish signing in. Copy the full URL starting with 127.0.0.1 or localhost from the address bar, paste it here, and click Complete sign-in.',\n },\n 'panel.source.pi.oauth.complete': {\n 'zh-CN': '完成登录',\n en: 'Complete sign-in',\n },\n 'panel.source.pi.oauth.preparing': {\n 'zh-CN': '正在准备安全授权链接…',\n en: 'Preparing a secure authorization URL…',\n },\n 'panel.source.pi.oauth.waitingCallback': {\n 'zh-CN': '请在授权页完成登录,然后粘贴完整回调链接。',\n en: 'Finish signing in on the authorization page, then paste the complete callback URL.',\n },\n 'panel.source.pi.oauth.exchanging': {\n 'zh-CN': '正在验证回调并交换凭据…',\n en: 'Validating the callback and exchanging credentials…',\n },\n 'panel.source.pi.oauth.loginSucceeded': {\n 'zh-CN': 'OAuth 登录成功。',\n en: 'OAuth sign-in succeeded.',\n },\n 'panel.source.pi.oauth.logoutSucceeded': {\n 'zh-CN': '已登出。',\n en: 'Signed out.',\n },\n 'panel.source.pi.oauth.logoutConfirm': {\n 'zh-CN': '确定在当前配置中登出 {provider} 吗?之后可以登录其他账号。',\n en: 'Sign out of {provider} for the current configuration? You can then sign in with another account.',\n },\n 'panel.source.pi.oauth.failureTitle': {\n 'zh-CN': '[MVU]OAuth 操作失败',\n en: '[MVU] OAuth operation failed',\n },\n 'panel.source.advanced': {\n 'zh-CN': '高级参数',\n en: 'Advanced parameters',\n },\n 'panel.source.unsupportedAdvanced': {\n 'zh-CN': '⚠️酒馆助手版本过低,不支持以下配置',\n en: '⚠️ Your Tavern Helper version is too old to support the settings below',\n },\n 'panel.source.maxTokens': {\n 'zh-CN': '最大回复 token',\n en: 'Maximum response tokens',\n },\n 'panel.source.chatHistory': {\n 'zh-CN': '聊天历史条数',\n en: 'Chat history entries',\n },\n 'panel.source.temperature': {\n 'zh-CN': '温度',\n en: 'Temperature',\n },\n 'panel.source.frequencyPenalty': {\n 'zh-CN': '频率惩罚',\n en: 'Frequency penalty',\n },\n 'panel.source.presencePenalty': {\n 'zh-CN': '存在惩罚',\n en: 'Presence penalty',\n },\n 'panel.source.topP': {\n 'zh-CN': 'Top P',\n en: 'Top P',\n },\n 'panel.source.topK': {\n 'zh-CN': 'Top K',\n en: 'Top K',\n },\n 'panel.source.switchDirty': {\n 'zh-CN': '当前方案有未保存的修改,切换将丢弃这些修改。是否继续?',\n en: 'The current profile has unsaved changes. Switching will discard them. Continue?',\n },\n 'panel.source.continue': {\n 'zh-CN': '继续',\n en: 'Continue',\n },\n 'panel.source.discardChanges': {\n 'zh-CN': '丢弃修改',\n en: 'Discard changes',\n },\n 'panel.source.delete': {\n 'zh-CN': '删除',\n en: 'Delete',\n },\n 'panel.source.deleteDirty': {\n 'zh-CN': '当前方案“{name}”有未保存的修改,删除将丢弃这些修改。是否继续?',\n en: 'Profile “{name}” has unsaved changes. Deleting it will discard them. Continue?',\n },\n 'panel.source.deleteConfirm': {\n 'zh-CN': '确定删除 API 方案“{name}”吗?此操作不可撤销。',\n en: 'Delete API profile “{name}”? This action cannot be undone.',\n },\n 'panel.source.profileSaved': {\n 'zh-CN': '已保存 API 方案“{name}”',\n en: 'Saved API profile “{name}”',\n },\n 'panel.source.profileSavedAs': {\n 'zh-CN': '已另存为 API 方案“{name}”',\n en: 'Saved as API profile “{name}”',\n },\n 'panel.source.profileDeleted': {\n 'zh-CN': '已删除 API 方案“{name}”',\n en: 'Deleted API profile “{name}”',\n },\n 'panel.source.enterProfileName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a new profile name first',\n },\n 'panel.source.keepTwoProfiles': {\n 'zh-CN': '至少保留两个 API 方案时才可删除',\n en: 'At least two API profiles are required before one can be deleted',\n },\n 'panel.source.switchFailureTitle': {\n 'zh-CN': '[MVU]切换 API 方案失败',\n en: '[MVU] Failed to switch API profile',\n },\n 'panel.source.saveFailureTitle': {\n 'zh-CN': '[MVU]保存 API 方案失败',\n en: '[MVU] Failed to save API profile',\n },\n 'panel.source.deleteFailureTitle': {\n 'zh-CN': '[MVU]删除 API 方案失败',\n en: '[MVU] Failed to delete API profile',\n },\n 'panel.character.titleActive': {\n 'zh-CN': '当前角色卡配置(覆盖中)',\n en: 'Current character-card settings (overriding)',\n },\n 'panel.character.titleInactive': {\n 'zh-CN': '当前角色卡配置(未启用)',\n en: 'Current character-card settings (not enabled)',\n },\n 'panel.character.worldBook': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.character.reading': {\n 'zh-CN': '正在读取…',\n en: 'Reading…',\n },\n 'panel.character.unbound': {\n 'zh-CN': '未绑定',\n en: 'Not bound',\n },\n 'panel.character.inherit': {\n 'zh-CN': '跟随用户配置',\n en: 'Inherit user settings',\n },\n 'panel.character.extraModelGroup': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.character.autoRequest': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.character.whitelist': {\n 'zh-CN': '角色卡世界书条目白名单正则',\n en: 'Character-card world-book entry whitelist regex',\n },\n 'panel.character.blacklist': {\n 'zh-CN': '角色卡世界书条目黑名单正则',\n en: 'Character-card world-book entry blacklist regex',\n },\n 'panel.character.regexInvalid': {\n 'zh-CN': '角色卡配置正则无效:{error}',\n en: 'Invalid character-card regex: {error}',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const runtimeMessages = defineMessages({\n 'runtime.common.mvuTitle': {\n 'zh-CN': '[MVU]',\n en: '[MVU]',\n },\n 'runtime.common.errorCause': {\n 'zh-CN': '错误详情:{cause}',\n en: 'Error details: {cause}',\n },\n 'runtime.common.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n\n 'runtime.main.chatReinitializeFailedLog': {\n 'zh-CN': '[MVU]切换聊天后重新初始化失败',\n en: '[MVU] Failed to reinitialize after switching chats',\n },\n 'runtime.main.reinitializeFailedTitle': {\n 'zh-CN': '[MVU]重新初始化失败',\n en: '[MVU] Reinitialization failed',\n },\n 'runtime.main.minimumVersionRequired': {\n 'zh-CN': 'MVU 变量框架需要酒馆助手版本不低于 {version}',\n en: 'MVU Variable Framework requires Tavern Helper {version} or later',\n },\n 'runtime.main.versionIncompatibleTitle': {\n 'zh-CN': '版本不兼容',\n en: 'Incompatible version',\n },\n\n 'runtime.parseString.invalidFormat': {\n 'zh-CN': '要解析的字符串不是有效的 YAML/JSON/JSON5 格式',\n en: 'The input is not valid YAML, JSON, or JSON5',\n },\n 'runtime.parseString.content': {\n 'zh-CN': '字符串内容',\n en: 'Input',\n },\n 'runtime.parseString.yamlError': {\n 'zh-CN': 'YAML 错误信息',\n en: 'YAML error',\n },\n 'runtime.parseString.json5Error': {\n 'zh-CN': 'JSON5 错误信息',\n en: 'JSON5 error',\n },\n 'runtime.parseString.jsonError': {\n 'zh-CN': 'JSON 错误信息',\n en: 'JSON error',\n },\n\n 'runtime.button.initVarDataNotFound': {\n 'zh-CN': '没有找到 InitVar 数据',\n en: 'No InitVar data was found',\n },\n 'runtime.button.initVarDataLoadFailed': {\n 'zh-CN': '加载 InitVar 数据失败:',\n en: 'Failed to load InitVar data',\n },\n 'runtime.button.messageNotFound': {\n 'zh-CN': '没有找到消息',\n en: 'No message was found',\n },\n 'runtime.button.initVarDescriptionsUpdated': {\n 'zh-CN': 'InitVar描述已更新',\n en: 'InitVar descriptions updated',\n },\n 'runtime.button.snapshotPrompt': {\n 'zh-CN':\n '

设置快照楼层可以避免指定的楼层在清理操作中被移除变量信息

请填写要保留变量信息的楼层(如 10 为第 10 层)
后续楼层的重演将可以从这一层开始',\n en: '

Marking a floor as a snapshot prevents its variable data from being removed during cleanup.

Enter the floor whose variable data should be retained (for example, 10 for floor 10).
Later floors can then be replayed starting from this floor.',\n },\n 'runtime.button.invalidFloorInput': {\n 'zh-CN': '请输入有效的楼层数,你输入的是“{value}”',\n en: 'Enter a valid floor number. You entered “{value}”.',\n },\n 'runtime.button.invalidFloor': {\n 'zh-CN': '无效的楼层“{value}”',\n en: 'Invalid floor “{value}”.',\n },\n 'runtime.button.snapshotConfigureFailedTitle': {\n 'zh-CN': '[MVU]配置楼层快照失败',\n en: '[MVU] Failed to configure floor snapshot',\n },\n 'runtime.button.snapshotConfigureTitle': {\n 'zh-CN': '[MVU]配置楼层快照',\n en: '[MVU] Configure floor snapshot',\n },\n 'runtime.button.snapshotConfigured': {\n 'zh-CN': '已将 {floor} 层配置为快照楼层',\n en: 'Floor {floor} is now a snapshot floor',\n },\n 'runtime.button.replayPrompt': {\n 'zh-CN':\n '

当变量更新出现 required/extensible 相关问题时,可以尝试从过去的楼层重演

请填写要进行重演的楼层(如 10 为第 10 层,-1 为最新楼层)
也就是出现问题的楼层',\n en: '

If variable updates have required/extensible issues, replaying from an earlier floor may help.

Enter the floor to replay (for example, 10 for floor 10, or -1 for the latest floor).
This should be the floor where the problem occurred.',\n },\n 'runtime.button.replayFailedTitle': {\n 'zh-CN': '[MVU]楼层重演失败',\n en: '[MVU] Floor replay failed',\n },\n 'runtime.button.replayNoAvailableFloor': {\n 'zh-CN': '无法找到可以进行重演的楼层',\n en: 'No floor with replayable variable data was found',\n },\n 'runtime.button.replayStartPrompt': {\n 'zh-CN': '请填写从哪个楼层开始重演,找到最近的支持重演楼层为 [{floor}]',\n en: 'Enter the floor from which to start replaying. The nearest replayable floor is [{floor}].',\n },\n 'runtime.button.replayNeedsVariables': {\n 'zh-CN': '请输入含变量信息的楼层,你输入的是“{value}”',\n en: 'Enter a floor that contains variable data. You entered “{value}”.',\n },\n 'runtime.button.replayProgress': {\n 'zh-CN': '处理变量中({processed} / {total})',\n en: 'Processing variables ({processed} / {total})',\n },\n 'runtime.button.replayingFloorLog': {\n 'zh-CN': '正在重演 {index},内容:{content}',\n en: 'Replaying {index}. Content: {content}',\n },\n 'runtime.button.replayTitle': {\n 'zh-CN': '[MVU]楼层重演',\n en: '[MVU] Floor replay',\n },\n 'runtime.button.replayCompleted': {\n 'zh-CN': '已将 {floor} 层变量状态重演完毕,共重演 {count} 楼',\n en: 'Variable state replay for floor {floor} is complete ({count} floors replayed)',\n },\n 'runtime.button.extraModelNotEnabled': {\n 'zh-CN': '当前配置没有启用额外模型解析,不需要进行此操作',\n en: 'Extra-model parsing is not enabled by the current configuration',\n },\n 'runtime.button.extraModelToolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或配置指定的 LLM 不支持工具调用,请调整额外模型解析设置',\n en: 'The current TavernHelper version or configured LLM does not support tool calling. Adjust the extra-model parsing settings.',\n },\n 'runtime.button.extraModelUnsupportedByCard': {\n 'zh-CN': '当前角色卡不支持额外模型解析,无法进行此操作',\n en: 'The current character card does not support extra-model parsing',\n },\n 'runtime.button.extraModelRetryTitle': {\n 'zh-CN': '[MVU]重试额外模型解析',\n en: '[MVU] Retry extra-model parsing',\n },\n 'runtime.button.extraModelParsingCompleted': {\n 'zh-CN': '解析完成',\n en: 'Parsing completed',\n },\n 'runtime.incrementalRepair.title': {\n 'zh-CN': '[MVU]增量校正变量',\n en: '[MVU] Incremental variable repair',\n },\n 'runtime.incrementalRepair.alreadyRunning': {\n 'zh-CN': '已有一项增量校正正在运行',\n en: 'An incremental repair is already running',\n },\n 'runtime.incrementalRepair.noUsableFloor': {\n 'zh-CN': '最新楼层或上一楼没有可用的 MVU 状态,无法进行增量校正',\n en: 'The latest or previous floor has no usable MVU state',\n },\n 'runtime.incrementalRepair.directionPrompt': {\n 'zh-CN':\n '可选:补充本次增量校正方向;直接确认留空则自动审计。\\n例如:核对生命值归零后的即时后果,或补齐本轮明确获得的物品。',\n en: 'Optional: add a direction for this incremental repair. Confirm with an empty input for automatic audit.\\nFor example: check the immediate zero-HP aftermath or add an item clearly obtained this floor.',\n },\n 'runtime.incrementalRepair.requestFailed': {\n 'zh-CN': '增量校正请求失败或已被中止,未修改变量',\n en: 'The incremental repair failed or was cancelled; variables were not changed',\n },\n 'runtime.incrementalRepair.replayFailed': {\n 'zh-CN': '整楼重放失败,未提交校正:\\n{detail}',\n en: 'Full-floor replay failed. No repair was committed:\\n{detail}',\n },\n 'runtime.incrementalRepair.sourceChanged': {\n 'zh-CN': '等待期间聊天、楼层、回复版本或变量已经变化,本次结果已作废',\n en: 'The chat, floor, reply version, or variables changed while waiting; this result was discarded',\n },\n 'runtime.incrementalRepair.noEffectiveChanges': {\n 'zh-CN': '校正补丁没有产生实际变化,未写入正文或变量',\n en: 'The repair patch made no effective changes and was not written',\n },\n 'runtime.incrementalRepair.previewTitle': {\n 'zh-CN': '增量校正预览',\n en: 'Incremental repair preview',\n },\n 'runtime.incrementalRepair.previewDescription': {\n 'zh-CN':\n '模型提出 {count} 项修正。确认后合并本楼更新块,并从上一楼变量完整重算本楼,统一保存正文与变量。',\n en: 'The model proposed {count} repairs. The merged floor will be replayed from the preceding variables, then saved together with its resulting state.',\n },\n 'runtime.incrementalRepair.showRawPatch': {\n 'zh-CN': '查看原始补丁',\n en: 'Show raw patch',\n },\n 'runtime.incrementalRepair.applyButton': {\n 'zh-CN': '应用修正',\n en: 'Apply repairs',\n },\n 'runtime.incrementalRepair.cancelButton': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n 'runtime.incrementalRepair.appliedClickToUndo': {\n 'zh-CN': '增量校正已应用并写回正文;点击此通知可撤销',\n en: 'Incremental repair applied and written back; click this notice to undo',\n },\n 'runtime.incrementalRepair.undoStateChanged': {\n 'zh-CN': '变量或回复在校正后又发生了变化,为避免覆盖新进度,不能直接撤销',\n en: 'Variables or the reply changed after repair; undo was blocked to protect newer progress',\n },\n 'runtime.incrementalRepair.undone': {\n 'zh-CN': '已撤销此次增量校正',\n en: 'The incremental repair was undone',\n },\n 'runtime.button.cleanupPrompt': {\n 'zh-CN':\n '

清除旧楼层变量信息以减小聊天文件大小,避免手机崩溃

请填写要保留变量信息的楼层数(如 10 为保留最后 10 层,每 [{interval}] 层保留一层作为快照)
注意:你需要通过重演才能回退游玩到未保留变量信息的楼层',\n en: '

Remove variable data from old floors to reduce chat file size and help prevent mobile crashes.

Enter how many recent floors to retain (for example, 10 retains the latest 10 floors, with one snapshot every [{interval}] floors).
Note: you must replay variables before returning to floors whose variable data was removed.',\n },\n 'runtime.button.cleanupFailedTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量失败',\n en: '[MVU] Failed to clean old floor variables',\n },\n 'runtime.button.cleanupSucceededTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量成功',\n en: '[MVU] Old floor variables cleaned',\n },\n 'runtime.button.cleanupCompleted': {\n 'zh-CN': '已清理旧变量,保留了最后 {depth} 层的变量',\n en: 'Old variables were cleaned; variables for the latest {depth} floors were retained',\n },\n 'runtime.button.snapshotMarkedLog': {\n 'zh-CN': '将 [{floor}] 层作为快照楼层',\n en: 'Marked floor [{floor}] as a snapshot floor',\n },\n\n 'runtime.characterOverride.worldbookReadFailed': {\n 'zh-CN': '无法读取角色世界书“{worldbook}”',\n en: 'Unable to read character lorebook “{worldbook}”',\n },\n 'runtime.characterOverride.contentMustBeObject': {\n 'zh-CN': '配置正文必须是 JSON 对象',\n en: 'The settings content must be a JSON object',\n },\n 'runtime.characterOverride.conflictPrompt': {\n 'zh-CN':\n '角色世界书“{worldbook}”中的 {entry} 已被其他来源修改。是否使用待保存的角色卡配置覆盖它?',\n en: '{entry} in character lorebook “{worldbook}” was modified by another source. Overwrite it with the pending character-card configuration?',\n },\n 'runtime.characterOverride.overwriteButton': {\n 'zh-CN': '覆盖',\n en: 'Overwrite',\n },\n 'runtime.characterOverride.loadLatestButton': {\n 'zh-CN': '加载最新配置',\n en: 'Load latest configuration',\n },\n 'runtime.characterOverride.bindingReadFailedLog': {\n 'zh-CN': '[MVU]读取角色世界书绑定失败',\n en: '[MVU] Failed to read the character lorebook binding',\n },\n 'runtime.characterOverride.readFailedLog': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read the character-card configuration',\n },\n 'runtime.characterOverride.readFailedTitle': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read character-card configuration',\n },\n 'runtime.characterOverride.stopWithUnsavedChangesLog': {\n 'zh-CN': '[MVU]停止角色卡配置控制器时仍未能保存“{worldbook}”中的修改',\n en: '[MVU] Changes in “{worldbook}” were still unsaved when the character-card configuration controller stopped',\n },\n 'runtime.characterOverride.reloadFailedLog': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload the character-card configuration',\n },\n 'runtime.characterOverride.reloadFailedTitle': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload character-card configuration',\n },\n 'runtime.characterOverride.duplicateEntries': {\n 'zh-CN':\n '角色世界书“{worldbook}”中存在 {count} 个关闭的 {entry} 条目,仅使用扫描到的第一个。',\n en: 'Character lorebook “{worldbook}” contains {count} disabled {entry} entries. Only the first scanned entry will be used.',\n },\n 'runtime.characterOverride.duplicateEntriesTitle': {\n 'zh-CN': '[MVU]角色卡配置重复',\n en: '[MVU] Duplicate character-card configuration',\n },\n 'runtime.characterOverride.invalidWorldbookData': {\n 'zh-CN': '角色世界书“{worldbook}”的数据格式无效',\n en: 'Character lorebook “{worldbook}” has an invalid data format',\n },\n 'runtime.characterOverride.invalidConfigurationLog': {\n 'zh-CN': '[MVU]角色世界书“{worldbook}”的 {entry} 配置无效',\n en: '[MVU] The {entry} configuration in character lorebook “{worldbook}” is invalid',\n },\n 'runtime.characterOverride.invalidConfiguration': {\n 'zh-CN': '角色世界书“{worldbook}”中的配置无效,已按不存在处理:{cause}',\n en: 'The configuration in character lorebook “{worldbook}” is invalid and will be treated as absent: {cause}',\n },\n 'runtime.characterOverride.invalidConfigurationTitle': {\n 'zh-CN': '[MVU]角色卡配置无效',\n en: '[MVU] Invalid character-card configuration',\n },\n 'runtime.characterOverride.saveFailedLog': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save the character-card configuration',\n },\n 'runtime.characterOverride.saveFailedTitle': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save character-card configuration',\n },\n\n 'runtime.cleanup.title': {\n 'zh-CN': '[MVU]自动清理',\n en: '[MVU] Automatic cleanup',\n },\n 'runtime.cleanup.legacyPrompt': {\n 'zh-CN':\n '检测到可以清理本聊天文件中的旧变量以减小文件体积,是否清理?(备份会消耗较多内存,手机上建议关闭其他后台应用后进行,或在计算机上备份)',\n en: 'Old variables can be removed from this chat to reduce its file size. Clean them now? (Creating a backup uses considerable memory; on mobile, close other background apps first or create the backup on a computer.)',\n },\n 'runtime.cleanup.cleanOnlyButton': {\n 'zh-CN': '仅清理',\n en: 'Clean only',\n },\n 'runtime.cleanup.doNotRemindButton': {\n 'zh-CN': '不再提醒',\n en: 'Do not remind me again',\n },\n 'runtime.cleanup.backupAndCleanButton': {\n 'zh-CN': '备份并清理',\n en: 'Back up and clean',\n },\n 'runtime.cleanup.starting': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量…',\n en: 'Cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.startingWithBackup': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量,并自动生成备份…',\n en: 'Creating a backup, then cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.exportFailed': {\n 'zh-CN': '聊天记录导出失败,放弃清理:{cause}',\n en: 'Chat export failed; cleanup was cancelled: {cause}',\n },\n 'runtime.cleanup.exportSucceeded': {\n 'zh-CN': '聊天记录导出成功:{message}',\n en: 'Chat exported successfully: {message}',\n },\n 'runtime.cleanup.cleanedMessages': {\n 'zh-CN': '已清理旧聊天记录中的 {count} 条消息',\n en: 'Cleaned {count} messages from the old chat history',\n },\n 'runtime.cleanup.cleanedFloorsLog': {\n 'zh-CN': '[MVU]已清理 {count} 层的消息',\n en: '[MVU] Cleaned messages on {count} floors',\n },\n 'runtime.cleanup.restoreNotNeededLog': {\n 'zh-CN': '最近 {count} 层都包含变量数据,不需要进行恢复。',\n en: 'All of the latest {count} floors contain variable data; restoration is not needed.',\n },\n 'runtime.cleanup.restoreUnavailable': {\n 'zh-CN': '在 0~{floor} 层找不到有效的变量信息,无法进行楼层变量恢复',\n en: 'No valid variable data was found between floors 0 and {floor}; floor variables cannot be restored',\n },\n 'runtime.cleanup.restoreTitle': {\n 'zh-CN': '[MVU]恢复旧楼层变量',\n en: '[MVU] Restore old floor variables',\n },\n\n 'runtime.initvar.noMessagesLog': {\n 'zh-CN': '不存在任何一条消息,退出',\n en: 'No messages exist; initialization stopped',\n },\n 'runtime.initvar.greetingRequired': {\n 'zh-CN': '需要有开场白才能初始化变量',\n en: 'A greeting message is required to initialize variables',\n },\n 'runtime.initvar.initializationFailedTitle': {\n 'zh-CN': '[MVU]变量初始化失败',\n en: '[MVU] Variable initialization failed',\n },\n 'runtime.initvar.oldLorebookFormatLog': {\n 'zh-CN': '检测到旧的 initialized_lorebooks 数组格式,正在迁移到新的对象格式。',\n en: 'Old initialized_lorebooks array format detected. Migrating to the new object format.',\n },\n 'runtime.initvar.chatVariablesInitializedLog': {\n 'zh-CN': '聊天变量已初始化。',\n en: 'Chat variables initialized.',\n },\n 'runtime.initvar.blockParseFailedLog': {\n 'zh-CN': '解析 initvar 块失败:{cause}',\n en: 'Failed to parse initvar block: {cause}',\n },\n 'runtime.initvar.initializationCompletedLog': {\n 'zh-CN': '变量初始化完成',\n en: 'Variable initialization completed',\n },\n 'runtime.initvar.initializationSucceeded': {\n 'zh-CN': '已加载新的世界书初始化变量,当前使用世界书:
{books}',\n en: 'New lorebook initialization variables were loaded. Active lorebooks:
{books}',\n },\n 'runtime.initvar.initializationSucceededTitle': {\n 'zh-CN': '[MVU]变量初始化成功',\n en: '[MVU] Variable initialization succeeded',\n },\n 'runtime.initvar.primaryLorebookReadFailedLog': {\n 'zh-CN': '获取角色主 lorebook 失败,已忽略',\n en: 'Failed to obtain the primary character lorebook; ignoring it',\n },\n 'runtime.initvar.entryParseFailedLog': {\n 'zh-CN': '解析世界书条目“{comment}”失败:{cause}',\n en: 'Failed to parse lorebook entry “{comment}”: {cause}',\n },\n 'runtime.initvar.entryParseFailedTitle': {\n 'zh-CN': '[MVU]解析世界书条目“{comment}”失败',\n en: '[MVU] Failed to parse lorebook entry “{comment}”',\n },\n 'runtime.initvar.noMessages': {\n 'zh-CN': '不存在任何一条消息',\n en: 'No messages exist',\n },\n\n 'runtime.notification.settingsUiUpdatedTitle': {\n 'zh-CN': '[MVU]已更新独立配置界面',\n en: '[MVU] Dedicated settings panel updated',\n },\n 'runtime.notification.settingsUiUpdatedBody': {\n 'zh-CN': '配置界面位于酒馆扩展界面中的“正则”下方,请点开了解新功能或自定义配置',\n en: 'The settings panel is located below “Regex” in SillyTavern Extensions. Open it to review new features or customize MVU.',\n },\n 'runtime.notification.autoCleanupFeatureTitle': {\n 'zh-CN': '[MVU]已更新自动清理旧变量功能',\n en: '[MVU] Automatic cleanup updated',\n },\n 'runtime.notification.autoCleanupFeatureBody': {\n 'zh-CN':\n 'MVU 现在可以自动清理旧变量来减少聊天文件大小;这不会影响你回退游玩以前的楼层。可在设置中开启“变量自动清理”。',\n en: 'MVU can now automatically remove old variable data to reduce chat file size without preventing you from returning to earlier floors. Enable Automatic Variable Cleanup in settings.',\n },\n 'runtime.notification.customApiOptionsTitle': {\n 'zh-CN': '[MVU]已更新更多自定义 API 配置',\n en: '[MVU] More custom API options added',\n },\n 'runtime.notification.customApiOptionsBody': {\n 'zh-CN':\n 'MVU 现在可以自定义 API 的温度、频率惩罚、存在惩罚和最大回复 token 数;需要酒馆助手版本 >= 4.0.14。',\n en: 'MVU now supports custom API temperature, frequency penalty, presence penalty, and maximum response tokens. TavernHelper >= 4.0.14 is required.',\n },\n 'runtime.notification.cleanupDefaultTitle': {\n 'zh-CN': '[MVU]已更新自动清理配置',\n en: '[MVU] Automatic cleanup settings updated',\n },\n 'runtime.notification.cleanupDefaultBody': {\n 'zh-CN': 'MVU 现在会自动清理较老楼层上的变量信息,以降低聊天文件大小。',\n en: 'MVU now automatically removes variable data from older floors to reduce chat file size.',\n },\n 'runtime.notification.builtinJailbreakTitle': {\n 'zh-CN': '[MVU]已内置破限',\n en: '[MVU] Built-in jailbreak prompt added',\n },\n 'runtime.notification.builtinJailbreakBody': {\n 'zh-CN':\n '现在,额外模型解析如果取消“发送预设”,则会使用内置的破限提示词——既避免写作任务又避免道歉。',\n en: 'When Send Preset is disabled, extra-model parsing now uses a built-in jailbreak prompt designed to avoid both writing tasks and refusals.',\n },\n 'runtime.notification.concurrentRequestsTitle': {\n 'zh-CN': '[MVU]已支持同时多次请求变量更新',\n en: '[MVU] Concurrent variable-update requests added',\n },\n 'runtime.notification.concurrentRequestsBody': {\n 'zh-CN':\n '现在,你可以在“变量更新方式 → 请求策略 → 请求方式”中选择并发请求,提高额外模型解析成功率并节省时间。',\n en: 'You can now choose concurrent requests under Variable Update Method → Request Strategy → Request Method to improve extra-model parsing success rates and save time.',\n },\n 'runtime.notification.buildInfo': {\n 'zh-CN': '构建信息:{date}({commit})',\n en: 'Build: {date} ({commit})',\n },\n 'runtime.notification.scriptLoadedTitle': {\n 'zh-CN': '[MVU]脚本加载成功',\n en: '[MVU] Script loaded',\n },\n\n 'runtime.filter.toolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或预设/API 不支持工具调用,已退化回 `随AI输出`',\n en: 'The current TavernHelper version or preset/API does not support tool calling. Falling back to Follow AI Output.',\n },\n 'runtime.filter.toolCallingUnsupportedTitle': {\n 'zh-CN': '[MVU]无法使用工具调用',\n en: '[MVU] Tool calling unavailable',\n },\n 'runtime.filter.globalSettingsSource': {\n 'zh-CN': '用户全局配置',\n en: 'global user settings',\n },\n 'runtime.filter.characterSettingsSource': {\n 'zh-CN': '角色卡配置',\n en: 'character-card settings',\n },\n 'runtime.filter.whitelistRegexLabel': {\n 'zh-CN': '白名单正则',\n en: 'whitelist regex',\n },\n 'runtime.filter.blacklistRegexLabel': {\n 'zh-CN': '黑名单正则',\n en: 'blacklist regex',\n },\n 'runtime.filter.invalidRegex': {\n 'zh-CN': '{source}的{label}无效,已在本轮世界书条目过滤中忽略:{cause}',\n en: 'The {label} from {source} is invalid and was ignored for this lorebook filtering pass: {cause}',\n },\n 'runtime.filter.invalidRegexTitle': {\n 'zh-CN': '[MVU]世界书条目过滤正则无效',\n en: '[MVU] Invalid lorebook entry filter regex',\n },\n 'runtime.filter.logTitle': {\n 'zh-CN': '[MVU]世界书条目黑/白名单筛选结果',\n en: '[MVU] Lorebook entry allowlist/blocklist filtering results',\n },\n 'runtime.regex.jsStyleSyntax': {\n 'zh-CN': 'JS 风格正则需要以 /pattern/flags 形式书写',\n en: 'JavaScript-style regular expressions must use the /pattern/flags form',\n },\n\n 'runtime.functionCalling.versionUnsupported': {\n 'zh-CN': '当前酒馆助手版本为 {current},工具调用需要酒馆助手 {required} 或更高版本',\n en: 'The current TavernHelper version is {current}. Tool calling requires TavernHelper {required} or later.',\n },\n 'runtime.extraModel.characterLorebookUnavailableLog': {\n 'zh-CN': '无法找到角色世界书,在多人聊天下不支持额外模型解析。',\n en: 'The character lorebook could not be found. Extra-model parsing is unavailable in group chats.',\n },\n 'runtime.extraModel.v4RequiresCustomSource': {\n 'zh-CN': '[MVU额外模型解析]格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同。',\n en: '[MVU extra-model parsing] Formatted Output (v4 Compatible) requires a Custom model source and cannot use Same as Primary Connection.',\n },\n 'runtime.extraModel.customIncludeBodyInvalid': {\n 'zh-CN': '[MVU额外模型解析]custom_include_body 不是 YAML object,无法合并配置。',\n en: '[MVU extra-model parsing] custom_include_body is not a YAML object, so the configuration cannot be merged.',\n },\n 'runtime.extraModel.saveSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern saveSettings,不能临时更新配置。',\n en: '[MVU extra-model parsing] SillyTavern saveSettings is unavailable; settings cannot be updated temporarily.',\n },\n 'runtime.extraModel.openAiSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable.',\n },\n 'runtime.extraModel.openAiSettingsRestoreUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置,不能恢复配置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable; the configuration cannot be restored.',\n },\n 'runtime.extraModel.requesting': {\n 'zh-CN': '正在请求 AI 回复…',\n en: 'Requesting an AI response…',\n },\n 'runtime.extraModel.retrying': {\n 'zh-CN': '正在重试({attempt} / {total})…',\n en: 'Retrying ({attempt} / {total})…',\n },\n 'runtime.extraModel.updateInProgressTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新中',\n en: '[MVU extra-model parsing] Updating variables',\n },\n 'runtime.extraModel.concurrentRequests': {\n 'zh-CN': '将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.firstRequest': {\n 'zh-CN': '将先请求一次,尝试是否能成功…',\n en: 'Trying one request first…',\n },\n 'runtime.extraModel.firstRequestFailed': {\n 'zh-CN': '首次请求失败,将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'The first request failed. Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.updateTagMissing': {\n 'zh-CN': '[MVU额外模型解析]没有能从回复中找到标签',\n en: '[MVU extra-model parsing] No tag was found in the response',\n },\n 'runtime.extraModel.updateCommandsInvalid': {\n 'zh-CN': '[MVU额外模型解析]从回复找到了标签,但其内的更新命令无效',\n en: '[MVU extra-model parsing] An tag was found, but its update commands are invalid',\n },\n 'runtime.extraModel.firstFloorSkippedLog': {\n 'zh-CN': '[MVU]对第一层永不进行额外模型解析',\n en: '[MVU] Extra-model parsing is never performed on the first floor',\n },\n 'runtime.extraModel.autoRequestDisabledLog': {\n 'zh-CN': '[MVU]不自动触发额外模型解析',\n en: '[MVU] Automatic extra-model parsing is disabled',\n },\n 'runtime.extraModel.updateFailed': {\n 'zh-CN': '建议调整变量更新方式;可在“输入框左下角魔棒 → 日志查看器”查看具体情况',\n en: 'Try adjusting the variable update method. Open Magic Wand → Log Viewer at the lower-left of the input box for details.',\n },\n 'runtime.extraModel.updateFailedTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新失败',\n en: '[MVU extra-model parsing] Variable update failed',\n },\n\n // Pi 运行时统一错误文案:只插入经过分类的参数,不拼接服务商原始响应或认证信息。\n 'runtime.pi.invalidConfig': {\n 'zh-CN': '“更多”配置无效,请检查其中的来源、API 和认证设置。',\n en: 'The More-source configuration is invalid. Check its provider, API, and authentication settings.',\n },\n 'runtime.pi.unknownProvider': {\n 'zh-CN': '“更多”中所选来源不可用,请重新选择来源。',\n en: 'The selected provider under More is unavailable. Select a provider again.',\n },\n 'runtime.pi.unsupportedApi': {\n 'zh-CN': '“更多”中所选来源不支持当前 API,请改用该来源支持的 API。',\n en: 'The selected provider under More does not support the current API. Choose an API supported by this provider.',\n },\n 'runtime.pi.unsupportedAuth': {\n 'zh-CN': '“更多”中所选来源不支持当前认证方式,请改用该来源支持的认证方式。',\n en: 'The selected provider under More does not support the current authentication method. Choose a supported method.',\n },\n 'runtime.pi.missingApiKey': {\n 'zh-CN': '请输入“更多”中所选来源的 API Key。',\n en: 'Enter an API key for the selected provider under More.',\n },\n 'runtime.pi.missingOAuthCredential': {\n 'zh-CN': '“更多”中所选来源尚未完成 OAuth 登录,请先登录。',\n en: 'The selected provider under More is not signed in with OAuth. Sign in before retrying.',\n },\n 'runtime.pi.oauthCredentialExpired': {\n 'zh-CN': '“更多”的 OAuth 凭据已过期且无法刷新,请登出后再登录。',\n en: 'The OAuth credential for More has expired and could not be refreshed. Sign out, then sign in again.',\n },\n 'runtime.pi.invalidEndpoint': {\n 'zh-CN':\n '自定义 endpoint 必须使用 HTTPS;HTTP 仅允许 localhost、127.0.0.1 或 [::1],且不能包含凭据、查询参数或片段。',\n en: 'The custom endpoint must use HTTPS. HTTP is limited to localhost, 127.0.0.1, or [::1]. Credentials, query parameters, and fragments are not allowed.',\n },\n 'runtime.pi.customEndpointNotAllowed': {\n 'zh-CN': '“更多”中所选来源使用固定 endpoint,不允许自定义。',\n en: 'The selected provider under More uses a fixed endpoint and does not allow a custom one.',\n },\n 'runtime.pi.oauthEndpointNotAllowed': {\n 'zh-CN': 'OAuth 来源必须使用注册的固定 endpoint,请清除自定义 endpoint。',\n en: 'OAuth providers must use their registered fixed endpoint. Clear the custom endpoint.',\n },\n 'runtime.pi.oauthApiMismatch': {\n 'zh-CN': '当前 OAuth 来源只能使用其锁定的兼容 API,请重新选择来源或认证方式。',\n en: 'This OAuth provider can only use its locked compatible API. Reselect the provider or authentication method.',\n },\n 'runtime.pi.missingModel': {\n 'zh-CN': '请填写“更多”来源的模型名称。',\n en: 'Enter a model name for the More source.',\n },\n 'runtime.pi.invalidContextWindow': {\n 'zh-CN': 'contextWindow 必须是正整数。',\n en: 'contextWindow must be a positive integer.',\n },\n 'runtime.pi.missingContextWindow': {\n 'zh-CN': '当前模型不在“更多”的内置目录中,请手动填写有效的 contextWindow。',\n en: 'The current model is not in the built-in catalog under More. Enter a valid contextWindow manually.',\n },\n 'runtime.pi.invalidMaxTokens': {\n 'zh-CN': '“最大回复 token 数”必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'runtime.pi.maxTokensExceedContext': {\n 'zh-CN': '“最大回复 token 数”不能大于 contextWindow。',\n en: 'Maximum response tokens must not exceed contextWindow.',\n },\n 'runtime.pi.anthropicSamplingConflict': {\n 'zh-CN': 'Anthropic 的温度和 top_p 只能调整其中一个,请将另一个恢复为默认值 1。',\n en: 'Adjust either temperature or top_p for Anthropic. Reset the other to its default value of 1.',\n },\n 'runtime.pi.customHeadersInvalid': {\n 'zh-CN': '“更多”的自定义请求头必须是 YAML/JSON 对象,且值只能是字符串或 null。',\n en: 'Custom headers under More must be a YAML/JSON object whose values are strings or null.',\n },\n 'runtime.pi.customConfigParseFailed': {\n 'zh-CN': '“更多”的自定义请求配置不是有效的 YAML 或 JSON,请检查格式。',\n en: 'The custom request settings under More are not valid YAML or JSON. Check their formatting.',\n },\n 'runtime.pi.customIncludeBodyInvalid': {\n 'zh-CN': '“更多”的自定义请求体必须是 YAML/JSON 对象。',\n en: 'The custom request body under More must be a YAML/JSON object.',\n },\n 'runtime.pi.customExcludeBodyInvalid': {\n 'zh-CN': '“更多”的排除请求字段必须是 YAML/JSON 字段名数组。',\n en: 'Excluded request fields under More must be a YAML/JSON array of field names.',\n },\n 'runtime.pi.customPayloadProtectedField': {\n 'zh-CN': '“更多”的自定义请求配置不能覆盖、排除或设置认证及关键协议字段。',\n en: 'Custom request settings under More cannot override, exclude, or set authentication or other protected protocol fields.',\n },\n 'runtime.pi.payloadInvalid': {\n 'zh-CN': '“更多”来源的请求体必须是对象,请检查 API 配置。',\n en: 'The provider payload under More must be an object. Check the API configuration.',\n },\n 'runtime.pi.structuredOutputSchemaMissing': {\n 'zh-CN': '“更多”的格式化输出需要有效的 JSON Schema。',\n en: 'Formatted output under More requires a valid JSON Schema.',\n },\n 'runtime.pi.structuredOutputUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持原生格式化输出,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support native formatted output. Change the configuration and retry.',\n },\n 'runtime.pi.toolCallingUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持所需的工具调用模式,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support the required tool-calling mode. Change the configuration and retry.',\n },\n 'runtime.pi.toolRequestRejected': {\n 'zh-CN':\n '目标端点未接受“工具调用”请求。请确认该端点和模型支持工具调用,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the tool-calling request. Confirm that the endpoint and model support tool calling, and check the API type, URL, and model name.',\n },\n 'runtime.pi.structuredOutputRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出”请求。请确认该端点和模型支持 JSON Schema 格式化输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the formatted-output request. Confirm that the endpoint and model support JSON Schema formatted output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.jsonObjectRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出(v4兼容)”请求。请确认该端点和模型支持 JSON Object 输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the v4-compatible formatted-output request. Confirm that the endpoint and model support JSON Object output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.namedToolChoiceUnsupported': {\n 'zh-CN': '“更多”中的当前 API 不支持指定工具调用,请更换 API 或调整工具选择方式。',\n en: 'The current API under More does not support named tool choice. Choose another API or adjust the tool-choice mode.',\n },\n 'runtime.pi.invalidToolDefinition': {\n 'zh-CN':\n '“更多”来源的工具定义无效:function name 不能为空,且 parameters 根节点必须是 object。',\n en: 'The tool definition under More is invalid: function name cannot be empty, and the parameters root must be an object.',\n },\n 'runtime.pi.invalidToolCall': {\n 'zh-CN': '“更多”来源返回了无效的工具调用,请重试或更换模型。',\n en: 'The More source returned an invalid tool call. Retry or choose another model.',\n },\n 'runtime.pi.toolUseMissingCall': {\n 'zh-CN': '“更多”来源以工具调用结束,但没有返回工具调用内容,请重试或更换模型。',\n en: 'The More source stopped for tool use without returning a tool call. Retry or choose another model.',\n },\n 'runtime.pi.imageInputUnsupported': {\n 'zh-CN': '“更多”中的当前模型不支持图片输入,请移除图片或更换模型。',\n en: 'The current model under More does not support image input. Remove the image or choose another model.',\n },\n 'runtime.pi.contextInvalidImage': {\n 'zh-CN': '“更多”的图片输入必须是可解码的 image/* base64 data URL。',\n en: 'Image input under More must be a decodable image/* base64 data URL.',\n },\n 'runtime.pi.remoteImageUnsupported': {\n 'zh-CN': '“更多”当前不支持远程图片 URL,请改用 base64 data URL。',\n en: 'The More source does not currently support remote image URLs. Use a base64 data URL instead.',\n },\n 'runtime.pi.videoUnsupported': {\n 'zh-CN': '“更多”链路当前不支持视频内容,请移除视频后重试。',\n en: 'The More path does not currently support video content. Remove the video and retry.',\n },\n 'runtime.pi.contextSystemPayloadMismatch': {\n 'zh-CN': '无法在服务商请求中完整恢复 system 消息,已停止发送。请更新脚本或更换协议后重试。',\n en: 'Could not restore all system messages in the provider payload. Sending was stopped. Update the script or select another API and try again.',\n },\n 'runtime.pi.contextMissingToolCall': {\n 'zh-CN': '第 {index} 条历史工具结果无法找到对应的工具调用,请检查聊天历史。',\n en: 'Historical tool result {index} has no matching tool call. Check the chat history.',\n },\n 'runtime.pi.contextUnsupportedContent': {\n 'zh-CN': '第 {index} 条消息包含“更多”当前不支持的内容,请移除该内容后重试。',\n en: 'Message {index} contains content that More does not currently support. Remove it and retry.',\n },\n 'runtime.pi.promptCaptureFailed': {\n 'zh-CN': '未能捕获 SillyTavern 构建后的最终提示词,请重试并查看日志。',\n en: 'Could not capture the final prompt built by SillyTavern. Retry and check the logs.',\n },\n 'runtime.pi.requestAlreadyActive': {\n 'zh-CN': '同一“更多”请求已在运行,请等待它结束或先停止该请求。',\n en: 'The same More-source request is already running. Wait for it to finish or stop it first.',\n },\n 'runtime.pi.tokenBudgetExceeded': {\n 'zh-CN':\n '请求预计占用 {estimatedInput} 个输入 token;加上 {maxTokens} 个回复 token 和 {reserve} 个预留 token 后,超过 {contextWindow} 的上下文窗口。请缩短聊天历史、降低最大回复 token 数,或修正 contextWindow。',\n en: 'The request is estimated to use {estimatedInput} input tokens. Together with {maxTokens} response tokens and {reserve} reserved tokens, it exceeds the {contextWindow}-token context window. Shorten chat history, reduce maximum response tokens, or correct contextWindow.',\n },\n 'runtime.pi.requestAborted': {\n 'zh-CN': '“更多”请求已取消。',\n en: 'The More-source request was cancelled.',\n },\n 'runtime.pi.browserNetworkError': {\n 'zh-CN':\n '浏览器无法连接“更多”中所选来源。请检查网络、地址配置,并确认来源允许浏览器 CORS 请求。',\n en: 'The browser could not reach the selected provider under More. Check the network and address settings, and confirm that the provider allows browser CORS requests.',\n },\n 'runtime.pi.proxyUnavailable': {\n 'zh-CN':\n '没有开启Proxy。请在 SillyTavern 的 config.yaml 中开启 enableCorsProxy,或使用 --corsProxy 启动参数,然后重启 SillyTavern。',\n en: 'Proxy is not enabled. Enable enableCorsProxy in SillyTavern config.yaml, or start SillyTavern with --corsProxy, then restart SillyTavern.',\n },\n 'runtime.pi.requestFailed': {\n 'zh-CN': '“更多”来源请求失败。请检查来源配置、凭据和网络后重试。',\n en: 'The provider request under More failed. Check the provider settings, credentials, and network, then retry.',\n },\n 'runtime.pi.featureDisabled': {\n 'zh-CN': '此构建已关闭“更多”模型来源;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Select Same as current connection or Custom.',\n },\n 'runtime.pi.protocolError': {\n 'zh-CN': '“更多”来源返回了无效的协议数据,请重试或更换来源/API。',\n en: 'The provider under More returned invalid protocol data. Retry or choose another provider/API.',\n },\n 'runtime.pi.lengthTruncated': {\n 'zh-CN': '“更多”回复因达到长度上限而被截断,请增加最大回复 token 数或缩短请求内容。',\n en: 'The response from More was truncated at the length limit. Increase maximum response tokens or shorten the request content.',\n },\n 'runtime.pi.emptyResponse': {\n 'zh-CN': '“更多”来源返回了空回复,请重试或更换模型。',\n en: 'The More source returned an empty response. Retry or choose another model.',\n },\n 'runtime.pi.thinkingOnlyResponse': {\n 'zh-CN': '“更多”回复仅包含 thinking,没有可用的业务内容,请重试或更换模型。',\n en: 'The response from More contained only thinking and no usable content. Retry or choose another model.',\n },\n 'runtime.pi.deferredResponse': {\n 'zh-CN': '“更多”回复尚未完成,当前不支持 deferred 结果。',\n en: 'The response from More is not complete; deferred results are not currently supported.',\n },\n 'runtime.pi.oauth.cancelled': {\n 'zh-CN': '“更多”的 OAuth 操作已取消,原凭据未更改。',\n en: 'The OAuth operation under More was cancelled. The existing credential was not changed.',\n },\n 'runtime.pi.oauth.browserUnavailable': {\n 'zh-CN':\n '当前页面缺少 OAuth 所需的 Fetch 或 Web Crypto 能力,请在支持的安全浏览器环境中重试。',\n en: 'Fetch or Web Crypto required for OAuth is unavailable. Retry in a supported secure browser context.',\n },\n 'runtime.pi.oauth.unsupportedProvider': {\n 'zh-CN': '“更多”中所选来源不支持浏览器 OAuth 登录,请改用 API Key 或其他来源。',\n en: 'The selected provider under More does not support browser OAuth sign-in. Use an API key or another provider.',\n },\n 'runtime.pi.oauth.invalidCallback': {\n 'zh-CN':\n 'OAuth callback URL 无效。请从浏览器地址栏复制完整的 loopback callback URL 后重试。',\n en: 'The OAuth callback URL is invalid. Copy the complete loopback callback URL from the browser address bar and retry.',\n },\n 'runtime.pi.oauth.stateMismatch': {\n 'zh-CN': 'OAuth callback 与当前登录尝试不匹配。请取消当前尝试并重新登录。',\n en: 'The OAuth callback does not match the current sign-in attempt. Cancel it and start a new sign-in.',\n },\n 'runtime.pi.oauth.authorizationFailed': {\n 'zh-CN': 'OAuth 授权未完成或被来源拒绝,请重新发起登录。',\n en: 'OAuth authorization was not completed or was rejected by the provider. Start a new sign-in.',\n },\n 'runtime.pi.oauth.browserNetwork': {\n 'zh-CN':\n '浏览器无法连接 OAuth token 服务。请检查网络,并确认来源允许浏览器 CORS 请求;MVU 不会自动改用代理。',\n en: 'The browser could not reach the OAuth token service. Check the network and confirm that the provider allows browser CORS requests; MVU will not silently use a proxy.',\n },\n 'runtime.pi.oauth.tokenHttp': {\n 'zh-CN': 'OAuth token 交换被来源拒绝。请重新发起登录,不要重用之前的 callback URL。',\n en: 'The provider rejected the OAuth token exchange. Start a new sign-in and do not reuse the previous callback URL.',\n },\n 'runtime.pi.oauth.tokenResponse': {\n 'zh-CN': 'OAuth token 服务返回了无效响应,请稍后重新登录。',\n en: 'The OAuth token service returned an invalid response. Sign in again later.',\n },\n 'runtime.pi.oauth.accountId': {\n 'zh-CN': '无法从 OAuth 凭据中确认账户,请重新登录或选择其他账户。',\n en: 'The account could not be identified from the OAuth credential. Sign in again or choose another account.',\n },\n 'runtime.pi.oauth.attemptExpired': {\n 'zh-CN': 'OAuth 登录尝试已过期,请重新点击登录并使用新的授权链接。',\n en: 'The OAuth sign-in attempt has expired. Start a new sign-in and use the new authorization link.',\n },\n 'runtime.pi.oauth.attemptUsed': {\n 'zh-CN': 'OAuth 登录尝试已结束或 callback URL 已使用,请重新发起登录。',\n en: 'The OAuth sign-in attempt has ended or its callback URL was already used. Start a new sign-in.',\n },\n 'runtime.pi.oauth.credentialStore': {\n 'zh-CN': '无法读取或保存“更多”的 OAuth 凭据,请检查浏览器存储后重试。',\n en: 'The OAuth credential for More could not be read or saved. Check browser storage and retry.',\n },\n\n 'runtime.variableUpdate.unknownCommand': {\n 'zh-CN': '未知命令',\n en: 'unknown command',\n },\n 'runtime.variableUpdate.errorTitle': {\n 'zh-CN': '[MVU]发生变量更新错误,可能需要重 Roll:{command}',\n en: '[MVU] Variable update error; rerolling may help: {command}',\n },\n 'runtime.variableUpdate.errorDetail': {\n 'zh-CN': '错误详情:{detail}',\n en: 'Error details: {detail}',\n },\n 'runtime.variableUpdate.messageOverwrittenTitle': {\n 'zh-CN': '[MVU]消息内容被覆盖',\n en: '[MVU] Message content overwritten',\n },\n 'runtime.variableUpdate.messageOverwritten': {\n 'zh-CN':\n '第 {messageId} 层消息中其他插件的修改被覆盖,可尝试重新运行相关插件对此楼层的解析。',\n en: \"Changes another plugin made to message #{messageId} were overwritten. Try rerunning that plugin's analysis for this message.\",\n },\n 'runtime.variableUpdate.setPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 set 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the set command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.assignPrimitive': {\n 'zh-CN': '路径“{path}”保存的是原始值({type}),无法向其中 assign;已跳过操作。{reason}',\n en: \"Cannot assign into path '{path}' because it holds a primitive value ({type}). The operation was skipped. {reason}\",\n },\n 'runtime.variableUpdate.mergeNonExtensibleObject': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象合并数据。{reason}',\n en: \"SCHEMA VIOLATION: Cannot merge data into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignUnknownKey': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象写入新键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign the new key '{key}' into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的数组写入元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign elements into the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignMissingParent': {\n 'zh-CN': '路径“{path}”不存在,且其父级不可扩展,无法向其中 assign。{reason}',\n en: \"Cannot assign into the non-existent path '{path}' without an extensible parent. {reason}\",\n },\n 'runtime.variableUpdate.mergeArrayIntoObject': {\n 'zh-CN': '无法将数组合并到路径“{path}”处的对象中。',\n en: \"Cannot merge an array into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.mergeNonObjectIntoObject': {\n 'zh-CN': '无法将非对象值合并到路径“{path}”处的对象中。',\n en: \"Cannot merge a non-object value into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.templateResolutionFailed': {\n 'zh-CN': '解析路径“{path}”处的模板元数据失败:{cause}',\n en: \"Failed to resolve template metadata at '{path}': {cause}\",\n },\n 'runtime.variableUpdate.assignInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.assign 参数无效。',\n en: \"Invalid arguments for _.assign at path '{path}'.\",\n },\n 'runtime.variableUpdate.removePathUndefined': {\n 'zh-CN': '_.remove 命令中的路径“{path}”未定义。',\n en: \"Path '{path}' is undefined in the _.remove command.\",\n },\n 'runtime.variableUpdate.deleteTargetUndetermined': {\n 'zh-CN': '无法确定路径“{path}”上的命令要删除的目标。{reason}',\n en: \"Could not determine the deletion target for the command at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removePathMissing': {\n 'zh-CN': '无法从不存在的路径“{path}”中删除内容。{reason}',\n en: \"Cannot remove from the non-existent path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”处不可扩展的数组中删除元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove an element from the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeRequiredKey': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”中删除必需键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove the required key '{key}' from path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonCollection': {\n 'zh-CN': '路径“{path}”处的值不是数组或对象,无法从中删除内容;已跳过命令。{reason}',\n en: \"Cannot remove from path '{path}' because it is not an array or object. The command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.removeExecutionFailed': {\n 'zh-CN': '无法在路径“{path}”上执行 remove。',\n en: \"Failed to execute remove at path '{path}'.\",\n },\n 'runtime.variableUpdate.addPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 add 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.dateDeltaNotNumber': {\n 'zh-CN': '日期操作的增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' for the date operation is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.deltaNotNumber': {\n 'zh-CN': '增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addUnsupportedValue': {\n 'zh-CN': '路径“{path}”处的值不是日期或数字,已跳过 add 命令。{reason}',\n en: \"The value at path '{path}' is not a date or number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.add 参数数量无效。{reason}',\n en: \"Invalid number of arguments for _.add at path '{path}'. {reason}\",\n },\n\n 'runtime.apiProfile.nameRequired': {\n 'zh-CN': 'API 方案名称不能为空',\n en: 'API profile name cannot be empty',\n },\n 'runtime.apiProfile.notFound': {\n 'zh-CN': '未找到 API 方案:{name}',\n en: 'API profile not found: {name}',\n },\n 'runtime.apiProfile.selectOrEnterName': {\n 'zh-CN': '请先输入或选择一个 API 方案名称',\n en: 'Enter or select an API profile name first',\n },\n 'runtime.apiProfile.alreadyExists': {\n 'zh-CN': 'API 方案「{name}」已存在',\n en: 'API profile “{name}” already exists',\n },\n 'runtime.apiProfile.piConfigRequired': {\n 'zh-CN': '“更多”API 方案缺少完整的连接配置,无法保存。',\n en: 'A More-source API profile requires a complete connection snapshot before it can be saved.',\n },\n 'runtime.apiProfile.enterNewName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a name for the new API profile first',\n },\n 'runtime.preset.notSelected': {\n 'zh-CN': '未选择额外模型预设',\n en: 'No extra-model preset was selected',\n },\n 'runtime.preset.apiUnavailable': {\n 'zh-CN': '当前环境未提供 getPreset 接口',\n en: 'The getPreset API is unavailable in the current environment',\n },\n 'runtime.preset.readFailed': {\n 'zh-CN': '无法读取预设“{name}”',\n en: 'Unable to read preset “{name}”',\n },\n\n 'runtime.functionCall.invalidJsonPatch': {\n 'zh-CN': '不是有效的 json patch',\n en: 'Invalid JSON Patch',\n },\n 'runtime.functionCall.updateBlockParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]无法解析变量更新块。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the variable update block. Content: {content}; error: {cause}',\n },\n 'runtime.functionCall.resultParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]函数调用结果解析失败:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the function-call result: {cause}',\n },\n 'runtime.functionCall.formattedOutputParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]格式化输出解析失败。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse formatted output. Content: {content}; error: {cause}',\n },\n});\n","/*!\n * vue-i18n v11.1.12\n * (c) 2025 kazuya kawaguchi\n * Released under the MIT License.\n */\nimport { CORE_ERROR_CODES_EXTEND_POINT, createCompileError, CORE_WARN_CODES_EXTEND_POINT, isMessageAST, AST_NODE_PROPS_KEYS, DEFAULT_LOCALE, updateFallbackLocale, setFallbackContext, createCoreContext, clearDateTimeFormat, clearNumberFormat, setAdditionalMeta, getFallbackContext, NOT_REOSLVED, isTranslateFallbackWarn, isTranslateMissingWarn, parseTranslateArgs, translate, MISSING_RESOLVE_VALUE, parseDateTimeArgs, datetime, parseNumberArgs, number, isMessageFunction, fallbackWithLocaleChain, NUMBER_FORMAT_OPTIONS_KEYS, DATETIME_FORMAT_OPTIONS_KEYS, registerMessageCompiler, compile, registerMessageResolver, resolveValue, registerLocaleFallbacker, setDevToolsHook } from '@intlify/core-base';\nimport { getGlobalThis, makeSymbol, format, isObject, create, isPlainObject, isArray, deepCopy, isString, hasOwn, warn, isBoolean, isRegExp, isFunction, inBrowser, assign, isNumber, createEmitter, warnOnce, isEmptyObject } from '@intlify/shared';\nimport { createVNode, Text, computed, watch, getCurrentInstance, ref, shallowRef, Fragment, defineComponent, h, effectScope, inject, onMounted, onUnmounted, isRef } from 'vue';\nimport { setupDevtoolsPlugin } from '@vue/devtools-api';\n\n/**\n * Vue I18n Version\n *\n * @remarks\n * Semver format. Same format as the package.json `version` field.\n *\n * @VueI18nGeneral\n */\nconst VERSION = '11.1.12';\n/**\n * This is only called in esm-bundler builds.\n * istanbul-ignore-next\n */\nfunction initFeatureFlags() {\n if (typeof __VUE_I18N_FULL_INSTALL__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_FULL_INSTALL__ = true;\n }\n if (typeof __VUE_I18N_LEGACY_API__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_LEGACY_API__ = true;\n }\n if (typeof __INTLIFY_DROP_MESSAGE_COMPILER__ !== 'boolean') {\n getGlobalThis().__INTLIFY_DROP_MESSAGE_COMPILER__ = false;\n }\n if (typeof __INTLIFY_PROD_DEVTOOLS__ !== 'boolean') {\n getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false;\n }\n}\n\nconst I18nErrorCodes = {\n // composer module errors\n UNEXPECTED_RETURN_TYPE: CORE_ERROR_CODES_EXTEND_POINT, // 24\n // legacy module errors\n INVALID_ARGUMENT: 25,\n // i18n module errors\n MUST_BE_CALL_SETUP_TOP: 26,\n NOT_INSTALLED: 27,\n // directive module errors\n REQUIRED_VALUE: 28,\n INVALID_VALUE: 29,\n // vue-devtools errors\n CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: 30,\n NOT_INSTALLED_WITH_PROVIDE: 31,\n // unexpected error\n UNEXPECTED_ERROR: 32,\n // not compatible legacy vue-i18n constructor\n NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,\n // Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly\n NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34\n};\nfunction createI18nError(code, ...args) {\n return createCompileError(code, null, (process.env.NODE_ENV !== 'production') ? { messages: errorMessages, args } : undefined);\n}\nconst errorMessages = {\n [I18nErrorCodes.UNEXPECTED_RETURN_TYPE]: 'Unexpected return type in composer',\n [I18nErrorCodes.INVALID_ARGUMENT]: 'Invalid argument',\n [I18nErrorCodes.MUST_BE_CALL_SETUP_TOP]: 'Must be called at the top of a `setup` function',\n [I18nErrorCodes.NOT_INSTALLED]: 'Need to install with `app.use` function',\n [I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpected error',\n [I18nErrorCodes.REQUIRED_VALUE]: `Required in value: {0}`,\n [I18nErrorCodes.INVALID_VALUE]: `Invalid value`,\n [I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN]: `Cannot setup vue-devtools plugin`,\n [I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE]: 'Need to install with `provide` function',\n [I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]: 'Not compatible legacy VueI18n.',\n [I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY]: 'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly'\n};\n\nconst TranslateVNodeSymbol = \n/* #__PURE__*/ makeSymbol('__translateVNode');\nconst DatetimePartsSymbol = /* #__PURE__*/ makeSymbol('__datetimeParts');\nconst NumberPartsSymbol = /* #__PURE__*/ makeSymbol('__numberParts');\nconst EnableEmitter = /* #__PURE__*/ makeSymbol('__enableEmitter');\nconst DisableEmitter = /* #__PURE__*/ makeSymbol('__disableEmitter');\nconst SetPluralRulesSymbol = makeSymbol('__setPluralRules');\nmakeSymbol('__intlifyMeta');\nconst InejctWithOptionSymbol = \n/* #__PURE__*/ makeSymbol('__injectWithOption');\nconst DisposeSymbol = /* #__PURE__*/ makeSymbol('__dispose');\n\nconst I18nWarnCodes = {\n FALLBACK_TO_ROOT: CORE_WARN_CODES_EXTEND_POINT, // 8\n NOT_FOUND_PARENT_SCOPE: 9,\n IGNORE_OBJ_FLATTEN: 10,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_LEGACY_MODE: 11,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12,\n // duplicate `useI18n` calling\n DUPLICATE_USE_I18N_CALLING: 13\n};\nconst warnMessages = {\n [I18nWarnCodes.FALLBACK_TO_ROOT]: `Fall back to {type} '{key}' with root locale.`,\n [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.`,\n [I18nWarnCodes.IGNORE_OBJ_FLATTEN]: `Ignore object flatten: '{key}' key has an string value`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_LEGACY_MODE]: `Legacy API mode has been deprecated in v11. Use Composition API mode instead.\\nAbout how to use the Composition API mode, see https://vue-i18n.intlify.dev/guide/advanced/composition.html`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.`,\n [I18nWarnCodes.DUPLICATE_USE_I18N_CALLING]: \"Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component.\"\n};\nfunction getWarnMessage(code, ...args) {\n return format(warnMessages[code], ...args);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Transform flat json in obj to normal json in obj\n */\nfunction handleFlatJson(obj) {\n // check obj\n if (!isObject(obj)) {\n return obj;\n }\n if (isMessageAST(obj)) {\n return obj;\n }\n for (const key in obj) {\n // check key\n if (!hasOwn(obj, key)) {\n continue;\n }\n // handle for normal json\n if (!key.includes('.')) {\n // recursive process value if value is also a object\n if (isObject(obj[key])) {\n handleFlatJson(obj[key]);\n }\n }\n // handle for flat json, transform to normal json\n else {\n // go to the last object\n const subKeys = key.split('.');\n const lastIndex = subKeys.length - 1;\n let currentObj = obj;\n let hasStringValue = false;\n for (let i = 0; i < lastIndex; i++) {\n if (subKeys[i] === '__proto__') {\n throw new Error(`unsafe key: ${subKeys[i]}`);\n }\n if (!(subKeys[i] in currentObj)) {\n currentObj[subKeys[i]] = create();\n }\n if (!isObject(currentObj[subKeys[i]])) {\n (process.env.NODE_ENV !== 'production') &&\n warn(getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, {\n key: subKeys[i]\n }));\n hasStringValue = true;\n break;\n }\n currentObj = currentObj[subKeys[i]];\n }\n // update last object value, delete old property\n if (!hasStringValue) {\n if (!isMessageAST(currentObj)) {\n currentObj[subKeys[lastIndex]] = obj[key];\n delete obj[key];\n }\n else {\n /**\n * NOTE:\n * if the last object is a message AST and subKeys[lastIndex] has message AST prop key, ignore to copy and key deletion\n */\n if (!AST_NODE_PROPS_KEYS.includes(subKeys[lastIndex])) {\n delete obj[key];\n }\n }\n }\n // recursive process value if value is also a object\n if (!isMessageAST(currentObj)) {\n const target = currentObj[subKeys[lastIndex]];\n if (isObject(target)) {\n handleFlatJson(target);\n }\n }\n }\n }\n return obj;\n}\nfunction getLocaleMessages(locale, options) {\n const { messages, __i18n, messageResolver, flatJson } = options;\n // prettier-ignore\n const ret = (isPlainObject(messages)\n ? messages\n : isArray(__i18n)\n ? create()\n : { [locale]: create() });\n // merge locale messages of i18n custom block\n if (isArray(__i18n)) {\n __i18n.forEach(custom => {\n if ('locale' in custom && 'resource' in custom) {\n const { locale, resource } = custom;\n if (locale) {\n ret[locale] = ret[locale] || create();\n deepCopy(resource, ret[locale]);\n }\n else {\n deepCopy(resource, ret);\n }\n }\n else {\n isString(custom) && deepCopy(JSON.parse(custom), ret);\n }\n });\n }\n // handle messages for flat json\n if (messageResolver == null && flatJson) {\n for (const key in ret) {\n if (hasOwn(ret, key)) {\n handleFlatJson(ret[key]);\n }\n }\n }\n return ret;\n}\nfunction getComponentOptions(instance) {\n return instance.type;\n}\nfunction adjustI18nResources(gl, options, componentOptions) {\n // prettier-ignore\n let messages = isObject(options.messages)\n ? options.messages\n : create();\n if ('__i18nGlobal' in componentOptions) {\n messages = getLocaleMessages(gl.locale.value, {\n messages,\n __i18n: componentOptions.__i18nGlobal\n });\n }\n // merge locale messages\n const locales = Object.keys(messages);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeLocaleMessage(locale, messages[locale]);\n });\n }\n {\n // merge datetime formats\n if (isObject(options.datetimeFormats)) {\n const locales = Object.keys(options.datetimeFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeDateTimeFormat(locale, options.datetimeFormats[locale]);\n });\n }\n }\n // merge number formats\n if (isObject(options.numberFormats)) {\n const locales = Object.keys(options.numberFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeNumberFormat(locale, options.numberFormats[locale]);\n });\n }\n }\n }\n}\nfunction createTextNode(key) {\n return createVNode(Text, null, key, 0);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// extend VNode interface\nconst DEVTOOLS_META = '__INTLIFY_META__';\nconst NOOP_RETURN_ARRAY = () => [];\nconst NOOP_RETURN_FALSE = () => false;\nlet composerID = 0;\nfunction defineCoreMissingHandler(missing) {\n return ((ctx, locale, key, type) => {\n return missing(locale, key, getCurrentInstance() || undefined, type);\n });\n}\n// for Intlify DevTools\n/* #__NO_SIDE_EFFECTS__ */\nconst getMetaInfo = () => {\n const instance = getCurrentInstance();\n let meta = null;\n return instance && (meta = getComponentOptions(instance)[DEVTOOLS_META])\n ? { [DEVTOOLS_META]: meta }\n : null;\n};\n/**\n * Create composer interface factory\n *\n * @internal\n */\nfunction createComposer(options = {}) {\n const { __root, __injectWithOption } = options;\n const _isGlobal = __root === undefined;\n const flatJson = options.flatJson;\n const _ref = inBrowser ? ref : shallowRef;\n let _inheritLocale = isBoolean(options.inheritLocale)\n ? options.inheritLocale\n : true;\n const _locale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.locale.value\n : isString(options.locale)\n ? options.locale\n : DEFAULT_LOCALE);\n const _fallbackLocale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.fallbackLocale.value\n : isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : _locale.value);\n const _messages = _ref(getLocaleMessages(_locale.value, options));\n // prettier-ignore\n const _datetimeFormats = _ref(isPlainObject(options.datetimeFormats)\n ? options.datetimeFormats\n : { [_locale.value]: {} })\n ;\n // prettier-ignore\n const _numberFormats = _ref(isPlainObject(options.numberFormats)\n ? options.numberFormats\n : { [_locale.value]: {} })\n ;\n // warning suppress options\n // prettier-ignore\n let _missingWarn = __root\n ? __root.missingWarn\n : isBoolean(options.missingWarn) || isRegExp(options.missingWarn)\n ? options.missingWarn\n : true;\n // prettier-ignore\n let _fallbackWarn = __root\n ? __root.fallbackWarn\n : isBoolean(options.fallbackWarn) || isRegExp(options.fallbackWarn)\n ? options.fallbackWarn\n : true;\n // prettier-ignore\n let _fallbackRoot = __root\n ? __root.fallbackRoot\n : isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n // configure fall back to root\n let _fallbackFormat = !!options.fallbackFormat;\n // runtime missing\n let _missing = isFunction(options.missing) ? options.missing : null;\n let _runtimeMissing = isFunction(options.missing)\n ? defineCoreMissingHandler(options.missing)\n : null;\n // postTranslation handler\n let _postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : null;\n // prettier-ignore\n let _warnHtmlMessage = __root\n ? __root.warnHtmlMessage\n : isBoolean(options.warnHtmlMessage)\n ? options.warnHtmlMessage\n : true;\n let _escapeParameter = !!options.escapeParameter;\n // custom linked modifiers\n // prettier-ignore\n const _modifiers = __root\n ? __root.modifiers\n : isPlainObject(options.modifiers)\n ? options.modifiers\n : {};\n // pluralRules\n let _pluralRules = options.pluralRules || (__root && __root.pluralRules);\n // runtime context\n // eslint-disable-next-line prefer-const\n let _context;\n const getCoreContext = () => {\n _isGlobal && setFallbackContext(null);\n const ctxOptions = {\n version: VERSION,\n locale: _locale.value,\n fallbackLocale: _fallbackLocale.value,\n messages: _messages.value,\n modifiers: _modifiers,\n pluralRules: _pluralRules,\n missing: _runtimeMissing === null ? undefined : _runtimeMissing,\n missingWarn: _missingWarn,\n fallbackWarn: _fallbackWarn,\n fallbackFormat: _fallbackFormat,\n unresolving: true,\n postTranslation: _postTranslation === null ? undefined : _postTranslation,\n warnHtmlMessage: _warnHtmlMessage,\n escapeParameter: _escapeParameter,\n messageResolver: options.messageResolver,\n messageCompiler: options.messageCompiler,\n __meta: { framework: 'vue' }\n };\n {\n ctxOptions.datetimeFormats = _datetimeFormats.value;\n ctxOptions.numberFormats = _numberFormats.value;\n ctxOptions.__datetimeFormatters = isPlainObject(_context)\n ? _context.__datetimeFormatters\n : undefined;\n ctxOptions.__numberFormatters = isPlainObject(_context)\n ? _context.__numberFormatters\n : undefined;\n }\n if ((process.env.NODE_ENV !== 'production')) {\n ctxOptions.__v_emitter = isPlainObject(_context)\n ? _context.__v_emitter\n : undefined;\n }\n const ctx = createCoreContext(ctxOptions);\n _isGlobal && setFallbackContext(ctx);\n return ctx;\n };\n _context = getCoreContext();\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n // track reactivity\n function trackReactivityValues() {\n return [\n _locale.value,\n _fallbackLocale.value,\n _messages.value,\n _datetimeFormats.value,\n _numberFormats.value\n ]\n ;\n }\n // locale\n const locale = computed({\n get: () => _locale.value,\n set: val => {\n _context.locale = val;\n _locale.value = val;\n }\n });\n // fallbackLocale\n const fallbackLocale = computed({\n get: () => _fallbackLocale.value,\n set: val => {\n _context.fallbackLocale = val;\n _fallbackLocale.value = val;\n updateFallbackLocale(_context, _locale.value, val);\n }\n });\n // messages\n const messages = computed(() => _messages.value);\n // datetimeFormats\n const datetimeFormats = /* #__PURE__*/ computed(() => _datetimeFormats.value);\n // numberFormats\n const numberFormats = /* #__PURE__*/ computed(() => _numberFormats.value);\n // getPostTranslationHandler\n function getPostTranslationHandler() {\n return isFunction(_postTranslation) ? _postTranslation : null;\n }\n // setPostTranslationHandler\n function setPostTranslationHandler(handler) {\n _postTranslation = handler;\n _context.postTranslation = handler;\n }\n // getMissingHandler\n function getMissingHandler() {\n return _missing;\n }\n // setMissingHandler\n function setMissingHandler(handler) {\n if (handler !== null) {\n _runtimeMissing = defineCoreMissingHandler(handler);\n }\n _missing = handler;\n _context.missing = _runtimeMissing;\n }\n function isResolvedTranslateMessage(type, arg) {\n return type !== 'translate' || !arg.resolvedMessage;\n }\n const wrapWithDeps = (fn, argumentParser, warnType, fallbackSuccess, fallbackFail, successCondition) => {\n trackReactivityValues(); // track reactive dependency\n // NOTE: experimental !!\n let ret;\n try {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(getMetaInfo());\n }\n if (!_isGlobal) {\n _context.fallbackContext = __root\n ? getFallbackContext()\n : undefined;\n }\n ret = fn(_context);\n }\n finally {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(null);\n }\n if (!_isGlobal) {\n _context.fallbackContext = undefined;\n }\n }\n if ((warnType !== 'translate exists' && // for not `te` (e.g `t`)\n isNumber(ret) &&\n ret === NOT_REOSLVED) ||\n (warnType === 'translate exists' && !ret) // for `te`\n ) {\n const [key, arg2] = argumentParser();\n if ((process.env.NODE_ENV !== 'production') &&\n __root &&\n isString(key) &&\n isResolvedTranslateMessage(warnType, arg2)) {\n if (_fallbackRoot &&\n (isTranslateFallbackWarn(_fallbackWarn, key) ||\n isTranslateMissingWarn(_missingWarn, key))) {\n warn(getWarnMessage(I18nWarnCodes.FALLBACK_TO_ROOT, {\n key,\n type: warnType\n }));\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n const { __v_emitter: emitter } = _context;\n if (emitter && _fallbackRoot) {\n emitter.emit('fallback', {\n type: warnType,\n key,\n to: 'global',\n groupId: `${warnType}:${key}`\n });\n }\n }\n }\n return __root && _fallbackRoot\n ? fallbackSuccess(__root)\n : fallbackFail(key);\n }\n else if (successCondition(ret)) {\n return ret;\n }\n else {\n /* istanbul ignore next */\n throw createI18nError(I18nErrorCodes.UNEXPECTED_RETURN_TYPE);\n }\n };\n // t\n function t(...args) {\n return wrapWithDeps(context => Reflect.apply(translate, null, [context, ...args]), () => parseTranslateArgs(...args), 'translate', root => Reflect.apply(root.t, root, [...args]), key => key, val => isString(val));\n }\n // rt\n function rt(...args) {\n const [arg1, arg2, arg3] = args;\n if (arg3 && !isObject(arg3)) {\n throw createI18nError(I18nErrorCodes.INVALID_ARGUMENT);\n }\n return t(...[arg1, arg2, assign({ resolvedMessage: true }, arg3 || {})]);\n }\n // d\n function d(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => Reflect.apply(root.d, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // n\n function n(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => Reflect.apply(root.n, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // for custom processor\n function normalize(values) {\n return values.map(val => isString(val) || isNumber(val) || isBoolean(val)\n ? createTextNode(String(val))\n : val);\n }\n const interpolate = (val) => val;\n const processor = {\n normalize,\n interpolate,\n type: 'vnode'\n };\n // translateVNode, using for `i18n-t` component\n function translateVNode(...args) {\n return wrapWithDeps(context => {\n let ret;\n const _context = context;\n try {\n _context.processor = processor;\n ret = Reflect.apply(translate, null, [_context, ...args]);\n }\n finally {\n _context.processor = null;\n }\n return ret;\n }, () => parseTranslateArgs(...args), 'translate', root => root[TranslateVNodeSymbol](...args), key => [createTextNode(key)], val => isArray(val));\n }\n // numberParts, using for `i18n-n` component\n function numberParts(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => root[NumberPartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n // datetimeParts, using for `i18n-d` component\n function datetimeParts(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => root[DatetimePartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n function setPluralRules(rules) {\n _pluralRules = rules;\n _context.pluralRules = _pluralRules;\n }\n // te\n function te(key, locale) {\n return wrapWithDeps(() => {\n if (!key) {\n return false;\n }\n const targetLocale = isString(locale) ? locale : _locale.value;\n const message = getLocaleMessage(targetLocale);\n const resolved = _context.messageResolver(message, key);\n return (isMessageAST(resolved) ||\n isMessageFunction(resolved) ||\n isString(resolved));\n }, () => [key], 'translate exists', root => {\n return Reflect.apply(root.te, root, [key, locale]);\n }, NOOP_RETURN_FALSE, val => isBoolean(val));\n }\n function resolveMessages(key) {\n let messages = null;\n const locales = fallbackWithLocaleChain(_context, _fallbackLocale.value, _locale.value);\n for (let i = 0; i < locales.length; i++) {\n const targetLocaleMessages = _messages.value[locales[i]] || {};\n const messageValue = _context.messageResolver(targetLocaleMessages, key);\n if (messageValue != null) {\n messages = messageValue;\n break;\n }\n }\n return messages;\n }\n // tm\n function tm(key) {\n const messages = resolveMessages(key);\n // prettier-ignore\n return messages != null\n ? messages\n : __root\n ? __root.tm(key) || {}\n : {};\n }\n // getLocaleMessage\n function getLocaleMessage(locale) {\n return (_messages.value[locale] || {});\n }\n // setLocaleMessage\n function setLocaleMessage(locale, message) {\n if (flatJson) {\n const _message = { [locale]: message };\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n message = _message[locale];\n }\n _messages.value[locale] = message;\n _context.messages = _messages.value;\n }\n // mergeLocaleMessage\n function mergeLocaleMessage(locale, message) {\n _messages.value[locale] = _messages.value[locale] || {};\n const _message = { [locale]: message };\n if (flatJson) {\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n }\n message = _message[locale];\n deepCopy(message, _messages.value[locale]);\n _context.messages = _messages.value;\n }\n // getDateTimeFormat\n function getDateTimeFormat(locale) {\n return _datetimeFormats.value[locale] || {};\n }\n // setDateTimeFormat\n function setDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = format;\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // mergeDateTimeFormat\n function mergeDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = assign(_datetimeFormats.value[locale] || {}, format);\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // getNumberFormat\n function getNumberFormat(locale) {\n return _numberFormats.value[locale] || {};\n }\n // setNumberFormat\n function setNumberFormat(locale, format) {\n _numberFormats.value[locale] = format;\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // mergeNumberFormat\n function mergeNumberFormat(locale, format) {\n _numberFormats.value[locale] = assign(_numberFormats.value[locale] || {}, format);\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // for debug\n composerID++;\n // watch root locale & fallbackLocale\n if (__root && inBrowser) {\n watch(__root.locale, (val) => {\n if (_inheritLocale) {\n _locale.value = val;\n _context.locale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n watch(__root.fallbackLocale, (val) => {\n if (_inheritLocale) {\n _fallbackLocale.value = val;\n _context.fallbackLocale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n }\n // define basic composition API!\n const composer = {\n id: composerID,\n locale,\n fallbackLocale,\n get inheritLocale() {\n return _inheritLocale;\n },\n set inheritLocale(val) {\n _inheritLocale = val;\n if (val && __root) {\n _locale.value = __root.locale.value;\n _fallbackLocale.value = __root.fallbackLocale.value;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n },\n get availableLocales() {\n return Object.keys(_messages.value).sort();\n },\n messages,\n get modifiers() {\n return _modifiers;\n },\n get pluralRules() {\n return _pluralRules || {};\n },\n get isGlobal() {\n return _isGlobal;\n },\n get missingWarn() {\n return _missingWarn;\n },\n set missingWarn(val) {\n _missingWarn = val;\n _context.missingWarn = _missingWarn;\n },\n get fallbackWarn() {\n return _fallbackWarn;\n },\n set fallbackWarn(val) {\n _fallbackWarn = val;\n _context.fallbackWarn = _fallbackWarn;\n },\n get fallbackRoot() {\n return _fallbackRoot;\n },\n set fallbackRoot(val) {\n _fallbackRoot = val;\n },\n get fallbackFormat() {\n return _fallbackFormat;\n },\n set fallbackFormat(val) {\n _fallbackFormat = val;\n _context.fallbackFormat = _fallbackFormat;\n },\n get warnHtmlMessage() {\n return _warnHtmlMessage;\n },\n set warnHtmlMessage(val) {\n _warnHtmlMessage = val;\n _context.warnHtmlMessage = val;\n },\n get escapeParameter() {\n return _escapeParameter;\n },\n set escapeParameter(val) {\n _escapeParameter = val;\n _context.escapeParameter = val;\n },\n t,\n getLocaleMessage,\n setLocaleMessage,\n mergeLocaleMessage,\n getPostTranslationHandler,\n setPostTranslationHandler,\n getMissingHandler,\n setMissingHandler,\n [SetPluralRulesSymbol]: setPluralRules\n };\n {\n composer.datetimeFormats = datetimeFormats;\n composer.numberFormats = numberFormats;\n composer.rt = rt;\n composer.te = te;\n composer.tm = tm;\n composer.d = d;\n composer.n = n;\n composer.getDateTimeFormat = getDateTimeFormat;\n composer.setDateTimeFormat = setDateTimeFormat;\n composer.mergeDateTimeFormat = mergeDateTimeFormat;\n composer.getNumberFormat = getNumberFormat;\n composer.setNumberFormat = setNumberFormat;\n composer.mergeNumberFormat = mergeNumberFormat;\n composer[InejctWithOptionSymbol] = __injectWithOption;\n composer[TranslateVNodeSymbol] = translateVNode;\n composer[DatetimePartsSymbol] = datetimeParts;\n composer[NumberPartsSymbol] = numberParts;\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n composer[EnableEmitter] = (emitter) => {\n _context.__v_emitter = emitter;\n };\n composer[DisableEmitter] = () => {\n _context.__v_emitter = undefined;\n };\n }\n return composer;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst VUE_I18N_COMPONENT_TYPES = 'vue-i18n: composer properties';\nconst VueDevToolsLabels = {\n 'vue-devtools-plugin-vue-i18n': 'Vue I18n DevTools',\n 'vue-i18n-resource-inspector': 'Vue I18n DevTools',\n 'vue-i18n-timeline': 'Vue I18n'\n};\nconst VueDevToolsPlaceholders = {\n 'vue-i18n-resource-inspector': 'Search for scopes ...'\n};\nconst VueDevToolsTimelineColors = {\n 'vue-i18n-timeline': 0xffcd19\n};\nlet devtoolsApi;\nasync function enableDevTools(app, i18n) {\n return new Promise((resolve, reject) => {\n try {\n setupDevtoolsPlugin({\n id: 'vue-devtools-plugin-vue-i18n',\n label: VueDevToolsLabels['vue-devtools-plugin-vue-i18n'],\n packageName: 'vue-i18n',\n homepage: 'https://vue-i18n.intlify.dev',\n logo: 'https://vue-i18n.intlify.dev/vue-i18n-devtools-logo.png',\n componentStateTypes: [VUE_I18N_COMPONENT_TYPES],\n app: app // eslint-disable-line @typescript-eslint/no-explicit-any\n }, api => {\n devtoolsApi = api;\n api.on.visitComponentTree(({ componentInstance, treeNode }) => {\n updateComponentTreeTags(componentInstance, treeNode, i18n);\n });\n api.on.inspectComponent(({ componentInstance, instanceData }) => {\n if (componentInstance.vnode.el &&\n componentInstance.vnode.el.__VUE_I18N__ &&\n instanceData) {\n if (i18n.mode === 'legacy') {\n // ignore global scope on legacy mode\n if (componentInstance.vnode.el.__VUE_I18N__ !==\n i18n.global.__composer) {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n else {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n });\n api.addInspector({\n id: 'vue-i18n-resource-inspector',\n label: VueDevToolsLabels['vue-i18n-resource-inspector'],\n icon: 'language',\n treeFilterPlaceholder: VueDevToolsPlaceholders['vue-i18n-resource-inspector']\n });\n api.on.getInspectorTree(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n registerScope(payload, i18n);\n }\n });\n const roots = new Map();\n api.on.getInspectorState(async (payload) => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n api.unhighlightElement();\n inspectScope(payload, i18n);\n if (payload.nodeId === 'global') {\n if (!roots.has(payload.app)) {\n const [root] = await api.getComponentInstances(payload.app);\n roots.set(payload.app, root);\n }\n api.highlightElement(roots.get(payload.app));\n }\n else {\n const instance = getComponentInstance(payload.nodeId, i18n);\n instance && api.highlightElement(instance);\n }\n }\n });\n api.on.editInspectorState(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n editScope(payload, i18n);\n }\n });\n api.addTimelineLayer({\n id: 'vue-i18n-timeline',\n label: VueDevToolsLabels['vue-i18n-timeline'],\n color: VueDevToolsTimelineColors['vue-i18n-timeline']\n });\n resolve(true);\n });\n }\n catch (e) {\n console.error(e);\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n reject(false);\n }\n });\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getI18nScopeLable(instance) {\n return (instance.type.name ||\n instance.type.displayName ||\n instance.type.__file ||\n 'Anonymous');\n}\nfunction updateComponentTreeTags(instance, // eslint-disable-line @typescript-eslint/no-explicit-any\ntreeNode, i18n) {\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n if (instance && instance.vnode.el && instance.vnode.el.__VUE_I18N__) {\n // add custom tags local scope only\n if (instance.vnode.el.__VUE_I18N__ !== global) {\n const tag = {\n label: `i18n (${getI18nScopeLable(instance)} Scope)`,\n textColor: 0x000000,\n backgroundColor: 0xffcd19\n };\n treeNode.tags.push(tag);\n }\n }\n}\nfunction inspectComposer(instanceData, composer) {\n const type = VUE_I18N_COMPONENT_TYPES;\n instanceData.state.push({\n type,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n });\n instanceData.state.push({\n type,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n });\n instanceData.state.push({\n type,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n });\n instanceData.state.push({\n type,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n });\n instanceData.state.push({\n type,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n });\n {\n instanceData.state.push({\n type,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n });\n instanceData.state.push({\n type,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n });\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getLocaleMessageValue(messages) {\n const value = {};\n Object.keys(messages).forEach((key) => {\n const v = messages[key];\n if (isFunction(v) && 'source' in v) {\n value[key] = getMessageFunctionDetails(v);\n }\n else if (isMessageAST(v) && v.loc && v.loc.source) {\n value[key] = v.loc.source;\n }\n else if (isObject(v)) {\n value[key] = getLocaleMessageValue(v);\n }\n else {\n value[key] = v;\n }\n });\n return value;\n}\nconst ESC = {\n '<': '<',\n '>': '>',\n '\"': '"',\n '&': '&'\n};\nfunction escape(s) {\n return s.replace(/[<>\"&]/g, escapeChar);\n}\nfunction escapeChar(a) {\n return ESC[a] || a;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getMessageFunctionDetails(func) {\n const argString = func.source ? `(\"${escape(func.source)}\")` : `(?)`;\n return {\n _custom: {\n type: 'function',\n display: `ƒ ${argString}`\n }\n };\n}\nfunction registerScope(payload, i18n) {\n payload.rootNodes.push({\n id: 'global',\n label: 'Global Scope'\n });\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n for (const [keyInstance, instance] of i18n.__instances) {\n // prettier-ignore\n const composer = i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n if (global === composer) {\n continue;\n }\n payload.rootNodes.push({\n id: composer.id.toString(),\n label: `${getI18nScopeLable(keyInstance)} Scope`\n });\n }\n}\nfunction getComponentInstance(nodeId, i18n) {\n let instance = null;\n if (nodeId !== 'global') {\n for (const [component, composer] of i18n.__instances.entries()) {\n if (composer.id.toString() === nodeId) {\n instance = component;\n break;\n }\n }\n }\n return instance;\n}\nfunction getComposer$2(nodeId, i18n) {\n if (nodeId === 'global') {\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n }\n else {\n const instance = Array.from(i18n.__instances.values()).find(item => item.id.toString() === nodeId);\n if (instance) {\n return i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n }\n else {\n return null;\n }\n }\n}\nfunction inspectScope(payload, i18n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n // TODO:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n payload.state = makeScopeInspectState(composer);\n }\n return null;\n}\nfunction makeScopeInspectState(composer) {\n const state = {};\n const localeType = 'Locale related info';\n const localeStates = [\n {\n type: localeType,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n },\n {\n type: localeType,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n },\n {\n type: localeType,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n },\n {\n type: localeType,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n }\n ];\n state[localeType] = localeStates;\n const localeMessagesType = 'Locale messages info';\n const localeMessagesStates = [\n {\n type: localeMessagesType,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n }\n ];\n state[localeMessagesType] = localeMessagesStates;\n {\n const datetimeFormatsType = 'Datetime formats info';\n const datetimeFormatsStates = [\n {\n type: datetimeFormatsType,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n }\n ];\n state[datetimeFormatsType] = datetimeFormatsStates;\n const numberFormatsType = 'Datetime formats info';\n const numberFormatsStates = [\n {\n type: numberFormatsType,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n }\n ];\n state[numberFormatsType] = numberFormatsStates;\n }\n return state;\n}\nfunction addTimelineEvent(event, payload) {\n if (devtoolsApi) {\n let groupId;\n if (payload && 'groupId' in payload) {\n groupId = payload.groupId;\n delete payload.groupId;\n }\n devtoolsApi.addTimelineEvent({\n layerId: 'vue-i18n-timeline',\n event: {\n title: event,\n groupId,\n time: Date.now(),\n meta: {},\n data: payload || {},\n logType: event === 'compile-error'\n ? 'error'\n : event === 'fallback' || event === 'missing'\n ? 'warning'\n : 'default'\n }\n });\n }\n}\nfunction editScope(payload, i18n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n const [field] = payload.path;\n if (field === 'locale' && isString(payload.state.value)) {\n composer.locale.value = payload.state.value;\n }\n else if (field === 'fallbackLocale' &&\n (isString(payload.state.value) ||\n isArray(payload.state.value) ||\n isObject(payload.state.value))) {\n composer.fallbackLocale.value = payload.state.value;\n }\n else if (field === 'inheritLocale' && isBoolean(payload.state.value)) {\n composer.inheritLocale = payload.state.value;\n }\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Convert to I18n Composer Options from VueI18n Options\n *\n * @internal\n */\nfunction convertComposerOptions(options) {\n const locale = isString(options.locale) ? options.locale : DEFAULT_LOCALE;\n const fallbackLocale = isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : locale;\n const missing = isFunction(options.missing) ? options.missing : undefined;\n const missingWarn = isBoolean(options.silentTranslationWarn) ||\n isRegExp(options.silentTranslationWarn)\n ? !options.silentTranslationWarn\n : true;\n const fallbackWarn = isBoolean(options.silentFallbackWarn) ||\n isRegExp(options.silentFallbackWarn)\n ? !options.silentFallbackWarn\n : true;\n const fallbackRoot = isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n const fallbackFormat = !!options.formatFallbackMessages;\n const modifiers = isPlainObject(options.modifiers) ? options.modifiers : {};\n const pluralizationRules = options.pluralizationRules;\n const postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : undefined;\n const warnHtmlMessage = isString(options.warnHtmlInMessage)\n ? options.warnHtmlInMessage !== 'off'\n : true;\n const escapeParameter = !!options.escapeParameterHtml;\n const inheritLocale = isBoolean(options.sync) ? options.sync : true;\n let messages = options.messages;\n if (isPlainObject(options.sharedMessages)) {\n const sharedMessages = options.sharedMessages;\n const locales = Object.keys(sharedMessages);\n messages = locales.reduce((messages, locale) => {\n const message = messages[locale] || (messages[locale] = {});\n assign(message, sharedMessages[locale]);\n return messages;\n }, (messages || {}));\n }\n const { __i18n, __root, __injectWithOption } = options;\n const datetimeFormats = options.datetimeFormats;\n const numberFormats = options.numberFormats;\n const flatJson = options.flatJson;\n return {\n locale,\n fallbackLocale,\n messages,\n flatJson,\n datetimeFormats,\n numberFormats,\n missing,\n missingWarn,\n fallbackWarn,\n fallbackRoot,\n fallbackFormat,\n modifiers,\n pluralRules: pluralizationRules,\n postTranslation,\n warnHtmlMessage,\n escapeParameter,\n messageResolver: options.messageResolver,\n inheritLocale,\n __i18n,\n __root,\n __injectWithOption\n };\n}\n/**\n * create VueI18n interface factory\n *\n * @internal\n *\n * @deprecated will be removed at vue-i18n v12\n */\nfunction createVueI18n(options = {}) {\n const composer = createComposer(convertComposerOptions(options));\n const { __extender } = options;\n // defines VueI18n\n const vueI18n = {\n // id\n id: composer.id,\n // locale\n get locale() {\n return composer.locale.value;\n },\n set locale(val) {\n composer.locale.value = val;\n },\n // fallbackLocale\n get fallbackLocale() {\n return composer.fallbackLocale.value;\n },\n set fallbackLocale(val) {\n composer.fallbackLocale.value = val;\n },\n // messages\n get messages() {\n return composer.messages.value;\n },\n // datetimeFormats\n get datetimeFormats() {\n return composer.datetimeFormats.value;\n },\n // numberFormats\n get numberFormats() {\n return composer.numberFormats.value;\n },\n // availableLocales\n get availableLocales() {\n return composer.availableLocales;\n },\n // missing\n get missing() {\n return composer.getMissingHandler();\n },\n set missing(handler) {\n composer.setMissingHandler(handler);\n },\n // silentTranslationWarn\n get silentTranslationWarn() {\n return isBoolean(composer.missingWarn)\n ? !composer.missingWarn\n : composer.missingWarn;\n },\n set silentTranslationWarn(val) {\n composer.missingWarn = isBoolean(val) ? !val : val;\n },\n // silentFallbackWarn\n get silentFallbackWarn() {\n return isBoolean(composer.fallbackWarn)\n ? !composer.fallbackWarn\n : composer.fallbackWarn;\n },\n set silentFallbackWarn(val) {\n composer.fallbackWarn = isBoolean(val) ? !val : val;\n },\n // modifiers\n get modifiers() {\n return composer.modifiers;\n },\n // formatFallbackMessages\n get formatFallbackMessages() {\n return composer.fallbackFormat;\n },\n set formatFallbackMessages(val) {\n composer.fallbackFormat = val;\n },\n // postTranslation\n get postTranslation() {\n return composer.getPostTranslationHandler();\n },\n set postTranslation(handler) {\n composer.setPostTranslationHandler(handler);\n },\n // sync\n get sync() {\n return composer.inheritLocale;\n },\n set sync(val) {\n composer.inheritLocale = val;\n },\n // warnInHtmlMessage\n get warnHtmlInMessage() {\n return composer.warnHtmlMessage ? 'warn' : 'off';\n },\n set warnHtmlInMessage(val) {\n composer.warnHtmlMessage = val !== 'off';\n },\n // escapeParameterHtml\n get escapeParameterHtml() {\n return composer.escapeParameter;\n },\n set escapeParameterHtml(val) {\n composer.escapeParameter = val;\n },\n // pluralizationRules\n get pluralizationRules() {\n return composer.pluralRules || {};\n },\n // for internal\n __composer: composer,\n // t\n t(...args) {\n return Reflect.apply(composer.t, composer, [...args]);\n },\n // rt\n rt(...args) {\n return Reflect.apply(composer.rt, composer, [...args]);\n },\n // te\n te(key, locale) {\n return composer.te(key, locale);\n },\n // tm\n tm(key) {\n return composer.tm(key);\n },\n // getLocaleMessage\n getLocaleMessage(locale) {\n return composer.getLocaleMessage(locale);\n },\n // setLocaleMessage\n setLocaleMessage(locale, message) {\n composer.setLocaleMessage(locale, message);\n },\n // mergeLocaleMessage\n mergeLocaleMessage(locale, message) {\n composer.mergeLocaleMessage(locale, message);\n },\n // d\n d(...args) {\n return Reflect.apply(composer.d, composer, [...args]);\n },\n // getDateTimeFormat\n getDateTimeFormat(locale) {\n return composer.getDateTimeFormat(locale);\n },\n // setDateTimeFormat\n setDateTimeFormat(locale, format) {\n composer.setDateTimeFormat(locale, format);\n },\n // mergeDateTimeFormat\n mergeDateTimeFormat(locale, format) {\n composer.mergeDateTimeFormat(locale, format);\n },\n // n\n n(...args) {\n return Reflect.apply(composer.n, composer, [...args]);\n },\n // getNumberFormat\n getNumberFormat(locale) {\n return composer.getNumberFormat(locale);\n },\n // setNumberFormat\n setNumberFormat(locale, format) {\n composer.setNumberFormat(locale, format);\n },\n // mergeNumberFormat\n mergeNumberFormat(locale, format) {\n composer.mergeNumberFormat(locale, format);\n }\n };\n vueI18n.__extender = __extender;\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n vueI18n.__enableEmitter = (emitter) => {\n const __composer = composer;\n __composer[EnableEmitter] && __composer[EnableEmitter](emitter);\n };\n vueI18n.__disableEmitter = () => {\n const __composer = composer;\n __composer[DisableEmitter] && __composer[DisableEmitter]();\n };\n }\n return vueI18n;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Supports compatibility for legacy vue-i18n APIs\n * This mixin is used when we use vue-i18n@v9.x or later\n */\nfunction defineMixin(vuei18n, composer, i18n) {\n return {\n beforeCreate() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const options = this.$options;\n if (options.i18n) {\n const optionsI18n = options.i18n;\n if (options.__i18n) {\n optionsI18n.__i18n = options.__i18n;\n }\n optionsI18n.__root = composer;\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, optionsI18n);\n }\n else {\n optionsI18n.__injectWithOption = true;\n optionsI18n.__extender = i18n.__vueI18nExtend;\n // atttach local VueI18n instance\n this.$i18n = createVueI18n(optionsI18n);\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else if (options.__i18n) {\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, options);\n }\n else {\n // atttach local VueI18n instance\n this.$i18n = createVueI18n({\n __i18n: options.__i18n,\n __injectWithOption: true,\n __extender: i18n.__vueI18nExtend,\n __root: composer\n });\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else {\n // attach global VueI18n instance\n this.$i18n = vuei18n;\n }\n if (options.__i18nGlobal) {\n adjustI18nResources(composer, options, options);\n }\n // defines vue-i18n legacy APIs\n this.$t = (...args) => this.$i18n.t(...args);\n this.$rt = (...args) => this.$i18n.rt(...args);\n this.$te = (key, locale) => this.$i18n.te(key, locale);\n this.$d = (...args) => this.$i18n.d(...args);\n this.$n = (...args) => this.$i18n.n(...args);\n this.$tm = (key) => this.$i18n.tm(key);\n i18n.__setInstance(instance, this.$i18n);\n },\n mounted() {\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$i18n) {\n const _vueI18n = this.$i18n;\n this.$el.__VUE_I18N__ = _vueI18n.__composer;\n const emitter = (this.__v_emitter =\n createEmitter());\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n emitter.on('*', addTimelineEvent);\n }\n },\n unmounted() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const _vueI18n = this.$i18n;\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$el.__VUE_I18N__) {\n if (this.__v_emitter) {\n this.__v_emitter.off('*', addTimelineEvent);\n delete this.__v_emitter;\n }\n if (this.$i18n) {\n _vueI18n.__disableEmitter && _vueI18n.__disableEmitter();\n delete this.$el.__VUE_I18N__;\n }\n }\n delete this.$t;\n delete this.$rt;\n delete this.$te;\n delete this.$d;\n delete this.$n;\n delete this.$tm;\n if (_vueI18n.__disposer) {\n _vueI18n.__disposer();\n delete _vueI18n.__disposer;\n delete _vueI18n.__extender;\n }\n i18n.__deleteInstance(instance);\n delete this.$i18n;\n }\n };\n}\nfunction mergeToGlobal(g, options) {\n g.locale = options.locale || g.locale;\n g.fallbackLocale = options.fallbackLocale || g.fallbackLocale;\n g.missing = options.missing || g.missing;\n g.silentTranslationWarn =\n options.silentTranslationWarn || g.silentFallbackWarn;\n g.silentFallbackWarn = options.silentFallbackWarn || g.silentFallbackWarn;\n g.formatFallbackMessages =\n options.formatFallbackMessages || g.formatFallbackMessages;\n g.postTranslation = options.postTranslation || g.postTranslation;\n g.warnHtmlInMessage = options.warnHtmlInMessage || g.warnHtmlInMessage;\n g.escapeParameterHtml = options.escapeParameterHtml || g.escapeParameterHtml;\n g.sync = options.sync || g.sync;\n g.__composer[SetPluralRulesSymbol](options.pluralizationRules || g.pluralizationRules);\n const messages = getLocaleMessages(g.locale, {\n messages: options.messages,\n __i18n: options.__i18n\n });\n Object.keys(messages).forEach(locale => g.mergeLocaleMessage(locale, messages[locale]));\n if (options.datetimeFormats) {\n Object.keys(options.datetimeFormats).forEach(locale => g.mergeDateTimeFormat(locale, options.datetimeFormats[locale]));\n }\n if (options.numberFormats) {\n Object.keys(options.numberFormats).forEach(locale => g.mergeNumberFormat(locale, options.numberFormats[locale]));\n }\n return g;\n}\n\nconst baseFormatProps = {\n tag: {\n type: [String, Object]\n },\n locale: {\n type: String\n },\n scope: {\n type: String,\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n validator: (val /* ComponentI18nScope */) => val === 'parent' || val === 'global',\n default: 'parent' /* ComponentI18nScope */\n },\n i18n: {\n type: Object\n }\n};\n\nfunction getInterpolateArg(\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n{ slots }, // SetupContext,\nkeys) {\n if (keys.length === 1 && keys[0] === 'default') {\n // default slot with list\n const ret = slots.default ? slots.default() : [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return ret.reduce((slot, current) => {\n return [\n ...slot,\n // prettier-ignore\n ...(current.type === Fragment ? current.children : [current])\n ];\n }, []);\n }\n else {\n // named slots\n return keys.reduce((arg, key) => {\n const slot = slots[key];\n if (slot) {\n arg[key] = slot();\n }\n return arg;\n }, create());\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getFragmentableTag() {\n return Fragment;\n}\n\nconst TranslationImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-t',\n props: assign({\n keypath: {\n type: String,\n required: true\n },\n plural: {\n type: [Number, String],\n validator: (val) => isNumber(val) || !isNaN(val)\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const { slots, attrs } = context;\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return () => {\n const keys = Object.keys(slots).filter(key => key[0] !== '_');\n const options = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (props.plural !== undefined) {\n options.plural = isString(props.plural) ? +props.plural : props.plural;\n }\n const arg = getInterpolateArg(context, keys);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const children = i18n[TranslateVNodeSymbol](props.keypath, arg, options);\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Translation Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [TranslationProps](component#translationprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Component Interpolation](../guide/advanced/component)\n *\n * @example\n * ```html\n *
\n * \n * \n * {{ $t('tos') }}\n * \n * \n *
\n * ```\n * ```js\n * import { createApp } from 'vue'\n * import { createI18n } from 'vue-i18n'\n *\n * const messages = {\n * en: {\n * tos: 'Term of Service',\n * term: 'I accept xxx {0}.'\n * },\n * ja: {\n * tos: '利用規約',\n * term: '私は xxx の{0}に同意します。'\n * }\n * }\n *\n * const i18n = createI18n({\n * locale: 'en',\n * messages\n * })\n *\n * const app = createApp({\n * data: {\n * url: '/term'\n * }\n * }).use(i18n).mount('#app')\n * ```\n *\n * @VueI18nComponent\n */\nconst Translation = TranslationImpl;\nconst I18nT = Translation;\n\nfunction isVNode(target) {\n return isArray(target) && !isString(target[0]);\n}\nfunction renderFormatter(props, context, slotKeys, partFormatter) {\n const { slots, attrs } = context;\n return () => {\n const options = { part: true };\n let overrides = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (isString(props.format)) {\n options.key = props.format;\n }\n else if (isObject(props.format)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (isString(props.format.key)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options.key = props.format.key;\n }\n // Filter out number format options only\n overrides = Object.keys(props.format).reduce((options, prop) => {\n return slotKeys.includes(prop)\n ? assign(create(), options, { [prop]: props.format[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any\n : options;\n }, create());\n }\n const parts = partFormatter(...[props.value, options, overrides]);\n let children = [options.key];\n if (isArray(parts)) {\n children = parts.map((part, index) => {\n const slot = slots[part.type];\n const node = slot\n ? slot({ [part.type]: part.value, index, parts })\n : [part.value];\n if (isVNode(node)) {\n node[0].key = `${part.type}-${index}`;\n }\n return node;\n });\n }\n else if (isString(parts)) {\n children = [parts];\n }\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n}\n\nconst NumberFormatImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-n',\n props: assign({\n value: {\n type: Number,\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, NUMBER_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[NumberPartsSymbol](...args));\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Number Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/number#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.NumberFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-numberformat)\n *\n * @VueI18nComponent\n */\nconst NumberFormat = NumberFormatImpl;\nconst I18nN = NumberFormat;\n\nfunction getComposer$1(i18n, instance) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n return (i18nInternal.__getInstance(instance) || i18n.global);\n }\n else {\n const vueI18n = i18nInternal.__getInstance(instance);\n return vueI18n != null\n ? vueI18n.__composer\n : i18n.global.__composer;\n }\n}\n/**\n * @deprecated will be removed at vue-i18n v12\n */\nfunction vTDirective(i18n) {\n const _process = (binding) => {\n if ((process.env.NODE_ENV !== 'production')) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE));\n }\n const { instance, value } = binding;\n /* istanbul ignore if */\n if (!instance || !instance.$) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const composer = getComposer$1(i18n, instance.$);\n const parsedValue = parseValue(value);\n return [\n Reflect.apply(composer.t, composer, [...makeParams(parsedValue)]),\n composer\n ];\n };\n const register = (el, binding) => {\n const [textContent, composer] = _process(binding);\n if (inBrowser && i18n.global === composer) {\n // global scope only\n el.__i18nWatcher = watch(composer.locale, () => {\n binding.instance && binding.instance.$forceUpdate();\n });\n }\n el.__composer = composer;\n el.textContent = textContent;\n };\n const unregister = (el) => {\n if (inBrowser && el.__i18nWatcher) {\n el.__i18nWatcher();\n el.__i18nWatcher = undefined;\n delete el.__i18nWatcher;\n }\n if (el.__composer) {\n el.__composer = undefined;\n delete el.__composer;\n }\n };\n const update = (el, { value }) => {\n if (el.__composer) {\n const composer = el.__composer;\n const parsedValue = parseValue(value);\n el.textContent = Reflect.apply(composer.t, composer, [\n ...makeParams(parsedValue)\n ]);\n }\n };\n const getSSRProps = (binding) => {\n const [textContent] = _process(binding);\n return { textContent };\n };\n return {\n created: register,\n unmounted: unregister,\n beforeUpdate: update,\n getSSRProps\n };\n}\nfunction parseValue(value) {\n if (isString(value)) {\n return { path: value };\n }\n else if (isPlainObject(value)) {\n if (!('path' in value)) {\n throw createI18nError(I18nErrorCodes.REQUIRED_VALUE, 'path');\n }\n return value;\n }\n else {\n throw createI18nError(I18nErrorCodes.INVALID_VALUE);\n }\n}\nfunction makeParams(value) {\n const { path, locale, args, choice, plural } = value;\n const options = {};\n const named = args || {};\n if (isString(locale)) {\n options.locale = locale;\n }\n if (isNumber(choice)) {\n options.plural = choice;\n }\n if (isNumber(plural)) {\n options.plural = plural;\n }\n return [path, named, options];\n}\n\nfunction apply(app, i18n, ...options) {\n const pluginOptions = isPlainObject(options[0])\n ? options[0]\n : {};\n const globalInstall = isBoolean(pluginOptions.globalInstall)\n ? pluginOptions.globalInstall\n : true;\n if (globalInstall) {\n [Translation.name, 'I18nT'].forEach(name => app.component(name, Translation));\n [NumberFormat.name, 'I18nN'].forEach(name => app.component(name, NumberFormat));\n [DatetimeFormat.name, 'I18nD'].forEach(name => app.component(name, DatetimeFormat));\n }\n // install directive\n {\n app.directive('t', vTDirective(i18n));\n }\n}\n\n/**\n * Injection key for {@link useI18n}\n *\n * @remarks\n * The global injection key for I18n instances with `useI18n`. this injection key is used in Web Components.\n * Specify the i18n instance created by {@link createI18n} together with `provide` function.\n *\n * @VueI18nGeneral\n */\nconst I18nInjectionKey = \n/* #__PURE__*/ makeSymbol('global-vue-i18n');\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction createI18n(options = {}) {\n // prettier-ignore\n const __legacyMode = __VUE_I18N_LEGACY_API__ && isBoolean(options.legacy)\n ? options.legacy\n : __VUE_I18N_LEGACY_API__;\n if ((process.env.NODE_ENV !== 'production') && __legacyMode) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_LEGACY_MODE));\n }\n // prettier-ignore\n const __globalInjection = isBoolean(options.globalInjection)\n ? options.globalInjection\n : true;\n const __instances = new Map();\n const [globalScope, __global] = createGlobal(options, __legacyMode);\n const symbol = /* #__PURE__*/ makeSymbol((process.env.NODE_ENV !== 'production') ? 'vue-i18n' : '');\n function __getInstance(component) {\n return __instances.get(component) || null;\n }\n function __setInstance(component, instance) {\n __instances.set(component, instance);\n }\n function __deleteInstance(component) {\n __instances.delete(component);\n }\n const i18n = {\n // mode\n get mode() {\n return __VUE_I18N_LEGACY_API__ && __legacyMode\n ? 'legacy'\n : 'composition';\n },\n // install plugin\n async install(app, ...options) {\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n app.__VUE_I18N__ = i18n;\n }\n // setup global provider\n app.__VUE_I18N_SYMBOL__ = symbol;\n app.provide(app.__VUE_I18N_SYMBOL__, i18n);\n // set composer & vuei18n extend hook options from plugin options\n if (isPlainObject(options[0])) {\n const opts = options[0];\n i18n.__composerExtend =\n opts.__composerExtend;\n i18n.__vueI18nExtend =\n opts.__vueI18nExtend;\n }\n // global method and properties injection for Composition API\n let globalReleaseHandler = null;\n if (!__legacyMode && __globalInjection) {\n globalReleaseHandler = injectGlobalFields(app, i18n.global);\n }\n // install built-in components and directive\n if (__VUE_I18N_FULL_INSTALL__) {\n apply(app, i18n, ...options);\n }\n // setup mixin for Legacy API\n if (__VUE_I18N_LEGACY_API__ && __legacyMode) {\n app.mixin(defineMixin(__global, __global.__composer, i18n));\n }\n // release global scope\n const unmountApp = app.unmount;\n app.unmount = () => {\n globalReleaseHandler && globalReleaseHandler();\n i18n.dispose();\n unmountApp();\n };\n // setup vue-devtools plugin\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n const ret = await enableDevTools(app, i18n);\n if (!ret) {\n throw createI18nError(I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN);\n }\n const emitter = createEmitter();\n if (__legacyMode) {\n const _vueI18n = __global;\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = __global;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n }\n emitter.on('*', addTimelineEvent);\n }\n },\n // global accessor\n get global() {\n return __global;\n },\n dispose() {\n globalScope.stop();\n },\n // @internal\n __instances,\n // @internal\n __getInstance,\n // @internal\n __setInstance,\n // @internal\n __deleteInstance\n };\n return i18n;\n}\nfunction useI18n(options = {}) {\n const instance = getCurrentInstance();\n if (instance == null) {\n throw createI18nError(I18nErrorCodes.MUST_BE_CALL_SETUP_TOP);\n }\n if (!instance.isCE &&\n instance.appContext.app != null &&\n !instance.appContext.app.__VUE_I18N_SYMBOL__) {\n throw createI18nError(I18nErrorCodes.NOT_INSTALLED);\n }\n const i18n = getI18nInstance(instance);\n const gl = getGlobalComposer(i18n);\n const componentOptions = getComponentOptions(instance);\n const scope = getScope(options, componentOptions);\n if (scope === 'global') {\n adjustI18nResources(gl, options, componentOptions);\n return gl;\n }\n if (scope === 'parent') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let composer = getComposer(i18n, instance, options.__useComponent);\n if (composer == null) {\n if ((process.env.NODE_ENV !== 'production')) {\n warn(getWarnMessage(I18nWarnCodes.NOT_FOUND_PARENT_SCOPE));\n }\n composer = gl;\n }\n return composer;\n }\n const i18nInternal = i18n;\n let composer = i18nInternal.__getInstance(instance);\n if (composer == null) {\n const composerOptions = assign({}, options);\n if ('__i18n' in componentOptions) {\n composerOptions.__i18n = componentOptions.__i18n;\n }\n if (gl) {\n composerOptions.__root = gl;\n }\n composer = createComposer(composerOptions);\n if (i18nInternal.__composerExtend) {\n composer[DisposeSymbol] =\n i18nInternal.__composerExtend(composer);\n }\n setupLifeCycle(i18nInternal, instance, composer);\n i18nInternal.__setInstance(instance, composer);\n }\n else {\n if ((process.env.NODE_ENV !== 'production') && scope === 'local') {\n warn(getWarnMessage(I18nWarnCodes.DUPLICATE_USE_I18N_CALLING));\n }\n }\n return composer;\n}\nfunction createGlobal(options, legacyMode) {\n const scope = effectScope();\n const obj = __VUE_I18N_LEGACY_API__ && legacyMode\n ? scope.run(() => createVueI18n(options))\n : scope.run(() => createComposer(options));\n if (obj == null) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n return [scope, obj];\n}\nfunction getI18nInstance(instance) {\n const i18n = inject(!instance.isCE\n ? instance.appContext.app.__VUE_I18N_SYMBOL__\n : I18nInjectionKey);\n /* istanbul ignore if */\n if (!i18n) {\n throw createI18nError(!instance.isCE\n ? I18nErrorCodes.UNEXPECTED_ERROR\n : I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE);\n }\n return i18n;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getScope(options, componentOptions) {\n // prettier-ignore\n return isEmptyObject(options)\n ? ('__i18n' in componentOptions)\n ? 'local'\n : 'global'\n : !options.useScope\n ? 'local'\n : options.useScope;\n}\nfunction getGlobalComposer(i18n) {\n // prettier-ignore\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n}\nfunction getComposer(i18n, target, useComponent = false) {\n let composer = null;\n const root = target.root;\n let current = getParentComponentInstance(target, useComponent);\n while (current != null) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n composer = i18nInternal.__getInstance(current);\n }\n else {\n if (__VUE_I18N_LEGACY_API__) {\n const vueI18n = i18nInternal.__getInstance(current);\n if (vueI18n != null) {\n composer = vueI18n\n .__composer;\n if (useComponent &&\n composer &&\n !composer[InejctWithOptionSymbol] // eslint-disable-line @typescript-eslint/no-explicit-any\n ) {\n composer = null;\n }\n }\n }\n }\n if (composer != null) {\n break;\n }\n if (root === current) {\n break;\n }\n current = current.parent;\n }\n return composer;\n}\nfunction getParentComponentInstance(target, useComponent = false) {\n if (target == null) {\n return null;\n }\n // if `useComponent: true` will be specified, we get lexical scope owner instance for use-case slots\n return !useComponent\n ? target.parent\n : target.vnode.ctx || target.parent; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\nfunction setupLifeCycle(i18n, target, composer) {\n let emitter = null;\n onMounted(() => {\n // inject composer instance to DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el) {\n target.vnode.el.__VUE_I18N__ = composer;\n emitter = createEmitter();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n emitter.on('*', addTimelineEvent);\n }\n }, target);\n onUnmounted(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n // remove composer instance from DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el &&\n target.vnode.el.__VUE_I18N__) {\n emitter && emitter.off('*', addTimelineEvent);\n _composer[DisableEmitter] && _composer[DisableEmitter]();\n delete target.vnode.el.__VUE_I18N__;\n }\n i18n.__deleteInstance(target);\n // dispose extended resources\n const dispose = _composer[DisposeSymbol];\n if (dispose) {\n dispose();\n delete _composer[DisposeSymbol];\n }\n }, target);\n}\nconst globalExportProps = [\n 'locale',\n 'fallbackLocale',\n 'availableLocales'\n];\nconst globalExportMethods = ['t', 'rt', 'd', 'n', 'tm', 'te']\n ;\nfunction injectGlobalFields(app, composer) {\n const i18n = Object.create(null);\n globalExportProps.forEach(prop => {\n const desc = Object.getOwnPropertyDescriptor(composer, prop);\n if (!desc) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const wrap = isRef(desc.value) // check computed props\n ? {\n get() {\n return desc.value.value;\n },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n set(val) {\n desc.value.value = val;\n }\n }\n : {\n get() {\n return desc.get && desc.get();\n }\n };\n Object.defineProperty(i18n, prop, wrap);\n });\n app.config.globalProperties.$i18n = i18n;\n globalExportMethods.forEach(method => {\n const desc = Object.getOwnPropertyDescriptor(composer, method);\n if (!desc || !desc.value) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n Object.defineProperty(app.config.globalProperties, `$${method}`, desc);\n });\n const dispose = () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties.$i18n;\n globalExportMethods.forEach(method => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties[`$${method}`];\n });\n };\n return dispose;\n}\n\nconst DatetimeFormatImpl = /* #__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-d',\n props: assign({\n value: {\n type: [Number, Date],\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, DATETIME_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[DatetimePartsSymbol](...args));\n }\n});\n/**\n * Datetime Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/datetime#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.DateTimeFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-datetimeformat)\n *\n * @VueI18nComponent\n */\nconst DatetimeFormat = DatetimeFormatImpl;\nconst I18nD = DatetimeFormat;\n\n{\n initFeatureFlags();\n}\n// register message compiler at vue-i18n\nregisterMessageCompiler(compile);\n// register message resolver at vue-i18n\nregisterMessageResolver(resolveValue);\n// register fallback locale at vue-i18n\nregisterLocaleFallbacker(fallbackWithLocaleChain);\n// NOTE: experimental !!\nif ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n const target = getGlobalThis();\n target.__INTLIFY__ = true;\n setDevToolsHook(target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__);\n}\nif ((process.env.NODE_ENV !== 'production')) ;\n\nexport { DatetimeFormat, I18nD, I18nInjectionKey, I18nN, I18nT, NumberFormat, Translation, VERSION, createI18n, useI18n, vTDirective };\n","import { commonMessages } from '@/i18n/messages/common';\nimport { panelMessages } from '@/i18n/messages/panel';\nimport { runtimeMessages } from '@/i18n/messages/runtime';\nimport type { AppLocale, MessageModule } from '@/i18n/messages/types';\nimport { createI18n, type LocaleMessageValue, useI18n } from 'vue-i18n';\n\nconst allMessages = {\n ...commonMessages,\n ...panelMessages,\n ...runtimeMessages,\n};\n\nexport type MessageKey = keyof typeof allMessages;\nexport type TranslationParams = Record;\n\nexport function resolveMvuLocale(locale: string): AppLocale {\n return locale.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';\n}\n\nfunction getHostLocale(): string {\n try {\n return SillyTavern.getCurrentLocale();\n } catch {\n return document.documentElement.lang || navigator.language || 'en';\n }\n}\n\ntype LocaleMessages = Record;\n\nfunction buildLocaleMessages(locale: AppLocale): LocaleMessages {\n // 消息源使用便于类型检查的扁平键,这里再展开成 vue-i18n 所需的嵌套对象。\n const result: LocaleMessages = {};\n for (const [key, translations] of Object.entries(allMessages) as [\n MessageKey,\n MessageModule[string],\n ][]) {\n const path = key.split('.');\n const leaf = path.pop()!;\n let parent = result;\n for (const segment of path) {\n const existing = parent[segment];\n if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {\n parent[segment] = {};\n }\n parent = parent[segment] as LocaleMessages;\n }\n parent[leaf] = translations[locale];\n }\n return result;\n}\n\nexport const i18n = createI18n({\n legacy: false,\n globalInjection: false,\n locale: resolveMvuLocale(getHostLocale()),\n fallbackLocale: 'zh-CN',\n messages: {\n 'zh-CN': buildLocaleMessages('zh-CN'),\n en: buildLocaleMessages('en'),\n },\n missingWarn: false,\n fallbackWarn: false,\n warnHtmlMessage: false,\n});\n\nfunction translate(key: MessageKey, params?: TranslationParams): string {\n return params ? i18n.global.t(key, params) : i18n.global.t(key);\n}\n\nexport function tr(key: MessageKey, params?: TranslationParams): string {\n return translate(key, params);\n}\n\nexport function useMvuI18n() {\n const composer = useI18n({ useScope: 'global' });\n return {\n locale: composer.locale,\n t: (key: MessageKey, params?: TranslationParams) =>\n params ? composer.t(key, params) : composer.t(key),\n };\n}\n","declare const jest: any;\ndeclare const process: any;\n\nexport const is_jest_environment =\n typeof jest !== 'undefined' ||\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test');\n","import {\n migrateExtraModelApiProfiles,\n normalizeExtraModelPiProfileContextWindow,\n} from '@/function/update/extra_model_api_profiles';\nimport {\n CharacterSettingsOverride,\n CharacterSettingsOverridePath,\n getCharacterSettingsOverrideValue,\n hasActiveCharacterSettingsOverride,\n hasOwnCharacterSettingsOverride,\n} from '@/function/character_override/schema';\nimport { PI_THINKING_LEVELS } from '@/function/update/pi/thinking_setting';\nimport { is_jest_environment } from '@/jest';\nimport { registerAsUniqueScript } from '@util/script';\nimport { klona } from 'klona';\nimport { defineStore } from 'pinia';\nimport { computed, ref, toRaw, watch } from 'vue';\nimport * as z from 'zod';\n\n/**\n * 保留导入或界面中的无效窗口值,使其与合法的“0 = 使用目录值”明确区分。\n * 请求预检只接受 0 或正整数,避免错误配置静默回退到目录元数据。\n */\nconst ExtraModelPiContextWindow = z\n .union([z.number(), z.string()])\n .catch('__invalid_context_window__')\n .default(0)\n .transform((value): number | string => {\n if (typeof value === 'number') {\n return value;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : value;\n });\n\n// 方案快照使用规范化整数;无效值使整个连接快照失效,而不污染其余设置。\nconst ExtraModelPiProfileContextWindow = z\n .union([z.number(), z.string()])\n .transform(value => normalizeExtraModelPiProfileContextWindow(value))\n .pipe(z.number());\n\n// 方案只保存连接与请求覆盖,OAuth 凭证和按目标缓存的密钥由活动设置独立持有。\nconst ExtraModelPiProfileSnapshot = z\n .object({\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n provider: z.string().trim(),\n api: z.string().trim(),\n authType: z\n .string()\n .trim()\n .pipe(z.enum(['api_key', 'oauth'])),\n endpoint: z.string().trim(),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim(),\n contextWindow: ExtraModelPiProfileContextWindow,\n customHeaders: z.string(),\n customIncludeBody: z.string(),\n customExcludeBody: z.string(),\n })\n .loose()\n .transform(({ credentials: _credentials, apiKeys: _apiKeys, ...connection }) => connection);\n\n// 导入的残缺 Pi 快照不能靠默认服务商或协议补成可发送连接。\n// 保留外层方案,只丢弃无效连接,应用时再要求用户补全。\nconst ExtraModelPiProfile = z.union([ExtraModelPiProfileSnapshot, z.undefined()]).catch(undefined);\n\n// 活动连接容许保留未知字段,实际可发送性由统一预检决定。\nconst ExtraModelPiSettings = z\n .object({\n provider: z.string().trim().default('openai'),\n api: z.string().trim().default('openai-responses'),\n // 保留未知认证标识供用户查看;请求和方案保存会拒绝它,迁移会清除无归属的活动密钥。\n authType: z.string().trim().default('api_key'),\n endpoint: z.string().trim().default(''),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim().default(''),\n contextWindow: ExtraModelPiContextWindow,\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n credentials: z.record(z.string(), z.unknown()).catch({}).default({}),\n apiKeys: z.record(z.string(), z.string()).catch({}).default({}),\n customHeaders: z.string().default(''),\n customIncludeBody: z.string().default(''),\n customExcludeBody: z.string().default(''),\n })\n .loose()\n .prefault({});\n\nexport const EXTRA_MODEL_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\nconst ExtraModelResponseFormat = z.enum(EXTRA_MODEL_RESPONSE_FORMATS);\nconst ExtraModelJailbreakStrategy = z.enum(['使用内置破限', '使用当前预设', '使用其他预设']);\n\nconst ExtraModelApiProfile = z\n .object({\n 名称: z.string().trim().min(1),\n backend: z.enum(['custom', 'pi']).default('custom'),\n api地址: z.string().default(''),\n 密钥: z.string().default(''),\n 模型名称: z.string().default(''),\n pi: ExtraModelPiProfile.optional(),\n // 缺失选项保持未设置,使旧自定义方案可以一次性继承当前请求选项。\n 破限方案: ExtraModelJailbreakStrategy.optional().catch(undefined),\n 其他预设名称: z.string().optional().catch(undefined),\n 随机头部: z.boolean().optional().catch(undefined),\n 应答格式: ExtraModelResponseFormat.optional().catch(undefined),\n 关闭thinking: z.boolean().optional().catch(undefined),\n 兼容假流式: z.boolean().optional().catch(undefined),\n })\n .loose()\n .transform(({ customApiKey: _customApiKey, ...profile }) => profile);\n\n// 逐项容错,单个损坏方案只被过滤,不使整份用户设置重置。\nconst ExtraModelApiProfileList = z\n .array(ExtraModelApiProfile.nullable().catch(null))\n .catch([])\n .transform(profiles =>\n profiles.filter(\n (profile): profile is z.infer => profile !== null\n )\n );\n\nconst OldSettings = z\n .object({\n 通知: z\n .object({\n 变量更新出错: z.boolean(),\n 额外模型解析中: z.boolean(),\n })\n .loose(),\n 更新方式: z.enum(['随AI输出', '额外模型解析']),\n 自动触发额外模型解析: z.boolean(),\n 额外模型解析配置: z\n .object({\n 发送预设: z.boolean(),\n 使用函数调用: z.boolean(),\n 模型来源: z.enum(['与插头相同', '自定义']),\n api地址: z.string(),\n 密钥: z.string(),\n 模型名称: z.string(),\n 温度: z.coerce.number(),\n 频率惩罚: z.coerce.number(),\n 存在惩罚: z.coerce.number(),\n top_p: z.coerce.number(),\n 最大回复token数: z.coerce.number(),\n })\n .loose(),\n 快照保留间隔: z.number(),\n 更新到聊天变量: z.boolean(),\n legacy: z\n .object({\n 显示老旧功能: z.boolean(),\n })\n .loose(),\n auto_cleanup: z\n .object({\n 启用: z.boolean(),\n 要保留变量的最近楼层数: z.number(),\n 触发恢复变量的最近楼层数: z.number(),\n })\n .loose(),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean(),\n 已提醒自动清理旧变量功能: z.boolean(),\n 已提醒更新了API温度等配置: z.boolean(),\n 已默认开启自动清理旧变量功能: z.boolean(),\n })\n .loose(),\n })\n .loose()\n .transform(data => {\n const {\n 自动触发额外模型解析,\n 额外模型解析配置: { 发送预设, 使用函数调用, ...extra_model_settings },\n 快照保留间隔,\n 更新到聊天变量,\n legacy,\n auto_cleanup,\n 自动清理变量: existing_auto_cleanup,\n 兼容性: existing_compatibility,\n ...settings\n } = data;\n const existing_auto_cleanup_settings = _.isPlainObject(existing_auto_cleanup)\n ? (existing_auto_cleanup as Record)\n : {};\n const existing_compatibility_settings = _.isPlainObject(existing_compatibility)\n ? (existing_compatibility as Record)\n : {};\n\n return NewSettings.decode({\n ...settings,\n 额外模型解析配置: {\n 破限方案: 发送预设 ? '使用当前预设' : '使用内置破限',\n 启用自动请求: 自动触发额外模型解析,\n 应答格式: 使用函数调用 ? '工具调用' : '聊天消息',\n ...extra_model_settings,\n },\n 自动清理变量: {\n ...auto_cleanup,\n 快照保留间隔,\n ...existing_auto_cleanup_settings,\n },\n 兼容性: {\n ...legacy,\n 更新到聊天变量,\n ...existing_compatibility_settings,\n },\n });\n });\n\nconst NewSettings = z\n .object({\n 通知: z\n .object({\n MVU框架加载成功: z.boolean().default(true),\n 变量初始化成功: z.boolean().default(true),\n 变量更新出错: z.boolean().default(false),\n 额外模型解析中: z.boolean().default(true),\n })\n .loose()\n .prefault({}),\n 更新方式: z.enum(['随AI输出', '额外模型解析']).default('随AI输出'),\n 额外模型解析配置: z\n .object({\n 破限方案: ExtraModelJailbreakStrategy.default('使用内置破限'),\n 其他预设名称: z.string().default(''),\n 使用函数调用: z.boolean().optional(),\n 应答格式: ExtraModelResponseFormat.optional(),\n 关闭thinking: z.boolean().default(false),\n 兼容假流式: z.boolean().default(false),\n 随机头部: z.boolean().default(true),\n\n 启用自动请求: z.boolean().default(true),\n 请求方式: z\n .enum([\n '依次请求,失败后重试',\n '同时请求多次',\n '先请求一次, 失败后再同时请求多次',\n ])\n .default('依次请求,失败后重试'),\n 请求次数: z.number().default(3),\n 世界书条目白名单正则: z.string().default(''),\n 世界书条目黑名单正则: z.string().default(''),\n\n 模型来源: z.enum(['与插头相同', '自定义', '更多']).default('与插头相同'),\n api地址: z.string().default('http://localhost:1234/v1'),\n 密钥: z.string().default(''),\n customApiKey: z.string().catch('').default(''),\n 模型名称: z.string().default('gemini-2.5-flash-nothinking'),\n pi: ExtraModelPiSettings,\n 温度: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 2)),\n 频率惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n 存在惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n top_p: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 1)),\n top_k: z.coerce\n .number()\n .default(0)\n .transform(value => _.clamp(value, 0, 500)),\n max_chat_history: z.coerce\n .number()\n .default(2)\n .transform(value => _.clamp(Math.round(value), 2, 100)),\n 最大回复token数: z.coerce\n .number()\n // 回复 token 的 0 没有默认值语义,可保留为阻止请求的无效导入值。\n .catch(0)\n .default(4096)\n .transform(value => Math.max(0, value)),\n api方案列表: ExtraModelApiProfileList.default([]),\n 当前api方案: z.string().catch('').default(''),\n })\n .loose()\n .transform(({ 使用函数调用, 应答格式, ...data }) =>\n migrateExtraModelApiProfiles({\n ...data,\n 应答格式: 应答格式 ?? (使用函数调用 ? '工具调用' : '聊天消息'),\n })\n )\n .prefault({}),\n 自动清理变量: z\n .object({\n 启用: z.boolean().default(true),\n 快照保留间隔: z.number().default(50),\n 要保留变量的最近楼层数: z.number().default(20),\n 触发恢复变量的最近楼层数: z.number().default(10),\n })\n .loose()\n .prefault({}),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().default(false),\n 显示老旧功能: z.boolean().default(false),\n 额外模型解析非阻塞: z.boolean().default(false),\n sendas不视为user消息: z.boolean().optional(),\n sandas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .transform(({ sendas不视为user消息, sandas不视为user消息, ...data }) => ({\n ...data,\n 更新到聊天变量: data.更新到聊天变量,\n 显示老旧功能: data.显示老旧功能,\n sendas不视为user消息: sendas不视为user消息 ?? sandas不视为user消息 ?? false,\n }))\n .prefault({}),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean().default(false),\n 已提醒自动清理旧变量功能: z.boolean().default(false),\n 已提醒更新了API温度等配置: z.boolean().default(false),\n 已默认开启自动清理旧变量功能: z.boolean().default(false),\n 已提醒内置破限: z.boolean().default(false),\n 已提醒额外模型同时请求: z.boolean().default(false),\n 已开启默认不兼容假流式: z.boolean().default(false),\n })\n .loose()\n .prefault({}),\n })\n .loose()\n .transform(data => {\n // 新安装由 Schema 提供 false;迁移时保留已保存的流式选项及方案继承值。\n data.internal.已开启默认不兼容假流式 = true;\n return data;\n })\n .prefault({});\n\nconst Settings = z.union([OldSettings, NewSettings]).catch(() => NewSettings.parse({}));\n\nexport type MvuSettings = z.infer;\n\nexport type CharacterSettingsState = {\n status: 'loading' | 'unbound' | 'ready' | 'error';\n character_name: string;\n worldbook_name: string | null;\n entry_uid: number | null;\n expected_content: string | null;\n draft: CharacterSettingsOverride;\n is_valid: boolean;\n revision: number;\n has_pending_save: boolean;\n is_saving: boolean;\n};\n\nfunction createCharacterSettingsState(): CharacterSettingsState {\n return {\n status: 'loading',\n character_name: '',\n worldbook_name: null,\n entry_uid: null,\n expected_content: null,\n draft: {},\n is_valid: true,\n revision: 0,\n has_pending_save: false,\n is_saving: false,\n };\n}\n\nconst Runtimes = z\n .object({\n unsupported_warnings: z.string().default(''),\n is_during_extra_analysis: z.boolean().default(false),\n is_function_call_enabled: z.boolean().default(false),\n 上次世界书条目过滤结果: z\n .array(\n z.object({\n lore: z.enum(['globalLore', 'characterLore', 'chatLore', 'personaLore']),\n world: z.string(),\n comment: z.string(),\n reason: z.enum(['白名单', '黑名单']),\n sources: z.array(z.enum(['用户全局配置', '角色卡配置'])).default([]),\n })\n )\n .default([]),\n debug: z\n .object({\n 首次额外请求必失败: z.boolean().default(false),\n })\n .prefault({}),\n })\n .prefault({});\n\nexport const useDataStore = defineStore('MVU变量框架', () => {\n const settings = ref(Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {})));\n watch(\n settings,\n new_settings => {\n _.set(SillyTavern.extensionSettings, 'mvu_settings', toRaw(new_settings));\n if (!is_jest_environment) SillyTavern.saveSettingsDebounced();\n },\n { deep: true }\n );\n const _reload_settings = () => {\n settings.value = Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {}));\n };\n\n const character_settings = ref(createCharacterSettingsState());\n const effective_settings = computed(() => {\n // 运行时读取合并后的配置,但不修改用户持久化的全局设置。\n const result = klona(settings.value);\n if (!character_settings.value.is_valid) {\n return result;\n }\n\n const draft = character_settings.value.draft;\n // 正则采用“全局 + 角色卡”的叠加语义,由 filterEntries 单独合并,不在此处覆盖。\n if (_.has(draft, '更新方式')) {\n result.更新方式 = draft.更新方式!;\n }\n if (_.has(draft, '额外模型解析配置.启用自动请求')) {\n result.额外模型解析配置.启用自动请求 = draft.额外模型解析配置!.启用自动请求!;\n }\n if (_.has(draft, '兼容性.更新到聊天变量')) {\n result.兼容性.更新到聊天变量 = draft.兼容性!.更新到聊天变量!;\n }\n if (_.has(draft, '兼容性.sendas不视为user消息')) {\n result.兼容性.sendas不视为user消息 = draft.兼容性!.sendas不视为user消息!;\n }\n return result;\n });\n const has_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid &&\n hasOwnCharacterSettingsOverride(character_settings.value.draft, path);\n const get_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid\n ? getCharacterSettingsOverrideValue(character_settings.value.draft, path)\n : undefined;\n const is_character_settings_override_active = computed(\n () =>\n character_settings.value.is_valid &&\n hasActiveCharacterSettingsOverride(character_settings.value.draft)\n );\n const resetCharacterSettings = () => {\n character_settings.value = createCharacterSettingsState();\n };\n\n const runtimes = ref(Runtimes.parse({}));\n watch(\n () => runtimes.value.is_during_extra_analysis,\n new_value => insertOrAssignVariables({ extra_analysis: new_value }, { type: 'global' }),\n { immediate: true }\n );\n const resetRuntimes = () => {\n runtimes.value = Runtimes.parse({});\n };\n\n const versions = ref<{ sillytavern: string; tavernhelper: string }>({\n sillytavern: '',\n tavernhelper: '',\n });\n const _wait_init = async () => {\n versions.value.sillytavern = await fetch('/version')\n .then(res => res.json())\n .then(data => data.pkgVersion)\n .catch(() => '1.0.0');\n versions.value.tavernhelper = await getTavernHelperVersion();\n };\n\n const should_enable = ref(false);\n watch(should_enable, (new_enable, old_enable) => {\n if (new_enable && !old_enable) {\n _reload_settings();\n }\n });\n\n // 当存在多个 MVU 脚本实例时,仅优先实例应启用运行逻辑。\n registerAsUniqueScript('MVU变量框架').listenPreferenceState(preferred_script_id => {\n should_enable.value = preferred_script_id === getScriptId();\n });\n\n return {\n settings,\n _reload_settings,\n character_settings,\n effective_settings,\n has_character_settings_override,\n get_character_settings_override,\n is_character_settings_override_active,\n resetCharacterSettings,\n runtimes,\n resetRuntimes,\n versions,\n _wait_init,\n should_enable,\n };\n});\n","import { tr } from '@/i18n';\nimport { compare } from 'compare-versions';\nimport JSON5 from 'json5';\nimport { jsonrepair } from 'jsonrepair';\n\n// 修正 _.merge 对数组的合并逻辑, [1, 2, 3] 和 [4, 5] 合并后变成 [4, 5] 而不是 [4, 5, 3]\nexport function correctlyMerge(lhs: TObject, rhs: TSource): TObject & TSource {\n return _.mergeWith(lhs, rhs, (_lhs, rhs) => (_.isArray(rhs) ? rhs : undefined));\n}\n\nexport function uuidv4(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nexport async function checkMinimumVersion(\n expected: string,\n notice: { message: string; title: string }\n) {\n if (compare(await getTavernHelperVersion(), expected, '<')) {\n toastr.error(notice.message, notice.title);\n }\n}\nexport function literalYamlify(value: any) {\n return YAML.stringify(value, { blockQuote: 'literal' });\n}\n\nexport function parseString(content: string): any {\n const json_first = /^[[{]/s.test(content.trimStart());\n try {\n if (json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error1) {\n try {\n // eslint-disable-next-line import-x/no-named-as-default-member\n return JSON5.parse(content);\n } catch (json5_error) {\n try {\n return JSON.parse(jsonrepair(content));\n } catch (json_error) {\n try {\n if (!json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error2) {\n const toError = (error: unknown) =>\n error instanceof Error\n ? `${error.stack ? error.stack : error.message}`\n : String(error);\n\n throw new Error(\n literalYamlify({\n [tr('runtime.parseString.invalidFormat')]: {\n [tr('runtime.parseString.content')]: content,\n [tr('runtime.parseString.yamlError')]: toError(\n json_first ? yaml_error2 : yaml_error1\n ),\n [tr('runtime.parseString.json5Error')]: toError(json5_error),\n [tr('runtime.parseString.jsonError')]: toError(json_error),\n },\n })\n );\n }\n }\n }\n }\n}\n","export function teleportStyle(\n append_to:\n | JQuery.Selector\n | JQuery.htmlString\n | JQuery.TypeOrArray\n | JQuery = 'head'\n): { destroy: () => void } {\n const $div = $(`
`)\n .attr('script_id', getScriptId())\n .append($(`head > style`, document).clone())\n .appendTo(append_to);\n\n return {\n destroy: () => $div.remove(),\n };\n}\n\nexport function createScriptIdDiv(): JQuery {\n return $('
').attr('script_id', getScriptId()) as JQuery;\n}\n\n/**\n * 将当前脚本实例注册到共享的“唯一脚本”命名空间。\n *\n * 当同一脚本被重复加载时,优先实例为:仍存在于 TavernHelper 脚本列表中、\n * 且在注册顺序上最后出现的 script id。\n * 调用方可通过 `listenPreferenceState` 订阅优先实例变化,并仅在\n * `getScriptId()` 与优先实例一致时启用功能。\n */\nexport function registerAsUniqueScript(id: string): {\n unregister: () => void;\n getPreferredScriptId: () => string | undefined;\n listenPreferenceState: (callback: (perferred_script_id: string) => void) => EventOnReturn;\n} {\n // 当前实例在 TavernHelper 中的唯一脚本 ID。\n const script_id = getScriptId();\n // 以业务 id 作为命名空间,避免不同功能之间冲突。\n const path = `th_unique_check.${id}`;\n\n const getPreferredScriptId = () => {\n // 从共享状态中取出已注册实例集合(跨脚本实例共享在 window.parent)。\n const registered_scripts = _.get(window.parent, path, new Set());\n // 以页面上实际存在的脚本顺序为准,选出“最后一个仍有效”的实例作为优先实例。\n return _($('#tavern_helper').find('div[data-script-id]').toArray())\n .map(element => String($(element).attr('data-script-id')))\n .filter(element => registered_scripts.has(element))\n .last();\n };\n\n // 将当前实例加入注册集合。不存在的场合创建。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value === undefined) {\n return new Set([script_id]);\n }\n //避免重复添加\n if (value.has(script_id)) return value;\n value.add(script_id);\n return value;\n });\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n\n return {\n unregister: () => {\n // 卸载时从注册集合移除,并重新广播优先实例。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value !== undefined) {\n value.delete(script_id);\n }\n return value;\n });\n eventEmit(path, getPreferredScriptId());\n },\n getPreferredScriptId,\n // 监听优先实例变化(回调入参是当前优先实例 script id)。\n listenPreferenceState: (callback: (enabled_script_id: string) => void) => {\n const ret = eventOn(path, callback);\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n return ret;\n },\n };\n}\n","/* unplugin-vue-components disabled */// style-loader: Adds some css to the DOM by adding a \\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-character-override__metadata[data-v-ac919d4c]{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:0.25rem 0.7rem;margin:0;padding:0.35rem 0.55rem;border-radius:8px;background-color:rgba(0,0,0,0.08)}.mvu-character-override__metadata dt[data-v-ac919d4c]{font-weight:600;opacity:0.82}.mvu-character-override__metadata dd[data-v-ac919d4c]{min-width:0;margin:0;overflow-wrap:anywhere}.mvu-character-override__fieldset[data-v-ac919d4c]{min-inline-size:0;margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:0.45rem}.mvu-character-override__fieldset[data-v-ac919d4c]:disabled{opacity:0.6}.mvu-character-override__group[data-v-ac919d4c]{display:flex;flex-direction:column;gap:0.3rem;padding-top:0.1rem}.mvu-character-override__group-title[data-v-ac919d4c]{padding:0 0.15rem}.mvu-character-override__select-field.mvu-field[data-v-ac919d4c]{flex-flow:row nowrap;align-items:center;gap:0.4rem;padding:0.3rem 0.5rem}.mvu-character-override__select-field>.text_pole[data-v-ac919d4c]{flex:1 1 auto;width:auto;min-width:0;margin:0}.mvu-character-override__regex-error[data-v-ac919d4c]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/CharacterOverride.vue\"],\"names\":[],\"mappings\":\"AAsMA,mDACI,YAAa,CACb,+CAAiD,CACjD,kBAAmB,CACnB,QAAS,CACT,uBAAwB,CACxB,iBAAkB,CAClB,iCACJ,CAEA,sDACI,eAAgB,CAChB,YACJ,CAEA,sDACI,WAAY,CACZ,QAAS,CACT,sBACJ,CAEA,mDACI,iBAAkB,CAClB,QAAS,CACT,SAAU,CACV,QAAS,CACT,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,4DACI,WACJ,CAEA,gDACI,YAAa,CACb,qBAAsB,CACtB,UAAW,CACX,kBACJ,CAEA,sDACI,iBACJ,CAEA,iEACI,oBAAqB,CACrB,kBAAmB,CACnB,UAAW,CACX,qBACJ,CAEA,kEACI,aAAc,CACd,UAAW,CACX,WAAY,CACZ,QACJ,CAEA,sDACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-license-table-wrap[data-v-67402f54]{max-height:18rem;overflow:auto;border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:6px}.mvu-license-table[data-v-67402f54]{width:100%;border-collapse:collapse;font-size:0.9em}.mvu-license-table th[data-v-67402f54],.mvu-license-table td[data-v-67402f54]{padding:0.35rem 0.5rem;text-align:start;vertical-align:top;border-bottom:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,0.45))}.mvu-license-table th[data-v-67402f54]:last-child,.mvu-license-table td[data-v-67402f54]:last-child{width:7.5rem;white-space:nowrap}.mvu-license-table tbody tr:last-child td[data-v-67402f54]{border-bottom:0}.mvu-license-table a[data-v-67402f54]{overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Compatibility.vue\"],\"names\":[],\"mappings\":\"AA0GA,yCACI,gBAAiB,CACjB,aAAc,CACd,8DAAmE,CACnE,iBACJ,CAEA,oCACI,UAAW,CACX,wBAAyB,CACzB,eACJ,CAEA,8EAEI,sBAAuB,CACvB,gBAAiB,CACjB,kBAAmB,CACnB,wEACJ,CAEA,oGAEI,YAAa,CACb,kBACJ,CAEA,2DACI,eACJ,CAEA,sCACI,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.inline-drawer-content[data-v-7a58cca8]{flex-direction:column;gap:0.75rem;padding-top:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Panel.vue\"],\"names\":[],\"mappings\":\"AAmCA,wCACI,qBAAsB,CACtB,WAAY,CACZ,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-details[data-v-3a7b912a]{border:1px dashed var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.5rem 0.7rem;background-color:rgba(0,0,0,0.06);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 70%,transparent)}.mvu-details__summary[data-v-3a7b912a]{cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;opacity:0.95}.mvu-details__title-suffix[data-v-3a7b912a]{display:inline-flex;align-items:center;margin-inline-start:0.35rem;vertical-align:middle}.mvu-details__content[data-v-3a7b912a]{margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Detail.vue\"],\"names\":[],\"mappings\":\"AAwBA,8BACI,+DAAoE,CACpE,kBAAmB,CACnB,qBAAsB,CACtB,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,cAAe,CACf,wBAAiB,CAAjB,gBAAiB,CACjB,eAAgB,CAChB,YACJ,CAEA,4CACI,mBAAoB,CACpB,kBAAmB,CACnB,2BAA4B,CAC5B,qBACJ,CAEA,uCACI,iBAAkB,CAClB,YAAa,CACb,qBAAsB,CACtB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field[data-v-1bd30ada]{padding:0.45rem 0.6rem;gap:0.25rem;border:1px solid color-mix(in srgb,var(--SmartThemeBorderColor,rgba(45,45,45,1)) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 55%,transparent)}.mvu-field__label[data-v-1bd30ada]{display:inline-flex;align-items:center;gap:0.35rem;font-weight:600;opacity:0.95}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Field.vue\"],\"names\":[],\"mappings\":\"AAeA,4BACI,sBAAuB,CACvB,WAAY,CACZ,iGAAwG,CACxG,kBAAmB,CACnB,mGAKJ,CAEA,mCACI,mBAAoB,CACpB,kBAAmB,CACnB,WAAY,CACZ,eAAgB,CAChB,YACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-help-icon[data-v-7a641daa]{cursor:pointer}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/HelpIcon.vue\"],\"names\":[],\"mappings\":\"AAoBA,gCACI,cACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-model-select[data-v-7b6ca08a]{display:flex;flex-direction:column;gap:0.5rem}.mvu-model-select__row[data-v-7b6ca08a]{width:100%}.mvu-model-select__row--controls[data-v-7b6ca08a]{display:grid;grid-template-columns:1fr auto;gap:0.5rem;align-items:center}.mvu-model-select__btn[data-v-7b6ca08a]{white-space:nowrap;text-align:left;padding:0.35rem 0.6rem;min-height:unset;height:2.05rem;line-height:1.1}@media (max-width:520px){.mvu-model-select__row--controls[data-v-7b6ca08a]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/ModelSelect.vue\"],\"names\":[],\"mappings\":\"AA4JA,mCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,wCACI,UACJ,CAEA,kDACI,YAAa,CACb,8BAA+B,CAC/B,UAAW,CACX,kBACJ,CAEA,wCACI,kBAAmB,CACnB,eAAgB,CAChB,sBAAuB,CACvB,gBAAiB,CACjB,cAAe,CACf,eACJ,CAEA,yBACI,kDACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-override-badge[data-v-14bdab4e]{display:inline-flex;flex:0 1 auto;align-items:center;max-width:100%;padding:0.08rem 0.38rem;border:1px solid currentColor;border-radius:999px;font-size:calc(var(--mainFontSize,1rem) * 0.76);font-weight:600;line-height:1.25;text-align:center;overflow-wrap:anywhere}.mvu-override-badge--override[data-v-14bdab4e]{color:var(--SmartThemeEmColor,#d39e00);background-color:color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 12%,transparent)}.mvu-override-badge--additive[data-v-14bdab4e]{color:var(--SmartThemeQuoteColor,#6ba7ff);background-color:color-mix(in srgb,var(--SmartThemeQuoteColor,#6ba7ff) 12%,transparent)}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/OverrideBadge.vue\"],\"names\":[],\"mappings\":\"AAuBA,qCACI,mBAAoB,CACpB,aAAc,CACd,kBAAmB,CACnB,cAAe,CACf,uBAAwB,CACxB,6BAA8B,CAC9B,mBAAoB,CACpB,+CAAiD,CACjD,eAAgB,CAChB,gBAAiB,CACjB,iBAAkB,CAClB,sBACJ,CAEA,+CACI,sCAAwC,CACxC,oFACJ,CAEA,+CACI,yCAA2C,CAC3C,uFACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-range-number[data-v-48562df7]{display:grid;grid-template-columns:1fr 7.5rem;gap:0.5rem;align-items:center}.mvu-range-number__range[data-v-48562df7]{width:100%}.mvu-range-number__number[data-v-48562df7]{text-align:left;padding-top:0.3rem;padding-bottom:0.3rem;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 33%,transparent)}@media (max-width:420px){.mvu-range-number[data-v-48562df7]{grid-template-columns:1fr 6.5rem}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/RangeNumber.vue\"],\"names\":[],\"mappings\":\"AA2CA,mCACI,YAAa,CACb,gCAAiC,CACjC,UAAW,CACX,kBACJ,CAEA,0CACI,UACJ,CAEA,2CACI,eAAgB,CAChB,kBAAmB,CACnB,qBAAsB,CACtB,mGAKJ,CAEA,yBACI,mCACI,gCACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-section[data-v-ab897094]{border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.6rem 0.75rem;gap:0.45rem;background-color:rgba(0,0,0,0.08);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 75%,transparent)}.mvu-section__content[data-v-ab897094]{gap:0.5rem}.mvu-section__title[data-v-ab897094]{display:flex;flex-wrap:wrap;align-items:center;gap:0.3rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Section.vue\"],\"names\":[],\"mappings\":\"AAmBA,8BACI,8DAAmE,CACnE,kBAAmB,CACnB,sBAAuB,CACvB,WAAY,CACZ,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,UACJ,CAEA,qCACI,YAAa,CACb,cAAe,CACf,kBAAmB,CACnB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-warning[data-v-58f80c4e]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-58f80c4e]{line-height:1}.mvu-warning__text[data-v-58f80c4e]{word-break:break-word}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Method.vue\"],\"names\":[],\"mappings\":\"AA4CA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-note[data-v-7ae42438]{padding:0 0.6rem 0.45rem;opacity:0.8;font-size:calc(var(--mainFontSize,1rem) * 0.9)}.mvu-regex-error[data-v-7ae42438]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;word-break:break-word}.mvu-regex-actions[data-v-7ae42438]{display:flex;padding:0 0.6rem 0.45rem}.mvu-regex-actions__button[data-v-7ae42438]{min-height:2rem;white-space:normal}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Prompt.vue\"],\"names\":[],\"mappings\":\"AA0MA,2BACI,wBAAyB,CACzB,WAAY,CACZ,8CACJ,CAEA,kCACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,qBACJ,CAEA,oCACI,YAAa,CACb,wBACJ,CAEA,4CACI,eAAgB,CAChB,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-auto-request-conditions[data-v-22baa9a5]{margin-left:1rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Request.vue\"],\"names\":[],\"mappings\":\"AAuIA,8CACI,gBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field-grid[data-v-0edef090]{display:flex;flex-direction:column;gap:0.5rem}.mvu-api-profile-controls[data-v-0edef090]{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem}.mvu-api-profile-actions[data-v-0edef090]{display:flex;flex-wrap:wrap;gap:0.5rem}.mvu-note[data-v-0edef090]{opacity:0.85;color:var(--SmartThemeEmColor,inherit)}.mvu-pi-proxy-option[data-v-0edef090]{padding:0 0.6rem}.mvu-warning[data-v-0edef090]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-0edef090]{line-height:1}.mvu-warning__text[data-v-0edef090]{word-break:break-word}.mvu-pi-advanced-textarea[data-v-0edef090]{width:100%;min-height:5rem;resize:vertical;font-family:var(--monoFontFamily,monospace);white-space:pre}.mvu-field-error[data-v-0edef090]{color:var(--SmartThemeQuoteColor,#ff6b6b);line-height:1.35;word-break:break-word}.mvu-oauth-status[data-v-0edef090]{display:flex;flex-direction:column;gap:0.25rem}.mvu-link-button[data-v-0edef090]{display:inline-flex;align-items:center;text-decoration:none}@media (max-width:520px){.mvu-api-profile-controls[data-v-0edef090]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Source.vue\"],\"names\":[],\"mappings\":\"AA0wDA,iCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,2CACI,YAAa,CACb,6BAA8B,CAC9B,UACJ,CAEA,0CACI,YAAa,CACb,cAAe,CACf,UACJ,CAEA,2BACI,YAAa,CACb,sCACJ,CAEA,sCACI,gBACJ,CAEA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ,CAEA,2CACI,UAAW,CACX,eAAgB,CAChB,eAAgB,CAChB,2CAA6C,CAC7C,eACJ,CAEA,kCACI,yCAA2C,CAC3C,gBAAiB,CACjB,qBACJ,CAEA,mCACI,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,kCACI,mBAAoB,CACpB,kBAAmB,CACnB,oBACJ,CAEA,yBACI,2CACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = [];\n\n // return the list of modules as css string\n list.toString = function toString() {\n return this.map(function (item) {\n var content = \"\";\n var needLayer = typeof item[5] !== \"undefined\";\n if (item[4]) {\n content += \"@supports (\".concat(item[4], \") {\");\n }\n if (item[2]) {\n content += \"@media \".concat(item[2], \" {\");\n }\n if (needLayer) {\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\n }\n content += cssWithMappingToString(item);\n if (needLayer) {\n content += \"}\";\n }\n if (item[2]) {\n content += \"}\";\n }\n if (item[4]) {\n content += \"}\";\n }\n return content;\n }).join(\"\");\n };\n\n // import a list of modules into the list\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === \"string\") {\n modules = [[null, modules, undefined]];\n }\n var alreadyImportedModules = {};\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n if (typeof layer !== \"undefined\") {\n if (typeof item[5] === \"undefined\") {\n item[5] = layer;\n } else {\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\n item[5] = layer;\n }\n }\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\n item[2] = media;\n }\n }\n if (supports) {\n if (!item[4]) {\n item[4] = \"\".concat(supports);\n } else {\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\n item[4] = supports;\n }\n }\n list.push(item);\n }\n };\n return list;\n};","\"use strict\";\n\nmodule.exports = function (item) {\n var content = item[1];\n var cssMapping = item[3];\n if (!cssMapping) {\n return content;\n }\n if (typeof btoa === \"function\") {\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n var sourceMapping = \"/*# \".concat(data, \" */\");\n return [content].concat([sourceMapping]).join(\"\\n\");\n }\n return [content].join(\"\\n\");\n};","import { compileEntryCommentRegex } from '@/function/request/entry_comment_regex';\nimport { tr } from '@/i18n';\nimport { parseString } from '@util/common';\nimport { klona } from 'klona';\nimport * as z from 'zod';\n\nexport const CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME = '[config_override]';\n\n// loose 模式用于保留未来版本或其他扩展写入的未知配置字段。\nexport const CharacterSettingsOverrideSchema = z\n .object({\n 更新方式: z.enum(['随AI输出', '额外模型解析']).optional(),\n 额外模型解析配置: z\n .object({\n 启用自动请求: z.boolean().optional(),\n 世界书条目白名单正则: z.string().optional(),\n 世界书条目黑名单正则: z.string().optional(),\n })\n .loose()\n .optional(),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().optional(),\n sendas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .optional(),\n })\n .loose();\n\nexport type CharacterSettingsOverride = z.infer;\n\nexport type CharacterSettingsOverridePath =\n | '更新方式'\n | '额外模型解析配置.启用自动请求'\n | '额外模型解析配置.世界书条目白名单正则'\n | '额外模型解析配置.世界书条目黑名单正则'\n | '兼容性.更新到聊天变量'\n | '兼容性.sendas不视为user消息';\n\nexport type CharacterSettingsOverrideValue = string | boolean | undefined;\n\nexport function isCharacterSettingsOverrideEntryName(name: unknown): boolean {\n return (\n typeof name === 'string' &&\n name.trim().toLowerCase() === CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME\n );\n}\n\nexport function hasOwnCharacterSettingsOverride(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): boolean {\n return _.has(draft, path);\n}\n\nexport function getCharacterSettingsOverrideValue(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): CharacterSettingsOverrideValue {\n return _.get(draft, path) as CharacterSettingsOverrideValue;\n}\n\nfunction removeEmptyKnownValues(draft: CharacterSettingsOverride): CharacterSettingsOverride {\n // “继承全局配置”在文件中表示字段不存在,而不是保存空字符串或空对象。\n const result = klona(draft);\n const extra_model = result.额外模型解析配置;\n if (extra_model) {\n for (const key of ['世界书条目白名单正则', '世界书条目黑名单正则'] as const) {\n if (extra_model[key]?.trim() === '') {\n delete extra_model[key];\n }\n }\n if (Object.keys(extra_model).length === 0) {\n delete result.额外模型解析配置;\n }\n }\n if (result.兼容性 && Object.keys(result.兼容性).length === 0) {\n delete result.兼容性;\n }\n return result;\n}\n\nexport function normalizeCharacterSettingsOverride(\n draft: CharacterSettingsOverride\n): CharacterSettingsOverride {\n return CharacterSettingsOverrideSchema.parse(removeEmptyKnownValues(draft));\n}\n\nfunction makeAdditionalPropertiesExplicit(schema: unknown): unknown {\n // Zod 会用空对象表示允许附加属性;显式改为 true 便于其他 JSON Schema 工具识别。\n if (Array.isArray(schema)) {\n return schema.map(makeAdditionalPropertiesExplicit);\n }\n if (!_.isPlainObject(schema)) {\n return schema;\n }\n\n const result = Object.fromEntries(\n Object.entries(schema as Record).map(([key, value]) => [\n key,\n key === 'additionalProperties' && _.isPlainObject(value) && _.isEmpty(value)\n ? true\n : makeAdditionalPropertiesExplicit(value),\n ])\n );\n return result;\n}\n\nexport function getCharacterSettingsOverrideJsonSchema(): Record {\n const generated = z.toJSONSchema(CharacterSettingsOverrideSchema, {\n target: 'draft-2020-12',\n });\n return {\n ...(makeAdditionalPropertiesExplicit(generated) as Record),\n title: 'CharacterSettingsOverride',\n };\n}\n\nexport function parseCharacterSettingsOverrideContent(content: string): CharacterSettingsOverride {\n const document = JSON.parse(content) as unknown;\n if (!_.isPlainObject(document)) {\n throw new Error(tr('runtime.characterOverride.contentMustBeObject'));\n }\n\n // schema 仅供角色卡编辑器展示,不属于实际覆盖配置。\n const { schema: _embedded_schema, ...draft } = document as Record;\n return normalizeCharacterSettingsOverride(CharacterSettingsOverrideSchema.parse(draft));\n}\n\nexport function recoverCharacterSettingsOverridePassthrough(\n content: string\n): CharacterSettingsOverride {\n const document = parseString(content) as unknown;\n if (!_.isPlainObject(document)) {\n return {};\n }\n\n // 丢弃可能损坏的已知字段,只抢救能够安全原样写回的未知扩展字段。\n const recovered = klona(document) as Record;\n delete recovered.schema;\n delete recovered.更新方式;\n\n const remove_known_nested_fields = (key: '额外模型解析配置' | '兼容性', fields: string[]) => {\n const nested = recovered[key];\n if (!_.isPlainObject(nested)) {\n delete recovered[key];\n return;\n }\n const nested_record = nested as Record;\n fields.forEach(field => delete nested_record[field]);\n if (_.isEmpty(nested_record)) {\n delete recovered[key];\n }\n };\n remove_known_nested_fields('额外模型解析配置', [\n '启用自动请求',\n '世界书条目白名单正则',\n '世界书条目黑名单正则',\n ]);\n remove_known_nested_fields('兼容性', ['更新到聊天变量', 'sendas不视为user消息']);\n\n return CharacterSettingsOverrideSchema.parse(recovered);\n}\n\nexport function serializeCharacterSettingsOverride(draft: CharacterSettingsOverride): string {\n const normalized = normalizeCharacterSettingsOverride(draft);\n const { schema: _existing_schema, ...config } = normalized;\n // schema 固定放在最后,兼顾人工阅读和 SillyTavern 的编辑体验。\n const document = {\n ...config,\n schema: getCharacterSettingsOverrideJsonSchema(),\n };\n return JSON.stringify(document, null, 4);\n}\n\nexport function hasActiveCharacterSettingsOverride(draft: CharacterSettingsOverride): boolean {\n const ordinary_paths: CharacterSettingsOverridePath[] = [\n '更新方式',\n '额外模型解析配置.启用自动请求',\n '兼容性.更新到聊天变量',\n '兼容性.sendas不视为user消息',\n ];\n if (ordinary_paths.some(path => hasOwnCharacterSettingsOverride(draft, path))) {\n return true;\n }\n\n // 空白或无效正则不会参与过滤,也不应让界面显示为“覆盖中”。\n return (\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目白名单正则 ?? '').regex !==\n undefined ||\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目黑名单正则 ?? '').regex !==\n undefined\n );\n}\n","import { tr } from '@/i18n';\n\nexport { compileRegex as compileEntryCommentRegex } from '@/util/regex';\nexport type { RegexCompileResult as EntryCommentRegexCompileResult } from '@/util/regex';\n\nexport type EntryCommentFilterSource = '用户全局配置' | '角色卡配置';\n\nexport type EntryCommentFilterResult = {\n lore: 'globalLore' | 'characterLore' | 'chatLore' | 'personaLore';\n world: string;\n comment: string;\n reason: '白名单' | '黑名单';\n sources: EntryCommentFilterSource[];\n};\n\nexport const ENTRY_COMMENT_FILTER_LOG_TITLE = tr('runtime.filter.logTitle');\n\nexport function logEntryCommentFilterResult(result: EntryCommentFilterResult[]) {\n console.log(\n ENTRY_COMMENT_FILTER_LOG_TITLE,\n result.map(entry => ({ ...entry }))\n );\n}\n\nexport function testEntryCommentRegex(regex: RegExp, comment: string): boolean {\n regex.lastIndex = 0;\n return regex.test(comment);\n}\n","import { normalizePiThinkingLevel, type PiThinkingLevel } from './pi/thinking_setting';\nimport { tr } from '@/i18n';\nimport {\n getPiProviderTargetDefinition,\n resolvePiApiKeyScope,\n} from '@/function/update/pi/provider_target';\nimport { klona } from 'klona';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './pi/credential_refs';\n\nexport type ExtraModelApiProfileBackend = 'custom' | 'pi';\n\nexport type ExtraModelApiRequestFields = {\n 破限方案: '使用内置破限' | '使用当前预设' | '使用其他预设';\n 其他预设名称: string;\n 随机头部: boolean;\n 应答格式: '聊天消息' | '工具调用' | '格式化输出' | '格式化输出(v4兼容)';\n 关闭thinking: boolean;\n 兼容假流式: boolean;\n};\n\nconst API_REQUEST_FIELDS = [\n '破限方案',\n '其他预设名称',\n '随机头部',\n '应答格式',\n '关闭thinking',\n '兼容假流式',\n] as const satisfies readonly (keyof ExtraModelApiRequestFields)[];\n\n/** 提取已配置的请求选项,保留缺失字段,供方案保存和旧方案补全使用。 */\nfunction extractApiRequestFields(\n config: Partial\n): Partial {\n return Object.fromEntries(\n API_REQUEST_FIELDS.filter(field => config[field] !== undefined).map(field => [\n field,\n config[field],\n ])\n );\n}\n\nexport type ExtraModelPiConnectionFields = {\n [key: string]: unknown;\n /** 各服务商绑定的集中 OAuth 凭证编号;空映射表示明确未登录。 */\n credentialIds?: PiCredentialReferences;\n provider: string;\n api: string;\n authType: string;\n endpoint: string;\n useProxy: boolean;\n thinkingLevel?: PiThinkingLevel;\n model: string;\n contextWindow: number | string;\n customHeaders: string;\n customIncludeBody: string;\n customExcludeBody: string;\n};\n\nexport type ExtraModelPiSettings = ExtraModelPiConnectionFields & {\n credentials: Record;\n apiKeys?: Record;\n};\n\nexport type ExtraModelApiProfile = Partial & {\n [key: string]: unknown;\n 名称: string;\n backend?: ExtraModelApiProfileBackend;\n api地址: string;\n 密钥: string;\n 模型名称: string;\n pi?: ExtraModelPiConnectionFields;\n};\n\nexport type ExtraModelApiProfileFields = Partial & {\n 模型来源?: '与插头相同' | '自定义' | '更多';\n api地址: string;\n 密钥: string;\n customApiKey?: string;\n 模型名称: string;\n pi?: ExtraModelPiSettings;\n api方案列表: ExtraModelApiProfile[];\n 当前api方案: string;\n};\n\nexport const DEFAULT_EXTRA_MODEL_API_PROFILE_NAME = '默认';\n\n/** 统一方案名两端空白,避免查找、去重和保存使用不同的名称。 */\nfunction normalizeExtraModelApiProfileName(name: string): string {\n return name.trim();\n}\n\n/**\n * 将方案中的上下文窗口规范为非负整数,0 表示使用目录值。\n * 返回 undefined 表示配置无效,调用方不得将其当作目录默认值。\n */\nexport function normalizeExtraModelPiProfileContextWindow(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isInteger(value) && value >= 0 ? value : undefined;\n }\n if (typeof value !== 'string') {\n return undefined;\n }\n\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;\n}\n\n/** 根据当前模型来源确定方案保存的是自定义连接还是 Pi 连接。 */\nfunction getExtraModelApiProfileBackend(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileBackend {\n return config.模型来源 === '更多' ? 'pi' : 'custom';\n}\n\n/**\n * 生成脱离响应式代理的连接快照,并规范连接标识与上下文窗口。\n * 快照不包含 OAuth 凭证或按目标缓存的 API Key,避免方案复制这些共享状态。\n */\nfunction clonePiConnectionFields(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): ExtraModelPiConnectionFields | undefined {\n if (pi === undefined) {\n return undefined;\n }\n\n // Settings and profile entries normally arrive as Vue/Pinia reactive proxies. Native\n // structuredClone rejects proxies with DataCloneError, while klona traverses them into\n // detached plain objects and still preserves unknown forward-compatible fields.\n const cloned = klona(pi);\n delete cloned.credentials;\n delete cloned.apiKeys;\n if (pi.credentialIds !== undefined || pi.credentials !== undefined) {\n cloned.credentialIds = resolvePiCredentialReferences(\n pi,\n (pi.credentials as Record | undefined) ?? {}\n );\n }\n // Runtime resolution treats surrounding whitespace on connection identifiers as insignificant.\n // Persist the same canonical representation so profile validation, the Source form, dirty\n // checks, and runtime resolution cannot disagree about a target after import.\n for (const field of ['provider', 'api', 'authType', 'endpoint', 'model'] as const) {\n if (typeof cloned[field] === 'string') {\n cloned[field] = cloned[field].trim();\n }\n }\n // Profiles saved before this option existed, and malformed imported values, must retain the\n // direct-transport default rather than becoming enabled through truthiness coercion.\n cloned.useProxy = cloned.useProxy === true;\n cloned.thinkingLevel = normalizePiThinkingLevel(cloned.thinkingLevel);\n const context_window = normalizeExtraModelPiProfileContextWindow(cloned.contextWindow);\n if (context_window !== undefined) {\n cloned.contextWindow = context_window;\n }\n return cloned;\n}\n\n/** 仅为完整且受支持的 API Key 目标解析密钥缓存位置;无效连接返回空字符串。 */\nfunction resolvePiConnectionApiKeyScope(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): string {\n if (\n pi === undefined ||\n typeof pi.provider !== 'string' ||\n typeof pi.api !== 'string' ||\n typeof pi.authType !== 'string' ||\n typeof pi.endpoint !== 'string'\n ) {\n return '';\n }\n\n return resolvePiApiKeyScope(\n getPiProviderTargetDefinition(pi.provider.trim()),\n pi.api.trim(),\n pi.authType.trim(),\n pi.endpoint\n );\n}\n\n/** 检查导入快照是否具备全部连接字段及合法类型,避免用默认值补成可发送连接。 */\nfunction hasCompletePiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n pi !== undefined &&\n typeof pi.provider === 'string' &&\n typeof pi.api === 'string' &&\n typeof pi.authType === 'string' &&\n (pi.authType.trim() === 'api_key' || pi.authType.trim() === 'oauth') &&\n typeof pi.endpoint === 'string' &&\n typeof pi.useProxy === 'boolean' &&\n typeof pi.model === 'string' &&\n typeof pi.contextWindow === 'number' &&\n Number.isInteger(pi.contextWindow) &&\n pi.contextWindow >= 0 &&\n typeof pi.customHeaders === 'string' &&\n typeof pi.customIncludeBody === 'string' &&\n typeof pi.customExcludeBody === 'string'\n );\n}\n\n/** 在结构完整的基础上检查服务商、协议和模型是否已填写。 */\nfunction hasConfiguredPiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n hasCompletePiConnectionSnapshot(pi) &&\n pi.provider.trim().length > 0 &&\n pi.api.trim().length > 0 &&\n pi.model.trim().length > 0\n );\n}\n\n/** 判断连接能否归属到有效的 API Key 目标,作为保留方案密钥的条件。 */\nfunction hasValidPiApiKeyTarget(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): boolean {\n return hasConfiguredPiConnectionSnapshot(pi) && resolvePiConnectionApiKeyScope(pi) !== '';\n}\n\n/** 规范单个方案的名称、后端和连接快照,并清除不属于当前认证方式的密钥。 */\nfunction normalizeExtraModelApiProfile(profile: ExtraModelApiProfile): ExtraModelApiProfile {\n const cloned = klona(profile);\n delete cloned.customApiKey;\n cloned.名称 = normalizeExtraModelApiProfileName(cloned.名称);\n const backend: ExtraModelApiProfileBackend = cloned.backend === 'pi' ? 'pi' : 'custom';\n cloned.backend = backend;\n\n if (backend === 'pi' && cloned.pi !== undefined) {\n cloned.pi = clonePiConnectionFields(cloned.pi);\n } else {\n delete cloned.pi;\n }\n if (backend === 'pi') {\n // These legacy top-level fields belong to the hidden Custom source. Pi owns its endpoint\n // and model inside `pi`, so importing or saving them here must never overwrite that cache.\n cloned.api地址 = '';\n cloned.模型名称 = '';\n\n // A profile-level key has wire meaning only for a registry-backed, normalizable API-key\n // target. OAuth and malformed/future targets retain metadata but never a dormant secret.\n if (!hasValidPiApiKeyTarget(cloned.pi)) {\n cloned.密钥 = '';\n }\n }\n return cloned;\n}\n\n/** 规范方案列表,剔除空名称和重复名称,同名方案只保留首次出现的条目。 */\nfunction normalizeExtraModelApiProfileList(\n profiles: ExtraModelApiProfile[]\n): ExtraModelApiProfile[] {\n const names = new Set();\n const normalized_profiles: ExtraModelApiProfile[] = [];\n for (const profile of profiles) {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n if (normalized_profile.名称 === '' || names.has(normalized_profile.名称)) {\n continue;\n }\n names.add(normalized_profile.名称);\n normalized_profiles.push(normalized_profile);\n }\n return normalized_profiles;\n}\n\n/** 将方案连接合并到运行设置,同时保留当前凭证仓库和目标密钥缓存。 */\nfunction mergePiConnectionFields(\n current: ExtraModelPiSettings | undefined,\n profile: ExtraModelPiConnectionFields\n): ExtraModelPiSettings {\n const current_clone = current === undefined ? undefined : klona(current);\n const profile_clone = clonePiConnectionFields(profile)!;\n return {\n ...current_clone,\n ...profile_clone,\n credentialIds: resolvePiCredentialReferences(profile, current_clone?.credentials),\n credentials: klona(current_clone?.credentials ?? {}),\n apiKeys: klona(current_clone?.apiKeys ?? {}),\n } as ExtraModelPiSettings;\n}\n\n/** 清空可发送的连接字段,保留共享凭证和密钥缓存,等待用户重新配置。 */\nfunction clearPiConnectionFields(pi: ExtraModelPiSettings): ExtraModelPiSettings {\n const cloned = klona(pi);\n return {\n ...cloned,\n credentialIds: {},\n provider: '',\n api: '',\n authType: 'api_key',\n endpoint: '',\n useProxy: false,\n thinkingLevel: 'default',\n model: '',\n contextWindow: 0,\n credentials: klona(cloned.credentials),\n apiKeys: klona(cloned.apiKeys ?? {}),\n customHeaders: '',\n customIncludeBody: '',\n customExcludeBody: '',\n };\n}\n\n/** 从当前设置提取可保存的连接和请求选项;Pi 方案必须先通过完整性校验。 */\nexport function extractExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfile {\n const backend = getExtraModelApiProfileBackend(config);\n const pi_snapshot = backend === 'pi' ? clonePiConnectionFields(config.pi) : undefined;\n if (backend === 'pi' && !hasConfiguredPiConnectionSnapshot(pi_snapshot)) {\n throw new Error(tr('runtime.apiProfile.piConfigRequired'));\n }\n return {\n 名称:\n normalizeExtraModelApiProfileName(config.当前api方案) ||\n DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n backend,\n api地址: backend === 'pi' ? '' : config.api地址,\n 密钥: backend === 'pi' && !hasValidPiApiKeyTarget(pi_snapshot) ? '' : config.密钥,\n 模型名称: backend === 'pi' ? '' : config.模型名称,\n ...extractApiRequestFields(config),\n ...(backend === 'pi' && pi_snapshot !== undefined ? { pi: pi_snapshot } : {}),\n };\n}\n\n/**\n * 应用方案的连接及请求选项,并切换到相应模型来源。\n * 无效 Pi 快照会清空活动连接,不能借用其他来源的密钥继续发送。\n */\nexport function applyExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile: ExtraModelApiProfile\n): ExtraModelApiProfileFields {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const result: ExtraModelApiProfileFields = {\n ...config,\n ...extractApiRequestFields(normalized_profile),\n 模型来源: normalized_profile.backend === 'pi' ? '更多' : '自定义',\n 密钥: normalized_profile.密钥,\n 当前api方案: normalized_profile.名称,\n ...(normalized_profile.backend === 'custom'\n ? {\n api地址: normalized_profile.api地址,\n 模型名称: normalized_profile.模型名称,\n }\n : {}),\n };\n if (normalized_profile.backend === 'pi') {\n if (hasConfiguredPiConnectionSnapshot(normalized_profile.pi)) {\n result.pi = mergePiConnectionFields(config.pi, normalized_profile.pi);\n } else {\n // Never combine a malformed Pi profile's key with the previously active provider,\n // endpoint, or model. Preserve credentials, but disable the connection until the\n // user repairs and saves a complete snapshot.\n result.密钥 = '';\n if (config.pi !== undefined) {\n result.pi = clearPiConnectionFields(config.pi);\n }\n }\n }\n return result;\n}\n\n/** 按规范化名称新增或更新方案,更新时保留已有条目的其他字段。 */\nexport function upsertExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile: ExtraModelApiProfile\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile.名称);\n if (!normalized_name) {\n throw new Error(tr('runtime.apiProfile.nameRequired'));\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(profiles);\n const next_profile = {\n ...normalizeExtraModelApiProfile(profile),\n 名称: normalized_name,\n };\n const existing_index = normalized_profiles.findIndex(item => item.名称 === normalized_name);\n if (existing_index === -1) {\n return [...normalized_profiles, next_profile];\n }\n\n const next_profiles = [...normalized_profiles];\n next_profiles[existing_index] = normalizeExtraModelApiProfile({\n ...normalized_profiles[existing_index],\n ...next_profile,\n });\n return next_profiles;\n}\n\n/** 返回删除指定方案后的规范化列表,不直接修改原列表。 */\nexport function removeExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).filter(\n profile => profile.名称 !== normalized_name\n );\n}\n\n/** 按规范化名称检查方案是否存在,供保存和删除操作校验。 */\nexport function hasExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): boolean {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).some(\n profile => profile.名称 === normalized_name\n );\n}\n\n/** 比较活动方案与当前连接、密钥及请求选项,判断是否存在未保存修改。 */\nexport function isActiveExtraModelApiProfileDirty(config: ExtraModelApiProfileFields): boolean {\n const active_name = normalizeExtraModelApiProfileName(config.当前api方案);\n if (!active_name) {\n return false;\n }\n\n const profile = normalizeExtraModelApiProfileList(config.api方案列表).find(\n item => item.名称 === active_name\n );\n if (!profile) {\n return false;\n }\n\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const backend = getExtraModelApiProfileBackend(config);\n if (normalized_profile.backend !== backend || normalized_profile.密钥 !== config.密钥) {\n return true;\n }\n\n if (\n API_REQUEST_FIELDS.some(\n field =>\n normalized_profile[field] !== undefined &&\n normalized_profile[field] !== config[field]\n )\n ) {\n return true;\n }\n\n if (\n backend === 'custom' &&\n (normalized_profile.api地址 !== config.api地址 ||\n normalized_profile.模型名称 !== config.模型名称)\n ) {\n return true;\n }\n\n return (\n backend === 'pi' && !_.isEqual(normalized_profile.pi, clonePiConnectionFields(config.pi))\n );\n}\n\n/** 解除方案绑定并清空活动连接;使用 Pi 时保留隐藏的自定义连接字段。 */\nexport function clearUnboundExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileFields {\n const preserve_hidden_custom_fields = config.模型来源 === '更多';\n return {\n ...config,\n 当前api方案: '',\n api地址: preserve_hidden_custom_fields ? config.api地址 : '',\n 密钥: '',\n 模型名称: preserve_hidden_custom_fields ? config.模型名称 : '',\n ...(config.模型来源 === '更多' && config.pi !== undefined\n ? { pi: clearPiConnectionFields(config.pi) }\n : {}),\n };\n}\n\n/** 校正活动方案引用,使列表、当前名称和连接状态保持一致。 */\nexport function reconcileExtraModelApiProfileSelection(\n config: T\n): T {\n const normalized_config = {\n ...config,\n api方案列表: normalizeExtraModelApiProfileList(config.api方案列表),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n const active_name = normalized_config.当前api方案;\n if (!active_name) {\n return normalized_config;\n }\n\n if (hasExtraModelApiProfile(normalized_config.api方案列表, active_name)) {\n return normalized_config;\n }\n\n if (normalized_config.api方案列表.length === 0) {\n return {\n ...normalized_config,\n 当前api方案: '',\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...normalized_config,\n api方案列表: normalized_config.api方案列表,\n },\n normalized_config.api方案列表[0]\n ) as T;\n}\n\n/** 删除指定的活动方案,并同步清理方案选择及相关连接字段。 */\nexport function deleteActiveExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n const remaining = removeExtraModelApiProfile(config.api方案列表, profile_name);\n if (remaining.length === 0) {\n return {\n ...clearUnboundExtraModelApiProfileFields(config),\n api方案列表: [],\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: remaining,\n },\n remaining[0]\n );\n}\n\nexport type ExtraModelApiProfileDeletionConfirmation = 'discard_unsaved_changes' | 'delete_profile';\n\n/**\n * 有未保存修改时先确认丢弃,再确认删除,任一步取消都返回 null。\n * 删除前再次核对活动方案,避免等待弹窗期间误删用户新选中的方案。\n */\nexport async function deleteActiveExtraModelApiProfileWithConfirmation(\n config: ExtraModelApiProfileFields,\n profile_name: string,\n confirm: (confirmation: ExtraModelApiProfileDeletionConfirmation) => Promise\n): Promise {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n if (isActiveExtraModelApiProfileDirty(config) && !(await confirm('discard_unsaved_changes'))) {\n return null;\n }\n\n if (!(await confirm('delete_profile'))) {\n return null;\n }\n\n return deleteActiveExtraModelApiProfile(config, profile_name);\n}\n\n/**\n * 补全方案中缺失的请求选项,并校正方案选择与密钥归属。\n * 不完整的 Pi 快照保持不可发送,不能从遗留自定义字段推导出 Pi 连接。\n */\nexport function migrateExtraModelApiProfiles(config: T): T {\n let migrated = {\n ...config,\n ...(config.pi === undefined\n ? {}\n : {\n pi: {\n ...config.pi,\n credentialIds: resolvePiCredentialReferences(\n config.pi,\n config.pi.credentials\n ),\n },\n }),\n // Old profiles only stored connection fields. Copy the current request options into\n // every profile once; saved per-profile values must survive subsequent loads/imports.\n api方案列表: normalizeExtraModelApiProfileList(\n config.api方案列表.map(profile => ({\n ...profile,\n ...(profile.backend === 'pi' && profile.pi !== undefined\n ? {\n pi: {\n ...profile.pi,\n credentialIds: resolvePiCredentialReferences(\n profile.pi,\n config.pi?.credentials\n ),\n },\n }\n : {}),\n ...extractApiRequestFields(config),\n ...extractApiRequestFields(profile),\n }))\n ),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n\n // The shared root key has wire meaning only for a registry-backed, normalizable API-key target.\n // Clear stale imported values even when there is no active profile (and before legacy-profile\n // detection), while retaining the source-specific Custom/API-key caches and OAuth store.\n if (\n migrated.模型来源 === '更多' &&\n resolvePiConnectionApiKeyScope(migrated.pi) === '' &&\n migrated.密钥 !== ''\n ) {\n migrated = {\n ...migrated,\n 密钥: '',\n } as T;\n }\n\n const active_profile = migrated.api方案列表.find(\n profile => profile.名称 === migrated.当前api方案\n );\n if (active_profile?.backend === 'pi' && !hasConfiguredPiConnectionSnapshot(active_profile.pi)) {\n migrated = applyExtraModelApiProfile(migrated, active_profile) as T;\n }\n\n if (migrated.api方案列表.length === 0) {\n const has_legacy_custom_api =\n migrated.api地址.trim().length > 0 ||\n migrated.密钥.trim().length > 0 ||\n migrated.模型名称.trim().length > 0;\n if (has_legacy_custom_api) {\n // A newly selected but not yet configured Pi source may still carry legacy root\n // Custom fields. Do not turn those unrelated fields into a sendable Pi profile.\n if (\n getExtraModelApiProfileBackend(migrated) === 'pi' &&\n !hasConfiguredPiConnectionSnapshot(migrated.pi)\n ) {\n return reconcileExtraModelApiProfileSelection(migrated);\n }\n const profile = extractExtraModelApiProfileFields(migrated);\n migrated = {\n ...migrated,\n api方案列表: [\n {\n ...profile,\n 名称: DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n },\n ],\n 当前api方案: migrated.当前api方案 || DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n };\n }\n }\n\n return reconcileExtraModelApiProfileSelection(migrated);\n}\n\n/** 按名称查找并应用方案;不存在时抛出可展示的配置错误。 */\nexport function selectExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const profile = normalized_profiles.find(item => item.名称 === normalized_name);\n if (!profile) {\n throw new Error(\n tr('runtime.apiProfile.notFound', {\n name: normalized_name,\n })\n );\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: normalized_profiles,\n },\n profile\n );\n}\n\n/** 保存当前连接和请求选项,支持更新活动方案或指定名称,并防止覆盖其他同名方案。 */\nexport function saveCurrentExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name?: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name ?? config.当前api方案);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.selectOrEnterName'));\n }\n\n if (\n hasExtraModelApiProfile(config.api方案列表, target_name) &&\n normalizeExtraModelApiProfileName(config.当前api方案) !== target_name\n ) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const source_profile = normalized_profiles.find(\n item => item.名称 === normalizeExtraModelApiProfileName(config.当前api方案)\n );\n const normalized_source_profile =\n source_profile === undefined ? undefined : normalizeExtraModelApiProfile(source_profile);\n const profile: ExtraModelApiProfile = {\n ...normalized_source_profile,\n ...extractExtraModelApiProfileFields(config),\n 名称: target_name,\n };\n if (profile.backend !== 'pi') {\n delete profile.pi;\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: upsertExtraModelApiProfile(normalized_profiles, profile),\n },\n profile\n );\n}\n\n/** 校验新名称不为空且未占用,再将当前配置另存为独立方案。 */\nexport function saveAsNewExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.enterNewName'));\n }\n if (hasExtraModelApiProfile(config.api方案列表, target_name)) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n return saveCurrentExtraModelApiProfile(config, target_name);\n}\n","/** 兼容旧浏览器的 AbortSignal 静态接口;可选方法由运行时检测后补齐。 */\ntype AbortSignalConstructorWithPolyfills = {\n any?: (signals: Iterable) => AbortSignal;\n timeout?: (milliseconds: number) => AbortSignal;\n};\n\n/**\n * 为旧版酒馆浏览器补齐 Pi OAuth 刷新所需的 AbortSignal.timeout 和 any。\n * 仅在原生实现缺失时安装,并在取消后解除组合信号的监听。\n */\nexport function installPiAbortSignalPolyfills(): void {\n const constructor = AbortSignal as unknown as AbortSignalConstructorWithPolyfills;\n if (typeof constructor.timeout !== 'function') {\n Object.defineProperty(constructor, 'timeout', {\n configurable: true,\n writable: true,\n /** 创建到期自动取消的信号,拒绝负数和非有限超时时间。 */\n value(milliseconds: number): AbortSignal {\n if (!Number.isFinite(milliseconds) || milliseconds < 0) {\n throw new RangeError(\n 'AbortSignal timeout must be a non-negative finite number'\n );\n }\n const controller = new AbortController();\n setTimeout(\n () =>\n controller.abort(\n new DOMException('The operation timed out', 'TimeoutError')\n ),\n milliseconds\n );\n return controller.signal;\n },\n });\n }\n\n if (typeof constructor.any === 'function') {\n return;\n }\n\n Object.defineProperty(constructor, 'any', {\n configurable: true,\n writable: true,\n /** 合并多个取消信号,首次取消时保留原始原因并清理其他监听。 */\n value(signals: Iterable): AbortSignal {\n const controller = new AbortController();\n const listeners = new Map void>();\n /** 解除已注册的取消监听,避免组合信号结束后继续持有输入信号。 */\n const cleanup = () => {\n for (const [signal, listener] of listeners) {\n signal.removeEventListener('abort', listener);\n }\n listeners.clear();\n };\n /** 只转发首次取消原因,并统一释放组合信号的监听。 */\n const forward = (signal: AbortSignal) => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n cleanup();\n };\n\n for (const signal of signals) {\n if (listeners.has(signal)) {\n continue;\n }\n if (signal.aborted) {\n forward(signal);\n break;\n }\n /** 将当前输入信号的取消传递给组合信号。 */\n const listener = () => forward(signal);\n listeners.set(signal, listener);\n signal.addEventListener('abort', listener, { once: true });\n }\n return controller.signal;\n },\n });\n}\n","import type { ProviderHeaders } from '@earendil-works/pi-ai';\nimport YAML from 'yaml';\n\n/** 解析用户输入的 YAML 或 JSON;空输入表示未配置,语法错误只返回字段级提示。 */\nfunction parseYaml(value: string, label: string): unknown {\n if (!value.trim()) {\n return undefined;\n }\n try {\n return YAML.parse(value);\n } catch {\n throw new Error(`More source ${label} is not valid YAML or JSON`);\n }\n}\n\n/** 判断解析结果是否为非空、非数组对象,供请求配置的结构校验使用。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 解析自定义请求头,允许字符串或 null,并禁止覆盖由认证层管理的密钥头。 */\nexport function parsePiCustomHeaders(value: string): ProviderHeaders | undefined {\n const parsed = parseYaml(value, 'customHeaders');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customHeaders must be an object');\n }\n const headers: ProviderHeaders = {};\n for (const [name, header_value] of Object.entries(parsed)) {\n const normalized_name = name.trim();\n if (!normalized_name || (typeof header_value !== 'string' && header_value !== null)) {\n throw new Error('More source customHeaders values must be strings or null');\n }\n if (\n /^(authorization|proxy-authorization|x-api-key|x-goog-api-key)$/i.test(normalized_name)\n ) {\n throw new Error(\n `More source customHeaders cannot override authentication header '${normalized_name}'`\n );\n }\n headers[normalized_name] = header_value;\n }\n return headers;\n}\n\n/** 解析要合并到请求体的对象;空输入不产生覆盖。 */\nexport function parsePiCustomIncludeBody(value: string): Record | undefined {\n const parsed = parseYaml(value, 'customIncludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customIncludeBody must be an object');\n }\n return parsed;\n}\n\n/** 将数组或逗号、换行分隔的字段名规范为去重列表,拒绝空名称和非字符串。 */\nexport function parsePiCustomExcludeBody(value: string): string[] | undefined {\n const parsed = parseYaml(value, 'customExcludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n const fields =\n typeof parsed === 'string'\n ? parsed\n .split(/[\\n,]/)\n .map(item => item.trim())\n .filter(Boolean)\n : parsed;\n if (\n !Array.isArray(fields) ||\n fields.some(field => typeof field !== 'string' || !field.trim())\n ) {\n throw new Error('More source customExcludeBody must be an array of field names');\n }\n return [...new Set(fields.map(field => field.trim()))];\n}\n","import { getPendingPromptCaptureDiagnostics } from './prompt_capture';\n\nconst pi_request_controllers = new Map();\nconst pi_request_attempts = new Map();\n\nexport class PiRequestAbortedError extends Error {\n readonly generationId: string;\n\n /** 将取消原因与生成编号绑定,供重试策略和界面识别主动停止。 */\n constructor(generationId: string, reason?: unknown) {\n const detail =\n reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';\n super(\n detail\n ? `More source request '${generationId}' was aborted: ${detail}`\n : `More source request '${generationId}' was aborted`\n );\n this.name = 'PiRequestAbortedError';\n this.generationId = generationId;\n }\n}\n\n/** 识别 Pi 的主动取消错误,避免将其当作普通请求失败重试。 */\nexport function isPiRequestAbortedError(error: unknown): error is PiRequestAbortedError {\n return error instanceof PiRequestAbortedError;\n}\n\n/** 实际服务商请求的取消句柄,连接本次请求、用户停止和完整尝试的取消信号。 */\nexport type PiRequestControllerRegistration = {\n controller: AbortController;\n signal: AbortSignal;\n /** 移除本次登记和关联监听;释放本身不会取消请求。 */\n release: () => void;\n};\n\n/** 跨越提示词捕获与服务商请求的尝试句柄,用于记住两阶段之间发生的取消。 */\nexport type PiRequestAttemptRegistration = {\n signal: AbortSignal;\n /** 完整尝试结束后移除登记;调用多次不会影响同编号的新尝试。 */\n release: () => void;\n};\n\n/**\n * 为“提示词捕获 → 服务商请求”的完整尝试保留生成编号和取消信号。\n * 该记录在捕获结束后仍保留停止标记,防止间隙中的取消被后续请求注册绕过。\n */\nexport function beginPiRequestAttempt(generation_id: string): PiRequestAttemptRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_attempts.has(generation_id) || pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n pi_request_attempts.set(generation_id, controller);\n let released = false;\n /** 只释放本次尝试登记的记录,避免清理同编号的新实例。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n if (pi_request_attempts.get(generation_id) === controller) {\n pi_request_attempts.delete(generation_id);\n }\n };\n\n return { signal: controller.signal, release };\n}\n\n/** 登记实际请求控制器,并继承完整尝试已发生或随后发生的取消。 */\nexport function registerPiRequestController(\n generation_id: string,\n caller_signal?: AbortSignal\n): PiRequestControllerRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n const attempt_signal = pi_request_attempts.get(generation_id)?.signal;\n const abort_sources = [\n ...new Set(\n [attempt_signal, caller_signal].filter(\n (signal): signal is AbortSignal => signal !== undefined\n )\n ),\n ];\n const abort_listeners = abort_sources.map(signal => {\n /** 把调用方或尝试层信号的取消原因传给实际请求控制器。 */\n const forward_abort = () => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n };\n signal.addEventListener('abort', forward_abort, { once: true });\n if (signal.aborted) {\n forward_abort();\n }\n return { signal, forward_abort };\n });\n\n pi_request_controllers.set(generation_id, controller);\n let released = false;\n /** 解除上游取消监听,并只移除当前请求的登记项。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n for (const { signal, forward_abort } of abort_listeners) {\n signal.removeEventListener('abort', forward_abort);\n }\n if (pi_request_controllers.get(generation_id) === controller) {\n pi_request_controllers.delete(generation_id);\n }\n };\n\n return { controller, signal: controller.signal, release };\n}\n\n/** 在实际请求执行期间登记控制器,无论成功、失败还是取消都释放登记。 */\nexport async function withPiRequestController(\n generation_id: string,\n run: (signal: AbortSignal) => Promise,\n caller_signal?: AbortSignal\n): Promise {\n const registration = registerPiRequestController(generation_id, caller_signal);\n try {\n return await run(registration.signal);\n } finally {\n registration.release();\n }\n}\n\n/** 按生成编号取消 Pi 尝试和实际请求,保留可供后续注册检查的停止状态。 */\nexport function stopPiRequestById(generation_id: string, reason?: unknown): boolean {\n const controllers = [\n pi_request_attempts.get(generation_id),\n pi_request_controllers.get(generation_id),\n ];\n let stopped = false;\n for (const controller of controllers) {\n if (!controller || controller.signal.aborted) {\n continue;\n }\n controller.abort(reason ?? new PiRequestAbortedError(generation_id));\n stopped = true;\n }\n return stopped;\n}\n\n/** 同时停止同一编号的 Slash 捕获和 Pi 请求,覆盖生成链路的各个阶段。 */\nexport function stopExtraModelRequestById(generation_id: string, reason?: unknown): boolean {\n const slash_stopped = stopGenerationById(generation_id);\n const pi_stopped = stopPiRequestById(generation_id, reason);\n return slash_stopped || pi_stopped;\n}\n\n/** 取消所有仍活动的 Pi 尝试与请求,并返回受影响的生成编号数量。 */\nexport function stopAllPiRequests(reason?: unknown): number {\n const ids = [\n ...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()]),\n ].filter(id => {\n const attempt = pi_request_attempts.get(id);\n const runtime = pi_request_controllers.get(id);\n return (\n (attempt !== undefined && !attempt.signal.aborted) ||\n (runtime !== undefined && !runtime.signal.aborted)\n );\n });\n for (const id of ids) {\n stopPiRequestById(id, reason);\n }\n return ids.length;\n}\n\n/** 停止全部额外模型尝试,包括仍处于 Slash 提示词捕获阶段的请求。 */\nexport function stopAllExtraModelRequests(reason?: unknown): number {\n const ids = new Set([\n ...getPendingPromptCaptureDiagnostics().map(capture => capture.generationId),\n ...pi_request_attempts.keys(),\n ...pi_request_controllers.keys(),\n ]);\n let stopped = 0;\n for (const id of ids) {\n if (stopExtraModelRequestById(id, reason)) {\n stopped += 1;\n }\n }\n return stopped;\n}\n\n/** 返回当前登记的生成编号,供取消操作及生命周期诊断使用。 */\nexport function getActivePiRequestIds(): readonly string[] {\n return [...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()])];\n}\n\n/**\n * 脚本卸载时取消全部请求,但保留登记项直到各自结束。\n * 尝试记录仍充当停止标记,防止并发完成的捕获在卸载后发起新请求。\n */\nexport function clearPiRequestControllers(): void {\n stopAllExtraModelRequests(new Error('More source request registry disposed'));\n}\n","/** 连接与方案仅保存服务商到集中凭证记录的引用,不复制会轮换的 refresh token。 */\nexport type PiCredentialReferences = Record;\n\n/** 接受旧版服务商键和带服务商命名空间的新凭证编号,拒绝跨服务商引用。 */\nexport function isPiCredentialId(providerId: string, credentialId: string): boolean {\n return (\n credentialId === providerId ||\n credentialId.startsWith(`oauth:${encodeURIComponent(providerId)}:`)\n );\n}\n\n/** 规范显式引用;缺少字段的旧设置才从原 providerId 记录一次性继承登录态。 */\nexport function resolvePiCredentialReferences(\n pi: { credentialIds?: unknown },\n credentials: Record = {}\n): PiCredentialReferences {\n if (pi.credentialIds !== undefined) {\n if (\n typeof pi.credentialIds !== 'object' ||\n pi.credentialIds === null ||\n Array.isArray(pi.credentialIds)\n ) {\n return {};\n }\n return Object.fromEntries(\n Object.entries(pi.credentialIds).filter(\n ([providerId, id]) => typeof id === 'string' && isPiCredentialId(providerId, id)\n )\n );\n }\n return Object.fromEntries(\n Object.entries(credentials)\n .filter(\n ([id, value]) =>\n !id.startsWith('oauth:') &&\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'oauth'\n )\n .map(([providerId]) => [providerId, providerId])\n );\n}\n","import { useDataStore } from '@/store';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './credential_refs';\nimport type {\n AuthOperationOptions,\n Credential,\n CredentialInfo,\n CredentialStore,\n OAuthCredential,\n} from './pi_gateway';\n\n/** 按凭证编号排队执行的凭证操作,进入队列前不提前读取或修改凭证。 */\ntype CredentialTask = () => Promise;\n\n/** 复制 OAuth 凭证,避免调用方修改持久化对象本身。 */\nfunction cloneOAuthCredential(credential: OAuthCredential): OAuthCredential {\n return { ...credential };\n}\n\n/**\n * 从宽松保存的设置中读取符合 Pi 契约的 OAuth 凭证。\n * 未知字段可以保留,但缺失或无效的 access、refresh 和 expires 不进入运行时。\n */\nfunction readOAuthCredential(value: unknown): OAuthCredential | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return undefined;\n }\n\n const candidate = value as Record;\n if (\n candidate.type !== 'oauth' ||\n typeof candidate.access !== 'string' ||\n candidate.access.trim().length === 0 ||\n typeof candidate.refresh !== 'string' ||\n candidate.refresh.trim().length === 0 ||\n typeof candidate.expires !== 'number' ||\n !Number.isFinite(candidate.expires)\n ) {\n return undefined;\n }\n\n return cloneOAuthCredential(candidate as OAuthCredential);\n}\n\n/** 所有捕获同一设置仓库的适配器共用真实凭证编号的队列。 */\nconst storeChains = new WeakMap>>();\n/** 登录和解绑的代次按所属连接记录,阻止已经退出的待完成登录重新绑定。 */\nconst bindingGenerations = new WeakMap>();\n\ntype DataStore = ReturnType;\ntype CredentialStoreHooks = {\n beforeWrite?: (providerId: string, credentialId: string) => void;\n /** 返回 false 时仅解除绑定,保留其他方案仍使用的集中凭证。 */\n beforeDelete?: (providerId: string, credentialId: string | undefined) => boolean;\n};\n\n/** 在读取或写入凭证前检查调用方是否已取消操作。 */\nfunction throwIfAborted(options?: AuthOperationOptions): void {\n options?.signal?.throwIfAborted();\n}\n\n/** 取消时立即结束调用方等待,同时允许内部串行任务安全收尾。 */\nfunction raceWithAbort(task: Promise, signal?: AbortSignal): Promise {\n if (signal === undefined) {\n return task;\n }\n\n if (signal.aborted) {\n return Promise.reject(signal.reason);\n }\n return new Promise((resolve, reject) => {\n /** 解除取消监听并使用原始取消原因拒绝等待中的调用。 */\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort);\n reject(signal.reason);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n\n task.then(\n value => {\n signal.removeEventListener('abort', onAbort);\n resolve(value);\n },\n error => {\n signal.removeEventListener('abort', onAbort);\n reject(error);\n }\n );\n });\n}\n\n/**\n * 创建基于设置持久化的 OAuth 凭证仓库。\n * 同一凭证编号的修改和删除串行执行,写入前复查取消状态,避免刷新覆盖登出或新凭证。\n */\nexport function createPiCredentialStore(\n credentialIds?: PiCredentialReferences,\n dataStore: DataStore = useDataStore(),\n hooks: CredentialStoreHooks = {}\n): CredentialStore {\n const references = credentialIds === undefined ? undefined : { ...credentialIds };\n const getPersistedCredentials = () => dataStore.settings.额外模型解析配置.pi.credentials;\n const resolveId = (providerId: string) =>\n references === undefined ? providerId : references[providerId];\n const chains = storeChains.get(dataStore) ?? new Map>();\n storeChains.set(dataStore, chains);\n\n /** 按凭证编号串行安排凭证修改;前序失败不阻塞后续任务,取消后也保持队列顺序。 */\n const enqueue = (\n providerId: string,\n task: CredentialTask,\n options?: AuthOperationOptions\n ): Promise => {\n const previous = chains.get(providerId) ?? Promise.resolve();\n const queued = (async () => {\n throwIfAborted(options);\n await previous.catch(() => undefined);\n throwIfAborted(options);\n return task();\n })();\n const tail = queued.then(\n () => undefined,\n () => undefined\n );\n chains.set(providerId, tail);\n void tail.then(() => {\n if (chains.get(providerId) === tail) {\n chains.delete(providerId);\n }\n });\n\n return raceWithAbort(queued, options?.signal);\n };\n\n return {\n /** 读取并返回有效凭证的副本,忽略仓库中无法识别的记录。 */\n async read(\n providerId: string,\n options?: AuthOperationOptions\n ): Promise {\n throwIfAborted(options);\n const credentialId = resolveId(providerId);\n const credential =\n credentialId === undefined\n ? undefined\n : readOAuthCredential(getPersistedCredentials()[credentialId]);\n return credential === undefined ? undefined : cloneOAuthCredential(credential);\n },\n\n /** 列出有效 OAuth 凭证的服务商和类型,不向列表调用方暴露令牌内容。 */\n async list(options?: AuthOperationOptions): Promise {\n throwIfAborted(options);\n const ids =\n references ??\n Object.fromEntries(Object.keys(getPersistedCredentials()).map(id => [id, id]));\n return Object.entries(ids).flatMap(([providerId, credentialId]) =>\n readOAuthCredential(getPersistedCredentials()[credentialId]) === undefined\n ? []\n : [{ providerId, type: 'oauth' as const }]\n );\n },\n\n /** 在凭证队列内读取、更新并保存凭证,提交前再次检查取消信号。 */\n modify(\n providerId: string,\n fn: (current: Credential | undefined) => Promise,\n options?: AuthOperationOptions\n ): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.reject(new Error('No OAuth credential is bound to this connection'));\n }\n return enqueue(\n credentialId,\n async () => {\n const current = readOAuthCredential(getPersistedCredentials()[credentialId]);\n const next = await fn(\n current === undefined ? undefined : cloneOAuthCredential(current)\n );\n throwIfAborted(options);\n\n if (next === undefined) {\n return current === undefined ? undefined : cloneOAuthCredential(current);\n }\n\n const oauth = readOAuthCredential(next);\n if (oauth === undefined) {\n throw new TypeError(\n 'More source credential store only persists valid OAuth credentials'\n );\n }\n\n const persisted = cloneOAuthCredential(oauth);\n hooks.beforeWrite?.(providerId, credentialId);\n getPersistedCredentials()[credentialId] = persisted;\n return cloneOAuthCredential(persisted);\n },\n options\n );\n },\n\n /** 在同一凭证队列中删除凭证,与自动或手动刷新保持确定的先后顺序。 */\n delete(providerId: string, options?: AuthOperationOptions): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.resolve().then(() => {\n throwIfAborted(options);\n hooks.beforeDelete?.(providerId, undefined);\n });\n }\n return enqueue(\n credentialId,\n async () => {\n throwIfAborted(options);\n if (hooks.beforeDelete?.(providerId, credentialId) !== false) {\n delete getPersistedCredentials()[credentialId];\n }\n },\n options\n );\n },\n };\n}\n\n/** 捕获操作发起时所属的方案及活动连接,回调提交前复核原引用。 */\nfunction captureBinding(dataStore: DataStore) {\n const config = dataStore.settings.额外模型解析配置;\n const profileName = config.当前api方案;\n const profile = config.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const activeReferences = resolvePiCredentialReferences(config.pi, config.pi.credentials);\n const profileReferences =\n profile?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(profile.pi, config.pi.credentials);\n const source = config.模型来源;\n const provider = config.pi.provider;\n const authType = config.pi.authType;\n const generations = bindingGenerations.get(dataStore) ?? new Map();\n bindingGenerations.set(dataStore, generations);\n const scope = JSON.stringify([profileName, source, provider, authType]);\n const observedGenerations = new Map(generations);\n\n /** 仅修改发起操作时的所属方案;活动连接只有仍绑定原值时才同步更新。 */\n const replace = (providerId: string, nextId: string | undefined) => {\n const generationKey = `${scope}:${providerId}`;\n const generation = observedGenerations.get(generationKey) ?? 0;\n if ((generations.get(generationKey) ?? 0) !== generation) {\n throw new Error('The OAuth connection was changed by another authentication operation');\n }\n const current = dataStore.settings.额外模型解析配置;\n const target =\n profileReferences === undefined\n ? undefined\n : current.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const isActive =\n current.当前api方案 === profileName &&\n current.模型来源 === source &&\n current.pi.provider === provider &&\n current.pi.authType === authType;\n const currentRefs = resolvePiCredentialReferences(current.pi, current.pi.credentials);\n const targetRefs =\n target?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(target.pi, current.pi.credentials);\n if (isActive && currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The active OAuth credential changed while authentication was pending');\n }\n if (profileReferences !== undefined) {\n if (!target?.pi || targetRefs?.[providerId] !== profileReferences[providerId]) {\n throw new Error('The OAuth profile changed while authentication was pending');\n }\n } else if (!isActive || currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The OAuth connection changed while authentication was pending');\n }\n const updated = (refs: PiCredentialReferences) => {\n const result = { ...refs };\n if (nextId === undefined) delete result[providerId];\n else result[providerId] = nextId;\n return result;\n };\n generations.set(generationKey, generation + 1);\n if (target?.pi && targetRefs) target.pi.credentialIds = updated(targetRefs);\n if (isActive && currentRefs[providerId] === activeReferences[providerId]) {\n current.pi.credentialIds = updated(currentRefs);\n }\n };\n return { references: activeReferences, replace };\n}\n\n/** 是否仍有方案或活动连接引用该凭证;未迁移的旧方案也参与引用检查。 */\nfunction isCredentialReferenced(dataStore: DataStore, credentialId: string): boolean {\n const config = dataStore.settings.额外模型解析配置;\n const connections = [\n config.pi,\n ...config.api方案列表.flatMap(profile =>\n profile.backend === 'pi' && profile.pi ? [profile.pi] : []\n ),\n ];\n return connections.some(pi =>\n Object.values(resolvePiCredentialReferences(pi, config.pi.credentials)).includes(\n credentialId\n )\n );\n}\n\n/** 捕获当前连接的凭证引用;随后切换方案不会改变此对象读写的账号。 */\nexport function getPiCredentialStore(pi?: { credentialIds?: unknown }): CredentialStore {\n const dataStore = useDataStore();\n const settings = dataStore.settings.额外模型解析配置.pi;\n const references = resolvePiCredentialReferences(pi ?? settings, settings.credentials);\n return createPiCredentialStore(references, dataStore);\n}\n\n/** 捕获退出登录的所属方案,仅解绑该方案,最后一个引用消失后才删除集中凭证。 */\nexport function createPiOAuthLogoutStore(): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n return createPiCredentialStore(binding.references, dataStore, {\n beforeDelete(providerId, credentialId) {\n binding.replace(providerId, undefined);\n return credentialId !== undefined && !isCredentialReferenced(dataStore, credentialId);\n },\n });\n}\n\n/** 为登录结果分配全新记录,成功提交时才绑定原方案,避免覆盖其他账号或失败时丢失旧登录态。 */\nexport function createPiOAuthLoginStore(providerId: string, nonce: string): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n const id = `oauth:${encodeURIComponent(providerId)}:${nonce}`;\n return createPiCredentialStore({ [providerId]: id }, dataStore, {\n beforeWrite(writtenProvider, credentialId) {\n binding.replace(writtenProvider, credentialId);\n },\n });\n}\n","import {\n GoogleGenAI,\n type GenerateContentConfig,\n type GenerateContentParameters,\n type ThinkingConfig,\n} from '@google/genai';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n buildBaseOptions,\n calculateCost,\n clampThinkingLevel,\n collapseSystemMessages,\n convertGoogleMessages,\n convertGoogleTools,\n createAssistantMessageEventStream,\n getCurrentSystemPrompt,\n getCurrentTools,\n getDisabledGoogleThinkingConfig,\n isGoogleThinkingPart,\n mapGoogleStopReason,\n resolveGoogleFunctionCallingMode,\n resolveGoogleThinkingLevel,\n retainGoogleThoughtSignature,\n retryGoogleRequest,\n supportsGoogleStrictToolSampling,\n toGoogleThinkingLevel,\n usesGoogleThinkingLevel,\n type Api,\n type AssistantMessage,\n type AssistantMessageEventStream,\n type FetchFunction,\n type GoogleApiThinkingLevel,\n type Model,\n type ProviderHeaders,\n type ProviderStreams,\n type ResolvedGoogleThinkingLevel,\n type SimpleStreamOptions,\n type StreamOptions,\n type TextContent,\n type ThinkingBudgets,\n type ThinkingContent,\n type ToolCall,\n type TranscriptContext,\n} from './pi_gateway';\n\n/**\n * 当前锁定的 Google SDK 未公开实例级 fetch 选项,但内部 apiClient 保留了 HTTP 入口。\n * 只声明经过核对的最小结构;SDK 升级后若不再匹配,发送带凭证请求前即停止。\n * 该入口只替换当前实例的传输,不修改全局 fetch 或 SDK 原型。\n */\ntype InjectableGoogleClient = {\n apiClient?: {\n apiCall?: (\n url: string,\n init: RequestInit,\n retryOptions?: unknown,\n timeout?: number,\n abortSignal?: AbortSignal\n ) => Promise;\n };\n};\n\nconst GOOGLE_TRANSPORT_PROBE_KEY = 'mvu-google-transport-compatibility-check';\n\nexport class GoogleProxyAdapterCompatibilityError extends Error {\n readonly code = 'google_proxy_adapter_incompatible';\n readonly retryable = false;\n\n /** 将 SDK 传输注入点不兼容标记为不可重试错误,避免被包装成普通服务商失败。 */\n constructor() {\n super(\n 'The installed Google Generative AI SDK no longer exposes the audited per-client HTTP transport required by the SillyTavern proxy.'\n );\n this.name = 'GoogleProxyAdapterCompatibilityError';\n }\n}\n\n/** Google 自定义传输路径的请求选项,补齐通用流选项之外的工具选择和思考控制。 */\nexport interface GoogleProxyOptions extends StreamOptions {\n toolChoice?: 'auto' | 'none' | 'any';\n thinking?: {\n enabled: boolean;\n /** -1 由 Google 动态分配预算;0 关闭基于预算的思考。 */\n budgetTokens?: number;\n level?: GoogleApiThinkingLevel;\n };\n}\n\n/**\n * 仅替换单个 Google 客户端的 HTTP 调用实现,保留取消和超时信号。\n * 先验证当前 SDK 注入点;不修改全局 fetch 或客户端原型。\n */\nexport function installGoogleClientFetch(client: unknown, fetch_impl: FetchFunction): void {\n const api_client = (client as InjectableGoogleClient | null)?.apiClient;\n if (\n !api_client ||\n typeof api_client.apiCall !== 'function' ||\n api_client.apiCall.length !== 5\n ) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n\n installPiAbortSignalPolyfills();\n // GenAI 2.x moved abort/timeout out of RequestInit into apiCall arguments. Pi owns retries,\n // but the injected transport must preserve those signals through response-body consumption.\n /** 合并 RequestInit、SDK 参数和超时产生的信号,再交给本次请求的传输实现。 */\n const injected = (\n url: string,\n init: RequestInit,\n _retry_options?: unknown,\n timeout?: number,\n abort_signal?: AbortSignal\n ) => {\n const signals = [init.signal, abort_signal].filter(\n (signal): signal is AbortSignal => signal != null\n );\n if (timeout !== undefined && timeout > 0) {\n signals.push(AbortSignal.timeout(timeout));\n }\n if (signals.length === 0) {\n return fetch_impl(url, init);\n }\n const signal = AbortSignal.any(signals);\n signal.throwIfAborted();\n return fetch_impl(url, { ...init, signal });\n };\n try {\n api_client.apiCall = injected;\n } catch {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n if (api_client.apiCall !== injected) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n}\n\n/**\n * 在 Pi 懒加载流边界之前验证 SDK 注入点,保留明确的兼容错误。\n * 构造客户端与校验过程不发送网络请求。\n */\nexport function assertGoogleProxyAdapterCompatible(): void {\n const client = new GoogleGenAI({ apiKey: GOOGLE_TRANSPORT_PROBE_KEY });\n installGoogleClientFetch(client, async () => {\n throw new GoogleProxyAdapterCompatibilityError();\n });\n}\n\n/** 仅在请求指定了不同于全局 fetch 的传输实现时启用本地桥接。 */\nfunction shouldUseInjectedFetch(\n fetch_impl: FetchFunction | undefined\n): fetch_impl is FetchFunction {\n return fetch_impl !== undefined && fetch_impl !== globalThis.fetch;\n}\n\n/** 移除表示删除操作的 null 请求头,转换为 Google SDK 接受的字符串映射。 */\nfunction providerHeadersToRecord(\n headers: ProviderHeaders | undefined\n): Record | undefined {\n if (!headers) {\n return undefined;\n }\n const result: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== null) {\n result[key] = value;\n }\n }\n return Object.keys(result).length === 0 ? undefined : result;\n}\n\n/** 删除孤立的 UTF-16 代理码元,避免系统提示词编码失败。 */\nfunction sanitizeSurrogates(text: string): string {\n return text.replace(\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?): AssistantMessage {\n return {\n role: 'assistant',\n content: [],\n api: 'google-generative-ai' as Api,\n provider: model.provider,\n model: model.id,\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'pending',\n timestamp: Date.now(),\n };\n}\n\n/** 按本次模型端点和请求头创建 Google 客户端,并安装仅作用于该实例的 fetch。 */\nfunction createClient(\n model: Model<'google-generative-ai'>,\n api_key: string,\n options_headers: ProviderHeaders | undefined,\n fetch_impl: FetchFunction\n): GoogleGenAI {\n const http_options: {\n baseUrl?: string;\n apiVersion?: string;\n headers?: Record;\n } = {};\n if (model.baseUrl) {\n http_options.baseUrl = model.baseUrl;\n // Pi model base URLs already include their API version path.\n http_options.apiVersion = '';\n }\n const headers = providerHeadersToRecord({\n 'User-Agent': 'pi (browser)',\n ...model.headers,\n ...options_headers,\n });\n if (headers) {\n http_options.headers = headers;\n }\n\n const client = new GoogleGenAI({\n apiKey: api_key,\n httpOptions: Object.keys(http_options).length === 0 ? undefined : http_options,\n });\n installGoogleClientFetch(client, fetch_impl);\n return client;\n}\n\n/** 将 Pi 上下文、工具、采样和思考配置转换为 Google 生成参数,并保留取消信号。 */\nfunction buildParams(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions = {}\n): GenerateContentParameters {\n // Pi 1.x 将系统提示词和工具声明放进 transcript;Google 使用折叠后的系统消息。\n const normalized_context = collapseSystemMessages(context);\n const contents = convertGoogleMessages(model, normalized_context);\n const system_prompt = getCurrentSystemPrompt(normalized_context.messages);\n const tools = getCurrentTools(normalized_context.messages);\n const generation_config: GenerateContentConfig = {};\n if (options.temperature !== undefined) {\n generation_config.temperature = options.temperature;\n }\n if (options.maxTokens !== undefined) {\n generation_config.maxOutputTokens = options.maxTokens;\n }\n\n const supports_strict_mode = supportsGoogleStrictToolSampling(model.id);\n const function_calling_mode = tools.length\n ? resolveGoogleFunctionCallingMode(tools, options.toolChoice, supports_strict_mode)\n : undefined;\n const config: GenerateContentConfig = {\n ...(Object.keys(generation_config).length === 0 ? {} : generation_config),\n ...(system_prompt ? { systemInstruction: sanitizeSurrogates(system_prompt) } : {}),\n ...(tools.length ? { tools: convertGoogleTools(tools, false, supports_strict_mode) } : {}),\n ...(function_calling_mode === undefined\n ? {}\n : { toolConfig: { functionCallingConfig: { mode: function_calling_mode } } }),\n };\n\n if (options.thinking?.enabled && model.reasoning) {\n const thinking_config: ThinkingConfig = { includeThoughts: true };\n if (options.thinking.level !== undefined) {\n thinking_config.thinkingLevel = options.thinking\n .level as ThinkingConfig['thinkingLevel'];\n } else if (options.thinking.budgetTokens !== undefined) {\n thinking_config.thinkingBudget = options.thinking.budgetTokens;\n }\n config.thinkingConfig = thinking_config;\n } else if (model.reasoning && options.thinking && !options.thinking.enabled) {\n config.thinkingConfig = getDisabledGoogleThinkingConfig(model);\n }\n\n if (options.signal) {\n if (options.signal.aborted) {\n throw new Error('Request aborted');\n }\n config.abortSignal = options.signal;\n }\n\n return {\n model: model.id,\n contents,\n config,\n };\n}\n\nlet tool_call_counter = 0;\n\n/**\n * 通过注入的传输执行 Google 生成,并转换为 Pi 的文本、思考、工具和用量事件。\n * 客户端兼容性同步校验,避免初始化失败被吞为普通模型响应。\n */\nfunction streamWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const output = createOutput(model);\n const stream = createAssistantMessageEventStream();\n const api_key = options.apiKey;\n\n // Construct and verify the instance-local injection point synchronously. This cannot perform\n // network I/O, and preserves the compatibility error instead of converting it to provider data.\n const client = api_key ? createClient(model, api_key, options.headers, fetch_impl) : undefined;\n\n void (async () => {\n try {\n if (!api_key || !client) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n\n let params = buildParams(model, context, options);\n const next_params = await options.onPayload?.(params, model);\n if (next_params !== undefined) {\n params = next_params as GenerateContentParameters;\n }\n const google_stream = await retryGoogleRequest(\n () => client.models.generateContentStream(params),\n options\n );\n\n stream.push({ type: 'start', partial: output });\n let current_block: TextContent | ThinkingContent | null = null;\n const blocks = output.content;\n /** 取得当前输出块下标,使增量事件始终指向最新内容块。 */\n const block_index = () => blocks.length - 1;\n\n for await (const chunk of google_stream) {\n output.responseId ||= chunk.responseId;\n const candidate = chunk.candidates?.[0];\n if (candidate?.content?.parts) {\n for (const part of candidate.content.parts) {\n if (part.text !== undefined) {\n const is_thinking = isGoogleThinkingPart(part);\n if (\n !current_block ||\n (is_thinking && current_block.type !== 'thinking') ||\n (!is_thinking && current_block.type !== 'text')\n ) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n if (is_thinking) {\n current_block = {\n type: 'thinking',\n thinking: '',\n thinkingSignature: undefined,\n };\n output.content.push(current_block);\n stream.push({\n type: 'thinking_start',\n contentIndex: block_index(),\n partial: output,\n });\n } else {\n current_block = { type: 'text', text: '' };\n output.content.push(current_block);\n stream.push({\n type: 'text_start',\n contentIndex: block_index(),\n partial: output,\n });\n }\n }\n\n if (current_block.type === 'thinking') {\n current_block.thinking += part.text;\n current_block.thinkingSignature = retainGoogleThoughtSignature(\n current_block.thinkingSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'thinking_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n } else {\n current_block.text += part.text;\n current_block.textSignature = retainGoogleThoughtSignature(\n current_block.textSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'text_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n }\n }\n\n if (part.functionCall) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n current_block = null;\n }\n\n const provided_id = part.functionCall.id;\n const needs_new_id =\n !provided_id ||\n output.content.some(\n block => block.type === 'toolCall' && block.id === provided_id\n );\n const tool_call_id = needs_new_id\n ? `${part.functionCall.name}_${Date.now()}_${++tool_call_counter}`\n : provided_id;\n const tool_call: ToolCall = {\n type: 'toolCall',\n id: tool_call_id,\n name: part.functionCall.name || '',\n arguments: (part.functionCall.args as ToolCall['arguments']) ?? {},\n ...(part.thoughtSignature\n ? { thoughtSignature: part.thoughtSignature }\n : {}),\n };\n output.content.push(tool_call);\n stream.push({\n type: 'toolcall_start',\n contentIndex: block_index(),\n partial: output,\n });\n stream.push({\n type: 'toolcall_delta',\n contentIndex: block_index(),\n delta: JSON.stringify(tool_call.arguments),\n partial: output,\n });\n stream.push({\n type: 'toolcall_end',\n contentIndex: block_index(),\n toolCall: tool_call,\n partial: output,\n });\n }\n }\n }\n\n if (candidate?.finishReason) {\n output.rawStopReason = candidate.finishReason;\n output.stopReason = mapGoogleStopReason(candidate.finishReason);\n if (\n output.stopReason === 'stop' &&\n output.content.some(block => block.type === 'toolCall')\n ) {\n output.stopReason = 'toolUse';\n }\n }\n\n if (chunk.usageMetadata) {\n output.usage = {\n input:\n (chunk.usageMetadata.promptTokenCount || 0) -\n (chunk.usageMetadata.cachedContentTokenCount || 0),\n output:\n (chunk.usageMetadata.candidatesTokenCount || 0) +\n (chunk.usageMetadata.thoughtsTokenCount || 0),\n cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,\n cacheWrite: 0,\n reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,\n totalTokens: chunk.usageMetadata.totalTokenCount || 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n calculateCost(model, output.usage);\n }\n }\n\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n\n if (options.signal?.aborted) {\n throw new Error('Request was aborted');\n }\n if (output.stopReason === 'pending') {\n throw new Error('Google stream ended without a finish reason');\n }\n if (output.stopReason === 'aborted' || output.stopReason === 'error') {\n throw new Error(\n output.rawStopReason\n ? `Provider stopped with: ${output.rawStopReason}`\n : 'An unknown error occurred'\n );\n }\n\n stream.push({ type: 'done', reason: output.stopReason, message: output });\n stream.end();\n } catch (error) {\n for (const block of output.content) {\n if ('index' in block) {\n delete (block as { index?: number }).index;\n }\n }\n output.stopReason = options.signal?.aborted ? 'aborted' : 'error';\n output.errorMessage = formatGoogleError(error);\n stream.push({ type: 'error', reason: output.stopReason, error: output });\n stream.end();\n }\n })();\n\n return stream;\n}\n\n/** 优先使用自定义思考预算,否则采用相应模型的预算映射;未知模型交由服务端动态分配。 */\nfunction getGoogleBudget(\n model: Model<'google-generative-ai'>,\n level: ResolvedGoogleThinkingLevel,\n custom_budgets?: ThinkingBudgets\n): number {\n if (custom_budgets?.[level] !== undefined) {\n return custom_budgets[level];\n }\n if (model.id.includes('2.5-pro')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 32768 }[level];\n }\n if (model.id.includes('2.5-flash-lite')) {\n return { minimal: 512, low: 2048, medium: 8192, high: 24576 }[level];\n }\n if (model.id.includes('2.5-flash')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 24576 }[level];\n }\n return -1;\n}\n\n/** 将 Pi 简化选项解析为模型适用的思考等级或预算,再复用注入传输的流实现。 */\nfunction streamSimpleWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: SimpleStreamOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const api_key = options.apiKey;\n if (!api_key) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n const base: GoogleProxyOptions = {\n ...buildBaseOptions(model, context, options, api_key),\n toolChoice: options.toolChoice,\n };\n if (!options.reasoning) {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n\n const clamped_reasoning = clampThinkingLevel(model, options.reasoning);\n if (clamped_reasoning === 'off') {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);\n if (usesGoogleThinkingLevel(model)) {\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: { enabled: true, level: toGoogleThinkingLevel(resolved_level) },\n },\n fetch_impl\n );\n }\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: {\n enabled: true,\n budgetTokens: getGoogleBudget(model, resolved_level, options.thinkingBudgets),\n },\n },\n fetch_impl\n );\n}\n\n/** 普通浏览器请求沿用 Pi 上游实现;具有独立 fetch 的代理或非流式请求走本地桥接。 */\nexport function createGoogleProxyAwareApi(upstream: ProviderStreams): ProviderStreams {\n return {\n /** 为完整流选项选择上游实现或带请求级传输的 Google 桥接。 */\n stream(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.stream(model, context, options);\n }\n return streamWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options as GoogleProxyOptions,\n options.fetch\n );\n },\n /** 为简化流选项选择上游实现或带请求级传输的 Google 桥接。 */\n streamSimple(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.streamSimple(model, context, options);\n }\n return streamSimpleWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options,\n options.fetch\n );\n },\n };\n}\n","import { type Api, type Model } from './pi_gateway';\nimport {\n getPiCatalogModels,\n getPiProviderDefinition,\n isPiCatalogModelApiCompatible,\n type PiAuthType,\n type PiProviderDefinition,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_registry';\nimport {\n getPiProviderApiBaseUrl,\n normalizePiApiBaseEndpoint,\n normalizePiTargetEndpoint,\n PiEndpointValidationError,\n} from './provider_target';\n\nexport { resolvePiApiKeyScope } from './provider_target';\n\n/** 设置中保存的 Pi 连接与模型选择,字符串取值仍需通过注册表校验。 */\nexport interface PiModelConfiguration {\n provider: string;\n api: string;\n authType: string;\n endpoint?: string;\n model: string;\n /** 0 或未填写表示使用目录窗口;正值覆盖目录,目录外模型必须显式填写。 */\n contextWindow?: number;\n}\n\n/** 模型解析边界的原始输入,接受 unknown 以校验旧设置和外部调用。 */\nexport interface ResolvePiModelInput {\n piConfig: PiModelConfiguration | unknown;\n /** 用户配置的最大回复 token 数,尚未应用模型目录的输出上限。 */\n maxTokens: number | unknown;\n apiKey?: string | unknown;\n}\n\n/** 连接、认证和模型预算校验的失败分类,供预检及本地化提示使用。 */\nexport type PiModelResolutionErrorCode =\n | 'invalid_config'\n | 'unknown_provider'\n | 'unsupported_api'\n | 'unsupported_auth'\n | 'missing_api_key'\n | 'invalid_endpoint'\n | 'custom_endpoint_not_allowed'\n | 'oauth_endpoint_not_allowed'\n | 'oauth_api_mismatch'\n | 'missing_model'\n | 'invalid_context_window'\n | 'missing_context_window'\n | 'invalid_max_tokens'\n | 'max_tokens_exceed_context';\n\nexport class PiModelResolutionError extends Error {\n /** 保留模型配置错误码,供预检和界面给出稳定的错误分类。 */\n constructor(\n public readonly code: PiModelResolutionErrorCode,\n message: string\n ) {\n super(message);\n this.name = 'PiModelResolutionError';\n }\n}\n\n/** 校验并规范化后的连接配置,保留目录依据与最终预算,供构造本次 Pi Model 使用。 */\nexport interface ValidatedPiConfiguration {\n definition: PiProviderDefinition;\n provider: PiProviderKey;\n api: PiWireApi;\n authType: PiAuthType;\n endpoint: string;\n modelId: string;\n /** 用户显式窗口;0 表示采用匹配目录项的上下文窗口。 */\n manualContextWindow: number;\n configuredMaxTokens: number;\n apiKey?: string;\n /** 仅在默认端点且协议兼容时使用,避免套用自定义端点的同名模型。 */\n catalogModel?: Model;\n effectiveContextWindow: number;\n /** 用户回复额度与适用目录输出上限的较小值。 */\n effectiveMaxTokens: number;\n}\n\n/** 交给运行时的模型解析结果,model 是本次请求独立的目录副本或动态元数据。 */\nexport interface ResolvedPiModel {\n definition: PiProviderDefinition;\n model: Model;\n /** 是否实际采用了匹配目录的元数据,而非仅命中同名模型。 */\n catalogHit: boolean;\n effectiveContextWindow: number;\n effectiveMaxTokens: number;\n authType: PiAuthType;\n apiKey?: string;\n}\n\n/** 判断输入是否为可读取配置字段的非空、非数组对象。 */\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 读取并规范必填字符串,缺失时抛出指定的模型配置错误。 */\nfunction requiredString(\n source: Record,\n field: string,\n errorCode: PiModelResolutionErrorCode,\n label: string\n): string {\n const value = source[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw new PiModelResolutionError(errorCode, `${label} must be a non-empty string`);\n }\n return value.trim();\n}\n\n/** 规范自定义端点并执行浏览器传输限制,将端点错误转换为模型配置错误。 */\nexport function normalizePiEndpoint(endpoint: string): string {\n try {\n return normalizePiTargetEndpoint(endpoint);\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n}\n\n/** 判断端点是否等同服务商的协议默认地址,空值与显式填写默认地址视为相同。 */\nexport function isPiDefaultProviderEndpoint(\n definition: PiProviderDefinition,\n endpoint: string,\n api: PiWireApi = definition.defaultApi\n): boolean {\n const requested = endpoint.trim();\n if (requested === '') {\n return true;\n }\n try {\n return (\n normalizePiApiBaseEndpoint(api, requested) ===\n normalizePiApiBaseEndpoint(api, getPiProviderApiBaseUrl(definition, api))\n );\n } catch {\n return false;\n }\n}\n\n/** 校验允许 0 哨兵值的配置整数,拒绝无效值隐式回退。 */\nfunction validateNonNegativeInteger(value: unknown, field: string): number {\n if (value === undefined) {\n return 0;\n }\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n `${field} must be a non-negative integer`\n );\n }\n return value;\n}\n\n/** 校验必须大于 0 的配置整数,供回复 token 等硬性预算使用。 */\nfunction validatePositiveInteger(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new PiModelResolutionError(\n 'invalid_max_tokens',\n `${field} must be a positive integer`\n );\n }\n return value;\n}\n\n/** 从所选服务商目录中查找模型,并检查目录项与当前协议是否兼容。 */\nfunction findCatalogModel(\n definition: PiProviderDefinition,\n modelId: string\n): Model | undefined {\n return getPiCatalogModels(definition.key).find(model => model.id === modelId);\n}\n\n/**\n * 校验服务商、协议、认证、端点、模型和 token 参数的组合。\n * 只使用适用于当前端点的目录元数据,不修改设置或共享目录对象。\n */\nexport function validatePiConfiguration(input: ResolvePiModelInput): ValidatedPiConfiguration {\n if (!isRecord(input.piConfig)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'More source configuration must be an object'\n );\n }\n\n const provider = requiredString(\n input.piConfig,\n 'provider',\n 'unknown_provider',\n 'More source provider'\n );\n const definition = getPiProviderDefinition(provider);\n if (!definition) {\n throw new PiModelResolutionError(\n 'unknown_provider',\n `Unknown More source provider: ${provider}`\n );\n }\n\n const api = requiredString(input.piConfig, 'api', 'unsupported_api', 'More source API');\n if (!definition.allowedApis.some(candidate => candidate === api)) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Provider ${definition.key} does not support API ${api}`\n );\n }\n\n const authType = requiredString(\n input.piConfig,\n 'authType',\n 'unsupported_auth',\n 'More source auth type'\n );\n if (!definition.allowedAuthTypes.some(candidate => candidate === authType)) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} does not support ${authType} authentication`\n );\n }\n\n const rawEndpoint = input.piConfig.endpoint;\n if (rawEndpoint !== undefined && typeof rawEndpoint !== 'string') {\n throw new PiModelResolutionError(\n 'invalid_endpoint',\n 'More source endpoint must be a string'\n );\n }\n const requestedEndpoint = (rawEndpoint ?? '').trim();\n\n if (authType === 'oauth') {\n if (!definition.oauth) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} has no OAuth registration`\n );\n }\n if (requestedEndpoint) {\n throw new PiModelResolutionError(\n 'oauth_endpoint_not_allowed',\n 'OAuth providers cannot use a custom endpoint'\n );\n }\n if (api !== definition.oauth.api) {\n throw new PiModelResolutionError(\n 'oauth_api_mismatch',\n `OAuth for ${definition.key} requires API ${definition.oauth.api}`\n );\n }\n } else if (requestedEndpoint && !definition.allowCustomEndpoint) {\n throw new PiModelResolutionError(\n 'custom_endpoint_not_allowed',\n `Provider ${definition.key} does not allow a custom endpoint`\n );\n }\n\n let endpoint: string;\n const defaultEndpoint = getPiProviderApiBaseUrl(definition, api as PiWireApi);\n try {\n endpoint = requestedEndpoint\n ? normalizePiApiBaseEndpoint(api as PiWireApi, requestedEndpoint)\n : defaultEndpoint;\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n const usesDefaultEndpoint = endpoint === normalizePiEndpoint(defaultEndpoint);\n const modelId = requiredString(input.piConfig, 'model', 'missing_model', 'More source model');\n const manualContextWindow = validateNonNegativeInteger(\n input.piConfig.contextWindow,\n 'More source contextWindow'\n );\n const configuredMaxTokens = validatePositiveInteger(input.maxTokens, 'maximum reply tokens');\n const catalogModelById = findCatalogModel(definition, modelId);\n const catalogModelMatchesApi =\n catalogModelById !== undefined &&\n isPiCatalogModelApiCompatible(definition, catalogModelById, api as PiWireApi);\n if (catalogModelById && !catalogModelMatchesApi && usesDefaultEndpoint) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Model ${modelId} is registered for ${catalogModelById.api}, not ${api}`\n );\n }\n // On a custom OpenAI-compatible endpoint the same identifier may refer to an entirely\n // different model. Require manual metadata and treat it as dynamic instead of replaying\n // catalog metadata from another wire API.\n const catalogModel =\n catalogModelMatchesApi && usesDefaultEndpoint ? catalogModelById : undefined;\n\n if (!catalogModel && manualContextWindow === 0) {\n throw new PiModelResolutionError(\n 'missing_context_window',\n `Model ${modelId} is not in the ${definition.key} catalog; contextWindow is required`\n );\n }\n\n const effectiveContextWindow =\n manualContextWindow > 0 ? manualContextWindow : catalogModel!.contextWindow;\n if (!Number.isInteger(effectiveContextWindow) || effectiveContextWindow <= 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n 'effective contextWindow must be a positive integer'\n );\n }\n if (configuredMaxTokens > effectiveContextWindow) {\n throw new PiModelResolutionError(\n 'max_tokens_exceed_context',\n 'maximum reply tokens must not exceed contextWindow'\n );\n }\n\n const effectiveMaxTokens = catalogModel\n ? Math.min(configuredMaxTokens, catalogModel.maxTokens)\n : configuredMaxTokens;\n\n let apiKey: string | undefined;\n if (authType === 'api_key') {\n if (typeof input.apiKey !== 'string' || input.apiKey.trim() === '') {\n throw new PiModelResolutionError(\n 'missing_api_key',\n `An API key is required for ${definition.key}`\n );\n }\n apiKey = input.apiKey.trim();\n }\n\n return {\n definition,\n provider: definition.providerId,\n api: api as PiWireApi,\n authType: authType as PiAuthType,\n endpoint,\n modelId,\n manualContextWindow,\n configuredMaxTokens,\n apiKey,\n catalogModel,\n effectiveContextWindow,\n effectiveMaxTokens,\n };\n}\n\n/** 为目录外或自定义端点模型构造独立元数据,使用显式窗口与保守能力默认值。 */\nfunction createDynamicModel(configuration: ValidatedPiConfiguration): Model {\n return {\n id: configuration.modelId,\n name: configuration.modelId,\n api: configuration.api,\n provider: configuration.provider,\n baseUrl: configuration.endpoint,\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: configuration.effectiveContextWindow,\n maxTokens: configuration.effectiveMaxTokens,\n };\n}\n\n/** 复制目录模型后应用本次端点和预算,防止请求修改共享目录元数据。 */\nfunction cloneCatalogModel(configuration: ValidatedPiConfiguration): Model {\n const original = configuration.catalogModel!;\n const model = structuredClone(original) as Model;\n\n model.api = configuration.api;\n model.provider = configuration.provider;\n model.baseUrl = configuration.endpoint;\n model.contextWindow = configuration.effectiveContextWindow;\n model.maxTokens = configuration.effectiveMaxTokens;\n\n // Compatibility metadata is API-shaped. Do not replay Responses flags into Completions.\n if (original.api !== configuration.api) {\n delete model.compat;\n }\n return model;\n}\n\n/** 在配置校验后选择目录模型或动态模型,并返回本次请求的有效预算及认证信息。 */\nexport function resolvePiModel(input: ResolvePiModelInput): ResolvedPiModel {\n const configuration = validatePiConfiguration(input);\n const catalogHit = configuration.catalogModel !== undefined;\n return {\n definition: configuration.definition,\n model: catalogHit ? cloneCatalogModel(configuration) : createDynamicModel(configuration),\n catalogHit,\n effectiveContextWindow: configuration.effectiveContextWindow,\n effectiveMaxTokens: configuration.effectiveMaxTokens,\n authType: configuration.authType,\n apiKey: configuration.apiKey,\n };\n}\n\n/** 从中文字段的额外模型设置中提取 Pi 连接和回复预算,再调用统一模型解析。 */\nexport function resolvePiModelFromExtraModelSettings(settings: unknown): ResolvedPiModel {\n if (!isRecord(settings)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'extra-model settings must be an object'\n );\n }\n return resolvePiModel({\n piConfig: settings.pi,\n maxTokens: settings['最大回复token数'],\n apiKey: settings['密钥'],\n });\n}\n","import {\n createPiOAuthLoginStore,\n createPiOAuthLogoutStore,\n getPiCredentialStore,\n} from './credential_store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport type { CredentialStore, ModelAuth, OAuthAuth, OAuthCredential } from './pi_gateway';\nimport { getPiProviderRegistration, type PiOAuthDefinition } from './provider_registry';\n\nconst DEFAULT_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst CLOSED_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst MAX_CLOSED_ATTEMPTS = 128;\nconst REFRESH_TIMEOUT_MS = 15_000;\nconst OPENAI_CODEX_PROVIDER_ID = 'openai-codex';\nconst ANTHROPIC_PROVIDER_ID = 'anthropic';\nconst OPENAI_ACCOUNT_CLAIM = 'https://api.openai.com/auth';\n\n/** 浏览器登录、回调校验、令牌交换及持久化的失败分类,供界面和取消逻辑使用。 */\nexport type PiOAuthErrorCode =\n | 'unsupported_provider'\n | 'browser_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_failed'\n | 'attempt_expired'\n | 'attempt_used'\n | 'cancelled'\n | 'browser_network'\n | 'token_http'\n | 'token_response'\n | 'account_id'\n | 'missing_credential'\n | 'credential_store';\n\nexport class PiOAuthError extends Error {\n readonly code: PiOAuthErrorCode;\n\n /** 为浏览器 OAuth 失败保留稳定错误码,供界面和取消逻辑分类处理。 */\n constructor(code: PiOAuthErrorCode, message: string) {\n super(message);\n this.name = 'PiOAuthError';\n this.code = code;\n }\n}\n\n/** 可展示给界面的登录尝试信息,不包含 PKCE verifier 或令牌。 */\nexport type PiOAuthAttemptView = {\n id: string;\n providerId: string;\n authorizationUrl: string;\n /** 本次回调可被接受的截止时间,使用 Unix 毫秒时间戳。 */\n expiresAt: number;\n};\n\n/** 对外展示的 OAuth 凭证摘要;loggedIn 表示已存有凭证,过期凭证仍可尝试刷新。 */\nexport type PiOAuthCredentialStatus = {\n loggedIn: boolean;\n type?: 'oauth';\n /** 已保存访问令牌的过期时间,使用 Unix 毫秒时间戳。 */\n expiresAt?: number;\n};\n\n/** 令牌请求需要的最小 fetch 契约,便于注入测试传输。 */\ntype FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise;\n\n/** 生成 state、PKCE verifier 和 SHA-256 challenge 所需的最小 Web Crypto 接口。 */\ntype CryptoLike = {\n getRandomValues(array: T): T;\n subtle: {\n digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise;\n };\n};\n\n/** 单次登录引用的只读服务商 OAuth 注册信息。 */\ntype OAuthMetadata = Readonly;\n\n/** 可替换的浏览器与存储依赖,未传入时使用页面环境和共享凭证仓库。 */\ntype PiOAuthDependencies = {\n fetch?: FetchLike;\n crypto?: CryptoLike;\n /** 返回 Unix 毫秒时间戳,用于登录有效期和令牌过期计算。 */\n now?: () => number;\n credentialStore?: CredentialStore;\n};\n\n/** 发起登录时的依赖、取消信号及回调等待期限。 */\nexport type BeginPiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n /** 从创建授权链接起计算的有效时长,单位为毫秒。 */\n attemptTtlMs?: number;\n};\n\n/** 完成回调校验和授权码交换时使用的依赖与取消信号。 */\nexport type CompletePiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n};\n\n/** 凭证查询、删除等操作共享的取消与存储配置。 */\nexport type PiOAuthOperationOptions = {\n signal?: AbortSignal;\n credentialStore?: CredentialStore;\n};\n\n/** 刷新令牌所需的操作选项,只额外依赖网络和时钟,不需要重新生成 PKCE。 */\nexport type RefreshPiOAuthOptions = PiOAuthOperationOptions &\n Pick;\n\n/** 构建 Pi OAuthAuth 桥接对象时注入的浏览器能力与登录有效时长。 */\nexport type BrowserOAuthAuthOptions = Pick & {\n attemptTtlMs?: number;\n};\n\n/** 仅保存在内存中的登录尝试,负责回调防重放、有效期和交换请求取消。 */\ntype PendingAttempt = {\n id: string;\n providerId: string;\n metadata: OAuthMetadata;\n /** 登录开始时捕获的所属方案仓库,回调不再解析当前界面。 */\n credentialStore: CredentialStore;\n /** PKCE 原始验证串,只在交换授权码时提交给令牌端点。 */\n verifier: string;\n /** 与回调严格匹配的随机 state,用于绑定本次授权请求。 */\n state: string;\n expiresAt: number;\n /** exchanging 表示授权码已被领取处理,阻止重复提交同一回调。 */\n phase: 'pending' | 'exchanging';\n controller: AbortController;\n timeout: ReturnType;\n /** 清理调用方取消监听,避免尝试结束后继续持有外部信号。 */\n detachCallerAbort?: () => void;\n};\n\n/** 短期保留已关闭尝试的原因,使迟到或重复回调得到明确错误。 */\ntype ClosedAttemptReason = 'used' | 'expired' | 'cancelled';\n\nconst pendingAttempts = new Map();\nconst closedAttempts = new Map();\n\n/** 构造带稳定错误码的 OAuth 错误。 */\nfunction oauthError(code: PiOAuthErrorCode, message: string): PiOAuthError {\n return new PiOAuthError(code, message);\n}\n\n/** 创建统一的登录取消错误,供各异步阶段使用。 */\nfunction cancellationError(): PiOAuthError {\n return oauthError('cancelled', 'More source OAuth login was cancelled.');\n}\n\n/** 将已取消信号转换为 OAuth 取消错误,阻止继续交换或写入令牌。 */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw cancellationError();\n }\n}\n\n/** 解析可注入的时钟,使登录有效期和刷新测试可以精确控制时间。 */\nfunction getNow(options?: Pick): () => number {\n return options?.now ?? Date.now;\n}\n\n/** 解析浏览器令牌请求使用的 fetch,缺少能力时返回明确的环境错误。 */\nfunction getFetch(options?: Pick): FetchLike {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires the browser Fetch API for token requests.'\n );\n }\n return fetchImpl.bind(globalThis) as FetchLike;\n}\n\n/** 确认 Web Crypto 提供随机数和 SHA-256,满足 OAuth state 与 PKCE 的生成要求。 */\nfunction getCrypto(options?: Pick): CryptoLike {\n const cryptoImpl = options?.crypto ?? (globalThis.crypto as CryptoLike | undefined);\n if (\n !cryptoImpl ||\n typeof cryptoImpl.getRandomValues !== 'function' ||\n typeof cryptoImpl.subtle?.digest !== 'function'\n ) {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires Web Crypto with SHA-256 support.'\n );\n }\n return cryptoImpl;\n}\n\n/** 优先使用注入的凭证仓库,否则复用运行时的共享仓库。 */\nfunction getCredentialStore(\n options?: Pick\n): CredentialStore {\n return options?.credentialStore ?? getPiCredentialStore();\n}\n\n/** 只解析已注册且支持浏览器登录的 OAuth 服务商,拒绝其他授权流程。 */\nfunction getOAuthMetadata(providerId: string): OAuthMetadata {\n const registration = getPiProviderRegistration(providerId);\n const metadata = registration?.oauth;\n if (!metadata || metadata.providerId !== providerId) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n if (providerId !== ANTHROPIC_PROVIDER_ID && providerId !== OPENAI_CODEX_PROVIDER_ID) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n return metadata;\n}\n\n/** 将随机字节或摘要编码为无填充的 Base64URL,供 PKCE 和 state 使用。 */\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return globalThis.btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\n/** 用密码学随机数生成指定字节长度的 Base64URL 标识。 */\nfunction randomBase64Url(cryptoImpl: CryptoLike, length: number): string {\n return bytesToBase64Url(cryptoImpl.getRandomValues(new Uint8Array(length)));\n}\n\n/** 生成仅保留在内存中的 verifier,并计算 S256 challenge。 */\nasync function createPkce(cryptoImpl: CryptoLike): Promise<{\n verifier: string;\n challenge: string;\n}> {\n const verifier = randomBase64Url(cryptoImpl, 32);\n const verifierBytes = Uint8Array.from(verifier, character => character.charCodeAt(0));\n const digest = await cryptoImpl.subtle.digest('SHA-256', verifierBytes);\n return {\n verifier,\n challenge: bytesToBase64Url(new Uint8Array(digest)),\n };\n}\n\n/** 按服务商元数据组装授权地址,附带回调地址、scope、PKCE 和 state。 */\nfunction createAuthorizationUrl(metadata: OAuthMetadata, challenge: string, state: string): string {\n const url = new URL(metadata.authorizeUrl);\n for (const [name, value] of Object.entries(metadata.authorizeParams)) {\n url.searchParams.set(name, value);\n }\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', metadata.clientId);\n url.searchParams.set('redirect_uri', metadata.redirectUri);\n url.searchParams.set('scope', metadata.scope);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n return url.toString();\n}\n\n/** 短期记录已关闭尝试的原因,并限制记录数量,使重复回调可得到准确错误。 */\nfunction rememberClosedAttempt(id: string, reason: ClosedAttemptReason): void {\n const now = Date.now();\n for (const [closedId, closed] of closedAttempts) {\n if (closed.removeAfter <= now) {\n closedAttempts.delete(closedId);\n }\n }\n while (closedAttempts.size >= MAX_CLOSED_ATTEMPTS) {\n const oldestId = closedAttempts.keys().next().value as string | undefined;\n if (oldestId === undefined) {\n break;\n }\n closedAttempts.delete(oldestId);\n }\n closedAttempts.set(id, { reason, removeAfter: now + CLOSED_ATTEMPT_TTL_MS });\n}\n\n/** 关闭当前登录尝试,释放计时器和监听,清除 verifier 与 state,并记录关闭原因。 */\nfunction closeAttempt(attempt: PendingAttempt, reason: ClosedAttemptReason, abort: boolean): void {\n if (pendingAttempts.get(attempt.id) !== attempt) {\n return;\n }\n pendingAttempts.delete(attempt.id);\n clearTimeout(attempt.timeout);\n attempt.detachCallerAbort?.();\n if (abort && !attempt.controller.signal.aborted) {\n attempt.controller.abort();\n }\n attempt.verifier = '';\n attempt.state = '';\n rememberClosedAttempt(attempt.id, reason);\n}\n\n/** 使超时尝试失效并取消进行中的令牌交换。 */\nfunction expireAttempt(attempt: PendingAttempt): void {\n closeAttempt(attempt, 'expired', true);\n}\n\n/** 将调用方取消传递给内部控制器,并返回解绑函数。 */\nfunction linkAbortSignal(signal: AbortSignal | undefined, controller: AbortController): () => void {\n if (!signal) {\n return () => undefined;\n }\n /** 将外部取消转发给当前令牌操作。 */\n const onAbort = () => controller.abort();\n if (signal.aborted) {\n onAbort();\n } else {\n signal.addEventListener('abort', onAbort, { once: true });\n }\n return () => signal.removeEventListener('abort', onAbort);\n}\n\n/** 根据已关闭原因区分过期、取消和回调重复使用。 */\nfunction errorForClosedAttempt(reason: ClosedAttemptReason): PiOAuthError {\n if (reason === 'expired') {\n return oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (reason === 'cancelled') {\n return cancellationError();\n }\n return oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n}\n\n/** 取得仍处于待回调阶段的有效尝试,拒绝过期、已消费或已取消的记录。 */\nfunction getPendingAttempt(attemptId: string, now: number): PendingAttempt {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n const closed = closedAttempts.get(attemptId);\n if (closed) {\n throw errorForClosedAttempt(closed.reason);\n }\n throw oauthError(\n 'attempt_used',\n 'No active More source OAuth login attempt matches this callback.'\n );\n }\n if (now >= attempt.expiresAt) {\n expireAttempt(attempt);\n throw oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (attempt.phase !== 'pending') {\n throw oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n }\n if (attempt.controller.signal.aborted) {\n closeAttempt(attempt, 'cancelled', false);\n throw cancellationError();\n }\n return attempt;\n}\n\n/** 通过地址与 state 校验后提取的授权码回调,不再携带完整 URL。 */\ntype ParsedCallback = { code: string; state: string };\n\n/** 补齐 URL 的默认端口,使回调地址校验不受显式端口写法影响。 */\nfunction normalizePort(url: URL): string {\n if (url.port) {\n return url.port;\n }\n return url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '';\n}\n\n/**\n * 校验完整回环回调地址的协议、主机、端口、路径和 state,再提取授权码。\n * 回调必须属于当前尝试;服务商返回的授权错误也在此转换为固定错误。\n */\nfunction parseAndValidateCallback(callbackUrl: string, attempt: PendingAttempt): ParsedCallback {\n let callback: URL;\n let redirect: URL;\n try {\n callback = new URL(callbackUrl.trim());\n redirect = new URL(attempt.metadata.redirectUri);\n } catch {\n throw oauthError(\n 'invalid_callback',\n 'Paste the complete loopback callback URL from the browser address bar.'\n );\n }\n\n const allowedHosts = new Set(\n attempt.metadata.allowedCallbackHosts.map(host => host.toLowerCase())\n );\n allowedHosts.add(redirect.hostname.toLowerCase());\n if (\n callback.protocol !== 'http:' ||\n callback.username !== '' ||\n callback.password !== '' ||\n callback.hash !== '' ||\n !allowedHosts.has(callback.hostname.toLowerCase()) ||\n normalizePort(callback) !== normalizePort(redirect) ||\n callback.pathname !== redirect.pathname\n ) {\n throw oauthError(\n 'invalid_callback',\n 'The More source OAuth callback does not match the registered loopback address.'\n );\n }\n\n const state = callback.searchParams.get('state') ?? '';\n if (!state || state !== attempt.state) {\n throw oauthError('state_mismatch', 'The More source OAuth callback state does not match.');\n }\n if (callback.searchParams.has('error')) {\n throw oauthError(\n 'authorization_failed',\n 'The OAuth provider did not authorize the More source login.'\n );\n }\n const code = callback.searchParams.get('code') ?? '';\n if (!code) {\n throw oauthError('invalid_callback', 'The More source OAuth callback is missing its code.');\n }\n return { code, state };\n}\n\n/** 综合信号和 OAuth 错误码识别取消,避免误报为网络或存储故障。 */\nfunction isCancellation(error: unknown, signal: AbortSignal): boolean {\n return signal.aborted || (error instanceof PiOAuthError && error.code === 'cancelled');\n}\n\n/** 区分主动取消与浏览器网络失败,不透传可能含敏感请求数据的底层错误。 */\nfunction normalizeFetchFailure(error: unknown, signal: AbortSignal): never {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n if (error instanceof TypeError) {\n throw oauthError(\n 'browser_network',\n 'The browser could not reach the More source OAuth token endpoint. Check network access and whether the provider allows browser CORS requests.'\n );\n }\n throw oauthError(\n 'browser_network',\n 'The More source OAuth token request failed in the browser.'\n );\n}\n\n/** 令牌交换的两种输入:首次登录使用授权码和 PKCE,续期使用刷新令牌。 */\ntype TokenGrant =\n | { type: 'authorization_code'; code: string; verifier: string; state: string }\n | { type: 'refresh_token'; refreshToken: string };\n\n/** 已校验并统一字段名的令牌响应,持久化前还需计算过期时间和补齐账号信息。 */\ntype TokenData = {\n access: string;\n refresh: string;\n /** 服务商返回的有效时长,单位为秒,并非绝对时间戳。 */\n expiresInSeconds: number;\n};\n\n/** 按授权码或刷新令牌两种 grant 生成令牌请求字段,并遵循服务商的 state 约定。 */\nfunction createTokenFields(metadata: OAuthMetadata, grant: TokenGrant): Record {\n const fields: Record = { ...metadata.tokenParams };\n fields.client_id = metadata.clientId;\n if (grant.type === 'authorization_code') {\n fields.grant_type = 'authorization_code';\n fields.code = grant.code;\n fields.code_verifier = grant.verifier;\n fields.redirect_uri = metadata.redirectUri;\n if (metadata.includeStateInTokenRequest) {\n fields.state = grant.state;\n }\n } else {\n fields.grant_type = 'refresh_token';\n fields.refresh_token = grant.refreshToken;\n }\n return fields;\n}\n\n/** 校验令牌接口状态和必需字段;刷新响应未返回 refresh_token 时保留原刷新令牌。 */\nasync function readTokenResponse(\n response: Response,\n fallbackRefreshToken?: string\n): Promise {\n if (!response.ok) {\n throw oauthError(\n 'token_http',\n `The More source OAuth token endpoint rejected the request (HTTP ${response.status}).`\n );\n }\n\n let body: unknown;\n try {\n body = JSON.parse(await response.text());\n } catch {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n if (!body || typeof body !== 'object') {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n const token = body as Record;\n const access = token.access_token;\n const refresh = token.refresh_token ?? fallbackRefreshToken;\n const expiresIn = token.expires_in;\n if (\n typeof access !== 'string' ||\n access.length === 0 ||\n typeof refresh !== 'string' ||\n refresh.length === 0 ||\n typeof expiresIn !== 'number' ||\n !Number.isFinite(expiresIn) ||\n expiresIn <= 0\n ) {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint response is missing required fields.'\n );\n }\n return { access, refresh, expiresInSeconds: expiresIn };\n}\n\n/** 按服务商要求用 JSON 或表单交换令牌,并在请求前后检查取消。 */\nasync function requestTokens(\n metadata: OAuthMetadata,\n grant: TokenGrant,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n const fields = createTokenFields(metadata, grant);\n const headers: Record = { Accept: 'application/json' };\n let body: string;\n if (metadata.exchangeKind === 'json') {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(fields);\n } else {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n body = new URLSearchParams(fields).toString();\n }\n\n let response: Response;\n try {\n response = await getFetch(options)(metadata.tokenUrl, {\n method: 'POST',\n headers,\n body,\n signal,\n });\n } catch (error) {\n normalizeFetchFailure(error, signal);\n }\n throwIfAborted(signal);\n return readTokenResponse(\n response!,\n grant.type === 'refresh_token' ? grant.refreshToken : undefined\n );\n}\n\n/** 解码 JWT 中的 Base64URL JSON 数据,兼容缺少 TextDecoder 的浏览器。 */\nfunction decodeBase64UrlJson(value: string): unknown {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const binary = globalThis.atob(padded);\n const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));\n const json =\n typeof globalThis.TextDecoder === 'function'\n ? new globalThis.TextDecoder().decode(bytes)\n : decodeURIComponent(\n [...bytes].map(byte => `%${byte.toString(16).padStart(2, '0')}`).join('')\n );\n return JSON.parse(json);\n}\n\n/** 从访问令牌的账号声明中读取 Codex 所需账号标识;缺失时拒绝建立凭证。 */\nfunction extractOpenAIAccountId(accessToken: string): string {\n try {\n const parts = accessToken.split('.');\n if (parts.length !== 3) {\n throw new Error('invalid JWT');\n }\n const payload = decodeBase64UrlJson(parts[1]);\n if (!payload || typeof payload !== 'object') {\n throw new Error('invalid JWT payload');\n }\n const authClaim = (payload as Record)[OPENAI_ACCOUNT_CLAIM];\n if (!authClaim || typeof authClaim !== 'object') {\n throw new Error('missing auth claim');\n }\n const accountId = (authClaim as Record).chatgpt_account_id;\n if (typeof accountId !== 'string' || accountId.length === 0) {\n throw new Error('missing account ID');\n }\n return accountId;\n } catch {\n throw oauthError(\n 'account_id',\n 'The OpenAI Codex OAuth token does not contain a ChatGPT account ID.'\n );\n }\n}\n\n/** 将令牌响应转换为 Pi OAuth 凭证,预留过期时间偏差,并补齐 Codex 账号标识。 */\nfunction credentialFromTokens(\n metadata: OAuthMetadata,\n token: TokenData,\n now: number\n): OAuthCredential {\n const credential: OAuthCredential = {\n type: 'oauth',\n access: token.access,\n refresh: token.refresh,\n expires: now + token.expiresInSeconds * 1000 - metadata.expirySkewMs,\n };\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n credential.accountId = extractOpenAIAccountId(token.access);\n }\n return credential;\n}\n\n/** 通过共享凭证队列保存登录结果,分别处理取消与存储失败。 */\nasync function persistCredential(\n providerId: string,\n credential: OAuthCredential,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n try {\n await getCredentialStore(options).modify(providerId, async () => credential, { signal });\n } catch (error) {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'More source OAuth login succeeded, but the credential could not be saved.'\n );\n }\n}\n\n/**\n * 创建带有效期的浏览器登录尝试,生成 PKCE 和 state,返回供界面打开的授权地址。\n * 秘密校验信息留在内存中,调用方取消或超时会关闭该尝试。\n */\nexport async function beginPiOAuth(\n providerId: string,\n options: BeginPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const metadata = getOAuthMetadata(providerId);\n const ttl = options.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new RangeError('More source OAuth attemptTtlMs must be a positive number.');\n }\n const cryptoImpl = getCrypto(options);\n const credentialStore =\n options.credentialStore ??\n createPiOAuthLoginStore(providerId, randomBase64Url(cryptoImpl, 18));\n const { verifier, challenge } = await createPkce(cryptoImpl);\n throwIfAborted(options.signal);\n\n const state = randomBase64Url(cryptoImpl, 32);\n const id = randomBase64Url(cryptoImpl, 18);\n const expiresAt = getNow(options)() + ttl;\n const controller = new AbortController();\n const attempt: PendingAttempt = {\n id,\n providerId,\n metadata,\n credentialStore,\n verifier,\n state,\n expiresAt,\n phase: 'pending' as const,\n controller,\n timeout: undefined as unknown as ReturnType,\n };\n attempt.timeout = setTimeout(() => expireAttempt(attempt), ttl);\n pendingAttempts.set(id, attempt);\n if (options.signal) {\n /** 关闭已取消的登录尝试,阻止随后到达的回调继续交换令牌。 */\n const onAbort = () => closeAttempt(attempt, 'cancelled', true);\n options.signal.addEventListener('abort', onAbort, { once: true });\n attempt.detachCallerAbort = () => options.signal?.removeEventListener('abort', onAbort);\n if (options.signal.aborted) {\n onAbort();\n throw cancellationError();\n }\n }\n\n return {\n id,\n providerId,\n authorizationUrl: createAuthorizationUrl(metadata, challenge, state),\n expiresAt,\n };\n}\n\n/**\n * 校验并一次性消费回调,交换授权码后保存凭证,最终清理尝试状态。\n * 重复回调或交换期间取消都不能产生第二次有效登录。\n */\nexport async function completePiOAuth(\n attemptId: string,\n callbackUrl: string,\n options: CompletePiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const now = getNow(options);\n const attempt = getPendingAttempt(attemptId, now());\n let parsed: ParsedCallback;\n try {\n parsed = parseAndValidateCallback(callbackUrl, attempt);\n } catch (error) {\n if (error instanceof PiOAuthError && error.code === 'authorization_failed') {\n closeAttempt(attempt, 'used', true);\n }\n throw error;\n }\n attempt.phase = 'exchanging';\n const detachCompleteAbort = linkAbortSignal(options.signal, attempt.controller);\n\n try {\n throwIfAborted(attempt.controller.signal);\n const token = await requestTokens(\n attempt.metadata,\n {\n type: 'authorization_code',\n code: parsed.code,\n verifier: attempt.verifier,\n state: parsed.state,\n },\n attempt.controller.signal,\n options\n );\n const credential = credentialFromTokens(attempt.metadata, token, now());\n await persistCredential(attempt.providerId, credential, attempt.controller.signal, {\n credentialStore: options.credentialStore ?? attempt.credentialStore,\n });\n closeAttempt(attempt, 'used', false);\n return credential;\n } catch (error) {\n const cancelled = isCancellation(error, attempt.controller.signal);\n closeAttempt(attempt, cancelled ? 'cancelled' : 'used', true);\n if (cancelled) {\n throw cancellationError();\n }\n throw error;\n } finally {\n detachCompleteAbort();\n }\n}\n\n/** 按尝试编号取消登录,清除回调校验状态并中止未完成的交换。 */\nexport function cancelPiOAuth(attemptId: string): boolean {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n return false;\n }\n closeAttempt(attempt, 'cancelled', true);\n return true;\n}\n\n/** 取消全部待完成的登录尝试,供界面卸载和脚本清理使用。 */\nexport function cancelAllPiOAuth(): void {\n for (const attempt of [...pendingAttempts.values()]) {\n closeAttempt(attempt, 'cancelled', true);\n }\n}\n\n/** 解除捕获方案的登录绑定;其他方案仍引用的集中凭证会继续保留。 */\nexport async function logoutPiOAuth(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n try {\n const store = options.credentialStore ?? createPiOAuthLogoutStore();\n await store.delete(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential could not be removed.'\n );\n }\n}\n\n/** 返回是否已登录及过期时间等界面状态,不暴露访问令牌和刷新令牌。 */\nexport async function getPiOAuthCredentialStatus(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n let credential;\n try {\n credential = await getCredentialStore(options).read(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential status could not be read.'\n );\n }\n if (!credential || credential.type !== 'oauth') {\n return { loggedIn: false };\n }\n return {\n loggedIn: true,\n type: 'oauth',\n expiresAt: credential.expires,\n };\n}\n\n/**\n * 由界面强制刷新现有 OAuth 凭证,与 Pi 自动刷新共用同一服务商修改锁。\n * 操作使用捕获的凭证仓库和取消信号,刷新完成后再提交新凭证。\n */\nexport async function refreshPiOAuth(\n providerId: string,\n options: RefreshPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const auth = getBrowserOAuthAuth(providerId, options);\n const store = getCredentialStore(options);\n installPiAbortSignalPolyfills();\n const signal = AbortSignal.any([\n ...(options.signal ? [options.signal] : []),\n AbortSignal.timeout(REFRESH_TIMEOUT_MS),\n ]);\n /** 生成缺少可刷新 OAuth 凭证的错误,供刷新入口统一报告。 */\n const missingCredential = () =>\n oauthError('missing_credential', 'Sign in before refreshing OAuth credentials.');\n try {\n const observed = await store.read(providerId, { signal });\n if (observed?.type !== 'oauth') {\n throw missingCredential();\n }\n const refreshed = await store.modify(\n providerId,\n async current => {\n throwIfAborted(signal);\n if (current?.type !== 'oauth') {\n throw missingCredential();\n }\n // An automatic refresh or another login may have completed while we queued.\n // Keep that result instead of reusing or rotating its token a second time.\n if (\n current.access !== observed.access ||\n current.refresh !== observed.refresh ||\n current.expires !== observed.expires\n ) {\n return undefined;\n }\n return auth.refresh(current, signal);\n },\n { signal }\n );\n if (refreshed?.type !== 'oauth') {\n throw missingCredential();\n }\n return { loggedIn: true, type: 'oauth', expiresAt: refreshed.expires };\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n if (signal.aborted) {\n throw oauthError('browser_network', 'The OAuth credential refresh timed out.');\n }\n if (error instanceof PiOAuthError) {\n throw error;\n }\n throw oauthError('credential_store', 'The refreshed OAuth credential could not be saved.');\n }\n}\n\n/**\n * 将浏览器授权、刷新和凭证转换接入 Pi 的 OAuthAuth 契约。\n * 登录通过授权地址与手动回调交互完成,不启动 Node 回环服务器。\n */\nfunction createBrowserOAuthAuth(\n metadata: OAuthMetadata,\n options: BrowserOAuthAuthOptions\n): OAuthAuth {\n const providerName =\n metadata.providerId === ANTHROPIC_PROVIDER_ID\n ? 'Anthropic (Claude Pro/Max)'\n : 'OpenAI (ChatGPT Plus/Pro)';\n return {\n name: providerName,\n isSubscription: true,\n /** 通过 Pi 认证交互展示授权地址、接收回调,并在结束时释放登录尝试。 */\n async login(interaction) {\n const attempt = await beginPiOAuth(metadata.providerId, {\n ...options,\n signal: interaction.signal,\n });\n interaction.notify({\n type: 'auth_url',\n url: attempt.authorizationUrl,\n instructions: 'Complete login, then paste the complete loopback callback URL here.',\n });\n let callbackUrl: string;\n try {\n callbackUrl = await interaction.prompt({\n type: 'manual_code',\n message: 'Paste the complete loopback callback URL:',\n placeholder: metadata.redirectUri.replace('localhost', '127.0.0.1'),\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n if (interaction.signal.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'invalid_callback',\n 'More source OAuth callback input was cancelled.'\n );\n }\n try {\n return await completePiOAuth(attempt.id, callbackUrl, {\n ...options,\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n throw error;\n }\n },\n /** 为 Pi 自动续期交换 refresh_token,保留请求的取消信号并更新过期时间。 */\n async refresh(credential, signal) {\n const token = await requestTokens(\n metadata,\n { type: 'refresh_token', refreshToken: credential.refresh },\n signal,\n options\n );\n return credentialFromTokens(metadata, token, getNow(options)());\n },\n /** 把持久化 OAuth 凭证转换为 Pi 请求认证信息,保留服务商需要的账号字段。 */\n async toAuth(credential): Promise {\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n extractOpenAIAccountId(credential.access);\n }\n return { apiKey: credential.access };\n },\n };\n}\n\n/** 按服务商解析浏览器 OAuth 实现,并传入可替换的请求和时钟依赖。 */\nexport function getBrowserOAuthAuth(\n providerId: string,\n options: BrowserOAuthAuthOptions = {}\n): OAuthAuth {\n return createBrowserOAuthAuth(getOAuthMetadata(providerId), options);\n}\n","import { uuidv4 } from '@util/common';\n\nexport const PI_PROMPT_CAPTURE_MODEL_PREFIX = 'mvu-pi-prompt-capture:';\nexport const PI_PROMPT_CAPTURE_API_URL =\n 'https://mvu-pi-prompt-capture.invalid/v1/chat/completions';\n\n/** 一次提示词捕获的诊断快照;只含状态和标记,不暴露完整提示词。 */\nexport type PromptCaptureDiagnostics = {\n generationId: string;\n /** 写入占位请求 model 字段的唯一标记,用于隔离并发生成事件。 */\n marker: string;\n markerMatched: boolean;\n captured: boolean;\n /** 占位生成是否已被成功停止,避免捕获后继续请求酒馆后端。 */\n stopSucceeded: boolean;\n /** 首次失败原因;清理阶段不会覆盖它。 */\n captureError?: string;\n};\n\n/** 从酒馆设置就绪事件复制出的最终消息及其所属生成编号。 */\nexport type CapturedPrompt = {\n generationId: string;\n messages: SillyTavern.SendingMessage[];\n};\n\n/** 捕获完成后的消息、诊断信息和接管回调的返回值。 */\nexport type PromptCaptureResult = PromptCaptureDiagnostics &\n CapturedPrompt & {\n result?: T;\n };\n\n/** 接管捕获结果并接收原生停止事件的回调,使 Pi 请求沿用酒馆的生成生命周期。 */\nexport type PromptCaptureOptions = {\n /** 在提示词就绪监听内等待此回调完成,期间保留酒馆原生停止按钮。 */\n onCaptured: (prompt: CapturedPrompt) => Promise;\n /** 仅转发用户停止当前生成的事件,不包含内部占位请求清理。 */\n onStopped: (generation_id: string) => void;\n};\n\n/** 进行中捕获的内部可变状态,messages 仅在成功复制提示词后存在。 */\ntype MutablePromptCaptureState = PromptCaptureDiagnostics & {\n messages?: SillyTavern.SendingMessage[];\n};\n\n/** 酒馆设置就绪事件中本适配层需要读取的字段,其余请求参数保持透明。 */\ntype SettingsReadyData = {\n messages: SillyTavern.SendingMessage[];\n model: string;\n [key: string]: unknown;\n};\n\n/** generate / generateRaw 的共同调用形状;捕获只依赖事件,不使用它们的返回结果。 */\ntype GenerateRunner = (config: TConfig) => Promise;\n\nconst pending_prompt_captures = new Map();\n\n/** 将非空生成编号编码为专用模型标记,用于从设置就绪事件中识别本次捕获。 */\nexport function encodePromptCaptureMarker(generation_id: string): string {\n if (generation_id.length === 0) {\n throw new Error('Prompt capture generation_id cannot be empty');\n }\n\n try {\n return `${PI_PROMPT_CAPTURE_MODEL_PREFIX}${encodeURIComponent(generation_id)}`;\n } catch (error) {\n throw new Error(`Prompt capture generation_id cannot be encoded: ${generation_id}`, {\n cause: error,\n });\n }\n}\n\n/** 只接受可规范往返编码的捕获标记,普通模型名或畸形标记返回 null。 */\nexport function decodePromptCaptureMarker(model: unknown): string | null {\n if (typeof model !== 'string' || !model.startsWith(PI_PROMPT_CAPTURE_MODEL_PREFIX)) {\n return null;\n }\n\n const encoded_generation_id = model.slice(PI_PROMPT_CAPTURE_MODEL_PREFIX.length);\n if (encoded_generation_id.length === 0) {\n return null;\n }\n\n try {\n const generation_id = decodeURIComponent(encoded_generation_id);\n if (\n generation_id.length === 0 ||\n encodeURIComponent(generation_id) !== encoded_generation_id\n ) {\n return null;\n }\n return generation_id;\n } catch {\n return null;\n }\n}\n\n/**\n * 将生成配置转换为专用捕获请求,移除业务工具、Schema、凭证和自定义覆盖。\n * 请求指向固定无效地址,并应在最终提示词捕获后被停止,避免误发到主模型。\n */\nexport function buildPromptCaptureConfig(\n config: TConfig,\n generation_id: string\n): TConfig {\n const marker = encodePromptCaptureMarker(generation_id);\n\n return {\n ...config,\n generation_id,\n should_stream: false,\n should_silence: config.should_silence ?? true,\n tools: [],\n tool_choice: 'none',\n json_schema: undefined,\n custom_api: {\n source: 'custom',\n apiurl: PI_PROMPT_CAPTURE_API_URL,\n key: '',\n model: marker,\n custom_include_body: {},\n custom_exclude_body: [],\n custom_include_headers: {},\n },\n } as TConfig;\n}\n\n/** 返回进行中捕获的诊断快照,不暴露完整提示词内容。 */\nexport function getPendingPromptCaptureDiagnostics(): PromptCaptureDiagnostics[] {\n return Array.from(pending_prompt_captures.values(), state => ({\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n ...(state.captureError === undefined ? {} : { captureError: state.captureError }),\n }));\n}\n\n/** 只有标记匹配、提示词复制成功且生成已停止,才认定捕获完整成功。 */\nfunction hasCompletedCapture(state: MutablePromptCaptureState): boolean {\n return state.markerMatched && state.captured && state.stopSucceeded;\n}\n\n/** 只记录首次捕获失败原因,避免清理阶段覆盖最有用的错误。 */\nfunction recordCaptureError(state: MutablePromptCaptureState, message: string): void {\n state.captureError ??= message;\n}\n\n/**\n * 在酒馆设置就绪事件的最后阶段复制最终提示词,并以生成编号隔离并发捕获。\n * 可等待 Pi 回调结束以维持原生停止按钮;随后停止占位请求,最终释放全部监听和登记。\n */\nexport async function capturePrompt(\n run: GenerateRunner,\n config: TConfig,\n options?: PromptCaptureOptions\n): Promise> {\n const generation_id = config.generation_id || uuidv4();\n const marker = encodePromptCaptureMarker(generation_id);\n\n if (pending_prompt_captures.has(generation_id)) {\n throw new Error(`Prompt capture generation_id is already pending: ${generation_id}`);\n }\n\n const state: MutablePromptCaptureState = {\n generationId: generation_id,\n marker,\n markerMatched: false,\n captured: false,\n stopSucceeded: false,\n };\n const capture_config = buildPromptCaptureConfig(config, generation_id);\n let captured_result: T | undefined;\n let callback_failed = false;\n let callback_error: unknown;\n let callback_started = false;\n let listening_for_stop = options !== undefined;\n /** 仅将当前生成编号的用户停止事件传给 Pi 回调,不影响其他并发请求。 */\n const stop_listener = (stopped_id?: string) => {\n if (listening_for_stop && stopped_id === generation_id) {\n options?.onStopped(generation_id);\n }\n };\n /** 关闭并移除停止监听,防止内部终止占位请求被误判为用户取消。 */\n const remove_stop_listener = () => {\n listening_for_stop = false;\n if (options) {\n eventRemoveListener(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n };\n\n /** 匹配当前捕获标记后复制最终提示词,等待接管回调,再阻止占位请求进入酒馆后端。 */\n const listener = async (generate_data: SettingsReadyData) => {\n const event_generation_id = decodePromptCaptureMarker(generate_data?.model);\n if (\n event_generation_id !== generation_id ||\n pending_prompt_captures.get(event_generation_id) !== state\n ) {\n return;\n }\n\n state.markerMatched = true;\n if (state.captureError !== undefined || state.captured) {\n return;\n }\n if (!Array.isArray(generate_data.messages)) {\n recordCaptureError(\n state,\n `Prompt capture event has no messages array: ${generation_id}`\n );\n return;\n }\n\n try {\n state.messages = structuredClone(generate_data.messages);\n } catch {\n recordCaptureError(state, `Failed to clone prompt capture messages: ${generation_id}`);\n return;\n }\n state.captured = true;\n if (options) {\n callback_started = true;\n try {\n // Settings-ready listeners are awaited by Slash. Keep generation (and its\n // native Stop button) active until the provider request has settled.\n captured_result = await options.onCaptured({\n generationId: generation_id,\n messages: state.messages,\n });\n } catch (error) {\n callback_failed = true;\n callback_error = error;\n }\n }\n // This stop only prevents the fixed capture request from reaching ST's backend.\n // It must not be mistaken for a user cancellation of the successful Pi request.\n remove_stop_listener();\n try {\n state.stopSucceeded = stopGenerationById(generation_id);\n } catch {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n return;\n }\n if (!state.stopSucceeded) {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n }\n };\n\n let subscription: { stop: () => void } | undefined;\n pending_prompt_captures.set(generation_id, state);\n\n try {\n if (options) {\n eventOn(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n subscription = eventMakeLast(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n\n let runner_failed = false;\n let runner_error: unknown;\n try {\n await run(capture_config);\n } catch (error) {\n runner_failed = true;\n runner_error = error;\n }\n\n if (state.captureError !== undefined) {\n throw new Error(state.captureError);\n }\n if (callback_failed) {\n throw callback_error;\n }\n if (runner_failed && !hasCompletedCapture(state)) {\n throw runner_error;\n }\n\n if (!hasCompletedCapture(state) || state.messages === undefined) {\n throw new Error(`Generation ended without a captured prompt: ${generation_id}`);\n }\n\n return {\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n messages: state.messages,\n ...(callback_started ? { result: captured_result } : {}),\n };\n } finally {\n remove_stop_listener();\n try {\n subscription?.stop();\n } finally {\n try {\n // Slash-Runner 4.9.1's EventOnReturn.stop() cannot reliably remove\n // its wrapped listener, so always remove the original listener too.\n eventRemoveListener(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n } finally {\n if (pending_prompt_captures.get(generation_id) === state) {\n pending_prompt_captures.delete(generation_id);\n }\n }\n }\n }\n}\n\n/** 使用当前预设的 generate 流程构造并捕获最终提示词。 */\nexport function captureGeneratePrompt(\n config: GenerateConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generate, config, options);\n}\n\n/** 使用显式顺序的 generateRaw 流程构造并捕获最终提示词。 */\nexport function captureGenerateRawPrompt(\n config: GenerateRawConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generateRaw, config, options);\n}\n","import { resolvePiThinkingLevel } from './thinking';\nimport { isPiThinkingEnabled } from './thinking_setting';\nimport {\n ANT_LING_MODELS,\n ANTHROPIC_MODELS,\n BASETEN_MODELS,\n CEREBRAS_MODELS,\n DEEPSEEK_MODELS,\n FIREWORKS_MODELS,\n GITHUB_COPILOT_MODELS,\n GOOGLE_MODELS,\n GROQ_MODELS,\n HUGGINGFACE_MODELS,\n KIMI_CODING_MODELS,\n MINIMAX_CN_MODELS,\n MINIMAX_MODELS,\n MISTRAL_MODELS,\n MOONSHOTAI_CN_MODELS,\n MOONSHOTAI_MODELS,\n NVIDIA_MODELS,\n OPENAI_CODEX_MODELS,\n OPENAI_MODELS,\n OPENCODE_GO_MODELS,\n OPENCODE_MODELS,\n OPENROUTER_MODELS,\n QWEN_TOKEN_PLAN_CN_MODELS,\n QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n QWEN_TOKEN_PLAN_MODELS,\n TOGETHER_MODELS,\n VERCEL_AI_GATEWAY_MODELS,\n XAI_MODELS,\n XIAOMI_MODELS,\n XIAOMI_TOKEN_PLAN_AMS_MODELS,\n XIAOMI_TOKEN_PLAN_CN_MODELS,\n XIAOMI_TOKEN_PLAN_SGP_MODELS,\n ZAI_CODING_CN_MODELS,\n ZAI_MODELS,\n anthropicMessagesApi,\n googleGenerativeAIApi,\n mistralConversationsApi,\n openAICodexResponsesApi,\n openAICompletionsApi,\n openAIResponsesApi,\n type Api,\n type Model,\n type ProviderStreams,\n} from './pi_gateway';\nimport { createGoogleProxyAwareApi } from './google_proxy_adapter';\nimport {\n getPiProviderApiBaseUrl,\n PI_PROVIDER_KEYS,\n PI_PROVIDER_TARGET_REGISTRY,\n type PiAuthType,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_target';\n\nexport {\n isPiCorsProxyRequired,\n PI_AUTH_TYPES,\n PI_PROVIDER_KEYS,\n PI_WIRE_APIS,\n shouldUsePiCorsProxy,\n} from './provider_target';\nexport type { PiAuthType, PiProviderKey, PiWireApi } from './provider_target';\n\n/**\n * Codex protocol level implemented by the pinned pi-ai adapter/catalog. Keep this tied to an\n * adapter upgrade rather than MVU's unrelated application version: the models endpoint uses it to\n * hide entries whose request shape the active adapter cannot yet support.\n */\nexport const OPENAI_CODEX_ADAPTER_CLIENT_VERSION = '0.144.0';\n\n/** 来源面板单个配置字段的呈现与编辑方式。 */\nexport type PiFieldMode = 'hidden' | 'readonly' | 'select' | 'editable';\n\n/** 服务商对应的面板字段规则,认证相关字段按所选认证方式显示。 */\nexport interface PiProviderFieldVisibility {\n api: PiFieldMode;\n authType: PiFieldMode;\n endpoint: PiFieldMode;\n apiKey: 'hidden' | 'when-api-key';\n oauth: 'hidden' | 'when-oauth';\n model: 'editable';\n contextWindow: 'editable';\n}\n\n/** 协议与模型组合可用的请求能力,供面板显隐和发送前参数校验共用。 */\nexport interface PiApiCapabilities {\n /** 是否支持 MVU 当前采用的 Pi 流式执行链路。 */\n streaming: boolean;\n tools: boolean;\n imageInput: boolean;\n /** 是否支持服务商原生 JSON Schema 输出约束。 */\n structuredOutput: boolean;\n /** 是否支持不指定 Schema 的原生 JSON 对象输出。 */\n jsonObjectOutput: boolean;\n temperature: boolean;\n /** temperature 可配置的闭区间,仅在支持该参数时生效。 */\n temperatureRange: readonly [minimum: number, maximum: number];\n /** 可直接传给当前协议的采样参数;未支持的配置在预检时拒绝。 */\n sampling: Readonly<{\n topP: boolean;\n topK: boolean;\n frequencyPenalty: boolean;\n presencePenalty: boolean;\n }>;\n}\n\n/** 令牌端点要求的请求正文编码:JSON 或 URL 编码表单。 */\nexport type PiOAuthExchangeKind = 'json' | 'form';\n\n/**\n * 来源面板与浏览器 OAuth 流程共享的公共客户端配置。\n * 每次登录独有的 PKCE verifier 和 state 保存在尝试状态中,不属于注册信息。\n */\nexport interface PiOAuthDefinition {\n providerId: PiProviderKey;\n api: PiWireApi;\n clientId: string;\n authorizeUrl: string;\n tokenUrl: string;\n redirectUri: string;\n /** 粘贴回调允许使用的回环主机名,端口和路径仍需与 redirectUri 匹配。 */\n allowedCallbackHosts: readonly ('localhost' | '127.0.0.1')[];\n scope: string;\n exchangeKind: PiOAuthExchangeKind;\n authorizeParams: Readonly>;\n tokenParams: Readonly>;\n /** 部分服务商要求交换授权码时同时提交本次 state。 */\n includeStateInTokenRequest: boolean;\n /** 持久化时从服务商有效期扣除的毫秒数,使刷新提前于实际过期。 */\n expirySkewMs: number;\n}\n\n/** 完整服务商注册信息,在轻量连接规则上加入能力表、界面配置和 OAuth 元数据。 */\nexport interface PiProviderDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n displayName: Readonly<{\n 'zh-CN': string;\n en: string;\n }>;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须经酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n /** 按协议登记的能力基线,查询具体模型时还会应用模型限制。 */\n apiCapabilities: Readonly>>>;\n fields: Readonly;\n oauth?: Readonly;\n}\n\n/** 按模型目录键索引的只读 Pi 元数据,不可直接修改为本次请求的配置。 */\ntype Catalog = Readonly>>;\n/** 创建协议流适配器的工厂,供 Provider 构建时选择实际协议实现。 */\ntype ApiLoader = () => ProviderStreams;\n\nconst CATALOGS: Readonly> = {\n 'ant-ling': ANT_LING_MODELS,\n anthropic: ANTHROPIC_MODELS,\n baseten: BASETEN_MODELS,\n cerebras: CEREBRAS_MODELS,\n deepseek: DEEPSEEK_MODELS,\n fireworks: FIREWORKS_MODELS,\n 'github-copilot': GITHUB_COPILOT_MODELS,\n google: GOOGLE_MODELS,\n groq: GROQ_MODELS,\n huggingface: HUGGINGFACE_MODELS,\n 'kimi-coding': KIMI_CODING_MODELS,\n minimax: MINIMAX_MODELS,\n 'minimax-cn': MINIMAX_CN_MODELS,\n mistral: MISTRAL_MODELS,\n moonshotai: MOONSHOTAI_MODELS,\n 'moonshotai-cn': MOONSHOTAI_CN_MODELS,\n nvidia: NVIDIA_MODELS,\n openai: OPENAI_MODELS,\n 'openai-codex': OPENAI_CODEX_MODELS,\n opencode: OPENCODE_MODELS,\n 'opencode-go': OPENCODE_GO_MODELS,\n openrouter: OPENROUTER_MODELS,\n 'qwen-token-plan': QWEN_TOKEN_PLAN_MODELS,\n 'qwen-token-plan-cn': QWEN_TOKEN_PLAN_CN_MODELS,\n 'qwen-token-plan-individual': QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n together: TOGETHER_MODELS,\n 'vercel-ai-gateway': VERCEL_AI_GATEWAY_MODELS,\n xai: XAI_MODELS,\n xiaomi: XIAOMI_MODELS,\n 'xiaomi-token-plan-ams': XIAOMI_TOKEN_PLAN_AMS_MODELS,\n 'xiaomi-token-plan-cn': XIAOMI_TOKEN_PLAN_CN_MODELS,\n 'xiaomi-token-plan-sgp': XIAOMI_TOKEN_PLAN_SGP_MODELS,\n zai: ZAI_MODELS,\n 'zai-coding-cn': ZAI_CODING_CN_MODELS,\n};\n\n/** Provider catalog entries that are known to have been shut down upstream. */\nconst GOOGLE_RETIRED_MODEL_IDS: ReadonlySet = new Set(['gemini-3.1-flash-lite-preview']);\n\nconst API_LOADERS: Readonly> = {\n 'openai-responses': openAIResponsesApi,\n 'openai-completions': openAICompletionsApi,\n 'openai-codex-responses': openAICodexResponsesApi,\n 'anthropic-messages': anthropicMessagesApi,\n 'google-generative-ai': () => createGoogleProxyAwareApi(googleGenerativeAIApi()),\n 'mistral-conversations': mistralConversationsApi,\n};\n\nconst TOP_P_ONLY = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TOP_P_TOP_K = Object.freeze({\n topP: true,\n topK: true,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst OPENAI_COMPLETIONS_SAMPLING = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: true,\n presencePenalty: true,\n});\n\nconst NO_SAMPLING = Object.freeze({\n topP: false,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TEMPERATURE_ZERO_TO_TWO = Object.freeze([0, 2] as const);\nconst TEMPERATURE_ZERO_TO_ONE = Object.freeze([0, 1] as const);\n\nconst OPENAI_RESPONSES_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_ONLY,\n});\n\nconst OPENAI_COMPLETIONS_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst CODEX_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: false,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: NO_SAMPLING,\n});\n\nconst ANTHROPIC_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: false,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: TOP_P_TOP_K,\n});\n\nconst GOOGLE_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_TOP_K,\n});\n\nconst MISTRAL_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst API_CAPABILITIES: Readonly>> = Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n 'openai-codex-responses': CODEX_CAPABILITIES,\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n 'mistral-conversations': MISTRAL_CAPABILITIES,\n});\n\nconst ANTHROPIC_OAUTH: Readonly = Object.freeze({\n providerId: 'anthropic',\n api: 'anthropic-messages',\n clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',\n authorizeUrl: 'https://claude.ai/oauth/authorize',\n tokenUrl: 'https://platform.claude.com/v1/oauth/token',\n redirectUri: 'http://localhost:53692/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',\n exchangeKind: 'json',\n authorizeParams: Object.freeze({ code: 'true' }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: true,\n expirySkewMs: 5 * 60 * 1000,\n});\n\nconst OPENAI_CODEX_OAUTH: Readonly = Object.freeze({\n providerId: 'openai-codex',\n api: 'openai-codex-responses',\n clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',\n authorizeUrl: 'https://auth.openai.com/oauth/authorize',\n tokenUrl: 'https://auth.openai.com/oauth/token',\n redirectUri: 'http://localhost:1455/auth/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'openid profile email offline_access',\n exchangeKind: 'form',\n authorizeParams: Object.freeze({\n id_token_add_organizations: 'true',\n codex_cli_simplified_flow: 'true',\n originator: 'pi',\n }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: false,\n expirySkewMs: 0,\n});\n\nconst EDITABLE_OPENAI_FIELDS: Readonly = Object.freeze({\n api: 'select',\n authType: 'readonly',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst OAUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'hidden',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MIXED_AUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'select',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst API_KEY_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MULTI_API_KEY_FIELDS: Readonly = Object.freeze({\n ...API_KEY_FIELDS,\n api: 'select',\n});\n\n/** 从轻量目标定义构造固定 API Key 服务商,补充展示信息、目录和协议能力。 */\nfunction fixedApiKeyProvider(\n key: PiProviderKey,\n name: string,\n chineseName = name\n): Readonly {\n const target = PI_PROVIDER_TARGET_REGISTRY[key];\n const apiCapabilities = Object.freeze(\n Object.fromEntries(target.allowedApis.map(api => [api, API_CAPABILITIES[api]]))\n ) as Readonly>>>;\n return Object.freeze({\n ...target,\n displayName: Object.freeze({ 'zh-CN': chineseName, en: name }),\n apiCapabilities,\n fields: target.allowedApis.length > 1 ? MULTI_API_KEY_FIELDS : API_KEY_FIELDS,\n });\n}\n\nexport const PI_PROVIDER_REGISTRY: Readonly> =\n Object.freeze({\n 'ant-ling': fixedApiKeyProvider('ant-ling', 'Ant Ling'),\n anthropic: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.anthropic,\n displayName: Object.freeze({ 'zh-CN': 'Anthropic', en: 'Anthropic' }),\n apiCapabilities: Object.freeze({\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n }),\n fields: MIXED_AUTH_FIELDS,\n oauth: ANTHROPIC_OAUTH,\n }),\n baseten: fixedApiKeyProvider('baseten', 'Baseten'),\n cerebras: fixedApiKeyProvider('cerebras', 'Cerebras'),\n deepseek: fixedApiKeyProvider('deepseek', 'DeepSeek'),\n fireworks: fixedApiKeyProvider('fireworks', 'Fireworks'),\n 'github-copilot': fixedApiKeyProvider('github-copilot', 'GitHub Copilot'),\n google: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.google,\n displayName: Object.freeze({ 'zh-CN': 'Google Gemini', en: 'Google Gemini' }),\n apiCapabilities: Object.freeze({\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n }),\n fields: API_KEY_FIELDS,\n }),\n groq: fixedApiKeyProvider('groq', 'Groq'),\n huggingface: fixedApiKeyProvider('huggingface', 'Hugging Face'),\n 'kimi-coding': fixedApiKeyProvider('kimi-coding', 'Kimi For Coding'),\n minimax: fixedApiKeyProvider('minimax', 'MiniMax'),\n 'minimax-cn': fixedApiKeyProvider('minimax-cn', 'MiniMax CN', 'MiniMax(中国)'),\n mistral: fixedApiKeyProvider('mistral', 'Mistral'),\n moonshotai: fixedApiKeyProvider('moonshotai', 'Moonshot AI'),\n 'moonshotai-cn': fixedApiKeyProvider(\n 'moonshotai-cn',\n 'Moonshot AI CN',\n 'Moonshot AI(中国)'\n ),\n nvidia: fixedApiKeyProvider('nvidia', 'NVIDIA'),\n openai: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.openai,\n displayName: Object.freeze({ 'zh-CN': 'OpenAI', en: 'OpenAI' }),\n apiCapabilities: Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n }),\n fields: EDITABLE_OPENAI_FIELDS,\n }),\n 'openai-codex': Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY['openai-codex'],\n displayName: Object.freeze({ 'zh-CN': 'OpenAI Codex', en: 'OpenAI Codex' }),\n apiCapabilities: Object.freeze({\n 'openai-codex-responses': CODEX_CAPABILITIES,\n }),\n fields: OAUTH_FIELDS,\n oauth: OPENAI_CODEX_OAUTH,\n }),\n opencode: fixedApiKeyProvider('opencode', 'OpenCode Zen'),\n 'opencode-go': fixedApiKeyProvider('opencode-go', 'OpenCode Go'),\n openrouter: fixedApiKeyProvider('openrouter', 'OpenRouter'),\n 'qwen-token-plan': fixedApiKeyProvider('qwen-token-plan', 'Qwen Token Plan'),\n 'qwen-token-plan-cn': fixedApiKeyProvider(\n 'qwen-token-plan-cn',\n 'Qwen Token Plan CN',\n 'Qwen Token Plan(中国)'\n ),\n 'qwen-token-plan-individual': fixedApiKeyProvider(\n 'qwen-token-plan-individual',\n 'Qwen Token Plan Individual'\n ),\n together: fixedApiKeyProvider('together', 'Together'),\n 'vercel-ai-gateway': fixedApiKeyProvider('vercel-ai-gateway', 'Vercel AI Gateway'),\n xai: fixedApiKeyProvider('xai', 'xAI'),\n xiaomi: fixedApiKeyProvider('xiaomi', 'Xiaomi'),\n 'xiaomi-token-plan-ams': fixedApiKeyProvider(\n 'xiaomi-token-plan-ams',\n 'Xiaomi Token Plan AMS'\n ),\n 'xiaomi-token-plan-cn': fixedApiKeyProvider(\n 'xiaomi-token-plan-cn',\n 'Xiaomi Token Plan CN',\n 'Xiaomi Token Plan(中国)'\n ),\n 'xiaomi-token-plan-sgp': fixedApiKeyProvider(\n 'xiaomi-token-plan-sgp',\n 'Xiaomi Token Plan SGP'\n ),\n zai: fixedApiKeyProvider('zai', 'Z.AI'),\n 'zai-coding-cn': fixedApiKeyProvider(\n 'zai-coding-cn',\n 'Z.AI Coding CN',\n 'Z.AI Coding(中国)'\n ),\n } satisfies Record);\n\n/** 按注册顺序返回支持的服务商定义,供来源界面生成选项。 */\nexport function listPiProviderDefinitions(): readonly PiProviderDefinition[] {\n return PI_PROVIDER_KEYS.map(key => PI_PROVIDER_REGISTRY[key]);\n}\n\n/** 按注册键读取服务商定义,拒绝命中对象原型上的同名属性。 */\nexport function getPiProviderDefinition(key: string): PiProviderDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_REGISTRY, key)\n ? PI_PROVIDER_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** Compatibility spelling used by the OAuth/UI integration. */\nexport const getPiProviderRegistration = getPiProviderDefinition;\n\n/** 取得服务商的固定模型目录,未知服务商返回空列表。 */\nexport function getPiCatalogModels(key: PiProviderKey | string): readonly Model[] {\n const definition = getPiProviderDefinition(key);\n if (!definition) {\n return [];\n }\n const models = Object.values(CATALOGS[definition.key]);\n return definition.key === 'google'\n ? models.filter(model => !GOOGLE_RETIRED_MODEL_IDS.has(model.id))\n : models;\n}\n\n/** 综合模型元数据和已知模型限制判断是否可发送 temperature。 */\nfunction modelSupportsTemperature(model: Model | undefined): boolean {\n const compat = model?.compat;\n return (\n compat === undefined ||\n !('supportsTemperature' in compat) ||\n compat.supportsTemperature !== false\n );\n}\n\n/**\n * Anthropic rejects every sampling control (temperature, top_p and top_k) for these catalog\n * models. Keep the explicit IDs alongside the provider registry so a missing compat flag cannot\n * accidentally make a newly published catalog entry permissive.\n */\nconst ANTHROPIC_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'claude-fable-5',\n 'claude-opus-4-7',\n 'claude-opus-4-8',\n 'claude-opus-5',\n 'claude-sonnet-5',\n]);\n\n/** Catalog models explicitly available through OpenAI Chat Completions as well as Responses. */\nconst OPENAI_CHAT_COMPLETIONS_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-4',\n 'gpt-4-turbo',\n 'gpt-4.1',\n 'gpt-4.1-mini',\n 'gpt-4.1-nano',\n 'gpt-4o',\n 'gpt-4o-2024-05-13',\n 'gpt-4o-2024-08-06',\n 'gpt-4o-2024-11-20',\n 'gpt-4o-mini',\n 'gpt-5',\n 'gpt-5-chat-latest',\n 'gpt-5-mini',\n 'gpt-5-nano',\n 'gpt-5.1',\n 'gpt-5.2',\n 'gpt-5.2-chat-latest',\n 'gpt-5.3-chat-latest',\n 'gpt-5.4',\n 'gpt-5.4-mini',\n 'gpt-5.4-nano',\n 'gpt-5.5',\n 'gpt-5.6-luna',\n 'gpt-5.6-sol',\n 'gpt-5.6-terra',\n 'o1',\n 'o3',\n 'o3-mini',\n 'o4-mini',\n]);\n\nconst OPENAI_NON_STREAMING_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-5.5-pro',\n 'gpt-realtime-2.1',\n 'o1-pro',\n 'o3-pro',\n]);\n\nconst GOOGLE_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'gemini-3.6-flash',\n 'gemini-3.7-flash',\n]);\n\n/** 验证固定目录模型是否适用于当前服务商协议,避免借用另一协议的能力元数据。 */\nexport function isPiCatalogModelApiCompatible(\n definition: PiProviderDefinition,\n model: Model,\n api: PiWireApi\n): boolean {\n if (model.api === api) {\n return true;\n }\n return (\n definition.key === 'openai' &&\n model.api === 'openai-responses' &&\n api === 'openai-completions' &&\n OPENAI_CHAT_COMPLETIONS_MODEL_IDS.has(model.id)\n );\n}\n\n/**\n * 重新查找并校验目录项,不直接信任调用方的 catalogHit 标记。\n * 只有确认匹配的元数据才能影响图片、流式和采样能力。\n */\nfunction verifiedCatalogModel(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean }\n): Model | undefined {\n const model = options.model;\n if (!options.catalogHit || !model) {\n return undefined;\n }\n\n const catalogModel = getPiCatalogModels(definition.key).find(\n candidate => candidate.id === model.id\n );\n if (\n !catalogModel ||\n catalogModel.provider !== definition.providerId ||\n model.provider !== definition.providerId ||\n !isPiCatalogModelApiCompatible(definition, catalogModel, api) ||\n (model.api !== api && model.api !== catalogModel.api) ||\n catalogModel.baseUrl !== getPiProviderApiBaseUrl(definition, api) ||\n model.baseUrl !== getPiProviderApiBaseUrl(definition, api)\n ) {\n return undefined;\n }\n\n return catalogModel;\n}\n\n/** 将服务商协议能力与可信目录模型限制合并,给出当前请求可用的能力。 */\nexport function resolvePiCapabilities(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean; thinkingLevel?: string }\n): Readonly | undefined {\n const registered = definition.apiCapabilities[api];\n if (!registered) {\n return undefined;\n }\n\n const catalogModel = verifiedCatalogModel(definition, api, options);\n const samplingModel = catalogModel ?? options.model;\n const thinkingEnabled = isPiThinkingEnabled(\n resolvePiThinkingLevel(options.thinkingLevel, catalogModel)\n );\n const anthropicSamplingAllowed =\n definition.key !== 'anthropic' ||\n catalogModel === undefined ||\n (samplingModel !== undefined &&\n modelSupportsTemperature(samplingModel) &&\n !ANTHROPIC_NO_SAMPLING_MODEL_IDS.has(samplingModel.id));\n // pi's Responses adapter selects `reasoning.effort: none` when the catalog explicitly\n // exposes it. Older GPT-5/o-series/pro models cannot turn reasoning off and reject\n // temperature/top_p. Dynamic OpenAI-compatible models remain API-level configurable.\n const openAISamplingAllowed =\n definition.key !== 'openai' ||\n !catalogModel?.reasoning ||\n catalogModel.thinkingLevelMap?.off === 'none';\n const googleSamplingAllowed =\n definition.key !== 'google' ||\n !catalogModel ||\n !GOOGLE_NO_SAMPLING_MODEL_IDS.has(catalogModel.id);\n const samplingAllowed =\n anthropicSamplingAllowed &&\n openAISamplingAllowed &&\n googleSamplingAllowed &&\n !(thinkingEnabled && (api === 'anthropic-messages' || definition.key === 'openai'));\n const streamingAllowed =\n definition.key !== 'openai' ||\n !catalogModel ||\n !OPENAI_NON_STREAMING_MODEL_IDS.has(catalogModel.id);\n\n return Object.freeze({\n ...registered,\n // These are adapter-level promises. Any narrower endpoint/model support is reported by\n // the provider request itself instead of being guessed from a pinned catalog.\n streaming: registered.streaming && streamingAllowed,\n tools: registered.tools,\n structuredOutput: registered.structuredOutput,\n jsonObjectOutput: registered.jsonObjectOutput,\n imageInput: registered.imageInput && catalogModel?.input.includes('image') === true,\n temperature:\n registered.temperature && modelSupportsTemperature(samplingModel) && samplingAllowed,\n sampling: samplingAllowed ? registered.sampling : NO_SAMPLING,\n });\n}\n\n/** 仅创建当前来源明确允许的协议懒加载适配器,避免引入无关服务商实现。 */\nexport function createPiApiImplementations(\n definitionOrKey: PiProviderDefinition | PiProviderKey | string\n): Partial> {\n const definition =\n typeof definitionOrKey === 'string'\n ? getPiProviderDefinition(definitionOrKey)\n : definitionOrKey;\n if (!definition) {\n return {};\n }\n\n return Object.fromEntries(\n definition.allowedApis.map(api => [api, API_LOADERS[api]()])\n ) as Partial>;\n}\n","export const PI_PROVIDER_KEYS = [\n 'ant-ling',\n 'anthropic',\n 'baseten',\n 'cerebras',\n 'deepseek',\n 'fireworks',\n 'github-copilot',\n 'google',\n 'groq',\n 'huggingface',\n 'kimi-coding',\n 'minimax',\n 'minimax-cn',\n 'mistral',\n 'moonshotai',\n 'moonshotai-cn',\n 'nvidia',\n 'openai',\n 'openai-codex',\n 'opencode',\n 'opencode-go',\n 'openrouter',\n 'qwen-token-plan',\n 'qwen-token-plan-cn',\n 'qwen-token-plan-individual',\n 'together',\n 'vercel-ai-gateway',\n 'xai',\n 'xiaomi',\n 'xiaomi-token-plan-ams',\n 'xiaomi-token-plan-cn',\n 'xiaomi-token-plan-sgp',\n 'zai',\n 'zai-coding-cn',\n] as const;\n/** MVU 当前注册的服务商标识,用于设置、连接规则与 Pi 模型目录之间的关联。 */\nexport type PiProviderKey = (typeof PI_PROVIDER_KEYS)[number];\n\nexport const PI_WIRE_APIS = [\n 'openai-responses',\n 'openai-completions',\n 'openai-codex-responses',\n 'anthropic-messages',\n 'google-generative-ai',\n 'mistral-conversations',\n] as const;\n/** 实际发送请求所用的协议,与服务商名称分开选择。 */\nexport type PiWireApi = (typeof PI_WIRE_APIS)[number];\n\n/** 标记必须使用流式传输的 Codex 账号接口,该接口没有 JSON 非流式模式。 */\nexport function isPiStreamingRequired(api: string): boolean {\n return api === 'openai-codex-responses';\n}\n\nexport const PI_AUTH_TYPES = ['api_key', 'oauth'] as const;\n/** Pi 连接的凭证来源:手填 API Key 或浏览器 OAuth 登录凭证。 */\nexport type PiAuthType = (typeof PI_AUTH_TYPES)[number];\n\n/** 不依赖 SDK 的连接规则,供设置迁移、凭证目标隔离和运行时校验共用。 */\nexport interface PiProviderTargetDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须通过酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n}\n\n/** 注册连接规则时允许省略代理列表,由定义函数补齐并冻结为空数组。 */\ntype PiProviderTargetDefinitionInput = Omit & {\n readonly corsProxyRequiredApis?: readonly PiWireApi[];\n};\n\nconst NO_CORS_PROXY_REQUIRED_APIS: readonly PiWireApi[] = Object.freeze([]);\n\n/** 补齐并冻结目标的代理策略,使共享注册信息不能被请求修改。 */\nfunction definePiProviderTarget(\n input: PiProviderTargetDefinitionInput\n): Readonly {\n return Object.freeze({\n ...input,\n corsProxyRequiredApis:\n input.corsProxyRequiredApis === undefined\n ? NO_CORS_PROXY_REQUIRED_APIS\n : Object.freeze([...input.corsProxyRequiredApis]),\n });\n}\n\n/**\n * Lightweight source of truth for provider/API/auth/endpoint compatibility. Keeping this separate\n * from catalogs and adapters lets settings migration validate credential targets without loading\n * the provider SDK boundary.\n */\nexport const PI_PROVIDER_TARGET_REGISTRY: Readonly<\n Record>\n> = Object.freeze({\n 'ant-ling': definePiProviderTarget({\n key: 'ant-ling',\n providerId: 'ant-ling',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.ant-ling.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n anthropic: definePiProviderTarget({\n key: 'anthropic',\n providerId: 'anthropic',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key', 'oauth'] as const),\n defaultBaseUrl: 'https://api.anthropic.com',\n allowCustomEndpoint: true,\n }),\n baseten: definePiProviderTarget({\n key: 'baseten',\n providerId: 'baseten',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://inference.baseten.co/v1',\n allowCustomEndpoint: false,\n }),\n cerebras: definePiProviderTarget({\n key: 'cerebras',\n providerId: 'cerebras',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.cerebras.ai/v1',\n allowCustomEndpoint: false,\n }),\n deepseek: definePiProviderTarget({\n key: 'deepseek',\n providerId: 'deepseek',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.deepseek.com',\n allowCustomEndpoint: false,\n }),\n fireworks: definePiProviderTarget({\n key: 'fireworks',\n providerId: 'fireworks',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.fireworks.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://api.fireworks.ai/inference',\n 'openai-completions': 'https://api.fireworks.ai/inference/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n 'github-copilot': definePiProviderTarget({\n key: 'github-copilot',\n providerId: 'github-copilot',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.individual.githubcopilot.com',\n corsProxyRequiredApis: ['anthropic-messages', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n google: definePiProviderTarget({\n key: 'google',\n providerId: 'google',\n defaultApi: 'google-generative-ai',\n allowedApis: Object.freeze(['google-generative-ai'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n allowCustomEndpoint: false,\n }),\n groq: definePiProviderTarget({\n key: 'groq',\n providerId: 'groq',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.groq.com/openai/v1',\n allowCustomEndpoint: false,\n }),\n huggingface: definePiProviderTarget({\n key: 'huggingface',\n providerId: 'huggingface',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://router.huggingface.co/v1',\n allowCustomEndpoint: false,\n }),\n 'kimi-coding': definePiProviderTarget({\n key: 'kimi-coding',\n providerId: 'kimi-coding',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.kimi.com/coding/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n minimax: definePiProviderTarget({\n key: 'minimax',\n providerId: 'minimax',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimax.io/anthropic',\n allowCustomEndpoint: false,\n }),\n 'minimax-cn': definePiProviderTarget({\n key: 'minimax-cn',\n providerId: 'minimax-cn',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimaxi.com/anthropic',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n mistral: definePiProviderTarget({\n key: 'mistral',\n providerId: 'mistral',\n defaultApi: 'mistral-conversations',\n allowedApis: Object.freeze(['mistral-conversations'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.mistral.ai',\n allowCustomEndpoint: false,\n }),\n moonshotai: definePiProviderTarget({\n key: 'moonshotai',\n providerId: 'moonshotai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'moonshotai-cn': definePiProviderTarget({\n key: 'moonshotai-cn',\n providerId: 'moonshotai-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.cn/v1',\n allowCustomEndpoint: false,\n }),\n nvidia: definePiProviderTarget({\n key: 'nvidia',\n providerId: 'nvidia',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://integrate.api.nvidia.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n openai: definePiProviderTarget({\n key: 'openai',\n providerId: 'openai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.openai.com/v1',\n allowCustomEndpoint: true,\n }),\n 'openai-codex': definePiProviderTarget({\n key: 'openai-codex',\n providerId: 'openai-codex',\n defaultApi: 'openai-codex-responses',\n allowedApis: Object.freeze(['openai-codex-responses'] as const),\n defaultAuthType: 'oauth',\n allowedAuthTypes: Object.freeze(['oauth'] as const),\n defaultBaseUrl: 'https://chatgpt.com/backend-api',\n corsProxyRequiredApis: ['openai-codex-responses'],\n allowCustomEndpoint: false,\n }),\n opencode: definePiProviderTarget({\n key: 'opencode',\n providerId: 'opencode',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/inference',\n 'google-generative-ai': 'https://opencode.ai/inference/v1',\n 'openai-completions': 'https://opencode.ai/inference/v1',\n 'openai-responses': 'https://opencode.ai/inference/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n 'opencode-go': definePiProviderTarget({\n key: 'opencode-go',\n providerId: 'opencode-go',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/inference/go',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/inference/go',\n 'google-generative-ai': 'https://opencode.ai/inference/go/v1',\n 'openai-completions': 'https://opencode.ai/inference/go/v1',\n 'openai-responses': 'https://opencode.ai/inference/go/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n openrouter: definePiProviderTarget({\n key: 'openrouter',\n providerId: 'openrouter',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions', 'anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://openrouter.ai/api/v1',\n apiBaseUrls: Object.freeze({\n 'openai-completions': 'https://openrouter.ai/api/v1',\n 'anthropic-messages': 'https://openrouter.ai/api',\n }),\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan': definePiProviderTarget({\n key: 'qwen-token-plan',\n providerId: 'qwen-token-plan',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-cn': definePiProviderTarget({\n key: 'qwen-token-plan-cn',\n providerId: 'qwen-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-individual': definePiProviderTarget({\n key: 'qwen-token-plan-individual',\n providerId: 'qwen-token-plan-individual',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n together: definePiProviderTarget({\n key: 'together',\n providerId: 'together',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.together.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'vercel-ai-gateway': definePiProviderTarget({\n key: 'vercel-ai-gateway',\n providerId: 'vercel-ai-gateway',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://ai-gateway.vercel.sh',\n allowCustomEndpoint: false,\n }),\n xai: definePiProviderTarget({\n key: 'xai',\n providerId: 'xai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.x.ai/v1',\n allowCustomEndpoint: false,\n }),\n xiaomi: definePiProviderTarget({\n key: 'xiaomi',\n providerId: 'xiaomi',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-ams': definePiProviderTarget({\n key: 'xiaomi-token-plan-ams',\n providerId: 'xiaomi-token-plan-ams',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-ams.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-cn': definePiProviderTarget({\n key: 'xiaomi-token-plan-cn',\n providerId: 'xiaomi-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-cn.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-sgp': definePiProviderTarget({\n key: 'xiaomi-token-plan-sgp',\n providerId: 'xiaomi-token-plan-sgp',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n zai: definePiProviderTarget({\n key: 'zai',\n providerId: 'zai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.z.ai/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n 'zai-coding-cn': definePiProviderTarget({\n key: 'zai-coding-cn',\n providerId: 'zai-coding-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n});\n\n/** 读取轻量目标注册信息,供配置和密钥归属校验使用,无需加载 SDK 目录。 */\nexport function getPiProviderTargetDefinition(\n key: string\n): Readonly | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_TARGET_REGISTRY, key)\n ? PI_PROVIDER_TARGET_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** 判断指定服务商的内置协议端点是否必须经酒馆 CORS 代理访问。 */\nexport function isPiCorsProxyRequired(\n definition: Readonly | undefined,\n api: string\n): boolean {\n return definition?.corsProxyRequiredApis.includes(api as PiWireApi) ?? false;\n}\n\n/**\n * 内置端点遵循注册的代理策略,自定义端点遵循用户勾选状态。\n * 等价写法先规范化,避免把显式填写的默认地址误认作自定义目标。\n */\nexport function shouldUsePiCorsProxy(\n definition: Readonly | undefined,\n api: string,\n endpoint: string,\n customEndpointUseProxy: unknown\n): boolean {\n const explicit_endpoint = endpoint.trim();\n if (\n !definition ||\n explicit_endpoint === '' ||\n !definition.allowedApis.includes(api as PiWireApi)\n ) {\n return isPiCorsProxyRequired(definition, api);\n }\n try {\n const wire_api = api as PiWireApi;\n const configured_base = normalizePiApiBaseEndpoint(wire_api, explicit_endpoint);\n const registered_base = normalizePiApiBaseEndpoint(\n wire_api,\n getPiProviderApiBaseUrl(definition, wire_api)\n );\n if (configured_base === registered_base) {\n return isPiCorsProxyRequired(definition, api);\n }\n } catch {\n // Target validation reports malformed endpoints. Until then, keep the explicit checkbox\n // semantics so the UI does not mislabel an invalid value as a registered route.\n }\n return customEndpointUseProxy === true;\n}\n\n/** 优先读取协议专用基础地址,否则使用服务商默认地址。 */\nexport function getPiProviderApiBaseUrl(\n definition: Readonly,\n api: PiWireApi\n): string {\n return definition.apiBaseUrls?.[api] ?? definition.defaultBaseUrl;\n}\n\nexport class PiEndpointValidationError extends Error {\n /** 将端点格式或传输限制失败标记为独立的端点校验错误。 */\n constructor(message: string) {\n super(message);\n this.name = 'PiEndpointValidationError';\n }\n}\n\nconst HTTP_LOOPBACK_HOSTNAMES: ReadonlySet = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/** 规范端点地址,要求 HTTPS 或回环 HTTP,并拒绝内嵌凭证、查询参数和片段。 */\nexport function normalizePiTargetEndpoint(endpoint: string): string {\n let parsed: URL;\n try {\n parsed = new URL(endpoint.trim());\n } catch {\n throw new PiEndpointValidationError('More source endpoint must be a valid URL');\n }\n\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new PiEndpointValidationError('More source endpoint must use http or https');\n }\n if (\n parsed.protocol === 'http:' &&\n !HTTP_LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase())\n ) {\n throw new PiEndpointValidationError(\n 'More source HTTP endpoint must use localhost, 127.0.0.1, or [::1]'\n );\n }\n if (parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new PiEndpointValidationError(\n 'More source endpoint must not contain credentials, a query, or a fragment'\n );\n }\n\n const normalized_path = parsed.pathname.replace(/\\/+$/, '');\n return `${parsed.origin}${normalized_path}`;\n}\n\nconst PI_API_OPERATION_PATH_SUFFIXES: Partial> = {\n 'openai-responses': ['/responses'],\n 'openai-completions': ['/chat/completions', '/completions'],\n 'anthropic-messages': ['/v1/messages', '/messages'],\n};\n\n/**\n * 将安全端点转换为 SDK 需要的 API 基础地址。\n * 最多移除一次当前协议会自行追加的操作路径,避免重复拼接或过度裁剪。\n */\nexport function normalizePiApiBaseEndpoint(api: PiWireApi, endpoint: string): string {\n const normalized_endpoint = normalizePiTargetEndpoint(endpoint);\n const parsed = new URL(normalized_endpoint);\n const normalized_path_lower = parsed.pathname.toLowerCase();\n const operation_path = PI_API_OPERATION_PATH_SUFFIXES[api]?.find(suffix =>\n normalized_path_lower.endsWith(suffix)\n );\n if (!operation_path) {\n return normalized_endpoint;\n }\n\n const base_path = parsed.pathname.slice(0, -operation_path.length).replace(/\\/+$/, '');\n return normalizePiTargetEndpoint(`${parsed.origin}${base_path}`);\n}\n\n/**\n * 仅为完整、合法的 API Key 目标生成服务商与端点对应的缓存键。\n * 未知协议、OAuth、无效地址或不允许自定义端点时返回空值。\n */\nexport function resolvePiApiKeyScope(\n definition: Readonly | undefined,\n api: string,\n auth_type: string,\n endpoint: string\n): string {\n if (\n !definition ||\n auth_type !== 'api_key' ||\n !definition.allowedAuthTypes.includes('api_key') ||\n !definition.allowedApis.includes(api as PiWireApi) ||\n (endpoint.trim() !== '' && !definition.allowCustomEndpoint)\n ) {\n return '';\n }\n\n try {\n const effective_endpoint = normalizePiApiBaseEndpoint(\n api as PiWireApi,\n endpoint.trim() === ''\n ? getPiProviderApiBaseUrl(definition, api as PiWireApi)\n : endpoint\n );\n return `${definition.key}\\n${effective_endpoint}`;\n } catch {\n return '';\n }\n}\n","import type {\n Api,\n AssistantMessage,\n Context,\n ImageContent,\n Message,\n TextContent,\n ToolCall,\n ToolResultMessage,\n Usage,\n UserMessage,\n} from '@earendil-works/pi-ai';\n\n/** 需要按酒馆原始位置保留的系统消息,交由请求载荷 hook 在原位置恢复。 */\nexport type PiLateSystemMessage = {\n sourceIndex: number;\n /** 插在转换后第几条普通消息之前;等于 messages.length 表示位于末尾。 */\n beforeMessageIndex: number;\n text: string;\n};\n\n/** 上下文转换的内容保留及空消息诊断,不包含完整提示词。 */\nexport type PiContextAdapterDiagnostics = {\n /** 保留待处理的 system 数量;最终角色由协议适配层自动决定。 */\n preservedLateSystemCount: number;\n droppedEmptyMessageIndexes: number[];\n};\n\n/** 标准 Pi 上下文与必须一并传给载荷适配层的原生 system 消息。 */\nexport type PiContextAdapterResult = {\n context: Context;\n lateSystemMessages: readonly PiLateSystemMessage[];\n diagnostics: PiContextAdapterDiagnostics;\n};\n\n/** 提示词转换或原生 system 恢复失败的稳定分类。 */\nexport type PiContextAdapterErrorCode =\n | 'invalid-image'\n | 'invalid-tool-call'\n | 'system-payload-mismatch'\n | 'missing-tool-call'\n | 'unsupported-content';\n\nexport class PiContextAdapterError extends Error {\n /** 保留消息下标和错误类别,便于将上下文转换失败定位到原始提示词。 */\n constructor(\n message: string,\n readonly code: PiContextAdapterErrorCode,\n readonly sourceIndex: number\n ) {\n super(message);\n this.name = 'PiContextAdapterError';\n }\n}\n\n/** 酒馆捕获消息,加上历史工具结果可能携带的错误标记。 */\ntype SendingMessage = SillyTavern.SendingMessage & {\n name?: string;\n is_error?: boolean;\n isError?: boolean;\n};\n\n/** 酒馆多模态内容数组中的单个块。 */\ntype ContentBlock = NonNullable>[number];\n/** 当前转换层允许交给 Pi 的文本或图片输入。 */\ntype PiInputContent = TextContent | ImageContent;\n\nconst IMPORTED_ASSISTANT_API = 'sillytavern-import' as Api;\nconst IMPORTED_ASSISTANT_PROVIDER = 'sillytavern';\nconst IMPORTED_ASSISTANT_MODEL = 'prepared-prompt';\n\nconst SUPPORTED_IMAGE_MIME_TYPES = new Set(['image/gif', 'image/jpeg', 'image/png', 'image/webp']);\nconst MEBIBYTE = 1024 * 1024;\nconst MAX_DATA_URL_HEADER_CHARACTERS = 64;\nconst MAX_DECODED_IMAGE_HEADER_CHARACTERS = 768;\nconst MAX_ENCODED_IMAGE_HEADER_CHARACTERS = Math.ceil(MAX_DECODED_IMAGE_HEADER_CHARACTERS / 3) * 4;\n\nexport const PI_IMAGE_INPUT_LIMITS = Object.freeze({\n // Five MiB matches the strictest supported provider's common per-image cap.\n // The aggregate/count ceilings bound base64 copies on mobile browsers.\n maxDecodedBytesPerImage: 5 * MEBIBYTE,\n maxDecodedBytesPerContext: 16 * MEBIBYTE,\n maxImagesPerContext: 20,\n});\n\nconst MAX_ENCODED_CHARACTERS_PER_IMAGE =\n Math.ceil(PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / 3) * 4;\n\n/** 图片校验时提取的预算元数据,避免 token 预检重复解码图片。 */\nexport type PiImageMetadata = Readonly<{\n decodedBytes: number;\n dimensions?: Readonly<{\n width: number;\n height: number;\n }>;\n}>;\n\n/** 单次上下文转换累计使用的图片数量与解码字节数。 */\ntype ImageInputBudget = {\n decodedBytes: number;\n imageCount: number;\n};\n\nconst imageMetadata = new WeakMap();\n\n/** 读取转换时记录的图片大小和尺寸,供预算检查复用,避免再次解码。 */\nexport function getPiImageMetadata(image: ImageContent): PiImageMetadata | undefined {\n return imageMetadata.get(image);\n}\n\n/** 为导入的历史助手消息补齐零用量结构,避免把历史内容记为本次生成消耗。 */\nfunction makeZeroUsage(): Usage {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n}\n\n/** 转义消息名称中的 XML 特殊字符,防止名称破坏注入标记结构。 */\nfunction escapeXmlText(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');\n}\n\n/** 将有效的酒馆消息名称转换为可拼接的文本前缀。 */\nexport function formatSendingMessageName(name: string | undefined): string {\n if (typeof name !== 'string' || name.trim() === '') {\n return '';\n }\n return `${escapeXmlText(name)}`;\n}\n\n/** 识别仅含空白的内容,供严格校验和空消息处理使用。 */\nfunction isBlank(value: string): boolean {\n return value.trim().length === 0;\n}\n\n/** 仅接受普通对象或空原型对象,避免把类实例作为结构化消息内容。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 检查指定偏移处的文件头字节,用于验证声明的图片类型。 */\nfunction hasBytesAt(bytes: string, offset: number, expected: readonly number[]): boolean {\n return expected.every((value, index) => bytes.charCodeAt(offset + index) === value);\n}\n\n/** 核对 PNG、JPEG、GIF 或 WebP 文件签名,拒绝伪装为图片的数据。 */\nfunction hasRecognizableImageSignature(mimeType: string, bytes: string): boolean {\n if (mimeType === 'image/png') {\n return hasBytesAt(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n }\n if (mimeType === 'image/jpeg') {\n return hasBytesAt(bytes, 0, [0xff, 0xd8, 0xff]);\n }\n if (mimeType === 'image/gif') {\n return bytes.startsWith('GIF87a') || bytes.startsWith('GIF89a');\n }\n if (mimeType === 'image/webp') {\n return bytes.startsWith('RIFF') && bytes.slice(8, 12) === 'WEBP';\n }\n return false;\n}\n\n/** 判断字符是否属于标准 Base64 字母表,供图片载荷逐字符校验使用。 */\nfunction isBase64Character(code: number): boolean {\n return (\n (code >= 0x41 && code <= 0x5a) ||\n (code >= 0x61 && code <= 0x7a) ||\n (code >= 0x30 && code <= 0x39) ||\n code === 0x2b ||\n code === 0x2f\n );\n}\n\n/** 按大端读取图片头中的 16 位整数。 */\nfunction readUint16BigEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) * 0x100 + bytes.charCodeAt(offset + 1);\n}\n\n/** 按小端读取图片头中的 16 位整数。 */\nfunction readUint16LittleEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) + bytes.charCodeAt(offset + 1) * 0x100;\n}\n\n/** 按小端读取 WebP 图片头中的 24 位整数。 */\nfunction readUint24LittleEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) +\n bytes.charCodeAt(offset + 1) * 0x100 +\n bytes.charCodeAt(offset + 2) * 0x10000\n );\n}\n\n/** 按大端读取图片头中的 32 位整数,并保持无符号取值。 */\nfunction readUint32BigEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) * 0x1000000 +\n bytes.charCodeAt(offset + 1) * 0x10000 +\n bytes.charCodeAt(offset + 2) * 0x100 +\n bytes.charCodeAt(offset + 3)\n );\n}\n\nconst JPEG_START_OF_FRAME_MARKERS = new Set([\n 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,\n]);\n\n/** 扫描已解码的 JPEG 头部标记以提取尺寸;信息不足时不推测尺寸。 */\nfunction readJpegDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 4 || bytes.charCodeAt(0) !== 0xff || bytes.charCodeAt(1) !== 0xd8) {\n return undefined;\n }\n\n let offset = 2;\n while (offset + 3 < bytes.length) {\n if (bytes.charCodeAt(offset) !== 0xff) {\n offset += 1;\n continue;\n }\n while (offset < bytes.length && bytes.charCodeAt(offset) === 0xff) {\n offset += 1;\n }\n const marker = bytes.charCodeAt(offset);\n offset += 1;\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {\n continue;\n }\n if (marker === 0xd9 || marker === 0xda || offset + 1 >= bytes.length) {\n break;\n }\n const segmentLength = readUint16BigEndian(bytes, offset);\n if (segmentLength < 2 || offset + segmentLength > bytes.length) {\n break;\n }\n if (JPEG_START_OF_FRAME_MARKERS.has(marker) && segmentLength >= 7) {\n return {\n height: readUint16BigEndian(bytes, offset + 3),\n width: readUint16BigEndian(bytes, offset + 5),\n };\n }\n offset += segmentLength;\n }\n return undefined;\n}\n\n/** 按 WebP 头部编码类型提取尺寸,兼容其不同帧头布局。 */\nfunction readWebpDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 30 || !bytes.startsWith('RIFF') || bytes.slice(8, 12) !== 'WEBP') {\n return undefined;\n }\n\n const chunkType = bytes.slice(12, 16);\n if (chunkType === 'VP8X') {\n return {\n width: readUint24LittleEndian(bytes, 24) + 1,\n height: readUint24LittleEndian(bytes, 27) + 1,\n };\n }\n if (chunkType === 'VP8 ' && bytes.slice(23, 26) === '\\x9d\\x01\\x2a') {\n return {\n width: readUint16LittleEndian(bytes, 26) & 0x3fff,\n height: readUint16LittleEndian(bytes, 28) & 0x3fff,\n };\n }\n if (chunkType === 'VP8L' && bytes.charCodeAt(20) === 0x2f) {\n const b1 = bytes.charCodeAt(21);\n const b2 = bytes.charCodeAt(22);\n const b3 = bytes.charCodeAt(23);\n const b4 = bytes.charCodeAt(24);\n return {\n width: 1 + b1 + ((b2 & 0x3f) << 8),\n height: 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10),\n };\n }\n return undefined;\n}\n\n/** 根据已验证的图片类型分派尺寸读取,无法读取时返回 undefined。 */\nfunction readImageDimensions(bytes: string, mimeType: string): PiImageMetadata['dimensions'] {\n if (mimeType === 'image/png' && bytes.length >= 24) {\n return {\n width: readUint32BigEndian(bytes, 16),\n height: readUint32BigEndian(bytes, 20),\n };\n }\n if (mimeType === 'image/gif' && bytes.length >= 10) {\n return {\n width: readUint16LittleEndian(bytes, 6),\n height: readUint16LittleEndian(bytes, 8),\n };\n }\n if (mimeType === 'image/jpeg') {\n return readJpegDimensions(bytes);\n }\n if (mimeType === 'image/webp') {\n return readWebpDimensions(bytes);\n }\n return undefined;\n}\n\n/** 在受限的头部长度内查找 data URL 分隔符,避免对异常长头部做无界扫描。 */\nfunction findDataUrlSeparator(url: string): number {\n const maximumOffset = Math.min(url.length, MAX_DATA_URL_HEADER_CHARACTERS + 1);\n for (let offset = 5; offset < maximumOffset; offset++) {\n if (url.charCodeAt(offset) === 0x2c) {\n return offset;\n }\n }\n return -1;\n}\n\n/** 只解码识别图片类型和尺寸所需的有限头部,控制大图转换的内存开销。 */\nfunction decodeImageHeader(url: string, dataStart: number, encodedLength: number): string {\n const prefixLength = Math.min(encodedLength, MAX_ENCODED_IMAGE_HEADER_CHARACTERS);\n return atob(url.slice(dataStart, dataStart + prefixLength));\n}\n\n/**\n * 校验 data URL、Base64 编码、文件签名和图片预算,并记录元数据。\n * 仅接收受支持的内嵌图片,失败时携带原始消息下标,不自动下载远程地址。\n */\nfunction parseBase64Image(\n url: string,\n sourceIndex: number,\n imageBudget: ImageInputBudget\n): ImageContent {\n if (url.slice(0, 5).toLowerCase() !== 'data:') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含远程图片 URL;更多来源当前只接受 data URL 图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n\n if (url.length > MAX_DATA_URL_HEADER_CHARACTERS + 1 + MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const separator = findDataUrlSeparator(url);\n if (separator === -1) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const headerMatch = /^data:([^;,]+);base64$/i.exec(url.slice(0, separator));\n if (!headerMatch) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const [, rawMimeType] = headerMatch;\n const mimeType = rawMimeType.toLowerCase();\n if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片 MIME 类型不受支持;只接受 PNG、JPEG、GIF 或 WebP`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const dataStart = separator + 1;\n const encodedLength = url.length - dataStart;\n if (encodedLength > MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (encodedLength === 0 || encodedLength % 4 !== 0) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n let padding = 0;\n if (url.charCodeAt(url.length - 1) === 0x3d) {\n padding += 1;\n if (url.charCodeAt(url.length - 2) === 0x3d) {\n padding += 1;\n }\n }\n const decodedBytes = (encodedLength / 4) * 3 - padding;\n if (decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.imageCount + 1 > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求最多接受 ${PI_IMAGE_INPUT_LIMITS.maxImagesPerContext} 张图片`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.decodedBytes + decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求的图片总量超过 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const contentEnd = url.length - padding;\n for (let offset = dataStart; offset < contentEnd; offset++) {\n if (!isBase64Character(url.charCodeAt(offset))) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n for (let offset = contentEnd; offset < url.length; offset++) {\n if (url.charCodeAt(offset) !== 0x3d) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n\n let decodedHeader: string;\n try {\n decodedHeader = decodeImageHeader(url, dataStart, encodedLength);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无法解码的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n if (!hasRecognizableImageSignature(mimeType, decodedHeader)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片数据与声明的 MIME 类型不匹配或缺少有效文件头`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n imageBudget.imageCount += 1;\n imageBudget.decodedBytes += decodedBytes;\n\n const image: ImageContent = {\n type: 'image',\n data: url.slice(dataStart),\n mimeType,\n };\n const dimensions = readImageDimensions(decodedHeader, mimeType);\n imageMetadata.set(image, {\n decodedBytes,\n ...(dimensions === undefined ? {} : { dimensions }),\n });\n return image;\n}\n\n/** 将酒馆文本或图片块转换为 Pi 内容块,并执行对应的结构与图片校验。 */\nfunction convertContentBlock(\n block: ContentBlock,\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent {\n if (block.type === 'text') {\n return { type: 'text', text: block.text };\n }\n if (block.type === 'video_url') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含更多来源不支持的视频内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n if (block.type === 'image_url') {\n if (!allowImages) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法导入更多来源历史的图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return parseBase64Image(block.image_url.url, sourceIndex, imageBudget);\n }\n\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含未知内容块`,\n 'unsupported-content',\n sourceIndex\n );\n}\n\n/** 统一处理字符串和多块消息内容,逐块转换为 Pi 输入格式。 */\nfunction convertContent(\n content: SendingMessage['content'],\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent[] {\n if (typeof content === 'string') {\n return [{ type: 'text', text: content }];\n }\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map(block => convertContentBlock(block, sourceIndex, allowImages, imageBudget));\n}\n\n/** 判断内容块是否包含可发送的文本或图片。 */\nfunction contentHasValue(content: PiInputContent[]): boolean {\n return content.some(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 拼接文本内容块,供只允许纯文本的消息角色使用。 */\nfunction compactTextBlocks(content: PiInputContent[]): PiInputContent[] {\n return content.filter(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 提取 system 消息的纯文本,统一拒绝不支持的系统内容。 */\nfunction extractSystemText(message: SendingMessage, sourceIndex: number): string {\n const content = message.content;\n let text: string;\n if (typeof content === 'string') {\n text = content;\n } else if (Array.isArray(content)) {\n const textBlocks = content.map(block => {\n if (block.type !== 'text') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 system 消息包含非文本内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return block.text;\n });\n text = textBlocks.join('');\n } else {\n text = '';\n }\n\n const name = formatSendingMessageName(message.name);\n return [name, text].filter(value => value !== '').join('\\n');\n}\n\n/** 在消息正文前保留酒馆角色名,并避免生成空的名称前缀。 */\nfunction addNamePrefix(content: PiInputContent[], name: string | undefined): PiInputContent[] {\n const prefix = formatSendingMessageName(name);\n if (!prefix) {\n return content;\n }\n return [{ type: 'text', text: prefix }, ...content];\n}\n\n/** 将历史工具参数解析为普通对象,拒绝无效 JSON 或非对象参数。 */\nfunction parseToolArguments(argumentsValue: string, sourceIndex: number): ToolCall['arguments'] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(argumentsValue);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法解析的工具参数`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n if (!isPlainObject(parsed)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息的工具参数必须是 JSON object`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n // JSON.parse 已排除非 JSON 值,且上方校验保证参数是对象而非数组或标量。\n return parsed as ToolCall['arguments'];\n}\n\n/** 校验历史工具调用标识和参数,并转换为 Pi 工具调用块。 */\nfunction convertToolCalls(\n message: SendingMessage,\n sourceIndex: number,\n toolNamesById: Map\n): ToolCall[] {\n return (message.tool_calls ?? []).map(call => {\n if (!call.id || !call.function?.name) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无效工具调用`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n const existingName = toolNamesById.get(call.id);\n if (existingName && existingName !== call.function.name) {\n throw new PiContextAdapterError(\n `工具调用 ID ${call.id} 对应了多个工具名`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n toolNamesById.set(call.id, call.function.name);\n return {\n type: 'toolCall',\n id: call.id,\n name: call.function.name,\n arguments: parseToolArguments(call.function.arguments, sourceIndex),\n };\n });\n}\n\n/** 转换历史助手文本和工具调用,并补齐 Pi 要求的历史消息元数据。 */\nfunction convertAssistantMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): AssistantMessage {\n const textContent = compactTextBlocks(\n addNamePrefix(\n convertContent(message.content, sourceIndex, false, imageBudget),\n message.name\n )\n ) as TextContent[];\n const toolCalls = convertToolCalls(message, sourceIndex, toolNamesById);\n return {\n role: 'assistant',\n content: [...textContent, ...toolCalls],\n api: IMPORTED_ASSISTANT_API,\n provider: IMPORTED_ASSISTANT_PROVIDER,\n model: IMPORTED_ASSISTANT_MODEL,\n usage: makeZeroUsage(),\n stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',\n timestamp: now(),\n };\n}\n\n/** 将工具返回值关联到已知调用,保留工具名、错误标记及多模态结果。 */\nfunction convertToolResultMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): ToolResultMessage {\n const toolCallId = message.tool_call_id;\n if (!toolCallId) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息缺少 tool_call_id`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n const toolName = toolNamesById.get(toolCallId);\n if (!toolName) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息找不到对应的历史工具调用`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n return {\n role: 'toolResult',\n toolCallId,\n toolName,\n content: compactTextBlocks(convertContent(message.content, sourceIndex, true, imageBudget)),\n isError: message.is_error === true || message.isError === true,\n timestamp: now(),\n };\n}\n\n/** 所有来源统一丢弃空消息,并保留原始下标用于诊断。 */\nfunction recordEmptyMessage(sourceIndex: number, diagnostics: PiContextAdapterDiagnostics): void {\n diagnostics.droppedEmptyMessageIndexes.push(sourceIndex);\n}\n\n/**\n * 把酒馆最终提示词转换为 Pi 上下文,并返回转换诊断。\n * 前置 system 合并为系统提示词;后置 system 单独保留内容及原位置,由载荷 hook 恢复。\n * 同时校验工具关联和图片预算、清理空消息;渠道兼容由载荷适配层处理。\n * now 只供测试注入稳定时间,不提供渠道相关的转换策略开关。\n */\nexport function toPiContext(\n input: readonly SendingMessage[],\n now: () => number = Date.now\n): PiContextAdapterResult {\n const messages = Array.from(input);\n const diagnostics: PiContextAdapterDiagnostics = {\n preservedLateSystemCount: 0,\n droppedEmptyMessageIndexes: [],\n };\n\n let leadingSystemEnd = 0;\n while (leadingSystemEnd < messages.length && messages[leadingSystemEnd].role === 'system') {\n leadingSystemEnd++;\n }\n\n const systemPromptParts: string[] = [];\n for (let index = 0; index < leadingSystemEnd; index++) {\n const text = extractSystemText(messages[index], index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n systemPromptParts.push(text);\n }\n\n const lateSystemMessages: PiLateSystemMessage[] = [];\n const piMessages: Message[] = [];\n const toolNamesById = new Map();\n const imageBudget: ImageInputBudget = { decodedBytes: 0, imageCount: 0 };\n for (let index = leadingSystemEnd; index < messages.length; index++) {\n const message = messages[index];\n if (message.role === 'system') {\n const text = extractSystemText(message, index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n lateSystemMessages.push({\n sourceIndex: index,\n beforeMessageIndex: piMessages.length,\n text,\n });\n continue;\n }\n if (message.role === 'user') {\n const content = addNamePrefix(\n convertContent(message.content, index, true, imageBudget),\n message.name\n );\n if (!contentHasValue(content)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n const compactContent = compactTextBlocks(content);\n const userMessage: UserMessage = {\n role: 'user',\n content:\n compactContent.length === 1 && compactContent[0].type === 'text'\n ? compactContent[0].text\n : compactContent,\n timestamp: now(),\n };\n piMessages.push(userMessage);\n continue;\n }\n if (message.role === 'assistant') {\n const assistantMessage = convertAssistantMessage(\n message,\n index,\n now,\n toolNamesById,\n imageBudget\n );\n if (assistantMessage.content.length === 0) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n piMessages.push(assistantMessage);\n continue;\n }\n if (message.role === 'tool') {\n piMessages.push(\n convertToolResultMessage(message, index, now, toolNamesById, imageBudget)\n );\n continue;\n }\n throw new PiContextAdapterError(\n `第 ${index} 条消息包含未知角色`,\n 'unsupported-content',\n index\n );\n }\n diagnostics.droppedEmptyMessageIndexes.sort((left, right) => left - right);\n diagnostics.preservedLateSystemCount = lateSystemMessages.length;\n\n return {\n context: {\n ...(systemPromptParts.length > 0\n ? { systemPrompt: systemPromptParts.join('\\n\\n') }\n : {}),\n messages: piMessages,\n },\n lateSystemMessages,\n diagnostics,\n };\n}\n","import type { FetchFunction } from './pi_gateway';\nimport type { PiWireApi } from './provider_target';\n\n/** 尚待逐字段校验的协议 JSON 对象,用于非流式响应和本地合成的流事件。 */\ntype JsonObject = Record;\n\n/** 校验响应结构为非空、非数组对象。 */\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 拒绝不符合协议的非流式响应,不将可能包含提示词或凭证的响应正文写入错误。 */\nfunction invalidResponse(): never {\n // Do not include provider bodies: they can echo prompts, headers, or credentials.\n throw new Error('More source received an invalid non-streaming response');\n}\n\n/** 把单个协议事件编码为 SSE 文本,供 Pi 现有流解析器消费。 */\nfunction event(data: JsonObject): string {\n return `${typeof data.type === 'string' ? `event: ${data.type}\\n` : ''}data: ${JSON.stringify(data)}\\n\\n`;\n}\n\n/** 把 Chat Completions 完整响应转换为增量结束事件,补齐工具调用下标。 */\nfunction completionEvents(response: JsonObject): string {\n if (!Array.isArray(response.choices) || response.choices.length === 0) {\n return invalidResponse();\n }\n const choices = response.choices.map((choice: unknown, index) => {\n if (\n !isObject(choice) ||\n !isObject(choice.message) ||\n typeof choice.finish_reason !== 'string' ||\n !choice.finish_reason\n ) {\n return invalidResponse();\n }\n const delta = { ...choice.message };\n if (Array.isArray(delta.tool_calls)) {\n delta.tool_calls = delta.tool_calls.map((call: unknown, index) => {\n if (!isObject(call)) {\n return invalidResponse();\n }\n return { ...call, index };\n });\n }\n const { message: _message, ...rest } = choice;\n return { ...rest, index, delta };\n });\n return event({ ...response, object: 'chat.completion.chunk', choices }) + 'data: [DONE]\\n\\n';\n}\n\n/** 把 Responses 完整输出转换为输出项事件和终态事件,保留失败与截断状态。 */\nfunction responsesEvents(response: JsonObject): string {\n const status = response.status;\n if (!['completed', 'incomplete', 'failed', 'cancelled'].includes(String(status))) {\n return invalidResponse();\n }\n if (!Array.isArray(response.output)) {\n return invalidResponse();\n }\n return (\n response.output\n .map((item: unknown, output_index) => {\n if (!isObject(item)) {\n return invalidResponse();\n }\n return event({ type: 'response.output_item.done', output_index, item });\n })\n .join('') +\n event({\n type: `response.${status === 'cancelled' ? 'failed' : status}`,\n response,\n })\n );\n}\n\n/** 把 Anthropic 完整响应转换为消息和内容块事件,工具参数通过增量事件交给 Pi 解析。 */\nfunction anthropicEvents(response: JsonObject): string {\n if (!Array.isArray(response.content) || typeof response.stop_reason !== 'string') {\n return invalidResponse();\n }\n const usage = isObject(response.usage) ? response.usage : {};\n let result = event({\n type: 'message_start',\n message: { ...response, content: [], usage },\n });\n response.content.forEach((block: unknown, index) => {\n if (!isObject(block)) {\n return invalidResponse();\n }\n result += event({\n type: 'content_block_start',\n index,\n content_block: block.type === 'tool_use' ? { ...block, input: {} } : block,\n });\n // Pi finalizes tool arguments from the delta buffer, even when start has an input.\n if (block.type === 'tool_use') {\n result += event({\n type: 'content_block_delta',\n index,\n delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) },\n });\n }\n result += event({ type: 'content_block_stop', index });\n });\n return (\n result +\n event({\n type: 'message_delta',\n delta: {\n stop_reason: response.stop_reason,\n stop_sequence: response.stop_sequence,\n stop_details: response.stop_details,\n },\n usage,\n }) +\n event({ type: 'message_stop' })\n );\n}\n\n/** 按请求协议选择非流式响应转换器,拒绝错误对象和不支持的响应结构。 */\nfunction toEvents(api: PiWireApi, response: unknown): string {\n if (!isObject(response) || response.error) {\n return invalidResponse();\n }\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return completionEvents(response);\n case 'openai-responses':\n return responsesEvents(response);\n case 'anthropic-messages':\n return anthropicEvents(response);\n case 'google-generative-ai':\n if (!Array.isArray(response.candidates)) {\n return invalidResponse();\n }\n return event(response);\n default:\n return invalidResponse();\n }\n}\n\n/** 识别需要改写传输方式的生成接口,避免影响认证和模型列表等其他请求。 */\nfunction isGenerationUrl(api: PiWireApi, url: URL): boolean {\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return url.pathname.endsWith('/chat/completions');\n case 'openai-responses':\n return url.pathname.endsWith('/responses');\n case 'anthropic-messages':\n return url.pathname.endsWith('/messages');\n case 'google-generative-ai':\n return url.pathname.endsWith(':streamGenerateContent');\n default:\n return false;\n }\n}\n\n/**\n * 保留 Pi 的请求构造、认证和结果解析,只将支持的生成请求改为真正的非流式 HTTP。\n * 返回的 JSON 在本地转换为 Pi 可消费的事件流;Codex 的强制流式协议保持原样。\n */\nexport function createPiNonStreamingFetch(\n api: PiWireApi,\n fetch_impl?: FetchFunction\n): FetchFunction {\n return async (input, init) => {\n const send = fetch_impl ?? globalThis.fetch;\n const request = new Request(input, init);\n const url = new URL(request.url);\n if (request.method !== 'POST' || !isGenerationUrl(api, url)) {\n return send(input, init);\n }\n request.signal.throwIfAborted();\n const body: unknown = await request.json();\n if (!isObject(body)) {\n throw new Error('More source requires a JSON request body');\n }\n if (api === 'google-generative-ai') {\n url.pathname = url.pathname.replace(/:streamGenerateContent$/, ':generateContent');\n if (url.searchParams.get('alt') === 'sse') {\n url.searchParams.delete('alt');\n }\n } else {\n body.stream = false;\n delete body.stream_options;\n delete body.tool_stream;\n if (api === 'anthropic-messages' && Array.isArray(body.tools)) {\n body.tools.forEach((tool: unknown) => {\n if (isObject(tool)) {\n delete tool.eager_input_streaming;\n }\n });\n }\n }\n const headers = new Headers(request.headers);\n headers.set('accept', 'application/json');\n headers.delete('content-length');\n const response = await send(\n new Request(url, {\n ...init,\n method: request.method,\n headers,\n body: JSON.stringify(body),\n signal: request.signal,\n credentials: request.credentials,\n redirect: request.redirect,\n })\n );\n request.signal.throwIfAborted();\n // Keep provider HTTP errors intact for Pi's existing retry/error handling. Some gateways\n // ignore stream:false and still return SSE; their valid stream can be consumed as-is.\n if (!response.ok || response.headers.get('content-type')?.includes('text/event-stream')) {\n return response;\n }\n const data: unknown = await response.json();\n request.signal.throwIfAborted();\n const response_headers = new Headers(response.headers);\n response_headers.set('content-type', 'text/event-stream');\n response_headers.delete('content-length');\n response_headers.delete('content-encoding');\n return new Response(toEvents(api, data), {\n status: response.status,\n statusText: response.statusText,\n headers: response_headers,\n });\n };\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** 需要映射到服务商原生 JSON 输出配置的两种应答格式。 */\nexport type PiStructuredResponseFormat = '格式化输出' | '格式化输出(v4兼容)';\n\n/** MVU 的结构化输出 Schema,包含协议需要的名称及可选严格校验标记。 */\nexport type PiJsonSchema = {\n name: string;\n description?: string;\n value: Record;\n strict?: boolean;\n};\n\n/** 单次请求的原生载荷转换选项;消息角色由独立的 system 桥接层恢复。 */\nexport type PiPayloadTransformOptions = {\n api: Api;\n responseFormat?: PiStructuredResponseFormat;\n jsonSchema?: PiJsonSchema;\n customIncludeBody?: Record;\n customExcludeBody?: readonly string[];\n sampling?: {\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n };\n};\n\nconst PROTECTED_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'config',\n 'instructions',\n 'model',\n 'messages',\n 'input',\n 'contents',\n 'system',\n 'stream',\n 'tools',\n 'tool_choice',\n 'toolChoice',\n 'toolConfig',\n]);\n\nconst GOOGLE_PROTECTED_CONFIG_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'abortSignal',\n 'automaticFunctionCalling',\n 'httpOptions',\n 'responseJsonSchema',\n 'responseMimeType',\n 'responseSchema',\n 'systemInstruction',\n 'temperature',\n 'thinkingConfig',\n 'toolConfig',\n 'tools',\n 'topK',\n 'topP',\n]);\n\n/** 判断请求体或自定义配置是否为非空、非数组对象。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * 先删除再合并自定义字段,同时阻止修改受保护的请求字段。\n * 只浅拷贝原请求体以保留 AbortSignal 等浏览器对象,用户覆盖值单独深拷贝。\n */\nfunction applyCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n // Provider payloads may contain live browser objects (notably Google's AbortSignal).\n // A deep structuredClone turns those into plain objects and breaks cancellation. Only clone\n // the top-level payload here; user-supplied include values are cloned individually below.\n const result = { ...payload };\n for (const name of exclude) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot exclude protected field '${name}'`);\n }\n delete result[name];\n }\n for (const [name, value] of Object.entries(include)) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot override protected field '${name}'`);\n }\n result[name] = structuredClone(value);\n }\n return result;\n}\n\n/** 只接受 config.<字段> 形式的 Google 删除路径,避免歧义或删除整个生成配置。 */\nfunction googleConfigField(path: string): string {\n const match = /^config\\.([^.]+)$/.exec(path);\n if (!match) {\n throw new Error(\n `More source customExcludeBody for Google must use a direct 'config.' path, received '${path}'`\n );\n }\n return match[1];\n}\n\n/** 阻止自定义配置覆盖或删除由运行时管理的 Google 核心字段。 */\nfunction assertGoogleConfigFieldAllowed(field: string, operation: 'override' | 'exclude'): void {\n if (GOOGLE_PROTECTED_CONFIG_FIELDS.has(field)) {\n throw new Error(\n `More source custom body cannot ${operation} protected field 'config.${field}'`\n );\n }\n}\n\n/** 将 Google 自定义参数限制在 config 内,保留现有配置中的取消信号等对象。 */\nfunction applyGoogleCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n const existing_config = isPlainObject(payload.config) ? payload.config : {};\n const config = { ...existing_config };\n\n for (const path of exclude) {\n if (path === 'config' || PROTECTED_FIELDS.has(path)) {\n throw new Error(`More source custom body cannot exclude protected field '${path}'`);\n }\n const field = googleConfigField(path);\n assertGoogleConfigFieldAllowed(field, 'exclude');\n delete config[field];\n }\n\n for (const [name, value] of Object.entries(include)) {\n if (name !== 'config') {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(\n `More source custom body cannot override protected field '${name}'`\n );\n }\n throw new Error(\n `More source customIncludeBody for Google must place '${name}' inside the 'config' object`\n );\n }\n if (!isPlainObject(value)) {\n throw new Error(\n \"More source customIncludeBody for Google requires 'config' to be an object\"\n );\n }\n for (const [field, field_value] of Object.entries(value)) {\n assertGoogleConfigFieldAllowed(field, 'override');\n config[field] = structuredClone(field_value);\n }\n }\n\n return { ...payload, config };\n}\n\n/** 按协议写入原生 JSON Schema 或 JSON 对象输出配置,不支持的组合直接报错。 */\nfunction applyNativeStructuredOutput(\n payload: Record,\n api: Api,\n response_format: PiStructuredResponseFormat,\n schema: PiJsonSchema | undefined\n): Record {\n if (response_format === '格式化输出' && !schema) {\n throw new Error('More source structured output requires a JSON schema');\n }\n\n const is_json_schema = response_format === '格式化输出';\n if (api === 'openai-completions') {\n return {\n ...payload,\n response_format: is_json_schema\n ? {\n type: 'json_schema',\n json_schema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n if (api === 'openai-responses' || api === 'openai-codex-responses') {\n const existing_text = isPlainObject(payload.text) ? payload.text : {};\n return {\n ...payload,\n text: {\n ...existing_text,\n format: is_json_schema\n ? {\n type: 'json_schema',\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n }\n : { type: 'json_object' },\n },\n };\n }\n\n if (api === 'anthropic-messages') {\n if (!is_json_schema) {\n throw new Error(`More source API '${api}' does not support native JSON-object output`);\n }\n const existing_output_config = isPlainObject(payload.output_config)\n ? payload.output_config\n : {};\n return {\n ...payload,\n output_config: {\n ...existing_output_config,\n format: {\n type: 'json_schema',\n schema: schema!.value,\n },\n },\n };\n }\n\n if (api === 'google-generative-ai') {\n if (!is_json_schema) {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n },\n };\n }\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n responseJsonSchema: schema!.value,\n },\n };\n }\n\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n responseFormat: is_json_schema\n ? {\n type: 'json_schema',\n jsonSchema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schemaDefinition: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n throw new Error(`More source API '${api}' does not support native structured output`);\n}\n\n/** 将统一采样选项转换为各协议的字段名称和嵌套位置。 */\nfunction applyApiSampling(\n payload: Record,\n api: Api,\n sampling: PiPayloadTransformOptions['sampling']\n): Record {\n if (!sampling) {\n return payload;\n }\n if (api === 'openai-completions') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequency_penalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presence_penalty: sampling.presencePenalty }),\n };\n }\n if (api === 'openai-responses') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n };\n }\n if (api === 'anthropic-messages') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { top_k: sampling.topK }),\n };\n }\n if (api === 'google-generative-ai') {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { topK: sampling.topK }),\n },\n };\n }\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequencyPenalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presencePenalty: sampling.presencePenalty }),\n };\n }\n return payload;\n}\n\n/** 统一应用请求覆盖、采样和原生应答格式,并保留协议核心字段的控制权。 */\nexport function transformPiPayload(payload: unknown, options: PiPayloadTransformOptions): unknown {\n if (!isPlainObject(payload)) {\n throw new Error('More source provider payload must be an object');\n }\n let result = applyApiSampling(payload, options.api, options.sampling);\n result =\n options.api === 'google-generative-ai'\n ? applyGoogleCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n )\n : applyCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n );\n if (options.responseFormat) {\n result = applyNativeStructuredOutput(\n result,\n options.api,\n options.responseFormat,\n options.jsonSchema\n );\n }\n return result;\n}\n\n/** 将本轮转换选项绑定为 Pi 的 onPayload 回调。 */\nexport function createPiPayloadTransform(options: PiPayloadTransformOptions) {\n return (payload: unknown) => transformPiPayload(payload, options);\n}\n","import type { AssistantMessage, Tool, ToolCall } from '@earendil-works/pi-ai';\n\n/** 模型输出无法交回原有变量更新流程的原因,区分取消、截断和无效结果。 */\nexport type PiResultAdapterErrorCode =\n | 'aborted'\n | 'deferred'\n | 'empty-response'\n | 'invalid-tool-call'\n | 'length'\n | 'network'\n | 'provider-error';\n\nexport class PiResultAdapterError extends Error {\n /** 保留结果转换错误码,供运行时决定取消、重试及界面提示。 */\n constructor(\n message: string,\n readonly code: PiResultAdapterErrorCode\n ) {\n super(message);\n this.name = 'PiResultAdapterError';\n }\n}\n\n/** 将酒馆函数工具转为 Pi Tool 时附加的协议选项。 */\nexport type PiToolDefinitionOptions = {\n /** 服务商端的 Schema 或语法约束配置;false 显式关闭约束采样。 */\n constrainedSampling?: Tool['constrainedSampling'];\n};\n\n/** 仅接受普通或空原型对象,作为工具参数与 Schema 的结构边界。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 复制工具 Schema,无法复制时转换为工具定义错误,避免共享结构被修改。 */\nfunction cloneSchema(schema: Record): Record {\n try {\n return structuredClone(schema);\n } catch (cause) {\n throw new PiResultAdapterError(\n `工具参数 schema 无法复制:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n}\n\n/** 校验函数工具名称和对象 Schema,再转换为 Pi 工具定义及约束采样配置。 */\nexport function toPiToolDefinition(\n definition: ToolDefinition,\n options: PiToolDefinitionOptions = {}\n): Tool {\n if (definition?.type !== 'function' || !definition.function?.name?.trim()) {\n throw new PiResultAdapterError('工具定义缺少有效的 function name', 'invalid-tool-call');\n }\n\n const parameters = definition.function.parameters ?? {\n type: 'object',\n properties: {},\n };\n if (!isPlainObject(parameters) || parameters.type !== 'object') {\n throw new PiResultAdapterError(\n '工具参数 schema 的根节点必须是 object',\n 'invalid-tool-call'\n );\n }\n\n return {\n name: definition.function.name,\n description: definition.function.description ?? '',\n parameters: cloneSchema(parameters) as Tool['parameters'],\n ...(options.constrainedSampling === undefined\n ? {}\n : { constrainedSampling: options.constrainedSampling }),\n };\n}\n\nexport const toPiTool = toPiToolDefinition;\n\n/** 把 Pi 工具调用转换为酒馆结果结构,序列化参数并保留思考签名。 */\nfunction normalizeToolCall(call: ToolCall): GenerateToolCallResult['tool_calls'][number] {\n if (!call.id?.trim() || !call.name?.trim() || !isPlainObject(call.arguments)) {\n throw new PiResultAdapterError('更多来源返回了无效的工具调用', 'invalid-tool-call');\n }\n\n let argumentsValue: string;\n try {\n argumentsValue = JSON.stringify(call.arguments);\n } catch (cause) {\n throw new PiResultAdapterError(\n `更多来源工具参数无法序列化:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: argumentsValue,\n },\n ...(call.thoughtSignature ? { thought_signature: call.thoughtSignature } : {}),\n };\n}\n\n/** 只允许完整文本或工具调用结果继续转换,分别拒绝截断、取消、延迟和服务商失败。 */\nfunction assertSuccessfulStopReason(message: AssistantMessage): void {\n switch (message.stopReason) {\n case 'stop':\n case 'toolUse':\n return;\n case 'length':\n throw new PiResultAdapterError('更多来源回复因达到长度上限而被截断', 'length');\n case 'aborted':\n throw new PiResultAdapterError('更多来源请求已取消', 'aborted');\n case 'error': {\n // Provider errorMessage values are untrusted response data. They have been observed to\n // contain echoed Authorization headers/API keys, so retain only a coarse classification.\n const isNetworkError =\n typeof message.errorMessage === 'string' &&\n /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message.errorMessage\n );\n throw new PiResultAdapterError(\n isNetworkError\n ? 'The browser could not complete the More source request.'\n : 'More source request failed.',\n isNetworkError ? 'network' : 'provider-error'\n );\n }\n case 'pending':\n case 'deferred':\n throw new PiResultAdapterError(\n '更多来源回复尚未完成,当前不支持 deferred 结果',\n 'deferred'\n );\n default: {\n const exhaustive: never = message.stopReason;\n void exhaustive;\n throw new PiResultAdapterError(\n 'More source provider returned an unknown stop reason.',\n 'provider-error'\n );\n }\n }\n}\n\n/**\n * 提取助手文本及工具调用并返回 MVU 可消费的结果。\n * 空响应、仅有思考或工具结束却没有调用都视为协议失败,不写入聊天上下文。\n */\nexport function fromPiAssistantMessage(message: AssistantMessage): string | GenerateToolCallResult {\n assertSuccessfulStopReason(message);\n\n const text = message.content\n .filter(content => content.type === 'text')\n .map(content => content.text)\n .join('');\n const toolCalls = message.content\n .filter((content): content is ToolCall => content.type === 'toolCall')\n .map(normalizeToolCall);\n\n if (message.stopReason === 'toolUse' && toolCalls.length === 0) {\n throw new PiResultAdapterError(\n '更多来源以 toolUse 结束,但没有返回工具调用',\n 'invalid-tool-call'\n );\n }\n if (toolCalls.length > 0) {\n return {\n content: text,\n tool_calls: toolCalls,\n };\n }\n if (text.length === 0) {\n const hasThinking = message.content.some(content => content.type === 'thinking');\n throw new PiResultAdapterError(\n hasThinking ? '更多来源回复仅包含 thinking,没有业务内容' : '更多来源返回了空回复',\n 'empty-response'\n );\n }\n return text;\n}\n","import type { Context, ImageContent, Message } from '@earendil-works/pi-ai';\nimport {\n getPiImageMetadata,\n PI_IMAGE_INPUT_LIMITS,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 发送前的保守预算结果,用于解释输入是否超出模型窗口,不代表计费 token 数。 */\nexport type PiTokenPreflightResult = {\n estimatedInputTokens: number;\n /** 额外预留的安全余量,不包含回复 token。 */\n reservedTokens: number;\n /** 上下文窗口扣除回复额度及安全余量后允许的输入上限。 */\n maxInputTokens: number;\n};\n\nconst MIN_IMAGE_TOKEN_ESTIMATE = 1024;\nconst ESTIMATED_IMAGE_BYTES_PER_TOKEN = 64;\n// Common multimodal APIs estimate image tokens from tiled/pixel area. Using a\n// lower pixels-per-token ratio leaves deliberate cross-provider headroom.\nconst ESTIMATED_IMAGE_PIXELS_PER_TOKEN = 500;\n\n/**\n * 按 UTF-8 字节保守估算文本 token,兼顾中文与非基本平面字符。\n * 这是发送前的预算保护,不用于精确计费。\n */\nfunction estimateTextTokens(value: string): number {\n if (!value) {\n return 0;\n }\n // This is a deliberately conservative boundary guard, not billing-grade\n // tokenization. UTF-8 bytes avoid undercounting CJK and astral characters\n // while ASCII/JSON remains cheaper than non-ASCII text.\n let ascii_bytes = 0;\n let non_ascii_bytes = 0;\n for (const character of value) {\n const code_point = character.codePointAt(0)!;\n if (code_point <= 0x7f) {\n ascii_bytes += 1;\n } else if (code_point <= 0x7ff) {\n non_ascii_bytes += 2;\n } else if (code_point <= 0xffff) {\n non_ascii_bytes += 3;\n } else {\n non_ascii_bytes += 4;\n }\n }\n return Math.ceil(ascii_bytes / 3 + non_ascii_bytes / 2);\n}\n\n/** 根据 Base64 长度与填充估算解码字节数,避免为预算检查复制或解码整张图片。 */\nfunction estimateDecodedBase64Bytes(data: string): number {\n if (!data) {\n return 0;\n }\n // Adapter-produced blocks are canonical base64. Counting whitespace in an\n // external block overestimates safely and avoids an image-sized copy.\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding);\n}\n\n/** 优先使用上下文转换时记录的图片大小,外部内容块再按 Base64 长度估算。 */\nfunction getEstimatedImageBytes(image: ImageContent): number {\n return getPiImageMetadata(image)?.decodedBytes ?? estimateDecodedBase64Bytes(image.data);\n}\n\n/** 取图片最小预算、字节估算和像素面积估算中的最大值,保留跨服务商余量。 */\nfunction estimateImageTokens(image: ImageContent): number {\n const metadata = getPiImageMetadata(image);\n const estimated_bytes = getEstimatedImageBytes(image);\n const dimensions = metadata?.dimensions;\n\n let pixel_tokens = 0;\n if (\n dimensions !== undefined &&\n Number.isSafeInteger(dimensions.width) &&\n dimensions.width > 0 &&\n Number.isSafeInteger(dimensions.height) &&\n dimensions.height > 0\n ) {\n pixel_tokens = Math.ceil(\n (dimensions.width * dimensions.height) / ESTIMATED_IMAGE_PIXELS_PER_TOKEN\n );\n }\n\n return Math.max(\n MIN_IMAGE_TOKEN_ESTIMATE,\n Math.ceil(estimated_bytes / ESTIMATED_IMAGE_BYTES_PER_TOKEN),\n pixel_tokens\n );\n}\n\n/** 检查单图、总图片数量和累计解码大小,即使外部绕过上下文转换也执行预算限制。 */\nfunction assertPiImageInputBudget(context: Context): void {\n let image_count = 0;\n let decoded_bytes = 0;\n /** 累计单个图片块并立即检查各项图片限制,超限时停止遍历。 */\n const countImage = (image: ImageContent): void => {\n const image_bytes = getEstimatedImageBytes(image);\n if (image_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new Error('More source image exceeds the per-image input limit');\n }\n image_count += 1;\n decoded_bytes += image_bytes;\n if (image_count > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new Error('More source image count exceeds the per-context input limit');\n }\n if (decoded_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new Error('More source image total exceeds the per-context input limit');\n }\n };\n\n for (const message of context.messages) {\n if (message.role === 'user' && Array.isArray(message.content)) {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n } else if (message.role === 'toolResult') {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n }\n }\n}\n\n/** 按消息角色估算文本、思考、工具参数和图片,并计入消息结构开销。 */\nfunction estimateMessageTokens(message: Message): number {\n let tokens = 6;\n if (message.role === 'system') {\n tokens += estimateTextTokens(\n typeof message.content === 'string'\n ? message.content\n : message.content.map(block => block.text).join('')\n );\n for (const value of [message.sections, message.toolsAdded, message.toolsRemoved]) {\n if (value) tokens += estimateTextTokens(JSON.stringify(value));\n }\n return tokens;\n }\n if (message.role === 'user') {\n if (typeof message.content === 'string') {\n return tokens + estimateTextTokens(message.content);\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens;\n }\n if (message.role === 'assistant') {\n for (const block of message.content) {\n if (block.type === 'text') {\n tokens += estimateTextTokens(block.text);\n } else if (block.type === 'thinking') {\n tokens += estimateTextTokens(block.thinking);\n } else {\n tokens +=\n estimateTextTokens(block.name) +\n estimateTextTokens(JSON.stringify(block.arguments));\n }\n }\n return tokens;\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens + estimateTextTokens(message.toolName);\n}\n\n/** 汇总系统提示词、历史消息和工具定义的输入 token 估算。 */\nexport function estimatePiContextTokens(\n context: Context,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): number {\n let tokens = estimateTextTokens(context.systemPrompt ?? '');\n for (const message of context.messages) {\n tokens += estimateMessageTokens(message);\n }\n if (context.tools?.length) {\n tokens += estimateTextTokens(JSON.stringify(context.tools));\n }\n // 后置 system 尚未写入 Pi Context,但最终请求会恢复其文本和独立消息开销。\n for (const message of lateSystemMessages) {\n tokens += estimateTextTokens(message.text) + 8;\n }\n return Math.max(1, tokens);\n}\n\n/**\n * 预留回复 token 和安全余量后检查完整输入预算,不在此截断提示词。\n * 同时校验图片限制;超限错误携带估算明细,供界面解释原因。\n */\nexport function assertPiTokenBudget(\n context: Context,\n context_window: number,\n max_tokens: number,\n reserve_ratio = 0.07,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): PiTokenPreflightResult {\n if (!Number.isInteger(context_window) || context_window <= 0) {\n throw new Error('More source contextWindow must be a positive integer');\n }\n if (!Number.isInteger(max_tokens) || max_tokens <= 0) {\n throw new Error('More source maxTokens must be a positive integer');\n }\n if (max_tokens > context_window) {\n throw new Error('More source maxTokens must not exceed contextWindow');\n }\n assertPiImageInputBudget(context);\n\n const normalized_reserve_ratio = Math.min(0.1, Math.max(0.05, reserve_ratio));\n const reserved_tokens = Math.ceil(context_window * normalized_reserve_ratio);\n const max_input_tokens = context_window - max_tokens - reserved_tokens;\n const estimated_input_tokens = estimatePiContextTokens(context, lateSystemMessages);\n if (max_input_tokens <= 0 || estimated_input_tokens > max_input_tokens) {\n throw new Error(\n `More source prompt is too long: estimated ${estimated_input_tokens} input tokens, ` +\n `limit ${Math.max(0, max_input_tokens)} after reserving ${max_tokens} reply tokens ` +\n `and ${reserved_tokens} safety tokens`\n );\n }\n\n return {\n estimatedInputTokens: estimated_input_tokens,\n reservedTokens: reserved_tokens,\n maxInputTokens: max_input_tokens,\n };\n}\n","import type { Api, Context, Message, TextContent } from './pi_gateway';\nimport {\n PiContextAdapterError,\n type PiContextAdapterResult,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 一次请求的 system 恢复桥接;context 仅供 Pi 构造请求,restore 必须用于 onPayload。 */\nexport interface PiSystemMessageBridge {\n context: Context;\n restore: (payload: unknown) => unknown;\n /** Pi 会把 hook 异常转为普通服务商错误,保留原始分类供运行时取回。 */\n readonly failure: PiContextAdapterError | undefined;\n assertRestored: () => void;\n}\n\n/** 同一原始位置的一组 system,锚定到下一条实际消息,不额外伪造对话回合。 */\ntype SystemAnchor = {\n marker: string;\n messages: readonly PiLateSystemMessage[];\n};\n\n/** 对服务商 JSON 节点移除锚点后的结果;remove 仅用于锚点独占的临时文本块。 */\ntype CleanedNode = { value: unknown; anchors: SystemAnchor[]; remove?: boolean };\n\nlet nextAnchorNamespace = 0;\n\n/** 选用原始内容中不存在的请求级标记,防止用户文本被误识别为内部锚点。 */\nfunction containsMarker(value: unknown, prefix: string): boolean {\n if (typeof value === 'string') return value.includes(prefix);\n if (Array.isArray(value)) return value.some(item => containsMarker(item, prefix));\n if (value && typeof value === 'object') {\n return Object.values(value).some(item => containsMarker(item, prefix));\n }\n return false;\n}\n\n/** 在已有消息的首个文本块前加锚点;保留消息角色,避免打断 Pi 的工具调用关联。 */\nfunction anchorMessage(message: Message, marker: string, api: Api): Message {\n if (typeof message.content === 'string') {\n return { ...message, role: 'user', content: marker + message.content };\n }\n const content = [...message.content];\n const index = content.findIndex(block => block.type === 'text');\n let fallback = '';\n if (\n message.role === 'toolResult' &&\n !message.content\n .filter((block): block is TextContent => block.type === 'text')\n .map(block => block.text)\n .join('\\n')\n ) {\n const hasImages = message.content.some(block => block.type === 'image');\n // 锚点不能抑制 SDK 原本为无文本工具结果生成的占位说明。\n if (api === 'openai-completions' || api === 'mistral-conversations') {\n fallback = hasImages ? '(see attached image)' : '(no tool output)';\n } else if (!hasImages && ['openai-responses', 'openai-codex-responses'].includes(api)) {\n fallback = '(no tool output)';\n }\n }\n if (index === -1) {\n content.unshift({ type: 'text', text: marker + fallback });\n } else {\n const block = content[index] as TextContent;\n content[index] = { ...block, text: marker + block.text + fallback };\n }\n return { ...message, content } as Message;\n}\n\n/** 按实际协议和消息位置自动选择角色,不暴露额外的渠道配置。 */\nfunction systemMessageRole(\n context: Context,\n api: Api,\n message: PiLateSystemMessage\n): 'system' | 'user' {\n if (api === 'google-generative-ai') return 'user';\n if (api === 'anthropic-messages') {\n const previous = context.messages[message.beforeMessageIndex - 1];\n const next = context.messages[message.beforeMessageIndex];\n // toolResult 在 Anthropic 协议里是携带工具结果的 user 回合。\n return (previous?.role === 'user' || previous?.role === 'toolResult') &&\n (!next || next.role === 'assistant')\n ? 'system'\n : 'user';\n }\n return 'system';\n}\n\n/**\n * 保留普通消息角色,在服务商完成拆分/合并之后按内容锚点恢复 system;不符合协议约束时转为 user。\n * 锚点只进入待转换的文本,发送前必须恰好移除一次;不修改捕获输入、共享上下文或全局 fetch。\n */\nexport function createPiSystemMessageBridge(\n adapted: PiContextAdapterResult,\n api: Api\n): PiSystemMessageBridge {\n const { context, lateSystemMessages } = adapted;\n if (!lateSystemMessages.length) {\n return {\n context,\n restore: payload => payload,\n failure: undefined,\n assertRestored: () => {},\n };\n }\n\n let prefix: string;\n do {\n prefix = `__mvu_pi_system_anchor_${++nextAnchorNamespace}_`;\n } while (containsMarker(adapted, prefix));\n\n const groups = new Map();\n for (const message of lateSystemMessages) {\n const group = groups.get(message.beforeMessageIndex) ?? [];\n group.push(message);\n groups.set(message.beforeMessageIndex, group);\n }\n const anchors: SystemAnchor[] = [];\n const messages = context.messages.map((message, index) => {\n const group = groups.get(index);\n if (!group) return message;\n const anchor = { marker: `${prefix}${index}__`, messages: group };\n anchors.push(anchor);\n return anchorMessage(message, anchor.marker, api);\n });\n const tail = groups.get(context.messages.length) ?? [];\n const field =\n api === 'google-generative-ai'\n ? 'contents'\n : api === 'openai-responses' || api === 'openai-codex-responses'\n ? 'input'\n : 'messages';\n const hasUserFallback = lateSystemMessages.some(\n message => systemMessageRole(context, api, message) === 'user'\n );\n let failure: PiContextAdapterError | undefined;\n let applied = false;\n const restoredPayloads = new WeakSet();\n\n const mismatch = () =>\n new PiContextAdapterError(\n 'More source could not restore intermediate system messages in the provider payload.',\n 'system-payload-mismatch',\n lateSystemMessages[0].sourceIndex\n );\n const nativeMessages = (group: readonly PiLateSystemMessage[]) =>\n group.map(message =>\n api === 'google-generative-ai'\n ? { role: 'user', parts: [{ text: message.text }] }\n : { role: systemMessageRole(context, api, message), content: message.text }\n );\n\n /** 工具结果可能被 SDK 合并进同一 user;按工具结果块拆出插入边界,避免把指令提前到整组结果前。 */\n const splitToolResultGroups = (items: unknown[]): unknown[] =>\n items.flatMap(item => {\n if (!item || typeof item !== 'object' || !hasUserFallback) return [item];\n const message = item as Record;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const blocks = message[key];\n if (message.role !== 'user' || !Array.isArray(blocks)) return [item];\n const result: unknown[] = [];\n let current: unknown[] = [];\n for (const block of blocks) {\n const isToolResult =\n block &&\n typeof block === 'object' &&\n (block.type === 'tool_result' || block.functionResponse);\n if (isToolResult && current.length && containsMarker(block, prefix)) {\n result.push({ ...message, [key]: current });\n current = [];\n }\n current.push(block);\n }\n result.push({ ...message, [key]: current });\n return result;\n });\n\n /** 与酒馆兼容处理一致,user 回退后合并相邻 user,同时保留内容块、图片及工具结果的顺序。 */\n const mergeFallbackUsers = (items: unknown[]): unknown[] => {\n if (!hasUserFallback || (api !== 'anthropic-messages' && api !== 'google-generative-ai'))\n return items;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const asBlocks = (value: unknown) =>\n typeof value === 'string' ? [{ type: 'text', text: value }] : value;\n const merged: unknown[] = [];\n for (const item of items) {\n const current = item as Record;\n const previous = merged.at(-1) as Record | undefined;\n if (current?.role === 'user' && previous?.role === 'user') {\n const before = asBlocks(previous[key]);\n const after = asBlocks(current[key]);\n if (Array.isArray(before) && Array.isArray(after)) {\n merged[merged.length - 1] = { ...previous, [key]: [...before, ...after] };\n continue;\n }\n }\n merged.push(item);\n }\n return merged;\n };\n\n /** 只遍历原生消息字段;完整保留工具 ID、图片、缓存标记和其他服务商元数据。 */\n const clean = (value: unknown): CleanedNode => {\n if (typeof value === 'string') {\n if (!value.includes(prefix)) return { value, anchors: [] };\n const found: SystemAnchor[] = [];\n let text = value;\n for (const anchor of anchors) {\n const parts = text.split(anchor.marker);\n for (let index = 1; index < parts.length; index++) found.push(anchor);\n text = parts.join('');\n }\n return { value: text, anchors: found };\n }\n if (Array.isArray(value)) {\n const children = value.map(clean);\n return {\n value: children.filter(child => !child.remove).map(child => child.value),\n anchors: children.flatMap(child => child.anchors),\n };\n }\n if (!value || typeof value !== 'object') return { value, anchors: [] };\n const entries = Object.entries(value).map(([key, item]) => [key, clean(item)] as const);\n const found = entries.flatMap(([, child]) => child.anchors);\n if (!found.length) return { value, anchors: [] };\n const result = Object.fromEntries(entries.map(([key, child]) => [key, child.value]));\n const emptyAnchorBlock =\n result.text === '' &&\n (api === 'google-generative-ai' ||\n (typeof result.type === 'string' &&\n ['text', 'input_text', 'output_text'].includes(result.type)));\n // Responses 会把仅含工具调用的 assistant 上新增的锚点文本拆成单独消息,恢复后移除它。\n const emptyAnchorMessage =\n result.type === 'message' &&\n Array.isArray(result.content) &&\n result.content.length === 0;\n // Mistral 的纯工具助手消息省略 content,不能把临时定位块变成空内容数组。\n if (\n api === 'mistral-conversations' &&\n result.role === 'assistant' &&\n Array.isArray(result.content) &&\n !result.content.length\n ) {\n delete result.content;\n }\n return { value: result, anchors: found, remove: emptyAnchorBlock || emptyAnchorMessage };\n };\n\n return {\n context: { ...context, messages },\n get failure() {\n return failure;\n },\n restore(payload) {\n try {\n if (!payload || typeof payload !== 'object') throw mismatch();\n if (restoredPayloads.has(payload)) return payload;\n const original = payload as Record;\n const items = original[field];\n if (!Array.isArray(items)) throw mismatch();\n const seen = new Set();\n const restored: unknown[] = [];\n for (const item of splitToolResultGroups(items)) {\n const result = clean(item);\n if (result.anchors.length > 1) throw mismatch();\n const anchor = result.anchors[0];\n if (anchor) {\n if (seen.has(anchor) || anchors[seen.size] !== anchor) throw mismatch();\n seen.add(anchor);\n restored.push(...nativeMessages(anchor.messages));\n }\n if (!result.remove) restored.push(result.value);\n }\n if (seen.size !== anchors.length) throw mismatch();\n restored.push(...nativeMessages(tail));\n const result = { ...original, [field]: mergeFallbackUsers(restored) };\n if (containsMarker(result, prefix)) throw mismatch();\n restoredPayloads.add(result);\n applied = true;\n return result;\n } catch (error) {\n failure = error instanceof PiContextAdapterError ? error : mismatch();\n throw failure;\n }\n },\n assertRestored() {\n if (failure) throw failure;\n if (!applied) throw mismatch();\n },\n };\n}\n","import {\n normalizePiThinkingLevel,\n isPiThinkingEnabled,\n type PiThinkingLevel,\n} from './thinking_setting';\nimport { preparePiThinkingModel, resolvePiThinkingLevel } from './thinking';\nimport type { MvuSettings } from '@/store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n parsePiCustomExcludeBody,\n parsePiCustomHeaders,\n parsePiCustomIncludeBody,\n} from './config_parser';\nimport { PiContextAdapterError, toPiContext } from './context_adapter';\nimport { PiRequestAbortedError, registerPiRequestController } from './controller_registry';\nimport { getPiCredentialStore } from './credential_store';\nimport { assertGoogleProxyAdapterCompatible } from './google_proxy_adapter';\nimport {\n isPiDefaultProviderEndpoint,\n PiModelResolutionError,\n resolvePiModelFromExtraModelSettings,\n type ResolvedPiModel,\n} from './model_resolver';\nimport { getBrowserOAuthAuth } from './oauth';\nimport { createPiNonStreamingFetch } from './non_streaming_fetch';\nimport { isPiStreamingRequired } from './provider_target';\nimport { createPiPayloadTransform, transformPiPayload, type PiJsonSchema } from './payload';\nimport {\n createModels,\n createProvider,\n type Api,\n type ApiStreamOptions,\n type AssistantMessageEvent,\n type AuthContext,\n type CredentialStore,\n type FetchFunction,\n type SimpleStreamOptions,\n type ProviderAuth,\n type ProviderHeaders,\n type Tool,\n} from './pi_gateway';\nimport {\n createPiApiImplementations,\n resolvePiCapabilities,\n shouldUsePiCorsProxy,\n type PiApiCapabilities,\n type PiWireApi,\n} from './provider_registry';\nimport { fromPiAssistantMessage, PiResultAdapterError, toPiToolDefinition } from './result_adapter';\nimport { assertPiTokenBudget } from './token_preflight';\nimport { resolvePiToolChoice, type MvuToolChoice } from './tool_choice';\nimport { createPiSystemMessageBridge, type PiSystemMessageBridge } from './system_messages';\nimport {\n assertSillyTavernProxyAvailable,\n createSillyTavernProxyFetch,\n getSillyTavernProxyStatus,\n PiProxyUnavailableError,\n} from './sillytavern_proxy';\n\ninstallPiAbortSignalPolyfills();\n\nexport const PI_RUNTIME_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\n/** 额外解析允许使用的应答格式,供预检和原生载荷转换共同约束。 */\nexport type PiRuntimeResponseFormat = (typeof PI_RUNTIME_RESPONSE_FORMATS)[number];\n/** 一次额外模型请求读取的完整设置结构。 */\nexport type PiExtraModelSettings = MvuSettings['额外模型解析配置'];\n\n/** 运行时失败分类;配合 retryable 决定请求策略与界面提示。 */\nexport type PiRuntimeErrorCode =\n | 'invalid_configuration'\n | 'invalid_prompt'\n | 'missing_oauth_credential'\n | 'request_already_active'\n | 'unsupported_capability'\n | 'unsupported_image_input'\n | 'token_budget'\n | 'proxy_unavailable'\n | 'network'\n | 'provider'\n | 'protocol';\n\nexport class PiRuntimeError extends Error {\n /** 为运行时错误保存稳定类别和重试标记,供额外模型策略统一决策。 */\n constructor(\n readonly code: PiRuntimeErrorCode,\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'PiRuntimeError';\n }\n}\n\n/** 为串行和并发策略提供统一的不可重试错误判断。 */\nexport function isNonRetryablePiRuntimeError(error: unknown): boolean {\n return (\n (error instanceof PiRuntimeError && !error.retryable) ||\n error instanceof PiModelResolutionError ||\n error instanceof PiContextAdapterError\n );\n}\n\n/** 已通过服务商能力校验的采样选项,不含独立传入 SDK 的 temperature。 */\nexport type PiRuntimeSampling = Readonly<{\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n}>;\n\n/** 在重试批次开始前进行静态预检所需的设置、工具和可注入依赖。 */\nexport interface AssertPiRuntimeConfigurationInput {\n /** The whole persisted `额外模型解析配置` object. */\n settings: unknown;\n /** Defaults to `settings.应答格式`. */\n responseFormat?: unknown;\n /** Required by `格式化输出`; ignored by non-structured response modes. */\n jsonSchema?: PiJsonSchema;\n /** Required by `工具调用`; the MVU ToolDefinition constants can be passed directly. */\n tools?: readonly ToolDefinition[];\n /** Defaults to `required` in tool mode. */\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n signal?: AbortSignal;\n}\n\n/**\n * 重试前解析并冻结的连接、认证、能力与载荷配置。\n * 不包含提示词或请求级 system 锚点,可在同批次的每次尝试中复用。\n */\nexport interface PiRuntimePreflight {\n readonly resolution: ResolvedPiModel;\n readonly responseFormat: PiRuntimeResponseFormat;\n readonly capabilities: Readonly;\n readonly headers?: Readonly;\n readonly customIncludeBody?: Readonly>;\n readonly customExcludeBody?: readonly string[];\n readonly temperature?: number;\n readonly sampling: PiRuntimeSampling;\n readonly tools?: readonly Tool[];\n readonly toolChoice?: unknown;\n readonly jsonSchema?: PiJsonSchema;\n readonly credentialStore: CredentialStore;\n readonly fetch?: FetchFunction;\n readonly useCorsProxy: boolean;\n readonly streaming: boolean;\n readonly thinkingLevel: PiThinkingLevel;\n}\n\n/** 可选的 Pi 进度监听,运行时不会向此回调转发未经处理的错误事件。 */\nexport type PiRuntimeProgressCallback = (event: AssistantMessageEvent) => void | Promise;\n\n/** 执行单次请求的输入:预检快照、捕获消息、取消编号及可选进度监听。 */\nexport interface RunPiRequestInput {\n /** Supply this when configuration was validated once outside the retry loop. */\n preflight?: PiRuntimePreflight;\n /** Required only when `preflight` is omitted. */\n settings?: unknown;\n responseFormat?: unknown;\n jsonSchema?: PiJsonSchema;\n tools?: readonly ToolDefinition[];\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n messages: readonly SillyTavern.SendingMessage[];\n generationId: string;\n signal?: AbortSignal;\n onProgress?: PiRuntimeProgressCallback;\n}\n\n/** 设置根对象完成形状检查后、各字段尚未解析时的中间结构。 */\ntype ExtraModelSettingsRecord = Record & {\n pi?: unknown;\n};\n\nconst BROWSER_AUTH_CONTEXT: AuthContext = Object.freeze({\n env: async () => undefined,\n fileExists: async () => false,\n});\n\n/** 确认设置是非空、非数组对象,再读取请求字段。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 校验额外模型设置的根结构,结构错误在构造请求前失败。 */\nfunction requireSettings(value: unknown): ExtraModelSettingsRecord {\n if (!isPlainObject(value)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source extra-model settings must be an object'\n );\n }\n return value;\n}\n\n/** 优先采用本次显式应答格式,否则读取设置,并拒绝未知格式。 */\nfunction resolveResponseFormat(\n settings: ExtraModelSettingsRecord,\n override: unknown\n): PiRuntimeResponseFormat {\n const value = override ?? settings['应答格式'];\n if (\n typeof value !== 'string' ||\n !PI_RUNTIME_RESPONSE_FORMATS.some(candidate => candidate === value)\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source response format is missing or unsupported'\n );\n }\n return value as PiRuntimeResponseFormat;\n}\n\n/** 读取可选字符串配置;缺失返回空字符串,类型错误不能隐式转换。 */\nfunction optionalStringField(source: Record, name: string): string {\n const value = source[name];\n if (value === undefined) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new PiRuntimeError('invalid_configuration', `More source ${name} must be a string`);\n }\n return value;\n}\n\n/** 校验可选数值的范围、有限性及整数要求,缺失字段保持未设置。 */\nfunction optionalNumberField(\n source: Record,\n name: string,\n minimum: number,\n maximum: number,\n integer = false\n): number | undefined {\n const value = source[name];\n if (value === undefined) {\n return undefined;\n }\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n value < minimum ||\n value > maximum ||\n (integer && !Number.isInteger(value))\n ) {\n const kind = integer ? 'integer' : 'number';\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source ${name} must be a ${kind} between ${minimum} and ${maximum}`\n );\n }\n return value;\n}\n\n/** 仅解析当前模型支持的采样字段,并处理协议或模型特有的组合限制。 */\nfunction resolveSampling(\n settings: ExtraModelSettingsRecord,\n capabilities: Readonly,\n resolution: ResolvedPiModel\n): {\n temperature?: number;\n sampling: PiRuntimeSampling;\n} {\n let temperature = capabilities.temperature\n ? optionalNumberField(\n settings,\n '温度',\n capabilities.temperatureRange[0],\n capabilities.temperatureRange[1]\n )\n : undefined;\n let topP = capabilities.sampling.topP\n ? optionalNumberField(settings, 'top_p', 0, 1)\n : undefined;\n const topK = capabilities.sampling.topK\n ? optionalNumberField(settings, 'top_k', 0, 500, true)\n : undefined;\n const frequencyPenalty = capabilities.sampling.frequencyPenalty\n ? optionalNumberField(settings, '频率惩罚', -2, 2)\n : undefined;\n const presencePenalty = capabilities.sampling.presencePenalty\n ? optionalNumberField(settings, '存在惩罚', -2, 2)\n : undefined;\n if (\n resolution.definition.key === 'anthropic' &&\n isPiDefaultProviderEndpoint(resolution.definition, resolution.model.baseUrl) &&\n temperature !== undefined &&\n topP !== undefined\n ) {\n // Native Claude models require at most one of these samplers. MVU persists both\n // controls with 1 as their default, so omit the untouched one from the wire request.\n if (topP === 1) {\n topP = undefined;\n } else if (temperature === 1) {\n temperature = undefined;\n } else {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source Anthropic sampling requires temperature or top_p; reset one to 1.'\n );\n }\n }\n return {\n temperature,\n sampling: Object.freeze({\n ...(topP === undefined ? {} : { topP }),\n // zero is the existing \"unset\" value and is invalid for APIs that send top_k.\n ...(topK === undefined || topK === 0 ? {} : { topK }),\n ...(frequencyPenalty === undefined ? {} : { frequencyPenalty }),\n ...(presencePenalty === undefined ? {} : { presencePenalty }),\n }),\n };\n}\n\n/** 复制本次结构化输出 Schema,避免执行时修改调用方定义。 */\nfunction cloneJsonSchema(schema: PiJsonSchema | undefined): PiJsonSchema | undefined {\n return schema === undefined ? undefined : structuredClone(schema);\n}\n\n/** 解析当前模型的可信协议能力,注册信息缺失时终止配置预检。 */\nfunction capabilityFor(\n resolution: ResolvedPiModel,\n thinkingLevel: PiThinkingLevel\n): Readonly {\n const capability = resolvePiCapabilities(\n resolution.definition,\n resolution.model.api as PiWireApi,\n {\n model: resolution.model,\n catalogHit: resolution.catalogHit,\n thinkingLevel,\n }\n );\n if (!capability) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source API '${resolution.model.api}' has no registered runtime capability metadata`\n );\n }\n return capability;\n}\n\n/** 在捕获提示词前确认所选应答格式受到支持,避免重复发送必然失败的请求。 */\nfunction assertResponseCapability(\n responseFormat: PiRuntimeResponseFormat,\n capabilities: Readonly,\n api: Api\n): void {\n if (responseFormat === '工具调用' && !capabilities.tools) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support tool calls`\n );\n }\n const structuredOutputUnsupported =\n responseFormat === '格式化输出' && !capabilities.structuredOutput;\n const jsonObjectOutputUnsupported =\n responseFormat === '格式化输出(v4兼容)' && !capabilities.jsonObjectOutput;\n if (structuredOutputUnsupported || jsonObjectOutputUnsupported) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support native structured output`\n );\n }\n}\n\n/** 仅在工具调用模式校验并复制工具定义,其他模式不携带业务工具。 */\nfunction prepareTools(\n responseFormat: PiRuntimeResponseFormat,\n definitions: readonly ToolDefinition[] | undefined\n): readonly Tool[] | undefined {\n if (responseFormat !== '工具调用') {\n return undefined;\n }\n if (!definitions?.length) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source tool-call mode requires at least one tool definition'\n );\n }\n return Object.freeze(\n definitions.map(definition =>\n toPiToolDefinition(definition, {\n constrainedSampling: { type: 'json_schema', strict: 'prefer' },\n })\n )\n );\n}\n\n/** JSON Schema 模式要求显式结构定义,其他应答格式不继承残留 Schema。 */\nfunction prepareJsonSchema(\n responseFormat: PiRuntimeResponseFormat,\n schema: PiJsonSchema | undefined\n): PiJsonSchema | undefined {\n if (responseFormat === '格式化输出' && schema === undefined) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source structured output requires a JSON schema'\n );\n }\n return responseFormat === '格式化输出' ? cloneJsonSchema(schema) : undefined;\n}\n\n/** 用空载荷预演请求转换,提前发现受保护字段覆盖及格式配置错误。 */\nfunction validatePayloadConfiguration(preflight: {\n resolution: ResolvedPiModel;\n responseFormat: PiRuntimeResponseFormat;\n customIncludeBody?: Readonly>;\n customExcludeBody?: readonly string[];\n sampling: PiRuntimeSampling;\n jsonSchema?: PiJsonSchema;\n}): void {\n try {\n transformPiPayload(\n {},\n {\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n }\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source payload configuration is invalid'\n );\n }\n}\n\n/** 确认 OAuth 来源已有可用类型的凭证;到期刷新由后续 Pi 认证流程处理。 */\nasync function assertOAuthCredential(\n resolution: ResolvedPiModel,\n credentialStore: CredentialStore,\n signal?: AbortSignal\n): Promise {\n if (resolution.authType !== 'oauth') {\n return;\n }\n signal?.throwIfAborted();\n const credential = await credentialStore.read(resolution.definition.providerId, { signal });\n signal?.throwIfAborted();\n if (!credential || credential.type !== 'oauth') {\n throw new PiRuntimeError(\n 'missing_oauth_credential',\n `More source provider '${resolution.definition.providerId}' is not logged in`\n );\n }\n}\n\n/**\n * 一次性校验不依赖提示词的连接、认证、能力、预算参数和请求覆盖,并冻结本轮快照。\n * 应在串行或并发重试前调用,避免把固定配置错误误判为可重试的服务商失败。\n */\nexport async function assertPiRuntimeConfiguration(\n input: AssertPiRuntimeConfigurationInput\n): Promise {\n input.signal?.throwIfAborted();\n const settings = requireSettings(input.settings);\n const resolution = resolvePiModelFromExtraModelSettings(settings);\n const responseFormat = resolveResponseFormat(settings, input.responseFormat);\n const requestedThinkingLevel = normalizePiThinkingLevel(\n isPlainObject(settings.pi) ? settings.pi.thinkingLevel : undefined\n );\n resolution.model = preparePiThinkingModel(\n resolution.model,\n resolution.catalogHit,\n requestedThinkingLevel\n );\n // Validation, sampling and dispatch must all use the same effective level.\n const thinkingLevel = resolvePiThinkingLevel(requestedThinkingLevel, resolution.model);\n const capabilities = capabilityFor(resolution, thinkingLevel);\n const streaming =\n settings['兼容假流式'] === true || isPiStreamingRequired(resolution.model.api);\n if (streaming && !capabilities.streaming) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source model '${resolution.model.id}' does not support streaming requests`\n );\n }\n assertResponseCapability(responseFormat, capabilities, resolution.model.api);\n\n const piSettings = settings.pi;\n if (!isPlainObject(piSettings)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source connection settings must be an object'\n );\n }\n const useCorsProxy = shouldUsePiCorsProxy(\n resolution.definition,\n resolution.model.api,\n optionalStringField(piSettings, 'endpoint'),\n piSettings.useProxy\n );\n let headers: ProviderHeaders | undefined;\n let customIncludeBody: Record | undefined;\n let customExcludeBody: string[] | undefined;\n try {\n headers = parsePiCustomHeaders(optionalStringField(piSettings, 'customHeaders'));\n customIncludeBody = parsePiCustomIncludeBody(\n optionalStringField(piSettings, 'customIncludeBody')\n );\n customExcludeBody = parsePiCustomExcludeBody(\n optionalStringField(piSettings, 'customExcludeBody')\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error\n ? error.message\n : 'More source custom request configuration is invalid'\n );\n }\n const { temperature, sampling } = resolveSampling(settings, capabilities, resolution);\n let tools: readonly Tool[] | undefined;\n let toolChoice: unknown;\n try {\n tools = prepareTools(responseFormat, input.tools);\n toolChoice =\n responseFormat === '工具调用'\n ? resolvePiToolChoice(resolution.model.api, input.toolChoice ?? 'required')\n : undefined;\n } catch (error) {\n if (error instanceof PiRuntimeError) {\n throw error;\n }\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source tool configuration is invalid'\n );\n }\n if (isPiThinkingEnabled(thinkingLevel) && resolution.model.api === 'anthropic-messages') {\n if (toolChoice !== undefined && toolChoice !== 'auto' && toolChoice !== 'none') {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic thinking does not support forced tool calls. Use chat/structured output or turn thinking off.'\n );\n }\n if (\n !(\n resolution.model.compat &&\n 'forceAdaptiveThinking' in resolution.model.compat &&\n resolution.model.compat.forceAdaptiveThinking === true\n ) &&\n resolution.effectiveMaxTokens < 2048\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic budget-based thinking requires at least 2048 max reply tokens.'\n );\n }\n }\n const jsonSchema = prepareJsonSchema(responseFormat, input.jsonSchema);\n const credentialStore = input.credentialStore ?? getPiCredentialStore(piSettings);\n await assertOAuthCredential(resolution, credentialStore, input.signal);\n\n const preflight: PiRuntimePreflight = Object.freeze({\n resolution,\n responseFormat,\n capabilities,\n ...(headers === undefined ? {} : { headers: Object.freeze({ ...headers }) }),\n ...(customIncludeBody === undefined\n ? {}\n : { customIncludeBody: Object.freeze(structuredClone(customIncludeBody)) }),\n ...(customExcludeBody === undefined\n ? {}\n : { customExcludeBody: Object.freeze([...customExcludeBody]) }),\n temperature: capabilities.temperature ? temperature : undefined,\n sampling,\n tools,\n toolChoice,\n jsonSchema,\n credentialStore,\n fetch: input.fetch,\n useCorsProxy,\n streaming,\n thinkingLevel,\n });\n validatePayloadConfiguration(preflight);\n return preflight;\n}\n\n/** 只为本次解析出的模型和协议创建 Pi 服务商,并接入浏览器 OAuth 或请求密钥。 */\nfunction createRuntimeProvider(preflight: PiRuntimePreflight) {\n const { resolution } = preflight;\n let auth: ProviderAuth;\n if (resolution.authType === 'oauth') {\n auth = {\n oauth: getBrowserOAuthAuth(resolution.definition.providerId, {\n fetch: preflight.fetch,\n }),\n };\n } else {\n auth = {\n apiKey: {\n name: `${resolution.definition.displayName.en} API key`,\n /** 将本次设置提供的 API Key 转为 Pi 认证结果,解析前检查请求是否取消。 */\n async resolve({ credential, signal }) {\n signal.throwIfAborted();\n if (!credential?.key) {\n return undefined;\n }\n return {\n auth: { apiKey: credential.key },\n source: 'extra-model settings',\n };\n },\n },\n };\n }\n\n return createProvider({\n id: resolution.definition.providerId,\n name: resolution.definition.displayName.en,\n baseUrl: resolution.model.baseUrl,\n auth,\n models: [resolution.model],\n api: createPiApiImplementations(resolution.definition),\n });\n}\n\n/** 检查用户消息和工具返回值是否包含图片,供能力预检使用。 */\nfunction contextHasImages(context: ReturnType['context']): boolean {\n return context.messages.some(message => {\n if (message.role === 'user') {\n return (\n Array.isArray(message.content) &&\n message.content.some(content => content.type === 'image')\n );\n }\n if (message.role === 'toolResult') {\n return message.content.some(content => content.type === 'image');\n }\n return false;\n });\n}\n\n/** 提示词含图片时同时核对协议能力和模型输入类型,发送前拒绝不支持的组合。 */\nfunction assertImageCapability(\n preflight: PiRuntimePreflight,\n context: ReturnType['context']\n): void {\n if (!contextHasImages(context)) {\n return;\n }\n if (!preflight.capabilities.imageInput || !preflight.resolution.model.input.includes('image')) {\n throw new PiRuntimeError(\n 'unsupported_image_input',\n `More source model '${preflight.resolution.model.id}' does not support image input`\n );\n }\n}\n\n/** 仅保留需要写入原生载荷的两种格式化输出模式。 */\nfunction nativeStructuredFormat(\n responseFormat: PiRuntimeResponseFormat\n): '格式化输出' | '格式化输出(v4兼容)' | undefined {\n return responseFormat === '格式化输出' || responseFormat === '格式化输出(v4兼容)'\n ? responseFormat\n : undefined;\n}\n\n/**\n * 按本轮快照组合认证、采样、代理、非流式传输及载荷转换回调。\n * Codex 经代理时强制使用 SSE,所有请求层共享同一取消信号。\n */\nfunction createStreamOptions(\n preflight: PiRuntimePreflight,\n signal: AbortSignal,\n systemMessages: PiSystemMessageBridge\n): ApiStreamOptions {\n const providerFetch = preflight.useCorsProxy\n ? createSillyTavernProxyFetch({\n baseUrl: preflight.resolution.model.baseUrl,\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : preflight.fetch;\n return {\n signal,\n apiKey: preflight.resolution.apiKey,\n fetch: preflight.streaming\n ? providerFetch\n : createPiNonStreamingFetch(preflight.resolution.model.api as PiWireApi, providerFetch),\n headers: preflight.headers === undefined ? undefined : { ...preflight.headers },\n temperature: preflight.capabilities.temperature ? preflight.temperature : undefined,\n maxTokens: preflight.resolution.effectiveMaxTokens,\n ...(preflight.toolChoice === undefined ? {} : { toolChoice: preflight.toolChoice }),\n ...(preflight.useCorsProxy && preflight.resolution.model.api === 'openai-codex-responses'\n ? { transport: 'sse' as const }\n : {}),\n onPayload: payload =>\n systemMessages.restore(\n createPiPayloadTransform({\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n })(payload)\n ),\n };\n}\n\n/** 识别常见浏览器 fetch、网络和 CORS 失败,供运行时选择网络错误类别。 */\nfunction isNetworkFailure(error: unknown): boolean {\n const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';\n return /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message\n );\n}\n\n/** 通过稳定错误码识别 Google 注入点不兼容,避免直接导入适配器产生循环依赖。 */\nfunction isGoogleProxyAdapterCompatibilityFailure(error: unknown): boolean {\n // Keep the runtime boundary independent from the Google adapter module: provider registration\n // imports the runtime-facing gateway, so importing the adapter here would create a cycle.\n return (\n error instanceof Error &&\n (error as Error & { code?: unknown }).code === 'google_proxy_adapter_incompatible'\n );\n}\n\n/**\n * 将已知底层失败归类为运行时错误并保留重试语义。\n * 未知错误可能附带请求头或正文,只返回固定提示,不保留原始 cause。\n */\nfunction normalizeRuntimeFailure(error: unknown): Error {\n if (error instanceof PiRuntimeError || error instanceof PiContextAdapterError) {\n return error;\n }\n if (error instanceof PiProxyUnavailableError) {\n return new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n );\n }\n if (isGoogleProxyAdapterCompatibilityFailure(error)) {\n return new PiRuntimeError(\n 'protocol',\n 'The installed Google Generative AI SDK is incompatible with the browser proxy transport required by this build.'\n );\n }\n if (error instanceof PiResultAdapterError && error.code === 'aborted') {\n // The generation id is supplied by the request-level abort conversion below.\n return error;\n }\n if (\n (error instanceof PiResultAdapterError && error.code === 'network') ||\n isNetworkFailure(error)\n ) {\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n if (error instanceof PiResultAdapterError) {\n switch (error.code) {\n case 'provider-error':\n return new PiRuntimeError('provider', 'More source request failed.', true);\n case 'deferred':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned a deferred response, which is not supported.'\n );\n case 'empty-response':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned no usable response content.'\n );\n case 'invalid-tool-call':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned an invalid tool call.'\n );\n case 'length':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider response was truncated because it reached the output limit.'\n );\n case 'aborted':\n return error;\n case 'network':\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n }\n // SDK/fetch implementations may attach response bodies, headers, or request configuration to\n // arbitrary errors. Do not surface the unknown error or retain it as a cause.\n return new PiRuntimeError('provider', 'More source request failed.', true);\n}\n\n/** 要求非空生成编号,使取消登记与提示词捕获可以准确关联。 */\nfunction assertGenerationId(generationId: string): void {\n if (!generationId.trim()) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source request generationId must not be empty'\n );\n }\n}\n\n/**\n * 将捕获的酒馆提示词转换为 Pi 上下文,检查图片及 token 预算后执行所选服务商请求。\n * 统一处理代理检查、取消登记、错误分类和结果转换;返回值不自动追加到聊天或上下文。\n */\nexport async function runPiRequest(\n input: RunPiRequestInput\n): Promise {\n assertGenerationId(input.generationId);\n let preflight: PiRuntimePreflight;\n try {\n preflight =\n input.preflight ??\n (await assertPiRuntimeConfiguration({\n settings: input.settings,\n responseFormat: input.responseFormat,\n jsonSchema: input.jsonSchema,\n tools: input.tools,\n toolChoice: input.toolChoice,\n credentialStore: input.credentialStore,\n fetch: input.fetch,\n signal: input.signal,\n }));\n } catch (error) {\n if (input.signal?.aborted) {\n throw new PiRequestAbortedError(input.generationId, input.signal.reason ?? error);\n }\n throw error;\n }\n\n let registration: ReturnType;\n try {\n registration = registerPiRequestController(input.generationId, input.signal);\n } catch (error) {\n throw new PiRuntimeError(\n 'request_already_active',\n error instanceof Error ? error.message : 'More source request is already active'\n );\n }\n let systemMessages: PiSystemMessageBridge | undefined;\n try {\n if (registration.signal.aborted) {\n throw new PiRequestAbortedError(input.generationId, registration.signal.reason);\n }\n\n if (preflight.useCorsProxy) {\n try {\n await assertSillyTavernProxyAvailable({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n signal: registration.signal,\n force: true,\n });\n } catch (error) {\n throw normalizeRuntimeFailure(error);\n }\n }\n\n let adapted: ReturnType;\n try {\n adapted = toPiContext(input.messages);\n } catch (error) {\n if (error instanceof PiContextAdapterError) {\n throw new PiRuntimeError('invalid_prompt', error.message);\n }\n throw error;\n }\n const { context, lateSystemMessages } = adapted;\n if (preflight.tools !== undefined) {\n context.tools = [...preflight.tools];\n }\n assertImageCapability(preflight, context);\n systemMessages = createPiSystemMessageBridge(adapted, preflight.resolution.model.api);\n try {\n assertPiTokenBudget(\n context,\n preflight.resolution.effectiveContextWindow,\n preflight.resolution.effectiveMaxTokens,\n undefined,\n lateSystemMessages\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'token_budget',\n error instanceof Error ? error.message : 'More source token preflight failed'\n );\n }\n\n const models = createModels({\n credentials: preflight.credentialStore,\n authContext: BROWSER_AUTH_CONTEXT,\n });\n models.setProvider(createRuntimeProvider(preflight));\n const options = createStreamOptions(preflight, registration.signal, systemMessages);\n if (\n preflight.resolution.model.api === 'google-generative-ai' &&\n options.fetch &&\n options.fetch !== globalThis.fetch\n ) {\n // Pi wraps provider setup in lazyStream and would otherwise erase the compatibility\n // error code. Check the instance-only SDK seam before entering that boundary.\n assertGoogleProxyAdapterCompatible();\n }\n let stream;\n if (preflight.thinkingLevel === 'default') {\n stream = models.stream(preflight.resolution.model, systemMessages.context, options);\n } else {\n const model = {\n ...preflight.resolution.model,\n // The shared answer/thinking budget must stay inside the preflight reservation.\n maxTokens: preflight.resolution.effectiveMaxTokens,\n };\n // Pi 1.0's simple adapters forward native toolChoice unchanged, although the public\n // simple type only declares auto/none. Keep required/named choices from our resolver.\n // Real-adapter transport tests cover this compatibility boundary.\n const simpleOptions = {\n ...options,\n reasoning: preflight.thinkingLevel === 'off' ? undefined : preflight.thinkingLevel,\n } as SimpleStreamOptions;\n stream = models.streamSimple(model, systemMessages.context, simpleOptions);\n }\n\n for await (const event of stream) {\n // Provider error events can contain raw response bodies, request headers, or echoed\n // credentials. The normalized terminal error below is the public failure boundary.\n if (event.type !== 'error') {\n await input.onProgress?.(event);\n }\n }\n const message = await stream.result();\n if (registration.signal.aborted || message.stopReason === 'aborted') {\n throw new PiRequestAbortedError(\n input.generationId,\n registration.signal.aborted ? registration.signal.reason : undefined\n );\n }\n if (systemMessages.failure) throw systemMessages.failure;\n const result = fromPiAssistantMessage(message);\n systemMessages.assertRestored();\n return result;\n } catch (error) {\n const wasAborted =\n registration.signal.aborted ||\n error instanceof PiRequestAbortedError ||\n (error instanceof PiResultAdapterError && error.code === 'aborted');\n if (wasAborted) {\n throw error instanceof PiRequestAbortedError\n ? error\n : new PiRequestAbortedError(input.generationId, error);\n }\n const requestError = systemMessages?.failure ?? error;\n const proxy_status = preflight.useCorsProxy\n ? getSillyTavernProxyStatus({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : 'unchecked';\n const normalizedError =\n proxy_status === 'disabled' || proxy_status === 'unavailable'\n ? new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n )\n : normalizeRuntimeFailure(requestError);\n if (!registration.signal.aborted) {\n // Abort listeners must not observe the raw provider error either.\n registration.controller.abort(normalizedError);\n }\n throw normalizedError;\n } finally {\n registration.release();\n }\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** MVU 接受的工具选择规则;required/any 均表示必须调用工具,发送前按协议转换。 */\nexport type MvuToolChoice =\n | 'auto'\n | 'none'\n | 'required'\n | 'any'\n | { type: 'function'; function: { name: string } };\n\n/**\n * 把 MVU 工具选择转换为当前协议接受的形式。\n * 保留 auto/none,转换 required/any 与具名调用;不支持具名选择的协议提前拒绝。\n */\nexport function resolvePiToolChoice(api: Api, choice: MvuToolChoice | undefined): unknown {\n const normalized = choice ?? 'auto';\n if (normalized === 'auto' || normalized === 'none') {\n return normalized;\n }\n\n const named = typeof normalized === 'object' ? normalized.function.name.trim() : undefined;\n if (named !== undefined && !named) {\n throw new Error('More source named tool choice requires a tool name');\n }\n\n if (api === 'openai-completions') {\n return named\n ? { type: 'function', function: { name: named } }\n : normalized === 'any'\n ? 'required'\n : normalized;\n }\n if (api === 'openai-responses') {\n return named ? { type: 'function', name: named } : 'required';\n }\n if (api === 'openai-codex-responses') {\n if (named) {\n throw new Error(\n \"More source API 'openai-codex-responses' does not support a named tool choice\"\n );\n }\n return 'required';\n }\n if (api === 'anthropic-messages') {\n return named ? { type: 'tool', name: named } : 'any';\n }\n if (api === 'google-generative-ai') {\n if (named) {\n throw new Error(\n \"More source API 'google-generative-ai' does not support a named tool choice\"\n );\n }\n return 'any';\n }\n if (api === 'mistral-conversations') {\n return named ? { type: 'function', function: { name: named } } : normalized;\n }\n throw new Error(`More source API '${api}' does not support required tool choice`);\n}\n","import type { FetchFunction } from './pi_gateway';\n\nconst PROXY_DISABLED_MESSAGE =\n 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';\nconst PROXY_PROBE_BODY = 'mvu-st-cors-proxy-probe';\nconst PROXY_PROBE_TARGET = `data:text/plain,${PROXY_PROBE_BODY}`;\nconst PROXY_PROBE_TIMEOUT_MS = 5_000;\n\n/** 酒馆 CORS 代理的探测状态,区分未检查、检查中和三种已完成结果。 */\nexport type SillyTavernProxyStatus =\n | 'unchecked'\n | 'checking'\n | 'enabled'\n | 'disabled'\n | 'unavailable';\n\n/** 一次探测完成后可缓存的结果:已启用、明确关闭或当前不可用。 */\nexport type SillyTavernProxyTerminalStatus = Extract<\n SillyTavernProxyStatus,\n 'enabled' | 'disabled' | 'unavailable'\n>;\n\n/** 代理探测的环境与缓存选项,允许测试注入传输和酒馆源地址。 */\nexport interface SillyTavernProxyProbeOptions {\n /** 默认使用页面的 fetch 实现。 */\n fetch?: FetchFunction;\n /** 默认从页面基础地址或 location 解析酒馆源地址。 */\n origin?: string;\n /** 仅取消当前调用方的等待,不中止其他调用方共用的探测。 */\n signal?: AbortSignal;\n /** 忽略已缓存的完成结果,重新检查酒馆代理路由。 */\n force?: boolean;\n}\n\n/** 创建代理 fetch 所需的配置,限定携带凭证请求能够访问的目标范围。 */\nexport interface SillyTavernProxyFetchOptions extends SillyTavernProxyProbeOptions {\n /**\n * 服务商 API 基础地址;返回的 fetch 仅允许向同源、同路径或下级路径转发凭证。\n */\n baseUrl: string | URL;\n}\n\nexport class PiProxyUnavailableError extends Error {\n readonly code = 'proxy_unavailable';\n readonly retryable = false;\n\n /** 区分代理未启用和不可用,并标记为发送前应终止的配置类失败。 */\n constructor(readonly status: Exclude) {\n super(\n status === 'disabled'\n ? 'SillyTavern CORS proxy is not enabled'\n : 'SillyTavern CORS proxy is unavailable'\n );\n this.name = 'PiProxyUnavailableError';\n }\n}\n\n/** 与 Pi fetch 契约保持一致的请求目标,可为 URL、字符串或 Request。 */\ntype FetchInput = Parameters[0];\n\n/** 按 fetch 实现及酒馆源地址共享的探测记录,用 promise 合并同时发起的检查。 */\ntype ProbeEntry = {\n status: SillyTavernProxyStatus;\n promise?: Promise;\n};\n\nlet probe_cache = new WeakMap>();\n\n/** 读取注入或全局 fetch;浏览器缺少请求能力时返回 undefined。 */\nfunction resolveFetch(fetch_override?: FetchFunction): FetchFunction | undefined {\n const candidate = fetch_override ?? globalThis.fetch;\n return typeof candidate === 'function' ? candidate : undefined;\n}\n\n/** 从覆盖值或文档基础地址解析酒馆源地址,兼容 origin 为 null 的 srcdoc 脚本。 */\nfunction resolveOrigin(origin_override?: string): string | undefined {\n // Slash normally hosts scripts in srcdoc, where location.origin is \"null\" despite\n // inheriting the parent's origin. Use the same document base as relative browser fetch;\n // Slash's Blob mode also supplies a pointing to SillyTavern.\n const value = origin_override ?? globalThis.document?.baseURI ?? globalThis.location?.origin;\n if (!value) {\n return undefined;\n }\n try {\n const origin = new URL(value).origin;\n return origin === 'null' ? undefined : origin;\n } catch {\n return undefined;\n }\n}\n\n/** 按 fetch 实现与酒馆源地址隔离探测缓存,复用同一目标的进行中检查。 */\nfunction getProbeEntry(fetch_impl: FetchFunction, origin: string): ProbeEntry {\n let entries = probe_cache.get(fetch_impl);\n if (!entries) {\n entries = new Map();\n probe_cache.set(fetch_impl, entries);\n }\n let entry = entries.get(origin);\n if (!entry) {\n entry = { status: 'unchecked' };\n entries.set(origin, entry);\n }\n return entry;\n}\n\n/** 将完整目标地址编码进酒馆通用 CORS 代理路径。 */\nfunction proxyUrl(target: string): string {\n return `/proxy/${encodeURIComponent(target)}`;\n}\n\n/** 忽略首尾空白后精确核对探测正文,避免把其他页面误识别为代理结果。 */\nfunction exactResponseText(actual: string, expected: string): boolean {\n return actual.trim() === expected;\n}\n\n/** 通过本地 data URL 和超时限制探测代理,区分启用、关闭和暂时不可达。 */\nasync function performProbe(fetch_impl: FetchFunction): Promise {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROXY_PROBE_TIMEOUT_MS);\n try {\n // node-fetch resolves this data URL locally. Unlike a same-origin circular sentinel, it\n // remains reliable when SillyTavern is behind an HTTPS reverse proxy.\n const response = await fetch_impl(proxyUrl(PROXY_PROBE_TARGET), {\n method: 'GET',\n credentials: 'same-origin',\n cache: 'no-store',\n signal: controller.signal,\n });\n const body = await response.text();\n if (response.status === 200 && exactResponseText(body, PROXY_PROBE_BODY)) {\n return 'enabled';\n }\n if (response.status === 404 && exactResponseText(body, PROXY_DISABLED_MESSAGE)) {\n return 'disabled';\n }\n return 'unavailable';\n } catch {\n return 'unavailable';\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/** 保留信号原始取消原因,缺失时补齐标准 AbortError。 */\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n}\n\n/** 允许调用方取消对共享探测的等待,不中断其他调用方共用的探测请求。 */\nfunction waitForProbe(\n promise: Promise,\n signal?: AbortSignal\n): Promise {\n if (!signal) {\n return promise;\n }\n if (signal.aborted) {\n return Promise.reject(abortReason(signal));\n }\n return new Promise((resolve, reject) => {\n /** 解除当前等待者的监听并拒绝其 Promise,保留共享探测继续运行。 */\n const on_abort = () => {\n cleanup();\n reject(abortReason(signal));\n };\n /** 移除当前等待者的取消监听,避免探测结束后残留引用。 */\n const cleanup = () => signal.removeEventListener('abort', on_abort);\n signal.addEventListener('abort', on_abort, { once: true });\n promise.then(\n status => {\n cleanup();\n resolve(status);\n },\n error => {\n cleanup();\n reject(error);\n }\n );\n });\n}\n\n/** 只读取缓存中的代理状态,不触发新的网络探测。 */\nexport function getSillyTavernProxyStatus(\n options: SillyTavernProxyProbeOptions = {}\n): SillyTavernProxyStatus {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return 'unavailable';\n }\n return getProbeEntry(fetch_impl, origin).status;\n}\n\n/**\n * 合并并发探测并缓存确定的启用或关闭状态,暂时失败允许重试。\n * force 可重新核对服务端状态;单个等待者取消不影响共享探测。\n */\nexport function probeSillyTavernProxy(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return Promise.resolve('unavailable');\n }\n\n const entry = getProbeEntry(fetch_impl, origin);\n if (entry.promise) {\n return waitForProbe(entry.promise, options.signal);\n }\n if (!options.force && (entry.status === 'enabled' || entry.status === 'disabled')) {\n return waitForProbe(Promise.resolve(entry.status), options.signal);\n }\n\n entry.status = 'checking';\n entry.promise = performProbe(fetch_impl).then(status => {\n entry.status = status;\n entry.promise = undefined;\n return status;\n });\n return waitForProbe(entry.promise, options.signal);\n}\n\n/** 在发送带凭证的服务商请求前确认代理可用,不可用时立即报错。 */\nexport async function assertSillyTavernProxyAvailable(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const status = await probeSillyTavernProxy(options);\n if (status !== 'enabled') {\n throw new PiProxyUnavailableError(status);\n }\n}\n\n/** 要求不含内嵌凭证的绝对 HTTP(S) 地址,作为代理目标的第一层校验。 */\nfunction parseHttpUrl(value: string | URL, name: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL`);\n }\n if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL without credentials`);\n }\n return url;\n}\n\n/** 去除基础路径末尾斜杠,便于严格比较目标路径边界。 */\nfunction normalizeBasePath(pathname: string): string {\n return pathname === '/' ? '' : pathname.replace(/\\/+$/, '');\n}\n\n/** 限制请求必须位于配置服务商的同源基础路径内,避免凭证随代理流向其他目标。 */\nfunction assertTargetAllowed(target: URL, base: URL): void {\n const base_path = normalizeBasePath(base.pathname);\n const in_base_path =\n base_path === '' ||\n target.pathname === base_path ||\n target.pathname.startsWith(`${base_path}/`);\n if (target.origin !== base.origin || !in_base_path) {\n throw new TypeError('SillyTavern proxy target is outside the configured provider base URL');\n }\n}\n\n/** 识别 Request 输入,供后续按 fetch 语义合并 init 覆盖值。 */\nfunction requestFromInput(input: FetchInput): Request | undefined {\n return typeof Request !== 'undefined' && input instanceof Request ? input : undefined;\n}\n\n/** 统一取得字符串、URL 或 Request 输入对应的目标地址。 */\nfunction requestUrl(input: FetchInput, request: Request | undefined): string {\n return request?.url ?? input.toString();\n}\n\n/** 按 init 优先、Request 次之的顺序解析 HTTP 方法并统一大写。 */\nfunction requestMethod(request: Request | undefined, init?: RequestInit): string {\n return (init?.method ?? request?.method ?? 'GET').toUpperCase();\n}\n\n/** 遵循 fetch 的覆盖顺序解析本次请求头。 */\nfunction requestHeaders(request: Request | undefined, init?: RequestInit): HeadersInit | undefined {\n return init?.headers ?? request?.headers;\n}\n\n/** 保留 init 显式传入的取消信号或 null,否则继承 Request 的信号。 */\nfunction requestSignal(\n request: Request | undefined,\n init?: RequestInit\n): AbortSignal | null | undefined {\n if (init && 'signal' in init) {\n return init.signal;\n }\n return request?.signal;\n}\n\n/** 将可重放的请求体转换为文本,拒绝无法安全复用的流或其他载荷。 */\nasync function bodyToText(body: BodyInit): Promise {\n if (typeof body === 'string') {\n return body;\n }\n if (body instanceof URLSearchParams) {\n return body.toString();\n }\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return body.text();\n }\n if (body instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(body));\n }\n if (ArrayBuffer.isView(body)) {\n return new TextDecoder().decode(\n new Uint8Array(body.buffer, body.byteOffset, body.byteLength)\n );\n }\n throw new TypeError('SillyTavern proxy only supports replayable JSON request bodies');\n}\n\n/** 优先读取 init 请求体,必要时克隆 Request 读取,避免消费原始输入。 */\nasync function requestBodyText(\n request: Request | undefined,\n init?: RequestInit\n): Promise {\n if (init?.body !== undefined && init.body !== null) {\n return bodyToText(init.body);\n }\n if (init && 'body' in init && init.body === null) {\n return undefined;\n }\n if (request?.body !== null && request !== undefined) {\n return request.clone().text();\n }\n return undefined;\n}\n\n/** 从 Headers、键值数组或对象中按大小写不敏感名称查找请求头。 */\nfunction getHeader(headers: HeadersInit | undefined, wanted_name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const wanted = wanted_name.toLowerCase();\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(wanted_name) ?? undefined;\n }\n if (Array.isArray(headers)) {\n return headers.find(([name]) => name.toLowerCase() === wanted)?.[1];\n }\n const entry = Object.entries(headers).find(([name]) => name.toLowerCase() === wanted);\n return entry?.[1] === undefined ? undefined : String(entry[1]);\n}\n\n/** 确认带正文的代理请求使用受支持的方法、JSON 类型和合法 JSON 内容。 */\nfunction assertJsonBody(method: string, headers: HeadersInit | undefined, body?: string): void {\n if (body === undefined) {\n return;\n }\n if (!['POST', 'PUT', 'PATCH'].includes(method)) {\n throw new TypeError(`SillyTavern proxy does not forward request bodies for ${method}`);\n }\n const content_type = getHeader(headers, 'content-type') ?? '';\n if (!/^application\\/json(?:\\s*;|$)/i.test(content_type)) {\n throw new TypeError('SillyTavern proxy requires Content-Type: application/json');\n }\n try {\n JSON.parse(body);\n } catch {\n throw new TypeError('SillyTavern proxy request body must be valid JSON');\n }\n}\n\n/** 在准备和转发请求的边界检查取消状态,保留原始取消原因。 */\nfunction throwIfAborted(signal: AbortSignal | null | undefined): void {\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n }\n}\n\n/** 保留原始应答格式,仅遮盖上游可能在错误正文中回显的本次请求凭证。 */\nfunction redactProxyResponseBody(\n body: string,\n headers: HeadersInit | undefined,\n target: URL\n): string {\n const credentials = new Set();\n for (const name of ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key']) {\n const value = getHeader(headers, name)?.trim();\n if (value) {\n credentials.add(value);\n credentials.add(value.replace(/^Bearer\\s+/i, ''));\n }\n }\n for (const [name, value] of target.searchParams) {\n if (/^(?:key|api[_-]?key|(?:access[_-]?)?token)$/i.test(name) && value) {\n credentials.add(value);\n }\n }\n for (const credential of [...credentials].filter(Boolean).sort((a, b) => b.length - a.length)) {\n body = body.split(credential).join('[REDACTED]');\n }\n return body;\n}\n\n/** 异步读取克隆应答,在流结束或中断后输出 debug 日志,不占用 SDK 的正文。 */\nasync function debugProxyResponse(\n response: Response,\n target: URL,\n method: string,\n headers: HeadersInit | undefined,\n signal: AbortSignal | null | undefined\n): Promise {\n const copy = response.clone();\n const reader = copy.body?.getReader();\n let body = '';\n let complete = false;\n // 不等待 tee 分支的 cancel:其 Promise 可能要等 SDK 也结束读取才完成。\n const on_abort = () => {\n void reader?.cancel().catch(() => {});\n };\n try {\n if (reader) {\n const decoder = new TextDecoder();\n signal?.addEventListener('abort', on_abort, { once: true });\n if (signal?.aborted) {\n on_abort();\n }\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) {\n body += decoder.decode();\n complete = !signal?.aborted;\n break;\n }\n body += decoder.decode(chunk.value, { stream: true });\n }\n } else {\n body = await copy.text();\n complete = !signal?.aborted;\n }\n } catch {\n // 读取失败时仍记录已收到的部分;日志失败不能改变请求结果。\n } finally {\n signal?.removeEventListener('abort', on_abort);\n reader?.releaseLock();\n }\n console.debug(\n '[MVU Pi Proxy] raw response',\n { method, url: `${target.origin}${target.pathname}`, status: response.status, complete },\n redactProxyResponseBody(body, headers, target)\n );\n}\n\n/** 检查返回值是否为酒馆明确的代理关闭提示,不把普通上游 404 误判为代理关闭。 */\nasync function isDisabledProxyResponse(response: Response): Promise {\n if (response.status !== 404) {\n return false;\n }\n try {\n return exactResponseText(await response.clone().text(), PROXY_DISABLED_MESSAGE);\n } catch {\n return false;\n }\n}\n\n/** 将实际请求发现的代理终态写回对应探测缓存。 */\nfunction cacheTerminalStatus(\n fetch_impl: FetchFunction,\n origin: string,\n status: SillyTavernProxyTerminalStatus\n): void {\n const entry = getProbeEntry(fetch_impl, origin);\n entry.status = status;\n entry.promise = undefined;\n}\n\n/**\n * 创建受单一服务商基础地址约束的代理 fetch,转发认证头、JSON 正文和取消信号。\n * 发送前确认代理可用,目标越界或载荷不兼容时拒绝转发。\n */\nexport function createSillyTavernProxyFetch(options: SillyTavernProxyFetchOptions): FetchFunction {\n const base = parseHttpUrl(options.baseUrl, 'baseUrl');\n\n return async (input: FetchInput, init?: RequestInit): Promise => {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n throw new PiProxyUnavailableError('unavailable');\n }\n\n const request = requestFromInput(input);\n const target = parseHttpUrl(requestUrl(input, request), 'proxy target');\n assertTargetAllowed(target, base);\n\n const method = requestMethod(request, init);\n const headers = requestHeaders(request, init);\n const signal = requestSignal(request, init);\n throwIfAborted(signal);\n const body = await requestBodyText(request, init);\n assertJsonBody(method, headers, body);\n\n await assertSillyTavernProxyAvailable({\n fetch: fetch_impl,\n origin,\n ...(signal == null ? {} : { signal }),\n });\n throwIfAborted(signal);\n\n const response = await fetch_impl(proxyUrl(target.href), {\n method,\n ...(headers === undefined ? {} : { headers }),\n ...(body === undefined ? {} : { body }),\n ...(signal === undefined ? {} : { signal }),\n credentials: 'same-origin',\n });\n void debugProxyResponse(response, target, method, headers, signal).catch(() => {});\n if (await isDisabledProxyResponse(response)) {\n cacheTerminalStatus(fetch_impl, origin, 'disabled');\n throw new PiProxyUnavailableError('disabled');\n }\n return response;\n };\n}\n\n/** @internal 仅供测试清空模块级探测缓存,避免用例之间共享状态。 */\nexport function resetSillyTavernProxyStatusForTests(): void {\n probe_cache = new WeakMap();\n}\n","import { clampThinkingLevel, type Api, type Model } from './pi_gateway';\nimport { normalizePiThinkingLevel, type PiThinkingLevel } from './thinking_setting';\n\n/** 按可信目录或协议适配元数据解析生效等级;缺少元数据时保留显式选择。 */\nexport function resolvePiThinkingLevel(value: unknown, model?: Model): PiThinkingLevel {\n const level = normalizePiThinkingLevel(value);\n if (level === 'default' || level === 'off' || !model) return level;\n return model.reasoning ? clampThinkingLevel(model, level) : 'off';\n}\n\n/** 为动态模型补充显式启用信息,避免 SDK 再次把支持 effort 的协议降为 high。 */\nexport function preparePiThinkingModel(\n model: Model,\n catalogHit: boolean,\n level: PiThinkingLevel\n): Model {\n if (catalogHit || level === 'default') return model;\n const extendedEffort =\n (level === 'xhigh' || level === 'max') &&\n [\n 'openai-completions',\n 'openai-responses',\n 'openai-codex-responses',\n 'mistral-conversations',\n ].includes(model.api);\n return {\n ...model,\n reasoning: true,\n ...(extendedEffort ? { thinkingLevelMap: { xhigh: 'xhigh', max: 'max' } } : {}),\n };\n}\n","/** 默认值沿用原始协议行为;显式等级由 Pi 按模型能力映射。 */\nexport const PI_THINKING_LEVELS = [\n 'default',\n 'off',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n] as const;\nexport type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];\n\nexport function normalizePiThinkingLevel(value: unknown): PiThinkingLevel {\n return PI_THINKING_LEVELS.includes(value as PiThinkingLevel)\n ? (value as PiThinkingLevel)\n : 'default';\n}\n\nexport function isPiThinkingEnabled(value: unknown): boolean {\n const level = normalizePiThinkingLevel(value);\n return level !== 'default' && level !== 'off';\n}\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const commonMessages = defineMessages({\n 'common.enabled': {\n 'zh-CN': '启用',\n en: 'Enabled',\n },\n 'common.disabled': {\n 'zh-CN': '关闭',\n en: 'Disabled',\n },\n 'common.yes': {\n 'zh-CN': '是',\n en: 'Yes',\n },\n 'common.no': {\n 'zh-CN': '否',\n en: 'No',\n },\n 'common.confirm': {\n 'zh-CN': '确认',\n en: 'Confirm',\n },\n 'common.cancel': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const panelMessages = defineMessages({\n 'panel.title': {\n 'zh-CN': 'MVU 变量框架',\n en: 'MVU Variable Framework',\n },\n 'panel.version.section': {\n 'zh-CN': '当前版本',\n en: 'Current version',\n },\n 'panel.version.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n 'panel.notification.section': {\n 'zh-CN': '通知设置',\n en: 'Notifications',\n },\n 'panel.notification.frameworkLoaded': {\n 'zh-CN': 'MVU 框架加载成功时通知',\n en: 'Notify when the MVU framework loads successfully',\n },\n 'panel.notification.variablesInitialized': {\n 'zh-CN': '变量初始化成功时通知',\n en: 'Notify when variables are initialized successfully',\n },\n 'panel.notification.variableError': {\n 'zh-CN': '变量初始化/更新出错时通知',\n en: 'Notify when variable initialization or updates fail',\n },\n 'panel.notification.extraModelParsing': {\n 'zh-CN': '额外模型解析中通知',\n en: 'Notify while extra-model parsing is running',\n },\n 'panel.cleanup.section': {\n 'zh-CN': '自动清理变量',\n en: 'Automatic variable cleanup',\n },\n 'panel.cleanup.enable': {\n 'zh-CN': '启用自动清理变量',\n en: 'Enable automatic variable cleanup',\n },\n 'panel.cleanup.strategy': {\n 'zh-CN': '清理策略',\n en: 'Cleanup strategy',\n },\n 'panel.cleanup.snapshotInterval': {\n 'zh-CN': '快照保留间隔',\n en: 'Snapshot retention interval',\n },\n 'panel.cleanup.recentFloors': {\n 'zh-CN': '要保留变量的最近楼层数',\n en: 'Recent floors whose variables are retained',\n },\n 'panel.cleanup.restoreFloors': {\n 'zh-CN': '触发恢复变量的最近楼层数',\n en: 'Recent floors that trigger variable restoration',\n },\n 'panel.compatibility.section': {\n 'zh-CN': '兼容性',\n en: 'Compatibility',\n },\n 'panel.compatibility.updateChatVariables': {\n 'zh-CN': '变量更新到聊天变量',\n en: 'Write variable updates to chat variables',\n },\n 'panel.compatibility.updateChatVariablesHelp': {\n 'zh-CN':\n '启用后,所有变量更新结果也会输出到聊天变量中。如果部分老角色卡无法正常游玩,可以开启这个开关。',\n en: 'When enabled, every variable update is also written to chat variables. Enable this if some older character cards do not work correctly.',\n },\n 'panel.compatibility.showLegacy': {\n 'zh-CN': '显示老旧功能',\n en: 'Show legacy features',\n },\n 'panel.compatibility.sendasNotUser': {\n 'zh-CN': 'sendas 不视为 user 消息',\n en: 'Do not treat sendas as a user message',\n },\n 'panel.compatibility.nonblockingExtraAnalysis': {\n 'zh-CN': '额外模型解析不阻塞正文显示(实验性)',\n en: 'Show replies without waiting for extra model analysis (experimental)',\n },\n 'panel.compatibility.nonblockingExtraAnalysisHelp': {\n 'zh-CN':\n '默认关闭。开启后,角色卡的部分默认前端页面会立即显示,其他插件的额外分析流程也可同时执行,无需等待变量解析完成。仅对非工具调用的自动解析生效。',\n en: \"Off by default. When enabled, some of a character card's default frontend pages appear immediately, and other plugins can run their additional analysis at the same time, without waiting for variable analysis to finish. Applies only to automatic analysis without tool calling.\",\n },\n 'panel.compatibility.license': {\n 'zh-CN': 'LICENSE',\n en: 'LICENSE',\n },\n 'panel.compatibility.licenseIntro': {\n 'zh-CN': 'MVU 采用 MIT 许可证。以下是运行时使用或随扩展打包的第三方开源组件:',\n en: 'MVU is licensed under MIT. The following third-party components are used at runtime or bundled with the extension:',\n },\n 'panel.compatibility.licenseComponent': {\n 'zh-CN': '组件',\n en: 'Component',\n },\n 'panel.compatibility.licenseIdentifier': {\n 'zh-CN': '许可证',\n en: 'License',\n },\n 'panel.compatibility.licenseDetails': {\n 'zh-CN':\n '许可证标识来自当前依赖元数据;点击组件名称可查看项目,完整条款以各项目 LICENSE 为准。',\n en: 'License identifiers come from current dependency metadata. Select a component name to view its project; the project LICENSE contains the complete terms.',\n },\n 'panel.button.section': {\n 'zh-CN': '修复按钮',\n en: 'Repair actions',\n },\n 'panel.button.reprocessVariables': {\n 'zh-CN': '重新处理变量',\n en: 'Reprocess variables',\n },\n 'panel.button.reloadInitialVariables': {\n 'zh-CN': '重新读取初始变量',\n en: 'Reload initial variables',\n },\n 'panel.button.snapshotFloor': {\n 'zh-CN': '快照楼层',\n en: 'Snapshot floor',\n },\n 'panel.button.replayFloor': {\n 'zh-CN': '重演楼层',\n en: 'Replay floor',\n },\n 'panel.button.retryExtraModelParsing': {\n 'zh-CN': '重试额外模型解析',\n en: 'Retry extra-model parsing',\n },\n 'panel.button.incrementalRepairExtraModelParsing': {\n 'zh-CN': '增量校正额外模型解析',\n en: 'Incrementally repair extra-model parsing',\n },\n 'panel.button.clearOldFloorVariables': {\n 'zh-CN': '清除旧楼层变量',\n en: 'Clear variables from old floors',\n },\n 'panel.help.ariaLabel': {\n 'zh-CN': '帮助',\n en: 'Help',\n },\n 'panel.badge.override': {\n 'zh-CN': '角色卡覆盖',\n en: 'Character-card override',\n },\n 'panel.badge.overrideWithValue': {\n 'zh-CN': '角色卡覆盖:{value}',\n en: 'Character-card override: {value}',\n },\n 'panel.badge.additive': {\n 'zh-CN': '角色卡规则叠加',\n en: 'Character-card rule added',\n },\n 'panel.update.section': {\n 'zh-CN': '变量更新方式',\n en: 'Variable update method',\n },\n 'panel.update.method.aiOutput': {\n 'zh-CN': '随 AI 输出',\n en: 'Alongside AI output',\n },\n 'panel.update.method.extraModel': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.update.method.unsupportedWarning': {\n 'zh-CN':\n '世界书 [{worldbooks}] 未适配额外模型解析,视为 [mvu_plot] 条目(只会发给剧情 AI、不会发给变量更新 AI)。',\n en: 'World book [{worldbooks}] is not adapted for extra-model parsing and will be treated as an [mvu_plot] entry (sent only to the story AI, not the variable-update AI).',\n },\n 'panel.modelSelect.ariaLabel': {\n 'zh-CN': '模型列表',\n en: 'Model list',\n },\n 'panel.modelSelect.chooseFromList': {\n 'zh-CN': '(从列表选择)',\n en: '(Choose from list)',\n },\n 'panel.modelSelect.loading': {\n 'zh-CN': '获取中…',\n en: 'Fetching…',\n },\n 'panel.modelSelect.fetch': {\n 'zh-CN': '获取模型列表',\n en: 'Fetch model list',\n },\n 'panel.modelSelect.empty': {\n 'zh-CN': '模型列表为空或获取失败',\n en: 'The model list is empty or could not be fetched',\n },\n 'panel.modelSelect.fetchTitle': {\n 'zh-CN': '[MVU]获取模型列表',\n en: '[MVU] Fetch model list',\n },\n 'panel.modelSelect.fetchFailureTitle': {\n 'zh-CN': '[MVU]获取模型列表失败',\n en: '[MVU] Failed to fetch model list',\n },\n 'panel.prompt.apiOptionsMigrated': {\n 'zh-CN': '破限/应答格式等配置 已迁移到 API 方案,与方案配置绑定',\n en: 'Jailbreak, response format, and related settings have moved to API profiles and are saved with each profile.',\n },\n 'panel.source.pi.auth.accountLogin': {\n 'zh-CN': '账号登录',\n en: 'Account sign-in',\n },\n 'panel.source.pi.contextWindowHelp': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。请按服务商说明填写;数值过小会使较长的请求无法发送。',\n en: 'The total length the model can read and generate, in tokens. Enter the limit given by your provider; setting it too low prevents longer requests from being sent.',\n },\n 'panel.prompt.section': {\n 'zh-CN': '请求内容',\n en: 'Request content',\n },\n 'panel.prompt.jailbreakStrategy': {\n 'zh-CN': '破限方案',\n en: 'Jailbreak strategy',\n },\n 'panel.prompt.jailbreak.builtin': {\n 'zh-CN': '使用内置破限',\n en: 'Use built-in jailbreak',\n },\n 'panel.prompt.jailbreak.currentPreset': {\n 'zh-CN': '使用当前预设',\n en: 'Use current preset',\n },\n 'panel.prompt.jailbreak.otherPreset': {\n 'zh-CN': '使用其他预设',\n en: 'Use another preset',\n },\n 'panel.prompt.targetPreset': {\n 'zh-CN': '目标预设',\n en: 'Target preset',\n },\n 'panel.prompt.noSavedPreset': {\n 'zh-CN': '未检测到可用的已保存预设',\n en: 'No available saved presets detected',\n },\n 'panel.prompt.randomHeader': {\n 'zh-CN': '随机头部',\n en: 'Randomized header',\n },\n 'panel.prompt.randomHeaderHelp': {\n 'zh-CN':\n 'Gemini 系模型会记录破限头部,因此需要在头部增加随机数。如果你使用的不是 Gemini 系模型,请关闭这个功能,以免缓存失效。',\n en: 'Gemini-family models may record the jailbreak header, so a random number is added to it. Disable this for non-Gemini models to avoid invalidating the prompt cache.',\n },\n 'panel.prompt.responseFormat': {\n 'zh-CN': '应答格式',\n en: 'Response format',\n },\n 'panel.prompt.response.chatMessage': {\n 'zh-CN': '聊天消息',\n en: 'Chat message',\n },\n 'panel.prompt.response.toolCall': {\n 'zh-CN': '工具调用',\n en: 'Tool call',\n },\n 'panel.prompt.response.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.prompt.response.structuredV4': {\n 'zh-CN': '格式化输出(v4 兼容)',\n en: 'Structured output (v4 compatible)',\n },\n 'panel.prompt.disableThinking': {\n 'zh-CN': '关闭 thinking',\n en: 'Disable thinking',\n },\n 'panel.prompt.disableThinkingHelp': {\n 'zh-CN': '关闭后可避免部分空回复问题。',\n en: 'Disabling thinking can prevent some empty responses.',\n },\n 'panel.prompt.disable': {\n 'zh-CN': '关闭',\n en: 'Disable',\n },\n 'panel.prompt.fakeStreaming': {\n 'zh-CN': '兼容假流式',\n en: 'Pseudo-streaming compatibility',\n },\n 'panel.prompt.fakeStreamingHelp': {\n 'zh-CN':\n '默认关闭,等待模型一次返回完整结果。若渠道要求流式请求,或等待时经常超时,可尝试勾选。开启后仍会等回复完整后再更新变量。OpenAI Codex 只支持流式传输,会始终使用流式。',\n en: 'Disabled by default: wait for the complete response. Enable this if your provider requires streaming or requests often time out while waiting. Variables are still updated after the full response arrives. OpenAI Codex requires streaming and always uses it.',\n },\n 'panel.prompt.whitelist': {\n 'zh-CN': '世界书条目白名单正则',\n en: 'World-book entry whitelist regex',\n },\n 'panel.prompt.whitelistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段只保留 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing retains only world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.whitelistPlaceholder': {\n 'zh-CN': '角色{or}地点 或 /角色{or}地点/i',\n en: 'character{or}location or /character{or}location/i',\n },\n 'panel.prompt.blacklist': {\n 'zh-CN': '世界书条目黑名单正则',\n en: 'World-book entry blacklist regex',\n },\n 'panel.prompt.blacklistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段会排除 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing excludes world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.blacklistPlaceholder': {\n 'zh-CN': '临时{or}禁用 或 /临时{or}禁用/i',\n en: 'temporary{or}disabled or /temporary{or}disabled/i',\n },\n 'panel.prompt.regexInvalid': {\n 'zh-CN': '正则无效:{error}',\n en: 'Invalid regex: {error}',\n },\n 'panel.prompt.filtered.title': {\n 'zh-CN': '上次分析被筛选的条目',\n en: 'Entries filtered from the last analysis',\n },\n 'panel.prompt.filtered.empty': {\n 'zh-CN': '上次分析没有被黑/白名单筛选掉的条目。',\n en: 'No entries were filtered by the whitelist or blacklist in the last analysis.',\n },\n 'panel.prompt.filtered.entrySource': {\n 'zh-CN': '条目来源',\n en: 'Entry source',\n },\n 'panel.prompt.filtered.worldBook': {\n 'zh-CN': '世界书',\n en: 'World book',\n },\n 'panel.prompt.filtered.reason': {\n 'zh-CN': '原因',\n en: 'Reason',\n },\n 'panel.prompt.filtered.configSource': {\n 'zh-CN': '配置来源',\n en: 'Configuration source',\n },\n 'panel.prompt.filtered.comment': {\n 'zh-CN': '条目备注',\n en: 'Comment',\n },\n 'panel.prompt.filtered.globalLore': {\n 'zh-CN': '全局世界书',\n en: 'Global world book',\n },\n 'panel.prompt.filtered.characterLore': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.prompt.filtered.chatLore': {\n 'zh-CN': '聊天世界书',\n en: 'Chat world book',\n },\n 'panel.prompt.filtered.personaLore': {\n 'zh-CN': '用户世界书',\n en: 'Persona world book',\n },\n 'panel.prompt.filtered.whitelistReason': {\n 'zh-CN': '白名单',\n en: 'Whitelist',\n },\n 'panel.prompt.filtered.blacklistReason': {\n 'zh-CN': '黑名单',\n en: 'Blacklist',\n },\n 'panel.prompt.filtered.globalConfig': {\n 'zh-CN': '用户全局配置',\n en: 'User global settings',\n },\n 'panel.prompt.filtered.characterConfig': {\n 'zh-CN': '角色卡配置',\n en: 'Character-card settings',\n },\n 'panel.prompt.toolCallUnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'工具调用'\",\n en: \"[MVU] Cannot use 'Tool call'\",\n },\n 'panel.prompt.toolCallUnsupported': {\n 'zh-CN': '当前 API 源不支持工具调用,请换用支持 tools 的渠道模型或改用其他应答格式',\n en: 'The current API source does not support tool calls. Choose a provider/model that supports tools, or use another response format.',\n },\n 'panel.prompt.structuredV4UnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'格式化输出(v4兼容)'\",\n en: \"[MVU] Cannot use 'Structured output (v4 compatible)'\",\n },\n 'panel.prompt.structuredV4RequiresCustom': {\n 'zh-CN': '格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同',\n en: 'Structured output (v4 compatible) requires a custom extra-model source and cannot use the current connection.',\n },\n 'panel.request.section': {\n 'zh-CN': '请求策略',\n en: 'Request strategy',\n },\n 'panel.request.method': {\n 'zh-CN': '请求方式',\n en: 'Request method',\n },\n 'panel.request.sequential': {\n 'zh-CN': '依次请求,失败后重试',\n en: 'Request sequentially and retry on failure',\n },\n 'panel.request.parallel': {\n 'zh-CN': '同时请求多次',\n en: 'Send multiple requests in parallel',\n },\n 'panel.request.onceThenParallel': {\n 'zh-CN': '先请求一次,失败后再同时请求多次',\n en: 'Request once, then retry in parallel on failure',\n },\n 'panel.request.count': {\n 'zh-CN': '请求次数',\n en: 'Number of requests',\n },\n 'panel.request.auto': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.request.autoHelp': {\n 'zh-CN':\n '关闭后,AI 回复完成时不会自动触发额外模型解析;你需要主动点击“重试额外模型解析”按钮,才会执行解析并添加状态栏占位符 <StatusPlaceHolderImpl/>。',\n en: 'When disabled, extra-model parsing is not triggered automatically after the AI finishes replying. Click “Retry extra-model parsing” to run it and add the status-bar placeholder <StatusPlaceHolderImpl/>.',\n },\n 'panel.request.minLength': {\n 'zh-CN': '最小请求字数',\n en: 'Minimum reply length',\n },\n 'panel.request.minLengthHelp': {\n 'zh-CN':\n 'AI 回复达到此字数才自动请求额外模型解析。按整条回复去除首尾空白后的字符数计算(包含标签,不是 token 数);0 表示不限制。若同时设置触发标签,两个条件都须满足。手动重试不受限制。',\n en: 'Automatically request extra-model parsing only when the AI reply reaches this many characters. Counts the entire reply, including tags, after trimming leading and trailing whitespace (not tokens). Set to 0 for no minimum. If a trigger tag is also set, both conditions must be met. Manual retries bypass these conditions.',\n },\n 'panel.request.triggerTag': {\n 'zh-CN': '触发标签',\n en: 'Trigger tag',\n },\n 'panel.request.triggerTagHelp': {\n 'zh-CN':\n 'AI 回复匹配此正则表达式才自动请求额外模型解析。可直接填写 </content>,或用 </(content|正文)> 匹配多个标签;也支持 /pattern/flags 格式,例如 /content/i 忽略大小写。默认区分大小写,特殊字符需用反斜杠转义。忽略配置首尾空白,留空表示不限制;正则无效时跳过自动请求。若同时设置最小请求字数,两个条件都须满足。手动重试不受限制。',\n en: 'Automatically request extra-model parsing only when the AI reply matches this regular expression. Enter </content> directly, or use </(content|正文)> to match either tag. The /pattern/flags format is also supported, such as /content/i for case-insensitive matching. Matching is case-sensitive by default; escape special characters with a backslash. Leading and trailing whitespace in this setting is ignored; leave empty for no restriction. Invalid regexes skip automatic requests. If a minimum reply length is also set, both conditions must be met. Manual retries bypass these conditions.',\n },\n 'panel.request.triggerTagInvalid': {\n 'zh-CN': '触发标签正则无效,将跳过自动请求:{error}',\n en: 'Invalid trigger-tag regex; automatic requests will be skipped: {error}',\n },\n 'panel.request.batchWarning': {\n 'zh-CN':\n '请升级酒馆助手到 4.4.3 或更高版本,否则批量请求功能可能让预设的“流式传输”设置失效',\n en: \"Upgrade Tavern Helper to version 4.4.3 or later. Otherwise, batch requests may override the preset's streaming setting.\",\n },\n 'panel.request.batchWarningTitle': {\n 'zh-CN': '[MVU]批量请求可能有问题',\n en: '[MVU] Batch requests may not work correctly',\n },\n 'panel.source.section': {\n 'zh-CN': '模型来源',\n en: 'Model source',\n },\n 'panel.source.sameAsConnection': {\n 'zh-CN': '与插头相同',\n en: 'Same as current connection',\n },\n 'panel.source.custom': {\n 'zh-CN': '自定义',\n en: 'Custom',\n },\n 'panel.source.more': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.profile.section': {\n 'zh-CN': 'API 方案',\n en: 'API profiles',\n },\n 'panel.source.profile.current': {\n 'zh-CN': '当前方案',\n en: 'Current profile',\n },\n 'panel.source.profile.ariaLabel': {\n 'zh-CN': 'API 方案',\n en: 'API profile',\n },\n 'panel.source.profile.manual': {\n 'zh-CN': '(手动编辑,未绑定方案)',\n en: '(Manual settings, not bound to a profile)',\n },\n 'panel.source.profile.newName': {\n 'zh-CN': '新方案名称',\n en: 'New profile name',\n },\n 'panel.source.profile.save': {\n 'zh-CN': '保存当前方案',\n en: 'Save current profile',\n },\n 'panel.source.profile.saveAs': {\n 'zh-CN': '另存为新方案',\n en: 'Save as new profile',\n },\n 'panel.source.profile.delete': {\n 'zh-CN': '删除当前方案',\n en: 'Delete current profile',\n },\n 'panel.source.profile.backendCustom': {\n 'zh-CN': '自定义 API',\n en: 'Custom API',\n },\n 'panel.source.profile.backendPi': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.apiAddress': {\n 'zh-CN': 'API 地址',\n en: 'API address',\n },\n 'panel.source.apiKey': {\n 'zh-CN': 'API 密钥',\n en: 'API key',\n },\n 'panel.source.apiKeyPlaceholder': {\n 'zh-CN': '留空表示无需密钥',\n en: 'Leave blank when no key is required',\n },\n 'panel.source.modelName': {\n 'zh-CN': '模型名称',\n en: 'Model name',\n },\n // “更多”来源的连接、协议能力和认证界面文案;底层 Pi 标识不作为产品名称展示。\n 'panel.source.pi.provider': {\n 'zh-CN': '来源',\n en: 'Provider',\n },\n 'panel.source.pi.endpoint': {\n 'zh-CN': 'API 基础地址',\n en: 'API base URL',\n },\n 'panel.source.pi.endpointDefault': {\n 'zh-CN': '留空使用默认地址:{endpoint}',\n en: 'Leave blank to use the default: {endpoint}',\n },\n 'panel.source.pi.endpointPlaceholder': {\n 'zh-CN': '请输入 API 基础地址',\n en: 'Enter an API base URL',\n },\n 'panel.source.pi.endpointHelp': {\n 'zh-CN':\n '填写服务商提供的 API 地址,留空使用默认地址。也可以直接粘贴完整的聊天或 Responses 接口地址,MVU 会自动处理末尾路径。',\n en: 'Enter the API URL supplied by your provider, or leave it blank to use the default. You can also paste a full chat or Responses URL; MVU adjusts its ending automatically.',\n },\n 'panel.source.pi.proxy.use': {\n 'zh-CN': '使用 Proxy',\n en: 'Use Proxy',\n },\n 'panel.source.pi.proxy.help': {\n 'zh-CN':\n '如果获取模型列表或生成请求因 CORS(浏览器跨域限制)失败,可以尝试勾选,通过 SillyTavern 转发请求。需要先在 SillyTavern 中开启 Proxy。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'If fetching models or generating a response fails because of CORS (browser cross-origin restrictions), try enabling this to forward requests through SillyTavern. Proxy must also be enabled in SillyTavern. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.proxy.notEnabled': {\n 'zh-CN': '当前来源需要 Proxy,但 SillyTavern 的 Proxy 未开启或无法连接。',\n en: 'This provider needs Proxy, but SillyTavern Proxy is disabled or unavailable.',\n },\n 'panel.source.pi.proxy.notEnabledHelp': {\n 'zh-CN':\n '打开 SillyTavern 的 config.yaml,将 enableCorsProxy 设为 true,保存后重启 SillyTavern。如果酒馆由他人托管,请联系管理员开启。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'Open SillyTavern config.yaml, set enableCorsProxy to true, save, and restart SillyTavern. If someone else hosts your server, ask its administrator to enable it. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.model': {\n 'zh-CN': '模型',\n en: 'Model',\n },\n 'panel.source.pi.catalogModel': {\n 'zh-CN': '模型目录',\n en: 'Model catalog',\n },\n 'panel.source.pi.customModel': {\n 'zh-CN': '(手动填写模型)',\n en: '(Enter a model manually)',\n },\n 'panel.source.pi.api.openaiResponses': {\n 'zh-CN': 'OpenAI Responses',\n en: 'OpenAI Responses',\n },\n 'panel.source.pi.api.openaiCompletions': {\n 'zh-CN': 'OpenAI Chat Completions',\n en: 'OpenAI Chat Completions',\n },\n 'panel.source.pi.api.openaiCodexResponses': {\n 'zh-CN': 'OpenAI Codex Responses',\n en: 'OpenAI Codex Responses',\n },\n 'panel.source.pi.api.anthropicMessages': {\n 'zh-CN': 'Anthropic Messages',\n en: 'Anthropic Messages',\n },\n 'panel.source.pi.api.googleGenerativeAi': {\n 'zh-CN': 'Google Generative AI',\n en: 'Google Generative AI',\n },\n 'panel.source.pi.api.mistralConversations': {\n 'zh-CN': 'Mistral Conversations',\n en: 'Mistral Conversations',\n },\n 'panel.source.pi.auth.apiKey': {\n 'zh-CN': 'API Key',\n en: 'API key',\n },\n 'panel.source.pi.capabilities': {\n 'zh-CN': '可尝试的功能:{capabilities}。具体是否可用,以你使用的模型和服务商为准。',\n en: 'Features you can try: {capabilities}. Availability depends on your model and provider.',\n },\n 'panel.source.pi.capability.tools': {\n 'zh-CN': '工具调用',\n en: 'tools',\n },\n 'panel.source.pi.capability.images': {\n 'zh-CN': '读取图片',\n en: 'Read images',\n },\n 'panel.source.pi.capability.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.source.pi.thinkingLevel': {\n 'zh-CN': '思考等级',\n en: 'Thinking level',\n },\n 'panel.source.pi.thinkingLevelHelp': {\n 'zh-CN':\n '默认保持现有请求行为;其他等级按模型能力映射,部分模型无法完全关闭思考。未知模型按协议转换所选等级;OpenAI/Mistral 的极高、最高会原样尝试,Google/预算式思考仍按协议映射。Anthropic 开启思考时不支持强制工具调用,旧模型至少需要 2048 回复 token。自定义请求体覆盖仍优先。',\n en: 'Default preserves existing requests. Other levels are mapped to model capabilities; some models cannot fully disable thinking. Unknown models use protocol-specific mappings: OpenAI/Mistral attempt xhigh/max unchanged, while Google and budget-based thinking use protocol mappings. Anthropic thinking cannot force tool calls; older models need at least 2048 reply tokens. Custom body overrides take precedence.',\n },\n 'panel.source.pi.thinking.default': {\n 'zh-CN': '默认(保持原行为)',\n en: 'Default (unchanged behavior)',\n },\n 'panel.source.pi.thinking.off': {\n 'zh-CN': '关闭 / 最低',\n en: 'Off / minimum',\n },\n 'panel.source.pi.thinking.minimal': {\n 'zh-CN': '极低',\n en: 'Minimal',\n },\n 'panel.source.pi.thinking.low': {\n 'zh-CN': '低',\n en: 'Low',\n },\n 'panel.source.pi.thinking.medium': {\n 'zh-CN': '中',\n en: 'Medium',\n },\n 'panel.source.pi.thinking.high': {\n 'zh-CN': '高',\n en: 'High',\n },\n 'panel.source.pi.thinking.xhigh': {\n 'zh-CN': '极高',\n en: 'Extra high',\n },\n 'panel.source.pi.thinking.max': {\n 'zh-CN': '最高',\n en: 'Maximum',\n },\n 'panel.source.pi.contextWindow': {\n 'zh-CN': '上下文窗口',\n en: 'Context window',\n },\n 'panel.source.pi.contextWindowCatalog': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。已自动填入 {value};如果服务商给出的限制不同,可手动修改,清空后恢复自动值。',\n en: 'The total length the model can read and generate, in tokens. Automatically set to {value}. Edit it if your provider gives a different limit; clear it to restore the automatic value.',\n },\n 'panel.source.pi.contextWindowOverride': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。当前使用你填写的值;清空后会尝试自动填写已知模型的限制。',\n en: 'The total length the model can read and generate, in tokens. Using your entered value; clear it to try the known model limit.',\n },\n 'panel.source.pi.contextWindowRequired': {\n 'zh-CN': '请输入正整数上下文窗口;模型目录未提供该值时必须手动填写。',\n en: 'Enter a positive integer context window. It is required when the model catalog has no value.',\n },\n 'panel.source.pi.error.unknownProvider': {\n 'zh-CN': '“更多”中的未知来源“{provider}”;请重新选择来源。',\n en: 'Unknown provider \"{provider}\" under More. Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedApi': {\n 'zh-CN': '已保存的来源“{provider}”不支持接口“{api}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support API \"{api}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedAuth': {\n 'zh-CN': '已保存的来源“{provider}”不支持认证方式“{auth}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support authentication type \"{auth}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedEndpoint': {\n 'zh-CN': '当前来源不支持自定义 API 地址,请重新选择来源或切换 API 方案。',\n en: 'This provider does not support a custom API URL. Select a provider again or switch API profiles.',\n },\n 'panel.source.pi.featureDisabled': {\n 'zh-CN':\n '此构建已关闭“更多”模型来源。已保存的“更多”配置仍会保留;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Saved More-source settings are preserved; select Same as current connection or Custom.',\n },\n 'panel.source.pi.maxTokensPositive': {\n 'zh-CN': '最大回复 token 必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'panel.source.pi.maxTokensExceedContext': {\n 'zh-CN': '最大回复 token 不能大于上下文窗口。',\n en: 'Maximum response tokens cannot exceed the context window.',\n },\n 'panel.source.pi.customHeaders': {\n 'zh-CN': '自定义请求头',\n en: 'Custom request headers',\n },\n 'panel.source.pi.customHeadersHelp': {\n 'zh-CN':\n '仅在服务商要求额外请求头时填写,格式为 YAML 或 JSON,例如 X-App: MVU。留空即可;API 密钥请填在上方的密钥栏。',\n en: 'Fill this in only if your provider requires extra headers. Use YAML or JSON, for example X-App: MVU. Otherwise leave it blank. Enter your API key in the key field above.',\n },\n 'panel.source.pi.customOverridesSwitchHelp': {\n 'zh-CN':\n '下方的自定义请求头、附加字段和排除字段会随 API 方案保存。手动切换来源或 API 地址时会清空这三项;切换方案会恢复该方案保存的内容。',\n en: 'Custom headers, additional fields, and excluded fields below are saved with the API profile. Manually changing the provider or API URL clears these three fields; switching profiles restores their saved values.',\n },\n 'panel.source.pi.customIncludeBody': {\n 'zh-CN': '请求体附加字段',\n en: 'Additional request body fields',\n },\n 'panel.source.pi.customIncludeBodyHelp': {\n 'zh-CN':\n '服务商要求额外参数时在这里填写,使用 YAML 或 JSON,例如 seed: 123。同名参数会采用这里的值。Google 参数请放在 config 下。不需要时留空;密钥、模型和流式开关请使用对应选项。',\n en: 'Add parameters required by your provider as YAML or JSON, for example seed: 123. These values replace matching parameters. Put Google parameters under config. Leave blank when unnecessary; use the dedicated controls for keys, models, and streaming.',\n },\n 'panel.source.pi.customExcludeBody': {\n 'zh-CN': '请求体排除字段',\n en: 'Excluded request body fields',\n },\n 'panel.source.pi.customExcludeBodyHelp': {\n 'zh-CN':\n '如果服务商提示某个参数不被支持,可在这里填写参数名,使请求不再携带它。多个名称用换行或逗号分隔,也支持 YAML 列表。Google 参数写为 config.<参数名>。不需要时留空。',\n en: 'If your provider rejects a parameter, enter its name here to leave it out of requests. Separate names with newlines or commas, or use a YAML list. For Google, use config.<parameter>. Otherwise leave blank.',\n },\n 'panel.source.pi.clearCustomField': {\n 'zh-CN': '清空',\n en: 'Clear',\n },\n 'panel.source.pi.oauth.section': {\n 'zh-CN': 'OAuth 登录',\n en: 'OAuth login',\n },\n 'panel.source.pi.oauth.status': {\n 'zh-CN': '状态',\n en: 'Status',\n },\n 'panel.source.pi.oauth.checking': {\n 'zh-CN': '检查中…',\n en: 'Checking…',\n },\n 'panel.source.pi.oauth.loggedIn': {\n 'zh-CN': '已登录',\n en: 'Signed in',\n },\n 'panel.source.pi.oauth.loggedOut': {\n 'zh-CN': '未登录',\n en: 'Not signed in',\n },\n 'panel.source.pi.oauth.expiresAt': {\n 'zh-CN': '凭据有效期:{time}',\n en: 'Credential expiry: {time}',\n },\n 'panel.source.pi.oauth.login': {\n 'zh-CN': '登录',\n en: 'Sign in',\n },\n 'panel.source.pi.oauth.refresh': {\n 'zh-CN': '刷新凭证',\n en: 'Refresh credentials',\n },\n 'panel.source.pi.oauth.refreshing': {\n 'zh-CN': '正在刷新凭证…',\n en: 'Refreshing credentials…',\n },\n 'panel.source.pi.oauth.refreshSucceeded': {\n 'zh-CN': '凭证已刷新。',\n en: 'Credentials refreshed.',\n },\n 'panel.source.pi.oauth.cancel': {\n 'zh-CN': '取消登录',\n en: 'Cancel sign-in',\n },\n 'panel.source.pi.oauth.logout': {\n 'zh-CN': '登出',\n en: 'Sign out',\n },\n 'panel.source.pi.oauth.authorizationUrl': {\n 'zh-CN': '授权链接',\n en: 'Authorization URL',\n },\n 'panel.source.pi.oauth.openAuthorization': {\n 'zh-CN': '打开授权页',\n en: 'Open authorization page',\n },\n 'panel.source.pi.oauth.copyAuthorization': {\n 'zh-CN': '复制授权链接',\n en: 'Copy authorization URL',\n },\n 'panel.source.pi.oauth.copySucceeded': {\n 'zh-CN': '已复制授权链接',\n en: 'Authorization URL copied',\n },\n 'panel.source.pi.oauth.copyFailed': {\n 'zh-CN': '无法复制授权链接,请手动选择并复制。',\n en: 'Could not copy the authorization URL. Select and copy it manually.',\n },\n 'panel.source.pi.oauth.callbackUrl': {\n 'zh-CN': '回调链接',\n en: 'Callback URL',\n },\n 'panel.source.pi.oauth.callbackHelp': {\n 'zh-CN':\n '完成授权后,如果跳转到的页面打不开,不影响登录。请复制浏览器地址栏中以 127.0.0.1 或 localhost 开头的完整链接,粘贴到这里,再点击“完成登录”。',\n en: 'After authorization, the redirected page may fail to open; you can still finish signing in. Copy the full URL starting with 127.0.0.1 or localhost from the address bar, paste it here, and click Complete sign-in.',\n },\n 'panel.source.pi.oauth.complete': {\n 'zh-CN': '完成登录',\n en: 'Complete sign-in',\n },\n 'panel.source.pi.oauth.preparing': {\n 'zh-CN': '正在准备安全授权链接…',\n en: 'Preparing a secure authorization URL…',\n },\n 'panel.source.pi.oauth.waitingCallback': {\n 'zh-CN': '请在授权页完成登录,然后粘贴完整回调链接。',\n en: 'Finish signing in on the authorization page, then paste the complete callback URL.',\n },\n 'panel.source.pi.oauth.exchanging': {\n 'zh-CN': '正在验证回调并交换凭据…',\n en: 'Validating the callback and exchanging credentials…',\n },\n 'panel.source.pi.oauth.loginSucceeded': {\n 'zh-CN': 'OAuth 登录成功。',\n en: 'OAuth sign-in succeeded.',\n },\n 'panel.source.pi.oauth.logoutSucceeded': {\n 'zh-CN': '已登出。',\n en: 'Signed out.',\n },\n 'panel.source.pi.oauth.logoutConfirm': {\n 'zh-CN': '确定在当前配置中登出 {provider} 吗?之后可以登录其他账号。',\n en: 'Sign out of {provider} for the current configuration? You can then sign in with another account.',\n },\n 'panel.source.pi.oauth.failureTitle': {\n 'zh-CN': '[MVU]OAuth 操作失败',\n en: '[MVU] OAuth operation failed',\n },\n 'panel.source.advanced': {\n 'zh-CN': '高级参数',\n en: 'Advanced parameters',\n },\n 'panel.source.unsupportedAdvanced': {\n 'zh-CN': '⚠️酒馆助手版本过低,不支持以下配置',\n en: '⚠️ Your Tavern Helper version is too old to support the settings below',\n },\n 'panel.source.maxTokens': {\n 'zh-CN': '最大回复 token',\n en: 'Maximum response tokens',\n },\n 'panel.source.chatHistory': {\n 'zh-CN': '聊天历史条数',\n en: 'Chat history entries',\n },\n 'panel.source.temperature': {\n 'zh-CN': '温度',\n en: 'Temperature',\n },\n 'panel.source.frequencyPenalty': {\n 'zh-CN': '频率惩罚',\n en: 'Frequency penalty',\n },\n 'panel.source.presencePenalty': {\n 'zh-CN': '存在惩罚',\n en: 'Presence penalty',\n },\n 'panel.source.topP': {\n 'zh-CN': 'Top P',\n en: 'Top P',\n },\n 'panel.source.topK': {\n 'zh-CN': 'Top K',\n en: 'Top K',\n },\n 'panel.source.switchDirty': {\n 'zh-CN': '当前方案有未保存的修改,切换将丢弃这些修改。是否继续?',\n en: 'The current profile has unsaved changes. Switching will discard them. Continue?',\n },\n 'panel.source.continue': {\n 'zh-CN': '继续',\n en: 'Continue',\n },\n 'panel.source.discardChanges': {\n 'zh-CN': '丢弃修改',\n en: 'Discard changes',\n },\n 'panel.source.delete': {\n 'zh-CN': '删除',\n en: 'Delete',\n },\n 'panel.source.deleteDirty': {\n 'zh-CN': '当前方案“{name}”有未保存的修改,删除将丢弃这些修改。是否继续?',\n en: 'Profile “{name}” has unsaved changes. Deleting it will discard them. Continue?',\n },\n 'panel.source.deleteConfirm': {\n 'zh-CN': '确定删除 API 方案“{name}”吗?此操作不可撤销。',\n en: 'Delete API profile “{name}”? This action cannot be undone.',\n },\n 'panel.source.profileSaved': {\n 'zh-CN': '已保存 API 方案“{name}”',\n en: 'Saved API profile “{name}”',\n },\n 'panel.source.profileSavedAs': {\n 'zh-CN': '已另存为 API 方案“{name}”',\n en: 'Saved as API profile “{name}”',\n },\n 'panel.source.profileDeleted': {\n 'zh-CN': '已删除 API 方案“{name}”',\n en: 'Deleted API profile “{name}”',\n },\n 'panel.source.enterProfileName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a new profile name first',\n },\n 'panel.source.keepTwoProfiles': {\n 'zh-CN': '至少保留两个 API 方案时才可删除',\n en: 'At least two API profiles are required before one can be deleted',\n },\n 'panel.source.switchFailureTitle': {\n 'zh-CN': '[MVU]切换 API 方案失败',\n en: '[MVU] Failed to switch API profile',\n },\n 'panel.source.saveFailureTitle': {\n 'zh-CN': '[MVU]保存 API 方案失败',\n en: '[MVU] Failed to save API profile',\n },\n 'panel.source.deleteFailureTitle': {\n 'zh-CN': '[MVU]删除 API 方案失败',\n en: '[MVU] Failed to delete API profile',\n },\n 'panel.character.titleActive': {\n 'zh-CN': '当前角色卡配置(覆盖中)',\n en: 'Current character-card settings (overriding)',\n },\n 'panel.character.titleInactive': {\n 'zh-CN': '当前角色卡配置(未启用)',\n en: 'Current character-card settings (not enabled)',\n },\n 'panel.character.worldBook': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.character.reading': {\n 'zh-CN': '正在读取…',\n en: 'Reading…',\n },\n 'panel.character.unbound': {\n 'zh-CN': '未绑定',\n en: 'Not bound',\n },\n 'panel.character.inherit': {\n 'zh-CN': '跟随用户配置',\n en: 'Inherit user settings',\n },\n 'panel.character.extraModelGroup': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.character.autoRequest': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.character.whitelist': {\n 'zh-CN': '角色卡世界书条目白名单正则',\n en: 'Character-card world-book entry whitelist regex',\n },\n 'panel.character.blacklist': {\n 'zh-CN': '角色卡世界书条目黑名单正则',\n en: 'Character-card world-book entry blacklist regex',\n },\n 'panel.character.regexInvalid': {\n 'zh-CN': '角色卡配置正则无效:{error}',\n en: 'Invalid character-card regex: {error}',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const runtimeMessages = defineMessages({\n 'runtime.common.mvuTitle': {\n 'zh-CN': '[MVU]',\n en: '[MVU]',\n },\n 'runtime.common.errorCause': {\n 'zh-CN': '错误详情:{cause}',\n en: 'Error details: {cause}',\n },\n 'runtime.common.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n\n 'runtime.main.chatReinitializeFailedLog': {\n 'zh-CN': '[MVU]切换聊天后重新初始化失败',\n en: '[MVU] Failed to reinitialize after switching chats',\n },\n 'runtime.main.reinitializeFailedTitle': {\n 'zh-CN': '[MVU]重新初始化失败',\n en: '[MVU] Reinitialization failed',\n },\n 'runtime.main.minimumVersionRequired': {\n 'zh-CN': 'MVU 变量框架需要酒馆助手版本不低于 {version}',\n en: 'MVU Variable Framework requires Tavern Helper {version} or later',\n },\n 'runtime.main.versionIncompatibleTitle': {\n 'zh-CN': '版本不兼容',\n en: 'Incompatible version',\n },\n\n 'runtime.parseString.invalidFormat': {\n 'zh-CN': '要解析的字符串不是有效的 YAML/JSON/JSON5 格式',\n en: 'The input is not valid YAML, JSON, or JSON5',\n },\n 'runtime.parseString.content': {\n 'zh-CN': '字符串内容',\n en: 'Input',\n },\n 'runtime.parseString.yamlError': {\n 'zh-CN': 'YAML 错误信息',\n en: 'YAML error',\n },\n 'runtime.parseString.json5Error': {\n 'zh-CN': 'JSON5 错误信息',\n en: 'JSON5 error',\n },\n 'runtime.parseString.jsonError': {\n 'zh-CN': 'JSON 错误信息',\n en: 'JSON error',\n },\n\n 'runtime.button.initVarDataNotFound': {\n 'zh-CN': '没有找到 InitVar 数据',\n en: 'No InitVar data was found',\n },\n 'runtime.button.initVarDataLoadFailed': {\n 'zh-CN': '加载 InitVar 数据失败:',\n en: 'Failed to load InitVar data',\n },\n 'runtime.button.messageNotFound': {\n 'zh-CN': '没有找到消息',\n en: 'No message was found',\n },\n 'runtime.button.initVarDescriptionsUpdated': {\n 'zh-CN': 'InitVar描述已更新',\n en: 'InitVar descriptions updated',\n },\n 'runtime.button.snapshotPrompt': {\n 'zh-CN':\n '

设置快照楼层可以避免指定的楼层在清理操作中被移除变量信息

请填写要保留变量信息的楼层(如 10 为第 10 层)
后续楼层的重演将可以从这一层开始',\n en: '

Marking a floor as a snapshot prevents its variable data from being removed during cleanup.

Enter the floor whose variable data should be retained (for example, 10 for floor 10).
Later floors can then be replayed starting from this floor.',\n },\n 'runtime.button.invalidFloorInput': {\n 'zh-CN': '请输入有效的楼层数,你输入的是“{value}”',\n en: 'Enter a valid floor number. You entered “{value}”.',\n },\n 'runtime.button.invalidFloor': {\n 'zh-CN': '无效的楼层“{value}”',\n en: 'Invalid floor “{value}”.',\n },\n 'runtime.button.snapshotConfigureFailedTitle': {\n 'zh-CN': '[MVU]配置楼层快照失败',\n en: '[MVU] Failed to configure floor snapshot',\n },\n 'runtime.button.snapshotConfigureTitle': {\n 'zh-CN': '[MVU]配置楼层快照',\n en: '[MVU] Configure floor snapshot',\n },\n 'runtime.button.snapshotConfigured': {\n 'zh-CN': '已将 {floor} 层配置为快照楼层',\n en: 'Floor {floor} is now a snapshot floor',\n },\n 'runtime.button.replayPrompt': {\n 'zh-CN':\n '

当变量更新出现 required/extensible 相关问题时,可以尝试从过去的楼层重演

请填写要进行重演的楼层(如 10 为第 10 层,-1 为最新楼层)
也就是出现问题的楼层',\n en: '

If variable updates have required/extensible issues, replaying from an earlier floor may help.

Enter the floor to replay (for example, 10 for floor 10, or -1 for the latest floor).
This should be the floor where the problem occurred.',\n },\n 'runtime.button.replayFailedTitle': {\n 'zh-CN': '[MVU]楼层重演失败',\n en: '[MVU] Floor replay failed',\n },\n 'runtime.button.replayNoAvailableFloor': {\n 'zh-CN': '无法找到可以进行重演的楼层',\n en: 'No floor with replayable variable data was found',\n },\n 'runtime.button.replayStartPrompt': {\n 'zh-CN': '请填写从哪个楼层开始重演,找到最近的支持重演楼层为 [{floor}]',\n en: 'Enter the floor from which to start replaying. The nearest replayable floor is [{floor}].',\n },\n 'runtime.button.replayNeedsVariables': {\n 'zh-CN': '请输入含变量信息的楼层,你输入的是“{value}”',\n en: 'Enter a floor that contains variable data. You entered “{value}”.',\n },\n 'runtime.button.replayProgress': {\n 'zh-CN': '处理变量中({processed} / {total})',\n en: 'Processing variables ({processed} / {total})',\n },\n 'runtime.button.replayingFloorLog': {\n 'zh-CN': '正在重演 {index},内容:{content}',\n en: 'Replaying {index}. Content: {content}',\n },\n 'runtime.button.replayTitle': {\n 'zh-CN': '[MVU]楼层重演',\n en: '[MVU] Floor replay',\n },\n 'runtime.button.replayCompleted': {\n 'zh-CN': '已将 {floor} 层变量状态重演完毕,共重演 {count} 楼',\n en: 'Variable state replay for floor {floor} is complete ({count} floors replayed)',\n },\n 'runtime.button.extraModelNotEnabled': {\n 'zh-CN': '当前配置没有启用额外模型解析,不需要进行此操作',\n en: 'Extra-model parsing is not enabled by the current configuration',\n },\n 'runtime.button.extraModelToolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或配置指定的 LLM 不支持工具调用,请调整额外模型解析设置',\n en: 'The current TavernHelper version or configured LLM does not support tool calling. Adjust the extra-model parsing settings.',\n },\n 'runtime.button.extraModelUnsupportedByCard': {\n 'zh-CN': '当前角色卡不支持额外模型解析,无法进行此操作',\n en: 'The current character card does not support extra-model parsing',\n },\n 'runtime.button.extraModelRetryTitle': {\n 'zh-CN': '[MVU]重试额外模型解析',\n en: '[MVU] Retry extra-model parsing',\n },\n 'runtime.button.extraModelParsingCompleted': {\n 'zh-CN': '解析完成',\n en: 'Parsing completed',\n },\n 'runtime.incrementalRepair.title': {\n 'zh-CN': '[MVU]增量校正变量',\n en: '[MVU] Incremental variable repair',\n },\n 'runtime.incrementalRepair.alreadyRunning': {\n 'zh-CN': '已有一项增量校正正在运行',\n en: 'An incremental repair is already running',\n },\n 'runtime.incrementalRepair.noUsableFloor': {\n 'zh-CN': '最新楼层或上一楼没有可用的 MVU 状态,无法进行增量校正',\n en: 'The latest or previous floor has no usable MVU state',\n },\n 'runtime.incrementalRepair.directionPrompt': {\n 'zh-CN':\n '可选:补充本次增量校正方向;直接确认留空则自动审计。\\n例如:核对生命值归零后的即时后果,或补齐本轮明确获得的物品。',\n en: 'Optional: add a direction for this incremental repair. Confirm with an empty input for automatic audit.\\nFor example: check the immediate zero-HP aftermath or add an item clearly obtained this floor.',\n },\n 'runtime.incrementalRepair.requestFailed': {\n 'zh-CN': '增量校正请求失败或已被中止,未修改变量',\n en: 'The incremental repair failed or was cancelled; variables were not changed',\n },\n 'runtime.incrementalRepair.replayFailed': {\n 'zh-CN': '整楼重放失败,未提交校正:\\n{detail}',\n en: 'Full-floor replay failed. No repair was committed:\\n{detail}',\n },\n 'runtime.incrementalRepair.sourceChanged': {\n 'zh-CN': '等待期间聊天、楼层、回复版本或变量已经变化,本次结果已作废',\n en: 'The chat, floor, reply version, or variables changed while waiting; this result was discarded',\n },\n 'runtime.incrementalRepair.noEffectiveChanges': {\n 'zh-CN': '校正补丁没有产生实际变化,未写入正文或变量',\n en: 'The repair patch made no effective changes and was not written',\n },\n 'runtime.incrementalRepair.previewTitle': {\n 'zh-CN': '增量校正预览',\n en: 'Incremental repair preview',\n },\n 'runtime.incrementalRepair.previewDescription': {\n 'zh-CN':\n '模型提出 {count} 项修正。确认后合并本楼更新块,并从上一楼变量完整重算本楼,统一保存正文与变量。',\n en: 'The model proposed {count} repairs. The merged floor will be replayed from the preceding variables, then saved together with its resulting state.',\n },\n 'runtime.incrementalRepair.showRawPatch': {\n 'zh-CN': '查看原始补丁',\n en: 'Show raw patch',\n },\n 'runtime.incrementalRepair.applyButton': {\n 'zh-CN': '应用修正',\n en: 'Apply repairs',\n },\n 'runtime.incrementalRepair.cancelButton': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n 'runtime.incrementalRepair.appliedClickToUndo': {\n 'zh-CN': '增量校正已应用并写回正文;点击此通知可撤销',\n en: 'Incremental repair applied and written back; click this notice to undo',\n },\n 'runtime.incrementalRepair.undoStateChanged': {\n 'zh-CN': '变量或回复在校正后又发生了变化,为避免覆盖新进度,不能直接撤销',\n en: 'Variables or the reply changed after repair; undo was blocked to protect newer progress',\n },\n 'runtime.incrementalRepair.undone': {\n 'zh-CN': '已撤销此次增量校正',\n en: 'The incremental repair was undone',\n },\n 'runtime.button.cleanupPrompt': {\n 'zh-CN':\n '

清除旧楼层变量信息以减小聊天文件大小,避免手机崩溃

请填写要保留变量信息的楼层数(如 10 为保留最后 10 层,每 [{interval}] 层保留一层作为快照)
注意:你需要通过重演才能回退游玩到未保留变量信息的楼层',\n en: '

Remove variable data from old floors to reduce chat file size and help prevent mobile crashes.

Enter how many recent floors to retain (for example, 10 retains the latest 10 floors, with one snapshot every [{interval}] floors).
Note: you must replay variables before returning to floors whose variable data was removed.',\n },\n 'runtime.button.cleanupFailedTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量失败',\n en: '[MVU] Failed to clean old floor variables',\n },\n 'runtime.button.cleanupSucceededTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量成功',\n en: '[MVU] Old floor variables cleaned',\n },\n 'runtime.button.cleanupCompleted': {\n 'zh-CN': '已清理旧变量,保留了最后 {depth} 层的变量',\n en: 'Old variables were cleaned; variables for the latest {depth} floors were retained',\n },\n 'runtime.button.snapshotMarkedLog': {\n 'zh-CN': '将 [{floor}] 层作为快照楼层',\n en: 'Marked floor [{floor}] as a snapshot floor',\n },\n\n 'runtime.characterOverride.worldbookReadFailed': {\n 'zh-CN': '无法读取角色世界书“{worldbook}”',\n en: 'Unable to read character lorebook “{worldbook}”',\n },\n 'runtime.characterOverride.contentMustBeObject': {\n 'zh-CN': '配置正文必须是 JSON 对象',\n en: 'The settings content must be a JSON object',\n },\n 'runtime.characterOverride.conflictPrompt': {\n 'zh-CN':\n '角色世界书“{worldbook}”中的 {entry} 已被其他来源修改。是否使用待保存的角色卡配置覆盖它?',\n en: '{entry} in character lorebook “{worldbook}” was modified by another source. Overwrite it with the pending character-card configuration?',\n },\n 'runtime.characterOverride.overwriteButton': {\n 'zh-CN': '覆盖',\n en: 'Overwrite',\n },\n 'runtime.characterOverride.loadLatestButton': {\n 'zh-CN': '加载最新配置',\n en: 'Load latest configuration',\n },\n 'runtime.characterOverride.bindingReadFailedLog': {\n 'zh-CN': '[MVU]读取角色世界书绑定失败',\n en: '[MVU] Failed to read the character lorebook binding',\n },\n 'runtime.characterOverride.readFailedLog': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read the character-card configuration',\n },\n 'runtime.characterOverride.readFailedTitle': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read character-card configuration',\n },\n 'runtime.characterOverride.stopWithUnsavedChangesLog': {\n 'zh-CN': '[MVU]停止角色卡配置控制器时仍未能保存“{worldbook}”中的修改',\n en: '[MVU] Changes in “{worldbook}” were still unsaved when the character-card configuration controller stopped',\n },\n 'runtime.characterOverride.reloadFailedLog': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload the character-card configuration',\n },\n 'runtime.characterOverride.reloadFailedTitle': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload character-card configuration',\n },\n 'runtime.characterOverride.duplicateEntries': {\n 'zh-CN':\n '角色世界书“{worldbook}”中存在 {count} 个关闭的 {entry} 条目,仅使用扫描到的第一个。',\n en: 'Character lorebook “{worldbook}” contains {count} disabled {entry} entries. Only the first scanned entry will be used.',\n },\n 'runtime.characterOverride.duplicateEntriesTitle': {\n 'zh-CN': '[MVU]角色卡配置重复',\n en: '[MVU] Duplicate character-card configuration',\n },\n 'runtime.characterOverride.invalidWorldbookData': {\n 'zh-CN': '角色世界书“{worldbook}”的数据格式无效',\n en: 'Character lorebook “{worldbook}” has an invalid data format',\n },\n 'runtime.characterOverride.invalidConfigurationLog': {\n 'zh-CN': '[MVU]角色世界书“{worldbook}”的 {entry} 配置无效',\n en: '[MVU] The {entry} configuration in character lorebook “{worldbook}” is invalid',\n },\n 'runtime.characterOverride.invalidConfiguration': {\n 'zh-CN': '角色世界书“{worldbook}”中的配置无效,已按不存在处理:{cause}',\n en: 'The configuration in character lorebook “{worldbook}” is invalid and will be treated as absent: {cause}',\n },\n 'runtime.characterOverride.invalidConfigurationTitle': {\n 'zh-CN': '[MVU]角色卡配置无效',\n en: '[MVU] Invalid character-card configuration',\n },\n 'runtime.characterOverride.saveFailedLog': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save the character-card configuration',\n },\n 'runtime.characterOverride.saveFailedTitle': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save character-card configuration',\n },\n\n 'runtime.cleanup.title': {\n 'zh-CN': '[MVU]自动清理',\n en: '[MVU] Automatic cleanup',\n },\n 'runtime.cleanup.legacyPrompt': {\n 'zh-CN':\n '检测到可以清理本聊天文件中的旧变量以减小文件体积,是否清理?(备份会消耗较多内存,手机上建议关闭其他后台应用后进行,或在计算机上备份)',\n en: 'Old variables can be removed from this chat to reduce its file size. Clean them now? (Creating a backup uses considerable memory; on mobile, close other background apps first or create the backup on a computer.)',\n },\n 'runtime.cleanup.cleanOnlyButton': {\n 'zh-CN': '仅清理',\n en: 'Clean only',\n },\n 'runtime.cleanup.doNotRemindButton': {\n 'zh-CN': '不再提醒',\n en: 'Do not remind me again',\n },\n 'runtime.cleanup.backupAndCleanButton': {\n 'zh-CN': '备份并清理',\n en: 'Back up and clean',\n },\n 'runtime.cleanup.starting': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量…',\n en: 'Cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.startingWithBackup': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量,并自动生成备份…',\n en: 'Creating a backup, then cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.exportFailed': {\n 'zh-CN': '聊天记录导出失败,放弃清理:{cause}',\n en: 'Chat export failed; cleanup was cancelled: {cause}',\n },\n 'runtime.cleanup.exportSucceeded': {\n 'zh-CN': '聊天记录导出成功:{message}',\n en: 'Chat exported successfully: {message}',\n },\n 'runtime.cleanup.cleanedMessages': {\n 'zh-CN': '已清理旧聊天记录中的 {count} 条消息',\n en: 'Cleaned {count} messages from the old chat history',\n },\n 'runtime.cleanup.cleanedFloorsLog': {\n 'zh-CN': '[MVU]已清理 {count} 层的消息',\n en: '[MVU] Cleaned messages on {count} floors',\n },\n 'runtime.cleanup.restoreNotNeededLog': {\n 'zh-CN': '最近 {count} 层都包含变量数据,不需要进行恢复。',\n en: 'All of the latest {count} floors contain variable data; restoration is not needed.',\n },\n 'runtime.cleanup.restoreUnavailable': {\n 'zh-CN': '在 0~{floor} 层找不到有效的变量信息,无法进行楼层变量恢复',\n en: 'No valid variable data was found between floors 0 and {floor}; floor variables cannot be restored',\n },\n 'runtime.cleanup.restoreTitle': {\n 'zh-CN': '[MVU]恢复旧楼层变量',\n en: '[MVU] Restore old floor variables',\n },\n\n 'runtime.initvar.noMessagesLog': {\n 'zh-CN': '不存在任何一条消息,退出',\n en: 'No messages exist; initialization stopped',\n },\n 'runtime.initvar.greetingRequired': {\n 'zh-CN': '需要有开场白才能初始化变量',\n en: 'A greeting message is required to initialize variables',\n },\n 'runtime.initvar.initializationFailedTitle': {\n 'zh-CN': '[MVU]变量初始化失败',\n en: '[MVU] Variable initialization failed',\n },\n 'runtime.initvar.oldLorebookFormatLog': {\n 'zh-CN': '检测到旧的 initialized_lorebooks 数组格式,正在迁移到新的对象格式。',\n en: 'Old initialized_lorebooks array format detected. Migrating to the new object format.',\n },\n 'runtime.initvar.chatVariablesInitializedLog': {\n 'zh-CN': '聊天变量已初始化。',\n en: 'Chat variables initialized.',\n },\n 'runtime.initvar.blockParseFailedLog': {\n 'zh-CN': '解析 initvar 块失败:{cause}',\n en: 'Failed to parse initvar block: {cause}',\n },\n 'runtime.initvar.initializationCompletedLog': {\n 'zh-CN': '变量初始化完成',\n en: 'Variable initialization completed',\n },\n 'runtime.initvar.initializationSucceeded': {\n 'zh-CN': '已加载新的世界书初始化变量,当前使用世界书:
{books}',\n en: 'New lorebook initialization variables were loaded. Active lorebooks:
{books}',\n },\n 'runtime.initvar.initializationSucceededTitle': {\n 'zh-CN': '[MVU]变量初始化成功',\n en: '[MVU] Variable initialization succeeded',\n },\n 'runtime.initvar.primaryLorebookReadFailedLog': {\n 'zh-CN': '获取角色主 lorebook 失败,已忽略',\n en: 'Failed to obtain the primary character lorebook; ignoring it',\n },\n 'runtime.initvar.entryParseFailedLog': {\n 'zh-CN': '解析世界书条目“{comment}”失败:{cause}',\n en: 'Failed to parse lorebook entry “{comment}”: {cause}',\n },\n 'runtime.initvar.entryParseFailedTitle': {\n 'zh-CN': '[MVU]解析世界书条目“{comment}”失败',\n en: '[MVU] Failed to parse lorebook entry “{comment}”',\n },\n 'runtime.initvar.noMessages': {\n 'zh-CN': '不存在任何一条消息',\n en: 'No messages exist',\n },\n\n 'runtime.notification.settingsUiUpdatedTitle': {\n 'zh-CN': '[MVU]已更新独立配置界面',\n en: '[MVU] Dedicated settings panel updated',\n },\n 'runtime.notification.settingsUiUpdatedBody': {\n 'zh-CN': '配置界面位于酒馆扩展界面中的“正则”下方,请点开了解新功能或自定义配置',\n en: 'The settings panel is located below “Regex” in SillyTavern Extensions. Open it to review new features or customize MVU.',\n },\n 'runtime.notification.autoCleanupFeatureTitle': {\n 'zh-CN': '[MVU]已更新自动清理旧变量功能',\n en: '[MVU] Automatic cleanup updated',\n },\n 'runtime.notification.autoCleanupFeatureBody': {\n 'zh-CN':\n 'MVU 现在可以自动清理旧变量来减少聊天文件大小;这不会影响你回退游玩以前的楼层。可在设置中开启“变量自动清理”。',\n en: 'MVU can now automatically remove old variable data to reduce chat file size without preventing you from returning to earlier floors. Enable Automatic Variable Cleanup in settings.',\n },\n 'runtime.notification.customApiOptionsTitle': {\n 'zh-CN': '[MVU]已更新更多自定义 API 配置',\n en: '[MVU] More custom API options added',\n },\n 'runtime.notification.customApiOptionsBody': {\n 'zh-CN':\n 'MVU 现在可以自定义 API 的温度、频率惩罚、存在惩罚和最大回复 token 数;需要酒馆助手版本 >= 4.0.14。',\n en: 'MVU now supports custom API temperature, frequency penalty, presence penalty, and maximum response tokens. TavernHelper >= 4.0.14 is required.',\n },\n 'runtime.notification.cleanupDefaultTitle': {\n 'zh-CN': '[MVU]已更新自动清理配置',\n en: '[MVU] Automatic cleanup settings updated',\n },\n 'runtime.notification.cleanupDefaultBody': {\n 'zh-CN': 'MVU 现在会自动清理较老楼层上的变量信息,以降低聊天文件大小。',\n en: 'MVU now automatically removes variable data from older floors to reduce chat file size.',\n },\n 'runtime.notification.builtinJailbreakTitle': {\n 'zh-CN': '[MVU]已内置破限',\n en: '[MVU] Built-in jailbreak prompt added',\n },\n 'runtime.notification.builtinJailbreakBody': {\n 'zh-CN':\n '现在,额外模型解析如果取消“发送预设”,则会使用内置的破限提示词——既避免写作任务又避免道歉。',\n en: 'When Send Preset is disabled, extra-model parsing now uses a built-in jailbreak prompt designed to avoid both writing tasks and refusals.',\n },\n 'runtime.notification.concurrentRequestsTitle': {\n 'zh-CN': '[MVU]已支持同时多次请求变量更新',\n en: '[MVU] Concurrent variable-update requests added',\n },\n 'runtime.notification.concurrentRequestsBody': {\n 'zh-CN':\n '现在,你可以在“变量更新方式 → 请求策略 → 请求方式”中选择并发请求,提高额外模型解析成功率并节省时间。',\n en: 'You can now choose concurrent requests under Variable Update Method → Request Strategy → Request Method to improve extra-model parsing success rates and save time.',\n },\n 'runtime.notification.buildInfo': {\n 'zh-CN': '构建信息:{date}({commit})',\n en: 'Build: {date} ({commit})',\n },\n 'runtime.notification.scriptLoadedTitle': {\n 'zh-CN': '[MVU]脚本加载成功',\n en: '[MVU] Script loaded',\n },\n\n 'runtime.filter.toolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或预设/API 不支持工具调用,已退化回 `随AI输出`',\n en: 'The current TavernHelper version or preset/API does not support tool calling. Falling back to Follow AI Output.',\n },\n 'runtime.filter.toolCallingUnsupportedTitle': {\n 'zh-CN': '[MVU]无法使用工具调用',\n en: '[MVU] Tool calling unavailable',\n },\n 'runtime.filter.globalSettingsSource': {\n 'zh-CN': '用户全局配置',\n en: 'global user settings',\n },\n 'runtime.filter.characterSettingsSource': {\n 'zh-CN': '角色卡配置',\n en: 'character-card settings',\n },\n 'runtime.filter.whitelistRegexLabel': {\n 'zh-CN': '白名单正则',\n en: 'whitelist regex',\n },\n 'runtime.filter.blacklistRegexLabel': {\n 'zh-CN': '黑名单正则',\n en: 'blacklist regex',\n },\n 'runtime.filter.invalidRegex': {\n 'zh-CN': '{source}的{label}无效,已在本轮世界书条目过滤中忽略:{cause}',\n en: 'The {label} from {source} is invalid and was ignored for this lorebook filtering pass: {cause}',\n },\n 'runtime.filter.invalidRegexTitle': {\n 'zh-CN': '[MVU]世界书条目过滤正则无效',\n en: '[MVU] Invalid lorebook entry filter regex',\n },\n 'runtime.filter.logTitle': {\n 'zh-CN': '[MVU]世界书条目黑/白名单筛选结果',\n en: '[MVU] Lorebook entry allowlist/blocklist filtering results',\n },\n 'runtime.regex.jsStyleSyntax': {\n 'zh-CN': 'JS 风格正则需要以 /pattern/flags 形式书写',\n en: 'JavaScript-style regular expressions must use the /pattern/flags form',\n },\n\n 'runtime.functionCalling.versionUnsupported': {\n 'zh-CN': '当前酒馆助手版本为 {current},工具调用需要酒馆助手 {required} 或更高版本',\n en: 'The current TavernHelper version is {current}. Tool calling requires TavernHelper {required} or later.',\n },\n 'runtime.extraModel.characterLorebookUnavailableLog': {\n 'zh-CN': '无法找到角色世界书,在多人聊天下不支持额外模型解析。',\n en: 'The character lorebook could not be found. Extra-model parsing is unavailable in group chats.',\n },\n 'runtime.extraModel.v4RequiresCustomSource': {\n 'zh-CN': '[MVU额外模型解析]格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同。',\n en: '[MVU extra-model parsing] Formatted Output (v4 Compatible) requires a Custom model source and cannot use Same as Primary Connection.',\n },\n 'runtime.extraModel.customIncludeBodyInvalid': {\n 'zh-CN': '[MVU额外模型解析]custom_include_body 不是 YAML object,无法合并配置。',\n en: '[MVU extra-model parsing] custom_include_body is not a YAML object, so the configuration cannot be merged.',\n },\n 'runtime.extraModel.saveSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern saveSettings,不能临时更新配置。',\n en: '[MVU extra-model parsing] SillyTavern saveSettings is unavailable; settings cannot be updated temporarily.',\n },\n 'runtime.extraModel.openAiSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable.',\n },\n 'runtime.extraModel.openAiSettingsRestoreUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置,不能恢复配置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable; the configuration cannot be restored.',\n },\n 'runtime.extraModel.requesting': {\n 'zh-CN': '正在请求 AI 回复…',\n en: 'Requesting an AI response…',\n },\n 'runtime.extraModel.retrying': {\n 'zh-CN': '正在重试({attempt} / {total})…',\n en: 'Retrying ({attempt} / {total})…',\n },\n 'runtime.extraModel.updateInProgressTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新中',\n en: '[MVU extra-model parsing] Updating variables',\n },\n 'runtime.extraModel.concurrentRequests': {\n 'zh-CN': '将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.firstRequest': {\n 'zh-CN': '将先请求一次,尝试是否能成功…',\n en: 'Trying one request first…',\n },\n 'runtime.extraModel.firstRequestFailed': {\n 'zh-CN': '首次请求失败,将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'The first request failed. Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.updateTagMissing': {\n 'zh-CN': '[MVU额外模型解析]没有能从回复中找到标签',\n en: '[MVU extra-model parsing] No tag was found in the response',\n },\n 'runtime.extraModel.updateCommandsInvalid': {\n 'zh-CN': '[MVU额外模型解析]从回复找到了标签,但其内的更新命令无效',\n en: '[MVU extra-model parsing] An tag was found, but its update commands are invalid',\n },\n 'runtime.extraModel.firstFloorSkippedLog': {\n 'zh-CN': '[MVU]对第一层永不进行额外模型解析',\n en: '[MVU] Extra-model parsing is never performed on the first floor',\n },\n 'runtime.extraModel.autoRequestDisabledLog': {\n 'zh-CN': '[MVU]不自动触发额外模型解析',\n en: '[MVU] Automatic extra-model parsing is disabled',\n },\n 'runtime.extraModel.belowMinLengthLog': {\n 'zh-CN': '[MVU]回复未达到最小请求字数,跳过自动额外模型解析',\n en: '[MVU] Reply is below the minimum length; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.triggerTagMissingLog': {\n 'zh-CN': '[MVU]回复未匹配触发标签正则,跳过自动额外模型解析',\n en: '[MVU] Reply does not match the trigger-tag regex; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.triggerTagInvalidLog': {\n 'zh-CN': '[MVU]触发标签正则无效,跳过自动额外模型解析',\n en: '[MVU] Invalid trigger-tag regex; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.updateFailed': {\n 'zh-CN': '建议调整变量更新方式;可在“输入框左下角魔棒 → 日志查看器”查看具体情况',\n en: 'Try adjusting the variable update method. Open Magic Wand → Log Viewer at the lower-left of the input box for details.',\n },\n 'runtime.extraModel.updateFailedTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新失败',\n en: '[MVU extra-model parsing] Variable update failed',\n },\n\n // Pi 运行时统一错误文案:只插入经过分类的参数,不拼接服务商原始响应或认证信息。\n 'runtime.pi.invalidConfig': {\n 'zh-CN': '“更多”配置无效,请检查其中的来源、API 和认证设置。',\n en: 'The More-source configuration is invalid. Check its provider, API, and authentication settings.',\n },\n 'runtime.pi.unknownProvider': {\n 'zh-CN': '“更多”中所选来源不可用,请重新选择来源。',\n en: 'The selected provider under More is unavailable. Select a provider again.',\n },\n 'runtime.pi.unsupportedApi': {\n 'zh-CN': '“更多”中所选来源不支持当前 API,请改用该来源支持的 API。',\n en: 'The selected provider under More does not support the current API. Choose an API supported by this provider.',\n },\n 'runtime.pi.unsupportedAuth': {\n 'zh-CN': '“更多”中所选来源不支持当前认证方式,请改用该来源支持的认证方式。',\n en: 'The selected provider under More does not support the current authentication method. Choose a supported method.',\n },\n 'runtime.pi.missingApiKey': {\n 'zh-CN': '请输入“更多”中所选来源的 API Key。',\n en: 'Enter an API key for the selected provider under More.',\n },\n 'runtime.pi.missingOAuthCredential': {\n 'zh-CN': '“更多”中所选来源尚未完成 OAuth 登录,请先登录。',\n en: 'The selected provider under More is not signed in with OAuth. Sign in before retrying.',\n },\n 'runtime.pi.oauthCredentialExpired': {\n 'zh-CN': '“更多”的 OAuth 凭据已过期且无法刷新,请登出后再登录。',\n en: 'The OAuth credential for More has expired and could not be refreshed. Sign out, then sign in again.',\n },\n 'runtime.pi.invalidEndpoint': {\n 'zh-CN':\n '自定义 endpoint 必须使用 HTTPS;HTTP 仅允许 localhost、127.0.0.1 或 [::1],且不能包含凭据、查询参数或片段。',\n en: 'The custom endpoint must use HTTPS. HTTP is limited to localhost, 127.0.0.1, or [::1]. Credentials, query parameters, and fragments are not allowed.',\n },\n 'runtime.pi.customEndpointNotAllowed': {\n 'zh-CN': '“更多”中所选来源使用固定 endpoint,不允许自定义。',\n en: 'The selected provider under More uses a fixed endpoint and does not allow a custom one.',\n },\n 'runtime.pi.oauthEndpointNotAllowed': {\n 'zh-CN': 'OAuth 来源必须使用注册的固定 endpoint,请清除自定义 endpoint。',\n en: 'OAuth providers must use their registered fixed endpoint. Clear the custom endpoint.',\n },\n 'runtime.pi.oauthApiMismatch': {\n 'zh-CN': '当前 OAuth 来源只能使用其锁定的兼容 API,请重新选择来源或认证方式。',\n en: 'This OAuth provider can only use its locked compatible API. Reselect the provider or authentication method.',\n },\n 'runtime.pi.missingModel': {\n 'zh-CN': '请填写“更多”来源的模型名称。',\n en: 'Enter a model name for the More source.',\n },\n 'runtime.pi.invalidContextWindow': {\n 'zh-CN': 'contextWindow 必须是正整数。',\n en: 'contextWindow must be a positive integer.',\n },\n 'runtime.pi.missingContextWindow': {\n 'zh-CN': '当前模型不在“更多”的内置目录中,请手动填写有效的 contextWindow。',\n en: 'The current model is not in the built-in catalog under More. Enter a valid contextWindow manually.',\n },\n 'runtime.pi.invalidMaxTokens': {\n 'zh-CN': '“最大回复 token 数”必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'runtime.pi.maxTokensExceedContext': {\n 'zh-CN': '“最大回复 token 数”不能大于 contextWindow。',\n en: 'Maximum response tokens must not exceed contextWindow.',\n },\n 'runtime.pi.anthropicSamplingConflict': {\n 'zh-CN': 'Anthropic 的温度和 top_p 只能调整其中一个,请将另一个恢复为默认值 1。',\n en: 'Adjust either temperature or top_p for Anthropic. Reset the other to its default value of 1.',\n },\n 'runtime.pi.customHeadersInvalid': {\n 'zh-CN': '“更多”的自定义请求头必须是 YAML/JSON 对象,且值只能是字符串或 null。',\n en: 'Custom headers under More must be a YAML/JSON object whose values are strings or null.',\n },\n 'runtime.pi.customConfigParseFailed': {\n 'zh-CN': '“更多”的自定义请求配置不是有效的 YAML 或 JSON,请检查格式。',\n en: 'The custom request settings under More are not valid YAML or JSON. Check their formatting.',\n },\n 'runtime.pi.customIncludeBodyInvalid': {\n 'zh-CN': '“更多”的自定义请求体必须是 YAML/JSON 对象。',\n en: 'The custom request body under More must be a YAML/JSON object.',\n },\n 'runtime.pi.customExcludeBodyInvalid': {\n 'zh-CN': '“更多”的排除请求字段必须是 YAML/JSON 字段名数组。',\n en: 'Excluded request fields under More must be a YAML/JSON array of field names.',\n },\n 'runtime.pi.customPayloadProtectedField': {\n 'zh-CN': '“更多”的自定义请求配置不能覆盖、排除或设置认证及关键协议字段。',\n en: 'Custom request settings under More cannot override, exclude, or set authentication or other protected protocol fields.',\n },\n 'runtime.pi.payloadInvalid': {\n 'zh-CN': '“更多”来源的请求体必须是对象,请检查 API 配置。',\n en: 'The provider payload under More must be an object. Check the API configuration.',\n },\n 'runtime.pi.structuredOutputSchemaMissing': {\n 'zh-CN': '“更多”的格式化输出需要有效的 JSON Schema。',\n en: 'Formatted output under More requires a valid JSON Schema.',\n },\n 'runtime.pi.structuredOutputUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持原生格式化输出,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support native formatted output. Change the configuration and retry.',\n },\n 'runtime.pi.toolCallingUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持所需的工具调用模式,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support the required tool-calling mode. Change the configuration and retry.',\n },\n 'runtime.pi.toolRequestRejected': {\n 'zh-CN':\n '目标端点未接受“工具调用”请求。请确认该端点和模型支持工具调用,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the tool-calling request. Confirm that the endpoint and model support tool calling, and check the API type, URL, and model name.',\n },\n 'runtime.pi.structuredOutputRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出”请求。请确认该端点和模型支持 JSON Schema 格式化输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the formatted-output request. Confirm that the endpoint and model support JSON Schema formatted output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.jsonObjectRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出(v4兼容)”请求。请确认该端点和模型支持 JSON Object 输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the v4-compatible formatted-output request. Confirm that the endpoint and model support JSON Object output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.namedToolChoiceUnsupported': {\n 'zh-CN': '“更多”中的当前 API 不支持指定工具调用,请更换 API 或调整工具选择方式。',\n en: 'The current API under More does not support named tool choice. Choose another API or adjust the tool-choice mode.',\n },\n 'runtime.pi.invalidToolDefinition': {\n 'zh-CN':\n '“更多”来源的工具定义无效:function name 不能为空,且 parameters 根节点必须是 object。',\n en: 'The tool definition under More is invalid: function name cannot be empty, and the parameters root must be an object.',\n },\n 'runtime.pi.invalidToolCall': {\n 'zh-CN': '“更多”来源返回了无效的工具调用,请重试或更换模型。',\n en: 'The More source returned an invalid tool call. Retry or choose another model.',\n },\n 'runtime.pi.toolUseMissingCall': {\n 'zh-CN': '“更多”来源以工具调用结束,但没有返回工具调用内容,请重试或更换模型。',\n en: 'The More source stopped for tool use without returning a tool call. Retry or choose another model.',\n },\n 'runtime.pi.imageInputUnsupported': {\n 'zh-CN': '“更多”中的当前模型不支持图片输入,请移除图片或更换模型。',\n en: 'The current model under More does not support image input. Remove the image or choose another model.',\n },\n 'runtime.pi.contextInvalidImage': {\n 'zh-CN': '“更多”的图片输入必须是可解码的 image/* base64 data URL。',\n en: 'Image input under More must be a decodable image/* base64 data URL.',\n },\n 'runtime.pi.remoteImageUnsupported': {\n 'zh-CN': '“更多”当前不支持远程图片 URL,请改用 base64 data URL。',\n en: 'The More source does not currently support remote image URLs. Use a base64 data URL instead.',\n },\n 'runtime.pi.videoUnsupported': {\n 'zh-CN': '“更多”链路当前不支持视频内容,请移除视频后重试。',\n en: 'The More path does not currently support video content. Remove the video and retry.',\n },\n 'runtime.pi.contextSystemPayloadMismatch': {\n 'zh-CN': '无法在服务商请求中完整恢复 system 消息,已停止发送。请更新脚本或更换协议后重试。',\n en: 'Could not restore all system messages in the provider payload. Sending was stopped. Update the script or select another API and try again.',\n },\n 'runtime.pi.contextMissingToolCall': {\n 'zh-CN': '第 {index} 条历史工具结果无法找到对应的工具调用,请检查聊天历史。',\n en: 'Historical tool result {index} has no matching tool call. Check the chat history.',\n },\n 'runtime.pi.contextUnsupportedContent': {\n 'zh-CN': '第 {index} 条消息包含“更多”当前不支持的内容,请移除该内容后重试。',\n en: 'Message {index} contains content that More does not currently support. Remove it and retry.',\n },\n 'runtime.pi.promptCaptureFailed': {\n 'zh-CN': '未能捕获 SillyTavern 构建后的最终提示词,请重试并查看日志。',\n en: 'Could not capture the final prompt built by SillyTavern. Retry and check the logs.',\n },\n 'runtime.pi.requestAlreadyActive': {\n 'zh-CN': '同一“更多”请求已在运行,请等待它结束或先停止该请求。',\n en: 'The same More-source request is already running. Wait for it to finish or stop it first.',\n },\n 'runtime.pi.tokenBudgetExceeded': {\n 'zh-CN':\n '请求预计占用 {estimatedInput} 个输入 token;加上 {maxTokens} 个回复 token 和 {reserve} 个预留 token 后,超过 {contextWindow} 的上下文窗口。请缩短聊天历史、降低最大回复 token 数,或修正 contextWindow。',\n en: 'The request is estimated to use {estimatedInput} input tokens. Together with {maxTokens} response tokens and {reserve} reserved tokens, it exceeds the {contextWindow}-token context window. Shorten chat history, reduce maximum response tokens, or correct contextWindow.',\n },\n 'runtime.pi.requestAborted': {\n 'zh-CN': '“更多”请求已取消。',\n en: 'The More-source request was cancelled.',\n },\n 'runtime.pi.browserNetworkError': {\n 'zh-CN':\n '浏览器无法连接“更多”中所选来源。请检查网络、地址配置,并确认来源允许浏览器 CORS 请求。',\n en: 'The browser could not reach the selected provider under More. Check the network and address settings, and confirm that the provider allows browser CORS requests.',\n },\n 'runtime.pi.proxyUnavailable': {\n 'zh-CN':\n '没有开启Proxy。请在 SillyTavern 的 config.yaml 中开启 enableCorsProxy,或使用 --corsProxy 启动参数,然后重启 SillyTavern。',\n en: 'Proxy is not enabled. Enable enableCorsProxy in SillyTavern config.yaml, or start SillyTavern with --corsProxy, then restart SillyTavern.',\n },\n 'runtime.pi.requestFailed': {\n 'zh-CN': '“更多”来源请求失败。请检查来源配置、凭据和网络后重试。',\n en: 'The provider request under More failed. Check the provider settings, credentials, and network, then retry.',\n },\n 'runtime.pi.featureDisabled': {\n 'zh-CN': '此构建已关闭“更多”模型来源;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Select Same as current connection or Custom.',\n },\n 'runtime.pi.protocolError': {\n 'zh-CN': '“更多”来源返回了无效的协议数据,请重试或更换来源/API。',\n en: 'The provider under More returned invalid protocol data. Retry or choose another provider/API.',\n },\n 'runtime.pi.lengthTruncated': {\n 'zh-CN': '“更多”回复因达到长度上限而被截断,请增加最大回复 token 数或缩短请求内容。',\n en: 'The response from More was truncated at the length limit. Increase maximum response tokens or shorten the request content.',\n },\n 'runtime.pi.emptyResponse': {\n 'zh-CN': '“更多”来源返回了空回复,请重试或更换模型。',\n en: 'The More source returned an empty response. Retry or choose another model.',\n },\n 'runtime.pi.thinkingOnlyResponse': {\n 'zh-CN': '“更多”回复仅包含 thinking,没有可用的业务内容,请重试或更换模型。',\n en: 'The response from More contained only thinking and no usable content. Retry or choose another model.',\n },\n 'runtime.pi.deferredResponse': {\n 'zh-CN': '“更多”回复尚未完成,当前不支持 deferred 结果。',\n en: 'The response from More is not complete; deferred results are not currently supported.',\n },\n 'runtime.pi.oauth.cancelled': {\n 'zh-CN': '“更多”的 OAuth 操作已取消,原凭据未更改。',\n en: 'The OAuth operation under More was cancelled. The existing credential was not changed.',\n },\n 'runtime.pi.oauth.browserUnavailable': {\n 'zh-CN':\n '当前页面缺少 OAuth 所需的 Fetch 或 Web Crypto 能力,请在支持的安全浏览器环境中重试。',\n en: 'Fetch or Web Crypto required for OAuth is unavailable. Retry in a supported secure browser context.',\n },\n 'runtime.pi.oauth.unsupportedProvider': {\n 'zh-CN': '“更多”中所选来源不支持浏览器 OAuth 登录,请改用 API Key 或其他来源。',\n en: 'The selected provider under More does not support browser OAuth sign-in. Use an API key or another provider.',\n },\n 'runtime.pi.oauth.invalidCallback': {\n 'zh-CN':\n 'OAuth callback URL 无效。请从浏览器地址栏复制完整的 loopback callback URL 后重试。',\n en: 'The OAuth callback URL is invalid. Copy the complete loopback callback URL from the browser address bar and retry.',\n },\n 'runtime.pi.oauth.stateMismatch': {\n 'zh-CN': 'OAuth callback 与当前登录尝试不匹配。请取消当前尝试并重新登录。',\n en: 'The OAuth callback does not match the current sign-in attempt. Cancel it and start a new sign-in.',\n },\n 'runtime.pi.oauth.authorizationFailed': {\n 'zh-CN': 'OAuth 授权未完成或被来源拒绝,请重新发起登录。',\n en: 'OAuth authorization was not completed or was rejected by the provider. Start a new sign-in.',\n },\n 'runtime.pi.oauth.browserNetwork': {\n 'zh-CN':\n '浏览器无法连接 OAuth token 服务。请检查网络,并确认来源允许浏览器 CORS 请求;MVU 不会自动改用代理。',\n en: 'The browser could not reach the OAuth token service. Check the network and confirm that the provider allows browser CORS requests; MVU will not silently use a proxy.',\n },\n 'runtime.pi.oauth.tokenHttp': {\n 'zh-CN': 'OAuth token 交换被来源拒绝。请重新发起登录,不要重用之前的 callback URL。',\n en: 'The provider rejected the OAuth token exchange. Start a new sign-in and do not reuse the previous callback URL.',\n },\n 'runtime.pi.oauth.tokenResponse': {\n 'zh-CN': 'OAuth token 服务返回了无效响应,请稍后重新登录。',\n en: 'The OAuth token service returned an invalid response. Sign in again later.',\n },\n 'runtime.pi.oauth.accountId': {\n 'zh-CN': '无法从 OAuth 凭据中确认账户,请重新登录或选择其他账户。',\n en: 'The account could not be identified from the OAuth credential. Sign in again or choose another account.',\n },\n 'runtime.pi.oauth.attemptExpired': {\n 'zh-CN': 'OAuth 登录尝试已过期,请重新点击登录并使用新的授权链接。',\n en: 'The OAuth sign-in attempt has expired. Start a new sign-in and use the new authorization link.',\n },\n 'runtime.pi.oauth.attemptUsed': {\n 'zh-CN': 'OAuth 登录尝试已结束或 callback URL 已使用,请重新发起登录。',\n en: 'The OAuth sign-in attempt has ended or its callback URL was already used. Start a new sign-in.',\n },\n 'runtime.pi.oauth.credentialStore': {\n 'zh-CN': '无法读取或保存“更多”的 OAuth 凭据,请检查浏览器存储后重试。',\n en: 'The OAuth credential for More could not be read or saved. Check browser storage and retry.',\n },\n\n 'runtime.variableUpdate.unknownCommand': {\n 'zh-CN': '未知命令',\n en: 'unknown command',\n },\n 'runtime.variableUpdate.errorTitle': {\n 'zh-CN': '[MVU]发生变量更新错误,可能需要重 Roll:{command}',\n en: '[MVU] Variable update error; rerolling may help: {command}',\n },\n 'runtime.variableUpdate.errorDetail': {\n 'zh-CN': '错误详情:{detail}',\n en: 'Error details: {detail}',\n },\n 'runtime.variableUpdate.messageOverwrittenTitle': {\n 'zh-CN': '[MVU]消息内容被覆盖',\n en: '[MVU] Message content overwritten',\n },\n 'runtime.variableUpdate.messageOverwritten': {\n 'zh-CN':\n '第 {messageId} 层消息中其他插件的修改被覆盖,可尝试重新运行相关插件对此楼层的解析。',\n en: \"Changes another plugin made to message #{messageId} were overwritten. Try rerunning that plugin's analysis for this message.\",\n },\n 'runtime.variableUpdate.setPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 set 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the set command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.assignPrimitive': {\n 'zh-CN': '路径“{path}”保存的是原始值({type}),无法向其中 assign;已跳过操作。{reason}',\n en: \"Cannot assign into path '{path}' because it holds a primitive value ({type}). The operation was skipped. {reason}\",\n },\n 'runtime.variableUpdate.mergeNonExtensibleObject': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象合并数据。{reason}',\n en: \"SCHEMA VIOLATION: Cannot merge data into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignUnknownKey': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象写入新键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign the new key '{key}' into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的数组写入元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign elements into the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignMissingParent': {\n 'zh-CN': '路径“{path}”不存在,且其父级不可扩展,无法向其中 assign。{reason}',\n en: \"Cannot assign into the non-existent path '{path}' without an extensible parent. {reason}\",\n },\n 'runtime.variableUpdate.mergeArrayIntoObject': {\n 'zh-CN': '无法将数组合并到路径“{path}”处的对象中。',\n en: \"Cannot merge an array into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.mergeNonObjectIntoObject': {\n 'zh-CN': '无法将非对象值合并到路径“{path}”处的对象中。',\n en: \"Cannot merge a non-object value into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.templateResolutionFailed': {\n 'zh-CN': '解析路径“{path}”处的模板元数据失败:{cause}',\n en: \"Failed to resolve template metadata at '{path}': {cause}\",\n },\n 'runtime.variableUpdate.assignInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.assign 参数无效。',\n en: \"Invalid arguments for _.assign at path '{path}'.\",\n },\n 'runtime.variableUpdate.removePathUndefined': {\n 'zh-CN': '_.remove 命令中的路径“{path}”未定义。',\n en: \"Path '{path}' is undefined in the _.remove command.\",\n },\n 'runtime.variableUpdate.deleteTargetUndetermined': {\n 'zh-CN': '无法确定路径“{path}”上的命令要删除的目标。{reason}',\n en: \"Could not determine the deletion target for the command at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removePathMissing': {\n 'zh-CN': '无法从不存在的路径“{path}”中删除内容。{reason}',\n en: \"Cannot remove from the non-existent path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”处不可扩展的数组中删除元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove an element from the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeRequiredKey': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”中删除必需键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove the required key '{key}' from path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonCollection': {\n 'zh-CN': '路径“{path}”处的值不是数组或对象,无法从中删除内容;已跳过命令。{reason}',\n en: \"Cannot remove from path '{path}' because it is not an array or object. The command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.removeExecutionFailed': {\n 'zh-CN': '无法在路径“{path}”上执行 remove。',\n en: \"Failed to execute remove at path '{path}'.\",\n },\n 'runtime.variableUpdate.addPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 add 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.dateDeltaNotNumber': {\n 'zh-CN': '日期操作的增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' for the date operation is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.deltaNotNumber': {\n 'zh-CN': '增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addUnsupportedValue': {\n 'zh-CN': '路径“{path}”处的值不是日期或数字,已跳过 add 命令。{reason}',\n en: \"The value at path '{path}' is not a date or number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.add 参数数量无效。{reason}',\n en: \"Invalid number of arguments for _.add at path '{path}'. {reason}\",\n },\n\n 'runtime.apiProfile.nameRequired': {\n 'zh-CN': 'API 方案名称不能为空',\n en: 'API profile name cannot be empty',\n },\n 'runtime.apiProfile.notFound': {\n 'zh-CN': '未找到 API 方案:{name}',\n en: 'API profile not found: {name}',\n },\n 'runtime.apiProfile.selectOrEnterName': {\n 'zh-CN': '请先输入或选择一个 API 方案名称',\n en: 'Enter or select an API profile name first',\n },\n 'runtime.apiProfile.alreadyExists': {\n 'zh-CN': 'API 方案「{name}」已存在',\n en: 'API profile “{name}” already exists',\n },\n 'runtime.apiProfile.piConfigRequired': {\n 'zh-CN': '“更多”API 方案缺少完整的连接配置,无法保存。',\n en: 'A More-source API profile requires a complete connection snapshot before it can be saved.',\n },\n 'runtime.apiProfile.enterNewName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a name for the new API profile first',\n },\n 'runtime.preset.notSelected': {\n 'zh-CN': '未选择额外模型预设',\n en: 'No extra-model preset was selected',\n },\n 'runtime.preset.apiUnavailable': {\n 'zh-CN': '当前环境未提供 getPreset 接口',\n en: 'The getPreset API is unavailable in the current environment',\n },\n 'runtime.preset.readFailed': {\n 'zh-CN': '无法读取预设“{name}”',\n en: 'Unable to read preset “{name}”',\n },\n\n 'runtime.functionCall.invalidJsonPatch': {\n 'zh-CN': '不是有效的 json patch',\n en: 'Invalid JSON Patch',\n },\n 'runtime.functionCall.updateBlockParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]无法解析变量更新块。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the variable update block. Content: {content}; error: {cause}',\n },\n 'runtime.functionCall.resultParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]函数调用结果解析失败:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the function-call result: {cause}',\n },\n 'runtime.functionCall.formattedOutputParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]格式化输出解析失败。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse formatted output. Content: {content}; error: {cause}',\n },\n});\n","export function getDevtoolsGlobalHook() {\n return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;\n}\nexport function getTarget() {\n // @ts-expect-error navigator and windows are not available in all environments\n return (typeof navigator !== 'undefined' && typeof window !== 'undefined')\n ? window\n : typeof globalThis !== 'undefined'\n ? globalThis\n : {};\n}\nexport const isProxyAvailable = typeof Proxy === 'function';\n","let supported;\nlet perf;\nexport function isPerformanceSupported() {\n var _a;\n if (supported !== undefined) {\n return supported;\n }\n if (typeof window !== 'undefined' && window.performance) {\n supported = true;\n perf = window.performance;\n }\n else if (typeof globalThis !== 'undefined' && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {\n supported = true;\n perf = globalThis.perf_hooks.performance;\n }\n else {\n supported = false;\n }\n return supported;\n}\nexport function now() {\n return isPerformanceSupported() ? perf.now() : Date.now();\n}\n","import { HOOK_PLUGIN_SETTINGS_SET } from './const.js';\nimport { now } from './time.js';\nexport class ApiProxy {\n constructor(plugin, hook) {\n this.target = null;\n this.targetQueue = [];\n this.onQueue = [];\n this.plugin = plugin;\n this.hook = hook;\n const defaultSettings = {};\n if (plugin.settings) {\n for (const id in plugin.settings) {\n const item = plugin.settings[id];\n defaultSettings[id] = item.defaultValue;\n }\n }\n const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;\n let currentSettings = Object.assign({}, defaultSettings);\n try {\n const raw = localStorage.getItem(localSettingsSaveId);\n const data = JSON.parse(raw);\n Object.assign(currentSettings, data);\n }\n catch (e) {\n // noop\n }\n this.fallbacks = {\n getSettings() {\n return currentSettings;\n },\n setSettings(value) {\n try {\n localStorage.setItem(localSettingsSaveId, JSON.stringify(value));\n }\n catch (e) {\n // noop\n }\n currentSettings = value;\n },\n now() {\n return now();\n },\n };\n if (hook) {\n hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {\n if (pluginId === this.plugin.id) {\n this.fallbacks.setSettings(value);\n }\n });\n }\n this.proxiedOn = new Proxy({}, {\n get: (_target, prop) => {\n if (this.target) {\n return this.target.on[prop];\n }\n else {\n return (...args) => {\n this.onQueue.push({\n method: prop,\n args,\n });\n };\n }\n },\n });\n this.proxiedTarget = new Proxy({}, {\n get: (_target, prop) => {\n if (this.target) {\n return this.target[prop];\n }\n else if (prop === 'on') {\n return this.proxiedOn;\n }\n else if (Object.keys(this.fallbacks).includes(prop)) {\n return (...args) => {\n this.targetQueue.push({\n method: prop,\n args,\n resolve: () => { },\n });\n return this.fallbacks[prop](...args);\n };\n }\n else {\n return (...args) => {\n return new Promise((resolve) => {\n this.targetQueue.push({\n method: prop,\n args,\n resolve,\n });\n });\n };\n }\n },\n });\n }\n async setRealTarget(target) {\n this.target = target;\n for (const item of this.onQueue) {\n this.target.on[item.method](...item.args);\n }\n for (const item of this.targetQueue) {\n item.resolve(await this.target[item.method](...item.args));\n }\n }\n}\n","export const HOOK_SETUP = 'devtools-plugin:setup';\nexport const HOOK_PLUGIN_SETTINGS_SET = 'plugin:settings:set';\n","import { getDevtoolsGlobalHook, getTarget, isProxyAvailable } from './env.js';\nimport { HOOK_SETUP } from './const.js';\nimport { ApiProxy } from './proxy.js';\nexport * from './api/index.js';\nexport * from './plugin.js';\nexport * from './time.js';\nexport function setupDevtoolsPlugin(pluginDescriptor, setupFn) {\n const descriptor = pluginDescriptor;\n const target = getTarget();\n const hook = getDevtoolsGlobalHook();\n const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;\n if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {\n hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);\n }\n else {\n const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;\n const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];\n list.push({\n pluginDescriptor: descriptor,\n setupFn,\n proxy,\n });\n if (proxy) {\n setupFn(proxy.proxiedTarget);\n }\n }\n}\n","/*!\n * vue-i18n v11.1.12\n * (c) 2025 kazuya kawaguchi\n * Released under the MIT License.\n */\nimport { CORE_ERROR_CODES_EXTEND_POINT, createCompileError, CORE_WARN_CODES_EXTEND_POINT, isMessageAST, AST_NODE_PROPS_KEYS, DEFAULT_LOCALE, updateFallbackLocale, setFallbackContext, createCoreContext, clearDateTimeFormat, clearNumberFormat, setAdditionalMeta, getFallbackContext, NOT_REOSLVED, isTranslateFallbackWarn, isTranslateMissingWarn, parseTranslateArgs, translate, MISSING_RESOLVE_VALUE, parseDateTimeArgs, datetime, parseNumberArgs, number, isMessageFunction, fallbackWithLocaleChain, NUMBER_FORMAT_OPTIONS_KEYS, DATETIME_FORMAT_OPTIONS_KEYS, registerMessageCompiler, compile, registerMessageResolver, resolveValue, registerLocaleFallbacker, setDevToolsHook } from '@intlify/core-base';\nimport { getGlobalThis, makeSymbol, format, isObject, create, isPlainObject, isArray, deepCopy, isString, hasOwn, warn, isBoolean, isRegExp, isFunction, inBrowser, assign, isNumber, createEmitter, warnOnce, isEmptyObject } from '@intlify/shared';\nimport { createVNode, Text, computed, watch, getCurrentInstance, ref, shallowRef, Fragment, defineComponent, h, effectScope, inject, onMounted, onUnmounted, isRef } from 'vue';\nimport { setupDevtoolsPlugin } from '@vue/devtools-api';\n\n/**\n * Vue I18n Version\n *\n * @remarks\n * Semver format. Same format as the package.json `version` field.\n *\n * @VueI18nGeneral\n */\nconst VERSION = '11.1.12';\n/**\n * This is only called in esm-bundler builds.\n * istanbul-ignore-next\n */\nfunction initFeatureFlags() {\n if (typeof __VUE_I18N_FULL_INSTALL__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_FULL_INSTALL__ = true;\n }\n if (typeof __VUE_I18N_LEGACY_API__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_LEGACY_API__ = true;\n }\n if (typeof __INTLIFY_DROP_MESSAGE_COMPILER__ !== 'boolean') {\n getGlobalThis().__INTLIFY_DROP_MESSAGE_COMPILER__ = false;\n }\n if (typeof __INTLIFY_PROD_DEVTOOLS__ !== 'boolean') {\n getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false;\n }\n}\n\nconst I18nErrorCodes = {\n // composer module errors\n UNEXPECTED_RETURN_TYPE: CORE_ERROR_CODES_EXTEND_POINT, // 24\n // legacy module errors\n INVALID_ARGUMENT: 25,\n // i18n module errors\n MUST_BE_CALL_SETUP_TOP: 26,\n NOT_INSTALLED: 27,\n // directive module errors\n REQUIRED_VALUE: 28,\n INVALID_VALUE: 29,\n // vue-devtools errors\n CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: 30,\n NOT_INSTALLED_WITH_PROVIDE: 31,\n // unexpected error\n UNEXPECTED_ERROR: 32,\n // not compatible legacy vue-i18n constructor\n NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,\n // Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly\n NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34\n};\nfunction createI18nError(code, ...args) {\n return createCompileError(code, null, (process.env.NODE_ENV !== 'production') ? { messages: errorMessages, args } : undefined);\n}\nconst errorMessages = {\n [I18nErrorCodes.UNEXPECTED_RETURN_TYPE]: 'Unexpected return type in composer',\n [I18nErrorCodes.INVALID_ARGUMENT]: 'Invalid argument',\n [I18nErrorCodes.MUST_BE_CALL_SETUP_TOP]: 'Must be called at the top of a `setup` function',\n [I18nErrorCodes.NOT_INSTALLED]: 'Need to install with `app.use` function',\n [I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpected error',\n [I18nErrorCodes.REQUIRED_VALUE]: `Required in value: {0}`,\n [I18nErrorCodes.INVALID_VALUE]: `Invalid value`,\n [I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN]: `Cannot setup vue-devtools plugin`,\n [I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE]: 'Need to install with `provide` function',\n [I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]: 'Not compatible legacy VueI18n.',\n [I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY]: 'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly'\n};\n\nconst TranslateVNodeSymbol = \n/* #__PURE__*/ makeSymbol('__translateVNode');\nconst DatetimePartsSymbol = /* #__PURE__*/ makeSymbol('__datetimeParts');\nconst NumberPartsSymbol = /* #__PURE__*/ makeSymbol('__numberParts');\nconst EnableEmitter = /* #__PURE__*/ makeSymbol('__enableEmitter');\nconst DisableEmitter = /* #__PURE__*/ makeSymbol('__disableEmitter');\nconst SetPluralRulesSymbol = makeSymbol('__setPluralRules');\nmakeSymbol('__intlifyMeta');\nconst InejctWithOptionSymbol = \n/* #__PURE__*/ makeSymbol('__injectWithOption');\nconst DisposeSymbol = /* #__PURE__*/ makeSymbol('__dispose');\n\nconst I18nWarnCodes = {\n FALLBACK_TO_ROOT: CORE_WARN_CODES_EXTEND_POINT, // 8\n NOT_FOUND_PARENT_SCOPE: 9,\n IGNORE_OBJ_FLATTEN: 10,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_LEGACY_MODE: 11,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12,\n // duplicate `useI18n` calling\n DUPLICATE_USE_I18N_CALLING: 13\n};\nconst warnMessages = {\n [I18nWarnCodes.FALLBACK_TO_ROOT]: `Fall back to {type} '{key}' with root locale.`,\n [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.`,\n [I18nWarnCodes.IGNORE_OBJ_FLATTEN]: `Ignore object flatten: '{key}' key has an string value`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_LEGACY_MODE]: `Legacy API mode has been deprecated in v11. Use Composition API mode instead.\\nAbout how to use the Composition API mode, see https://vue-i18n.intlify.dev/guide/advanced/composition.html`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.`,\n [I18nWarnCodes.DUPLICATE_USE_I18N_CALLING]: \"Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component.\"\n};\nfunction getWarnMessage(code, ...args) {\n return format(warnMessages[code], ...args);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Transform flat json in obj to normal json in obj\n */\nfunction handleFlatJson(obj) {\n // check obj\n if (!isObject(obj)) {\n return obj;\n }\n if (isMessageAST(obj)) {\n return obj;\n }\n for (const key in obj) {\n // check key\n if (!hasOwn(obj, key)) {\n continue;\n }\n // handle for normal json\n if (!key.includes('.')) {\n // recursive process value if value is also a object\n if (isObject(obj[key])) {\n handleFlatJson(obj[key]);\n }\n }\n // handle for flat json, transform to normal json\n else {\n // go to the last object\n const subKeys = key.split('.');\n const lastIndex = subKeys.length - 1;\n let currentObj = obj;\n let hasStringValue = false;\n for (let i = 0; i < lastIndex; i++) {\n if (subKeys[i] === '__proto__') {\n throw new Error(`unsafe key: ${subKeys[i]}`);\n }\n if (!(subKeys[i] in currentObj)) {\n currentObj[subKeys[i]] = create();\n }\n if (!isObject(currentObj[subKeys[i]])) {\n (process.env.NODE_ENV !== 'production') &&\n warn(getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, {\n key: subKeys[i]\n }));\n hasStringValue = true;\n break;\n }\n currentObj = currentObj[subKeys[i]];\n }\n // update last object value, delete old property\n if (!hasStringValue) {\n if (!isMessageAST(currentObj)) {\n currentObj[subKeys[lastIndex]] = obj[key];\n delete obj[key];\n }\n else {\n /**\n * NOTE:\n * if the last object is a message AST and subKeys[lastIndex] has message AST prop key, ignore to copy and key deletion\n */\n if (!AST_NODE_PROPS_KEYS.includes(subKeys[lastIndex])) {\n delete obj[key];\n }\n }\n }\n // recursive process value if value is also a object\n if (!isMessageAST(currentObj)) {\n const target = currentObj[subKeys[lastIndex]];\n if (isObject(target)) {\n handleFlatJson(target);\n }\n }\n }\n }\n return obj;\n}\nfunction getLocaleMessages(locale, options) {\n const { messages, __i18n, messageResolver, flatJson } = options;\n // prettier-ignore\n const ret = (isPlainObject(messages)\n ? messages\n : isArray(__i18n)\n ? create()\n : { [locale]: create() });\n // merge locale messages of i18n custom block\n if (isArray(__i18n)) {\n __i18n.forEach(custom => {\n if ('locale' in custom && 'resource' in custom) {\n const { locale, resource } = custom;\n if (locale) {\n ret[locale] = ret[locale] || create();\n deepCopy(resource, ret[locale]);\n }\n else {\n deepCopy(resource, ret);\n }\n }\n else {\n isString(custom) && deepCopy(JSON.parse(custom), ret);\n }\n });\n }\n // handle messages for flat json\n if (messageResolver == null && flatJson) {\n for (const key in ret) {\n if (hasOwn(ret, key)) {\n handleFlatJson(ret[key]);\n }\n }\n }\n return ret;\n}\nfunction getComponentOptions(instance) {\n return instance.type;\n}\nfunction adjustI18nResources(gl, options, componentOptions) {\n // prettier-ignore\n let messages = isObject(options.messages)\n ? options.messages\n : create();\n if ('__i18nGlobal' in componentOptions) {\n messages = getLocaleMessages(gl.locale.value, {\n messages,\n __i18n: componentOptions.__i18nGlobal\n });\n }\n // merge locale messages\n const locales = Object.keys(messages);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeLocaleMessage(locale, messages[locale]);\n });\n }\n {\n // merge datetime formats\n if (isObject(options.datetimeFormats)) {\n const locales = Object.keys(options.datetimeFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeDateTimeFormat(locale, options.datetimeFormats[locale]);\n });\n }\n }\n // merge number formats\n if (isObject(options.numberFormats)) {\n const locales = Object.keys(options.numberFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeNumberFormat(locale, options.numberFormats[locale]);\n });\n }\n }\n }\n}\nfunction createTextNode(key) {\n return createVNode(Text, null, key, 0);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// extend VNode interface\nconst DEVTOOLS_META = '__INTLIFY_META__';\nconst NOOP_RETURN_ARRAY = () => [];\nconst NOOP_RETURN_FALSE = () => false;\nlet composerID = 0;\nfunction defineCoreMissingHandler(missing) {\n return ((ctx, locale, key, type) => {\n return missing(locale, key, getCurrentInstance() || undefined, type);\n });\n}\n// for Intlify DevTools\n/* #__NO_SIDE_EFFECTS__ */\nconst getMetaInfo = () => {\n const instance = getCurrentInstance();\n let meta = null;\n return instance && (meta = getComponentOptions(instance)[DEVTOOLS_META])\n ? { [DEVTOOLS_META]: meta }\n : null;\n};\n/**\n * Create composer interface factory\n *\n * @internal\n */\nfunction createComposer(options = {}) {\n const { __root, __injectWithOption } = options;\n const _isGlobal = __root === undefined;\n const flatJson = options.flatJson;\n const _ref = inBrowser ? ref : shallowRef;\n let _inheritLocale = isBoolean(options.inheritLocale)\n ? options.inheritLocale\n : true;\n const _locale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.locale.value\n : isString(options.locale)\n ? options.locale\n : DEFAULT_LOCALE);\n const _fallbackLocale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.fallbackLocale.value\n : isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : _locale.value);\n const _messages = _ref(getLocaleMessages(_locale.value, options));\n // prettier-ignore\n const _datetimeFormats = _ref(isPlainObject(options.datetimeFormats)\n ? options.datetimeFormats\n : { [_locale.value]: {} })\n ;\n // prettier-ignore\n const _numberFormats = _ref(isPlainObject(options.numberFormats)\n ? options.numberFormats\n : { [_locale.value]: {} })\n ;\n // warning suppress options\n // prettier-ignore\n let _missingWarn = __root\n ? __root.missingWarn\n : isBoolean(options.missingWarn) || isRegExp(options.missingWarn)\n ? options.missingWarn\n : true;\n // prettier-ignore\n let _fallbackWarn = __root\n ? __root.fallbackWarn\n : isBoolean(options.fallbackWarn) || isRegExp(options.fallbackWarn)\n ? options.fallbackWarn\n : true;\n // prettier-ignore\n let _fallbackRoot = __root\n ? __root.fallbackRoot\n : isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n // configure fall back to root\n let _fallbackFormat = !!options.fallbackFormat;\n // runtime missing\n let _missing = isFunction(options.missing) ? options.missing : null;\n let _runtimeMissing = isFunction(options.missing)\n ? defineCoreMissingHandler(options.missing)\n : null;\n // postTranslation handler\n let _postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : null;\n // prettier-ignore\n let _warnHtmlMessage = __root\n ? __root.warnHtmlMessage\n : isBoolean(options.warnHtmlMessage)\n ? options.warnHtmlMessage\n : true;\n let _escapeParameter = !!options.escapeParameter;\n // custom linked modifiers\n // prettier-ignore\n const _modifiers = __root\n ? __root.modifiers\n : isPlainObject(options.modifiers)\n ? options.modifiers\n : {};\n // pluralRules\n let _pluralRules = options.pluralRules || (__root && __root.pluralRules);\n // runtime context\n // eslint-disable-next-line prefer-const\n let _context;\n const getCoreContext = () => {\n _isGlobal && setFallbackContext(null);\n const ctxOptions = {\n version: VERSION,\n locale: _locale.value,\n fallbackLocale: _fallbackLocale.value,\n messages: _messages.value,\n modifiers: _modifiers,\n pluralRules: _pluralRules,\n missing: _runtimeMissing === null ? undefined : _runtimeMissing,\n missingWarn: _missingWarn,\n fallbackWarn: _fallbackWarn,\n fallbackFormat: _fallbackFormat,\n unresolving: true,\n postTranslation: _postTranslation === null ? undefined : _postTranslation,\n warnHtmlMessage: _warnHtmlMessage,\n escapeParameter: _escapeParameter,\n messageResolver: options.messageResolver,\n messageCompiler: options.messageCompiler,\n __meta: { framework: 'vue' }\n };\n {\n ctxOptions.datetimeFormats = _datetimeFormats.value;\n ctxOptions.numberFormats = _numberFormats.value;\n ctxOptions.__datetimeFormatters = isPlainObject(_context)\n ? _context.__datetimeFormatters\n : undefined;\n ctxOptions.__numberFormatters = isPlainObject(_context)\n ? _context.__numberFormatters\n : undefined;\n }\n if ((process.env.NODE_ENV !== 'production')) {\n ctxOptions.__v_emitter = isPlainObject(_context)\n ? _context.__v_emitter\n : undefined;\n }\n const ctx = createCoreContext(ctxOptions);\n _isGlobal && setFallbackContext(ctx);\n return ctx;\n };\n _context = getCoreContext();\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n // track reactivity\n function trackReactivityValues() {\n return [\n _locale.value,\n _fallbackLocale.value,\n _messages.value,\n _datetimeFormats.value,\n _numberFormats.value\n ]\n ;\n }\n // locale\n const locale = computed({\n get: () => _locale.value,\n set: val => {\n _context.locale = val;\n _locale.value = val;\n }\n });\n // fallbackLocale\n const fallbackLocale = computed({\n get: () => _fallbackLocale.value,\n set: val => {\n _context.fallbackLocale = val;\n _fallbackLocale.value = val;\n updateFallbackLocale(_context, _locale.value, val);\n }\n });\n // messages\n const messages = computed(() => _messages.value);\n // datetimeFormats\n const datetimeFormats = /* #__PURE__*/ computed(() => _datetimeFormats.value);\n // numberFormats\n const numberFormats = /* #__PURE__*/ computed(() => _numberFormats.value);\n // getPostTranslationHandler\n function getPostTranslationHandler() {\n return isFunction(_postTranslation) ? _postTranslation : null;\n }\n // setPostTranslationHandler\n function setPostTranslationHandler(handler) {\n _postTranslation = handler;\n _context.postTranslation = handler;\n }\n // getMissingHandler\n function getMissingHandler() {\n return _missing;\n }\n // setMissingHandler\n function setMissingHandler(handler) {\n if (handler !== null) {\n _runtimeMissing = defineCoreMissingHandler(handler);\n }\n _missing = handler;\n _context.missing = _runtimeMissing;\n }\n function isResolvedTranslateMessage(type, arg) {\n return type !== 'translate' || !arg.resolvedMessage;\n }\n const wrapWithDeps = (fn, argumentParser, warnType, fallbackSuccess, fallbackFail, successCondition) => {\n trackReactivityValues(); // track reactive dependency\n // NOTE: experimental !!\n let ret;\n try {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(getMetaInfo());\n }\n if (!_isGlobal) {\n _context.fallbackContext = __root\n ? getFallbackContext()\n : undefined;\n }\n ret = fn(_context);\n }\n finally {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(null);\n }\n if (!_isGlobal) {\n _context.fallbackContext = undefined;\n }\n }\n if ((warnType !== 'translate exists' && // for not `te` (e.g `t`)\n isNumber(ret) &&\n ret === NOT_REOSLVED) ||\n (warnType === 'translate exists' && !ret) // for `te`\n ) {\n const [key, arg2] = argumentParser();\n if ((process.env.NODE_ENV !== 'production') &&\n __root &&\n isString(key) &&\n isResolvedTranslateMessage(warnType, arg2)) {\n if (_fallbackRoot &&\n (isTranslateFallbackWarn(_fallbackWarn, key) ||\n isTranslateMissingWarn(_missingWarn, key))) {\n warn(getWarnMessage(I18nWarnCodes.FALLBACK_TO_ROOT, {\n key,\n type: warnType\n }));\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n const { __v_emitter: emitter } = _context;\n if (emitter && _fallbackRoot) {\n emitter.emit('fallback', {\n type: warnType,\n key,\n to: 'global',\n groupId: `${warnType}:${key}`\n });\n }\n }\n }\n return __root && _fallbackRoot\n ? fallbackSuccess(__root)\n : fallbackFail(key);\n }\n else if (successCondition(ret)) {\n return ret;\n }\n else {\n /* istanbul ignore next */\n throw createI18nError(I18nErrorCodes.UNEXPECTED_RETURN_TYPE);\n }\n };\n // t\n function t(...args) {\n return wrapWithDeps(context => Reflect.apply(translate, null, [context, ...args]), () => parseTranslateArgs(...args), 'translate', root => Reflect.apply(root.t, root, [...args]), key => key, val => isString(val));\n }\n // rt\n function rt(...args) {\n const [arg1, arg2, arg3] = args;\n if (arg3 && !isObject(arg3)) {\n throw createI18nError(I18nErrorCodes.INVALID_ARGUMENT);\n }\n return t(...[arg1, arg2, assign({ resolvedMessage: true }, arg3 || {})]);\n }\n // d\n function d(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => Reflect.apply(root.d, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // n\n function n(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => Reflect.apply(root.n, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // for custom processor\n function normalize(values) {\n return values.map(val => isString(val) || isNumber(val) || isBoolean(val)\n ? createTextNode(String(val))\n : val);\n }\n const interpolate = (val) => val;\n const processor = {\n normalize,\n interpolate,\n type: 'vnode'\n };\n // translateVNode, using for `i18n-t` component\n function translateVNode(...args) {\n return wrapWithDeps(context => {\n let ret;\n const _context = context;\n try {\n _context.processor = processor;\n ret = Reflect.apply(translate, null, [_context, ...args]);\n }\n finally {\n _context.processor = null;\n }\n return ret;\n }, () => parseTranslateArgs(...args), 'translate', root => root[TranslateVNodeSymbol](...args), key => [createTextNode(key)], val => isArray(val));\n }\n // numberParts, using for `i18n-n` component\n function numberParts(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => root[NumberPartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n // datetimeParts, using for `i18n-d` component\n function datetimeParts(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => root[DatetimePartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n function setPluralRules(rules) {\n _pluralRules = rules;\n _context.pluralRules = _pluralRules;\n }\n // te\n function te(key, locale) {\n return wrapWithDeps(() => {\n if (!key) {\n return false;\n }\n const targetLocale = isString(locale) ? locale : _locale.value;\n const message = getLocaleMessage(targetLocale);\n const resolved = _context.messageResolver(message, key);\n return (isMessageAST(resolved) ||\n isMessageFunction(resolved) ||\n isString(resolved));\n }, () => [key], 'translate exists', root => {\n return Reflect.apply(root.te, root, [key, locale]);\n }, NOOP_RETURN_FALSE, val => isBoolean(val));\n }\n function resolveMessages(key) {\n let messages = null;\n const locales = fallbackWithLocaleChain(_context, _fallbackLocale.value, _locale.value);\n for (let i = 0; i < locales.length; i++) {\n const targetLocaleMessages = _messages.value[locales[i]] || {};\n const messageValue = _context.messageResolver(targetLocaleMessages, key);\n if (messageValue != null) {\n messages = messageValue;\n break;\n }\n }\n return messages;\n }\n // tm\n function tm(key) {\n const messages = resolveMessages(key);\n // prettier-ignore\n return messages != null\n ? messages\n : __root\n ? __root.tm(key) || {}\n : {};\n }\n // getLocaleMessage\n function getLocaleMessage(locale) {\n return (_messages.value[locale] || {});\n }\n // setLocaleMessage\n function setLocaleMessage(locale, message) {\n if (flatJson) {\n const _message = { [locale]: message };\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n message = _message[locale];\n }\n _messages.value[locale] = message;\n _context.messages = _messages.value;\n }\n // mergeLocaleMessage\n function mergeLocaleMessage(locale, message) {\n _messages.value[locale] = _messages.value[locale] || {};\n const _message = { [locale]: message };\n if (flatJson) {\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n }\n message = _message[locale];\n deepCopy(message, _messages.value[locale]);\n _context.messages = _messages.value;\n }\n // getDateTimeFormat\n function getDateTimeFormat(locale) {\n return _datetimeFormats.value[locale] || {};\n }\n // setDateTimeFormat\n function setDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = format;\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // mergeDateTimeFormat\n function mergeDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = assign(_datetimeFormats.value[locale] || {}, format);\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // getNumberFormat\n function getNumberFormat(locale) {\n return _numberFormats.value[locale] || {};\n }\n // setNumberFormat\n function setNumberFormat(locale, format) {\n _numberFormats.value[locale] = format;\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // mergeNumberFormat\n function mergeNumberFormat(locale, format) {\n _numberFormats.value[locale] = assign(_numberFormats.value[locale] || {}, format);\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // for debug\n composerID++;\n // watch root locale & fallbackLocale\n if (__root && inBrowser) {\n watch(__root.locale, (val) => {\n if (_inheritLocale) {\n _locale.value = val;\n _context.locale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n watch(__root.fallbackLocale, (val) => {\n if (_inheritLocale) {\n _fallbackLocale.value = val;\n _context.fallbackLocale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n }\n // define basic composition API!\n const composer = {\n id: composerID,\n locale,\n fallbackLocale,\n get inheritLocale() {\n return _inheritLocale;\n },\n set inheritLocale(val) {\n _inheritLocale = val;\n if (val && __root) {\n _locale.value = __root.locale.value;\n _fallbackLocale.value = __root.fallbackLocale.value;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n },\n get availableLocales() {\n return Object.keys(_messages.value).sort();\n },\n messages,\n get modifiers() {\n return _modifiers;\n },\n get pluralRules() {\n return _pluralRules || {};\n },\n get isGlobal() {\n return _isGlobal;\n },\n get missingWarn() {\n return _missingWarn;\n },\n set missingWarn(val) {\n _missingWarn = val;\n _context.missingWarn = _missingWarn;\n },\n get fallbackWarn() {\n return _fallbackWarn;\n },\n set fallbackWarn(val) {\n _fallbackWarn = val;\n _context.fallbackWarn = _fallbackWarn;\n },\n get fallbackRoot() {\n return _fallbackRoot;\n },\n set fallbackRoot(val) {\n _fallbackRoot = val;\n },\n get fallbackFormat() {\n return _fallbackFormat;\n },\n set fallbackFormat(val) {\n _fallbackFormat = val;\n _context.fallbackFormat = _fallbackFormat;\n },\n get warnHtmlMessage() {\n return _warnHtmlMessage;\n },\n set warnHtmlMessage(val) {\n _warnHtmlMessage = val;\n _context.warnHtmlMessage = val;\n },\n get escapeParameter() {\n return _escapeParameter;\n },\n set escapeParameter(val) {\n _escapeParameter = val;\n _context.escapeParameter = val;\n },\n t,\n getLocaleMessage,\n setLocaleMessage,\n mergeLocaleMessage,\n getPostTranslationHandler,\n setPostTranslationHandler,\n getMissingHandler,\n setMissingHandler,\n [SetPluralRulesSymbol]: setPluralRules\n };\n {\n composer.datetimeFormats = datetimeFormats;\n composer.numberFormats = numberFormats;\n composer.rt = rt;\n composer.te = te;\n composer.tm = tm;\n composer.d = d;\n composer.n = n;\n composer.getDateTimeFormat = getDateTimeFormat;\n composer.setDateTimeFormat = setDateTimeFormat;\n composer.mergeDateTimeFormat = mergeDateTimeFormat;\n composer.getNumberFormat = getNumberFormat;\n composer.setNumberFormat = setNumberFormat;\n composer.mergeNumberFormat = mergeNumberFormat;\n composer[InejctWithOptionSymbol] = __injectWithOption;\n composer[TranslateVNodeSymbol] = translateVNode;\n composer[DatetimePartsSymbol] = datetimeParts;\n composer[NumberPartsSymbol] = numberParts;\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n composer[EnableEmitter] = (emitter) => {\n _context.__v_emitter = emitter;\n };\n composer[DisableEmitter] = () => {\n _context.__v_emitter = undefined;\n };\n }\n return composer;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst VUE_I18N_COMPONENT_TYPES = 'vue-i18n: composer properties';\nconst VueDevToolsLabels = {\n 'vue-devtools-plugin-vue-i18n': 'Vue I18n DevTools',\n 'vue-i18n-resource-inspector': 'Vue I18n DevTools',\n 'vue-i18n-timeline': 'Vue I18n'\n};\nconst VueDevToolsPlaceholders = {\n 'vue-i18n-resource-inspector': 'Search for scopes ...'\n};\nconst VueDevToolsTimelineColors = {\n 'vue-i18n-timeline': 0xffcd19\n};\nlet devtoolsApi;\nasync function enableDevTools(app, i18n) {\n return new Promise((resolve, reject) => {\n try {\n setupDevtoolsPlugin({\n id: 'vue-devtools-plugin-vue-i18n',\n label: VueDevToolsLabels['vue-devtools-plugin-vue-i18n'],\n packageName: 'vue-i18n',\n homepage: 'https://vue-i18n.intlify.dev',\n logo: 'https://vue-i18n.intlify.dev/vue-i18n-devtools-logo.png',\n componentStateTypes: [VUE_I18N_COMPONENT_TYPES],\n app: app // eslint-disable-line @typescript-eslint/no-explicit-any\n }, api => {\n devtoolsApi = api;\n api.on.visitComponentTree(({ componentInstance, treeNode }) => {\n updateComponentTreeTags(componentInstance, treeNode, i18n);\n });\n api.on.inspectComponent(({ componentInstance, instanceData }) => {\n if (componentInstance.vnode.el &&\n componentInstance.vnode.el.__VUE_I18N__ &&\n instanceData) {\n if (i18n.mode === 'legacy') {\n // ignore global scope on legacy mode\n if (componentInstance.vnode.el.__VUE_I18N__ !==\n i18n.global.__composer) {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n else {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n });\n api.addInspector({\n id: 'vue-i18n-resource-inspector',\n label: VueDevToolsLabels['vue-i18n-resource-inspector'],\n icon: 'language',\n treeFilterPlaceholder: VueDevToolsPlaceholders['vue-i18n-resource-inspector']\n });\n api.on.getInspectorTree(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n registerScope(payload, i18n);\n }\n });\n const roots = new Map();\n api.on.getInspectorState(async (payload) => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n api.unhighlightElement();\n inspectScope(payload, i18n);\n if (payload.nodeId === 'global') {\n if (!roots.has(payload.app)) {\n const [root] = await api.getComponentInstances(payload.app);\n roots.set(payload.app, root);\n }\n api.highlightElement(roots.get(payload.app));\n }\n else {\n const instance = getComponentInstance(payload.nodeId, i18n);\n instance && api.highlightElement(instance);\n }\n }\n });\n api.on.editInspectorState(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n editScope(payload, i18n);\n }\n });\n api.addTimelineLayer({\n id: 'vue-i18n-timeline',\n label: VueDevToolsLabels['vue-i18n-timeline'],\n color: VueDevToolsTimelineColors['vue-i18n-timeline']\n });\n resolve(true);\n });\n }\n catch (e) {\n console.error(e);\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n reject(false);\n }\n });\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getI18nScopeLable(instance) {\n return (instance.type.name ||\n instance.type.displayName ||\n instance.type.__file ||\n 'Anonymous');\n}\nfunction updateComponentTreeTags(instance, // eslint-disable-line @typescript-eslint/no-explicit-any\ntreeNode, i18n) {\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n if (instance && instance.vnode.el && instance.vnode.el.__VUE_I18N__) {\n // add custom tags local scope only\n if (instance.vnode.el.__VUE_I18N__ !== global) {\n const tag = {\n label: `i18n (${getI18nScopeLable(instance)} Scope)`,\n textColor: 0x000000,\n backgroundColor: 0xffcd19\n };\n treeNode.tags.push(tag);\n }\n }\n}\nfunction inspectComposer(instanceData, composer) {\n const type = VUE_I18N_COMPONENT_TYPES;\n instanceData.state.push({\n type,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n });\n instanceData.state.push({\n type,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n });\n instanceData.state.push({\n type,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n });\n instanceData.state.push({\n type,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n });\n instanceData.state.push({\n type,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n });\n {\n instanceData.state.push({\n type,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n });\n instanceData.state.push({\n type,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n });\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getLocaleMessageValue(messages) {\n const value = {};\n Object.keys(messages).forEach((key) => {\n const v = messages[key];\n if (isFunction(v) && 'source' in v) {\n value[key] = getMessageFunctionDetails(v);\n }\n else if (isMessageAST(v) && v.loc && v.loc.source) {\n value[key] = v.loc.source;\n }\n else if (isObject(v)) {\n value[key] = getLocaleMessageValue(v);\n }\n else {\n value[key] = v;\n }\n });\n return value;\n}\nconst ESC = {\n '<': '<',\n '>': '>',\n '\"': '"',\n '&': '&'\n};\nfunction escape(s) {\n return s.replace(/[<>\"&]/g, escapeChar);\n}\nfunction escapeChar(a) {\n return ESC[a] || a;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getMessageFunctionDetails(func) {\n const argString = func.source ? `(\"${escape(func.source)}\")` : `(?)`;\n return {\n _custom: {\n type: 'function',\n display: `ƒ ${argString}`\n }\n };\n}\nfunction registerScope(payload, i18n) {\n payload.rootNodes.push({\n id: 'global',\n label: 'Global Scope'\n });\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n for (const [keyInstance, instance] of i18n.__instances) {\n // prettier-ignore\n const composer = i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n if (global === composer) {\n continue;\n }\n payload.rootNodes.push({\n id: composer.id.toString(),\n label: `${getI18nScopeLable(keyInstance)} Scope`\n });\n }\n}\nfunction getComponentInstance(nodeId, i18n) {\n let instance = null;\n if (nodeId !== 'global') {\n for (const [component, composer] of i18n.__instances.entries()) {\n if (composer.id.toString() === nodeId) {\n instance = component;\n break;\n }\n }\n }\n return instance;\n}\nfunction getComposer$2(nodeId, i18n) {\n if (nodeId === 'global') {\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n }\n else {\n const instance = Array.from(i18n.__instances.values()).find(item => item.id.toString() === nodeId);\n if (instance) {\n return i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n }\n else {\n return null;\n }\n }\n}\nfunction inspectScope(payload, i18n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n // TODO:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n payload.state = makeScopeInspectState(composer);\n }\n return null;\n}\nfunction makeScopeInspectState(composer) {\n const state = {};\n const localeType = 'Locale related info';\n const localeStates = [\n {\n type: localeType,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n },\n {\n type: localeType,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n },\n {\n type: localeType,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n },\n {\n type: localeType,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n }\n ];\n state[localeType] = localeStates;\n const localeMessagesType = 'Locale messages info';\n const localeMessagesStates = [\n {\n type: localeMessagesType,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n }\n ];\n state[localeMessagesType] = localeMessagesStates;\n {\n const datetimeFormatsType = 'Datetime formats info';\n const datetimeFormatsStates = [\n {\n type: datetimeFormatsType,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n }\n ];\n state[datetimeFormatsType] = datetimeFormatsStates;\n const numberFormatsType = 'Datetime formats info';\n const numberFormatsStates = [\n {\n type: numberFormatsType,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n }\n ];\n state[numberFormatsType] = numberFormatsStates;\n }\n return state;\n}\nfunction addTimelineEvent(event, payload) {\n if (devtoolsApi) {\n let groupId;\n if (payload && 'groupId' in payload) {\n groupId = payload.groupId;\n delete payload.groupId;\n }\n devtoolsApi.addTimelineEvent({\n layerId: 'vue-i18n-timeline',\n event: {\n title: event,\n groupId,\n time: Date.now(),\n meta: {},\n data: payload || {},\n logType: event === 'compile-error'\n ? 'error'\n : event === 'fallback' || event === 'missing'\n ? 'warning'\n : 'default'\n }\n });\n }\n}\nfunction editScope(payload, i18n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n const [field] = payload.path;\n if (field === 'locale' && isString(payload.state.value)) {\n composer.locale.value = payload.state.value;\n }\n else if (field === 'fallbackLocale' &&\n (isString(payload.state.value) ||\n isArray(payload.state.value) ||\n isObject(payload.state.value))) {\n composer.fallbackLocale.value = payload.state.value;\n }\n else if (field === 'inheritLocale' && isBoolean(payload.state.value)) {\n composer.inheritLocale = payload.state.value;\n }\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Convert to I18n Composer Options from VueI18n Options\n *\n * @internal\n */\nfunction convertComposerOptions(options) {\n const locale = isString(options.locale) ? options.locale : DEFAULT_LOCALE;\n const fallbackLocale = isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : locale;\n const missing = isFunction(options.missing) ? options.missing : undefined;\n const missingWarn = isBoolean(options.silentTranslationWarn) ||\n isRegExp(options.silentTranslationWarn)\n ? !options.silentTranslationWarn\n : true;\n const fallbackWarn = isBoolean(options.silentFallbackWarn) ||\n isRegExp(options.silentFallbackWarn)\n ? !options.silentFallbackWarn\n : true;\n const fallbackRoot = isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n const fallbackFormat = !!options.formatFallbackMessages;\n const modifiers = isPlainObject(options.modifiers) ? options.modifiers : {};\n const pluralizationRules = options.pluralizationRules;\n const postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : undefined;\n const warnHtmlMessage = isString(options.warnHtmlInMessage)\n ? options.warnHtmlInMessage !== 'off'\n : true;\n const escapeParameter = !!options.escapeParameterHtml;\n const inheritLocale = isBoolean(options.sync) ? options.sync : true;\n let messages = options.messages;\n if (isPlainObject(options.sharedMessages)) {\n const sharedMessages = options.sharedMessages;\n const locales = Object.keys(sharedMessages);\n messages = locales.reduce((messages, locale) => {\n const message = messages[locale] || (messages[locale] = {});\n assign(message, sharedMessages[locale]);\n return messages;\n }, (messages || {}));\n }\n const { __i18n, __root, __injectWithOption } = options;\n const datetimeFormats = options.datetimeFormats;\n const numberFormats = options.numberFormats;\n const flatJson = options.flatJson;\n return {\n locale,\n fallbackLocale,\n messages,\n flatJson,\n datetimeFormats,\n numberFormats,\n missing,\n missingWarn,\n fallbackWarn,\n fallbackRoot,\n fallbackFormat,\n modifiers,\n pluralRules: pluralizationRules,\n postTranslation,\n warnHtmlMessage,\n escapeParameter,\n messageResolver: options.messageResolver,\n inheritLocale,\n __i18n,\n __root,\n __injectWithOption\n };\n}\n/**\n * create VueI18n interface factory\n *\n * @internal\n *\n * @deprecated will be removed at vue-i18n v12\n */\nfunction createVueI18n(options = {}) {\n const composer = createComposer(convertComposerOptions(options));\n const { __extender } = options;\n // defines VueI18n\n const vueI18n = {\n // id\n id: composer.id,\n // locale\n get locale() {\n return composer.locale.value;\n },\n set locale(val) {\n composer.locale.value = val;\n },\n // fallbackLocale\n get fallbackLocale() {\n return composer.fallbackLocale.value;\n },\n set fallbackLocale(val) {\n composer.fallbackLocale.value = val;\n },\n // messages\n get messages() {\n return composer.messages.value;\n },\n // datetimeFormats\n get datetimeFormats() {\n return composer.datetimeFormats.value;\n },\n // numberFormats\n get numberFormats() {\n return composer.numberFormats.value;\n },\n // availableLocales\n get availableLocales() {\n return composer.availableLocales;\n },\n // missing\n get missing() {\n return composer.getMissingHandler();\n },\n set missing(handler) {\n composer.setMissingHandler(handler);\n },\n // silentTranslationWarn\n get silentTranslationWarn() {\n return isBoolean(composer.missingWarn)\n ? !composer.missingWarn\n : composer.missingWarn;\n },\n set silentTranslationWarn(val) {\n composer.missingWarn = isBoolean(val) ? !val : val;\n },\n // silentFallbackWarn\n get silentFallbackWarn() {\n return isBoolean(composer.fallbackWarn)\n ? !composer.fallbackWarn\n : composer.fallbackWarn;\n },\n set silentFallbackWarn(val) {\n composer.fallbackWarn = isBoolean(val) ? !val : val;\n },\n // modifiers\n get modifiers() {\n return composer.modifiers;\n },\n // formatFallbackMessages\n get formatFallbackMessages() {\n return composer.fallbackFormat;\n },\n set formatFallbackMessages(val) {\n composer.fallbackFormat = val;\n },\n // postTranslation\n get postTranslation() {\n return composer.getPostTranslationHandler();\n },\n set postTranslation(handler) {\n composer.setPostTranslationHandler(handler);\n },\n // sync\n get sync() {\n return composer.inheritLocale;\n },\n set sync(val) {\n composer.inheritLocale = val;\n },\n // warnInHtmlMessage\n get warnHtmlInMessage() {\n return composer.warnHtmlMessage ? 'warn' : 'off';\n },\n set warnHtmlInMessage(val) {\n composer.warnHtmlMessage = val !== 'off';\n },\n // escapeParameterHtml\n get escapeParameterHtml() {\n return composer.escapeParameter;\n },\n set escapeParameterHtml(val) {\n composer.escapeParameter = val;\n },\n // pluralizationRules\n get pluralizationRules() {\n return composer.pluralRules || {};\n },\n // for internal\n __composer: composer,\n // t\n t(...args) {\n return Reflect.apply(composer.t, composer, [...args]);\n },\n // rt\n rt(...args) {\n return Reflect.apply(composer.rt, composer, [...args]);\n },\n // te\n te(key, locale) {\n return composer.te(key, locale);\n },\n // tm\n tm(key) {\n return composer.tm(key);\n },\n // getLocaleMessage\n getLocaleMessage(locale) {\n return composer.getLocaleMessage(locale);\n },\n // setLocaleMessage\n setLocaleMessage(locale, message) {\n composer.setLocaleMessage(locale, message);\n },\n // mergeLocaleMessage\n mergeLocaleMessage(locale, message) {\n composer.mergeLocaleMessage(locale, message);\n },\n // d\n d(...args) {\n return Reflect.apply(composer.d, composer, [...args]);\n },\n // getDateTimeFormat\n getDateTimeFormat(locale) {\n return composer.getDateTimeFormat(locale);\n },\n // setDateTimeFormat\n setDateTimeFormat(locale, format) {\n composer.setDateTimeFormat(locale, format);\n },\n // mergeDateTimeFormat\n mergeDateTimeFormat(locale, format) {\n composer.mergeDateTimeFormat(locale, format);\n },\n // n\n n(...args) {\n return Reflect.apply(composer.n, composer, [...args]);\n },\n // getNumberFormat\n getNumberFormat(locale) {\n return composer.getNumberFormat(locale);\n },\n // setNumberFormat\n setNumberFormat(locale, format) {\n composer.setNumberFormat(locale, format);\n },\n // mergeNumberFormat\n mergeNumberFormat(locale, format) {\n composer.mergeNumberFormat(locale, format);\n }\n };\n vueI18n.__extender = __extender;\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n vueI18n.__enableEmitter = (emitter) => {\n const __composer = composer;\n __composer[EnableEmitter] && __composer[EnableEmitter](emitter);\n };\n vueI18n.__disableEmitter = () => {\n const __composer = composer;\n __composer[DisableEmitter] && __composer[DisableEmitter]();\n };\n }\n return vueI18n;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Supports compatibility for legacy vue-i18n APIs\n * This mixin is used when we use vue-i18n@v9.x or later\n */\nfunction defineMixin(vuei18n, composer, i18n) {\n return {\n beforeCreate() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const options = this.$options;\n if (options.i18n) {\n const optionsI18n = options.i18n;\n if (options.__i18n) {\n optionsI18n.__i18n = options.__i18n;\n }\n optionsI18n.__root = composer;\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, optionsI18n);\n }\n else {\n optionsI18n.__injectWithOption = true;\n optionsI18n.__extender = i18n.__vueI18nExtend;\n // atttach local VueI18n instance\n this.$i18n = createVueI18n(optionsI18n);\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else if (options.__i18n) {\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, options);\n }\n else {\n // atttach local VueI18n instance\n this.$i18n = createVueI18n({\n __i18n: options.__i18n,\n __injectWithOption: true,\n __extender: i18n.__vueI18nExtend,\n __root: composer\n });\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else {\n // attach global VueI18n instance\n this.$i18n = vuei18n;\n }\n if (options.__i18nGlobal) {\n adjustI18nResources(composer, options, options);\n }\n // defines vue-i18n legacy APIs\n this.$t = (...args) => this.$i18n.t(...args);\n this.$rt = (...args) => this.$i18n.rt(...args);\n this.$te = (key, locale) => this.$i18n.te(key, locale);\n this.$d = (...args) => this.$i18n.d(...args);\n this.$n = (...args) => this.$i18n.n(...args);\n this.$tm = (key) => this.$i18n.tm(key);\n i18n.__setInstance(instance, this.$i18n);\n },\n mounted() {\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$i18n) {\n const _vueI18n = this.$i18n;\n this.$el.__VUE_I18N__ = _vueI18n.__composer;\n const emitter = (this.__v_emitter =\n createEmitter());\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n emitter.on('*', addTimelineEvent);\n }\n },\n unmounted() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const _vueI18n = this.$i18n;\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$el.__VUE_I18N__) {\n if (this.__v_emitter) {\n this.__v_emitter.off('*', addTimelineEvent);\n delete this.__v_emitter;\n }\n if (this.$i18n) {\n _vueI18n.__disableEmitter && _vueI18n.__disableEmitter();\n delete this.$el.__VUE_I18N__;\n }\n }\n delete this.$t;\n delete this.$rt;\n delete this.$te;\n delete this.$d;\n delete this.$n;\n delete this.$tm;\n if (_vueI18n.__disposer) {\n _vueI18n.__disposer();\n delete _vueI18n.__disposer;\n delete _vueI18n.__extender;\n }\n i18n.__deleteInstance(instance);\n delete this.$i18n;\n }\n };\n}\nfunction mergeToGlobal(g, options) {\n g.locale = options.locale || g.locale;\n g.fallbackLocale = options.fallbackLocale || g.fallbackLocale;\n g.missing = options.missing || g.missing;\n g.silentTranslationWarn =\n options.silentTranslationWarn || g.silentFallbackWarn;\n g.silentFallbackWarn = options.silentFallbackWarn || g.silentFallbackWarn;\n g.formatFallbackMessages =\n options.formatFallbackMessages || g.formatFallbackMessages;\n g.postTranslation = options.postTranslation || g.postTranslation;\n g.warnHtmlInMessage = options.warnHtmlInMessage || g.warnHtmlInMessage;\n g.escapeParameterHtml = options.escapeParameterHtml || g.escapeParameterHtml;\n g.sync = options.sync || g.sync;\n g.__composer[SetPluralRulesSymbol](options.pluralizationRules || g.pluralizationRules);\n const messages = getLocaleMessages(g.locale, {\n messages: options.messages,\n __i18n: options.__i18n\n });\n Object.keys(messages).forEach(locale => g.mergeLocaleMessage(locale, messages[locale]));\n if (options.datetimeFormats) {\n Object.keys(options.datetimeFormats).forEach(locale => g.mergeDateTimeFormat(locale, options.datetimeFormats[locale]));\n }\n if (options.numberFormats) {\n Object.keys(options.numberFormats).forEach(locale => g.mergeNumberFormat(locale, options.numberFormats[locale]));\n }\n return g;\n}\n\nconst baseFormatProps = {\n tag: {\n type: [String, Object]\n },\n locale: {\n type: String\n },\n scope: {\n type: String,\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n validator: (val /* ComponentI18nScope */) => val === 'parent' || val === 'global',\n default: 'parent' /* ComponentI18nScope */\n },\n i18n: {\n type: Object\n }\n};\n\nfunction getInterpolateArg(\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n{ slots }, // SetupContext,\nkeys) {\n if (keys.length === 1 && keys[0] === 'default') {\n // default slot with list\n const ret = slots.default ? slots.default() : [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return ret.reduce((slot, current) => {\n return [\n ...slot,\n // prettier-ignore\n ...(current.type === Fragment ? current.children : [current])\n ];\n }, []);\n }\n else {\n // named slots\n return keys.reduce((arg, key) => {\n const slot = slots[key];\n if (slot) {\n arg[key] = slot();\n }\n return arg;\n }, create());\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getFragmentableTag() {\n return Fragment;\n}\n\nconst TranslationImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-t',\n props: assign({\n keypath: {\n type: String,\n required: true\n },\n plural: {\n type: [Number, String],\n validator: (val) => isNumber(val) || !isNaN(val)\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const { slots, attrs } = context;\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return () => {\n const keys = Object.keys(slots).filter(key => key[0] !== '_');\n const options = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (props.plural !== undefined) {\n options.plural = isString(props.plural) ? +props.plural : props.plural;\n }\n const arg = getInterpolateArg(context, keys);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const children = i18n[TranslateVNodeSymbol](props.keypath, arg, options);\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Translation Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [TranslationProps](component#translationprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Component Interpolation](../guide/advanced/component)\n *\n * @example\n * ```html\n *
\n * \n * \n * {{ $t('tos') }}\n * \n * \n *
\n * ```\n * ```js\n * import { createApp } from 'vue'\n * import { createI18n } from 'vue-i18n'\n *\n * const messages = {\n * en: {\n * tos: 'Term of Service',\n * term: 'I accept xxx {0}.'\n * },\n * ja: {\n * tos: '利用規約',\n * term: '私は xxx の{0}に同意します。'\n * }\n * }\n *\n * const i18n = createI18n({\n * locale: 'en',\n * messages\n * })\n *\n * const app = createApp({\n * data: {\n * url: '/term'\n * }\n * }).use(i18n).mount('#app')\n * ```\n *\n * @VueI18nComponent\n */\nconst Translation = TranslationImpl;\nconst I18nT = Translation;\n\nfunction isVNode(target) {\n return isArray(target) && !isString(target[0]);\n}\nfunction renderFormatter(props, context, slotKeys, partFormatter) {\n const { slots, attrs } = context;\n return () => {\n const options = { part: true };\n let overrides = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (isString(props.format)) {\n options.key = props.format;\n }\n else if (isObject(props.format)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (isString(props.format.key)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options.key = props.format.key;\n }\n // Filter out number format options only\n overrides = Object.keys(props.format).reduce((options, prop) => {\n return slotKeys.includes(prop)\n ? assign(create(), options, { [prop]: props.format[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any\n : options;\n }, create());\n }\n const parts = partFormatter(...[props.value, options, overrides]);\n let children = [options.key];\n if (isArray(parts)) {\n children = parts.map((part, index) => {\n const slot = slots[part.type];\n const node = slot\n ? slot({ [part.type]: part.value, index, parts })\n : [part.value];\n if (isVNode(node)) {\n node[0].key = `${part.type}-${index}`;\n }\n return node;\n });\n }\n else if (isString(parts)) {\n children = [parts];\n }\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n}\n\nconst NumberFormatImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-n',\n props: assign({\n value: {\n type: Number,\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, NUMBER_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[NumberPartsSymbol](...args));\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Number Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/number#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.NumberFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-numberformat)\n *\n * @VueI18nComponent\n */\nconst NumberFormat = NumberFormatImpl;\nconst I18nN = NumberFormat;\n\nfunction getComposer$1(i18n, instance) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n return (i18nInternal.__getInstance(instance) || i18n.global);\n }\n else {\n const vueI18n = i18nInternal.__getInstance(instance);\n return vueI18n != null\n ? vueI18n.__composer\n : i18n.global.__composer;\n }\n}\n/**\n * @deprecated will be removed at vue-i18n v12\n */\nfunction vTDirective(i18n) {\n const _process = (binding) => {\n if ((process.env.NODE_ENV !== 'production')) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE));\n }\n const { instance, value } = binding;\n /* istanbul ignore if */\n if (!instance || !instance.$) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const composer = getComposer$1(i18n, instance.$);\n const parsedValue = parseValue(value);\n return [\n Reflect.apply(composer.t, composer, [...makeParams(parsedValue)]),\n composer\n ];\n };\n const register = (el, binding) => {\n const [textContent, composer] = _process(binding);\n if (inBrowser && i18n.global === composer) {\n // global scope only\n el.__i18nWatcher = watch(composer.locale, () => {\n binding.instance && binding.instance.$forceUpdate();\n });\n }\n el.__composer = composer;\n el.textContent = textContent;\n };\n const unregister = (el) => {\n if (inBrowser && el.__i18nWatcher) {\n el.__i18nWatcher();\n el.__i18nWatcher = undefined;\n delete el.__i18nWatcher;\n }\n if (el.__composer) {\n el.__composer = undefined;\n delete el.__composer;\n }\n };\n const update = (el, { value }) => {\n if (el.__composer) {\n const composer = el.__composer;\n const parsedValue = parseValue(value);\n el.textContent = Reflect.apply(composer.t, composer, [\n ...makeParams(parsedValue)\n ]);\n }\n };\n const getSSRProps = (binding) => {\n const [textContent] = _process(binding);\n return { textContent };\n };\n return {\n created: register,\n unmounted: unregister,\n beforeUpdate: update,\n getSSRProps\n };\n}\nfunction parseValue(value) {\n if (isString(value)) {\n return { path: value };\n }\n else if (isPlainObject(value)) {\n if (!('path' in value)) {\n throw createI18nError(I18nErrorCodes.REQUIRED_VALUE, 'path');\n }\n return value;\n }\n else {\n throw createI18nError(I18nErrorCodes.INVALID_VALUE);\n }\n}\nfunction makeParams(value) {\n const { path, locale, args, choice, plural } = value;\n const options = {};\n const named = args || {};\n if (isString(locale)) {\n options.locale = locale;\n }\n if (isNumber(choice)) {\n options.plural = choice;\n }\n if (isNumber(plural)) {\n options.plural = plural;\n }\n return [path, named, options];\n}\n\nfunction apply(app, i18n, ...options) {\n const pluginOptions = isPlainObject(options[0])\n ? options[0]\n : {};\n const globalInstall = isBoolean(pluginOptions.globalInstall)\n ? pluginOptions.globalInstall\n : true;\n if (globalInstall) {\n [Translation.name, 'I18nT'].forEach(name => app.component(name, Translation));\n [NumberFormat.name, 'I18nN'].forEach(name => app.component(name, NumberFormat));\n [DatetimeFormat.name, 'I18nD'].forEach(name => app.component(name, DatetimeFormat));\n }\n // install directive\n {\n app.directive('t', vTDirective(i18n));\n }\n}\n\n/**\n * Injection key for {@link useI18n}\n *\n * @remarks\n * The global injection key for I18n instances with `useI18n`. this injection key is used in Web Components.\n * Specify the i18n instance created by {@link createI18n} together with `provide` function.\n *\n * @VueI18nGeneral\n */\nconst I18nInjectionKey = \n/* #__PURE__*/ makeSymbol('global-vue-i18n');\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction createI18n(options = {}) {\n // prettier-ignore\n const __legacyMode = __VUE_I18N_LEGACY_API__ && isBoolean(options.legacy)\n ? options.legacy\n : __VUE_I18N_LEGACY_API__;\n if ((process.env.NODE_ENV !== 'production') && __legacyMode) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_LEGACY_MODE));\n }\n // prettier-ignore\n const __globalInjection = isBoolean(options.globalInjection)\n ? options.globalInjection\n : true;\n const __instances = new Map();\n const [globalScope, __global] = createGlobal(options, __legacyMode);\n const symbol = /* #__PURE__*/ makeSymbol((process.env.NODE_ENV !== 'production') ? 'vue-i18n' : '');\n function __getInstance(component) {\n return __instances.get(component) || null;\n }\n function __setInstance(component, instance) {\n __instances.set(component, instance);\n }\n function __deleteInstance(component) {\n __instances.delete(component);\n }\n const i18n = {\n // mode\n get mode() {\n return __VUE_I18N_LEGACY_API__ && __legacyMode\n ? 'legacy'\n : 'composition';\n },\n // install plugin\n async install(app, ...options) {\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n app.__VUE_I18N__ = i18n;\n }\n // setup global provider\n app.__VUE_I18N_SYMBOL__ = symbol;\n app.provide(app.__VUE_I18N_SYMBOL__, i18n);\n // set composer & vuei18n extend hook options from plugin options\n if (isPlainObject(options[0])) {\n const opts = options[0];\n i18n.__composerExtend =\n opts.__composerExtend;\n i18n.__vueI18nExtend =\n opts.__vueI18nExtend;\n }\n // global method and properties injection for Composition API\n let globalReleaseHandler = null;\n if (!__legacyMode && __globalInjection) {\n globalReleaseHandler = injectGlobalFields(app, i18n.global);\n }\n // install built-in components and directive\n if (__VUE_I18N_FULL_INSTALL__) {\n apply(app, i18n, ...options);\n }\n // setup mixin for Legacy API\n if (__VUE_I18N_LEGACY_API__ && __legacyMode) {\n app.mixin(defineMixin(__global, __global.__composer, i18n));\n }\n // release global scope\n const unmountApp = app.unmount;\n app.unmount = () => {\n globalReleaseHandler && globalReleaseHandler();\n i18n.dispose();\n unmountApp();\n };\n // setup vue-devtools plugin\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n const ret = await enableDevTools(app, i18n);\n if (!ret) {\n throw createI18nError(I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN);\n }\n const emitter = createEmitter();\n if (__legacyMode) {\n const _vueI18n = __global;\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = __global;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n }\n emitter.on('*', addTimelineEvent);\n }\n },\n // global accessor\n get global() {\n return __global;\n },\n dispose() {\n globalScope.stop();\n },\n // @internal\n __instances,\n // @internal\n __getInstance,\n // @internal\n __setInstance,\n // @internal\n __deleteInstance\n };\n return i18n;\n}\nfunction useI18n(options = {}) {\n const instance = getCurrentInstance();\n if (instance == null) {\n throw createI18nError(I18nErrorCodes.MUST_BE_CALL_SETUP_TOP);\n }\n if (!instance.isCE &&\n instance.appContext.app != null &&\n !instance.appContext.app.__VUE_I18N_SYMBOL__) {\n throw createI18nError(I18nErrorCodes.NOT_INSTALLED);\n }\n const i18n = getI18nInstance(instance);\n const gl = getGlobalComposer(i18n);\n const componentOptions = getComponentOptions(instance);\n const scope = getScope(options, componentOptions);\n if (scope === 'global') {\n adjustI18nResources(gl, options, componentOptions);\n return gl;\n }\n if (scope === 'parent') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let composer = getComposer(i18n, instance, options.__useComponent);\n if (composer == null) {\n if ((process.env.NODE_ENV !== 'production')) {\n warn(getWarnMessage(I18nWarnCodes.NOT_FOUND_PARENT_SCOPE));\n }\n composer = gl;\n }\n return composer;\n }\n const i18nInternal = i18n;\n let composer = i18nInternal.__getInstance(instance);\n if (composer == null) {\n const composerOptions = assign({}, options);\n if ('__i18n' in componentOptions) {\n composerOptions.__i18n = componentOptions.__i18n;\n }\n if (gl) {\n composerOptions.__root = gl;\n }\n composer = createComposer(composerOptions);\n if (i18nInternal.__composerExtend) {\n composer[DisposeSymbol] =\n i18nInternal.__composerExtend(composer);\n }\n setupLifeCycle(i18nInternal, instance, composer);\n i18nInternal.__setInstance(instance, composer);\n }\n else {\n if ((process.env.NODE_ENV !== 'production') && scope === 'local') {\n warn(getWarnMessage(I18nWarnCodes.DUPLICATE_USE_I18N_CALLING));\n }\n }\n return composer;\n}\nfunction createGlobal(options, legacyMode) {\n const scope = effectScope();\n const obj = __VUE_I18N_LEGACY_API__ && legacyMode\n ? scope.run(() => createVueI18n(options))\n : scope.run(() => createComposer(options));\n if (obj == null) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n return [scope, obj];\n}\nfunction getI18nInstance(instance) {\n const i18n = inject(!instance.isCE\n ? instance.appContext.app.__VUE_I18N_SYMBOL__\n : I18nInjectionKey);\n /* istanbul ignore if */\n if (!i18n) {\n throw createI18nError(!instance.isCE\n ? I18nErrorCodes.UNEXPECTED_ERROR\n : I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE);\n }\n return i18n;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getScope(options, componentOptions) {\n // prettier-ignore\n return isEmptyObject(options)\n ? ('__i18n' in componentOptions)\n ? 'local'\n : 'global'\n : !options.useScope\n ? 'local'\n : options.useScope;\n}\nfunction getGlobalComposer(i18n) {\n // prettier-ignore\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n}\nfunction getComposer(i18n, target, useComponent = false) {\n let composer = null;\n const root = target.root;\n let current = getParentComponentInstance(target, useComponent);\n while (current != null) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n composer = i18nInternal.__getInstance(current);\n }\n else {\n if (__VUE_I18N_LEGACY_API__) {\n const vueI18n = i18nInternal.__getInstance(current);\n if (vueI18n != null) {\n composer = vueI18n\n .__composer;\n if (useComponent &&\n composer &&\n !composer[InejctWithOptionSymbol] // eslint-disable-line @typescript-eslint/no-explicit-any\n ) {\n composer = null;\n }\n }\n }\n }\n if (composer != null) {\n break;\n }\n if (root === current) {\n break;\n }\n current = current.parent;\n }\n return composer;\n}\nfunction getParentComponentInstance(target, useComponent = false) {\n if (target == null) {\n return null;\n }\n // if `useComponent: true` will be specified, we get lexical scope owner instance for use-case slots\n return !useComponent\n ? target.parent\n : target.vnode.ctx || target.parent; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\nfunction setupLifeCycle(i18n, target, composer) {\n let emitter = null;\n onMounted(() => {\n // inject composer instance to DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el) {\n target.vnode.el.__VUE_I18N__ = composer;\n emitter = createEmitter();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n emitter.on('*', addTimelineEvent);\n }\n }, target);\n onUnmounted(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n // remove composer instance from DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el &&\n target.vnode.el.__VUE_I18N__) {\n emitter && emitter.off('*', addTimelineEvent);\n _composer[DisableEmitter] && _composer[DisableEmitter]();\n delete target.vnode.el.__VUE_I18N__;\n }\n i18n.__deleteInstance(target);\n // dispose extended resources\n const dispose = _composer[DisposeSymbol];\n if (dispose) {\n dispose();\n delete _composer[DisposeSymbol];\n }\n }, target);\n}\nconst globalExportProps = [\n 'locale',\n 'fallbackLocale',\n 'availableLocales'\n];\nconst globalExportMethods = ['t', 'rt', 'd', 'n', 'tm', 'te']\n ;\nfunction injectGlobalFields(app, composer) {\n const i18n = Object.create(null);\n globalExportProps.forEach(prop => {\n const desc = Object.getOwnPropertyDescriptor(composer, prop);\n if (!desc) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const wrap = isRef(desc.value) // check computed props\n ? {\n get() {\n return desc.value.value;\n },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n set(val) {\n desc.value.value = val;\n }\n }\n : {\n get() {\n return desc.get && desc.get();\n }\n };\n Object.defineProperty(i18n, prop, wrap);\n });\n app.config.globalProperties.$i18n = i18n;\n globalExportMethods.forEach(method => {\n const desc = Object.getOwnPropertyDescriptor(composer, method);\n if (!desc || !desc.value) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n Object.defineProperty(app.config.globalProperties, `$${method}`, desc);\n });\n const dispose = () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties.$i18n;\n globalExportMethods.forEach(method => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties[`$${method}`];\n });\n };\n return dispose;\n}\n\nconst DatetimeFormatImpl = /* #__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-d',\n props: assign({\n value: {\n type: [Number, Date],\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, DATETIME_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[DatetimePartsSymbol](...args));\n }\n});\n/**\n * Datetime Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/datetime#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.DateTimeFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-datetimeformat)\n *\n * @VueI18nComponent\n */\nconst DatetimeFormat = DatetimeFormatImpl;\nconst I18nD = DatetimeFormat;\n\n{\n initFeatureFlags();\n}\n// register message compiler at vue-i18n\nregisterMessageCompiler(compile);\n// register message resolver at vue-i18n\nregisterMessageResolver(resolveValue);\n// register fallback locale at vue-i18n\nregisterLocaleFallbacker(fallbackWithLocaleChain);\n// NOTE: experimental !!\nif ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n const target = getGlobalThis();\n target.__INTLIFY__ = true;\n setDevToolsHook(target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__);\n}\nif ((process.env.NODE_ENV !== 'production')) ;\n\nexport { DatetimeFormat, I18nD, I18nInjectionKey, I18nN, I18nT, NumberFormat, Translation, VERSION, createI18n, useI18n, vTDirective };\n","import { commonMessages } from '@/i18n/messages/common';\nimport { panelMessages } from '@/i18n/messages/panel';\nimport { runtimeMessages } from '@/i18n/messages/runtime';\nimport type { AppLocale, MessageModule } from '@/i18n/messages/types';\nimport { createI18n, type LocaleMessageValue, useI18n } from 'vue-i18n';\n\nconst allMessages = {\n ...commonMessages,\n ...panelMessages,\n ...runtimeMessages,\n};\n\nexport type MessageKey = keyof typeof allMessages;\nexport type TranslationParams = Record;\n\nexport function resolveMvuLocale(locale: string): AppLocale {\n return locale.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';\n}\n\nfunction getHostLocale(): string {\n try {\n return SillyTavern.getCurrentLocale();\n } catch {\n return document.documentElement.lang || navigator.language || 'en';\n }\n}\n\ntype LocaleMessages = Record;\n\nfunction buildLocaleMessages(locale: AppLocale): LocaleMessages {\n // 消息源使用便于类型检查的扁平键,这里再展开成 vue-i18n 所需的嵌套对象。\n const result: LocaleMessages = {};\n for (const [key, translations] of Object.entries(allMessages) as [\n MessageKey,\n MessageModule[string],\n ][]) {\n const path = key.split('.');\n const leaf = path.pop()!;\n let parent = result;\n for (const segment of path) {\n const existing = parent[segment];\n if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {\n parent[segment] = {};\n }\n parent = parent[segment] as LocaleMessages;\n }\n parent[leaf] = translations[locale];\n }\n return result;\n}\n\nexport const i18n = createI18n({\n legacy: false,\n globalInjection: false,\n locale: resolveMvuLocale(getHostLocale()),\n fallbackLocale: 'zh-CN',\n messages: {\n 'zh-CN': buildLocaleMessages('zh-CN'),\n en: buildLocaleMessages('en'),\n },\n missingWarn: false,\n fallbackWarn: false,\n warnHtmlMessage: false,\n});\n\nfunction translate(key: MessageKey, params?: TranslationParams): string {\n return params ? i18n.global.t(key, params) : i18n.global.t(key);\n}\n\nexport function tr(key: MessageKey, params?: TranslationParams): string {\n return translate(key, params);\n}\n\nexport function useMvuI18n() {\n const composer = useI18n({ useScope: 'global' });\n return {\n locale: composer.locale,\n t: (key: MessageKey, params?: TranslationParams) =>\n params ? composer.t(key, params) : composer.t(key),\n };\n}\n","declare const jest: any;\ndeclare const process: any;\n\nexport const is_jest_environment =\n typeof jest !== 'undefined' ||\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test');\n","import {\n migrateExtraModelApiProfiles,\n normalizeExtraModelPiProfileContextWindow,\n} from '@/function/update/extra_model_api_profiles';\nimport {\n CharacterSettingsOverride,\n CharacterSettingsOverridePath,\n getCharacterSettingsOverrideValue,\n hasActiveCharacterSettingsOverride,\n hasOwnCharacterSettingsOverride,\n} from '@/function/character_override/schema';\nimport { PI_THINKING_LEVELS } from '@/function/update/pi/thinking_setting';\nimport { is_jest_environment } from '@/jest';\nimport { registerAsUniqueScript } from '@util/script';\nimport { klona } from 'klona';\nimport { defineStore } from 'pinia';\nimport { computed, ref, toRaw, watch } from 'vue';\nimport * as z from 'zod';\n\n/**\n * 保留导入或界面中的无效窗口值,使其与合法的“0 = 使用目录值”明确区分。\n * 请求预检只接受 0 或正整数,避免错误配置静默回退到目录元数据。\n */\nconst ExtraModelPiContextWindow = z\n .union([z.number(), z.string()])\n .catch('__invalid_context_window__')\n .default(0)\n .transform((value): number | string => {\n if (typeof value === 'number') {\n return value;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : value;\n });\n\n// 方案快照使用规范化整数;无效值使整个连接快照失效,而不污染其余设置。\nconst ExtraModelPiProfileContextWindow = z\n .union([z.number(), z.string()])\n .transform(value => normalizeExtraModelPiProfileContextWindow(value))\n .pipe(z.number());\n\n// 方案只保存连接与请求覆盖,OAuth 凭证和按目标缓存的密钥由活动设置独立持有。\nconst ExtraModelPiProfileSnapshot = z\n .object({\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n provider: z.string().trim(),\n api: z.string().trim(),\n authType: z\n .string()\n .trim()\n .pipe(z.enum(['api_key', 'oauth'])),\n endpoint: z.string().trim(),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim(),\n contextWindow: ExtraModelPiProfileContextWindow,\n customHeaders: z.string(),\n customIncludeBody: z.string(),\n customExcludeBody: z.string(),\n })\n .loose()\n .transform(({ credentials: _credentials, apiKeys: _apiKeys, ...connection }) => connection);\n\n// 导入的残缺 Pi 快照不能靠默认服务商或协议补成可发送连接。\n// 保留外层方案,只丢弃无效连接,应用时再要求用户补全。\nconst ExtraModelPiProfile = z.union([ExtraModelPiProfileSnapshot, z.undefined()]).catch(undefined);\n\n// 活动连接容许保留未知字段,实际可发送性由统一预检决定。\nconst ExtraModelPiSettings = z\n .object({\n provider: z.string().trim().default('openai'),\n api: z.string().trim().default('openai-responses'),\n // 保留未知认证标识供用户查看;请求和方案保存会拒绝它,迁移会清除无归属的活动密钥。\n authType: z.string().trim().default('api_key'),\n endpoint: z.string().trim().default(''),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim().default(''),\n contextWindow: ExtraModelPiContextWindow,\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n credentials: z.record(z.string(), z.unknown()).catch({}).default({}),\n apiKeys: z.record(z.string(), z.string()).catch({}).default({}),\n customHeaders: z.string().default(''),\n customIncludeBody: z.string().default(''),\n customExcludeBody: z.string().default(''),\n })\n .loose()\n .prefault({});\n\nexport const EXTRA_MODEL_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\nconst ExtraModelResponseFormat = z.enum(EXTRA_MODEL_RESPONSE_FORMATS);\nconst ExtraModelJailbreakStrategy = z.enum(['使用内置破限', '使用当前预设', '使用其他预设']);\n\nconst ExtraModelApiProfile = z\n .object({\n 名称: z.string().trim().min(1),\n backend: z.enum(['custom', 'pi']).default('custom'),\n api地址: z.string().default(''),\n 密钥: z.string().default(''),\n 模型名称: z.string().default(''),\n pi: ExtraModelPiProfile.optional(),\n // 缺失选项保持未设置,使旧自定义方案可以一次性继承当前请求选项。\n 破限方案: ExtraModelJailbreakStrategy.optional().catch(undefined),\n 其他预设名称: z.string().optional().catch(undefined),\n 随机头部: z.boolean().optional().catch(undefined),\n 应答格式: ExtraModelResponseFormat.optional().catch(undefined),\n 关闭thinking: z.boolean().optional().catch(undefined),\n 兼容假流式: z.boolean().optional().catch(undefined),\n })\n .loose()\n .transform(({ customApiKey: _customApiKey, ...profile }) => profile);\n\n// 逐项容错,单个损坏方案只被过滤,不使整份用户设置重置。\nconst ExtraModelApiProfileList = z\n .array(ExtraModelApiProfile.nullable().catch(null))\n .catch([])\n .transform(profiles =>\n profiles.filter(\n (profile): profile is z.infer => profile !== null\n )\n );\n\nconst OldSettings = z\n .object({\n 通知: z\n .object({\n 变量更新出错: z.boolean(),\n 额外模型解析中: z.boolean(),\n })\n .loose(),\n 更新方式: z.enum(['随AI输出', '额外模型解析']),\n 自动触发额外模型解析: z.boolean(),\n 额外模型解析配置: z\n .object({\n 发送预设: z.boolean(),\n 使用函数调用: z.boolean(),\n 模型来源: z.enum(['与插头相同', '自定义']),\n api地址: z.string(),\n 密钥: z.string(),\n 模型名称: z.string(),\n 温度: z.coerce.number(),\n 频率惩罚: z.coerce.number(),\n 存在惩罚: z.coerce.number(),\n top_p: z.coerce.number(),\n 最大回复token数: z.coerce.number(),\n })\n .loose(),\n 快照保留间隔: z.number(),\n 更新到聊天变量: z.boolean(),\n legacy: z\n .object({\n 显示老旧功能: z.boolean(),\n })\n .loose(),\n auto_cleanup: z\n .object({\n 启用: z.boolean(),\n 要保留变量的最近楼层数: z.number(),\n 触发恢复变量的最近楼层数: z.number(),\n })\n .loose(),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean(),\n 已提醒自动清理旧变量功能: z.boolean(),\n 已提醒更新了API温度等配置: z.boolean(),\n 已默认开启自动清理旧变量功能: z.boolean(),\n })\n .loose(),\n })\n .loose()\n .transform(data => {\n const {\n 自动触发额外模型解析,\n 额外模型解析配置: { 发送预设, 使用函数调用, ...extra_model_settings },\n 快照保留间隔,\n 更新到聊天变量,\n legacy,\n auto_cleanup,\n 自动清理变量: existing_auto_cleanup,\n 兼容性: existing_compatibility,\n ...settings\n } = data;\n const existing_auto_cleanup_settings = _.isPlainObject(existing_auto_cleanup)\n ? (existing_auto_cleanup as Record)\n : {};\n const existing_compatibility_settings = _.isPlainObject(existing_compatibility)\n ? (existing_compatibility as Record)\n : {};\n\n return NewSettings.decode({\n ...settings,\n 额外模型解析配置: {\n 破限方案: 发送预设 ? '使用当前预设' : '使用内置破限',\n 启用自动请求: 自动触发额外模型解析,\n 应答格式: 使用函数调用 ? '工具调用' : '聊天消息',\n ...extra_model_settings,\n },\n 自动清理变量: {\n ...auto_cleanup,\n 快照保留间隔,\n ...existing_auto_cleanup_settings,\n },\n 兼容性: {\n ...legacy,\n 更新到聊天变量,\n ...existing_compatibility_settings,\n },\n });\n });\n\nconst NewSettings = z\n .object({\n 通知: z\n .object({\n MVU框架加载成功: z.boolean().default(true),\n 变量初始化成功: z.boolean().default(true),\n 变量更新出错: z.boolean().default(false),\n 额外模型解析中: z.boolean().default(true),\n })\n .loose()\n .prefault({}),\n 更新方式: z.enum(['随AI输出', '额外模型解析']).default('随AI输出'),\n 额外模型解析配置: z\n .object({\n 破限方案: ExtraModelJailbreakStrategy.default('使用内置破限'),\n 其他预设名称: z.string().default(''),\n 使用函数调用: z.boolean().optional(),\n 应答格式: ExtraModelResponseFormat.optional(),\n 关闭thinking: z.boolean().default(false),\n 兼容假流式: z.boolean().default(false),\n 随机头部: z.boolean().default(true),\n\n 启用自动请求: z.boolean().default(true),\n 最小请求字数: z.coerce\n .number()\n .catch(0)\n .default(0)\n .transform(value => Math.max(0, Math.floor(value))),\n 触发标签: z.string().default(''),\n 请求方式: z\n .enum([\n '依次请求,失败后重试',\n '同时请求多次',\n '先请求一次, 失败后再同时请求多次',\n ])\n .default('依次请求,失败后重试'),\n 请求次数: z.number().default(3),\n 世界书条目白名单正则: z.string().default(''),\n 世界书条目黑名单正则: z.string().default(''),\n\n 模型来源: z.enum(['与插头相同', '自定义', '更多']).default('与插头相同'),\n api地址: z.string().default('http://localhost:1234/v1'),\n 密钥: z.string().default(''),\n customApiKey: z.string().catch('').default(''),\n 模型名称: z.string().default('gemini-2.5-flash-nothinking'),\n pi: ExtraModelPiSettings,\n 温度: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 2)),\n 频率惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n 存在惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n top_p: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 1)),\n top_k: z.coerce\n .number()\n .default(0)\n .transform(value => _.clamp(value, 0, 500)),\n max_chat_history: z.coerce\n .number()\n .default(2)\n .transform(value => _.clamp(Math.round(value), 2, 100)),\n 最大回复token数: z.coerce\n .number()\n // 回复 token 的 0 没有默认值语义,可保留为阻止请求的无效导入值。\n .catch(0)\n .default(4096)\n .transform(value => Math.max(0, value)),\n api方案列表: ExtraModelApiProfileList.default([]),\n 当前api方案: z.string().catch('').default(''),\n })\n .loose()\n .transform(({ 使用函数调用, 应答格式, ...data }) =>\n migrateExtraModelApiProfiles({\n ...data,\n 应答格式: 应答格式 ?? (使用函数调用 ? '工具调用' : '聊天消息'),\n })\n )\n .prefault({}),\n 自动清理变量: z\n .object({\n 启用: z.boolean().default(true),\n 快照保留间隔: z.number().default(50),\n 要保留变量的最近楼层数: z.number().default(20),\n 触发恢复变量的最近楼层数: z.number().default(10),\n })\n .loose()\n .prefault({}),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().default(false),\n 显示老旧功能: z.boolean().default(false),\n 额外模型解析非阻塞: z.boolean().default(false),\n sendas不视为user消息: z.boolean().optional(),\n sandas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .transform(({ sendas不视为user消息, sandas不视为user消息, ...data }) => ({\n ...data,\n 更新到聊天变量: data.更新到聊天变量,\n 显示老旧功能: data.显示老旧功能,\n sendas不视为user消息: sendas不视为user消息 ?? sandas不视为user消息 ?? false,\n }))\n .prefault({}),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean().default(false),\n 已提醒自动清理旧变量功能: z.boolean().default(false),\n 已提醒更新了API温度等配置: z.boolean().default(false),\n 已默认开启自动清理旧变量功能: z.boolean().default(false),\n 已提醒内置破限: z.boolean().default(false),\n 已提醒额外模型同时请求: z.boolean().default(false),\n 已开启默认不兼容假流式: z.boolean().default(false),\n })\n .loose()\n .prefault({}),\n })\n .loose()\n .transform(data => {\n // 新安装由 Schema 提供 false;迁移时保留已保存的流式选项及方案继承值。\n data.internal.已开启默认不兼容假流式 = true;\n return data;\n })\n .prefault({});\n\nconst Settings = z.union([OldSettings, NewSettings]).catch(() => NewSettings.parse({}));\n\nexport type MvuSettings = z.infer;\n\nexport type CharacterSettingsState = {\n status: 'loading' | 'unbound' | 'ready' | 'error';\n character_name: string;\n worldbook_name: string | null;\n entry_uid: number | null;\n expected_content: string | null;\n draft: CharacterSettingsOverride;\n is_valid: boolean;\n revision: number;\n has_pending_save: boolean;\n is_saving: boolean;\n};\n\nfunction createCharacterSettingsState(): CharacterSettingsState {\n return {\n status: 'loading',\n character_name: '',\n worldbook_name: null,\n entry_uid: null,\n expected_content: null,\n draft: {},\n is_valid: true,\n revision: 0,\n has_pending_save: false,\n is_saving: false,\n };\n}\n\nconst Runtimes = z\n .object({\n unsupported_warnings: z.string().default(''),\n is_during_extra_analysis: z.boolean().default(false),\n is_function_call_enabled: z.boolean().default(false),\n 上次世界书条目过滤结果: z\n .array(\n z.object({\n lore: z.enum(['globalLore', 'characterLore', 'chatLore', 'personaLore']),\n world: z.string(),\n comment: z.string(),\n reason: z.enum(['白名单', '黑名单']),\n sources: z.array(z.enum(['用户全局配置', '角色卡配置'])).default([]),\n })\n )\n .default([]),\n debug: z\n .object({\n 首次额外请求必失败: z.boolean().default(false),\n })\n .prefault({}),\n })\n .prefault({});\n\nexport const useDataStore = defineStore('MVU变量框架', () => {\n const settings = ref(Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {})));\n watch(\n settings,\n new_settings => {\n _.set(SillyTavern.extensionSettings, 'mvu_settings', toRaw(new_settings));\n if (!is_jest_environment) SillyTavern.saveSettingsDebounced();\n },\n { deep: true }\n );\n const _reload_settings = () => {\n settings.value = Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {}));\n };\n\n const character_settings = ref(createCharacterSettingsState());\n const effective_settings = computed(() => {\n // 运行时读取合并后的配置,但不修改用户持久化的全局设置。\n const result = klona(settings.value);\n if (!character_settings.value.is_valid) {\n return result;\n }\n\n const draft = character_settings.value.draft;\n // 正则采用“全局 + 角色卡”的叠加语义,由 filterEntries 单独合并,不在此处覆盖。\n if (_.has(draft, '更新方式')) {\n result.更新方式 = draft.更新方式!;\n }\n if (_.has(draft, '额外模型解析配置.启用自动请求')) {\n result.额外模型解析配置.启用自动请求 = draft.额外模型解析配置!.启用自动请求!;\n }\n if (_.has(draft, '兼容性.更新到聊天变量')) {\n result.兼容性.更新到聊天变量 = draft.兼容性!.更新到聊天变量!;\n }\n if (_.has(draft, '兼容性.sendas不视为user消息')) {\n result.兼容性.sendas不视为user消息 = draft.兼容性!.sendas不视为user消息!;\n }\n return result;\n });\n const has_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid &&\n hasOwnCharacterSettingsOverride(character_settings.value.draft, path);\n const get_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid\n ? getCharacterSettingsOverrideValue(character_settings.value.draft, path)\n : undefined;\n const is_character_settings_override_active = computed(\n () =>\n character_settings.value.is_valid &&\n hasActiveCharacterSettingsOverride(character_settings.value.draft)\n );\n const resetCharacterSettings = () => {\n character_settings.value = createCharacterSettingsState();\n };\n\n const runtimes = ref(Runtimes.parse({}));\n watch(\n () => runtimes.value.is_during_extra_analysis,\n new_value => insertOrAssignVariables({ extra_analysis: new_value }, { type: 'global' }),\n { immediate: true }\n );\n const resetRuntimes = () => {\n runtimes.value = Runtimes.parse({});\n };\n\n const versions = ref<{ sillytavern: string; tavernhelper: string }>({\n sillytavern: '',\n tavernhelper: '',\n });\n const _wait_init = async () => {\n versions.value.sillytavern = await fetch('/version')\n .then(res => res.json())\n .then(data => data.pkgVersion)\n .catch(() => '1.0.0');\n versions.value.tavernhelper = await getTavernHelperVersion();\n };\n\n const should_enable = ref(false);\n watch(should_enable, (new_enable, old_enable) => {\n if (new_enable && !old_enable) {\n _reload_settings();\n }\n });\n\n // 当存在多个 MVU 脚本实例时,仅优先实例应启用运行逻辑。\n registerAsUniqueScript('MVU变量框架').listenPreferenceState(preferred_script_id => {\n should_enable.value = preferred_script_id === getScriptId();\n });\n\n return {\n settings,\n _reload_settings,\n character_settings,\n effective_settings,\n has_character_settings_override,\n get_character_settings_override,\n is_character_settings_override_active,\n resetCharacterSettings,\n runtimes,\n resetRuntimes,\n versions,\n _wait_init,\n should_enable,\n };\n});\n","import { tr } from '@/i18n';\n\nexport type RegexCompileResult = {\n regex?: RegExp;\n error?: string;\n};\n\nfunction isEscaped(value: string, index: number): boolean {\n let backslash_count = 0;\n for (let i = index - 1; i >= 0 && value[i] === '\\\\'; i--) {\n backslash_count++;\n }\n return backslash_count % 2 === 1;\n}\n\nfunction findClosingSlash(value: string): number {\n for (let i = value.length - 1; i > 0; i--) {\n if (value[i] === '/' && !isEscaped(value, i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function compileRegex(value: string): RegexCompileResult {\n const trimmed = value.trim();\n if (trimmed === '') {\n return {};\n }\n\n try {\n if (trimmed.startsWith('/')) {\n const closing_slash = findClosingSlash(trimmed);\n if (closing_slash <= 0) {\n throw new Error(tr('runtime.regex.jsStyleSyntax'));\n }\n return {\n regex: new RegExp(\n trimmed.slice(1, closing_slash),\n trimmed.slice(closing_slash + 1)\n ),\n };\n }\n\n return { regex: new RegExp(trimmed) };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n}\n","import { tr } from '@/i18n';\nimport { compare } from 'compare-versions';\nimport JSON5 from 'json5';\nimport { jsonrepair } from 'jsonrepair';\n\n// 修正 _.merge 对数组的合并逻辑, [1, 2, 3] 和 [4, 5] 合并后变成 [4, 5] 而不是 [4, 5, 3]\nexport function correctlyMerge(lhs: TObject, rhs: TSource): TObject & TSource {\n return _.mergeWith(lhs, rhs, (_lhs, rhs) => (_.isArray(rhs) ? rhs : undefined));\n}\n\nexport function uuidv4(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nexport async function checkMinimumVersion(\n expected: string,\n notice: { message: string; title: string }\n) {\n if (compare(await getTavernHelperVersion(), expected, '<')) {\n toastr.error(notice.message, notice.title);\n }\n}\nexport function literalYamlify(value: any) {\n return YAML.stringify(value, { blockQuote: 'literal' });\n}\n\nexport function parseString(content: string): any {\n const json_first = /^[[{]/s.test(content.trimStart());\n try {\n if (json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error1) {\n try {\n // eslint-disable-next-line import-x/no-named-as-default-member\n return JSON5.parse(content);\n } catch (json5_error) {\n try {\n return JSON.parse(jsonrepair(content));\n } catch (json_error) {\n try {\n if (!json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error2) {\n const toError = (error: unknown) =>\n error instanceof Error\n ? `${error.stack ? error.stack : error.message}`\n : String(error);\n\n throw new Error(\n literalYamlify({\n [tr('runtime.parseString.invalidFormat')]: {\n [tr('runtime.parseString.content')]: content,\n [tr('runtime.parseString.yamlError')]: toError(\n json_first ? yaml_error2 : yaml_error1\n ),\n [tr('runtime.parseString.json5Error')]: toError(json5_error),\n [tr('runtime.parseString.jsonError')]: toError(json_error),\n },\n })\n );\n }\n }\n }\n }\n}\n","export function teleportStyle(\n append_to:\n | JQuery.Selector\n | JQuery.htmlString\n | JQuery.TypeOrArray\n | JQuery = 'head'\n): { destroy: () => void } {\n const $div = $(`
`)\n .attr('script_id', getScriptId())\n .append($(`head > style`, document).clone())\n .appendTo(append_to);\n\n return {\n destroy: () => $div.remove(),\n };\n}\n\nexport function createScriptIdDiv(): JQuery {\n return $('
').attr('script_id', getScriptId()) as JQuery;\n}\n\n/**\n * 将当前脚本实例注册到共享的“唯一脚本”命名空间。\n *\n * 当同一脚本被重复加载时,优先实例为:仍存在于 TavernHelper 脚本列表中、\n * 且在注册顺序上最后出现的 script id。\n * 调用方可通过 `listenPreferenceState` 订阅优先实例变化,并仅在\n * `getScriptId()` 与优先实例一致时启用功能。\n */\nexport function registerAsUniqueScript(id: string): {\n unregister: () => void;\n getPreferredScriptId: () => string | undefined;\n listenPreferenceState: (callback: (perferred_script_id: string) => void) => EventOnReturn;\n} {\n // 当前实例在 TavernHelper 中的唯一脚本 ID。\n const script_id = getScriptId();\n // 以业务 id 作为命名空间,避免不同功能之间冲突。\n const path = `th_unique_check.${id}`;\n\n const getPreferredScriptId = () => {\n // 从共享状态中取出已注册实例集合(跨脚本实例共享在 window.parent)。\n const registered_scripts = _.get(window.parent, path, new Set());\n // 以页面上实际存在的脚本顺序为准,选出“最后一个仍有效”的实例作为优先实例。\n return _($('#tavern_helper').find('div[data-script-id]').toArray())\n .map(element => String($(element).attr('data-script-id')))\n .filter(element => registered_scripts.has(element))\n .last();\n };\n\n // 将当前实例加入注册集合。不存在的场合创建。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value === undefined) {\n return new Set([script_id]);\n }\n //避免重复添加\n if (value.has(script_id)) return value;\n value.add(script_id);\n return value;\n });\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n\n return {\n unregister: () => {\n // 卸载时从注册集合移除,并重新广播优先实例。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value !== undefined) {\n value.delete(script_id);\n }\n return value;\n });\n eventEmit(path, getPreferredScriptId());\n },\n getPreferredScriptId,\n // 监听优先实例变化(回调入参是当前优先实例 script id)。\n listenPreferenceState: (callback: (enabled_script_id: string) => void) => {\n const ret = eventOn(path, callback);\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n return ret;\n },\n };\n}\n","/* unplugin-vue-components disabled */// style-loader: Adds some css to the DOM by adding a \n'],sourceRoot:''}]);const s=i},8126(e,t,n){n.r(t),n.d(t,{default:()=>s});var o=n(1354),r=n.n(o),a=n(6314),i=n.n(a)()(r());i.push([e.id,'.mvu-character-override__metadata[data-v-ac919d4c]{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:0.25rem 0.7rem;margin:0;padding:0.35rem 0.55rem;border-radius:8px;background-color:rgba(0,0,0,0.08)}.mvu-character-override__metadata dt[data-v-ac919d4c]{font-weight:600;opacity:0.82}.mvu-character-override__metadata dd[data-v-ac919d4c]{min-width:0;margin:0;overflow-wrap:anywhere}.mvu-character-override__fieldset[data-v-ac919d4c]{min-inline-size:0;margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:0.45rem}.mvu-character-override__fieldset[data-v-ac919d4c]:disabled{opacity:0.6}.mvu-character-override__group[data-v-ac919d4c]{display:flex;flex-direction:column;gap:0.3rem;padding-top:0.1rem}.mvu-character-override__group-title[data-v-ac919d4c]{padding:0 0.15rem}.mvu-character-override__select-field.mvu-field[data-v-ac919d4c]{flex-flow:row nowrap;align-items:center;gap:0.4rem;padding:0.3rem 0.5rem}.mvu-character-override__select-field>.text_pole[data-v-ac919d4c]{flex:1 1 auto;width:auto;min-width:0;margin:0}.mvu-character-override__regex-error[data-v-ac919d4c]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;overflow-wrap:anywhere}\n','',{version:3,sources:['webpack://./src/panel/CharacterOverride.vue'],names:[],mappings:'AAsMA,mDACI,YAAa,CACb,+CAAiD,CACjD,kBAAmB,CACnB,QAAS,CACT,uBAAwB,CACxB,iBAAkB,CAClB,iCACJ,CAEA,sDACI,eAAgB,CAChB,YACJ,CAEA,sDACI,WAAY,CACZ,QAAS,CACT,sBACJ,CAEA,mDACI,iBAAkB,CAClB,QAAS,CACT,SAAU,CACV,QAAS,CACT,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,4DACI,WACJ,CAEA,gDACI,YAAa,CACb,qBAAsB,CACtB,UAAW,CACX,kBACJ,CAEA,sDACI,iBACJ,CAEA,iEACI,oBAAqB,CACrB,kBAAmB,CACnB,UAAW,CACX,qBACJ,CAEA,kEACI,aAAc,CACd,UAAW,CACX,WAAY,CACZ,QACJ,CAEA,sDACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,sBACJ',sourcesContent:['\n\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-character-override__metadata[data-v-ac919d4c]{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:0.25rem 0.7rem;margin:0;padding:0.35rem 0.55rem;border-radius:8px;background-color:rgba(0,0,0,0.08)}.mvu-character-override__metadata dt[data-v-ac919d4c]{font-weight:600;opacity:0.82}.mvu-character-override__metadata dd[data-v-ac919d4c]{min-width:0;margin:0;overflow-wrap:anywhere}.mvu-character-override__fieldset[data-v-ac919d4c]{min-inline-size:0;margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:0.45rem}.mvu-character-override__fieldset[data-v-ac919d4c]:disabled{opacity:0.6}.mvu-character-override__group[data-v-ac919d4c]{display:flex;flex-direction:column;gap:0.3rem;padding-top:0.1rem}.mvu-character-override__group-title[data-v-ac919d4c]{padding:0 0.15rem}.mvu-character-override__select-field.mvu-field[data-v-ac919d4c]{flex-flow:row nowrap;align-items:center;gap:0.4rem;padding:0.3rem 0.5rem}.mvu-character-override__select-field>.text_pole[data-v-ac919d4c]{flex:1 1 auto;width:auto;min-width:0;margin:0}.mvu-character-override__regex-error[data-v-ac919d4c]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/CharacterOverride.vue\"],\"names\":[],\"mappings\":\"AAsMA,mDACI,YAAa,CACb,+CAAiD,CACjD,kBAAmB,CACnB,QAAS,CACT,uBAAwB,CACxB,iBAAkB,CAClB,iCACJ,CAEA,sDACI,eAAgB,CAChB,YACJ,CAEA,sDACI,WAAY,CACZ,QAAS,CACT,sBACJ,CAEA,mDACI,iBAAkB,CAClB,QAAS,CACT,SAAU,CACV,QAAS,CACT,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,4DACI,WACJ,CAEA,gDACI,YAAa,CACb,qBAAsB,CACtB,UAAW,CACX,kBACJ,CAEA,sDACI,iBACJ,CAEA,iEACI,oBAAqB,CACrB,kBAAmB,CACnB,UAAW,CACX,qBACJ,CAEA,kEACI,aAAc,CACd,UAAW,CACX,WAAY,CACZ,QACJ,CAEA,sDACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-license-table-wrap[data-v-67402f54]{max-height:18rem;overflow:auto;border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:6px}.mvu-license-table[data-v-67402f54]{width:100%;border-collapse:collapse;font-size:0.9em}.mvu-license-table th[data-v-67402f54],.mvu-license-table td[data-v-67402f54]{padding:0.35rem 0.5rem;text-align:start;vertical-align:top;border-bottom:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,0.45))}.mvu-license-table th[data-v-67402f54]:last-child,.mvu-license-table td[data-v-67402f54]:last-child{width:7.5rem;white-space:nowrap}.mvu-license-table tbody tr:last-child td[data-v-67402f54]{border-bottom:0}.mvu-license-table a[data-v-67402f54]{overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Compatibility.vue\"],\"names\":[],\"mappings\":\"AA0GA,yCACI,gBAAiB,CACjB,aAAc,CACd,8DAAmE,CACnE,iBACJ,CAEA,oCACI,UAAW,CACX,wBAAyB,CACzB,eACJ,CAEA,8EAEI,sBAAuB,CACvB,gBAAiB,CACjB,kBAAmB,CACnB,wEACJ,CAEA,oGAEI,YAAa,CACb,kBACJ,CAEA,2DACI,eACJ,CAEA,sCACI,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.inline-drawer-content[data-v-7a58cca8]{flex-direction:column;gap:0.75rem;padding-top:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Panel.vue\"],\"names\":[],\"mappings\":\"AAmCA,wCACI,qBAAsB,CACtB,WAAY,CACZ,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-details[data-v-3a7b912a]{border:1px dashed var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.5rem 0.7rem;background-color:rgba(0,0,0,0.06);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 70%,transparent)}.mvu-details__summary[data-v-3a7b912a]{cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;opacity:0.95}.mvu-details__title-suffix[data-v-3a7b912a]{display:inline-flex;align-items:center;margin-inline-start:0.35rem;vertical-align:middle}.mvu-details__content[data-v-3a7b912a]{margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Detail.vue\"],\"names\":[],\"mappings\":\"AAwBA,8BACI,+DAAoE,CACpE,kBAAmB,CACnB,qBAAsB,CACtB,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,cAAe,CACf,wBAAiB,CAAjB,gBAAiB,CACjB,eAAgB,CAChB,YACJ,CAEA,4CACI,mBAAoB,CACpB,kBAAmB,CACnB,2BAA4B,CAC5B,qBACJ,CAEA,uCACI,iBAAkB,CAClB,YAAa,CACb,qBAAsB,CACtB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field[data-v-1bd30ada]{padding:0.45rem 0.6rem;gap:0.25rem;border:1px solid color-mix(in srgb,var(--SmartThemeBorderColor,rgba(45,45,45,1)) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 55%,transparent)}.mvu-field__label[data-v-1bd30ada]{display:inline-flex;align-items:center;gap:0.35rem;font-weight:600;opacity:0.95}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Field.vue\"],\"names\":[],\"mappings\":\"AAeA,4BACI,sBAAuB,CACvB,WAAY,CACZ,iGAAwG,CACxG,kBAAmB,CACnB,mGAKJ,CAEA,mCACI,mBAAoB,CACpB,kBAAmB,CACnB,WAAY,CACZ,eAAgB,CAChB,YACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-help-icon[data-v-7a641daa]{cursor:pointer}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/HelpIcon.vue\"],\"names\":[],\"mappings\":\"AAoBA,gCACI,cACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-model-select[data-v-7b6ca08a]{display:flex;flex-direction:column;gap:0.5rem}.mvu-model-select__row[data-v-7b6ca08a]{width:100%}.mvu-model-select__row--controls[data-v-7b6ca08a]{display:grid;grid-template-columns:1fr auto;gap:0.5rem;align-items:center}.mvu-model-select__btn[data-v-7b6ca08a]{white-space:nowrap;text-align:left;padding:0.35rem 0.6rem;min-height:unset;height:2.05rem;line-height:1.1}@media (max-width:520px){.mvu-model-select__row--controls[data-v-7b6ca08a]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/ModelSelect.vue\"],\"names\":[],\"mappings\":\"AA4JA,mCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,wCACI,UACJ,CAEA,kDACI,YAAa,CACb,8BAA+B,CAC/B,UAAW,CACX,kBACJ,CAEA,wCACI,kBAAmB,CACnB,eAAgB,CAChB,sBAAuB,CACvB,gBAAiB,CACjB,cAAe,CACf,eACJ,CAEA,yBACI,kDACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-override-badge[data-v-14bdab4e]{display:inline-flex;flex:0 1 auto;align-items:center;max-width:100%;padding:0.08rem 0.38rem;border:1px solid currentColor;border-radius:999px;font-size:calc(var(--mainFontSize,1rem) * 0.76);font-weight:600;line-height:1.25;text-align:center;overflow-wrap:anywhere}.mvu-override-badge--override[data-v-14bdab4e]{color:var(--SmartThemeEmColor,#d39e00);background-color:color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 12%,transparent)}.mvu-override-badge--additive[data-v-14bdab4e]{color:var(--SmartThemeQuoteColor,#6ba7ff);background-color:color-mix(in srgb,var(--SmartThemeQuoteColor,#6ba7ff) 12%,transparent)}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/OverrideBadge.vue\"],\"names\":[],\"mappings\":\"AAuBA,qCACI,mBAAoB,CACpB,aAAc,CACd,kBAAmB,CACnB,cAAe,CACf,uBAAwB,CACxB,6BAA8B,CAC9B,mBAAoB,CACpB,+CAAiD,CACjD,eAAgB,CAChB,gBAAiB,CACjB,iBAAkB,CAClB,sBACJ,CAEA,+CACI,sCAAwC,CACxC,oFACJ,CAEA,+CACI,yCAA2C,CAC3C,uFACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-range-number[data-v-48562df7]{display:grid;grid-template-columns:1fr 7.5rem;gap:0.5rem;align-items:center}.mvu-range-number__range[data-v-48562df7]{width:100%}.mvu-range-number__number[data-v-48562df7]{text-align:left;padding-top:0.3rem;padding-bottom:0.3rem;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 33%,transparent)}@media (max-width:420px){.mvu-range-number[data-v-48562df7]{grid-template-columns:1fr 6.5rem}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/RangeNumber.vue\"],\"names\":[],\"mappings\":\"AA2CA,mCACI,YAAa,CACb,gCAAiC,CACjC,UAAW,CACX,kBACJ,CAEA,0CACI,UACJ,CAEA,2CACI,eAAgB,CAChB,kBAAmB,CACnB,qBAAsB,CACtB,mGAKJ,CAEA,yBACI,mCACI,gCACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-section[data-v-ab897094]{border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.6rem 0.75rem;gap:0.45rem;background-color:rgba(0,0,0,0.08);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 75%,transparent)}.mvu-section__content[data-v-ab897094]{gap:0.5rem}.mvu-section__title[data-v-ab897094]{display:flex;flex-wrap:wrap;align-items:center;gap:0.3rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Section.vue\"],\"names\":[],\"mappings\":\"AAmBA,8BACI,8DAAmE,CACnE,kBAAmB,CACnB,sBAAuB,CACvB,WAAY,CACZ,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,UACJ,CAEA,qCACI,YAAa,CACb,cAAe,CACf,kBAAmB,CACnB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-warning[data-v-58f80c4e]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-58f80c4e]{line-height:1}.mvu-warning__text[data-v-58f80c4e]{word-break:break-word}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Method.vue\"],\"names\":[],\"mappings\":\"AA4CA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-note[data-v-7ae42438]{padding:0 0.6rem 0.45rem;opacity:0.8;font-size:calc(var(--mainFontSize,1rem) * 0.9)}.mvu-regex-error[data-v-7ae42438]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;word-break:break-word}.mvu-regex-actions[data-v-7ae42438]{display:flex;padding:0 0.6rem 0.45rem}.mvu-regex-actions__button[data-v-7ae42438]{min-height:2rem;white-space:normal}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Prompt.vue\"],\"names\":[],\"mappings\":\"AA0MA,2BACI,wBAAyB,CACzB,WAAY,CACZ,8CACJ,CAEA,kCACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,qBACJ,CAEA,oCACI,YAAa,CACb,wBACJ,CAEA,4CACI,eAAgB,CAChB,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-auto-request-conditions[data-v-22baa9a5]{margin-left:1rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Request.vue\"],\"names\":[],\"mappings\":\"AAuIA,8CACI,gBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field-grid[data-v-0edef090]{display:flex;flex-direction:column;gap:0.5rem}.mvu-api-profile-controls[data-v-0edef090]{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem}.mvu-api-profile-actions[data-v-0edef090]{display:flex;flex-wrap:wrap;gap:0.5rem}.mvu-note[data-v-0edef090]{opacity:0.85;color:var(--SmartThemeEmColor,inherit)}.mvu-pi-proxy-option[data-v-0edef090]{padding:0 0.6rem}.mvu-warning[data-v-0edef090]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-0edef090]{line-height:1}.mvu-warning__text[data-v-0edef090]{word-break:break-word}.mvu-pi-advanced-textarea[data-v-0edef090]{width:100%;min-height:5rem;resize:vertical;font-family:var(--monoFontFamily,monospace);white-space:pre}.mvu-field-error[data-v-0edef090]{color:var(--SmartThemeQuoteColor,#ff6b6b);line-height:1.35;word-break:break-word}.mvu-oauth-status[data-v-0edef090]{display:flex;flex-direction:column;gap:0.25rem}.mvu-link-button[data-v-0edef090]{display:inline-flex;align-items:center;text-decoration:none}@media (max-width:520px){.mvu-api-profile-controls[data-v-0edef090]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Source.vue\"],\"names\":[],\"mappings\":\"AA0wDA,iCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,2CACI,YAAa,CACb,6BAA8B,CAC9B,UACJ,CAEA,0CACI,YAAa,CACb,cAAe,CACf,UACJ,CAEA,2BACI,YAAa,CACb,sCACJ,CAEA,sCACI,gBACJ,CAEA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ,CAEA,2CACI,UAAW,CACX,eAAgB,CAChB,eAAgB,CAChB,2CAA6C,CAC7C,eACJ,CAEA,kCACI,yCAA2C,CAC3C,gBAAiB,CACjB,qBACJ,CAEA,mCACI,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,kCACI,mBAAoB,CACpB,kBAAmB,CACnB,oBACJ,CAEA,yBACI,2CACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = [];\n\n // return the list of modules as css string\n list.toString = function toString() {\n return this.map(function (item) {\n var content = \"\";\n var needLayer = typeof item[5] !== \"undefined\";\n if (item[4]) {\n content += \"@supports (\".concat(item[4], \") {\");\n }\n if (item[2]) {\n content += \"@media \".concat(item[2], \" {\");\n }\n if (needLayer) {\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\n }\n content += cssWithMappingToString(item);\n if (needLayer) {\n content += \"}\";\n }\n if (item[2]) {\n content += \"}\";\n }\n if (item[4]) {\n content += \"}\";\n }\n return content;\n }).join(\"\");\n };\n\n // import a list of modules into the list\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === \"string\") {\n modules = [[null, modules, undefined]];\n }\n var alreadyImportedModules = {};\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n if (typeof layer !== \"undefined\") {\n if (typeof item[5] === \"undefined\") {\n item[5] = layer;\n } else {\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\n item[5] = layer;\n }\n }\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\n item[2] = media;\n }\n }\n if (supports) {\n if (!item[4]) {\n item[4] = \"\".concat(supports);\n } else {\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\n item[4] = supports;\n }\n }\n list.push(item);\n }\n };\n return list;\n};","\"use strict\";\n\nmodule.exports = function (item) {\n var content = item[1];\n var cssMapping = item[3];\n if (!cssMapping) {\n return content;\n }\n if (typeof btoa === \"function\") {\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n var sourceMapping = \"/*# \".concat(data, \" */\");\n return [content].concat([sourceMapping]).join(\"\\n\");\n }\n return [content].join(\"\\n\");\n};","import { compileEntryCommentRegex } from '@/function/request/entry_comment_regex';\nimport { tr } from '@/i18n';\nimport { parseString } from '@util/common';\nimport { klona } from 'klona';\nimport * as z from 'zod';\n\nexport const CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME = '[config_override]';\n\n// loose 模式用于保留未来版本或其他扩展写入的未知配置字段。\nexport const CharacterSettingsOverrideSchema = z\n .object({\n 更新方式: z.enum(['随AI输出', '额外模型解析']).optional(),\n 额外模型解析配置: z\n .object({\n 启用自动请求: z.boolean().optional(),\n 世界书条目白名单正则: z.string().optional(),\n 世界书条目黑名单正则: z.string().optional(),\n })\n .loose()\n .optional(),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().optional(),\n sendas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .optional(),\n })\n .loose();\n\nexport type CharacterSettingsOverride = z.infer;\n\nexport type CharacterSettingsOverridePath =\n | '更新方式'\n | '额外模型解析配置.启用自动请求'\n | '额外模型解析配置.世界书条目白名单正则'\n | '额外模型解析配置.世界书条目黑名单正则'\n | '兼容性.更新到聊天变量'\n | '兼容性.sendas不视为user消息';\n\nexport type CharacterSettingsOverrideValue = string | boolean | undefined;\n\nexport function isCharacterSettingsOverrideEntryName(name: unknown): boolean {\n return (\n typeof name === 'string' &&\n name.trim().toLowerCase() === CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME\n );\n}\n\nexport function hasOwnCharacterSettingsOverride(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): boolean {\n return _.has(draft, path);\n}\n\nexport function getCharacterSettingsOverrideValue(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): CharacterSettingsOverrideValue {\n return _.get(draft, path) as CharacterSettingsOverrideValue;\n}\n\nfunction removeEmptyKnownValues(draft: CharacterSettingsOverride): CharacterSettingsOverride {\n // “继承全局配置”在文件中表示字段不存在,而不是保存空字符串或空对象。\n const result = klona(draft);\n const extra_model = result.额外模型解析配置;\n if (extra_model) {\n for (const key of ['世界书条目白名单正则', '世界书条目黑名单正则'] as const) {\n if (extra_model[key]?.trim() === '') {\n delete extra_model[key];\n }\n }\n if (Object.keys(extra_model).length === 0) {\n delete result.额外模型解析配置;\n }\n }\n if (result.兼容性 && Object.keys(result.兼容性).length === 0) {\n delete result.兼容性;\n }\n return result;\n}\n\nexport function normalizeCharacterSettingsOverride(\n draft: CharacterSettingsOverride\n): CharacterSettingsOverride {\n return CharacterSettingsOverrideSchema.parse(removeEmptyKnownValues(draft));\n}\n\nfunction makeAdditionalPropertiesExplicit(schema: unknown): unknown {\n // Zod 会用空对象表示允许附加属性;显式改为 true 便于其他 JSON Schema 工具识别。\n if (Array.isArray(schema)) {\n return schema.map(makeAdditionalPropertiesExplicit);\n }\n if (!_.isPlainObject(schema)) {\n return schema;\n }\n\n const result = Object.fromEntries(\n Object.entries(schema as Record).map(([key, value]) => [\n key,\n key === 'additionalProperties' && _.isPlainObject(value) && _.isEmpty(value)\n ? true\n : makeAdditionalPropertiesExplicit(value),\n ])\n );\n return result;\n}\n\nexport function getCharacterSettingsOverrideJsonSchema(): Record {\n const generated = z.toJSONSchema(CharacterSettingsOverrideSchema, {\n target: 'draft-2020-12',\n });\n return {\n ...(makeAdditionalPropertiesExplicit(generated) as Record),\n title: 'CharacterSettingsOverride',\n };\n}\n\nexport function parseCharacterSettingsOverrideContent(content: string): CharacterSettingsOverride {\n const document = JSON.parse(content) as unknown;\n if (!_.isPlainObject(document)) {\n throw new Error(tr('runtime.characterOverride.contentMustBeObject'));\n }\n\n // schema 仅供角色卡编辑器展示,不属于实际覆盖配置。\n const { schema: _embedded_schema, ...draft } = document as Record;\n return normalizeCharacterSettingsOverride(CharacterSettingsOverrideSchema.parse(draft));\n}\n\nexport function recoverCharacterSettingsOverridePassthrough(\n content: string\n): CharacterSettingsOverride {\n const document = parseString(content) as unknown;\n if (!_.isPlainObject(document)) {\n return {};\n }\n\n // 丢弃可能损坏的已知字段,只抢救能够安全原样写回的未知扩展字段。\n const recovered = klona(document) as Record;\n delete recovered.schema;\n delete recovered.更新方式;\n\n const remove_known_nested_fields = (key: '额外模型解析配置' | '兼容性', fields: string[]) => {\n const nested = recovered[key];\n if (!_.isPlainObject(nested)) {\n delete recovered[key];\n return;\n }\n const nested_record = nested as Record;\n fields.forEach(field => delete nested_record[field]);\n if (_.isEmpty(nested_record)) {\n delete recovered[key];\n }\n };\n remove_known_nested_fields('额外模型解析配置', [\n '启用自动请求',\n '世界书条目白名单正则',\n '世界书条目黑名单正则',\n ]);\n remove_known_nested_fields('兼容性', ['更新到聊天变量', 'sendas不视为user消息']);\n\n return CharacterSettingsOverrideSchema.parse(recovered);\n}\n\nexport function serializeCharacterSettingsOverride(draft: CharacterSettingsOverride): string {\n const normalized = normalizeCharacterSettingsOverride(draft);\n const { schema: _existing_schema, ...config } = normalized;\n // schema 固定放在最后,兼顾人工阅读和 SillyTavern 的编辑体验。\n const document = {\n ...config,\n schema: getCharacterSettingsOverrideJsonSchema(),\n };\n return JSON.stringify(document, null, 4);\n}\n\nexport function hasActiveCharacterSettingsOverride(draft: CharacterSettingsOverride): boolean {\n const ordinary_paths: CharacterSettingsOverridePath[] = [\n '更新方式',\n '额外模型解析配置.启用自动请求',\n '兼容性.更新到聊天变量',\n '兼容性.sendas不视为user消息',\n ];\n if (ordinary_paths.some(path => hasOwnCharacterSettingsOverride(draft, path))) {\n return true;\n }\n\n // 空白或无效正则不会参与过滤,也不应让界面显示为“覆盖中”。\n return (\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目白名单正则 ?? '').regex !==\n undefined ||\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目黑名单正则 ?? '').regex !==\n undefined\n );\n}\n","import { tr } from '@/i18n';\n\nexport { compileRegex as compileEntryCommentRegex } from '@/util/regex';\nexport type { RegexCompileResult as EntryCommentRegexCompileResult } from '@/util/regex';\n\nexport type EntryCommentFilterSource = '用户全局配置' | '角色卡配置';\n\nexport type EntryCommentFilterResult = {\n lore: 'globalLore' | 'characterLore' | 'chatLore' | 'personaLore';\n world: string;\n comment: string;\n reason: '白名单' | '黑名单';\n sources: EntryCommentFilterSource[];\n};\n\nexport const ENTRY_COMMENT_FILTER_LOG_TITLE = tr('runtime.filter.logTitle');\n\nexport function logEntryCommentFilterResult(result: EntryCommentFilterResult[]) {\n console.log(\n ENTRY_COMMENT_FILTER_LOG_TITLE,\n result.map(entry => ({ ...entry }))\n );\n}\n\nexport function testEntryCommentRegex(regex: RegExp, comment: string): boolean {\n regex.lastIndex = 0;\n return regex.test(comment);\n}\n","import { normalizePiThinkingLevel, type PiThinkingLevel } from './pi/thinking_setting';\nimport { tr } from '@/i18n';\nimport {\n getPiProviderTargetDefinition,\n resolvePiApiKeyScope,\n} from '@/function/update/pi/provider_target';\nimport { klona } from 'klona';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './pi/credential_refs';\n\nexport type ExtraModelApiProfileBackend = 'custom' | 'pi';\n\nexport type ExtraModelApiRequestFields = {\n 破限方案: '使用内置破限' | '使用当前预设' | '使用其他预设';\n 其他预设名称: string;\n 随机头部: boolean;\n 应答格式: '聊天消息' | '工具调用' | '格式化输出' | '格式化输出(v4兼容)';\n 关闭thinking: boolean;\n 兼容假流式: boolean;\n};\n\nconst API_REQUEST_FIELDS = [\n '破限方案',\n '其他预设名称',\n '随机头部',\n '应答格式',\n '关闭thinking',\n '兼容假流式',\n] as const satisfies readonly (keyof ExtraModelApiRequestFields)[];\n\n/** 提取已配置的请求选项,保留缺失字段,供方案保存和旧方案补全使用。 */\nfunction extractApiRequestFields(\n config: Partial\n): Partial {\n return Object.fromEntries(\n API_REQUEST_FIELDS.filter(field => config[field] !== undefined).map(field => [\n field,\n config[field],\n ])\n );\n}\n\nexport type ExtraModelPiConnectionFields = {\n [key: string]: unknown;\n /** 各服务商绑定的集中 OAuth 凭证编号;空映射表示明确未登录。 */\n credentialIds?: PiCredentialReferences;\n provider: string;\n api: string;\n authType: string;\n endpoint: string;\n useProxy: boolean;\n thinkingLevel?: PiThinkingLevel;\n model: string;\n contextWindow: number | string;\n customHeaders: string;\n customIncludeBody: string;\n customExcludeBody: string;\n};\n\nexport type ExtraModelPiSettings = ExtraModelPiConnectionFields & {\n credentials: Record;\n apiKeys?: Record;\n};\n\nexport type ExtraModelApiProfile = Partial & {\n [key: string]: unknown;\n 名称: string;\n backend?: ExtraModelApiProfileBackend;\n api地址: string;\n 密钥: string;\n 模型名称: string;\n pi?: ExtraModelPiConnectionFields;\n};\n\nexport type ExtraModelApiProfileFields = Partial & {\n 模型来源?: '与插头相同' | '自定义' | '更多';\n api地址: string;\n 密钥: string;\n customApiKey?: string;\n 模型名称: string;\n pi?: ExtraModelPiSettings;\n api方案列表: ExtraModelApiProfile[];\n 当前api方案: string;\n};\n\nexport const DEFAULT_EXTRA_MODEL_API_PROFILE_NAME = '默认';\n\n/** 统一方案名两端空白,避免查找、去重和保存使用不同的名称。 */\nfunction normalizeExtraModelApiProfileName(name: string): string {\n return name.trim();\n}\n\n/**\n * 将方案中的上下文窗口规范为非负整数,0 表示使用目录值。\n * 返回 undefined 表示配置无效,调用方不得将其当作目录默认值。\n */\nexport function normalizeExtraModelPiProfileContextWindow(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isInteger(value) && value >= 0 ? value : undefined;\n }\n if (typeof value !== 'string') {\n return undefined;\n }\n\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;\n}\n\n/** 根据当前模型来源确定方案保存的是自定义连接还是 Pi 连接。 */\nfunction getExtraModelApiProfileBackend(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileBackend {\n return config.模型来源 === '更多' ? 'pi' : 'custom';\n}\n\n/**\n * 生成脱离响应式代理的连接快照,并规范连接标识与上下文窗口。\n * 快照不包含 OAuth 凭证或按目标缓存的 API Key,避免方案复制这些共享状态。\n */\nfunction clonePiConnectionFields(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): ExtraModelPiConnectionFields | undefined {\n if (pi === undefined) {\n return undefined;\n }\n\n // Settings and profile entries normally arrive as Vue/Pinia reactive proxies. Native\n // structuredClone rejects proxies with DataCloneError, while klona traverses them into\n // detached plain objects and still preserves unknown forward-compatible fields.\n const cloned = klona(pi);\n delete cloned.credentials;\n delete cloned.apiKeys;\n if (pi.credentialIds !== undefined || pi.credentials !== undefined) {\n cloned.credentialIds = resolvePiCredentialReferences(\n pi,\n (pi.credentials as Record | undefined) ?? {}\n );\n }\n // Runtime resolution treats surrounding whitespace on connection identifiers as insignificant.\n // Persist the same canonical representation so profile validation, the Source form, dirty\n // checks, and runtime resolution cannot disagree about a target after import.\n for (const field of ['provider', 'api', 'authType', 'endpoint', 'model'] as const) {\n if (typeof cloned[field] === 'string') {\n cloned[field] = cloned[field].trim();\n }\n }\n // Profiles saved before this option existed, and malformed imported values, must retain the\n // direct-transport default rather than becoming enabled through truthiness coercion.\n cloned.useProxy = cloned.useProxy === true;\n cloned.thinkingLevel = normalizePiThinkingLevel(cloned.thinkingLevel);\n const context_window = normalizeExtraModelPiProfileContextWindow(cloned.contextWindow);\n if (context_window !== undefined) {\n cloned.contextWindow = context_window;\n }\n return cloned;\n}\n\n/** 仅为完整且受支持的 API Key 目标解析密钥缓存位置;无效连接返回空字符串。 */\nfunction resolvePiConnectionApiKeyScope(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): string {\n if (\n pi === undefined ||\n typeof pi.provider !== 'string' ||\n typeof pi.api !== 'string' ||\n typeof pi.authType !== 'string' ||\n typeof pi.endpoint !== 'string'\n ) {\n return '';\n }\n\n return resolvePiApiKeyScope(\n getPiProviderTargetDefinition(pi.provider.trim()),\n pi.api.trim(),\n pi.authType.trim(),\n pi.endpoint\n );\n}\n\n/** 检查导入快照是否具备全部连接字段及合法类型,避免用默认值补成可发送连接。 */\nfunction hasCompletePiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n pi !== undefined &&\n typeof pi.provider === 'string' &&\n typeof pi.api === 'string' &&\n typeof pi.authType === 'string' &&\n (pi.authType.trim() === 'api_key' || pi.authType.trim() === 'oauth') &&\n typeof pi.endpoint === 'string' &&\n typeof pi.useProxy === 'boolean' &&\n typeof pi.model === 'string' &&\n typeof pi.contextWindow === 'number' &&\n Number.isInteger(pi.contextWindow) &&\n pi.contextWindow >= 0 &&\n typeof pi.customHeaders === 'string' &&\n typeof pi.customIncludeBody === 'string' &&\n typeof pi.customExcludeBody === 'string'\n );\n}\n\n/** 在结构完整的基础上检查服务商、协议和模型是否已填写。 */\nfunction hasConfiguredPiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n hasCompletePiConnectionSnapshot(pi) &&\n pi.provider.trim().length > 0 &&\n pi.api.trim().length > 0 &&\n pi.model.trim().length > 0\n );\n}\n\n/** 判断连接能否归属到有效的 API Key 目标,作为保留方案密钥的条件。 */\nfunction hasValidPiApiKeyTarget(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): boolean {\n return hasConfiguredPiConnectionSnapshot(pi) && resolvePiConnectionApiKeyScope(pi) !== '';\n}\n\n/** 规范单个方案的名称、后端和连接快照,并清除不属于当前认证方式的密钥。 */\nfunction normalizeExtraModelApiProfile(profile: ExtraModelApiProfile): ExtraModelApiProfile {\n const cloned = klona(profile);\n delete cloned.customApiKey;\n cloned.名称 = normalizeExtraModelApiProfileName(cloned.名称);\n const backend: ExtraModelApiProfileBackend = cloned.backend === 'pi' ? 'pi' : 'custom';\n cloned.backend = backend;\n\n if (backend === 'pi' && cloned.pi !== undefined) {\n cloned.pi = clonePiConnectionFields(cloned.pi);\n } else {\n delete cloned.pi;\n }\n if (backend === 'pi') {\n // These legacy top-level fields belong to the hidden Custom source. Pi owns its endpoint\n // and model inside `pi`, so importing or saving them here must never overwrite that cache.\n cloned.api地址 = '';\n cloned.模型名称 = '';\n\n // A profile-level key has wire meaning only for a registry-backed, normalizable API-key\n // target. OAuth and malformed/future targets retain metadata but never a dormant secret.\n if (!hasValidPiApiKeyTarget(cloned.pi)) {\n cloned.密钥 = '';\n }\n }\n return cloned;\n}\n\n/** 规范方案列表,剔除空名称和重复名称,同名方案只保留首次出现的条目。 */\nfunction normalizeExtraModelApiProfileList(\n profiles: ExtraModelApiProfile[]\n): ExtraModelApiProfile[] {\n const names = new Set();\n const normalized_profiles: ExtraModelApiProfile[] = [];\n for (const profile of profiles) {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n if (normalized_profile.名称 === '' || names.has(normalized_profile.名称)) {\n continue;\n }\n names.add(normalized_profile.名称);\n normalized_profiles.push(normalized_profile);\n }\n return normalized_profiles;\n}\n\n/** 将方案连接合并到运行设置,同时保留当前凭证仓库和目标密钥缓存。 */\nfunction mergePiConnectionFields(\n current: ExtraModelPiSettings | undefined,\n profile: ExtraModelPiConnectionFields\n): ExtraModelPiSettings {\n const current_clone = current === undefined ? undefined : klona(current);\n const profile_clone = clonePiConnectionFields(profile)!;\n return {\n ...current_clone,\n ...profile_clone,\n credentialIds: resolvePiCredentialReferences(profile, current_clone?.credentials),\n credentials: klona(current_clone?.credentials ?? {}),\n apiKeys: klona(current_clone?.apiKeys ?? {}),\n } as ExtraModelPiSettings;\n}\n\n/** 清空可发送的连接字段,保留共享凭证和密钥缓存,等待用户重新配置。 */\nfunction clearPiConnectionFields(pi: ExtraModelPiSettings): ExtraModelPiSettings {\n const cloned = klona(pi);\n return {\n ...cloned,\n credentialIds: {},\n provider: '',\n api: '',\n authType: 'api_key',\n endpoint: '',\n useProxy: false,\n thinkingLevel: 'default',\n model: '',\n contextWindow: 0,\n credentials: klona(cloned.credentials),\n apiKeys: klona(cloned.apiKeys ?? {}),\n customHeaders: '',\n customIncludeBody: '',\n customExcludeBody: '',\n };\n}\n\n/** 从当前设置提取可保存的连接和请求选项;Pi 方案必须先通过完整性校验。 */\nexport function extractExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfile {\n const backend = getExtraModelApiProfileBackend(config);\n const pi_snapshot = backend === 'pi' ? clonePiConnectionFields(config.pi) : undefined;\n if (backend === 'pi' && !hasConfiguredPiConnectionSnapshot(pi_snapshot)) {\n throw new Error(tr('runtime.apiProfile.piConfigRequired'));\n }\n return {\n 名称:\n normalizeExtraModelApiProfileName(config.当前api方案) ||\n DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n backend,\n api地址: backend === 'pi' ? '' : config.api地址,\n 密钥: backend === 'pi' && !hasValidPiApiKeyTarget(pi_snapshot) ? '' : config.密钥,\n 模型名称: backend === 'pi' ? '' : config.模型名称,\n ...extractApiRequestFields(config),\n ...(backend === 'pi' && pi_snapshot !== undefined ? { pi: pi_snapshot } : {}),\n };\n}\n\n/**\n * 应用方案的连接及请求选项,并切换到相应模型来源。\n * 无效 Pi 快照会清空活动连接,不能借用其他来源的密钥继续发送。\n */\nexport function applyExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile: ExtraModelApiProfile\n): ExtraModelApiProfileFields {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const result: ExtraModelApiProfileFields = {\n ...config,\n ...extractApiRequestFields(normalized_profile),\n 模型来源: normalized_profile.backend === 'pi' ? '更多' : '自定义',\n 密钥: normalized_profile.密钥,\n 当前api方案: normalized_profile.名称,\n ...(normalized_profile.backend === 'custom'\n ? {\n api地址: normalized_profile.api地址,\n 模型名称: normalized_profile.模型名称,\n }\n : {}),\n };\n if (normalized_profile.backend === 'pi') {\n if (hasConfiguredPiConnectionSnapshot(normalized_profile.pi)) {\n result.pi = mergePiConnectionFields(config.pi, normalized_profile.pi);\n } else {\n // Never combine a malformed Pi profile's key with the previously active provider,\n // endpoint, or model. Preserve credentials, but disable the connection until the\n // user repairs and saves a complete snapshot.\n result.密钥 = '';\n if (config.pi !== undefined) {\n result.pi = clearPiConnectionFields(config.pi);\n }\n }\n }\n return result;\n}\n\n/** 按规范化名称新增或更新方案,更新时保留已有条目的其他字段。 */\nexport function upsertExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile: ExtraModelApiProfile\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile.名称);\n if (!normalized_name) {\n throw new Error(tr('runtime.apiProfile.nameRequired'));\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(profiles);\n const next_profile = {\n ...normalizeExtraModelApiProfile(profile),\n 名称: normalized_name,\n };\n const existing_index = normalized_profiles.findIndex(item => item.名称 === normalized_name);\n if (existing_index === -1) {\n return [...normalized_profiles, next_profile];\n }\n\n const next_profiles = [...normalized_profiles];\n next_profiles[existing_index] = normalizeExtraModelApiProfile({\n ...normalized_profiles[existing_index],\n ...next_profile,\n });\n return next_profiles;\n}\n\n/** 返回删除指定方案后的规范化列表,不直接修改原列表。 */\nexport function removeExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).filter(\n profile => profile.名称 !== normalized_name\n );\n}\n\n/** 按规范化名称检查方案是否存在,供保存和删除操作校验。 */\nexport function hasExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): boolean {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).some(\n profile => profile.名称 === normalized_name\n );\n}\n\n/** 比较活动方案与当前连接、密钥及请求选项,判断是否存在未保存修改。 */\nexport function isActiveExtraModelApiProfileDirty(config: ExtraModelApiProfileFields): boolean {\n const active_name = normalizeExtraModelApiProfileName(config.当前api方案);\n if (!active_name) {\n return false;\n }\n\n const profile = normalizeExtraModelApiProfileList(config.api方案列表).find(\n item => item.名称 === active_name\n );\n if (!profile) {\n return false;\n }\n\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const backend = getExtraModelApiProfileBackend(config);\n if (normalized_profile.backend !== backend || normalized_profile.密钥 !== config.密钥) {\n return true;\n }\n\n if (\n API_REQUEST_FIELDS.some(\n field =>\n normalized_profile[field] !== undefined &&\n normalized_profile[field] !== config[field]\n )\n ) {\n return true;\n }\n\n if (\n backend === 'custom' &&\n (normalized_profile.api地址 !== config.api地址 ||\n normalized_profile.模型名称 !== config.模型名称)\n ) {\n return true;\n }\n\n return (\n backend === 'pi' && !_.isEqual(normalized_profile.pi, clonePiConnectionFields(config.pi))\n );\n}\n\n/** 解除方案绑定并清空活动连接;使用 Pi 时保留隐藏的自定义连接字段。 */\nexport function clearUnboundExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileFields {\n const preserve_hidden_custom_fields = config.模型来源 === '更多';\n return {\n ...config,\n 当前api方案: '',\n api地址: preserve_hidden_custom_fields ? config.api地址 : '',\n 密钥: '',\n 模型名称: preserve_hidden_custom_fields ? config.模型名称 : '',\n ...(config.模型来源 === '更多' && config.pi !== undefined\n ? { pi: clearPiConnectionFields(config.pi) }\n : {}),\n };\n}\n\n/** 校正活动方案引用,使列表、当前名称和连接状态保持一致。 */\nexport function reconcileExtraModelApiProfileSelection(\n config: T\n): T {\n const normalized_config = {\n ...config,\n api方案列表: normalizeExtraModelApiProfileList(config.api方案列表),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n const active_name = normalized_config.当前api方案;\n if (!active_name) {\n return normalized_config;\n }\n\n if (hasExtraModelApiProfile(normalized_config.api方案列表, active_name)) {\n return normalized_config;\n }\n\n if (normalized_config.api方案列表.length === 0) {\n return {\n ...normalized_config,\n 当前api方案: '',\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...normalized_config,\n api方案列表: normalized_config.api方案列表,\n },\n normalized_config.api方案列表[0]\n ) as T;\n}\n\n/** 删除指定的活动方案,并同步清理方案选择及相关连接字段。 */\nexport function deleteActiveExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n const remaining = removeExtraModelApiProfile(config.api方案列表, profile_name);\n if (remaining.length === 0) {\n return {\n ...clearUnboundExtraModelApiProfileFields(config),\n api方案列表: [],\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: remaining,\n },\n remaining[0]\n );\n}\n\nexport type ExtraModelApiProfileDeletionConfirmation = 'discard_unsaved_changes' | 'delete_profile';\n\n/**\n * 有未保存修改时先确认丢弃,再确认删除,任一步取消都返回 null。\n * 删除前再次核对活动方案,避免等待弹窗期间误删用户新选中的方案。\n */\nexport async function deleteActiveExtraModelApiProfileWithConfirmation(\n config: ExtraModelApiProfileFields,\n profile_name: string,\n confirm: (confirmation: ExtraModelApiProfileDeletionConfirmation) => Promise\n): Promise {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n if (isActiveExtraModelApiProfileDirty(config) && !(await confirm('discard_unsaved_changes'))) {\n return null;\n }\n\n if (!(await confirm('delete_profile'))) {\n return null;\n }\n\n return deleteActiveExtraModelApiProfile(config, profile_name);\n}\n\n/**\n * 补全方案中缺失的请求选项,并校正方案选择与密钥归属。\n * 不完整的 Pi 快照保持不可发送,不能从遗留自定义字段推导出 Pi 连接。\n */\nexport function migrateExtraModelApiProfiles(config: T): T {\n let migrated = {\n ...config,\n ...(config.pi === undefined\n ? {}\n : {\n pi: {\n ...config.pi,\n credentialIds: resolvePiCredentialReferences(\n config.pi,\n config.pi.credentials\n ),\n },\n }),\n // Old profiles only stored connection fields. Copy the current request options into\n // every profile once; saved per-profile values must survive subsequent loads/imports.\n api方案列表: normalizeExtraModelApiProfileList(\n config.api方案列表.map(profile => ({\n ...profile,\n ...(profile.backend === 'pi' && profile.pi !== undefined\n ? {\n pi: {\n ...profile.pi,\n credentialIds: resolvePiCredentialReferences(\n profile.pi,\n config.pi?.credentials\n ),\n },\n }\n : {}),\n ...extractApiRequestFields(config),\n ...extractApiRequestFields(profile),\n }))\n ),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n\n // The shared root key has wire meaning only for a registry-backed, normalizable API-key target.\n // Clear stale imported values even when there is no active profile (and before legacy-profile\n // detection), while retaining the source-specific Custom/API-key caches and OAuth store.\n if (\n migrated.模型来源 === '更多' &&\n resolvePiConnectionApiKeyScope(migrated.pi) === '' &&\n migrated.密钥 !== ''\n ) {\n migrated = {\n ...migrated,\n 密钥: '',\n } as T;\n }\n\n const active_profile = migrated.api方案列表.find(\n profile => profile.名称 === migrated.当前api方案\n );\n if (active_profile?.backend === 'pi' && !hasConfiguredPiConnectionSnapshot(active_profile.pi)) {\n migrated = applyExtraModelApiProfile(migrated, active_profile) as T;\n }\n\n if (migrated.api方案列表.length === 0) {\n const has_legacy_custom_api =\n migrated.api地址.trim().length > 0 ||\n migrated.密钥.trim().length > 0 ||\n migrated.模型名称.trim().length > 0;\n if (has_legacy_custom_api) {\n // A newly selected but not yet configured Pi source may still carry legacy root\n // Custom fields. Do not turn those unrelated fields into a sendable Pi profile.\n if (\n getExtraModelApiProfileBackend(migrated) === 'pi' &&\n !hasConfiguredPiConnectionSnapshot(migrated.pi)\n ) {\n return reconcileExtraModelApiProfileSelection(migrated);\n }\n const profile = extractExtraModelApiProfileFields(migrated);\n migrated = {\n ...migrated,\n api方案列表: [\n {\n ...profile,\n 名称: DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n },\n ],\n 当前api方案: migrated.当前api方案 || DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n };\n }\n }\n\n return reconcileExtraModelApiProfileSelection(migrated);\n}\n\n/** 按名称查找并应用方案;不存在时抛出可展示的配置错误。 */\nexport function selectExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const profile = normalized_profiles.find(item => item.名称 === normalized_name);\n if (!profile) {\n throw new Error(\n tr('runtime.apiProfile.notFound', {\n name: normalized_name,\n })\n );\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: normalized_profiles,\n },\n profile\n );\n}\n\n/** 保存当前连接和请求选项,支持更新活动方案或指定名称,并防止覆盖其他同名方案。 */\nexport function saveCurrentExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name?: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name ?? config.当前api方案);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.selectOrEnterName'));\n }\n\n if (\n hasExtraModelApiProfile(config.api方案列表, target_name) &&\n normalizeExtraModelApiProfileName(config.当前api方案) !== target_name\n ) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const source_profile = normalized_profiles.find(\n item => item.名称 === normalizeExtraModelApiProfileName(config.当前api方案)\n );\n const normalized_source_profile =\n source_profile === undefined ? undefined : normalizeExtraModelApiProfile(source_profile);\n const profile: ExtraModelApiProfile = {\n ...normalized_source_profile,\n ...extractExtraModelApiProfileFields(config),\n 名称: target_name,\n };\n if (profile.backend !== 'pi') {\n delete profile.pi;\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: upsertExtraModelApiProfile(normalized_profiles, profile),\n },\n profile\n );\n}\n\n/** 校验新名称不为空且未占用,再将当前配置另存为独立方案。 */\nexport function saveAsNewExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.enterNewName'));\n }\n if (hasExtraModelApiProfile(config.api方案列表, target_name)) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n return saveCurrentExtraModelApiProfile(config, target_name);\n}\n","/** 兼容旧浏览器的 AbortSignal 静态接口;可选方法由运行时检测后补齐。 */\ntype AbortSignalConstructorWithPolyfills = {\n any?: (signals: Iterable) => AbortSignal;\n timeout?: (milliseconds: number) => AbortSignal;\n};\n\n/**\n * 为旧版酒馆浏览器补齐 Pi OAuth 刷新所需的 AbortSignal.timeout 和 any。\n * 仅在原生实现缺失时安装,并在取消后解除组合信号的监听。\n */\nexport function installPiAbortSignalPolyfills(): void {\n const constructor = AbortSignal as unknown as AbortSignalConstructorWithPolyfills;\n if (typeof constructor.timeout !== 'function') {\n Object.defineProperty(constructor, 'timeout', {\n configurable: true,\n writable: true,\n /** 创建到期自动取消的信号,拒绝负数和非有限超时时间。 */\n value(milliseconds: number): AbortSignal {\n if (!Number.isFinite(milliseconds) || milliseconds < 0) {\n throw new RangeError(\n 'AbortSignal timeout must be a non-negative finite number'\n );\n }\n const controller = new AbortController();\n setTimeout(\n () =>\n controller.abort(\n new DOMException('The operation timed out', 'TimeoutError')\n ),\n milliseconds\n );\n return controller.signal;\n },\n });\n }\n\n if (typeof constructor.any === 'function') {\n return;\n }\n\n Object.defineProperty(constructor, 'any', {\n configurable: true,\n writable: true,\n /** 合并多个取消信号,首次取消时保留原始原因并清理其他监听。 */\n value(signals: Iterable): AbortSignal {\n const controller = new AbortController();\n const listeners = new Map void>();\n /** 解除已注册的取消监听,避免组合信号结束后继续持有输入信号。 */\n const cleanup = () => {\n for (const [signal, listener] of listeners) {\n signal.removeEventListener('abort', listener);\n }\n listeners.clear();\n };\n /** 只转发首次取消原因,并统一释放组合信号的监听。 */\n const forward = (signal: AbortSignal) => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n cleanup();\n };\n\n for (const signal of signals) {\n if (listeners.has(signal)) {\n continue;\n }\n if (signal.aborted) {\n forward(signal);\n break;\n }\n /** 将当前输入信号的取消传递给组合信号。 */\n const listener = () => forward(signal);\n listeners.set(signal, listener);\n signal.addEventListener('abort', listener, { once: true });\n }\n return controller.signal;\n },\n });\n}\n","import type { ProviderHeaders } from '@earendil-works/pi-ai';\nimport YAML from 'yaml';\n\n/** 解析用户输入的 YAML 或 JSON;空输入表示未配置,语法错误只返回字段级提示。 */\nfunction parseYaml(value: string, label: string): unknown {\n if (!value.trim()) {\n return undefined;\n }\n try {\n return YAML.parse(value);\n } catch {\n throw new Error(`More source ${label} is not valid YAML or JSON`);\n }\n}\n\n/** 判断解析结果是否为非空、非数组对象,供请求配置的结构校验使用。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 解析自定义请求头,允许字符串或 null,并禁止覆盖由认证层管理的密钥头。 */\nexport function parsePiCustomHeaders(value: string): ProviderHeaders | undefined {\n const parsed = parseYaml(value, 'customHeaders');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customHeaders must be an object');\n }\n const headers: ProviderHeaders = {};\n for (const [name, header_value] of Object.entries(parsed)) {\n const normalized_name = name.trim();\n if (!normalized_name || (typeof header_value !== 'string' && header_value !== null)) {\n throw new Error('More source customHeaders values must be strings or null');\n }\n if (\n /^(authorization|proxy-authorization|x-api-key|x-goog-api-key)$/i.test(normalized_name)\n ) {\n throw new Error(\n `More source customHeaders cannot override authentication header '${normalized_name}'`\n );\n }\n headers[normalized_name] = header_value;\n }\n return headers;\n}\n\n/** 解析要合并到请求体的对象;空输入不产生覆盖。 */\nexport function parsePiCustomIncludeBody(value: string): Record | undefined {\n const parsed = parseYaml(value, 'customIncludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customIncludeBody must be an object');\n }\n return parsed;\n}\n\n/** 将数组或逗号、换行分隔的字段名规范为去重列表,拒绝空名称和非字符串。 */\nexport function parsePiCustomExcludeBody(value: string): string[] | undefined {\n const parsed = parseYaml(value, 'customExcludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n const fields =\n typeof parsed === 'string'\n ? parsed\n .split(/[\\n,]/)\n .map(item => item.trim())\n .filter(Boolean)\n : parsed;\n if (\n !Array.isArray(fields) ||\n fields.some(field => typeof field !== 'string' || !field.trim())\n ) {\n throw new Error('More source customExcludeBody must be an array of field names');\n }\n return [...new Set(fields.map(field => field.trim()))];\n}\n","import { getPendingPromptCaptureDiagnostics } from './prompt_capture';\n\nconst pi_request_controllers = new Map();\nconst pi_request_attempts = new Map();\n\nexport class PiRequestAbortedError extends Error {\n readonly generationId: string;\n\n /** 将取消原因与生成编号绑定,供重试策略和界面识别主动停止。 */\n constructor(generationId: string, reason?: unknown) {\n const detail =\n reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';\n super(\n detail\n ? `More source request '${generationId}' was aborted: ${detail}`\n : `More source request '${generationId}' was aborted`\n );\n this.name = 'PiRequestAbortedError';\n this.generationId = generationId;\n }\n}\n\n/** 识别 Pi 的主动取消错误,避免将其当作普通请求失败重试。 */\nexport function isPiRequestAbortedError(error: unknown): error is PiRequestAbortedError {\n return error instanceof PiRequestAbortedError;\n}\n\n/** 实际服务商请求的取消句柄,连接本次请求、用户停止和完整尝试的取消信号。 */\nexport type PiRequestControllerRegistration = {\n controller: AbortController;\n signal: AbortSignal;\n /** 移除本次登记和关联监听;释放本身不会取消请求。 */\n release: () => void;\n};\n\n/** 跨越提示词捕获与服务商请求的尝试句柄,用于记住两阶段之间发生的取消。 */\nexport type PiRequestAttemptRegistration = {\n signal: AbortSignal;\n /** 完整尝试结束后移除登记;调用多次不会影响同编号的新尝试。 */\n release: () => void;\n};\n\n/**\n * 为“提示词捕获 → 服务商请求”的完整尝试保留生成编号和取消信号。\n * 该记录在捕获结束后仍保留停止标记,防止间隙中的取消被后续请求注册绕过。\n */\nexport function beginPiRequestAttempt(generation_id: string): PiRequestAttemptRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_attempts.has(generation_id) || pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n pi_request_attempts.set(generation_id, controller);\n let released = false;\n /** 只释放本次尝试登记的记录,避免清理同编号的新实例。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n if (pi_request_attempts.get(generation_id) === controller) {\n pi_request_attempts.delete(generation_id);\n }\n };\n\n return { signal: controller.signal, release };\n}\n\n/** 登记实际请求控制器,并继承完整尝试已发生或随后发生的取消。 */\nexport function registerPiRequestController(\n generation_id: string,\n caller_signal?: AbortSignal\n): PiRequestControllerRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n const attempt_signal = pi_request_attempts.get(generation_id)?.signal;\n const abort_sources = [\n ...new Set(\n [attempt_signal, caller_signal].filter(\n (signal): signal is AbortSignal => signal !== undefined\n )\n ),\n ];\n const abort_listeners = abort_sources.map(signal => {\n /** 把调用方或尝试层信号的取消原因传给实际请求控制器。 */\n const forward_abort = () => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n };\n signal.addEventListener('abort', forward_abort, { once: true });\n if (signal.aborted) {\n forward_abort();\n }\n return { signal, forward_abort };\n });\n\n pi_request_controllers.set(generation_id, controller);\n let released = false;\n /** 解除上游取消监听,并只移除当前请求的登记项。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n for (const { signal, forward_abort } of abort_listeners) {\n signal.removeEventListener('abort', forward_abort);\n }\n if (pi_request_controllers.get(generation_id) === controller) {\n pi_request_controllers.delete(generation_id);\n }\n };\n\n return { controller, signal: controller.signal, release };\n}\n\n/** 在实际请求执行期间登记控制器,无论成功、失败还是取消都释放登记。 */\nexport async function withPiRequestController(\n generation_id: string,\n run: (signal: AbortSignal) => Promise,\n caller_signal?: AbortSignal\n): Promise {\n const registration = registerPiRequestController(generation_id, caller_signal);\n try {\n return await run(registration.signal);\n } finally {\n registration.release();\n }\n}\n\n/** 按生成编号取消 Pi 尝试和实际请求,保留可供后续注册检查的停止状态。 */\nexport function stopPiRequestById(generation_id: string, reason?: unknown): boolean {\n const controllers = [\n pi_request_attempts.get(generation_id),\n pi_request_controllers.get(generation_id),\n ];\n let stopped = false;\n for (const controller of controllers) {\n if (!controller || controller.signal.aborted) {\n continue;\n }\n controller.abort(reason ?? new PiRequestAbortedError(generation_id));\n stopped = true;\n }\n return stopped;\n}\n\n/** 同时停止同一编号的 Slash 捕获和 Pi 请求,覆盖生成链路的各个阶段。 */\nexport function stopExtraModelRequestById(generation_id: string, reason?: unknown): boolean {\n const slash_stopped = stopGenerationById(generation_id);\n const pi_stopped = stopPiRequestById(generation_id, reason);\n return slash_stopped || pi_stopped;\n}\n\n/** 取消所有仍活动的 Pi 尝试与请求,并返回受影响的生成编号数量。 */\nexport function stopAllPiRequests(reason?: unknown): number {\n const ids = [\n ...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()]),\n ].filter(id => {\n const attempt = pi_request_attempts.get(id);\n const runtime = pi_request_controllers.get(id);\n return (\n (attempt !== undefined && !attempt.signal.aborted) ||\n (runtime !== undefined && !runtime.signal.aborted)\n );\n });\n for (const id of ids) {\n stopPiRequestById(id, reason);\n }\n return ids.length;\n}\n\n/** 停止全部额外模型尝试,包括仍处于 Slash 提示词捕获阶段的请求。 */\nexport function stopAllExtraModelRequests(reason?: unknown): number {\n const ids = new Set([\n ...getPendingPromptCaptureDiagnostics().map(capture => capture.generationId),\n ...pi_request_attempts.keys(),\n ...pi_request_controllers.keys(),\n ]);\n let stopped = 0;\n for (const id of ids) {\n if (stopExtraModelRequestById(id, reason)) {\n stopped += 1;\n }\n }\n return stopped;\n}\n\n/** 返回当前登记的生成编号,供取消操作及生命周期诊断使用。 */\nexport function getActivePiRequestIds(): readonly string[] {\n return [...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()])];\n}\n\n/**\n * 脚本卸载时取消全部请求,但保留登记项直到各自结束。\n * 尝试记录仍充当停止标记,防止并发完成的捕获在卸载后发起新请求。\n */\nexport function clearPiRequestControllers(): void {\n stopAllExtraModelRequests(new Error('More source request registry disposed'));\n}\n","/** 连接与方案仅保存服务商到集中凭证记录的引用,不复制会轮换的 refresh token。 */\nexport type PiCredentialReferences = Record;\n\n/** 接受旧版服务商键和带服务商命名空间的新凭证编号,拒绝跨服务商引用。 */\nexport function isPiCredentialId(providerId: string, credentialId: string): boolean {\n return (\n credentialId === providerId ||\n credentialId.startsWith(`oauth:${encodeURIComponent(providerId)}:`)\n );\n}\n\n/** 规范显式引用;缺少字段的旧设置才从原 providerId 记录一次性继承登录态。 */\nexport function resolvePiCredentialReferences(\n pi: { credentialIds?: unknown },\n credentials: Record = {}\n): PiCredentialReferences {\n if (pi.credentialIds !== undefined) {\n if (\n typeof pi.credentialIds !== 'object' ||\n pi.credentialIds === null ||\n Array.isArray(pi.credentialIds)\n ) {\n return {};\n }\n return Object.fromEntries(\n Object.entries(pi.credentialIds).filter(\n ([providerId, id]) => typeof id === 'string' && isPiCredentialId(providerId, id)\n )\n );\n }\n return Object.fromEntries(\n Object.entries(credentials)\n .filter(\n ([id, value]) =>\n !id.startsWith('oauth:') &&\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'oauth'\n )\n .map(([providerId]) => [providerId, providerId])\n );\n}\n","import { useDataStore } from '@/store';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './credential_refs';\nimport type {\n AuthOperationOptions,\n Credential,\n CredentialInfo,\n CredentialStore,\n OAuthCredential,\n} from './pi_gateway';\n\n/** 按凭证编号排队执行的凭证操作,进入队列前不提前读取或修改凭证。 */\ntype CredentialTask = () => Promise;\n\n/** 复制 OAuth 凭证,避免调用方修改持久化对象本身。 */\nfunction cloneOAuthCredential(credential: OAuthCredential): OAuthCredential {\n return { ...credential };\n}\n\n/**\n * 从宽松保存的设置中读取符合 Pi 契约的 OAuth 凭证。\n * 未知字段可以保留,但缺失或无效的 access、refresh 和 expires 不进入运行时。\n */\nfunction readOAuthCredential(value: unknown): OAuthCredential | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return undefined;\n }\n\n const candidate = value as Record;\n if (\n candidate.type !== 'oauth' ||\n typeof candidate.access !== 'string' ||\n candidate.access.trim().length === 0 ||\n typeof candidate.refresh !== 'string' ||\n candidate.refresh.trim().length === 0 ||\n typeof candidate.expires !== 'number' ||\n !Number.isFinite(candidate.expires)\n ) {\n return undefined;\n }\n\n return cloneOAuthCredential(candidate as OAuthCredential);\n}\n\n/** 所有捕获同一设置仓库的适配器共用真实凭证编号的队列。 */\nconst storeChains = new WeakMap>>();\n/** 登录和解绑的代次按所属连接记录,阻止已经退出的待完成登录重新绑定。 */\nconst bindingGenerations = new WeakMap>();\n\ntype DataStore = ReturnType;\ntype CredentialStoreHooks = {\n beforeWrite?: (providerId: string, credentialId: string) => void;\n /** 返回 false 时仅解除绑定,保留其他方案仍使用的集中凭证。 */\n beforeDelete?: (providerId: string, credentialId: string | undefined) => boolean;\n};\n\n/** 在读取或写入凭证前检查调用方是否已取消操作。 */\nfunction throwIfAborted(options?: AuthOperationOptions): void {\n options?.signal?.throwIfAborted();\n}\n\n/** 取消时立即结束调用方等待,同时允许内部串行任务安全收尾。 */\nfunction raceWithAbort(task: Promise, signal?: AbortSignal): Promise {\n if (signal === undefined) {\n return task;\n }\n\n if (signal.aborted) {\n return Promise.reject(signal.reason);\n }\n return new Promise((resolve, reject) => {\n /** 解除取消监听并使用原始取消原因拒绝等待中的调用。 */\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort);\n reject(signal.reason);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n\n task.then(\n value => {\n signal.removeEventListener('abort', onAbort);\n resolve(value);\n },\n error => {\n signal.removeEventListener('abort', onAbort);\n reject(error);\n }\n );\n });\n}\n\n/**\n * 创建基于设置持久化的 OAuth 凭证仓库。\n * 同一凭证编号的修改和删除串行执行,写入前复查取消状态,避免刷新覆盖登出或新凭证。\n */\nexport function createPiCredentialStore(\n credentialIds?: PiCredentialReferences,\n dataStore: DataStore = useDataStore(),\n hooks: CredentialStoreHooks = {}\n): CredentialStore {\n const references = credentialIds === undefined ? undefined : { ...credentialIds };\n const getPersistedCredentials = () => dataStore.settings.额外模型解析配置.pi.credentials;\n const resolveId = (providerId: string) =>\n references === undefined ? providerId : references[providerId];\n const chains = storeChains.get(dataStore) ?? new Map>();\n storeChains.set(dataStore, chains);\n\n /** 按凭证编号串行安排凭证修改;前序失败不阻塞后续任务,取消后也保持队列顺序。 */\n const enqueue = (\n providerId: string,\n task: CredentialTask,\n options?: AuthOperationOptions\n ): Promise => {\n const previous = chains.get(providerId) ?? Promise.resolve();\n const queued = (async () => {\n throwIfAborted(options);\n await previous.catch(() => undefined);\n throwIfAborted(options);\n return task();\n })();\n const tail = queued.then(\n () => undefined,\n () => undefined\n );\n chains.set(providerId, tail);\n void tail.then(() => {\n if (chains.get(providerId) === tail) {\n chains.delete(providerId);\n }\n });\n\n return raceWithAbort(queued, options?.signal);\n };\n\n return {\n /** 读取并返回有效凭证的副本,忽略仓库中无法识别的记录。 */\n async read(\n providerId: string,\n options?: AuthOperationOptions\n ): Promise {\n throwIfAborted(options);\n const credentialId = resolveId(providerId);\n const credential =\n credentialId === undefined\n ? undefined\n : readOAuthCredential(getPersistedCredentials()[credentialId]);\n return credential === undefined ? undefined : cloneOAuthCredential(credential);\n },\n\n /** 列出有效 OAuth 凭证的服务商和类型,不向列表调用方暴露令牌内容。 */\n async list(options?: AuthOperationOptions): Promise {\n throwIfAborted(options);\n const ids =\n references ??\n Object.fromEntries(Object.keys(getPersistedCredentials()).map(id => [id, id]));\n return Object.entries(ids).flatMap(([providerId, credentialId]) =>\n readOAuthCredential(getPersistedCredentials()[credentialId]) === undefined\n ? []\n : [{ providerId, type: 'oauth' as const }]\n );\n },\n\n /** 在凭证队列内读取、更新并保存凭证,提交前再次检查取消信号。 */\n modify(\n providerId: string,\n fn: (current: Credential | undefined) => Promise,\n options?: AuthOperationOptions\n ): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.reject(new Error('No OAuth credential is bound to this connection'));\n }\n return enqueue(\n credentialId,\n async () => {\n const current = readOAuthCredential(getPersistedCredentials()[credentialId]);\n const next = await fn(\n current === undefined ? undefined : cloneOAuthCredential(current)\n );\n throwIfAborted(options);\n\n if (next === undefined) {\n return current === undefined ? undefined : cloneOAuthCredential(current);\n }\n\n const oauth = readOAuthCredential(next);\n if (oauth === undefined) {\n throw new TypeError(\n 'More source credential store only persists valid OAuth credentials'\n );\n }\n\n const persisted = cloneOAuthCredential(oauth);\n hooks.beforeWrite?.(providerId, credentialId);\n getPersistedCredentials()[credentialId] = persisted;\n return cloneOAuthCredential(persisted);\n },\n options\n );\n },\n\n /** 在同一凭证队列中删除凭证,与自动或手动刷新保持确定的先后顺序。 */\n delete(providerId: string, options?: AuthOperationOptions): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.resolve().then(() => {\n throwIfAborted(options);\n hooks.beforeDelete?.(providerId, undefined);\n });\n }\n return enqueue(\n credentialId,\n async () => {\n throwIfAborted(options);\n if (hooks.beforeDelete?.(providerId, credentialId) !== false) {\n delete getPersistedCredentials()[credentialId];\n }\n },\n options\n );\n },\n };\n}\n\n/** 捕获操作发起时所属的方案及活动连接,回调提交前复核原引用。 */\nfunction captureBinding(dataStore: DataStore) {\n const config = dataStore.settings.额外模型解析配置;\n const profileName = config.当前api方案;\n const profile = config.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const activeReferences = resolvePiCredentialReferences(config.pi, config.pi.credentials);\n const profileReferences =\n profile?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(profile.pi, config.pi.credentials);\n const source = config.模型来源;\n const provider = config.pi.provider;\n const authType = config.pi.authType;\n const generations = bindingGenerations.get(dataStore) ?? new Map();\n bindingGenerations.set(dataStore, generations);\n const scope = JSON.stringify([profileName, source, provider, authType]);\n const observedGenerations = new Map(generations);\n\n /** 仅修改发起操作时的所属方案;活动连接只有仍绑定原值时才同步更新。 */\n const replace = (providerId: string, nextId: string | undefined) => {\n const generationKey = `${scope}:${providerId}`;\n const generation = observedGenerations.get(generationKey) ?? 0;\n if ((generations.get(generationKey) ?? 0) !== generation) {\n throw new Error('The OAuth connection was changed by another authentication operation');\n }\n const current = dataStore.settings.额外模型解析配置;\n const target =\n profileReferences === undefined\n ? undefined\n : current.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const isActive =\n current.当前api方案 === profileName &&\n current.模型来源 === source &&\n current.pi.provider === provider &&\n current.pi.authType === authType;\n const currentRefs = resolvePiCredentialReferences(current.pi, current.pi.credentials);\n const targetRefs =\n target?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(target.pi, current.pi.credentials);\n if (isActive && currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The active OAuth credential changed while authentication was pending');\n }\n if (profileReferences !== undefined) {\n if (!target?.pi || targetRefs?.[providerId] !== profileReferences[providerId]) {\n throw new Error('The OAuth profile changed while authentication was pending');\n }\n } else if (!isActive || currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The OAuth connection changed while authentication was pending');\n }\n const updated = (refs: PiCredentialReferences) => {\n const result = { ...refs };\n if (nextId === undefined) delete result[providerId];\n else result[providerId] = nextId;\n return result;\n };\n generations.set(generationKey, generation + 1);\n if (target?.pi && targetRefs) target.pi.credentialIds = updated(targetRefs);\n if (isActive && currentRefs[providerId] === activeReferences[providerId]) {\n current.pi.credentialIds = updated(currentRefs);\n }\n };\n return { references: activeReferences, replace };\n}\n\n/** 是否仍有方案或活动连接引用该凭证;未迁移的旧方案也参与引用检查。 */\nfunction isCredentialReferenced(dataStore: DataStore, credentialId: string): boolean {\n const config = dataStore.settings.额外模型解析配置;\n const connections = [\n config.pi,\n ...config.api方案列表.flatMap(profile =>\n profile.backend === 'pi' && profile.pi ? [profile.pi] : []\n ),\n ];\n return connections.some(pi =>\n Object.values(resolvePiCredentialReferences(pi, config.pi.credentials)).includes(\n credentialId\n )\n );\n}\n\n/** 捕获当前连接的凭证引用;随后切换方案不会改变此对象读写的账号。 */\nexport function getPiCredentialStore(pi?: { credentialIds?: unknown }): CredentialStore {\n const dataStore = useDataStore();\n const settings = dataStore.settings.额外模型解析配置.pi;\n const references = resolvePiCredentialReferences(pi ?? settings, settings.credentials);\n return createPiCredentialStore(references, dataStore);\n}\n\n/** 捕获退出登录的所属方案,仅解绑该方案,最后一个引用消失后才删除集中凭证。 */\nexport function createPiOAuthLogoutStore(): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n return createPiCredentialStore(binding.references, dataStore, {\n beforeDelete(providerId, credentialId) {\n binding.replace(providerId, undefined);\n return credentialId !== undefined && !isCredentialReferenced(dataStore, credentialId);\n },\n });\n}\n\n/** 为登录结果分配全新记录,成功提交时才绑定原方案,避免覆盖其他账号或失败时丢失旧登录态。 */\nexport function createPiOAuthLoginStore(providerId: string, nonce: string): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n const id = `oauth:${encodeURIComponent(providerId)}:${nonce}`;\n return createPiCredentialStore({ [providerId]: id }, dataStore, {\n beforeWrite(writtenProvider, credentialId) {\n binding.replace(writtenProvider, credentialId);\n },\n });\n}\n","import {\n GoogleGenAI,\n type GenerateContentConfig,\n type GenerateContentParameters,\n type ThinkingConfig,\n} from '@google/genai';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n buildBaseOptions,\n calculateCost,\n clampThinkingLevel,\n collapseSystemMessages,\n convertGoogleMessages,\n convertGoogleTools,\n createAssistantMessageEventStream,\n getCurrentSystemPrompt,\n getCurrentTools,\n getDisabledGoogleThinkingConfig,\n isGoogleThinkingPart,\n mapGoogleStopReason,\n resolveGoogleFunctionCallingMode,\n resolveGoogleThinkingLevel,\n retainGoogleThoughtSignature,\n retryGoogleRequest,\n supportsGoogleStrictToolSampling,\n toGoogleThinkingLevel,\n usesGoogleThinkingLevel,\n type Api,\n type AssistantMessage,\n type AssistantMessageEventStream,\n type FetchFunction,\n type GoogleApiThinkingLevel,\n type Model,\n type ProviderHeaders,\n type ProviderStreams,\n type ResolvedGoogleThinkingLevel,\n type SimpleStreamOptions,\n type StreamOptions,\n type TextContent,\n type ThinkingBudgets,\n type ThinkingContent,\n type ToolCall,\n type TranscriptContext,\n} from './pi_gateway';\n\n/**\n * 当前锁定的 Google SDK 未公开实例级 fetch 选项,但内部 apiClient 保留了 HTTP 入口。\n * 只声明经过核对的最小结构;SDK 升级后若不再匹配,发送带凭证请求前即停止。\n * 该入口只替换当前实例的传输,不修改全局 fetch 或 SDK 原型。\n */\ntype InjectableGoogleClient = {\n apiClient?: {\n apiCall?: (\n url: string,\n init: RequestInit,\n retryOptions?: unknown,\n timeout?: number,\n abortSignal?: AbortSignal\n ) => Promise;\n };\n};\n\nconst GOOGLE_TRANSPORT_PROBE_KEY = 'mvu-google-transport-compatibility-check';\n\nexport class GoogleProxyAdapterCompatibilityError extends Error {\n readonly code = 'google_proxy_adapter_incompatible';\n readonly retryable = false;\n\n /** 将 SDK 传输注入点不兼容标记为不可重试错误,避免被包装成普通服务商失败。 */\n constructor() {\n super(\n 'The installed Google Generative AI SDK no longer exposes the audited per-client HTTP transport required by the SillyTavern proxy.'\n );\n this.name = 'GoogleProxyAdapterCompatibilityError';\n }\n}\n\n/** Google 自定义传输路径的请求选项,补齐通用流选项之外的工具选择和思考控制。 */\nexport interface GoogleProxyOptions extends StreamOptions {\n toolChoice?: 'auto' | 'none' | 'any';\n thinking?: {\n enabled: boolean;\n /** -1 由 Google 动态分配预算;0 关闭基于预算的思考。 */\n budgetTokens?: number;\n level?: GoogleApiThinkingLevel;\n };\n}\n\n/**\n * 仅替换单个 Google 客户端的 HTTP 调用实现,保留取消和超时信号。\n * 先验证当前 SDK 注入点;不修改全局 fetch 或客户端原型。\n */\nexport function installGoogleClientFetch(client: unknown, fetch_impl: FetchFunction): void {\n const api_client = (client as InjectableGoogleClient | null)?.apiClient;\n if (\n !api_client ||\n typeof api_client.apiCall !== 'function' ||\n api_client.apiCall.length !== 5\n ) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n\n installPiAbortSignalPolyfills();\n // GenAI 2.x moved abort/timeout out of RequestInit into apiCall arguments. Pi owns retries,\n // but the injected transport must preserve those signals through response-body consumption.\n /** 合并 RequestInit、SDK 参数和超时产生的信号,再交给本次请求的传输实现。 */\n const injected = (\n url: string,\n init: RequestInit,\n _retry_options?: unknown,\n timeout?: number,\n abort_signal?: AbortSignal\n ) => {\n const signals = [init.signal, abort_signal].filter(\n (signal): signal is AbortSignal => signal != null\n );\n if (timeout !== undefined && timeout > 0) {\n signals.push(AbortSignal.timeout(timeout));\n }\n if (signals.length === 0) {\n return fetch_impl(url, init);\n }\n const signal = AbortSignal.any(signals);\n signal.throwIfAborted();\n return fetch_impl(url, { ...init, signal });\n };\n try {\n api_client.apiCall = injected;\n } catch {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n if (api_client.apiCall !== injected) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n}\n\n/**\n * 在 Pi 懒加载流边界之前验证 SDK 注入点,保留明确的兼容错误。\n * 构造客户端与校验过程不发送网络请求。\n */\nexport function assertGoogleProxyAdapterCompatible(): void {\n const client = new GoogleGenAI({ apiKey: GOOGLE_TRANSPORT_PROBE_KEY });\n installGoogleClientFetch(client, async () => {\n throw new GoogleProxyAdapterCompatibilityError();\n });\n}\n\n/** 仅在请求指定了不同于全局 fetch 的传输实现时启用本地桥接。 */\nfunction shouldUseInjectedFetch(\n fetch_impl: FetchFunction | undefined\n): fetch_impl is FetchFunction {\n return fetch_impl !== undefined && fetch_impl !== globalThis.fetch;\n}\n\n/** 移除表示删除操作的 null 请求头,转换为 Google SDK 接受的字符串映射。 */\nfunction providerHeadersToRecord(\n headers: ProviderHeaders | undefined\n): Record | undefined {\n if (!headers) {\n return undefined;\n }\n const result: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== null) {\n result[key] = value;\n }\n }\n return Object.keys(result).length === 0 ? undefined : result;\n}\n\n/** 删除孤立的 UTF-16 代理码元,避免系统提示词编码失败。 */\nfunction sanitizeSurrogates(text: string): string {\n return text.replace(\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?): AssistantMessage {\n return {\n role: 'assistant',\n content: [],\n api: 'google-generative-ai' as Api,\n provider: model.provider,\n model: model.id,\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'pending',\n timestamp: Date.now(),\n };\n}\n\n/** 按本次模型端点和请求头创建 Google 客户端,并安装仅作用于该实例的 fetch。 */\nfunction createClient(\n model: Model<'google-generative-ai'>,\n api_key: string,\n options_headers: ProviderHeaders | undefined,\n fetch_impl: FetchFunction\n): GoogleGenAI {\n const http_options: {\n baseUrl?: string;\n apiVersion?: string;\n headers?: Record;\n } = {};\n if (model.baseUrl) {\n http_options.baseUrl = model.baseUrl;\n // Pi model base URLs already include their API version path.\n http_options.apiVersion = '';\n }\n const headers = providerHeadersToRecord({\n 'User-Agent': 'pi (browser)',\n ...model.headers,\n ...options_headers,\n });\n if (headers) {\n http_options.headers = headers;\n }\n\n const client = new GoogleGenAI({\n apiKey: api_key,\n httpOptions: Object.keys(http_options).length === 0 ? undefined : http_options,\n });\n installGoogleClientFetch(client, fetch_impl);\n return client;\n}\n\n/** 将 Pi 上下文、工具、采样和思考配置转换为 Google 生成参数,并保留取消信号。 */\nfunction buildParams(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions = {}\n): GenerateContentParameters {\n // Pi 1.x 将系统提示词和工具声明放进 transcript;Google 使用折叠后的系统消息。\n const normalized_context = collapseSystemMessages(context);\n const contents = convertGoogleMessages(model, normalized_context);\n const system_prompt = getCurrentSystemPrompt(normalized_context.messages);\n const tools = getCurrentTools(normalized_context.messages);\n const generation_config: GenerateContentConfig = {};\n if (options.temperature !== undefined) {\n generation_config.temperature = options.temperature;\n }\n if (options.maxTokens !== undefined) {\n generation_config.maxOutputTokens = options.maxTokens;\n }\n\n const supports_strict_mode = supportsGoogleStrictToolSampling(model.id);\n const function_calling_mode = tools.length\n ? resolveGoogleFunctionCallingMode(tools, options.toolChoice, supports_strict_mode)\n : undefined;\n const config: GenerateContentConfig = {\n ...(Object.keys(generation_config).length === 0 ? {} : generation_config),\n ...(system_prompt ? { systemInstruction: sanitizeSurrogates(system_prompt) } : {}),\n ...(tools.length ? { tools: convertGoogleTools(tools, false, supports_strict_mode) } : {}),\n ...(function_calling_mode === undefined\n ? {}\n : { toolConfig: { functionCallingConfig: { mode: function_calling_mode } } }),\n };\n\n if (options.thinking?.enabled && model.reasoning) {\n const thinking_config: ThinkingConfig = { includeThoughts: true };\n if (options.thinking.level !== undefined) {\n thinking_config.thinkingLevel = options.thinking\n .level as ThinkingConfig['thinkingLevel'];\n } else if (options.thinking.budgetTokens !== undefined) {\n thinking_config.thinkingBudget = options.thinking.budgetTokens;\n }\n config.thinkingConfig = thinking_config;\n } else if (model.reasoning && options.thinking && !options.thinking.enabled) {\n config.thinkingConfig = getDisabledGoogleThinkingConfig(model);\n }\n\n if (options.signal) {\n if (options.signal.aborted) {\n throw new Error('Request aborted');\n }\n config.abortSignal = options.signal;\n }\n\n return {\n model: model.id,\n contents,\n config,\n };\n}\n\nlet tool_call_counter = 0;\n\n/**\n * 通过注入的传输执行 Google 生成,并转换为 Pi 的文本、思考、工具和用量事件。\n * 客户端兼容性同步校验,避免初始化失败被吞为普通模型响应。\n */\nfunction streamWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const output = createOutput(model);\n const stream = createAssistantMessageEventStream();\n const api_key = options.apiKey;\n\n // Construct and verify the instance-local injection point synchronously. This cannot perform\n // network I/O, and preserves the compatibility error instead of converting it to provider data.\n const client = api_key ? createClient(model, api_key, options.headers, fetch_impl) : undefined;\n\n void (async () => {\n try {\n if (!api_key || !client) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n\n let params = buildParams(model, context, options);\n const next_params = await options.onPayload?.(params, model);\n if (next_params !== undefined) {\n params = next_params as GenerateContentParameters;\n }\n const google_stream = await retryGoogleRequest(\n () => client.models.generateContentStream(params),\n options\n );\n\n stream.push({ type: 'start', partial: output });\n let current_block: TextContent | ThinkingContent | null = null;\n const blocks = output.content;\n /** 取得当前输出块下标,使增量事件始终指向最新内容块。 */\n const block_index = () => blocks.length - 1;\n\n for await (const chunk of google_stream) {\n output.responseId ||= chunk.responseId;\n const candidate = chunk.candidates?.[0];\n if (candidate?.content?.parts) {\n for (const part of candidate.content.parts) {\n if (part.text !== undefined) {\n const is_thinking = isGoogleThinkingPart(part);\n if (\n !current_block ||\n (is_thinking && current_block.type !== 'thinking') ||\n (!is_thinking && current_block.type !== 'text')\n ) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n if (is_thinking) {\n current_block = {\n type: 'thinking',\n thinking: '',\n thinkingSignature: undefined,\n };\n output.content.push(current_block);\n stream.push({\n type: 'thinking_start',\n contentIndex: block_index(),\n partial: output,\n });\n } else {\n current_block = { type: 'text', text: '' };\n output.content.push(current_block);\n stream.push({\n type: 'text_start',\n contentIndex: block_index(),\n partial: output,\n });\n }\n }\n\n if (current_block.type === 'thinking') {\n current_block.thinking += part.text;\n current_block.thinkingSignature = retainGoogleThoughtSignature(\n current_block.thinkingSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'thinking_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n } else {\n current_block.text += part.text;\n current_block.textSignature = retainGoogleThoughtSignature(\n current_block.textSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'text_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n }\n }\n\n if (part.functionCall) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n current_block = null;\n }\n\n const provided_id = part.functionCall.id;\n const needs_new_id =\n !provided_id ||\n output.content.some(\n block => block.type === 'toolCall' && block.id === provided_id\n );\n const tool_call_id = needs_new_id\n ? `${part.functionCall.name}_${Date.now()}_${++tool_call_counter}`\n : provided_id;\n const tool_call: ToolCall = {\n type: 'toolCall',\n id: tool_call_id,\n name: part.functionCall.name || '',\n arguments: (part.functionCall.args as ToolCall['arguments']) ?? {},\n ...(part.thoughtSignature\n ? { thoughtSignature: part.thoughtSignature }\n : {}),\n };\n output.content.push(tool_call);\n stream.push({\n type: 'toolcall_start',\n contentIndex: block_index(),\n partial: output,\n });\n stream.push({\n type: 'toolcall_delta',\n contentIndex: block_index(),\n delta: JSON.stringify(tool_call.arguments),\n partial: output,\n });\n stream.push({\n type: 'toolcall_end',\n contentIndex: block_index(),\n toolCall: tool_call,\n partial: output,\n });\n }\n }\n }\n\n if (candidate?.finishReason) {\n output.rawStopReason = candidate.finishReason;\n output.stopReason = mapGoogleStopReason(candidate.finishReason);\n if (\n output.stopReason === 'stop' &&\n output.content.some(block => block.type === 'toolCall')\n ) {\n output.stopReason = 'toolUse';\n }\n }\n\n if (chunk.usageMetadata) {\n output.usage = {\n input:\n (chunk.usageMetadata.promptTokenCount || 0) -\n (chunk.usageMetadata.cachedContentTokenCount || 0),\n output:\n (chunk.usageMetadata.candidatesTokenCount || 0) +\n (chunk.usageMetadata.thoughtsTokenCount || 0),\n cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,\n cacheWrite: 0,\n reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,\n totalTokens: chunk.usageMetadata.totalTokenCount || 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n calculateCost(model, output.usage);\n }\n }\n\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n\n if (options.signal?.aborted) {\n throw new Error('Request was aborted');\n }\n if (output.stopReason === 'pending') {\n throw new Error('Google stream ended without a finish reason');\n }\n if (output.stopReason === 'aborted' || output.stopReason === 'error') {\n throw new Error(\n output.rawStopReason\n ? `Provider stopped with: ${output.rawStopReason}`\n : 'An unknown error occurred'\n );\n }\n\n stream.push({ type: 'done', reason: output.stopReason, message: output });\n stream.end();\n } catch (error) {\n for (const block of output.content) {\n if ('index' in block) {\n delete (block as { index?: number }).index;\n }\n }\n output.stopReason = options.signal?.aborted ? 'aborted' : 'error';\n output.errorMessage = formatGoogleError(error);\n stream.push({ type: 'error', reason: output.stopReason, error: output });\n stream.end();\n }\n })();\n\n return stream;\n}\n\n/** 优先使用自定义思考预算,否则采用相应模型的预算映射;未知模型交由服务端动态分配。 */\nfunction getGoogleBudget(\n model: Model<'google-generative-ai'>,\n level: ResolvedGoogleThinkingLevel,\n custom_budgets?: ThinkingBudgets\n): number {\n if (custom_budgets?.[level] !== undefined) {\n return custom_budgets[level];\n }\n if (model.id.includes('2.5-pro')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 32768 }[level];\n }\n if (model.id.includes('2.5-flash-lite')) {\n return { minimal: 512, low: 2048, medium: 8192, high: 24576 }[level];\n }\n if (model.id.includes('2.5-flash')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 24576 }[level];\n }\n return -1;\n}\n\n/** 将 Pi 简化选项解析为模型适用的思考等级或预算,再复用注入传输的流实现。 */\nfunction streamSimpleWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: SimpleStreamOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const api_key = options.apiKey;\n if (!api_key) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n const base: GoogleProxyOptions = {\n ...buildBaseOptions(model, context, options, api_key),\n toolChoice: options.toolChoice,\n };\n if (!options.reasoning) {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n\n const clamped_reasoning = clampThinkingLevel(model, options.reasoning);\n if (clamped_reasoning === 'off') {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);\n if (usesGoogleThinkingLevel(model)) {\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: { enabled: true, level: toGoogleThinkingLevel(resolved_level) },\n },\n fetch_impl\n );\n }\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: {\n enabled: true,\n budgetTokens: getGoogleBudget(model, resolved_level, options.thinkingBudgets),\n },\n },\n fetch_impl\n );\n}\n\n/** 普通浏览器请求沿用 Pi 上游实现;具有独立 fetch 的代理或非流式请求走本地桥接。 */\nexport function createGoogleProxyAwareApi(upstream: ProviderStreams): ProviderStreams {\n return {\n /** 为完整流选项选择上游实现或带请求级传输的 Google 桥接。 */\n stream(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.stream(model, context, options);\n }\n return streamWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options as GoogleProxyOptions,\n options.fetch\n );\n },\n /** 为简化流选项选择上游实现或带请求级传输的 Google 桥接。 */\n streamSimple(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.streamSimple(model, context, options);\n }\n return streamSimpleWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options,\n options.fetch\n );\n },\n };\n}\n","import { type Api, type Model } from './pi_gateway';\nimport {\n getPiCatalogModels,\n getPiProviderDefinition,\n isPiCatalogModelApiCompatible,\n type PiAuthType,\n type PiProviderDefinition,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_registry';\nimport {\n getPiProviderApiBaseUrl,\n normalizePiApiBaseEndpoint,\n normalizePiTargetEndpoint,\n PiEndpointValidationError,\n} from './provider_target';\n\nexport { resolvePiApiKeyScope } from './provider_target';\n\n/** 设置中保存的 Pi 连接与模型选择,字符串取值仍需通过注册表校验。 */\nexport interface PiModelConfiguration {\n provider: string;\n api: string;\n authType: string;\n endpoint?: string;\n model: string;\n /** 0 或未填写表示使用目录窗口;正值覆盖目录,目录外模型必须显式填写。 */\n contextWindow?: number;\n}\n\n/** 模型解析边界的原始输入,接受 unknown 以校验旧设置和外部调用。 */\nexport interface ResolvePiModelInput {\n piConfig: PiModelConfiguration | unknown;\n /** 用户配置的最大回复 token 数,尚未应用模型目录的输出上限。 */\n maxTokens: number | unknown;\n apiKey?: string | unknown;\n}\n\n/** 连接、认证和模型预算校验的失败分类,供预检及本地化提示使用。 */\nexport type PiModelResolutionErrorCode =\n | 'invalid_config'\n | 'unknown_provider'\n | 'unsupported_api'\n | 'unsupported_auth'\n | 'missing_api_key'\n | 'invalid_endpoint'\n | 'custom_endpoint_not_allowed'\n | 'oauth_endpoint_not_allowed'\n | 'oauth_api_mismatch'\n | 'missing_model'\n | 'invalid_context_window'\n | 'missing_context_window'\n | 'invalid_max_tokens'\n | 'max_tokens_exceed_context';\n\nexport class PiModelResolutionError extends Error {\n /** 保留模型配置错误码,供预检和界面给出稳定的错误分类。 */\n constructor(\n public readonly code: PiModelResolutionErrorCode,\n message: string\n ) {\n super(message);\n this.name = 'PiModelResolutionError';\n }\n}\n\n/** 校验并规范化后的连接配置,保留目录依据与最终预算,供构造本次 Pi Model 使用。 */\nexport interface ValidatedPiConfiguration {\n definition: PiProviderDefinition;\n provider: PiProviderKey;\n api: PiWireApi;\n authType: PiAuthType;\n endpoint: string;\n modelId: string;\n /** 用户显式窗口;0 表示采用匹配目录项的上下文窗口。 */\n manualContextWindow: number;\n configuredMaxTokens: number;\n apiKey?: string;\n /** 仅在默认端点且协议兼容时使用,避免套用自定义端点的同名模型。 */\n catalogModel?: Model;\n effectiveContextWindow: number;\n /** 用户回复额度与适用目录输出上限的较小值。 */\n effectiveMaxTokens: number;\n}\n\n/** 交给运行时的模型解析结果,model 是本次请求独立的目录副本或动态元数据。 */\nexport interface ResolvedPiModel {\n definition: PiProviderDefinition;\n model: Model;\n /** 是否实际采用了匹配目录的元数据,而非仅命中同名模型。 */\n catalogHit: boolean;\n effectiveContextWindow: number;\n effectiveMaxTokens: number;\n authType: PiAuthType;\n apiKey?: string;\n}\n\n/** 判断输入是否为可读取配置字段的非空、非数组对象。 */\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 读取并规范必填字符串,缺失时抛出指定的模型配置错误。 */\nfunction requiredString(\n source: Record,\n field: string,\n errorCode: PiModelResolutionErrorCode,\n label: string\n): string {\n const value = source[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw new PiModelResolutionError(errorCode, `${label} must be a non-empty string`);\n }\n return value.trim();\n}\n\n/** 规范自定义端点并执行浏览器传输限制,将端点错误转换为模型配置错误。 */\nexport function normalizePiEndpoint(endpoint: string): string {\n try {\n return normalizePiTargetEndpoint(endpoint);\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n}\n\n/** 判断端点是否等同服务商的协议默认地址,空值与显式填写默认地址视为相同。 */\nexport function isPiDefaultProviderEndpoint(\n definition: PiProviderDefinition,\n endpoint: string,\n api: PiWireApi = definition.defaultApi\n): boolean {\n const requested = endpoint.trim();\n if (requested === '') {\n return true;\n }\n try {\n return (\n normalizePiApiBaseEndpoint(api, requested) ===\n normalizePiApiBaseEndpoint(api, getPiProviderApiBaseUrl(definition, api))\n );\n } catch {\n return false;\n }\n}\n\n/** 校验允许 0 哨兵值的配置整数,拒绝无效值隐式回退。 */\nfunction validateNonNegativeInteger(value: unknown, field: string): number {\n if (value === undefined) {\n return 0;\n }\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n `${field} must be a non-negative integer`\n );\n }\n return value;\n}\n\n/** 校验必须大于 0 的配置整数,供回复 token 等硬性预算使用。 */\nfunction validatePositiveInteger(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new PiModelResolutionError(\n 'invalid_max_tokens',\n `${field} must be a positive integer`\n );\n }\n return value;\n}\n\n/** 从所选服务商目录中查找模型,并检查目录项与当前协议是否兼容。 */\nfunction findCatalogModel(\n definition: PiProviderDefinition,\n modelId: string\n): Model | undefined {\n return getPiCatalogModels(definition.key).find(model => model.id === modelId);\n}\n\n/**\n * 校验服务商、协议、认证、端点、模型和 token 参数的组合。\n * 只使用适用于当前端点的目录元数据,不修改设置或共享目录对象。\n */\nexport function validatePiConfiguration(input: ResolvePiModelInput): ValidatedPiConfiguration {\n if (!isRecord(input.piConfig)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'More source configuration must be an object'\n );\n }\n\n const provider = requiredString(\n input.piConfig,\n 'provider',\n 'unknown_provider',\n 'More source provider'\n );\n const definition = getPiProviderDefinition(provider);\n if (!definition) {\n throw new PiModelResolutionError(\n 'unknown_provider',\n `Unknown More source provider: ${provider}`\n );\n }\n\n const api = requiredString(input.piConfig, 'api', 'unsupported_api', 'More source API');\n if (!definition.allowedApis.some(candidate => candidate === api)) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Provider ${definition.key} does not support API ${api}`\n );\n }\n\n const authType = requiredString(\n input.piConfig,\n 'authType',\n 'unsupported_auth',\n 'More source auth type'\n );\n if (!definition.allowedAuthTypes.some(candidate => candidate === authType)) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} does not support ${authType} authentication`\n );\n }\n\n const rawEndpoint = input.piConfig.endpoint;\n if (rawEndpoint !== undefined && typeof rawEndpoint !== 'string') {\n throw new PiModelResolutionError(\n 'invalid_endpoint',\n 'More source endpoint must be a string'\n );\n }\n const requestedEndpoint = (rawEndpoint ?? '').trim();\n\n if (authType === 'oauth') {\n if (!definition.oauth) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} has no OAuth registration`\n );\n }\n if (requestedEndpoint) {\n throw new PiModelResolutionError(\n 'oauth_endpoint_not_allowed',\n 'OAuth providers cannot use a custom endpoint'\n );\n }\n if (api !== definition.oauth.api) {\n throw new PiModelResolutionError(\n 'oauth_api_mismatch',\n `OAuth for ${definition.key} requires API ${definition.oauth.api}`\n );\n }\n } else if (requestedEndpoint && !definition.allowCustomEndpoint) {\n throw new PiModelResolutionError(\n 'custom_endpoint_not_allowed',\n `Provider ${definition.key} does not allow a custom endpoint`\n );\n }\n\n let endpoint: string;\n const defaultEndpoint = getPiProviderApiBaseUrl(definition, api as PiWireApi);\n try {\n endpoint = requestedEndpoint\n ? normalizePiApiBaseEndpoint(api as PiWireApi, requestedEndpoint)\n : defaultEndpoint;\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n const usesDefaultEndpoint = endpoint === normalizePiEndpoint(defaultEndpoint);\n const modelId = requiredString(input.piConfig, 'model', 'missing_model', 'More source model');\n const manualContextWindow = validateNonNegativeInteger(\n input.piConfig.contextWindow,\n 'More source contextWindow'\n );\n const configuredMaxTokens = validatePositiveInteger(input.maxTokens, 'maximum reply tokens');\n const catalogModelById = findCatalogModel(definition, modelId);\n const catalogModelMatchesApi =\n catalogModelById !== undefined &&\n isPiCatalogModelApiCompatible(definition, catalogModelById, api as PiWireApi);\n if (catalogModelById && !catalogModelMatchesApi && usesDefaultEndpoint) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Model ${modelId} is registered for ${catalogModelById.api}, not ${api}`\n );\n }\n // On a custom OpenAI-compatible endpoint the same identifier may refer to an entirely\n // different model. Require manual metadata and treat it as dynamic instead of replaying\n // catalog metadata from another wire API.\n const catalogModel =\n catalogModelMatchesApi && usesDefaultEndpoint ? catalogModelById : undefined;\n\n if (!catalogModel && manualContextWindow === 0) {\n throw new PiModelResolutionError(\n 'missing_context_window',\n `Model ${modelId} is not in the ${definition.key} catalog; contextWindow is required`\n );\n }\n\n const effectiveContextWindow =\n manualContextWindow > 0 ? manualContextWindow : catalogModel!.contextWindow;\n if (!Number.isInteger(effectiveContextWindow) || effectiveContextWindow <= 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n 'effective contextWindow must be a positive integer'\n );\n }\n if (configuredMaxTokens > effectiveContextWindow) {\n throw new PiModelResolutionError(\n 'max_tokens_exceed_context',\n 'maximum reply tokens must not exceed contextWindow'\n );\n }\n\n const effectiveMaxTokens = catalogModel\n ? Math.min(configuredMaxTokens, catalogModel.maxTokens)\n : configuredMaxTokens;\n\n let apiKey: string | undefined;\n if (authType === 'api_key') {\n if (typeof input.apiKey !== 'string' || input.apiKey.trim() === '') {\n throw new PiModelResolutionError(\n 'missing_api_key',\n `An API key is required for ${definition.key}`\n );\n }\n apiKey = input.apiKey.trim();\n }\n\n return {\n definition,\n provider: definition.providerId,\n api: api as PiWireApi,\n authType: authType as PiAuthType,\n endpoint,\n modelId,\n manualContextWindow,\n configuredMaxTokens,\n apiKey,\n catalogModel,\n effectiveContextWindow,\n effectiveMaxTokens,\n };\n}\n\n/** 为目录外或自定义端点模型构造独立元数据,使用显式窗口与保守能力默认值。 */\nfunction createDynamicModel(configuration: ValidatedPiConfiguration): Model {\n return {\n id: configuration.modelId,\n name: configuration.modelId,\n api: configuration.api,\n provider: configuration.provider,\n baseUrl: configuration.endpoint,\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: configuration.effectiveContextWindow,\n maxTokens: configuration.effectiveMaxTokens,\n };\n}\n\n/** 复制目录模型后应用本次端点和预算,防止请求修改共享目录元数据。 */\nfunction cloneCatalogModel(configuration: ValidatedPiConfiguration): Model {\n const original = configuration.catalogModel!;\n const model = structuredClone(original) as Model;\n\n model.api = configuration.api;\n model.provider = configuration.provider;\n model.baseUrl = configuration.endpoint;\n model.contextWindow = configuration.effectiveContextWindow;\n model.maxTokens = configuration.effectiveMaxTokens;\n\n // Compatibility metadata is API-shaped. Do not replay Responses flags into Completions.\n if (original.api !== configuration.api) {\n delete model.compat;\n }\n return model;\n}\n\n/** 在配置校验后选择目录模型或动态模型,并返回本次请求的有效预算及认证信息。 */\nexport function resolvePiModel(input: ResolvePiModelInput): ResolvedPiModel {\n const configuration = validatePiConfiguration(input);\n const catalogHit = configuration.catalogModel !== undefined;\n return {\n definition: configuration.definition,\n model: catalogHit ? cloneCatalogModel(configuration) : createDynamicModel(configuration),\n catalogHit,\n effectiveContextWindow: configuration.effectiveContextWindow,\n effectiveMaxTokens: configuration.effectiveMaxTokens,\n authType: configuration.authType,\n apiKey: configuration.apiKey,\n };\n}\n\n/** 从中文字段的额外模型设置中提取 Pi 连接和回复预算,再调用统一模型解析。 */\nexport function resolvePiModelFromExtraModelSettings(settings: unknown): ResolvedPiModel {\n if (!isRecord(settings)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'extra-model settings must be an object'\n );\n }\n return resolvePiModel({\n piConfig: settings.pi,\n maxTokens: settings['最大回复token数'],\n apiKey: settings['密钥'],\n });\n}\n","import {\n createPiOAuthLoginStore,\n createPiOAuthLogoutStore,\n getPiCredentialStore,\n} from './credential_store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport type { CredentialStore, ModelAuth, OAuthAuth, OAuthCredential } from './pi_gateway';\nimport { getPiProviderRegistration, type PiOAuthDefinition } from './provider_registry';\n\nconst DEFAULT_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst CLOSED_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst MAX_CLOSED_ATTEMPTS = 128;\nconst REFRESH_TIMEOUT_MS = 15_000;\nconst OPENAI_CODEX_PROVIDER_ID = 'openai-codex';\nconst ANTHROPIC_PROVIDER_ID = 'anthropic';\nconst OPENAI_ACCOUNT_CLAIM = 'https://api.openai.com/auth';\n\n/** 浏览器登录、回调校验、令牌交换及持久化的失败分类,供界面和取消逻辑使用。 */\nexport type PiOAuthErrorCode =\n | 'unsupported_provider'\n | 'browser_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_failed'\n | 'attempt_expired'\n | 'attempt_used'\n | 'cancelled'\n | 'browser_network'\n | 'token_http'\n | 'token_response'\n | 'account_id'\n | 'missing_credential'\n | 'credential_store';\n\nexport class PiOAuthError extends Error {\n readonly code: PiOAuthErrorCode;\n\n /** 为浏览器 OAuth 失败保留稳定错误码,供界面和取消逻辑分类处理。 */\n constructor(code: PiOAuthErrorCode, message: string) {\n super(message);\n this.name = 'PiOAuthError';\n this.code = code;\n }\n}\n\n/** 可展示给界面的登录尝试信息,不包含 PKCE verifier 或令牌。 */\nexport type PiOAuthAttemptView = {\n id: string;\n providerId: string;\n authorizationUrl: string;\n /** 本次回调可被接受的截止时间,使用 Unix 毫秒时间戳。 */\n expiresAt: number;\n};\n\n/** 对外展示的 OAuth 凭证摘要;loggedIn 表示已存有凭证,过期凭证仍可尝试刷新。 */\nexport type PiOAuthCredentialStatus = {\n loggedIn: boolean;\n type?: 'oauth';\n /** 已保存访问令牌的过期时间,使用 Unix 毫秒时间戳。 */\n expiresAt?: number;\n};\n\n/** 令牌请求需要的最小 fetch 契约,便于注入测试传输。 */\ntype FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise;\n\n/** 生成 state、PKCE verifier 和 SHA-256 challenge 所需的最小 Web Crypto 接口。 */\ntype CryptoLike = {\n getRandomValues(array: T): T;\n subtle: {\n digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise;\n };\n};\n\n/** 单次登录引用的只读服务商 OAuth 注册信息。 */\ntype OAuthMetadata = Readonly;\n\n/** 可替换的浏览器与存储依赖,未传入时使用页面环境和共享凭证仓库。 */\ntype PiOAuthDependencies = {\n fetch?: FetchLike;\n crypto?: CryptoLike;\n /** 返回 Unix 毫秒时间戳,用于登录有效期和令牌过期计算。 */\n now?: () => number;\n credentialStore?: CredentialStore;\n};\n\n/** 发起登录时的依赖、取消信号及回调等待期限。 */\nexport type BeginPiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n /** 从创建授权链接起计算的有效时长,单位为毫秒。 */\n attemptTtlMs?: number;\n};\n\n/** 完成回调校验和授权码交换时使用的依赖与取消信号。 */\nexport type CompletePiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n};\n\n/** 凭证查询、删除等操作共享的取消与存储配置。 */\nexport type PiOAuthOperationOptions = {\n signal?: AbortSignal;\n credentialStore?: CredentialStore;\n};\n\n/** 刷新令牌所需的操作选项,只额外依赖网络和时钟,不需要重新生成 PKCE。 */\nexport type RefreshPiOAuthOptions = PiOAuthOperationOptions &\n Pick;\n\n/** 构建 Pi OAuthAuth 桥接对象时注入的浏览器能力与登录有效时长。 */\nexport type BrowserOAuthAuthOptions = Pick & {\n attemptTtlMs?: number;\n};\n\n/** 仅保存在内存中的登录尝试,负责回调防重放、有效期和交换请求取消。 */\ntype PendingAttempt = {\n id: string;\n providerId: string;\n metadata: OAuthMetadata;\n /** 登录开始时捕获的所属方案仓库,回调不再解析当前界面。 */\n credentialStore: CredentialStore;\n /** PKCE 原始验证串,只在交换授权码时提交给令牌端点。 */\n verifier: string;\n /** 与回调严格匹配的随机 state,用于绑定本次授权请求。 */\n state: string;\n expiresAt: number;\n /** exchanging 表示授权码已被领取处理,阻止重复提交同一回调。 */\n phase: 'pending' | 'exchanging';\n controller: AbortController;\n timeout: ReturnType;\n /** 清理调用方取消监听,避免尝试结束后继续持有外部信号。 */\n detachCallerAbort?: () => void;\n};\n\n/** 短期保留已关闭尝试的原因,使迟到或重复回调得到明确错误。 */\ntype ClosedAttemptReason = 'used' | 'expired' | 'cancelled';\n\nconst pendingAttempts = new Map();\nconst closedAttempts = new Map();\n\n/** 构造带稳定错误码的 OAuth 错误。 */\nfunction oauthError(code: PiOAuthErrorCode, message: string): PiOAuthError {\n return new PiOAuthError(code, message);\n}\n\n/** 创建统一的登录取消错误,供各异步阶段使用。 */\nfunction cancellationError(): PiOAuthError {\n return oauthError('cancelled', 'More source OAuth login was cancelled.');\n}\n\n/** 将已取消信号转换为 OAuth 取消错误,阻止继续交换或写入令牌。 */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw cancellationError();\n }\n}\n\n/** 解析可注入的时钟,使登录有效期和刷新测试可以精确控制时间。 */\nfunction getNow(options?: Pick): () => number {\n return options?.now ?? Date.now;\n}\n\n/** 解析浏览器令牌请求使用的 fetch,缺少能力时返回明确的环境错误。 */\nfunction getFetch(options?: Pick): FetchLike {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires the browser Fetch API for token requests.'\n );\n }\n return fetchImpl.bind(globalThis) as FetchLike;\n}\n\n/** 确认 Web Crypto 提供随机数和 SHA-256,满足 OAuth state 与 PKCE 的生成要求。 */\nfunction getCrypto(options?: Pick): CryptoLike {\n const cryptoImpl = options?.crypto ?? (globalThis.crypto as CryptoLike | undefined);\n if (\n !cryptoImpl ||\n typeof cryptoImpl.getRandomValues !== 'function' ||\n typeof cryptoImpl.subtle?.digest !== 'function'\n ) {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires Web Crypto with SHA-256 support.'\n );\n }\n return cryptoImpl;\n}\n\n/** 优先使用注入的凭证仓库,否则复用运行时的共享仓库。 */\nfunction getCredentialStore(\n options?: Pick\n): CredentialStore {\n return options?.credentialStore ?? getPiCredentialStore();\n}\n\n/** 只解析已注册且支持浏览器登录的 OAuth 服务商,拒绝其他授权流程。 */\nfunction getOAuthMetadata(providerId: string): OAuthMetadata {\n const registration = getPiProviderRegistration(providerId);\n const metadata = registration?.oauth;\n if (!metadata || metadata.providerId !== providerId) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n if (providerId !== ANTHROPIC_PROVIDER_ID && providerId !== OPENAI_CODEX_PROVIDER_ID) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n return metadata;\n}\n\n/** 将随机字节或摘要编码为无填充的 Base64URL,供 PKCE 和 state 使用。 */\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return globalThis.btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\n/** 用密码学随机数生成指定字节长度的 Base64URL 标识。 */\nfunction randomBase64Url(cryptoImpl: CryptoLike, length: number): string {\n return bytesToBase64Url(cryptoImpl.getRandomValues(new Uint8Array(length)));\n}\n\n/** 生成仅保留在内存中的 verifier,并计算 S256 challenge。 */\nasync function createPkce(cryptoImpl: CryptoLike): Promise<{\n verifier: string;\n challenge: string;\n}> {\n const verifier = randomBase64Url(cryptoImpl, 32);\n const verifierBytes = Uint8Array.from(verifier, character => character.charCodeAt(0));\n const digest = await cryptoImpl.subtle.digest('SHA-256', verifierBytes);\n return {\n verifier,\n challenge: bytesToBase64Url(new Uint8Array(digest)),\n };\n}\n\n/** 按服务商元数据组装授权地址,附带回调地址、scope、PKCE 和 state。 */\nfunction createAuthorizationUrl(metadata: OAuthMetadata, challenge: string, state: string): string {\n const url = new URL(metadata.authorizeUrl);\n for (const [name, value] of Object.entries(metadata.authorizeParams)) {\n url.searchParams.set(name, value);\n }\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', metadata.clientId);\n url.searchParams.set('redirect_uri', metadata.redirectUri);\n url.searchParams.set('scope', metadata.scope);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n return url.toString();\n}\n\n/** 短期记录已关闭尝试的原因,并限制记录数量,使重复回调可得到准确错误。 */\nfunction rememberClosedAttempt(id: string, reason: ClosedAttemptReason): void {\n const now = Date.now();\n for (const [closedId, closed] of closedAttempts) {\n if (closed.removeAfter <= now) {\n closedAttempts.delete(closedId);\n }\n }\n while (closedAttempts.size >= MAX_CLOSED_ATTEMPTS) {\n const oldestId = closedAttempts.keys().next().value as string | undefined;\n if (oldestId === undefined) {\n break;\n }\n closedAttempts.delete(oldestId);\n }\n closedAttempts.set(id, { reason, removeAfter: now + CLOSED_ATTEMPT_TTL_MS });\n}\n\n/** 关闭当前登录尝试,释放计时器和监听,清除 verifier 与 state,并记录关闭原因。 */\nfunction closeAttempt(attempt: PendingAttempt, reason: ClosedAttemptReason, abort: boolean): void {\n if (pendingAttempts.get(attempt.id) !== attempt) {\n return;\n }\n pendingAttempts.delete(attempt.id);\n clearTimeout(attempt.timeout);\n attempt.detachCallerAbort?.();\n if (abort && !attempt.controller.signal.aborted) {\n attempt.controller.abort();\n }\n attempt.verifier = '';\n attempt.state = '';\n rememberClosedAttempt(attempt.id, reason);\n}\n\n/** 使超时尝试失效并取消进行中的令牌交换。 */\nfunction expireAttempt(attempt: PendingAttempt): void {\n closeAttempt(attempt, 'expired', true);\n}\n\n/** 将调用方取消传递给内部控制器,并返回解绑函数。 */\nfunction linkAbortSignal(signal: AbortSignal | undefined, controller: AbortController): () => void {\n if (!signal) {\n return () => undefined;\n }\n /** 将外部取消转发给当前令牌操作。 */\n const onAbort = () => controller.abort();\n if (signal.aborted) {\n onAbort();\n } else {\n signal.addEventListener('abort', onAbort, { once: true });\n }\n return () => signal.removeEventListener('abort', onAbort);\n}\n\n/** 根据已关闭原因区分过期、取消和回调重复使用。 */\nfunction errorForClosedAttempt(reason: ClosedAttemptReason): PiOAuthError {\n if (reason === 'expired') {\n return oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (reason === 'cancelled') {\n return cancellationError();\n }\n return oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n}\n\n/** 取得仍处于待回调阶段的有效尝试,拒绝过期、已消费或已取消的记录。 */\nfunction getPendingAttempt(attemptId: string, now: number): PendingAttempt {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n const closed = closedAttempts.get(attemptId);\n if (closed) {\n throw errorForClosedAttempt(closed.reason);\n }\n throw oauthError(\n 'attempt_used',\n 'No active More source OAuth login attempt matches this callback.'\n );\n }\n if (now >= attempt.expiresAt) {\n expireAttempt(attempt);\n throw oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (attempt.phase !== 'pending') {\n throw oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n }\n if (attempt.controller.signal.aborted) {\n closeAttempt(attempt, 'cancelled', false);\n throw cancellationError();\n }\n return attempt;\n}\n\n/** 通过地址与 state 校验后提取的授权码回调,不再携带完整 URL。 */\ntype ParsedCallback = { code: string; state: string };\n\n/** 补齐 URL 的默认端口,使回调地址校验不受显式端口写法影响。 */\nfunction normalizePort(url: URL): string {\n if (url.port) {\n return url.port;\n }\n return url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '';\n}\n\n/**\n * 校验完整回环回调地址的协议、主机、端口、路径和 state,再提取授权码。\n * 回调必须属于当前尝试;服务商返回的授权错误也在此转换为固定错误。\n */\nfunction parseAndValidateCallback(callbackUrl: string, attempt: PendingAttempt): ParsedCallback {\n let callback: URL;\n let redirect: URL;\n try {\n callback = new URL(callbackUrl.trim());\n redirect = new URL(attempt.metadata.redirectUri);\n } catch {\n throw oauthError(\n 'invalid_callback',\n 'Paste the complete loopback callback URL from the browser address bar.'\n );\n }\n\n const allowedHosts = new Set(\n attempt.metadata.allowedCallbackHosts.map(host => host.toLowerCase())\n );\n allowedHosts.add(redirect.hostname.toLowerCase());\n if (\n callback.protocol !== 'http:' ||\n callback.username !== '' ||\n callback.password !== '' ||\n callback.hash !== '' ||\n !allowedHosts.has(callback.hostname.toLowerCase()) ||\n normalizePort(callback) !== normalizePort(redirect) ||\n callback.pathname !== redirect.pathname\n ) {\n throw oauthError(\n 'invalid_callback',\n 'The More source OAuth callback does not match the registered loopback address.'\n );\n }\n\n const state = callback.searchParams.get('state') ?? '';\n if (!state || state !== attempt.state) {\n throw oauthError('state_mismatch', 'The More source OAuth callback state does not match.');\n }\n if (callback.searchParams.has('error')) {\n throw oauthError(\n 'authorization_failed',\n 'The OAuth provider did not authorize the More source login.'\n );\n }\n const code = callback.searchParams.get('code') ?? '';\n if (!code) {\n throw oauthError('invalid_callback', 'The More source OAuth callback is missing its code.');\n }\n return { code, state };\n}\n\n/** 综合信号和 OAuth 错误码识别取消,避免误报为网络或存储故障。 */\nfunction isCancellation(error: unknown, signal: AbortSignal): boolean {\n return signal.aborted || (error instanceof PiOAuthError && error.code === 'cancelled');\n}\n\n/** 区分主动取消与浏览器网络失败,不透传可能含敏感请求数据的底层错误。 */\nfunction normalizeFetchFailure(error: unknown, signal: AbortSignal): never {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n if (error instanceof TypeError) {\n throw oauthError(\n 'browser_network',\n 'The browser could not reach the More source OAuth token endpoint. Check network access and whether the provider allows browser CORS requests.'\n );\n }\n throw oauthError(\n 'browser_network',\n 'The More source OAuth token request failed in the browser.'\n );\n}\n\n/** 令牌交换的两种输入:首次登录使用授权码和 PKCE,续期使用刷新令牌。 */\ntype TokenGrant =\n | { type: 'authorization_code'; code: string; verifier: string; state: string }\n | { type: 'refresh_token'; refreshToken: string };\n\n/** 已校验并统一字段名的令牌响应,持久化前还需计算过期时间和补齐账号信息。 */\ntype TokenData = {\n access: string;\n refresh: string;\n /** 服务商返回的有效时长,单位为秒,并非绝对时间戳。 */\n expiresInSeconds: number;\n};\n\n/** 按授权码或刷新令牌两种 grant 生成令牌请求字段,并遵循服务商的 state 约定。 */\nfunction createTokenFields(metadata: OAuthMetadata, grant: TokenGrant): Record {\n const fields: Record = { ...metadata.tokenParams };\n fields.client_id = metadata.clientId;\n if (grant.type === 'authorization_code') {\n fields.grant_type = 'authorization_code';\n fields.code = grant.code;\n fields.code_verifier = grant.verifier;\n fields.redirect_uri = metadata.redirectUri;\n if (metadata.includeStateInTokenRequest) {\n fields.state = grant.state;\n }\n } else {\n fields.grant_type = 'refresh_token';\n fields.refresh_token = grant.refreshToken;\n }\n return fields;\n}\n\n/** 校验令牌接口状态和必需字段;刷新响应未返回 refresh_token 时保留原刷新令牌。 */\nasync function readTokenResponse(\n response: Response,\n fallbackRefreshToken?: string\n): Promise {\n if (!response.ok) {\n throw oauthError(\n 'token_http',\n `The More source OAuth token endpoint rejected the request (HTTP ${response.status}).`\n );\n }\n\n let body: unknown;\n try {\n body = JSON.parse(await response.text());\n } catch {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n if (!body || typeof body !== 'object') {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n const token = body as Record;\n const access = token.access_token;\n const refresh = token.refresh_token ?? fallbackRefreshToken;\n const expiresIn = token.expires_in;\n if (\n typeof access !== 'string' ||\n access.length === 0 ||\n typeof refresh !== 'string' ||\n refresh.length === 0 ||\n typeof expiresIn !== 'number' ||\n !Number.isFinite(expiresIn) ||\n expiresIn <= 0\n ) {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint response is missing required fields.'\n );\n }\n return { access, refresh, expiresInSeconds: expiresIn };\n}\n\n/** 按服务商要求用 JSON 或表单交换令牌,并在请求前后检查取消。 */\nasync function requestTokens(\n metadata: OAuthMetadata,\n grant: TokenGrant,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n const fields = createTokenFields(metadata, grant);\n const headers: Record = { Accept: 'application/json' };\n let body: string;\n if (metadata.exchangeKind === 'json') {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(fields);\n } else {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n body = new URLSearchParams(fields).toString();\n }\n\n let response: Response;\n try {\n response = await getFetch(options)(metadata.tokenUrl, {\n method: 'POST',\n headers,\n body,\n signal,\n });\n } catch (error) {\n normalizeFetchFailure(error, signal);\n }\n throwIfAborted(signal);\n return readTokenResponse(\n response!,\n grant.type === 'refresh_token' ? grant.refreshToken : undefined\n );\n}\n\n/** 解码 JWT 中的 Base64URL JSON 数据,兼容缺少 TextDecoder 的浏览器。 */\nfunction decodeBase64UrlJson(value: string): unknown {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const binary = globalThis.atob(padded);\n const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));\n const json =\n typeof globalThis.TextDecoder === 'function'\n ? new globalThis.TextDecoder().decode(bytes)\n : decodeURIComponent(\n [...bytes].map(byte => `%${byte.toString(16).padStart(2, '0')}`).join('')\n );\n return JSON.parse(json);\n}\n\n/** 从访问令牌的账号声明中读取 Codex 所需账号标识;缺失时拒绝建立凭证。 */\nfunction extractOpenAIAccountId(accessToken: string): string {\n try {\n const parts = accessToken.split('.');\n if (parts.length !== 3) {\n throw new Error('invalid JWT');\n }\n const payload = decodeBase64UrlJson(parts[1]);\n if (!payload || typeof payload !== 'object') {\n throw new Error('invalid JWT payload');\n }\n const authClaim = (payload as Record)[OPENAI_ACCOUNT_CLAIM];\n if (!authClaim || typeof authClaim !== 'object') {\n throw new Error('missing auth claim');\n }\n const accountId = (authClaim as Record).chatgpt_account_id;\n if (typeof accountId !== 'string' || accountId.length === 0) {\n throw new Error('missing account ID');\n }\n return accountId;\n } catch {\n throw oauthError(\n 'account_id',\n 'The OpenAI Codex OAuth token does not contain a ChatGPT account ID.'\n );\n }\n}\n\n/** 将令牌响应转换为 Pi OAuth 凭证,预留过期时间偏差,并补齐 Codex 账号标识。 */\nfunction credentialFromTokens(\n metadata: OAuthMetadata,\n token: TokenData,\n now: number\n): OAuthCredential {\n const credential: OAuthCredential = {\n type: 'oauth',\n access: token.access,\n refresh: token.refresh,\n expires: now + token.expiresInSeconds * 1000 - metadata.expirySkewMs,\n };\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n credential.accountId = extractOpenAIAccountId(token.access);\n }\n return credential;\n}\n\n/** 通过共享凭证队列保存登录结果,分别处理取消与存储失败。 */\nasync function persistCredential(\n providerId: string,\n credential: OAuthCredential,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n try {\n await getCredentialStore(options).modify(providerId, async () => credential, { signal });\n } catch (error) {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'More source OAuth login succeeded, but the credential could not be saved.'\n );\n }\n}\n\n/**\n * 创建带有效期的浏览器登录尝试,生成 PKCE 和 state,返回供界面打开的授权地址。\n * 秘密校验信息留在内存中,调用方取消或超时会关闭该尝试。\n */\nexport async function beginPiOAuth(\n providerId: string,\n options: BeginPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const metadata = getOAuthMetadata(providerId);\n const ttl = options.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new RangeError('More source OAuth attemptTtlMs must be a positive number.');\n }\n const cryptoImpl = getCrypto(options);\n const credentialStore =\n options.credentialStore ??\n createPiOAuthLoginStore(providerId, randomBase64Url(cryptoImpl, 18));\n const { verifier, challenge } = await createPkce(cryptoImpl);\n throwIfAborted(options.signal);\n\n const state = randomBase64Url(cryptoImpl, 32);\n const id = randomBase64Url(cryptoImpl, 18);\n const expiresAt = getNow(options)() + ttl;\n const controller = new AbortController();\n const attempt: PendingAttempt = {\n id,\n providerId,\n metadata,\n credentialStore,\n verifier,\n state,\n expiresAt,\n phase: 'pending' as const,\n controller,\n timeout: undefined as unknown as ReturnType,\n };\n attempt.timeout = setTimeout(() => expireAttempt(attempt), ttl);\n pendingAttempts.set(id, attempt);\n if (options.signal) {\n /** 关闭已取消的登录尝试,阻止随后到达的回调继续交换令牌。 */\n const onAbort = () => closeAttempt(attempt, 'cancelled', true);\n options.signal.addEventListener('abort', onAbort, { once: true });\n attempt.detachCallerAbort = () => options.signal?.removeEventListener('abort', onAbort);\n if (options.signal.aborted) {\n onAbort();\n throw cancellationError();\n }\n }\n\n return {\n id,\n providerId,\n authorizationUrl: createAuthorizationUrl(metadata, challenge, state),\n expiresAt,\n };\n}\n\n/**\n * 校验并一次性消费回调,交换授权码后保存凭证,最终清理尝试状态。\n * 重复回调或交换期间取消都不能产生第二次有效登录。\n */\nexport async function completePiOAuth(\n attemptId: string,\n callbackUrl: string,\n options: CompletePiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const now = getNow(options);\n const attempt = getPendingAttempt(attemptId, now());\n let parsed: ParsedCallback;\n try {\n parsed = parseAndValidateCallback(callbackUrl, attempt);\n } catch (error) {\n if (error instanceof PiOAuthError && error.code === 'authorization_failed') {\n closeAttempt(attempt, 'used', true);\n }\n throw error;\n }\n attempt.phase = 'exchanging';\n const detachCompleteAbort = linkAbortSignal(options.signal, attempt.controller);\n\n try {\n throwIfAborted(attempt.controller.signal);\n const token = await requestTokens(\n attempt.metadata,\n {\n type: 'authorization_code',\n code: parsed.code,\n verifier: attempt.verifier,\n state: parsed.state,\n },\n attempt.controller.signal,\n options\n );\n const credential = credentialFromTokens(attempt.metadata, token, now());\n await persistCredential(attempt.providerId, credential, attempt.controller.signal, {\n credentialStore: options.credentialStore ?? attempt.credentialStore,\n });\n closeAttempt(attempt, 'used', false);\n return credential;\n } catch (error) {\n const cancelled = isCancellation(error, attempt.controller.signal);\n closeAttempt(attempt, cancelled ? 'cancelled' : 'used', true);\n if (cancelled) {\n throw cancellationError();\n }\n throw error;\n } finally {\n detachCompleteAbort();\n }\n}\n\n/** 按尝试编号取消登录,清除回调校验状态并中止未完成的交换。 */\nexport function cancelPiOAuth(attemptId: string): boolean {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n return false;\n }\n closeAttempt(attempt, 'cancelled', true);\n return true;\n}\n\n/** 取消全部待完成的登录尝试,供界面卸载和脚本清理使用。 */\nexport function cancelAllPiOAuth(): void {\n for (const attempt of [...pendingAttempts.values()]) {\n closeAttempt(attempt, 'cancelled', true);\n }\n}\n\n/** 解除捕获方案的登录绑定;其他方案仍引用的集中凭证会继续保留。 */\nexport async function logoutPiOAuth(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n try {\n const store = options.credentialStore ?? createPiOAuthLogoutStore();\n await store.delete(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential could not be removed.'\n );\n }\n}\n\n/** 返回是否已登录及过期时间等界面状态,不暴露访问令牌和刷新令牌。 */\nexport async function getPiOAuthCredentialStatus(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n let credential;\n try {\n credential = await getCredentialStore(options).read(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential status could not be read.'\n );\n }\n if (!credential || credential.type !== 'oauth') {\n return { loggedIn: false };\n }\n return {\n loggedIn: true,\n type: 'oauth',\n expiresAt: credential.expires,\n };\n}\n\n/**\n * 由界面强制刷新现有 OAuth 凭证,与 Pi 自动刷新共用同一服务商修改锁。\n * 操作使用捕获的凭证仓库和取消信号,刷新完成后再提交新凭证。\n */\nexport async function refreshPiOAuth(\n providerId: string,\n options: RefreshPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const auth = getBrowserOAuthAuth(providerId, options);\n const store = getCredentialStore(options);\n installPiAbortSignalPolyfills();\n const signal = AbortSignal.any([\n ...(options.signal ? [options.signal] : []),\n AbortSignal.timeout(REFRESH_TIMEOUT_MS),\n ]);\n /** 生成缺少可刷新 OAuth 凭证的错误,供刷新入口统一报告。 */\n const missingCredential = () =>\n oauthError('missing_credential', 'Sign in before refreshing OAuth credentials.');\n try {\n const observed = await store.read(providerId, { signal });\n if (observed?.type !== 'oauth') {\n throw missingCredential();\n }\n const refreshed = await store.modify(\n providerId,\n async current => {\n throwIfAborted(signal);\n if (current?.type !== 'oauth') {\n throw missingCredential();\n }\n // An automatic refresh or another login may have completed while we queued.\n // Keep that result instead of reusing or rotating its token a second time.\n if (\n current.access !== observed.access ||\n current.refresh !== observed.refresh ||\n current.expires !== observed.expires\n ) {\n return undefined;\n }\n return auth.refresh(current, signal);\n },\n { signal }\n );\n if (refreshed?.type !== 'oauth') {\n throw missingCredential();\n }\n return { loggedIn: true, type: 'oauth', expiresAt: refreshed.expires };\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n if (signal.aborted) {\n throw oauthError('browser_network', 'The OAuth credential refresh timed out.');\n }\n if (error instanceof PiOAuthError) {\n throw error;\n }\n throw oauthError('credential_store', 'The refreshed OAuth credential could not be saved.');\n }\n}\n\n/**\n * 将浏览器授权、刷新和凭证转换接入 Pi 的 OAuthAuth 契约。\n * 登录通过授权地址与手动回调交互完成,不启动 Node 回环服务器。\n */\nfunction createBrowserOAuthAuth(\n metadata: OAuthMetadata,\n options: BrowserOAuthAuthOptions\n): OAuthAuth {\n const providerName =\n metadata.providerId === ANTHROPIC_PROVIDER_ID\n ? 'Anthropic (Claude Pro/Max)'\n : 'OpenAI (ChatGPT Plus/Pro)';\n return {\n name: providerName,\n isSubscription: true,\n /** 通过 Pi 认证交互展示授权地址、接收回调,并在结束时释放登录尝试。 */\n async login(interaction) {\n const attempt = await beginPiOAuth(metadata.providerId, {\n ...options,\n signal: interaction.signal,\n });\n interaction.notify({\n type: 'auth_url',\n url: attempt.authorizationUrl,\n instructions: 'Complete login, then paste the complete loopback callback URL here.',\n });\n let callbackUrl: string;\n try {\n callbackUrl = await interaction.prompt({\n type: 'manual_code',\n message: 'Paste the complete loopback callback URL:',\n placeholder: metadata.redirectUri.replace('localhost', '127.0.0.1'),\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n if (interaction.signal.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'invalid_callback',\n 'More source OAuth callback input was cancelled.'\n );\n }\n try {\n return await completePiOAuth(attempt.id, callbackUrl, {\n ...options,\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n throw error;\n }\n },\n /** 为 Pi 自动续期交换 refresh_token,保留请求的取消信号并更新过期时间。 */\n async refresh(credential, signal) {\n const token = await requestTokens(\n metadata,\n { type: 'refresh_token', refreshToken: credential.refresh },\n signal,\n options\n );\n return credentialFromTokens(metadata, token, getNow(options)());\n },\n /** 把持久化 OAuth 凭证转换为 Pi 请求认证信息,保留服务商需要的账号字段。 */\n async toAuth(credential): Promise {\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n extractOpenAIAccountId(credential.access);\n }\n return { apiKey: credential.access };\n },\n };\n}\n\n/** 按服务商解析浏览器 OAuth 实现,并传入可替换的请求和时钟依赖。 */\nexport function getBrowserOAuthAuth(\n providerId: string,\n options: BrowserOAuthAuthOptions = {}\n): OAuthAuth {\n return createBrowserOAuthAuth(getOAuthMetadata(providerId), options);\n}\n","import { uuidv4 } from '@util/common';\n\nexport const PI_PROMPT_CAPTURE_MODEL_PREFIX = 'mvu-pi-prompt-capture:';\nexport const PI_PROMPT_CAPTURE_API_URL =\n 'https://mvu-pi-prompt-capture.invalid/v1/chat/completions';\n\n/** 一次提示词捕获的诊断快照;只含状态和标记,不暴露完整提示词。 */\nexport type PromptCaptureDiagnostics = {\n generationId: string;\n /** 写入占位请求 model 字段的唯一标记,用于隔离并发生成事件。 */\n marker: string;\n markerMatched: boolean;\n captured: boolean;\n /** 占位生成是否已被成功停止,避免捕获后继续请求酒馆后端。 */\n stopSucceeded: boolean;\n /** 首次失败原因;清理阶段不会覆盖它。 */\n captureError?: string;\n};\n\n/** 从酒馆设置就绪事件复制出的最终消息及其所属生成编号。 */\nexport type CapturedPrompt = {\n generationId: string;\n messages: SillyTavern.SendingMessage[];\n};\n\n/** 捕获完成后的消息、诊断信息和接管回调的返回值。 */\nexport type PromptCaptureResult = PromptCaptureDiagnostics &\n CapturedPrompt & {\n result?: T;\n };\n\n/** 接管捕获结果并接收原生停止事件的回调,使 Pi 请求沿用酒馆的生成生命周期。 */\nexport type PromptCaptureOptions = {\n /** 在提示词就绪监听内等待此回调完成,期间保留酒馆原生停止按钮。 */\n onCaptured: (prompt: CapturedPrompt) => Promise;\n /** 仅转发用户停止当前生成的事件,不包含内部占位请求清理。 */\n onStopped: (generation_id: string) => void;\n};\n\n/** 进行中捕获的内部可变状态,messages 仅在成功复制提示词后存在。 */\ntype MutablePromptCaptureState = PromptCaptureDiagnostics & {\n messages?: SillyTavern.SendingMessage[];\n};\n\n/** 酒馆设置就绪事件中本适配层需要读取的字段,其余请求参数保持透明。 */\ntype SettingsReadyData = {\n messages: SillyTavern.SendingMessage[];\n model: string;\n [key: string]: unknown;\n};\n\n/** generate / generateRaw 的共同调用形状;捕获只依赖事件,不使用它们的返回结果。 */\ntype GenerateRunner = (config: TConfig) => Promise;\n\nconst pending_prompt_captures = new Map();\n\n/** 将非空生成编号编码为专用模型标记,用于从设置就绪事件中识别本次捕获。 */\nexport function encodePromptCaptureMarker(generation_id: string): string {\n if (generation_id.length === 0) {\n throw new Error('Prompt capture generation_id cannot be empty');\n }\n\n try {\n return `${PI_PROMPT_CAPTURE_MODEL_PREFIX}${encodeURIComponent(generation_id)}`;\n } catch (error) {\n throw new Error(`Prompt capture generation_id cannot be encoded: ${generation_id}`, {\n cause: error,\n });\n }\n}\n\n/** 只接受可规范往返编码的捕获标记,普通模型名或畸形标记返回 null。 */\nexport function decodePromptCaptureMarker(model: unknown): string | null {\n if (typeof model !== 'string' || !model.startsWith(PI_PROMPT_CAPTURE_MODEL_PREFIX)) {\n return null;\n }\n\n const encoded_generation_id = model.slice(PI_PROMPT_CAPTURE_MODEL_PREFIX.length);\n if (encoded_generation_id.length === 0) {\n return null;\n }\n\n try {\n const generation_id = decodeURIComponent(encoded_generation_id);\n if (\n generation_id.length === 0 ||\n encodeURIComponent(generation_id) !== encoded_generation_id\n ) {\n return null;\n }\n return generation_id;\n } catch {\n return null;\n }\n}\n\n/**\n * 将生成配置转换为专用捕获请求,移除业务工具、Schema、凭证和自定义覆盖。\n * 请求指向固定无效地址,并应在最终提示词捕获后被停止,避免误发到主模型。\n */\nexport function buildPromptCaptureConfig(\n config: TConfig,\n generation_id: string\n): TConfig {\n const marker = encodePromptCaptureMarker(generation_id);\n\n return {\n ...config,\n generation_id,\n should_stream: false,\n should_silence: config.should_silence ?? true,\n tools: [],\n tool_choice: 'none',\n json_schema: undefined,\n custom_api: {\n source: 'custom',\n apiurl: PI_PROMPT_CAPTURE_API_URL,\n key: '',\n model: marker,\n custom_include_body: {},\n custom_exclude_body: [],\n custom_include_headers: {},\n },\n } as TConfig;\n}\n\n/** 返回进行中捕获的诊断快照,不暴露完整提示词内容。 */\nexport function getPendingPromptCaptureDiagnostics(): PromptCaptureDiagnostics[] {\n return Array.from(pending_prompt_captures.values(), state => ({\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n ...(state.captureError === undefined ? {} : { captureError: state.captureError }),\n }));\n}\n\n/** 只有标记匹配、提示词复制成功且生成已停止,才认定捕获完整成功。 */\nfunction hasCompletedCapture(state: MutablePromptCaptureState): boolean {\n return state.markerMatched && state.captured && state.stopSucceeded;\n}\n\n/** 只记录首次捕获失败原因,避免清理阶段覆盖最有用的错误。 */\nfunction recordCaptureError(state: MutablePromptCaptureState, message: string): void {\n state.captureError ??= message;\n}\n\n/**\n * 在酒馆设置就绪事件的最后阶段复制最终提示词,并以生成编号隔离并发捕获。\n * 可等待 Pi 回调结束以维持原生停止按钮;随后停止占位请求,最终释放全部监听和登记。\n */\nexport async function capturePrompt(\n run: GenerateRunner,\n config: TConfig,\n options?: PromptCaptureOptions\n): Promise> {\n const generation_id = config.generation_id || uuidv4();\n const marker = encodePromptCaptureMarker(generation_id);\n\n if (pending_prompt_captures.has(generation_id)) {\n throw new Error(`Prompt capture generation_id is already pending: ${generation_id}`);\n }\n\n const state: MutablePromptCaptureState = {\n generationId: generation_id,\n marker,\n markerMatched: false,\n captured: false,\n stopSucceeded: false,\n };\n const capture_config = buildPromptCaptureConfig(config, generation_id);\n let captured_result: T | undefined;\n let callback_failed = false;\n let callback_error: unknown;\n let callback_started = false;\n let listening_for_stop = options !== undefined;\n /** 仅将当前生成编号的用户停止事件传给 Pi 回调,不影响其他并发请求。 */\n const stop_listener = (stopped_id?: string) => {\n if (listening_for_stop && stopped_id === generation_id) {\n options?.onStopped(generation_id);\n }\n };\n /** 关闭并移除停止监听,防止内部终止占位请求被误判为用户取消。 */\n const remove_stop_listener = () => {\n listening_for_stop = false;\n if (options) {\n eventRemoveListener(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n };\n\n /** 匹配当前捕获标记后复制最终提示词,等待接管回调,再阻止占位请求进入酒馆后端。 */\n const listener = async (generate_data: SettingsReadyData) => {\n const event_generation_id = decodePromptCaptureMarker(generate_data?.model);\n if (\n event_generation_id !== generation_id ||\n pending_prompt_captures.get(event_generation_id) !== state\n ) {\n return;\n }\n\n state.markerMatched = true;\n if (state.captureError !== undefined || state.captured) {\n return;\n }\n if (!Array.isArray(generate_data.messages)) {\n recordCaptureError(\n state,\n `Prompt capture event has no messages array: ${generation_id}`\n );\n return;\n }\n\n try {\n state.messages = structuredClone(generate_data.messages);\n } catch {\n recordCaptureError(state, `Failed to clone prompt capture messages: ${generation_id}`);\n return;\n }\n state.captured = true;\n if (options) {\n callback_started = true;\n try {\n // Settings-ready listeners are awaited by Slash. Keep generation (and its\n // native Stop button) active until the provider request has settled.\n captured_result = await options.onCaptured({\n generationId: generation_id,\n messages: state.messages,\n });\n } catch (error) {\n callback_failed = true;\n callback_error = error;\n }\n }\n // This stop only prevents the fixed capture request from reaching ST's backend.\n // It must not be mistaken for a user cancellation of the successful Pi request.\n remove_stop_listener();\n try {\n state.stopSucceeded = stopGenerationById(generation_id);\n } catch {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n return;\n }\n if (!state.stopSucceeded) {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n }\n };\n\n let subscription: { stop: () => void } | undefined;\n pending_prompt_captures.set(generation_id, state);\n\n try {\n if (options) {\n eventOn(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n subscription = eventMakeLast(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n\n let runner_failed = false;\n let runner_error: unknown;\n try {\n await run(capture_config);\n } catch (error) {\n runner_failed = true;\n runner_error = error;\n }\n\n if (state.captureError !== undefined) {\n throw new Error(state.captureError);\n }\n if (callback_failed) {\n throw callback_error;\n }\n if (runner_failed && !hasCompletedCapture(state)) {\n throw runner_error;\n }\n\n if (!hasCompletedCapture(state) || state.messages === undefined) {\n throw new Error(`Generation ended without a captured prompt: ${generation_id}`);\n }\n\n return {\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n messages: state.messages,\n ...(callback_started ? { result: captured_result } : {}),\n };\n } finally {\n remove_stop_listener();\n try {\n subscription?.stop();\n } finally {\n try {\n // Slash-Runner 4.9.1's EventOnReturn.stop() cannot reliably remove\n // its wrapped listener, so always remove the original listener too.\n eventRemoveListener(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n } finally {\n if (pending_prompt_captures.get(generation_id) === state) {\n pending_prompt_captures.delete(generation_id);\n }\n }\n }\n }\n}\n\n/** 使用当前预设的 generate 流程构造并捕获最终提示词。 */\nexport function captureGeneratePrompt(\n config: GenerateConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generate, config, options);\n}\n\n/** 使用显式顺序的 generateRaw 流程构造并捕获最终提示词。 */\nexport function captureGenerateRawPrompt(\n config: GenerateRawConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generateRaw, config, options);\n}\n","import { resolvePiThinkingLevel } from './thinking';\nimport { isPiThinkingEnabled } from './thinking_setting';\nimport {\n ANT_LING_MODELS,\n ANTHROPIC_MODELS,\n BASETEN_MODELS,\n CEREBRAS_MODELS,\n DEEPSEEK_MODELS,\n FIREWORKS_MODELS,\n GITHUB_COPILOT_MODELS,\n GOOGLE_MODELS,\n GROQ_MODELS,\n HUGGINGFACE_MODELS,\n KIMI_CODING_MODELS,\n MINIMAX_CN_MODELS,\n MINIMAX_MODELS,\n MISTRAL_MODELS,\n MOONSHOTAI_CN_MODELS,\n MOONSHOTAI_MODELS,\n NVIDIA_MODELS,\n OPENAI_CODEX_MODELS,\n OPENAI_MODELS,\n OPENCODE_GO_MODELS,\n OPENCODE_MODELS,\n OPENROUTER_MODELS,\n QWEN_TOKEN_PLAN_CN_MODELS,\n QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n QWEN_TOKEN_PLAN_MODELS,\n TOGETHER_MODELS,\n VERCEL_AI_GATEWAY_MODELS,\n XAI_MODELS,\n XIAOMI_MODELS,\n XIAOMI_TOKEN_PLAN_AMS_MODELS,\n XIAOMI_TOKEN_PLAN_CN_MODELS,\n XIAOMI_TOKEN_PLAN_SGP_MODELS,\n ZAI_CODING_CN_MODELS,\n ZAI_MODELS,\n anthropicMessagesApi,\n googleGenerativeAIApi,\n mistralConversationsApi,\n openAICodexResponsesApi,\n openAICompletionsApi,\n openAIResponsesApi,\n type Api,\n type Model,\n type ProviderStreams,\n} from './pi_gateway';\nimport { createGoogleProxyAwareApi } from './google_proxy_adapter';\nimport {\n getPiProviderApiBaseUrl,\n PI_PROVIDER_KEYS,\n PI_PROVIDER_TARGET_REGISTRY,\n type PiAuthType,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_target';\n\nexport {\n isPiCorsProxyRequired,\n PI_AUTH_TYPES,\n PI_PROVIDER_KEYS,\n PI_WIRE_APIS,\n shouldUsePiCorsProxy,\n} from './provider_target';\nexport type { PiAuthType, PiProviderKey, PiWireApi } from './provider_target';\n\n/**\n * Codex protocol level implemented by the pinned pi-ai adapter/catalog. Keep this tied to an\n * adapter upgrade rather than MVU's unrelated application version: the models endpoint uses it to\n * hide entries whose request shape the active adapter cannot yet support.\n */\nexport const OPENAI_CODEX_ADAPTER_CLIENT_VERSION = '0.144.0';\n\n/** 来源面板单个配置字段的呈现与编辑方式。 */\nexport type PiFieldMode = 'hidden' | 'readonly' | 'select' | 'editable';\n\n/** 服务商对应的面板字段规则,认证相关字段按所选认证方式显示。 */\nexport interface PiProviderFieldVisibility {\n api: PiFieldMode;\n authType: PiFieldMode;\n endpoint: PiFieldMode;\n apiKey: 'hidden' | 'when-api-key';\n oauth: 'hidden' | 'when-oauth';\n model: 'editable';\n contextWindow: 'editable';\n}\n\n/** 协议与模型组合可用的请求能力,供面板显隐和发送前参数校验共用。 */\nexport interface PiApiCapabilities {\n /** 是否支持 MVU 当前采用的 Pi 流式执行链路。 */\n streaming: boolean;\n tools: boolean;\n imageInput: boolean;\n /** 是否支持服务商原生 JSON Schema 输出约束。 */\n structuredOutput: boolean;\n /** 是否支持不指定 Schema 的原生 JSON 对象输出。 */\n jsonObjectOutput: boolean;\n temperature: boolean;\n /** temperature 可配置的闭区间,仅在支持该参数时生效。 */\n temperatureRange: readonly [minimum: number, maximum: number];\n /** 可直接传给当前协议的采样参数;未支持的配置在预检时拒绝。 */\n sampling: Readonly<{\n topP: boolean;\n topK: boolean;\n frequencyPenalty: boolean;\n presencePenalty: boolean;\n }>;\n}\n\n/** 令牌端点要求的请求正文编码:JSON 或 URL 编码表单。 */\nexport type PiOAuthExchangeKind = 'json' | 'form';\n\n/**\n * 来源面板与浏览器 OAuth 流程共享的公共客户端配置。\n * 每次登录独有的 PKCE verifier 和 state 保存在尝试状态中,不属于注册信息。\n */\nexport interface PiOAuthDefinition {\n providerId: PiProviderKey;\n api: PiWireApi;\n clientId: string;\n authorizeUrl: string;\n tokenUrl: string;\n redirectUri: string;\n /** 粘贴回调允许使用的回环主机名,端口和路径仍需与 redirectUri 匹配。 */\n allowedCallbackHosts: readonly ('localhost' | '127.0.0.1')[];\n scope: string;\n exchangeKind: PiOAuthExchangeKind;\n authorizeParams: Readonly>;\n tokenParams: Readonly>;\n /** 部分服务商要求交换授权码时同时提交本次 state。 */\n includeStateInTokenRequest: boolean;\n /** 持久化时从服务商有效期扣除的毫秒数,使刷新提前于实际过期。 */\n expirySkewMs: number;\n}\n\n/** 完整服务商注册信息,在轻量连接规则上加入能力表、界面配置和 OAuth 元数据。 */\nexport interface PiProviderDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n displayName: Readonly<{\n 'zh-CN': string;\n en: string;\n }>;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须经酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n /** 按协议登记的能力基线,查询具体模型时还会应用模型限制。 */\n apiCapabilities: Readonly>>>;\n fields: Readonly;\n oauth?: Readonly;\n}\n\n/** 按模型目录键索引的只读 Pi 元数据,不可直接修改为本次请求的配置。 */\ntype Catalog = Readonly>>;\n/** 创建协议流适配器的工厂,供 Provider 构建时选择实际协议实现。 */\ntype ApiLoader = () => ProviderStreams;\n\nconst CATALOGS: Readonly> = {\n 'ant-ling': ANT_LING_MODELS,\n anthropic: ANTHROPIC_MODELS,\n baseten: BASETEN_MODELS,\n cerebras: CEREBRAS_MODELS,\n deepseek: DEEPSEEK_MODELS,\n fireworks: FIREWORKS_MODELS,\n 'github-copilot': GITHUB_COPILOT_MODELS,\n google: GOOGLE_MODELS,\n groq: GROQ_MODELS,\n huggingface: HUGGINGFACE_MODELS,\n 'kimi-coding': KIMI_CODING_MODELS,\n minimax: MINIMAX_MODELS,\n 'minimax-cn': MINIMAX_CN_MODELS,\n mistral: MISTRAL_MODELS,\n moonshotai: MOONSHOTAI_MODELS,\n 'moonshotai-cn': MOONSHOTAI_CN_MODELS,\n nvidia: NVIDIA_MODELS,\n openai: OPENAI_MODELS,\n 'openai-codex': OPENAI_CODEX_MODELS,\n opencode: OPENCODE_MODELS,\n 'opencode-go': OPENCODE_GO_MODELS,\n openrouter: OPENROUTER_MODELS,\n 'qwen-token-plan': QWEN_TOKEN_PLAN_MODELS,\n 'qwen-token-plan-cn': QWEN_TOKEN_PLAN_CN_MODELS,\n 'qwen-token-plan-individual': QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n together: TOGETHER_MODELS,\n 'vercel-ai-gateway': VERCEL_AI_GATEWAY_MODELS,\n xai: XAI_MODELS,\n xiaomi: XIAOMI_MODELS,\n 'xiaomi-token-plan-ams': XIAOMI_TOKEN_PLAN_AMS_MODELS,\n 'xiaomi-token-plan-cn': XIAOMI_TOKEN_PLAN_CN_MODELS,\n 'xiaomi-token-plan-sgp': XIAOMI_TOKEN_PLAN_SGP_MODELS,\n zai: ZAI_MODELS,\n 'zai-coding-cn': ZAI_CODING_CN_MODELS,\n};\n\n/** Provider catalog entries that are known to have been shut down upstream. */\nconst GOOGLE_RETIRED_MODEL_IDS: ReadonlySet = new Set(['gemini-3.1-flash-lite-preview']);\n\nconst API_LOADERS: Readonly> = {\n 'openai-responses': openAIResponsesApi,\n 'openai-completions': openAICompletionsApi,\n 'openai-codex-responses': openAICodexResponsesApi,\n 'anthropic-messages': anthropicMessagesApi,\n 'google-generative-ai': () => createGoogleProxyAwareApi(googleGenerativeAIApi()),\n 'mistral-conversations': mistralConversationsApi,\n};\n\nconst TOP_P_ONLY = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TOP_P_TOP_K = Object.freeze({\n topP: true,\n topK: true,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst OPENAI_COMPLETIONS_SAMPLING = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: true,\n presencePenalty: true,\n});\n\nconst NO_SAMPLING = Object.freeze({\n topP: false,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TEMPERATURE_ZERO_TO_TWO = Object.freeze([0, 2] as const);\nconst TEMPERATURE_ZERO_TO_ONE = Object.freeze([0, 1] as const);\n\nconst OPENAI_RESPONSES_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_ONLY,\n});\n\nconst OPENAI_COMPLETIONS_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst CODEX_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: false,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: NO_SAMPLING,\n});\n\nconst ANTHROPIC_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: false,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: TOP_P_TOP_K,\n});\n\nconst GOOGLE_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_TOP_K,\n});\n\nconst MISTRAL_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst API_CAPABILITIES: Readonly>> = Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n 'openai-codex-responses': CODEX_CAPABILITIES,\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n 'mistral-conversations': MISTRAL_CAPABILITIES,\n});\n\nconst ANTHROPIC_OAUTH: Readonly = Object.freeze({\n providerId: 'anthropic',\n api: 'anthropic-messages',\n clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',\n authorizeUrl: 'https://claude.ai/oauth/authorize',\n tokenUrl: 'https://platform.claude.com/v1/oauth/token',\n redirectUri: 'http://localhost:53692/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',\n exchangeKind: 'json',\n authorizeParams: Object.freeze({ code: 'true' }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: true,\n expirySkewMs: 5 * 60 * 1000,\n});\n\nconst OPENAI_CODEX_OAUTH: Readonly = Object.freeze({\n providerId: 'openai-codex',\n api: 'openai-codex-responses',\n clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',\n authorizeUrl: 'https://auth.openai.com/oauth/authorize',\n tokenUrl: 'https://auth.openai.com/oauth/token',\n redirectUri: 'http://localhost:1455/auth/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'openid profile email offline_access',\n exchangeKind: 'form',\n authorizeParams: Object.freeze({\n id_token_add_organizations: 'true',\n codex_cli_simplified_flow: 'true',\n originator: 'pi',\n }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: false,\n expirySkewMs: 0,\n});\n\nconst EDITABLE_OPENAI_FIELDS: Readonly = Object.freeze({\n api: 'select',\n authType: 'readonly',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst OAUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'hidden',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MIXED_AUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'select',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst API_KEY_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MULTI_API_KEY_FIELDS: Readonly = Object.freeze({\n ...API_KEY_FIELDS,\n api: 'select',\n});\n\n/** 从轻量目标定义构造固定 API Key 服务商,补充展示信息、目录和协议能力。 */\nfunction fixedApiKeyProvider(\n key: PiProviderKey,\n name: string,\n chineseName = name\n): Readonly {\n const target = PI_PROVIDER_TARGET_REGISTRY[key];\n const apiCapabilities = Object.freeze(\n Object.fromEntries(target.allowedApis.map(api => [api, API_CAPABILITIES[api]]))\n ) as Readonly>>>;\n return Object.freeze({\n ...target,\n displayName: Object.freeze({ 'zh-CN': chineseName, en: name }),\n apiCapabilities,\n fields: target.allowedApis.length > 1 ? MULTI_API_KEY_FIELDS : API_KEY_FIELDS,\n });\n}\n\nexport const PI_PROVIDER_REGISTRY: Readonly> =\n Object.freeze({\n 'ant-ling': fixedApiKeyProvider('ant-ling', 'Ant Ling'),\n anthropic: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.anthropic,\n displayName: Object.freeze({ 'zh-CN': 'Anthropic', en: 'Anthropic' }),\n apiCapabilities: Object.freeze({\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n }),\n fields: MIXED_AUTH_FIELDS,\n oauth: ANTHROPIC_OAUTH,\n }),\n baseten: fixedApiKeyProvider('baseten', 'Baseten'),\n cerebras: fixedApiKeyProvider('cerebras', 'Cerebras'),\n deepseek: fixedApiKeyProvider('deepseek', 'DeepSeek'),\n fireworks: fixedApiKeyProvider('fireworks', 'Fireworks'),\n 'github-copilot': fixedApiKeyProvider('github-copilot', 'GitHub Copilot'),\n google: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.google,\n displayName: Object.freeze({ 'zh-CN': 'Google Gemini', en: 'Google Gemini' }),\n apiCapabilities: Object.freeze({\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n }),\n fields: API_KEY_FIELDS,\n }),\n groq: fixedApiKeyProvider('groq', 'Groq'),\n huggingface: fixedApiKeyProvider('huggingface', 'Hugging Face'),\n 'kimi-coding': fixedApiKeyProvider('kimi-coding', 'Kimi For Coding'),\n minimax: fixedApiKeyProvider('minimax', 'MiniMax'),\n 'minimax-cn': fixedApiKeyProvider('minimax-cn', 'MiniMax CN', 'MiniMax(中国)'),\n mistral: fixedApiKeyProvider('mistral', 'Mistral'),\n moonshotai: fixedApiKeyProvider('moonshotai', 'Moonshot AI'),\n 'moonshotai-cn': fixedApiKeyProvider(\n 'moonshotai-cn',\n 'Moonshot AI CN',\n 'Moonshot AI(中国)'\n ),\n nvidia: fixedApiKeyProvider('nvidia', 'NVIDIA'),\n openai: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.openai,\n displayName: Object.freeze({ 'zh-CN': 'OpenAI', en: 'OpenAI' }),\n apiCapabilities: Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n }),\n fields: EDITABLE_OPENAI_FIELDS,\n }),\n 'openai-codex': Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY['openai-codex'],\n displayName: Object.freeze({ 'zh-CN': 'OpenAI Codex', en: 'OpenAI Codex' }),\n apiCapabilities: Object.freeze({\n 'openai-codex-responses': CODEX_CAPABILITIES,\n }),\n fields: OAUTH_FIELDS,\n oauth: OPENAI_CODEX_OAUTH,\n }),\n opencode: fixedApiKeyProvider('opencode', 'OpenCode Zen'),\n 'opencode-go': fixedApiKeyProvider('opencode-go', 'OpenCode Go'),\n openrouter: fixedApiKeyProvider('openrouter', 'OpenRouter'),\n 'qwen-token-plan': fixedApiKeyProvider('qwen-token-plan', 'Qwen Token Plan'),\n 'qwen-token-plan-cn': fixedApiKeyProvider(\n 'qwen-token-plan-cn',\n 'Qwen Token Plan CN',\n 'Qwen Token Plan(中国)'\n ),\n 'qwen-token-plan-individual': fixedApiKeyProvider(\n 'qwen-token-plan-individual',\n 'Qwen Token Plan Individual'\n ),\n together: fixedApiKeyProvider('together', 'Together'),\n 'vercel-ai-gateway': fixedApiKeyProvider('vercel-ai-gateway', 'Vercel AI Gateway'),\n xai: fixedApiKeyProvider('xai', 'xAI'),\n xiaomi: fixedApiKeyProvider('xiaomi', 'Xiaomi'),\n 'xiaomi-token-plan-ams': fixedApiKeyProvider(\n 'xiaomi-token-plan-ams',\n 'Xiaomi Token Plan AMS'\n ),\n 'xiaomi-token-plan-cn': fixedApiKeyProvider(\n 'xiaomi-token-plan-cn',\n 'Xiaomi Token Plan CN',\n 'Xiaomi Token Plan(中国)'\n ),\n 'xiaomi-token-plan-sgp': fixedApiKeyProvider(\n 'xiaomi-token-plan-sgp',\n 'Xiaomi Token Plan SGP'\n ),\n zai: fixedApiKeyProvider('zai', 'Z.AI'),\n 'zai-coding-cn': fixedApiKeyProvider(\n 'zai-coding-cn',\n 'Z.AI Coding CN',\n 'Z.AI Coding(中国)'\n ),\n } satisfies Record);\n\n/** 按注册顺序返回支持的服务商定义,供来源界面生成选项。 */\nexport function listPiProviderDefinitions(): readonly PiProviderDefinition[] {\n return PI_PROVIDER_KEYS.map(key => PI_PROVIDER_REGISTRY[key]);\n}\n\n/** 按注册键读取服务商定义,拒绝命中对象原型上的同名属性。 */\nexport function getPiProviderDefinition(key: string): PiProviderDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_REGISTRY, key)\n ? PI_PROVIDER_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** Compatibility spelling used by the OAuth/UI integration. */\nexport const getPiProviderRegistration = getPiProviderDefinition;\n\n/** 取得服务商的固定模型目录,未知服务商返回空列表。 */\nexport function getPiCatalogModels(key: PiProviderKey | string): readonly Model[] {\n const definition = getPiProviderDefinition(key);\n if (!definition) {\n return [];\n }\n const models = Object.values(CATALOGS[definition.key]);\n return definition.key === 'google'\n ? models.filter(model => !GOOGLE_RETIRED_MODEL_IDS.has(model.id))\n : models;\n}\n\n/** 综合模型元数据和已知模型限制判断是否可发送 temperature。 */\nfunction modelSupportsTemperature(model: Model | undefined): boolean {\n const compat = model?.compat;\n return (\n compat === undefined ||\n !('supportsTemperature' in compat) ||\n compat.supportsTemperature !== false\n );\n}\n\n/**\n * Anthropic rejects every sampling control (temperature, top_p and top_k) for these catalog\n * models. Keep the explicit IDs alongside the provider registry so a missing compat flag cannot\n * accidentally make a newly published catalog entry permissive.\n */\nconst ANTHROPIC_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'claude-fable-5',\n 'claude-opus-4-7',\n 'claude-opus-4-8',\n 'claude-opus-5',\n 'claude-sonnet-5',\n]);\n\n/** Catalog models explicitly available through OpenAI Chat Completions as well as Responses. */\nconst OPENAI_CHAT_COMPLETIONS_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-4',\n 'gpt-4-turbo',\n 'gpt-4.1',\n 'gpt-4.1-mini',\n 'gpt-4.1-nano',\n 'gpt-4o',\n 'gpt-4o-2024-05-13',\n 'gpt-4o-2024-08-06',\n 'gpt-4o-2024-11-20',\n 'gpt-4o-mini',\n 'gpt-5',\n 'gpt-5-chat-latest',\n 'gpt-5-mini',\n 'gpt-5-nano',\n 'gpt-5.1',\n 'gpt-5.2',\n 'gpt-5.2-chat-latest',\n 'gpt-5.3-chat-latest',\n 'gpt-5.4',\n 'gpt-5.4-mini',\n 'gpt-5.4-nano',\n 'gpt-5.5',\n 'gpt-5.6-luna',\n 'gpt-5.6-sol',\n 'gpt-5.6-terra',\n 'o1',\n 'o3',\n 'o3-mini',\n 'o4-mini',\n]);\n\nconst OPENAI_NON_STREAMING_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-5.5-pro',\n 'gpt-realtime-2.1',\n 'o1-pro',\n 'o3-pro',\n]);\n\nconst GOOGLE_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'gemini-3.6-flash',\n 'gemini-3.7-flash',\n]);\n\n/** 验证固定目录模型是否适用于当前服务商协议,避免借用另一协议的能力元数据。 */\nexport function isPiCatalogModelApiCompatible(\n definition: PiProviderDefinition,\n model: Model,\n api: PiWireApi\n): boolean {\n if (model.api === api) {\n return true;\n }\n return (\n definition.key === 'openai' &&\n model.api === 'openai-responses' &&\n api === 'openai-completions' &&\n OPENAI_CHAT_COMPLETIONS_MODEL_IDS.has(model.id)\n );\n}\n\n/**\n * 重新查找并校验目录项,不直接信任调用方的 catalogHit 标记。\n * 只有确认匹配的元数据才能影响图片、流式和采样能力。\n */\nfunction verifiedCatalogModel(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean }\n): Model | undefined {\n const model = options.model;\n if (!options.catalogHit || !model) {\n return undefined;\n }\n\n const catalogModel = getPiCatalogModels(definition.key).find(\n candidate => candidate.id === model.id\n );\n if (\n !catalogModel ||\n catalogModel.provider !== definition.providerId ||\n model.provider !== definition.providerId ||\n !isPiCatalogModelApiCompatible(definition, catalogModel, api) ||\n (model.api !== api && model.api !== catalogModel.api) ||\n catalogModel.baseUrl !== getPiProviderApiBaseUrl(definition, api) ||\n model.baseUrl !== getPiProviderApiBaseUrl(definition, api)\n ) {\n return undefined;\n }\n\n return catalogModel;\n}\n\n/** 将服务商协议能力与可信目录模型限制合并,给出当前请求可用的能力。 */\nexport function resolvePiCapabilities(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean; thinkingLevel?: string }\n): Readonly | undefined {\n const registered = definition.apiCapabilities[api];\n if (!registered) {\n return undefined;\n }\n\n const catalogModel = verifiedCatalogModel(definition, api, options);\n const samplingModel = catalogModel ?? options.model;\n const thinkingEnabled = isPiThinkingEnabled(\n resolvePiThinkingLevel(options.thinkingLevel, catalogModel)\n );\n const anthropicSamplingAllowed =\n definition.key !== 'anthropic' ||\n catalogModel === undefined ||\n (samplingModel !== undefined &&\n modelSupportsTemperature(samplingModel) &&\n !ANTHROPIC_NO_SAMPLING_MODEL_IDS.has(samplingModel.id));\n // pi's Responses adapter selects `reasoning.effort: none` when the catalog explicitly\n // exposes it. Older GPT-5/o-series/pro models cannot turn reasoning off and reject\n // temperature/top_p. Dynamic OpenAI-compatible models remain API-level configurable.\n const openAISamplingAllowed =\n definition.key !== 'openai' ||\n !catalogModel?.reasoning ||\n catalogModel.thinkingLevelMap?.off === 'none';\n const googleSamplingAllowed =\n definition.key !== 'google' ||\n !catalogModel ||\n !GOOGLE_NO_SAMPLING_MODEL_IDS.has(catalogModel.id);\n const samplingAllowed =\n anthropicSamplingAllowed &&\n openAISamplingAllowed &&\n googleSamplingAllowed &&\n !(thinkingEnabled && (api === 'anthropic-messages' || definition.key === 'openai'));\n const streamingAllowed =\n definition.key !== 'openai' ||\n !catalogModel ||\n !OPENAI_NON_STREAMING_MODEL_IDS.has(catalogModel.id);\n\n return Object.freeze({\n ...registered,\n // These are adapter-level promises. Any narrower endpoint/model support is reported by\n // the provider request itself instead of being guessed from a pinned catalog.\n streaming: registered.streaming && streamingAllowed,\n tools: registered.tools,\n structuredOutput: registered.structuredOutput,\n jsonObjectOutput: registered.jsonObjectOutput,\n imageInput: registered.imageInput && catalogModel?.input.includes('image') === true,\n temperature:\n registered.temperature && modelSupportsTemperature(samplingModel) && samplingAllowed,\n sampling: samplingAllowed ? registered.sampling : NO_SAMPLING,\n });\n}\n\n/** 仅创建当前来源明确允许的协议懒加载适配器,避免引入无关服务商实现。 */\nexport function createPiApiImplementations(\n definitionOrKey: PiProviderDefinition | PiProviderKey | string\n): Partial> {\n const definition =\n typeof definitionOrKey === 'string'\n ? getPiProviderDefinition(definitionOrKey)\n : definitionOrKey;\n if (!definition) {\n return {};\n }\n\n return Object.fromEntries(\n definition.allowedApis.map(api => [api, API_LOADERS[api]()])\n ) as Partial>;\n}\n","export const PI_PROVIDER_KEYS = [\n 'ant-ling',\n 'anthropic',\n 'baseten',\n 'cerebras',\n 'deepseek',\n 'fireworks',\n 'github-copilot',\n 'google',\n 'groq',\n 'huggingface',\n 'kimi-coding',\n 'minimax',\n 'minimax-cn',\n 'mistral',\n 'moonshotai',\n 'moonshotai-cn',\n 'nvidia',\n 'openai',\n 'openai-codex',\n 'opencode',\n 'opencode-go',\n 'openrouter',\n 'qwen-token-plan',\n 'qwen-token-plan-cn',\n 'qwen-token-plan-individual',\n 'together',\n 'vercel-ai-gateway',\n 'xai',\n 'xiaomi',\n 'xiaomi-token-plan-ams',\n 'xiaomi-token-plan-cn',\n 'xiaomi-token-plan-sgp',\n 'zai',\n 'zai-coding-cn',\n] as const;\n/** MVU 当前注册的服务商标识,用于设置、连接规则与 Pi 模型目录之间的关联。 */\nexport type PiProviderKey = (typeof PI_PROVIDER_KEYS)[number];\n\nexport const PI_WIRE_APIS = [\n 'openai-responses',\n 'openai-completions',\n 'openai-codex-responses',\n 'anthropic-messages',\n 'google-generative-ai',\n 'mistral-conversations',\n] as const;\n/** 实际发送请求所用的协议,与服务商名称分开选择。 */\nexport type PiWireApi = (typeof PI_WIRE_APIS)[number];\n\n/** 标记必须使用流式传输的 Codex 账号接口,该接口没有 JSON 非流式模式。 */\nexport function isPiStreamingRequired(api: string): boolean {\n return api === 'openai-codex-responses';\n}\n\nexport const PI_AUTH_TYPES = ['api_key', 'oauth'] as const;\n/** Pi 连接的凭证来源:手填 API Key 或浏览器 OAuth 登录凭证。 */\nexport type PiAuthType = (typeof PI_AUTH_TYPES)[number];\n\n/** 不依赖 SDK 的连接规则,供设置迁移、凭证目标隔离和运行时校验共用。 */\nexport interface PiProviderTargetDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须通过酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n}\n\n/** 注册连接规则时允许省略代理列表,由定义函数补齐并冻结为空数组。 */\ntype PiProviderTargetDefinitionInput = Omit & {\n readonly corsProxyRequiredApis?: readonly PiWireApi[];\n};\n\nconst NO_CORS_PROXY_REQUIRED_APIS: readonly PiWireApi[] = Object.freeze([]);\n\n/** 补齐并冻结目标的代理策略,使共享注册信息不能被请求修改。 */\nfunction definePiProviderTarget(\n input: PiProviderTargetDefinitionInput\n): Readonly {\n return Object.freeze({\n ...input,\n corsProxyRequiredApis:\n input.corsProxyRequiredApis === undefined\n ? NO_CORS_PROXY_REQUIRED_APIS\n : Object.freeze([...input.corsProxyRequiredApis]),\n });\n}\n\n/**\n * Lightweight source of truth for provider/API/auth/endpoint compatibility. Keeping this separate\n * from catalogs and adapters lets settings migration validate credential targets without loading\n * the provider SDK boundary.\n */\nexport const PI_PROVIDER_TARGET_REGISTRY: Readonly<\n Record>\n> = Object.freeze({\n 'ant-ling': definePiProviderTarget({\n key: 'ant-ling',\n providerId: 'ant-ling',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.ant-ling.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n anthropic: definePiProviderTarget({\n key: 'anthropic',\n providerId: 'anthropic',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key', 'oauth'] as const),\n defaultBaseUrl: 'https://api.anthropic.com',\n allowCustomEndpoint: true,\n }),\n baseten: definePiProviderTarget({\n key: 'baseten',\n providerId: 'baseten',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://inference.baseten.co/v1',\n allowCustomEndpoint: false,\n }),\n cerebras: definePiProviderTarget({\n key: 'cerebras',\n providerId: 'cerebras',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.cerebras.ai/v1',\n allowCustomEndpoint: false,\n }),\n deepseek: definePiProviderTarget({\n key: 'deepseek',\n providerId: 'deepseek',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.deepseek.com',\n allowCustomEndpoint: false,\n }),\n fireworks: definePiProviderTarget({\n key: 'fireworks',\n providerId: 'fireworks',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.fireworks.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://api.fireworks.ai/inference',\n 'openai-completions': 'https://api.fireworks.ai/inference/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n 'github-copilot': definePiProviderTarget({\n key: 'github-copilot',\n providerId: 'github-copilot',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.individual.githubcopilot.com',\n corsProxyRequiredApis: ['anthropic-messages', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n google: definePiProviderTarget({\n key: 'google',\n providerId: 'google',\n defaultApi: 'google-generative-ai',\n allowedApis: Object.freeze(['google-generative-ai'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n allowCustomEndpoint: false,\n }),\n groq: definePiProviderTarget({\n key: 'groq',\n providerId: 'groq',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.groq.com/openai/v1',\n allowCustomEndpoint: false,\n }),\n huggingface: definePiProviderTarget({\n key: 'huggingface',\n providerId: 'huggingface',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://router.huggingface.co/v1',\n allowCustomEndpoint: false,\n }),\n 'kimi-coding': definePiProviderTarget({\n key: 'kimi-coding',\n providerId: 'kimi-coding',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.kimi.com/coding/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n minimax: definePiProviderTarget({\n key: 'minimax',\n providerId: 'minimax',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimax.io/anthropic',\n allowCustomEndpoint: false,\n }),\n 'minimax-cn': definePiProviderTarget({\n key: 'minimax-cn',\n providerId: 'minimax-cn',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimaxi.com/anthropic',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n mistral: definePiProviderTarget({\n key: 'mistral',\n providerId: 'mistral',\n defaultApi: 'mistral-conversations',\n allowedApis: Object.freeze(['mistral-conversations'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.mistral.ai',\n allowCustomEndpoint: false,\n }),\n moonshotai: definePiProviderTarget({\n key: 'moonshotai',\n providerId: 'moonshotai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'moonshotai-cn': definePiProviderTarget({\n key: 'moonshotai-cn',\n providerId: 'moonshotai-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.cn/v1',\n allowCustomEndpoint: false,\n }),\n nvidia: definePiProviderTarget({\n key: 'nvidia',\n providerId: 'nvidia',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://integrate.api.nvidia.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n openai: definePiProviderTarget({\n key: 'openai',\n providerId: 'openai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.openai.com/v1',\n allowCustomEndpoint: true,\n }),\n 'openai-codex': definePiProviderTarget({\n key: 'openai-codex',\n providerId: 'openai-codex',\n defaultApi: 'openai-codex-responses',\n allowedApis: Object.freeze(['openai-codex-responses'] as const),\n defaultAuthType: 'oauth',\n allowedAuthTypes: Object.freeze(['oauth'] as const),\n defaultBaseUrl: 'https://chatgpt.com/backend-api',\n corsProxyRequiredApis: ['openai-codex-responses'],\n allowCustomEndpoint: false,\n }),\n opencode: definePiProviderTarget({\n key: 'opencode',\n providerId: 'opencode',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/inference',\n 'google-generative-ai': 'https://opencode.ai/inference/v1',\n 'openai-completions': 'https://opencode.ai/inference/v1',\n 'openai-responses': 'https://opencode.ai/inference/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n 'opencode-go': definePiProviderTarget({\n key: 'opencode-go',\n providerId: 'opencode-go',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/inference/go',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/inference/go',\n 'google-generative-ai': 'https://opencode.ai/inference/go/v1',\n 'openai-completions': 'https://opencode.ai/inference/go/v1',\n 'openai-responses': 'https://opencode.ai/inference/go/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n openrouter: definePiProviderTarget({\n key: 'openrouter',\n providerId: 'openrouter',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions', 'anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://openrouter.ai/api/v1',\n apiBaseUrls: Object.freeze({\n 'openai-completions': 'https://openrouter.ai/api/v1',\n 'anthropic-messages': 'https://openrouter.ai/api',\n }),\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan': definePiProviderTarget({\n key: 'qwen-token-plan',\n providerId: 'qwen-token-plan',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-cn': definePiProviderTarget({\n key: 'qwen-token-plan-cn',\n providerId: 'qwen-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-individual': definePiProviderTarget({\n key: 'qwen-token-plan-individual',\n providerId: 'qwen-token-plan-individual',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n together: definePiProviderTarget({\n key: 'together',\n providerId: 'together',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.together.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'vercel-ai-gateway': definePiProviderTarget({\n key: 'vercel-ai-gateway',\n providerId: 'vercel-ai-gateway',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://ai-gateway.vercel.sh',\n allowCustomEndpoint: false,\n }),\n xai: definePiProviderTarget({\n key: 'xai',\n providerId: 'xai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.x.ai/v1',\n allowCustomEndpoint: false,\n }),\n xiaomi: definePiProviderTarget({\n key: 'xiaomi',\n providerId: 'xiaomi',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-ams': definePiProviderTarget({\n key: 'xiaomi-token-plan-ams',\n providerId: 'xiaomi-token-plan-ams',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-ams.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-cn': definePiProviderTarget({\n key: 'xiaomi-token-plan-cn',\n providerId: 'xiaomi-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-cn.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-sgp': definePiProviderTarget({\n key: 'xiaomi-token-plan-sgp',\n providerId: 'xiaomi-token-plan-sgp',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n zai: definePiProviderTarget({\n key: 'zai',\n providerId: 'zai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.z.ai/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n 'zai-coding-cn': definePiProviderTarget({\n key: 'zai-coding-cn',\n providerId: 'zai-coding-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n});\n\n/** 读取轻量目标注册信息,供配置和密钥归属校验使用,无需加载 SDK 目录。 */\nexport function getPiProviderTargetDefinition(\n key: string\n): Readonly | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_TARGET_REGISTRY, key)\n ? PI_PROVIDER_TARGET_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** 判断指定服务商的内置协议端点是否必须经酒馆 CORS 代理访问。 */\nexport function isPiCorsProxyRequired(\n definition: Readonly | undefined,\n api: string\n): boolean {\n return definition?.corsProxyRequiredApis.includes(api as PiWireApi) ?? false;\n}\n\n/**\n * 内置端点遵循注册的代理策略,自定义端点遵循用户勾选状态。\n * 等价写法先规范化,避免把显式填写的默认地址误认作自定义目标。\n */\nexport function shouldUsePiCorsProxy(\n definition: Readonly | undefined,\n api: string,\n endpoint: string,\n customEndpointUseProxy: unknown\n): boolean {\n const explicit_endpoint = endpoint.trim();\n if (\n !definition ||\n explicit_endpoint === '' ||\n !definition.allowedApis.includes(api as PiWireApi)\n ) {\n return isPiCorsProxyRequired(definition, api);\n }\n try {\n const wire_api = api as PiWireApi;\n const configured_base = normalizePiApiBaseEndpoint(wire_api, explicit_endpoint);\n const registered_base = normalizePiApiBaseEndpoint(\n wire_api,\n getPiProviderApiBaseUrl(definition, wire_api)\n );\n if (configured_base === registered_base) {\n return isPiCorsProxyRequired(definition, api);\n }\n } catch {\n // Target validation reports malformed endpoints. Until then, keep the explicit checkbox\n // semantics so the UI does not mislabel an invalid value as a registered route.\n }\n return customEndpointUseProxy === true;\n}\n\n/** 优先读取协议专用基础地址,否则使用服务商默认地址。 */\nexport function getPiProviderApiBaseUrl(\n definition: Readonly,\n api: PiWireApi\n): string {\n return definition.apiBaseUrls?.[api] ?? definition.defaultBaseUrl;\n}\n\nexport class PiEndpointValidationError extends Error {\n /** 将端点格式或传输限制失败标记为独立的端点校验错误。 */\n constructor(message: string) {\n super(message);\n this.name = 'PiEndpointValidationError';\n }\n}\n\nconst HTTP_LOOPBACK_HOSTNAMES: ReadonlySet = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/** 规范端点地址,要求 HTTPS 或回环 HTTP,并拒绝内嵌凭证、查询参数和片段。 */\nexport function normalizePiTargetEndpoint(endpoint: string): string {\n let parsed: URL;\n try {\n parsed = new URL(endpoint.trim());\n } catch {\n throw new PiEndpointValidationError('More source endpoint must be a valid URL');\n }\n\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new PiEndpointValidationError('More source endpoint must use http or https');\n }\n if (\n parsed.protocol === 'http:' &&\n !HTTP_LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase())\n ) {\n throw new PiEndpointValidationError(\n 'More source HTTP endpoint must use localhost, 127.0.0.1, or [::1]'\n );\n }\n if (parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new PiEndpointValidationError(\n 'More source endpoint must not contain credentials, a query, or a fragment'\n );\n }\n\n const normalized_path = parsed.pathname.replace(/\\/+$/, '');\n return `${parsed.origin}${normalized_path}`;\n}\n\nconst PI_API_OPERATION_PATH_SUFFIXES: Partial> = {\n 'openai-responses': ['/responses'],\n 'openai-completions': ['/chat/completions', '/completions'],\n 'anthropic-messages': ['/v1/messages', '/messages'],\n};\n\n/**\n * 将安全端点转换为 SDK 需要的 API 基础地址。\n * 最多移除一次当前协议会自行追加的操作路径,避免重复拼接或过度裁剪。\n */\nexport function normalizePiApiBaseEndpoint(api: PiWireApi, endpoint: string): string {\n const normalized_endpoint = normalizePiTargetEndpoint(endpoint);\n const parsed = new URL(normalized_endpoint);\n const normalized_path_lower = parsed.pathname.toLowerCase();\n const operation_path = PI_API_OPERATION_PATH_SUFFIXES[api]?.find(suffix =>\n normalized_path_lower.endsWith(suffix)\n );\n if (!operation_path) {\n return normalized_endpoint;\n }\n\n const base_path = parsed.pathname.slice(0, -operation_path.length).replace(/\\/+$/, '');\n return normalizePiTargetEndpoint(`${parsed.origin}${base_path}`);\n}\n\n/**\n * 仅为完整、合法的 API Key 目标生成服务商与端点对应的缓存键。\n * 未知协议、OAuth、无效地址或不允许自定义端点时返回空值。\n */\nexport function resolvePiApiKeyScope(\n definition: Readonly | undefined,\n api: string,\n auth_type: string,\n endpoint: string\n): string {\n if (\n !definition ||\n auth_type !== 'api_key' ||\n !definition.allowedAuthTypes.includes('api_key') ||\n !definition.allowedApis.includes(api as PiWireApi) ||\n (endpoint.trim() !== '' && !definition.allowCustomEndpoint)\n ) {\n return '';\n }\n\n try {\n const effective_endpoint = normalizePiApiBaseEndpoint(\n api as PiWireApi,\n endpoint.trim() === ''\n ? getPiProviderApiBaseUrl(definition, api as PiWireApi)\n : endpoint\n );\n return `${definition.key}\\n${effective_endpoint}`;\n } catch {\n return '';\n }\n}\n","import type {\n Api,\n AssistantMessage,\n Context,\n ImageContent,\n Message,\n TextContent,\n ToolCall,\n ToolResultMessage,\n Usage,\n UserMessage,\n} from '@earendil-works/pi-ai';\n\n/** 需要按酒馆原始位置保留的系统消息,交由请求载荷 hook 在原位置恢复。 */\nexport type PiLateSystemMessage = {\n sourceIndex: number;\n /** 插在转换后第几条普通消息之前;等于 messages.length 表示位于末尾。 */\n beforeMessageIndex: number;\n text: string;\n};\n\n/** 上下文转换的内容保留及空消息诊断,不包含完整提示词。 */\nexport type PiContextAdapterDiagnostics = {\n /** 保留待处理的 system 数量;最终角色由协议适配层自动决定。 */\n preservedLateSystemCount: number;\n droppedEmptyMessageIndexes: number[];\n};\n\n/** 标准 Pi 上下文与必须一并传给载荷适配层的原生 system 消息。 */\nexport type PiContextAdapterResult = {\n context: Context;\n lateSystemMessages: readonly PiLateSystemMessage[];\n diagnostics: PiContextAdapterDiagnostics;\n};\n\n/** 提示词转换或原生 system 恢复失败的稳定分类。 */\nexport type PiContextAdapterErrorCode =\n | 'invalid-image'\n | 'invalid-tool-call'\n | 'system-payload-mismatch'\n | 'missing-tool-call'\n | 'unsupported-content';\n\nexport class PiContextAdapterError extends Error {\n /** 保留消息下标和错误类别,便于将上下文转换失败定位到原始提示词。 */\n constructor(\n message: string,\n readonly code: PiContextAdapterErrorCode,\n readonly sourceIndex: number\n ) {\n super(message);\n this.name = 'PiContextAdapterError';\n }\n}\n\n/** 酒馆捕获消息,加上历史工具结果可能携带的错误标记。 */\ntype SendingMessage = SillyTavern.SendingMessage & {\n name?: string;\n is_error?: boolean;\n isError?: boolean;\n};\n\n/** 酒馆多模态内容数组中的单个块。 */\ntype ContentBlock = NonNullable>[number];\n/** 当前转换层允许交给 Pi 的文本或图片输入。 */\ntype PiInputContent = TextContent | ImageContent;\n\nconst IMPORTED_ASSISTANT_API = 'sillytavern-import' as Api;\nconst IMPORTED_ASSISTANT_PROVIDER = 'sillytavern';\nconst IMPORTED_ASSISTANT_MODEL = 'prepared-prompt';\n\nconst SUPPORTED_IMAGE_MIME_TYPES = new Set(['image/gif', 'image/jpeg', 'image/png', 'image/webp']);\nconst MEBIBYTE = 1024 * 1024;\nconst MAX_DATA_URL_HEADER_CHARACTERS = 64;\nconst MAX_DECODED_IMAGE_HEADER_CHARACTERS = 768;\nconst MAX_ENCODED_IMAGE_HEADER_CHARACTERS = Math.ceil(MAX_DECODED_IMAGE_HEADER_CHARACTERS / 3) * 4;\n\nexport const PI_IMAGE_INPUT_LIMITS = Object.freeze({\n // Five MiB matches the strictest supported provider's common per-image cap.\n // The aggregate/count ceilings bound base64 copies on mobile browsers.\n maxDecodedBytesPerImage: 5 * MEBIBYTE,\n maxDecodedBytesPerContext: 16 * MEBIBYTE,\n maxImagesPerContext: 20,\n});\n\nconst MAX_ENCODED_CHARACTERS_PER_IMAGE =\n Math.ceil(PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / 3) * 4;\n\n/** 图片校验时提取的预算元数据,避免 token 预检重复解码图片。 */\nexport type PiImageMetadata = Readonly<{\n decodedBytes: number;\n dimensions?: Readonly<{\n width: number;\n height: number;\n }>;\n}>;\n\n/** 单次上下文转换累计使用的图片数量与解码字节数。 */\ntype ImageInputBudget = {\n decodedBytes: number;\n imageCount: number;\n};\n\nconst imageMetadata = new WeakMap();\n\n/** 读取转换时记录的图片大小和尺寸,供预算检查复用,避免再次解码。 */\nexport function getPiImageMetadata(image: ImageContent): PiImageMetadata | undefined {\n return imageMetadata.get(image);\n}\n\n/** 为导入的历史助手消息补齐零用量结构,避免把历史内容记为本次生成消耗。 */\nfunction makeZeroUsage(): Usage {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n}\n\n/** 转义消息名称中的 XML 特殊字符,防止名称破坏注入标记结构。 */\nfunction escapeXmlText(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');\n}\n\n/** 将有效的酒馆消息名称转换为可拼接的文本前缀。 */\nexport function formatSendingMessageName(name: string | undefined): string {\n if (typeof name !== 'string' || name.trim() === '') {\n return '';\n }\n return `${escapeXmlText(name)}`;\n}\n\n/** 识别仅含空白的内容,供严格校验和空消息处理使用。 */\nfunction isBlank(value: string): boolean {\n return value.trim().length === 0;\n}\n\n/** 仅接受普通对象或空原型对象,避免把类实例作为结构化消息内容。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 检查指定偏移处的文件头字节,用于验证声明的图片类型。 */\nfunction hasBytesAt(bytes: string, offset: number, expected: readonly number[]): boolean {\n return expected.every((value, index) => bytes.charCodeAt(offset + index) === value);\n}\n\n/** 核对 PNG、JPEG、GIF 或 WebP 文件签名,拒绝伪装为图片的数据。 */\nfunction hasRecognizableImageSignature(mimeType: string, bytes: string): boolean {\n if (mimeType === 'image/png') {\n return hasBytesAt(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n }\n if (mimeType === 'image/jpeg') {\n return hasBytesAt(bytes, 0, [0xff, 0xd8, 0xff]);\n }\n if (mimeType === 'image/gif') {\n return bytes.startsWith('GIF87a') || bytes.startsWith('GIF89a');\n }\n if (mimeType === 'image/webp') {\n return bytes.startsWith('RIFF') && bytes.slice(8, 12) === 'WEBP';\n }\n return false;\n}\n\n/** 判断字符是否属于标准 Base64 字母表,供图片载荷逐字符校验使用。 */\nfunction isBase64Character(code: number): boolean {\n return (\n (code >= 0x41 && code <= 0x5a) ||\n (code >= 0x61 && code <= 0x7a) ||\n (code >= 0x30 && code <= 0x39) ||\n code === 0x2b ||\n code === 0x2f\n );\n}\n\n/** 按大端读取图片头中的 16 位整数。 */\nfunction readUint16BigEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) * 0x100 + bytes.charCodeAt(offset + 1);\n}\n\n/** 按小端读取图片头中的 16 位整数。 */\nfunction readUint16LittleEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) + bytes.charCodeAt(offset + 1) * 0x100;\n}\n\n/** 按小端读取 WebP 图片头中的 24 位整数。 */\nfunction readUint24LittleEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) +\n bytes.charCodeAt(offset + 1) * 0x100 +\n bytes.charCodeAt(offset + 2) * 0x10000\n );\n}\n\n/** 按大端读取图片头中的 32 位整数,并保持无符号取值。 */\nfunction readUint32BigEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) * 0x1000000 +\n bytes.charCodeAt(offset + 1) * 0x10000 +\n bytes.charCodeAt(offset + 2) * 0x100 +\n bytes.charCodeAt(offset + 3)\n );\n}\n\nconst JPEG_START_OF_FRAME_MARKERS = new Set([\n 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,\n]);\n\n/** 扫描已解码的 JPEG 头部标记以提取尺寸;信息不足时不推测尺寸。 */\nfunction readJpegDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 4 || bytes.charCodeAt(0) !== 0xff || bytes.charCodeAt(1) !== 0xd8) {\n return undefined;\n }\n\n let offset = 2;\n while (offset + 3 < bytes.length) {\n if (bytes.charCodeAt(offset) !== 0xff) {\n offset += 1;\n continue;\n }\n while (offset < bytes.length && bytes.charCodeAt(offset) === 0xff) {\n offset += 1;\n }\n const marker = bytes.charCodeAt(offset);\n offset += 1;\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {\n continue;\n }\n if (marker === 0xd9 || marker === 0xda || offset + 1 >= bytes.length) {\n break;\n }\n const segmentLength = readUint16BigEndian(bytes, offset);\n if (segmentLength < 2 || offset + segmentLength > bytes.length) {\n break;\n }\n if (JPEG_START_OF_FRAME_MARKERS.has(marker) && segmentLength >= 7) {\n return {\n height: readUint16BigEndian(bytes, offset + 3),\n width: readUint16BigEndian(bytes, offset + 5),\n };\n }\n offset += segmentLength;\n }\n return undefined;\n}\n\n/** 按 WebP 头部编码类型提取尺寸,兼容其不同帧头布局。 */\nfunction readWebpDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 30 || !bytes.startsWith('RIFF') || bytes.slice(8, 12) !== 'WEBP') {\n return undefined;\n }\n\n const chunkType = bytes.slice(12, 16);\n if (chunkType === 'VP8X') {\n return {\n width: readUint24LittleEndian(bytes, 24) + 1,\n height: readUint24LittleEndian(bytes, 27) + 1,\n };\n }\n if (chunkType === 'VP8 ' && bytes.slice(23, 26) === '\\x9d\\x01\\x2a') {\n return {\n width: readUint16LittleEndian(bytes, 26) & 0x3fff,\n height: readUint16LittleEndian(bytes, 28) & 0x3fff,\n };\n }\n if (chunkType === 'VP8L' && bytes.charCodeAt(20) === 0x2f) {\n const b1 = bytes.charCodeAt(21);\n const b2 = bytes.charCodeAt(22);\n const b3 = bytes.charCodeAt(23);\n const b4 = bytes.charCodeAt(24);\n return {\n width: 1 + b1 + ((b2 & 0x3f) << 8),\n height: 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10),\n };\n }\n return undefined;\n}\n\n/** 根据已验证的图片类型分派尺寸读取,无法读取时返回 undefined。 */\nfunction readImageDimensions(bytes: string, mimeType: string): PiImageMetadata['dimensions'] {\n if (mimeType === 'image/png' && bytes.length >= 24) {\n return {\n width: readUint32BigEndian(bytes, 16),\n height: readUint32BigEndian(bytes, 20),\n };\n }\n if (mimeType === 'image/gif' && bytes.length >= 10) {\n return {\n width: readUint16LittleEndian(bytes, 6),\n height: readUint16LittleEndian(bytes, 8),\n };\n }\n if (mimeType === 'image/jpeg') {\n return readJpegDimensions(bytes);\n }\n if (mimeType === 'image/webp') {\n return readWebpDimensions(bytes);\n }\n return undefined;\n}\n\n/** 在受限的头部长度内查找 data URL 分隔符,避免对异常长头部做无界扫描。 */\nfunction findDataUrlSeparator(url: string): number {\n const maximumOffset = Math.min(url.length, MAX_DATA_URL_HEADER_CHARACTERS + 1);\n for (let offset = 5; offset < maximumOffset; offset++) {\n if (url.charCodeAt(offset) === 0x2c) {\n return offset;\n }\n }\n return -1;\n}\n\n/** 只解码识别图片类型和尺寸所需的有限头部,控制大图转换的内存开销。 */\nfunction decodeImageHeader(url: string, dataStart: number, encodedLength: number): string {\n const prefixLength = Math.min(encodedLength, MAX_ENCODED_IMAGE_HEADER_CHARACTERS);\n return atob(url.slice(dataStart, dataStart + prefixLength));\n}\n\n/**\n * 校验 data URL、Base64 编码、文件签名和图片预算,并记录元数据。\n * 仅接收受支持的内嵌图片,失败时携带原始消息下标,不自动下载远程地址。\n */\nfunction parseBase64Image(\n url: string,\n sourceIndex: number,\n imageBudget: ImageInputBudget\n): ImageContent {\n if (url.slice(0, 5).toLowerCase() !== 'data:') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含远程图片 URL;更多来源当前只接受 data URL 图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n\n if (url.length > MAX_DATA_URL_HEADER_CHARACTERS + 1 + MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const separator = findDataUrlSeparator(url);\n if (separator === -1) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const headerMatch = /^data:([^;,]+);base64$/i.exec(url.slice(0, separator));\n if (!headerMatch) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const [, rawMimeType] = headerMatch;\n const mimeType = rawMimeType.toLowerCase();\n if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片 MIME 类型不受支持;只接受 PNG、JPEG、GIF 或 WebP`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const dataStart = separator + 1;\n const encodedLength = url.length - dataStart;\n if (encodedLength > MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (encodedLength === 0 || encodedLength % 4 !== 0) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n let padding = 0;\n if (url.charCodeAt(url.length - 1) === 0x3d) {\n padding += 1;\n if (url.charCodeAt(url.length - 2) === 0x3d) {\n padding += 1;\n }\n }\n const decodedBytes = (encodedLength / 4) * 3 - padding;\n if (decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.imageCount + 1 > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求最多接受 ${PI_IMAGE_INPUT_LIMITS.maxImagesPerContext} 张图片`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.decodedBytes + decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求的图片总量超过 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const contentEnd = url.length - padding;\n for (let offset = dataStart; offset < contentEnd; offset++) {\n if (!isBase64Character(url.charCodeAt(offset))) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n for (let offset = contentEnd; offset < url.length; offset++) {\n if (url.charCodeAt(offset) !== 0x3d) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n\n let decodedHeader: string;\n try {\n decodedHeader = decodeImageHeader(url, dataStart, encodedLength);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无法解码的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n if (!hasRecognizableImageSignature(mimeType, decodedHeader)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片数据与声明的 MIME 类型不匹配或缺少有效文件头`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n imageBudget.imageCount += 1;\n imageBudget.decodedBytes += decodedBytes;\n\n const image: ImageContent = {\n type: 'image',\n data: url.slice(dataStart),\n mimeType,\n };\n const dimensions = readImageDimensions(decodedHeader, mimeType);\n imageMetadata.set(image, {\n decodedBytes,\n ...(dimensions === undefined ? {} : { dimensions }),\n });\n return image;\n}\n\n/** 将酒馆文本或图片块转换为 Pi 内容块,并执行对应的结构与图片校验。 */\nfunction convertContentBlock(\n block: ContentBlock,\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent {\n if (block.type === 'text') {\n return { type: 'text', text: block.text };\n }\n if (block.type === 'video_url') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含更多来源不支持的视频内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n if (block.type === 'image_url') {\n if (!allowImages) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法导入更多来源历史的图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return parseBase64Image(block.image_url.url, sourceIndex, imageBudget);\n }\n\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含未知内容块`,\n 'unsupported-content',\n sourceIndex\n );\n}\n\n/** 统一处理字符串和多块消息内容,逐块转换为 Pi 输入格式。 */\nfunction convertContent(\n content: SendingMessage['content'],\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent[] {\n if (typeof content === 'string') {\n return [{ type: 'text', text: content }];\n }\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map(block => convertContentBlock(block, sourceIndex, allowImages, imageBudget));\n}\n\n/** 判断内容块是否包含可发送的文本或图片。 */\nfunction contentHasValue(content: PiInputContent[]): boolean {\n return content.some(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 拼接文本内容块,供只允许纯文本的消息角色使用。 */\nfunction compactTextBlocks(content: PiInputContent[]): PiInputContent[] {\n return content.filter(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 提取 system 消息的纯文本,统一拒绝不支持的系统内容。 */\nfunction extractSystemText(message: SendingMessage, sourceIndex: number): string {\n const content = message.content;\n let text: string;\n if (typeof content === 'string') {\n text = content;\n } else if (Array.isArray(content)) {\n const textBlocks = content.map(block => {\n if (block.type !== 'text') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 system 消息包含非文本内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return block.text;\n });\n text = textBlocks.join('');\n } else {\n text = '';\n }\n\n const name = formatSendingMessageName(message.name);\n return [name, text].filter(value => value !== '').join('\\n');\n}\n\n/** 在消息正文前保留酒馆角色名,并避免生成空的名称前缀。 */\nfunction addNamePrefix(content: PiInputContent[], name: string | undefined): PiInputContent[] {\n const prefix = formatSendingMessageName(name);\n if (!prefix) {\n return content;\n }\n return [{ type: 'text', text: prefix }, ...content];\n}\n\n/** 将历史工具参数解析为普通对象,拒绝无效 JSON 或非对象参数。 */\nfunction parseToolArguments(argumentsValue: string, sourceIndex: number): ToolCall['arguments'] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(argumentsValue);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法解析的工具参数`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n if (!isPlainObject(parsed)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息的工具参数必须是 JSON object`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n // JSON.parse 已排除非 JSON 值,且上方校验保证参数是对象而非数组或标量。\n return parsed as ToolCall['arguments'];\n}\n\n/** 校验历史工具调用标识和参数,并转换为 Pi 工具调用块。 */\nfunction convertToolCalls(\n message: SendingMessage,\n sourceIndex: number,\n toolNamesById: Map\n): ToolCall[] {\n return (message.tool_calls ?? []).map(call => {\n if (!call.id || !call.function?.name) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无效工具调用`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n const existingName = toolNamesById.get(call.id);\n if (existingName && existingName !== call.function.name) {\n throw new PiContextAdapterError(\n `工具调用 ID ${call.id} 对应了多个工具名`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n toolNamesById.set(call.id, call.function.name);\n return {\n type: 'toolCall',\n id: call.id,\n name: call.function.name,\n arguments: parseToolArguments(call.function.arguments, sourceIndex),\n };\n });\n}\n\n/** 转换历史助手文本和工具调用,并补齐 Pi 要求的历史消息元数据。 */\nfunction convertAssistantMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): AssistantMessage {\n const textContent = compactTextBlocks(\n addNamePrefix(\n convertContent(message.content, sourceIndex, false, imageBudget),\n message.name\n )\n ) as TextContent[];\n const toolCalls = convertToolCalls(message, sourceIndex, toolNamesById);\n return {\n role: 'assistant',\n content: [...textContent, ...toolCalls],\n api: IMPORTED_ASSISTANT_API,\n provider: IMPORTED_ASSISTANT_PROVIDER,\n model: IMPORTED_ASSISTANT_MODEL,\n usage: makeZeroUsage(),\n stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',\n timestamp: now(),\n };\n}\n\n/** 将工具返回值关联到已知调用,保留工具名、错误标记及多模态结果。 */\nfunction convertToolResultMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): ToolResultMessage {\n const toolCallId = message.tool_call_id;\n if (!toolCallId) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息缺少 tool_call_id`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n const toolName = toolNamesById.get(toolCallId);\n if (!toolName) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息找不到对应的历史工具调用`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n return {\n role: 'toolResult',\n toolCallId,\n toolName,\n content: compactTextBlocks(convertContent(message.content, sourceIndex, true, imageBudget)),\n isError: message.is_error === true || message.isError === true,\n timestamp: now(),\n };\n}\n\n/** 所有来源统一丢弃空消息,并保留原始下标用于诊断。 */\nfunction recordEmptyMessage(sourceIndex: number, diagnostics: PiContextAdapterDiagnostics): void {\n diagnostics.droppedEmptyMessageIndexes.push(sourceIndex);\n}\n\n/**\n * 把酒馆最终提示词转换为 Pi 上下文,并返回转换诊断。\n * 前置 system 合并为系统提示词;后置 system 单独保留内容及原位置,由载荷 hook 恢复。\n * 同时校验工具关联和图片预算、清理空消息;渠道兼容由载荷适配层处理。\n * now 只供测试注入稳定时间,不提供渠道相关的转换策略开关。\n */\nexport function toPiContext(\n input: readonly SendingMessage[],\n now: () => number = Date.now\n): PiContextAdapterResult {\n const messages = Array.from(input);\n const diagnostics: PiContextAdapterDiagnostics = {\n preservedLateSystemCount: 0,\n droppedEmptyMessageIndexes: [],\n };\n\n let leadingSystemEnd = 0;\n while (leadingSystemEnd < messages.length && messages[leadingSystemEnd].role === 'system') {\n leadingSystemEnd++;\n }\n\n const systemPromptParts: string[] = [];\n for (let index = 0; index < leadingSystemEnd; index++) {\n const text = extractSystemText(messages[index], index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n systemPromptParts.push(text);\n }\n\n const lateSystemMessages: PiLateSystemMessage[] = [];\n const piMessages: Message[] = [];\n const toolNamesById = new Map();\n const imageBudget: ImageInputBudget = { decodedBytes: 0, imageCount: 0 };\n for (let index = leadingSystemEnd; index < messages.length; index++) {\n const message = messages[index];\n if (message.role === 'system') {\n const text = extractSystemText(message, index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n lateSystemMessages.push({\n sourceIndex: index,\n beforeMessageIndex: piMessages.length,\n text,\n });\n continue;\n }\n if (message.role === 'user') {\n const content = addNamePrefix(\n convertContent(message.content, index, true, imageBudget),\n message.name\n );\n if (!contentHasValue(content)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n const compactContent = compactTextBlocks(content);\n const userMessage: UserMessage = {\n role: 'user',\n content:\n compactContent.length === 1 && compactContent[0].type === 'text'\n ? compactContent[0].text\n : compactContent,\n timestamp: now(),\n };\n piMessages.push(userMessage);\n continue;\n }\n if (message.role === 'assistant') {\n const assistantMessage = convertAssistantMessage(\n message,\n index,\n now,\n toolNamesById,\n imageBudget\n );\n if (assistantMessage.content.length === 0) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n piMessages.push(assistantMessage);\n continue;\n }\n if (message.role === 'tool') {\n piMessages.push(\n convertToolResultMessage(message, index, now, toolNamesById, imageBudget)\n );\n continue;\n }\n throw new PiContextAdapterError(\n `第 ${index} 条消息包含未知角色`,\n 'unsupported-content',\n index\n );\n }\n diagnostics.droppedEmptyMessageIndexes.sort((left, right) => left - right);\n diagnostics.preservedLateSystemCount = lateSystemMessages.length;\n\n return {\n context: {\n ...(systemPromptParts.length > 0\n ? { systemPrompt: systemPromptParts.join('\\n\\n') }\n : {}),\n messages: piMessages,\n },\n lateSystemMessages,\n diagnostics,\n };\n}\n","import type { FetchFunction } from './pi_gateway';\nimport type { PiWireApi } from './provider_target';\n\n/** 尚待逐字段校验的协议 JSON 对象,用于非流式响应和本地合成的流事件。 */\ntype JsonObject = Record;\n\n/** 校验响应结构为非空、非数组对象。 */\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 拒绝不符合协议的非流式响应,不将可能包含提示词或凭证的响应正文写入错误。 */\nfunction invalidResponse(): never {\n // Do not include provider bodies: they can echo prompts, headers, or credentials.\n throw new Error('More source received an invalid non-streaming response');\n}\n\n/** 把单个协议事件编码为 SSE 文本,供 Pi 现有流解析器消费。 */\nfunction event(data: JsonObject): string {\n return `${typeof data.type === 'string' ? `event: ${data.type}\\n` : ''}data: ${JSON.stringify(data)}\\n\\n`;\n}\n\n/** 把 Chat Completions 完整响应转换为增量结束事件,补齐工具调用下标。 */\nfunction completionEvents(response: JsonObject): string {\n if (!Array.isArray(response.choices) || response.choices.length === 0) {\n return invalidResponse();\n }\n const choices = response.choices.map((choice: unknown, index) => {\n if (\n !isObject(choice) ||\n !isObject(choice.message) ||\n typeof choice.finish_reason !== 'string' ||\n !choice.finish_reason\n ) {\n return invalidResponse();\n }\n const delta = { ...choice.message };\n if (Array.isArray(delta.tool_calls)) {\n delta.tool_calls = delta.tool_calls.map((call: unknown, index) => {\n if (!isObject(call)) {\n return invalidResponse();\n }\n return { ...call, index };\n });\n }\n const { message: _message, ...rest } = choice;\n return { ...rest, index, delta };\n });\n return event({ ...response, object: 'chat.completion.chunk', choices }) + 'data: [DONE]\\n\\n';\n}\n\n/** 把 Responses 完整输出转换为输出项事件和终态事件,保留失败与截断状态。 */\nfunction responsesEvents(response: JsonObject): string {\n const status = response.status;\n if (!['completed', 'incomplete', 'failed', 'cancelled'].includes(String(status))) {\n return invalidResponse();\n }\n if (!Array.isArray(response.output)) {\n return invalidResponse();\n }\n return (\n response.output\n .map((item: unknown, output_index) => {\n if (!isObject(item)) {\n return invalidResponse();\n }\n return event({ type: 'response.output_item.done', output_index, item });\n })\n .join('') +\n event({\n type: `response.${status === 'cancelled' ? 'failed' : status}`,\n response,\n })\n );\n}\n\n/** 把 Anthropic 完整响应转换为消息和内容块事件,工具参数通过增量事件交给 Pi 解析。 */\nfunction anthropicEvents(response: JsonObject): string {\n if (!Array.isArray(response.content) || typeof response.stop_reason !== 'string') {\n return invalidResponse();\n }\n const usage = isObject(response.usage) ? response.usage : {};\n let result = event({\n type: 'message_start',\n message: { ...response, content: [], usage },\n });\n response.content.forEach((block: unknown, index) => {\n if (!isObject(block)) {\n return invalidResponse();\n }\n result += event({\n type: 'content_block_start',\n index,\n content_block: block.type === 'tool_use' ? { ...block, input: {} } : block,\n });\n // Pi finalizes tool arguments from the delta buffer, even when start has an input.\n if (block.type === 'tool_use') {\n result += event({\n type: 'content_block_delta',\n index,\n delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) },\n });\n }\n result += event({ type: 'content_block_stop', index });\n });\n return (\n result +\n event({\n type: 'message_delta',\n delta: {\n stop_reason: response.stop_reason,\n stop_sequence: response.stop_sequence,\n stop_details: response.stop_details,\n },\n usage,\n }) +\n event({ type: 'message_stop' })\n );\n}\n\n/** 按请求协议选择非流式响应转换器,拒绝错误对象和不支持的响应结构。 */\nfunction toEvents(api: PiWireApi, response: unknown): string {\n if (!isObject(response) || response.error) {\n return invalidResponse();\n }\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return completionEvents(response);\n case 'openai-responses':\n return responsesEvents(response);\n case 'anthropic-messages':\n return anthropicEvents(response);\n case 'google-generative-ai':\n if (!Array.isArray(response.candidates)) {\n return invalidResponse();\n }\n return event(response);\n default:\n return invalidResponse();\n }\n}\n\n/** 识别需要改写传输方式的生成接口,避免影响认证和模型列表等其他请求。 */\nfunction isGenerationUrl(api: PiWireApi, url: URL): boolean {\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return url.pathname.endsWith('/chat/completions');\n case 'openai-responses':\n return url.pathname.endsWith('/responses');\n case 'anthropic-messages':\n return url.pathname.endsWith('/messages');\n case 'google-generative-ai':\n return url.pathname.endsWith(':streamGenerateContent');\n default:\n return false;\n }\n}\n\n/**\n * 保留 Pi 的请求构造、认证和结果解析,只将支持的生成请求改为真正的非流式 HTTP。\n * 返回的 JSON 在本地转换为 Pi 可消费的事件流;Codex 的强制流式协议保持原样。\n */\nexport function createPiNonStreamingFetch(\n api: PiWireApi,\n fetch_impl?: FetchFunction\n): FetchFunction {\n return async (input, init) => {\n const send = fetch_impl ?? globalThis.fetch;\n const request = new Request(input, init);\n const url = new URL(request.url);\n if (request.method !== 'POST' || !isGenerationUrl(api, url)) {\n return send(input, init);\n }\n request.signal.throwIfAborted();\n const body: unknown = await request.json();\n if (!isObject(body)) {\n throw new Error('More source requires a JSON request body');\n }\n if (api === 'google-generative-ai') {\n url.pathname = url.pathname.replace(/:streamGenerateContent$/, ':generateContent');\n if (url.searchParams.get('alt') === 'sse') {\n url.searchParams.delete('alt');\n }\n } else {\n body.stream = false;\n delete body.stream_options;\n delete body.tool_stream;\n if (api === 'anthropic-messages' && Array.isArray(body.tools)) {\n body.tools.forEach((tool: unknown) => {\n if (isObject(tool)) {\n delete tool.eager_input_streaming;\n }\n });\n }\n }\n const headers = new Headers(request.headers);\n headers.set('accept', 'application/json');\n headers.delete('content-length');\n const response = await send(\n new Request(url, {\n ...init,\n method: request.method,\n headers,\n body: JSON.stringify(body),\n signal: request.signal,\n credentials: request.credentials,\n redirect: request.redirect,\n })\n );\n request.signal.throwIfAborted();\n // Keep provider HTTP errors intact for Pi's existing retry/error handling. Some gateways\n // ignore stream:false and still return SSE; their valid stream can be consumed as-is.\n if (!response.ok || response.headers.get('content-type')?.includes('text/event-stream')) {\n return response;\n }\n const data: unknown = await response.json();\n request.signal.throwIfAborted();\n const response_headers = new Headers(response.headers);\n response_headers.set('content-type', 'text/event-stream');\n response_headers.delete('content-length');\n response_headers.delete('content-encoding');\n return new Response(toEvents(api, data), {\n status: response.status,\n statusText: response.statusText,\n headers: response_headers,\n });\n };\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** 需要映射到服务商原生 JSON 输出配置的两种应答格式。 */\nexport type PiStructuredResponseFormat = '格式化输出' | '格式化输出(v4兼容)';\n\n/** MVU 的结构化输出 Schema,包含协议需要的名称及可选严格校验标记。 */\nexport type PiJsonSchema = {\n name: string;\n description?: string;\n value: Record;\n strict?: boolean;\n};\n\n/** 单次请求的原生载荷转换选项;消息角色由独立的 system 桥接层恢复。 */\nexport type PiPayloadTransformOptions = {\n api: Api;\n responseFormat?: PiStructuredResponseFormat;\n jsonSchema?: PiJsonSchema;\n customIncludeBody?: Record;\n customExcludeBody?: readonly string[];\n sampling?: {\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n };\n};\n\nconst PROTECTED_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'config',\n 'instructions',\n 'model',\n 'messages',\n 'input',\n 'contents',\n 'system',\n 'stream',\n 'tools',\n 'tool_choice',\n 'toolChoice',\n 'toolConfig',\n]);\n\nconst GOOGLE_PROTECTED_CONFIG_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'abortSignal',\n 'automaticFunctionCalling',\n 'httpOptions',\n 'responseJsonSchema',\n 'responseMimeType',\n 'responseSchema',\n 'systemInstruction',\n 'temperature',\n 'thinkingConfig',\n 'toolConfig',\n 'tools',\n 'topK',\n 'topP',\n]);\n\n/** 判断请求体或自定义配置是否为非空、非数组对象。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * 先删除再合并自定义字段,同时阻止修改受保护的请求字段。\n * 只浅拷贝原请求体以保留 AbortSignal 等浏览器对象,用户覆盖值单独深拷贝。\n */\nfunction applyCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n // Provider payloads may contain live browser objects (notably Google's AbortSignal).\n // A deep structuredClone turns those into plain objects and breaks cancellation. Only clone\n // the top-level payload here; user-supplied include values are cloned individually below.\n const result = { ...payload };\n for (const name of exclude) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot exclude protected field '${name}'`);\n }\n delete result[name];\n }\n for (const [name, value] of Object.entries(include)) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot override protected field '${name}'`);\n }\n result[name] = structuredClone(value);\n }\n return result;\n}\n\n/** 只接受 config.<字段> 形式的 Google 删除路径,避免歧义或删除整个生成配置。 */\nfunction googleConfigField(path: string): string {\n const match = /^config\\.([^.]+)$/.exec(path);\n if (!match) {\n throw new Error(\n `More source customExcludeBody for Google must use a direct 'config.' path, received '${path}'`\n );\n }\n return match[1];\n}\n\n/** 阻止自定义配置覆盖或删除由运行时管理的 Google 核心字段。 */\nfunction assertGoogleConfigFieldAllowed(field: string, operation: 'override' | 'exclude'): void {\n if (GOOGLE_PROTECTED_CONFIG_FIELDS.has(field)) {\n throw new Error(\n `More source custom body cannot ${operation} protected field 'config.${field}'`\n );\n }\n}\n\n/** 将 Google 自定义参数限制在 config 内,保留现有配置中的取消信号等对象。 */\nfunction applyGoogleCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n const existing_config = isPlainObject(payload.config) ? payload.config : {};\n const config = { ...existing_config };\n\n for (const path of exclude) {\n if (path === 'config' || PROTECTED_FIELDS.has(path)) {\n throw new Error(`More source custom body cannot exclude protected field '${path}'`);\n }\n const field = googleConfigField(path);\n assertGoogleConfigFieldAllowed(field, 'exclude');\n delete config[field];\n }\n\n for (const [name, value] of Object.entries(include)) {\n if (name !== 'config') {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(\n `More source custom body cannot override protected field '${name}'`\n );\n }\n throw new Error(\n `More source customIncludeBody for Google must place '${name}' inside the 'config' object`\n );\n }\n if (!isPlainObject(value)) {\n throw new Error(\n \"More source customIncludeBody for Google requires 'config' to be an object\"\n );\n }\n for (const [field, field_value] of Object.entries(value)) {\n assertGoogleConfigFieldAllowed(field, 'override');\n config[field] = structuredClone(field_value);\n }\n }\n\n return { ...payload, config };\n}\n\n/** 按协议写入原生 JSON Schema 或 JSON 对象输出配置,不支持的组合直接报错。 */\nfunction applyNativeStructuredOutput(\n payload: Record,\n api: Api,\n response_format: PiStructuredResponseFormat,\n schema: PiJsonSchema | undefined\n): Record {\n if (response_format === '格式化输出' && !schema) {\n throw new Error('More source structured output requires a JSON schema');\n }\n\n const is_json_schema = response_format === '格式化输出';\n if (api === 'openai-completions') {\n return {\n ...payload,\n response_format: is_json_schema\n ? {\n type: 'json_schema',\n json_schema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n if (api === 'openai-responses' || api === 'openai-codex-responses') {\n const existing_text = isPlainObject(payload.text) ? payload.text : {};\n return {\n ...payload,\n text: {\n ...existing_text,\n format: is_json_schema\n ? {\n type: 'json_schema',\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n }\n : { type: 'json_object' },\n },\n };\n }\n\n if (api === 'anthropic-messages') {\n if (!is_json_schema) {\n throw new Error(`More source API '${api}' does not support native JSON-object output`);\n }\n const existing_output_config = isPlainObject(payload.output_config)\n ? payload.output_config\n : {};\n return {\n ...payload,\n output_config: {\n ...existing_output_config,\n format: {\n type: 'json_schema',\n schema: schema!.value,\n },\n },\n };\n }\n\n if (api === 'google-generative-ai') {\n if (!is_json_schema) {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n },\n };\n }\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n responseJsonSchema: schema!.value,\n },\n };\n }\n\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n responseFormat: is_json_schema\n ? {\n type: 'json_schema',\n jsonSchema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schemaDefinition: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n throw new Error(`More source API '${api}' does not support native structured output`);\n}\n\n/** 将统一采样选项转换为各协议的字段名称和嵌套位置。 */\nfunction applyApiSampling(\n payload: Record,\n api: Api,\n sampling: PiPayloadTransformOptions['sampling']\n): Record {\n if (!sampling) {\n return payload;\n }\n if (api === 'openai-completions') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequency_penalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presence_penalty: sampling.presencePenalty }),\n };\n }\n if (api === 'openai-responses') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n };\n }\n if (api === 'anthropic-messages') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { top_k: sampling.topK }),\n };\n }\n if (api === 'google-generative-ai') {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { topK: sampling.topK }),\n },\n };\n }\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequencyPenalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presencePenalty: sampling.presencePenalty }),\n };\n }\n return payload;\n}\n\n/** 统一应用请求覆盖、采样和原生应答格式,并保留协议核心字段的控制权。 */\nexport function transformPiPayload(payload: unknown, options: PiPayloadTransformOptions): unknown {\n if (!isPlainObject(payload)) {\n throw new Error('More source provider payload must be an object');\n }\n let result = applyApiSampling(payload, options.api, options.sampling);\n result =\n options.api === 'google-generative-ai'\n ? applyGoogleCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n )\n : applyCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n );\n if (options.responseFormat) {\n result = applyNativeStructuredOutput(\n result,\n options.api,\n options.responseFormat,\n options.jsonSchema\n );\n }\n return result;\n}\n\n/** 将本轮转换选项绑定为 Pi 的 onPayload 回调。 */\nexport function createPiPayloadTransform(options: PiPayloadTransformOptions) {\n return (payload: unknown) => transformPiPayload(payload, options);\n}\n","import type { AssistantMessage, Tool, ToolCall } from '@earendil-works/pi-ai';\n\n/** 模型输出无法交回原有变量更新流程的原因,区分取消、截断和无效结果。 */\nexport type PiResultAdapterErrorCode =\n | 'aborted'\n | 'deferred'\n | 'empty-response'\n | 'invalid-tool-call'\n | 'length'\n | 'network'\n | 'provider-error';\n\nexport class PiResultAdapterError extends Error {\n /** 保留结果转换错误码,供运行时决定取消、重试及界面提示。 */\n constructor(\n message: string,\n readonly code: PiResultAdapterErrorCode\n ) {\n super(message);\n this.name = 'PiResultAdapterError';\n }\n}\n\n/** 将酒馆函数工具转为 Pi Tool 时附加的协议选项。 */\nexport type PiToolDefinitionOptions = {\n /** 服务商端的 Schema 或语法约束配置;false 显式关闭约束采样。 */\n constrainedSampling?: Tool['constrainedSampling'];\n};\n\n/** 仅接受普通或空原型对象,作为工具参数与 Schema 的结构边界。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 复制工具 Schema,无法复制时转换为工具定义错误,避免共享结构被修改。 */\nfunction cloneSchema(schema: Record): Record {\n try {\n return structuredClone(schema);\n } catch (cause) {\n throw new PiResultAdapterError(\n `工具参数 schema 无法复制:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n}\n\n/** 校验函数工具名称和对象 Schema,再转换为 Pi 工具定义及约束采样配置。 */\nexport function toPiToolDefinition(\n definition: ToolDefinition,\n options: PiToolDefinitionOptions = {}\n): Tool {\n if (definition?.type !== 'function' || !definition.function?.name?.trim()) {\n throw new PiResultAdapterError('工具定义缺少有效的 function name', 'invalid-tool-call');\n }\n\n const parameters = definition.function.parameters ?? {\n type: 'object',\n properties: {},\n };\n if (!isPlainObject(parameters) || parameters.type !== 'object') {\n throw new PiResultAdapterError(\n '工具参数 schema 的根节点必须是 object',\n 'invalid-tool-call'\n );\n }\n\n return {\n name: definition.function.name,\n description: definition.function.description ?? '',\n parameters: cloneSchema(parameters) as Tool['parameters'],\n ...(options.constrainedSampling === undefined\n ? {}\n : { constrainedSampling: options.constrainedSampling }),\n };\n}\n\nexport const toPiTool = toPiToolDefinition;\n\n/** 把 Pi 工具调用转换为酒馆结果结构,序列化参数并保留思考签名。 */\nfunction normalizeToolCall(call: ToolCall): GenerateToolCallResult['tool_calls'][number] {\n if (!call.id?.trim() || !call.name?.trim() || !isPlainObject(call.arguments)) {\n throw new PiResultAdapterError('更多来源返回了无效的工具调用', 'invalid-tool-call');\n }\n\n let argumentsValue: string;\n try {\n argumentsValue = JSON.stringify(call.arguments);\n } catch (cause) {\n throw new PiResultAdapterError(\n `更多来源工具参数无法序列化:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: argumentsValue,\n },\n ...(call.thoughtSignature ? { thought_signature: call.thoughtSignature } : {}),\n };\n}\n\n/** 只允许完整文本或工具调用结果继续转换,分别拒绝截断、取消、延迟和服务商失败。 */\nfunction assertSuccessfulStopReason(message: AssistantMessage): void {\n switch (message.stopReason) {\n case 'stop':\n case 'toolUse':\n return;\n case 'length':\n throw new PiResultAdapterError('更多来源回复因达到长度上限而被截断', 'length');\n case 'aborted':\n throw new PiResultAdapterError('更多来源请求已取消', 'aborted');\n case 'error': {\n // Provider errorMessage values are untrusted response data. They have been observed to\n // contain echoed Authorization headers/API keys, so retain only a coarse classification.\n const isNetworkError =\n typeof message.errorMessage === 'string' &&\n /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message.errorMessage\n );\n throw new PiResultAdapterError(\n isNetworkError\n ? 'The browser could not complete the More source request.'\n : 'More source request failed.',\n isNetworkError ? 'network' : 'provider-error'\n );\n }\n case 'pending':\n case 'deferred':\n throw new PiResultAdapterError(\n '更多来源回复尚未完成,当前不支持 deferred 结果',\n 'deferred'\n );\n default: {\n const exhaustive: never = message.stopReason;\n void exhaustive;\n throw new PiResultAdapterError(\n 'More source provider returned an unknown stop reason.',\n 'provider-error'\n );\n }\n }\n}\n\n/**\n * 提取助手文本及工具调用并返回 MVU 可消费的结果。\n * 空响应、仅有思考或工具结束却没有调用都视为协议失败,不写入聊天上下文。\n */\nexport function fromPiAssistantMessage(message: AssistantMessage): string | GenerateToolCallResult {\n assertSuccessfulStopReason(message);\n\n const text = message.content\n .filter(content => content.type === 'text')\n .map(content => content.text)\n .join('');\n const toolCalls = message.content\n .filter((content): content is ToolCall => content.type === 'toolCall')\n .map(normalizeToolCall);\n\n if (message.stopReason === 'toolUse' && toolCalls.length === 0) {\n throw new PiResultAdapterError(\n '更多来源以 toolUse 结束,但没有返回工具调用',\n 'invalid-tool-call'\n );\n }\n if (toolCalls.length > 0) {\n return {\n content: text,\n tool_calls: toolCalls,\n };\n }\n if (text.length === 0) {\n const hasThinking = message.content.some(content => content.type === 'thinking');\n throw new PiResultAdapterError(\n hasThinking ? '更多来源回复仅包含 thinking,没有业务内容' : '更多来源返回了空回复',\n 'empty-response'\n );\n }\n return text;\n}\n","import type { Context, ImageContent, Message } from '@earendil-works/pi-ai';\nimport {\n getPiImageMetadata,\n PI_IMAGE_INPUT_LIMITS,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 发送前的保守预算结果,用于解释输入是否超出模型窗口,不代表计费 token 数。 */\nexport type PiTokenPreflightResult = {\n estimatedInputTokens: number;\n /** 额外预留的安全余量,不包含回复 token。 */\n reservedTokens: number;\n /** 上下文窗口扣除回复额度及安全余量后允许的输入上限。 */\n maxInputTokens: number;\n};\n\nconst MIN_IMAGE_TOKEN_ESTIMATE = 1024;\nconst ESTIMATED_IMAGE_BYTES_PER_TOKEN = 64;\n// Common multimodal APIs estimate image tokens from tiled/pixel area. Using a\n// lower pixels-per-token ratio leaves deliberate cross-provider headroom.\nconst ESTIMATED_IMAGE_PIXELS_PER_TOKEN = 500;\n\n/**\n * 按 UTF-8 字节保守估算文本 token,兼顾中文与非基本平面字符。\n * 这是发送前的预算保护,不用于精确计费。\n */\nfunction estimateTextTokens(value: string): number {\n if (!value) {\n return 0;\n }\n // This is a deliberately conservative boundary guard, not billing-grade\n // tokenization. UTF-8 bytes avoid undercounting CJK and astral characters\n // while ASCII/JSON remains cheaper than non-ASCII text.\n let ascii_bytes = 0;\n let non_ascii_bytes = 0;\n for (const character of value) {\n const code_point = character.codePointAt(0)!;\n if (code_point <= 0x7f) {\n ascii_bytes += 1;\n } else if (code_point <= 0x7ff) {\n non_ascii_bytes += 2;\n } else if (code_point <= 0xffff) {\n non_ascii_bytes += 3;\n } else {\n non_ascii_bytes += 4;\n }\n }\n return Math.ceil(ascii_bytes / 3 + non_ascii_bytes / 2);\n}\n\n/** 根据 Base64 长度与填充估算解码字节数,避免为预算检查复制或解码整张图片。 */\nfunction estimateDecodedBase64Bytes(data: string): number {\n if (!data) {\n return 0;\n }\n // Adapter-produced blocks are canonical base64. Counting whitespace in an\n // external block overestimates safely and avoids an image-sized copy.\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding);\n}\n\n/** 优先使用上下文转换时记录的图片大小,外部内容块再按 Base64 长度估算。 */\nfunction getEstimatedImageBytes(image: ImageContent): number {\n return getPiImageMetadata(image)?.decodedBytes ?? estimateDecodedBase64Bytes(image.data);\n}\n\n/** 取图片最小预算、字节估算和像素面积估算中的最大值,保留跨服务商余量。 */\nfunction estimateImageTokens(image: ImageContent): number {\n const metadata = getPiImageMetadata(image);\n const estimated_bytes = getEstimatedImageBytes(image);\n const dimensions = metadata?.dimensions;\n\n let pixel_tokens = 0;\n if (\n dimensions !== undefined &&\n Number.isSafeInteger(dimensions.width) &&\n dimensions.width > 0 &&\n Number.isSafeInteger(dimensions.height) &&\n dimensions.height > 0\n ) {\n pixel_tokens = Math.ceil(\n (dimensions.width * dimensions.height) / ESTIMATED_IMAGE_PIXELS_PER_TOKEN\n );\n }\n\n return Math.max(\n MIN_IMAGE_TOKEN_ESTIMATE,\n Math.ceil(estimated_bytes / ESTIMATED_IMAGE_BYTES_PER_TOKEN),\n pixel_tokens\n );\n}\n\n/** 检查单图、总图片数量和累计解码大小,即使外部绕过上下文转换也执行预算限制。 */\nfunction assertPiImageInputBudget(context: Context): void {\n let image_count = 0;\n let decoded_bytes = 0;\n /** 累计单个图片块并立即检查各项图片限制,超限时停止遍历。 */\n const countImage = (image: ImageContent): void => {\n const image_bytes = getEstimatedImageBytes(image);\n if (image_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new Error('More source image exceeds the per-image input limit');\n }\n image_count += 1;\n decoded_bytes += image_bytes;\n if (image_count > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new Error('More source image count exceeds the per-context input limit');\n }\n if (decoded_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new Error('More source image total exceeds the per-context input limit');\n }\n };\n\n for (const message of context.messages) {\n if (message.role === 'user' && Array.isArray(message.content)) {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n } else if (message.role === 'toolResult') {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n }\n }\n}\n\n/** 按消息角色估算文本、思考、工具参数和图片,并计入消息结构开销。 */\nfunction estimateMessageTokens(message: Message): number {\n let tokens = 6;\n if (message.role === 'system') {\n tokens += estimateTextTokens(\n typeof message.content === 'string'\n ? message.content\n : message.content.map(block => block.text).join('')\n );\n for (const value of [message.sections, message.toolsAdded, message.toolsRemoved]) {\n if (value) tokens += estimateTextTokens(JSON.stringify(value));\n }\n return tokens;\n }\n if (message.role === 'user') {\n if (typeof message.content === 'string') {\n return tokens + estimateTextTokens(message.content);\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens;\n }\n if (message.role === 'assistant') {\n for (const block of message.content) {\n if (block.type === 'text') {\n tokens += estimateTextTokens(block.text);\n } else if (block.type === 'thinking') {\n tokens += estimateTextTokens(block.thinking);\n } else {\n tokens +=\n estimateTextTokens(block.name) +\n estimateTextTokens(JSON.stringify(block.arguments));\n }\n }\n return tokens;\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens + estimateTextTokens(message.toolName);\n}\n\n/** 汇总系统提示词、历史消息和工具定义的输入 token 估算。 */\nexport function estimatePiContextTokens(\n context: Context,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): number {\n let tokens = estimateTextTokens(context.systemPrompt ?? '');\n for (const message of context.messages) {\n tokens += estimateMessageTokens(message);\n }\n if (context.tools?.length) {\n tokens += estimateTextTokens(JSON.stringify(context.tools));\n }\n // 后置 system 尚未写入 Pi Context,但最终请求会恢复其文本和独立消息开销。\n for (const message of lateSystemMessages) {\n tokens += estimateTextTokens(message.text) + 8;\n }\n return Math.max(1, tokens);\n}\n\n/**\n * 预留回复 token 和安全余量后检查完整输入预算,不在此截断提示词。\n * 同时校验图片限制;超限错误携带估算明细,供界面解释原因。\n */\nexport function assertPiTokenBudget(\n context: Context,\n context_window: number,\n max_tokens: number,\n reserve_ratio = 0.07,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): PiTokenPreflightResult {\n if (!Number.isInteger(context_window) || context_window <= 0) {\n throw new Error('More source contextWindow must be a positive integer');\n }\n if (!Number.isInteger(max_tokens) || max_tokens <= 0) {\n throw new Error('More source maxTokens must be a positive integer');\n }\n if (max_tokens > context_window) {\n throw new Error('More source maxTokens must not exceed contextWindow');\n }\n assertPiImageInputBudget(context);\n\n const normalized_reserve_ratio = Math.min(0.1, Math.max(0.05, reserve_ratio));\n const reserved_tokens = Math.ceil(context_window * normalized_reserve_ratio);\n const max_input_tokens = context_window - max_tokens - reserved_tokens;\n const estimated_input_tokens = estimatePiContextTokens(context, lateSystemMessages);\n if (max_input_tokens <= 0 || estimated_input_tokens > max_input_tokens) {\n throw new Error(\n `More source prompt is too long: estimated ${estimated_input_tokens} input tokens, ` +\n `limit ${Math.max(0, max_input_tokens)} after reserving ${max_tokens} reply tokens ` +\n `and ${reserved_tokens} safety tokens`\n );\n }\n\n return {\n estimatedInputTokens: estimated_input_tokens,\n reservedTokens: reserved_tokens,\n maxInputTokens: max_input_tokens,\n };\n}\n","import type { Api, Context, Message, TextContent } from './pi_gateway';\nimport {\n PiContextAdapterError,\n type PiContextAdapterResult,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 一次请求的 system 恢复桥接;context 仅供 Pi 构造请求,restore 必须用于 onPayload。 */\nexport interface PiSystemMessageBridge {\n context: Context;\n restore: (payload: unknown) => unknown;\n /** Pi 会把 hook 异常转为普通服务商错误,保留原始分类供运行时取回。 */\n readonly failure: PiContextAdapterError | undefined;\n assertRestored: () => void;\n}\n\n/** 同一原始位置的一组 system,锚定到下一条实际消息,不额外伪造对话回合。 */\ntype SystemAnchor = {\n marker: string;\n messages: readonly PiLateSystemMessage[];\n};\n\n/** 对服务商 JSON 节点移除锚点后的结果;remove 仅用于锚点独占的临时文本块。 */\ntype CleanedNode = { value: unknown; anchors: SystemAnchor[]; remove?: boolean };\n\nlet nextAnchorNamespace = 0;\n\n/** 选用原始内容中不存在的请求级标记,防止用户文本被误识别为内部锚点。 */\nfunction containsMarker(value: unknown, prefix: string): boolean {\n if (typeof value === 'string') return value.includes(prefix);\n if (Array.isArray(value)) return value.some(item => containsMarker(item, prefix));\n if (value && typeof value === 'object') {\n return Object.values(value).some(item => containsMarker(item, prefix));\n }\n return false;\n}\n\n/** 在已有消息的首个文本块前加锚点;保留消息角色,避免打断 Pi 的工具调用关联。 */\nfunction anchorMessage(message: Message, marker: string, api: Api): Message {\n if (typeof message.content === 'string') {\n return { ...message, role: 'user', content: marker + message.content };\n }\n const content = [...message.content];\n const index = content.findIndex(block => block.type === 'text');\n let fallback = '';\n if (\n message.role === 'toolResult' &&\n !message.content\n .filter((block): block is TextContent => block.type === 'text')\n .map(block => block.text)\n .join('\\n')\n ) {\n const hasImages = message.content.some(block => block.type === 'image');\n // 锚点不能抑制 SDK 原本为无文本工具结果生成的占位说明。\n if (api === 'openai-completions' || api === 'mistral-conversations') {\n fallback = hasImages ? '(see attached image)' : '(no tool output)';\n } else if (!hasImages && ['openai-responses', 'openai-codex-responses'].includes(api)) {\n fallback = '(no tool output)';\n }\n }\n if (index === -1) {\n content.unshift({ type: 'text', text: marker + fallback });\n } else {\n const block = content[index] as TextContent;\n content[index] = { ...block, text: marker + block.text + fallback };\n }\n return { ...message, content } as Message;\n}\n\n/** 按实际协议和消息位置自动选择角色,不暴露额外的渠道配置。 */\nfunction systemMessageRole(\n context: Context,\n api: Api,\n message: PiLateSystemMessage\n): 'system' | 'user' {\n if (api === 'google-generative-ai') return 'user';\n if (api === 'anthropic-messages') {\n const previous = context.messages[message.beforeMessageIndex - 1];\n const next = context.messages[message.beforeMessageIndex];\n // toolResult 在 Anthropic 协议里是携带工具结果的 user 回合。\n return (previous?.role === 'user' || previous?.role === 'toolResult') &&\n (!next || next.role === 'assistant')\n ? 'system'\n : 'user';\n }\n return 'system';\n}\n\n/**\n * 保留普通消息角色,在服务商完成拆分/合并之后按内容锚点恢复 system;不符合协议约束时转为 user。\n * 锚点只进入待转换的文本,发送前必须恰好移除一次;不修改捕获输入、共享上下文或全局 fetch。\n */\nexport function createPiSystemMessageBridge(\n adapted: PiContextAdapterResult,\n api: Api\n): PiSystemMessageBridge {\n const { context, lateSystemMessages } = adapted;\n if (!lateSystemMessages.length) {\n return {\n context,\n restore: payload => payload,\n failure: undefined,\n assertRestored: () => {},\n };\n }\n\n let prefix: string;\n do {\n prefix = `__mvu_pi_system_anchor_${++nextAnchorNamespace}_`;\n } while (containsMarker(adapted, prefix));\n\n const groups = new Map();\n for (const message of lateSystemMessages) {\n const group = groups.get(message.beforeMessageIndex) ?? [];\n group.push(message);\n groups.set(message.beforeMessageIndex, group);\n }\n const anchors: SystemAnchor[] = [];\n const messages = context.messages.map((message, index) => {\n const group = groups.get(index);\n if (!group) return message;\n const anchor = { marker: `${prefix}${index}__`, messages: group };\n anchors.push(anchor);\n return anchorMessage(message, anchor.marker, api);\n });\n const tail = groups.get(context.messages.length) ?? [];\n const field =\n api === 'google-generative-ai'\n ? 'contents'\n : api === 'openai-responses' || api === 'openai-codex-responses'\n ? 'input'\n : 'messages';\n const hasUserFallback = lateSystemMessages.some(\n message => systemMessageRole(context, api, message) === 'user'\n );\n let failure: PiContextAdapterError | undefined;\n let applied = false;\n const restoredPayloads = new WeakSet();\n\n const mismatch = () =>\n new PiContextAdapterError(\n 'More source could not restore intermediate system messages in the provider payload.',\n 'system-payload-mismatch',\n lateSystemMessages[0].sourceIndex\n );\n const nativeMessages = (group: readonly PiLateSystemMessage[]) =>\n group.map(message =>\n api === 'google-generative-ai'\n ? { role: 'user', parts: [{ text: message.text }] }\n : { role: systemMessageRole(context, api, message), content: message.text }\n );\n\n /** 工具结果可能被 SDK 合并进同一 user;按工具结果块拆出插入边界,避免把指令提前到整组结果前。 */\n const splitToolResultGroups = (items: unknown[]): unknown[] =>\n items.flatMap(item => {\n if (!item || typeof item !== 'object' || !hasUserFallback) return [item];\n const message = item as Record;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const blocks = message[key];\n if (message.role !== 'user' || !Array.isArray(blocks)) return [item];\n const result: unknown[] = [];\n let current: unknown[] = [];\n for (const block of blocks) {\n const isToolResult =\n block &&\n typeof block === 'object' &&\n (block.type === 'tool_result' || block.functionResponse);\n if (isToolResult && current.length && containsMarker(block, prefix)) {\n result.push({ ...message, [key]: current });\n current = [];\n }\n current.push(block);\n }\n result.push({ ...message, [key]: current });\n return result;\n });\n\n /** 与酒馆兼容处理一致,user 回退后合并相邻 user,同时保留内容块、图片及工具结果的顺序。 */\n const mergeFallbackUsers = (items: unknown[]): unknown[] => {\n if (!hasUserFallback || (api !== 'anthropic-messages' && api !== 'google-generative-ai'))\n return items;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const asBlocks = (value: unknown) =>\n typeof value === 'string' ? [{ type: 'text', text: value }] : value;\n const merged: unknown[] = [];\n for (const item of items) {\n const current = item as Record;\n const previous = merged.at(-1) as Record | undefined;\n if (current?.role === 'user' && previous?.role === 'user') {\n const before = asBlocks(previous[key]);\n const after = asBlocks(current[key]);\n if (Array.isArray(before) && Array.isArray(after)) {\n merged[merged.length - 1] = { ...previous, [key]: [...before, ...after] };\n continue;\n }\n }\n merged.push(item);\n }\n return merged;\n };\n\n /** 只遍历原生消息字段;完整保留工具 ID、图片、缓存标记和其他服务商元数据。 */\n const clean = (value: unknown): CleanedNode => {\n if (typeof value === 'string') {\n if (!value.includes(prefix)) return { value, anchors: [] };\n const found: SystemAnchor[] = [];\n let text = value;\n for (const anchor of anchors) {\n const parts = text.split(anchor.marker);\n for (let index = 1; index < parts.length; index++) found.push(anchor);\n text = parts.join('');\n }\n return { value: text, anchors: found };\n }\n if (Array.isArray(value)) {\n const children = value.map(clean);\n return {\n value: children.filter(child => !child.remove).map(child => child.value),\n anchors: children.flatMap(child => child.anchors),\n };\n }\n if (!value || typeof value !== 'object') return { value, anchors: [] };\n const entries = Object.entries(value).map(([key, item]) => [key, clean(item)] as const);\n const found = entries.flatMap(([, child]) => child.anchors);\n if (!found.length) return { value, anchors: [] };\n const result = Object.fromEntries(entries.map(([key, child]) => [key, child.value]));\n const emptyAnchorBlock =\n result.text === '' &&\n (api === 'google-generative-ai' ||\n (typeof result.type === 'string' &&\n ['text', 'input_text', 'output_text'].includes(result.type)));\n // Responses 会把仅含工具调用的 assistant 上新增的锚点文本拆成单独消息,恢复后移除它。\n const emptyAnchorMessage =\n result.type === 'message' &&\n Array.isArray(result.content) &&\n result.content.length === 0;\n // Mistral 的纯工具助手消息省略 content,不能把临时定位块变成空内容数组。\n if (\n api === 'mistral-conversations' &&\n result.role === 'assistant' &&\n Array.isArray(result.content) &&\n !result.content.length\n ) {\n delete result.content;\n }\n return { value: result, anchors: found, remove: emptyAnchorBlock || emptyAnchorMessage };\n };\n\n return {\n context: { ...context, messages },\n get failure() {\n return failure;\n },\n restore(payload) {\n try {\n if (!payload || typeof payload !== 'object') throw mismatch();\n if (restoredPayloads.has(payload)) return payload;\n const original = payload as Record;\n const items = original[field];\n if (!Array.isArray(items)) throw mismatch();\n const seen = new Set();\n const restored: unknown[] = [];\n for (const item of splitToolResultGroups(items)) {\n const result = clean(item);\n if (result.anchors.length > 1) throw mismatch();\n const anchor = result.anchors[0];\n if (anchor) {\n if (seen.has(anchor) || anchors[seen.size] !== anchor) throw mismatch();\n seen.add(anchor);\n restored.push(...nativeMessages(anchor.messages));\n }\n if (!result.remove) restored.push(result.value);\n }\n if (seen.size !== anchors.length) throw mismatch();\n restored.push(...nativeMessages(tail));\n const result = { ...original, [field]: mergeFallbackUsers(restored) };\n if (containsMarker(result, prefix)) throw mismatch();\n restoredPayloads.add(result);\n applied = true;\n return result;\n } catch (error) {\n failure = error instanceof PiContextAdapterError ? error : mismatch();\n throw failure;\n }\n },\n assertRestored() {\n if (failure) throw failure;\n if (!applied) throw mismatch();\n },\n };\n}\n","import {\n normalizePiThinkingLevel,\n isPiThinkingEnabled,\n type PiThinkingLevel,\n} from './thinking_setting';\nimport { preparePiThinkingModel, resolvePiThinkingLevel } from './thinking';\nimport type { MvuSettings } from '@/store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n parsePiCustomExcludeBody,\n parsePiCustomHeaders,\n parsePiCustomIncludeBody,\n} from './config_parser';\nimport { PiContextAdapterError, toPiContext } from './context_adapter';\nimport { PiRequestAbortedError, registerPiRequestController } from './controller_registry';\nimport { getPiCredentialStore } from './credential_store';\nimport { assertGoogleProxyAdapterCompatible } from './google_proxy_adapter';\nimport {\n isPiDefaultProviderEndpoint,\n PiModelResolutionError,\n resolvePiModelFromExtraModelSettings,\n type ResolvedPiModel,\n} from './model_resolver';\nimport { getBrowserOAuthAuth } from './oauth';\nimport { createPiNonStreamingFetch } from './non_streaming_fetch';\nimport { isPiStreamingRequired } from './provider_target';\nimport { createPiPayloadTransform, transformPiPayload, type PiJsonSchema } from './payload';\nimport {\n createModels,\n createProvider,\n type Api,\n type ApiStreamOptions,\n type AssistantMessageEvent,\n type AuthContext,\n type CredentialStore,\n type FetchFunction,\n type SimpleStreamOptions,\n type ProviderAuth,\n type ProviderHeaders,\n type Tool,\n} from './pi_gateway';\nimport {\n createPiApiImplementations,\n resolvePiCapabilities,\n shouldUsePiCorsProxy,\n type PiApiCapabilities,\n type PiWireApi,\n} from './provider_registry';\nimport { fromPiAssistantMessage, PiResultAdapterError, toPiToolDefinition } from './result_adapter';\nimport { assertPiTokenBudget } from './token_preflight';\nimport { resolvePiToolChoice, type MvuToolChoice } from './tool_choice';\nimport { createPiSystemMessageBridge, type PiSystemMessageBridge } from './system_messages';\nimport {\n assertSillyTavernProxyAvailable,\n createSillyTavernProxyFetch,\n getSillyTavernProxyStatus,\n PiProxyUnavailableError,\n} from './sillytavern_proxy';\n\ninstallPiAbortSignalPolyfills();\n\nexport const PI_RUNTIME_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\n/** 额外解析允许使用的应答格式,供预检和原生载荷转换共同约束。 */\nexport type PiRuntimeResponseFormat = (typeof PI_RUNTIME_RESPONSE_FORMATS)[number];\n/** 一次额外模型请求读取的完整设置结构。 */\nexport type PiExtraModelSettings = MvuSettings['额外模型解析配置'];\n\n/** 运行时失败分类;配合 retryable 决定请求策略与界面提示。 */\nexport type PiRuntimeErrorCode =\n | 'invalid_configuration'\n | 'invalid_prompt'\n | 'missing_oauth_credential'\n | 'request_already_active'\n | 'unsupported_capability'\n | 'unsupported_image_input'\n | 'token_budget'\n | 'proxy_unavailable'\n | 'network'\n | 'provider'\n | 'protocol';\n\nexport class PiRuntimeError extends Error {\n /** 为运行时错误保存稳定类别和重试标记,供额外模型策略统一决策。 */\n constructor(\n readonly code: PiRuntimeErrorCode,\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'PiRuntimeError';\n }\n}\n\n/** 为串行和并发策略提供统一的不可重试错误判断。 */\nexport function isNonRetryablePiRuntimeError(error: unknown): boolean {\n return (\n (error instanceof PiRuntimeError && !error.retryable) ||\n error instanceof PiModelResolutionError ||\n error instanceof PiContextAdapterError\n );\n}\n\n/** 已通过服务商能力校验的采样选项,不含独立传入 SDK 的 temperature。 */\nexport type PiRuntimeSampling = Readonly<{\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n}>;\n\n/** 在重试批次开始前进行静态预检所需的设置、工具和可注入依赖。 */\nexport interface AssertPiRuntimeConfigurationInput {\n /** The whole persisted `额外模型解析配置` object. */\n settings: unknown;\n /** Defaults to `settings.应答格式`. */\n responseFormat?: unknown;\n /** Required by `格式化输出`; ignored by non-structured response modes. */\n jsonSchema?: PiJsonSchema;\n /** Required by `工具调用`; the MVU ToolDefinition constants can be passed directly. */\n tools?: readonly ToolDefinition[];\n /** Defaults to `required` in tool mode. */\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n signal?: AbortSignal;\n}\n\n/**\n * 重试前解析并冻结的连接、认证、能力与载荷配置。\n * 不包含提示词或请求级 system 锚点,可在同批次的每次尝试中复用。\n */\nexport interface PiRuntimePreflight {\n readonly resolution: ResolvedPiModel;\n readonly responseFormat: PiRuntimeResponseFormat;\n readonly capabilities: Readonly;\n readonly headers?: Readonly;\n readonly customIncludeBody?: Readonly>;\n readonly customExcludeBody?: readonly string[];\n readonly temperature?: number;\n readonly sampling: PiRuntimeSampling;\n readonly tools?: readonly Tool[];\n readonly toolChoice?: unknown;\n readonly jsonSchema?: PiJsonSchema;\n readonly credentialStore: CredentialStore;\n readonly fetch?: FetchFunction;\n readonly useCorsProxy: boolean;\n readonly streaming: boolean;\n readonly thinkingLevel: PiThinkingLevel;\n}\n\n/** 可选的 Pi 进度监听,运行时不会向此回调转发未经处理的错误事件。 */\nexport type PiRuntimeProgressCallback = (event: AssistantMessageEvent) => void | Promise;\n\n/** 执行单次请求的输入:预检快照、捕获消息、取消编号及可选进度监听。 */\nexport interface RunPiRequestInput {\n /** Supply this when configuration was validated once outside the retry loop. */\n preflight?: PiRuntimePreflight;\n /** Required only when `preflight` is omitted. */\n settings?: unknown;\n responseFormat?: unknown;\n jsonSchema?: PiJsonSchema;\n tools?: readonly ToolDefinition[];\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n messages: readonly SillyTavern.SendingMessage[];\n generationId: string;\n signal?: AbortSignal;\n onProgress?: PiRuntimeProgressCallback;\n}\n\n/** 设置根对象完成形状检查后、各字段尚未解析时的中间结构。 */\ntype ExtraModelSettingsRecord = Record & {\n pi?: unknown;\n};\n\nconst BROWSER_AUTH_CONTEXT: AuthContext = Object.freeze({\n env: async () => undefined,\n fileExists: async () => false,\n});\n\n/** 确认设置是非空、非数组对象,再读取请求字段。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 校验额外模型设置的根结构,结构错误在构造请求前失败。 */\nfunction requireSettings(value: unknown): ExtraModelSettingsRecord {\n if (!isPlainObject(value)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source extra-model settings must be an object'\n );\n }\n return value;\n}\n\n/** 优先采用本次显式应答格式,否则读取设置,并拒绝未知格式。 */\nfunction resolveResponseFormat(\n settings: ExtraModelSettingsRecord,\n override: unknown\n): PiRuntimeResponseFormat {\n const value = override ?? settings['应答格式'];\n if (\n typeof value !== 'string' ||\n !PI_RUNTIME_RESPONSE_FORMATS.some(candidate => candidate === value)\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source response format is missing or unsupported'\n );\n }\n return value as PiRuntimeResponseFormat;\n}\n\n/** 读取可选字符串配置;缺失返回空字符串,类型错误不能隐式转换。 */\nfunction optionalStringField(source: Record, name: string): string {\n const value = source[name];\n if (value === undefined) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new PiRuntimeError('invalid_configuration', `More source ${name} must be a string`);\n }\n return value;\n}\n\n/** 校验可选数值的范围、有限性及整数要求,缺失字段保持未设置。 */\nfunction optionalNumberField(\n source: Record,\n name: string,\n minimum: number,\n maximum: number,\n integer = false\n): number | undefined {\n const value = source[name];\n if (value === undefined) {\n return undefined;\n }\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n value < minimum ||\n value > maximum ||\n (integer && !Number.isInteger(value))\n ) {\n const kind = integer ? 'integer' : 'number';\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source ${name} must be a ${kind} between ${minimum} and ${maximum}`\n );\n }\n return value;\n}\n\n/** 仅解析当前模型支持的采样字段,并处理协议或模型特有的组合限制。 */\nfunction resolveSampling(\n settings: ExtraModelSettingsRecord,\n capabilities: Readonly,\n resolution: ResolvedPiModel\n): {\n temperature?: number;\n sampling: PiRuntimeSampling;\n} {\n let temperature = capabilities.temperature\n ? optionalNumberField(\n settings,\n '温度',\n capabilities.temperatureRange[0],\n capabilities.temperatureRange[1]\n )\n : undefined;\n let topP = capabilities.sampling.topP\n ? optionalNumberField(settings, 'top_p', 0, 1)\n : undefined;\n const topK = capabilities.sampling.topK\n ? optionalNumberField(settings, 'top_k', 0, 500, true)\n : undefined;\n const frequencyPenalty = capabilities.sampling.frequencyPenalty\n ? optionalNumberField(settings, '频率惩罚', -2, 2)\n : undefined;\n const presencePenalty = capabilities.sampling.presencePenalty\n ? optionalNumberField(settings, '存在惩罚', -2, 2)\n : undefined;\n if (\n resolution.definition.key === 'anthropic' &&\n isPiDefaultProviderEndpoint(resolution.definition, resolution.model.baseUrl) &&\n temperature !== undefined &&\n topP !== undefined\n ) {\n // Native Claude models require at most one of these samplers. MVU persists both\n // controls with 1 as their default, so omit the untouched one from the wire request.\n if (topP === 1) {\n topP = undefined;\n } else if (temperature === 1) {\n temperature = undefined;\n } else {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source Anthropic sampling requires temperature or top_p; reset one to 1.'\n );\n }\n }\n return {\n temperature,\n sampling: Object.freeze({\n ...(topP === undefined ? {} : { topP }),\n // zero is the existing \"unset\" value and is invalid for APIs that send top_k.\n ...(topK === undefined || topK === 0 ? {} : { topK }),\n ...(frequencyPenalty === undefined ? {} : { frequencyPenalty }),\n ...(presencePenalty === undefined ? {} : { presencePenalty }),\n }),\n };\n}\n\n/** 复制本次结构化输出 Schema,避免执行时修改调用方定义。 */\nfunction cloneJsonSchema(schema: PiJsonSchema | undefined): PiJsonSchema | undefined {\n return schema === undefined ? undefined : structuredClone(schema);\n}\n\n/** 解析当前模型的可信协议能力,注册信息缺失时终止配置预检。 */\nfunction capabilityFor(\n resolution: ResolvedPiModel,\n thinkingLevel: PiThinkingLevel\n): Readonly {\n const capability = resolvePiCapabilities(\n resolution.definition,\n resolution.model.api as PiWireApi,\n {\n model: resolution.model,\n catalogHit: resolution.catalogHit,\n thinkingLevel,\n }\n );\n if (!capability) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source API '${resolution.model.api}' has no registered runtime capability metadata`\n );\n }\n return capability;\n}\n\n/** 在捕获提示词前确认所选应答格式受到支持,避免重复发送必然失败的请求。 */\nfunction assertResponseCapability(\n responseFormat: PiRuntimeResponseFormat,\n capabilities: Readonly,\n api: Api\n): void {\n if (responseFormat === '工具调用' && !capabilities.tools) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support tool calls`\n );\n }\n const structuredOutputUnsupported =\n responseFormat === '格式化输出' && !capabilities.structuredOutput;\n const jsonObjectOutputUnsupported =\n responseFormat === '格式化输出(v4兼容)' && !capabilities.jsonObjectOutput;\n if (structuredOutputUnsupported || jsonObjectOutputUnsupported) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support native structured output`\n );\n }\n}\n\n/** 仅在工具调用模式校验并复制工具定义,其他模式不携带业务工具。 */\nfunction prepareTools(\n responseFormat: PiRuntimeResponseFormat,\n definitions: readonly ToolDefinition[] | undefined\n): readonly Tool[] | undefined {\n if (responseFormat !== '工具调用') {\n return undefined;\n }\n if (!definitions?.length) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source tool-call mode requires at least one tool definition'\n );\n }\n return Object.freeze(\n definitions.map(definition =>\n toPiToolDefinition(definition, {\n constrainedSampling: { type: 'json_schema', strict: 'prefer' },\n })\n )\n );\n}\n\n/** JSON Schema 模式要求显式结构定义,其他应答格式不继承残留 Schema。 */\nfunction prepareJsonSchema(\n responseFormat: PiRuntimeResponseFormat,\n schema: PiJsonSchema | undefined\n): PiJsonSchema | undefined {\n if (responseFormat === '格式化输出' && schema === undefined) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source structured output requires a JSON schema'\n );\n }\n return responseFormat === '格式化输出' ? cloneJsonSchema(schema) : undefined;\n}\n\n/** 用空载荷预演请求转换,提前发现受保护字段覆盖及格式配置错误。 */\nfunction validatePayloadConfiguration(preflight: {\n resolution: ResolvedPiModel;\n responseFormat: PiRuntimeResponseFormat;\n customIncludeBody?: Readonly>;\n customExcludeBody?: readonly string[];\n sampling: PiRuntimeSampling;\n jsonSchema?: PiJsonSchema;\n}): void {\n try {\n transformPiPayload(\n {},\n {\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n }\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source payload configuration is invalid'\n );\n }\n}\n\n/** 确认 OAuth 来源已有可用类型的凭证;到期刷新由后续 Pi 认证流程处理。 */\nasync function assertOAuthCredential(\n resolution: ResolvedPiModel,\n credentialStore: CredentialStore,\n signal?: AbortSignal\n): Promise {\n if (resolution.authType !== 'oauth') {\n return;\n }\n signal?.throwIfAborted();\n const credential = await credentialStore.read(resolution.definition.providerId, { signal });\n signal?.throwIfAborted();\n if (!credential || credential.type !== 'oauth') {\n throw new PiRuntimeError(\n 'missing_oauth_credential',\n `More source provider '${resolution.definition.providerId}' is not logged in`\n );\n }\n}\n\n/**\n * 一次性校验不依赖提示词的连接、认证、能力、预算参数和请求覆盖,并冻结本轮快照。\n * 应在串行或并发重试前调用,避免把固定配置错误误判为可重试的服务商失败。\n */\nexport async function assertPiRuntimeConfiguration(\n input: AssertPiRuntimeConfigurationInput\n): Promise {\n input.signal?.throwIfAborted();\n const settings = requireSettings(input.settings);\n const resolution = resolvePiModelFromExtraModelSettings(settings);\n const responseFormat = resolveResponseFormat(settings, input.responseFormat);\n const requestedThinkingLevel = normalizePiThinkingLevel(\n isPlainObject(settings.pi) ? settings.pi.thinkingLevel : undefined\n );\n resolution.model = preparePiThinkingModel(\n resolution.model,\n resolution.catalogHit,\n requestedThinkingLevel\n );\n // Validation, sampling and dispatch must all use the same effective level.\n const thinkingLevel = resolvePiThinkingLevel(requestedThinkingLevel, resolution.model);\n const capabilities = capabilityFor(resolution, thinkingLevel);\n const streaming =\n settings['兼容假流式'] === true || isPiStreamingRequired(resolution.model.api);\n if (streaming && !capabilities.streaming) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source model '${resolution.model.id}' does not support streaming requests`\n );\n }\n assertResponseCapability(responseFormat, capabilities, resolution.model.api);\n\n const piSettings = settings.pi;\n if (!isPlainObject(piSettings)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source connection settings must be an object'\n );\n }\n const useCorsProxy = shouldUsePiCorsProxy(\n resolution.definition,\n resolution.model.api,\n optionalStringField(piSettings, 'endpoint'),\n piSettings.useProxy\n );\n let headers: ProviderHeaders | undefined;\n let customIncludeBody: Record | undefined;\n let customExcludeBody: string[] | undefined;\n try {\n headers = parsePiCustomHeaders(optionalStringField(piSettings, 'customHeaders'));\n customIncludeBody = parsePiCustomIncludeBody(\n optionalStringField(piSettings, 'customIncludeBody')\n );\n customExcludeBody = parsePiCustomExcludeBody(\n optionalStringField(piSettings, 'customExcludeBody')\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error\n ? error.message\n : 'More source custom request configuration is invalid'\n );\n }\n const { temperature, sampling } = resolveSampling(settings, capabilities, resolution);\n let tools: readonly Tool[] | undefined;\n let toolChoice: unknown;\n try {\n tools = prepareTools(responseFormat, input.tools);\n toolChoice =\n responseFormat === '工具调用'\n ? resolvePiToolChoice(resolution.model.api, input.toolChoice ?? 'required')\n : undefined;\n } catch (error) {\n if (error instanceof PiRuntimeError) {\n throw error;\n }\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source tool configuration is invalid'\n );\n }\n if (isPiThinkingEnabled(thinkingLevel) && resolution.model.api === 'anthropic-messages') {\n if (toolChoice !== undefined && toolChoice !== 'auto' && toolChoice !== 'none') {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic thinking does not support forced tool calls. Use chat/structured output or turn thinking off.'\n );\n }\n if (\n !(\n resolution.model.compat &&\n 'forceAdaptiveThinking' in resolution.model.compat &&\n resolution.model.compat.forceAdaptiveThinking === true\n ) &&\n resolution.effectiveMaxTokens < 2048\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic budget-based thinking requires at least 2048 max reply tokens.'\n );\n }\n }\n const jsonSchema = prepareJsonSchema(responseFormat, input.jsonSchema);\n const credentialStore = input.credentialStore ?? getPiCredentialStore(piSettings);\n await assertOAuthCredential(resolution, credentialStore, input.signal);\n\n const preflight: PiRuntimePreflight = Object.freeze({\n resolution,\n responseFormat,\n capabilities,\n ...(headers === undefined ? {} : { headers: Object.freeze({ ...headers }) }),\n ...(customIncludeBody === undefined\n ? {}\n : { customIncludeBody: Object.freeze(structuredClone(customIncludeBody)) }),\n ...(customExcludeBody === undefined\n ? {}\n : { customExcludeBody: Object.freeze([...customExcludeBody]) }),\n temperature: capabilities.temperature ? temperature : undefined,\n sampling,\n tools,\n toolChoice,\n jsonSchema,\n credentialStore,\n fetch: input.fetch,\n useCorsProxy,\n streaming,\n thinkingLevel,\n });\n validatePayloadConfiguration(preflight);\n return preflight;\n}\n\n/** 只为本次解析出的模型和协议创建 Pi 服务商,并接入浏览器 OAuth 或请求密钥。 */\nfunction createRuntimeProvider(preflight: PiRuntimePreflight) {\n const { resolution } = preflight;\n let auth: ProviderAuth;\n if (resolution.authType === 'oauth') {\n auth = {\n oauth: getBrowserOAuthAuth(resolution.definition.providerId, {\n fetch: preflight.fetch,\n }),\n };\n } else {\n auth = {\n apiKey: {\n name: `${resolution.definition.displayName.en} API key`,\n /** 将本次设置提供的 API Key 转为 Pi 认证结果,解析前检查请求是否取消。 */\n async resolve({ credential, signal }) {\n signal.throwIfAborted();\n if (!credential?.key) {\n return undefined;\n }\n return {\n auth: { apiKey: credential.key },\n source: 'extra-model settings',\n };\n },\n },\n };\n }\n\n return createProvider({\n id: resolution.definition.providerId,\n name: resolution.definition.displayName.en,\n baseUrl: resolution.model.baseUrl,\n auth,\n models: [resolution.model],\n api: createPiApiImplementations(resolution.definition),\n });\n}\n\n/** 检查用户消息和工具返回值是否包含图片,供能力预检使用。 */\nfunction contextHasImages(context: ReturnType['context']): boolean {\n return context.messages.some(message => {\n if (message.role === 'user') {\n return (\n Array.isArray(message.content) &&\n message.content.some(content => content.type === 'image')\n );\n }\n if (message.role === 'toolResult') {\n return message.content.some(content => content.type === 'image');\n }\n return false;\n });\n}\n\n/** 提示词含图片时同时核对协议能力和模型输入类型,发送前拒绝不支持的组合。 */\nfunction assertImageCapability(\n preflight: PiRuntimePreflight,\n context: ReturnType['context']\n): void {\n if (!contextHasImages(context)) {\n return;\n }\n if (!preflight.capabilities.imageInput || !preflight.resolution.model.input.includes('image')) {\n throw new PiRuntimeError(\n 'unsupported_image_input',\n `More source model '${preflight.resolution.model.id}' does not support image input`\n );\n }\n}\n\n/** 仅保留需要写入原生载荷的两种格式化输出模式。 */\nfunction nativeStructuredFormat(\n responseFormat: PiRuntimeResponseFormat\n): '格式化输出' | '格式化输出(v4兼容)' | undefined {\n return responseFormat === '格式化输出' || responseFormat === '格式化输出(v4兼容)'\n ? responseFormat\n : undefined;\n}\n\n/**\n * 按本轮快照组合认证、采样、代理、非流式传输及载荷转换回调。\n * Codex 经代理时强制使用 SSE,所有请求层共享同一取消信号。\n */\nfunction createStreamOptions(\n preflight: PiRuntimePreflight,\n signal: AbortSignal,\n systemMessages: PiSystemMessageBridge\n): ApiStreamOptions {\n const providerFetch = preflight.useCorsProxy\n ? createSillyTavernProxyFetch({\n baseUrl: preflight.resolution.model.baseUrl,\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : preflight.fetch;\n return {\n signal,\n apiKey: preflight.resolution.apiKey,\n fetch: preflight.streaming\n ? providerFetch\n : createPiNonStreamingFetch(preflight.resolution.model.api as PiWireApi, providerFetch),\n headers: preflight.headers === undefined ? undefined : { ...preflight.headers },\n temperature: preflight.capabilities.temperature ? preflight.temperature : undefined,\n maxTokens: preflight.resolution.effectiveMaxTokens,\n ...(preflight.toolChoice === undefined ? {} : { toolChoice: preflight.toolChoice }),\n ...(preflight.useCorsProxy && preflight.resolution.model.api === 'openai-codex-responses'\n ? { transport: 'sse' as const }\n : {}),\n onPayload: payload =>\n systemMessages.restore(\n createPiPayloadTransform({\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n })(payload)\n ),\n };\n}\n\n/** 识别常见浏览器 fetch、网络和 CORS 失败,供运行时选择网络错误类别。 */\nfunction isNetworkFailure(error: unknown): boolean {\n const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';\n return /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message\n );\n}\n\n/** 通过稳定错误码识别 Google 注入点不兼容,避免直接导入适配器产生循环依赖。 */\nfunction isGoogleProxyAdapterCompatibilityFailure(error: unknown): boolean {\n // Keep the runtime boundary independent from the Google adapter module: provider registration\n // imports the runtime-facing gateway, so importing the adapter here would create a cycle.\n return (\n error instanceof Error &&\n (error as Error & { code?: unknown }).code === 'google_proxy_adapter_incompatible'\n );\n}\n\n/**\n * 将已知底层失败归类为运行时错误并保留重试语义。\n * 未知错误可能附带请求头或正文,只返回固定提示,不保留原始 cause。\n */\nfunction normalizeRuntimeFailure(error: unknown): Error {\n if (error instanceof PiRuntimeError || error instanceof PiContextAdapterError) {\n return error;\n }\n if (error instanceof PiProxyUnavailableError) {\n return new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n );\n }\n if (isGoogleProxyAdapterCompatibilityFailure(error)) {\n return new PiRuntimeError(\n 'protocol',\n 'The installed Google Generative AI SDK is incompatible with the browser proxy transport required by this build.'\n );\n }\n if (error instanceof PiResultAdapterError && error.code === 'aborted') {\n // The generation id is supplied by the request-level abort conversion below.\n return error;\n }\n if (\n (error instanceof PiResultAdapterError && error.code === 'network') ||\n isNetworkFailure(error)\n ) {\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n if (error instanceof PiResultAdapterError) {\n switch (error.code) {\n case 'provider-error':\n return new PiRuntimeError('provider', 'More source request failed.', true);\n case 'deferred':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned a deferred response, which is not supported.'\n );\n case 'empty-response':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned no usable response content.'\n );\n case 'invalid-tool-call':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned an invalid tool call.'\n );\n case 'length':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider response was truncated because it reached the output limit.'\n );\n case 'aborted':\n return error;\n case 'network':\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n }\n // SDK/fetch implementations may attach response bodies, headers, or request configuration to\n // arbitrary errors. Do not surface the unknown error or retain it as a cause.\n return new PiRuntimeError('provider', 'More source request failed.', true);\n}\n\n/** 要求非空生成编号,使取消登记与提示词捕获可以准确关联。 */\nfunction assertGenerationId(generationId: string): void {\n if (!generationId.trim()) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source request generationId must not be empty'\n );\n }\n}\n\n/**\n * 将捕获的酒馆提示词转换为 Pi 上下文,检查图片及 token 预算后执行所选服务商请求。\n * 统一处理代理检查、取消登记、错误分类和结果转换;返回值不自动追加到聊天或上下文。\n */\nexport async function runPiRequest(\n input: RunPiRequestInput\n): Promise {\n assertGenerationId(input.generationId);\n let preflight: PiRuntimePreflight;\n try {\n preflight =\n input.preflight ??\n (await assertPiRuntimeConfiguration({\n settings: input.settings,\n responseFormat: input.responseFormat,\n jsonSchema: input.jsonSchema,\n tools: input.tools,\n toolChoice: input.toolChoice,\n credentialStore: input.credentialStore,\n fetch: input.fetch,\n signal: input.signal,\n }));\n } catch (error) {\n if (input.signal?.aborted) {\n throw new PiRequestAbortedError(input.generationId, input.signal.reason ?? error);\n }\n throw error;\n }\n\n let registration: ReturnType;\n try {\n registration = registerPiRequestController(input.generationId, input.signal);\n } catch (error) {\n throw new PiRuntimeError(\n 'request_already_active',\n error instanceof Error ? error.message : 'More source request is already active'\n );\n }\n let systemMessages: PiSystemMessageBridge | undefined;\n try {\n if (registration.signal.aborted) {\n throw new PiRequestAbortedError(input.generationId, registration.signal.reason);\n }\n\n if (preflight.useCorsProxy) {\n try {\n await assertSillyTavernProxyAvailable({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n signal: registration.signal,\n force: true,\n });\n } catch (error) {\n throw normalizeRuntimeFailure(error);\n }\n }\n\n let adapted: ReturnType;\n try {\n adapted = toPiContext(input.messages);\n } catch (error) {\n if (error instanceof PiContextAdapterError) {\n throw new PiRuntimeError('invalid_prompt', error.message);\n }\n throw error;\n }\n const { context, lateSystemMessages } = adapted;\n if (preflight.tools !== undefined) {\n context.tools = [...preflight.tools];\n }\n assertImageCapability(preflight, context);\n systemMessages = createPiSystemMessageBridge(adapted, preflight.resolution.model.api);\n try {\n assertPiTokenBudget(\n context,\n preflight.resolution.effectiveContextWindow,\n preflight.resolution.effectiveMaxTokens,\n undefined,\n lateSystemMessages\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'token_budget',\n error instanceof Error ? error.message : 'More source token preflight failed'\n );\n }\n\n const models = createModels({\n credentials: preflight.credentialStore,\n authContext: BROWSER_AUTH_CONTEXT,\n });\n models.setProvider(createRuntimeProvider(preflight));\n const options = createStreamOptions(preflight, registration.signal, systemMessages);\n if (\n preflight.resolution.model.api === 'google-generative-ai' &&\n options.fetch &&\n options.fetch !== globalThis.fetch\n ) {\n // Pi wraps provider setup in lazyStream and would otherwise erase the compatibility\n // error code. Check the instance-only SDK seam before entering that boundary.\n assertGoogleProxyAdapterCompatible();\n }\n let stream;\n if (preflight.thinkingLevel === 'default') {\n stream = models.stream(preflight.resolution.model, systemMessages.context, options);\n } else {\n const model = {\n ...preflight.resolution.model,\n // The shared answer/thinking budget must stay inside the preflight reservation.\n maxTokens: preflight.resolution.effectiveMaxTokens,\n };\n // Pi 1.0's simple adapters forward native toolChoice unchanged, although the public\n // simple type only declares auto/none. Keep required/named choices from our resolver.\n // Real-adapter transport tests cover this compatibility boundary.\n const simpleOptions = {\n ...options,\n reasoning: preflight.thinkingLevel === 'off' ? undefined : preflight.thinkingLevel,\n } as SimpleStreamOptions;\n stream = models.streamSimple(model, systemMessages.context, simpleOptions);\n }\n\n for await (const event of stream) {\n // Provider error events can contain raw response bodies, request headers, or echoed\n // credentials. The normalized terminal error below is the public failure boundary.\n if (event.type !== 'error') {\n await input.onProgress?.(event);\n }\n }\n const message = await stream.result();\n if (registration.signal.aborted || message.stopReason === 'aborted') {\n throw new PiRequestAbortedError(\n input.generationId,\n registration.signal.aborted ? registration.signal.reason : undefined\n );\n }\n if (systemMessages.failure) throw systemMessages.failure;\n const result = fromPiAssistantMessage(message);\n systemMessages.assertRestored();\n return result;\n } catch (error) {\n const wasAborted =\n registration.signal.aborted ||\n error instanceof PiRequestAbortedError ||\n (error instanceof PiResultAdapterError && error.code === 'aborted');\n if (wasAborted) {\n throw error instanceof PiRequestAbortedError\n ? error\n : new PiRequestAbortedError(input.generationId, error);\n }\n const requestError = systemMessages?.failure ?? error;\n const proxy_status = preflight.useCorsProxy\n ? getSillyTavernProxyStatus({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : 'unchecked';\n const normalizedError =\n proxy_status === 'disabled' || proxy_status === 'unavailable'\n ? new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n )\n : normalizeRuntimeFailure(requestError);\n if (!registration.signal.aborted) {\n // Abort listeners must not observe the raw provider error either.\n registration.controller.abort(normalizedError);\n }\n throw normalizedError;\n } finally {\n registration.release();\n }\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** MVU 接受的工具选择规则;required/any 均表示必须调用工具,发送前按协议转换。 */\nexport type MvuToolChoice =\n | 'auto'\n | 'none'\n | 'required'\n | 'any'\n | { type: 'function'; function: { name: string } };\n\n/**\n * 把 MVU 工具选择转换为当前协议接受的形式。\n * 保留 auto/none,转换 required/any 与具名调用;不支持具名选择的协议提前拒绝。\n */\nexport function resolvePiToolChoice(api: Api, choice: MvuToolChoice | undefined): unknown {\n const normalized = choice ?? 'auto';\n if (normalized === 'auto' || normalized === 'none') {\n return normalized;\n }\n\n const named = typeof normalized === 'object' ? normalized.function.name.trim() : undefined;\n if (named !== undefined && !named) {\n throw new Error('More source named tool choice requires a tool name');\n }\n\n if (api === 'openai-completions') {\n return named\n ? { type: 'function', function: { name: named } }\n : normalized === 'any'\n ? 'required'\n : normalized;\n }\n if (api === 'openai-responses') {\n return named ? { type: 'function', name: named } : 'required';\n }\n if (api === 'openai-codex-responses') {\n if (named) {\n throw new Error(\n \"More source API 'openai-codex-responses' does not support a named tool choice\"\n );\n }\n return 'required';\n }\n if (api === 'anthropic-messages') {\n return named ? { type: 'tool', name: named } : 'any';\n }\n if (api === 'google-generative-ai') {\n if (named) {\n throw new Error(\n \"More source API 'google-generative-ai' does not support a named tool choice\"\n );\n }\n return 'any';\n }\n if (api === 'mistral-conversations') {\n return named ? { type: 'function', function: { name: named } } : normalized;\n }\n throw new Error(`More source API '${api}' does not support required tool choice`);\n}\n","import type { FetchFunction } from './pi_gateway';\n\nconst PROXY_DISABLED_MESSAGE =\n 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';\nconst PROXY_PROBE_BODY = 'mvu-st-cors-proxy-probe';\nconst PROXY_PROBE_TARGET = `data:text/plain,${PROXY_PROBE_BODY}`;\nconst PROXY_PROBE_TIMEOUT_MS = 5_000;\n\n/** 酒馆 CORS 代理的探测状态,区分未检查、检查中和三种已完成结果。 */\nexport type SillyTavernProxyStatus =\n | 'unchecked'\n | 'checking'\n | 'enabled'\n | 'disabled'\n | 'unavailable';\n\n/** 一次探测完成后可缓存的结果:已启用、明确关闭或当前不可用。 */\nexport type SillyTavernProxyTerminalStatus = Extract<\n SillyTavernProxyStatus,\n 'enabled' | 'disabled' | 'unavailable'\n>;\n\n/** 代理探测的环境与缓存选项,允许测试注入传输和酒馆源地址。 */\nexport interface SillyTavernProxyProbeOptions {\n /** 默认使用页面的 fetch 实现。 */\n fetch?: FetchFunction;\n /** 默认从页面基础地址或 location 解析酒馆源地址。 */\n origin?: string;\n /** 仅取消当前调用方的等待,不中止其他调用方共用的探测。 */\n signal?: AbortSignal;\n /** 忽略已缓存的完成结果,重新检查酒馆代理路由。 */\n force?: boolean;\n}\n\n/** 创建代理 fetch 所需的配置,限定携带凭证请求能够访问的目标范围。 */\nexport interface SillyTavernProxyFetchOptions extends SillyTavernProxyProbeOptions {\n /**\n * 服务商 API 基础地址;返回的 fetch 仅允许向同源、同路径或下级路径转发凭证。\n */\n baseUrl: string | URL;\n}\n\nexport class PiProxyUnavailableError extends Error {\n readonly code = 'proxy_unavailable';\n readonly retryable = false;\n\n /** 区分代理未启用和不可用,并标记为发送前应终止的配置类失败。 */\n constructor(readonly status: Exclude) {\n super(\n status === 'disabled'\n ? 'SillyTavern CORS proxy is not enabled'\n : 'SillyTavern CORS proxy is unavailable'\n );\n this.name = 'PiProxyUnavailableError';\n }\n}\n\n/** 与 Pi fetch 契约保持一致的请求目标,可为 URL、字符串或 Request。 */\ntype FetchInput = Parameters[0];\n\n/** 按 fetch 实现及酒馆源地址共享的探测记录,用 promise 合并同时发起的检查。 */\ntype ProbeEntry = {\n status: SillyTavernProxyStatus;\n promise?: Promise;\n};\n\nlet probe_cache = new WeakMap>();\n\n/** 读取注入或全局 fetch;浏览器缺少请求能力时返回 undefined。 */\nfunction resolveFetch(fetch_override?: FetchFunction): FetchFunction | undefined {\n const candidate = fetch_override ?? globalThis.fetch;\n return typeof candidate === 'function' ? candidate : undefined;\n}\n\n/** 从覆盖值或文档基础地址解析酒馆源地址,兼容 origin 为 null 的 srcdoc 脚本。 */\nfunction resolveOrigin(origin_override?: string): string | undefined {\n // Slash normally hosts scripts in srcdoc, where location.origin is \"null\" despite\n // inheriting the parent's origin. Use the same document base as relative browser fetch;\n // Slash's Blob mode also supplies a pointing to SillyTavern.\n const value = origin_override ?? globalThis.document?.baseURI ?? globalThis.location?.origin;\n if (!value) {\n return undefined;\n }\n try {\n const origin = new URL(value).origin;\n return origin === 'null' ? undefined : origin;\n } catch {\n return undefined;\n }\n}\n\n/** 按 fetch 实现与酒馆源地址隔离探测缓存,复用同一目标的进行中检查。 */\nfunction getProbeEntry(fetch_impl: FetchFunction, origin: string): ProbeEntry {\n let entries = probe_cache.get(fetch_impl);\n if (!entries) {\n entries = new Map();\n probe_cache.set(fetch_impl, entries);\n }\n let entry = entries.get(origin);\n if (!entry) {\n entry = { status: 'unchecked' };\n entries.set(origin, entry);\n }\n return entry;\n}\n\n/** 将完整目标地址编码进酒馆通用 CORS 代理路径。 */\nfunction proxyUrl(target: string): string {\n return `/proxy/${encodeURIComponent(target)}`;\n}\n\n/** 忽略首尾空白后精确核对探测正文,避免把其他页面误识别为代理结果。 */\nfunction exactResponseText(actual: string, expected: string): boolean {\n return actual.trim() === expected;\n}\n\n/** 通过本地 data URL 和超时限制探测代理,区分启用、关闭和暂时不可达。 */\nasync function performProbe(fetch_impl: FetchFunction): Promise {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROXY_PROBE_TIMEOUT_MS);\n try {\n // node-fetch resolves this data URL locally. Unlike a same-origin circular sentinel, it\n // remains reliable when SillyTavern is behind an HTTPS reverse proxy.\n const response = await fetch_impl(proxyUrl(PROXY_PROBE_TARGET), {\n method: 'GET',\n credentials: 'same-origin',\n cache: 'no-store',\n signal: controller.signal,\n });\n const body = await response.text();\n if (response.status === 200 && exactResponseText(body, PROXY_PROBE_BODY)) {\n return 'enabled';\n }\n if (response.status === 404 && exactResponseText(body, PROXY_DISABLED_MESSAGE)) {\n return 'disabled';\n }\n return 'unavailable';\n } catch {\n return 'unavailable';\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/** 保留信号原始取消原因,缺失时补齐标准 AbortError。 */\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n}\n\n/** 允许调用方取消对共享探测的等待,不中断其他调用方共用的探测请求。 */\nfunction waitForProbe(\n promise: Promise,\n signal?: AbortSignal\n): Promise {\n if (!signal) {\n return promise;\n }\n if (signal.aborted) {\n return Promise.reject(abortReason(signal));\n }\n return new Promise((resolve, reject) => {\n /** 解除当前等待者的监听并拒绝其 Promise,保留共享探测继续运行。 */\n const on_abort = () => {\n cleanup();\n reject(abortReason(signal));\n };\n /** 移除当前等待者的取消监听,避免探测结束后残留引用。 */\n const cleanup = () => signal.removeEventListener('abort', on_abort);\n signal.addEventListener('abort', on_abort, { once: true });\n promise.then(\n status => {\n cleanup();\n resolve(status);\n },\n error => {\n cleanup();\n reject(error);\n }\n );\n });\n}\n\n/** 只读取缓存中的代理状态,不触发新的网络探测。 */\nexport function getSillyTavernProxyStatus(\n options: SillyTavernProxyProbeOptions = {}\n): SillyTavernProxyStatus {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return 'unavailable';\n }\n return getProbeEntry(fetch_impl, origin).status;\n}\n\n/**\n * 合并并发探测并缓存确定的启用或关闭状态,暂时失败允许重试。\n * force 可重新核对服务端状态;单个等待者取消不影响共享探测。\n */\nexport function probeSillyTavernProxy(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return Promise.resolve('unavailable');\n }\n\n const entry = getProbeEntry(fetch_impl, origin);\n if (entry.promise) {\n return waitForProbe(entry.promise, options.signal);\n }\n if (!options.force && (entry.status === 'enabled' || entry.status === 'disabled')) {\n return waitForProbe(Promise.resolve(entry.status), options.signal);\n }\n\n entry.status = 'checking';\n entry.promise = performProbe(fetch_impl).then(status => {\n entry.status = status;\n entry.promise = undefined;\n return status;\n });\n return waitForProbe(entry.promise, options.signal);\n}\n\n/** 在发送带凭证的服务商请求前确认代理可用,不可用时立即报错。 */\nexport async function assertSillyTavernProxyAvailable(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const status = await probeSillyTavernProxy(options);\n if (status !== 'enabled') {\n throw new PiProxyUnavailableError(status);\n }\n}\n\n/** 要求不含内嵌凭证的绝对 HTTP(S) 地址,作为代理目标的第一层校验。 */\nfunction parseHttpUrl(value: string | URL, name: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL`);\n }\n if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL without credentials`);\n }\n return url;\n}\n\n/** 去除基础路径末尾斜杠,便于严格比较目标路径边界。 */\nfunction normalizeBasePath(pathname: string): string {\n return pathname === '/' ? '' : pathname.replace(/\\/+$/, '');\n}\n\n/** 限制请求必须位于配置服务商的同源基础路径内,避免凭证随代理流向其他目标。 */\nfunction assertTargetAllowed(target: URL, base: URL): void {\n const base_path = normalizeBasePath(base.pathname);\n const in_base_path =\n base_path === '' ||\n target.pathname === base_path ||\n target.pathname.startsWith(`${base_path}/`);\n if (target.origin !== base.origin || !in_base_path) {\n throw new TypeError('SillyTavern proxy target is outside the configured provider base URL');\n }\n}\n\n/** 识别 Request 输入,供后续按 fetch 语义合并 init 覆盖值。 */\nfunction requestFromInput(input: FetchInput): Request | undefined {\n return typeof Request !== 'undefined' && input instanceof Request ? input : undefined;\n}\n\n/** 统一取得字符串、URL 或 Request 输入对应的目标地址。 */\nfunction requestUrl(input: FetchInput, request: Request | undefined): string {\n return request?.url ?? input.toString();\n}\n\n/** 按 init 优先、Request 次之的顺序解析 HTTP 方法并统一大写。 */\nfunction requestMethod(request: Request | undefined, init?: RequestInit): string {\n return (init?.method ?? request?.method ?? 'GET').toUpperCase();\n}\n\n/** 遵循 fetch 的覆盖顺序解析本次请求头。 */\nfunction requestHeaders(request: Request | undefined, init?: RequestInit): HeadersInit | undefined {\n return init?.headers ?? request?.headers;\n}\n\n/** 保留 init 显式传入的取消信号或 null,否则继承 Request 的信号。 */\nfunction requestSignal(\n request: Request | undefined,\n init?: RequestInit\n): AbortSignal | null | undefined {\n if (init && 'signal' in init) {\n return init.signal;\n }\n return request?.signal;\n}\n\n/** 将可重放的请求体转换为文本,拒绝无法安全复用的流或其他载荷。 */\nasync function bodyToText(body: BodyInit): Promise {\n if (typeof body === 'string') {\n return body;\n }\n if (body instanceof URLSearchParams) {\n return body.toString();\n }\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return body.text();\n }\n if (body instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(body));\n }\n if (ArrayBuffer.isView(body)) {\n return new TextDecoder().decode(\n new Uint8Array(body.buffer, body.byteOffset, body.byteLength)\n );\n }\n throw new TypeError('SillyTavern proxy only supports replayable JSON request bodies');\n}\n\n/** 优先读取 init 请求体,必要时克隆 Request 读取,避免消费原始输入。 */\nasync function requestBodyText(\n request: Request | undefined,\n init?: RequestInit\n): Promise {\n if (init?.body !== undefined && init.body !== null) {\n return bodyToText(init.body);\n }\n if (init && 'body' in init && init.body === null) {\n return undefined;\n }\n if (request?.body !== null && request !== undefined) {\n return request.clone().text();\n }\n return undefined;\n}\n\n/** 从 Headers、键值数组或对象中按大小写不敏感名称查找请求头。 */\nfunction getHeader(headers: HeadersInit | undefined, wanted_name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const wanted = wanted_name.toLowerCase();\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(wanted_name) ?? undefined;\n }\n if (Array.isArray(headers)) {\n return headers.find(([name]) => name.toLowerCase() === wanted)?.[1];\n }\n const entry = Object.entries(headers).find(([name]) => name.toLowerCase() === wanted);\n return entry?.[1] === undefined ? undefined : String(entry[1]);\n}\n\n/** 确认带正文的代理请求使用受支持的方法、JSON 类型和合法 JSON 内容。 */\nfunction assertJsonBody(method: string, headers: HeadersInit | undefined, body?: string): void {\n if (body === undefined) {\n return;\n }\n if (!['POST', 'PUT', 'PATCH'].includes(method)) {\n throw new TypeError(`SillyTavern proxy does not forward request bodies for ${method}`);\n }\n const content_type = getHeader(headers, 'content-type') ?? '';\n if (!/^application\\/json(?:\\s*;|$)/i.test(content_type)) {\n throw new TypeError('SillyTavern proxy requires Content-Type: application/json');\n }\n try {\n JSON.parse(body);\n } catch {\n throw new TypeError('SillyTavern proxy request body must be valid JSON');\n }\n}\n\n/** 在准备和转发请求的边界检查取消状态,保留原始取消原因。 */\nfunction throwIfAborted(signal: AbortSignal | null | undefined): void {\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n }\n}\n\n/** 保留原始应答格式,仅遮盖上游可能在错误正文中回显的本次请求凭证。 */\nfunction redactProxyResponseBody(\n body: string,\n headers: HeadersInit | undefined,\n target: URL\n): string {\n const credentials = new Set();\n for (const name of ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key']) {\n const value = getHeader(headers, name)?.trim();\n if (value) {\n credentials.add(value);\n credentials.add(value.replace(/^Bearer\\s+/i, ''));\n }\n }\n for (const [name, value] of target.searchParams) {\n if (/^(?:key|api[_-]?key|(?:access[_-]?)?token)$/i.test(name) && value) {\n credentials.add(value);\n }\n }\n for (const credential of [...credentials].filter(Boolean).sort((a, b) => b.length - a.length)) {\n body = body.split(credential).join('[REDACTED]');\n }\n return body;\n}\n\n/** 异步读取克隆应答,在流结束或中断后输出 debug 日志,不占用 SDK 的正文。 */\nasync function debugProxyResponse(\n response: Response,\n target: URL,\n method: string,\n headers: HeadersInit | undefined,\n signal: AbortSignal | null | undefined\n): Promise {\n const copy = response.clone();\n const reader = copy.body?.getReader();\n let body = '';\n let complete = false;\n // 不等待 tee 分支的 cancel:其 Promise 可能要等 SDK 也结束读取才完成。\n const on_abort = () => {\n void reader?.cancel().catch(() => {});\n };\n try {\n if (reader) {\n const decoder = new TextDecoder();\n signal?.addEventListener('abort', on_abort, { once: true });\n if (signal?.aborted) {\n on_abort();\n }\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) {\n body += decoder.decode();\n complete = !signal?.aborted;\n break;\n }\n body += decoder.decode(chunk.value, { stream: true });\n }\n } else {\n body = await copy.text();\n complete = !signal?.aborted;\n }\n } catch {\n // 读取失败时仍记录已收到的部分;日志失败不能改变请求结果。\n } finally {\n signal?.removeEventListener('abort', on_abort);\n reader?.releaseLock();\n }\n console.debug(\n '[MVU Pi Proxy] raw response',\n { method, url: `${target.origin}${target.pathname}`, status: response.status, complete },\n redactProxyResponseBody(body, headers, target)\n );\n}\n\n/** 检查返回值是否为酒馆明确的代理关闭提示,不把普通上游 404 误判为代理关闭。 */\nasync function isDisabledProxyResponse(response: Response): Promise {\n if (response.status !== 404) {\n return false;\n }\n try {\n return exactResponseText(await response.clone().text(), PROXY_DISABLED_MESSAGE);\n } catch {\n return false;\n }\n}\n\n/** 将实际请求发现的代理终态写回对应探测缓存。 */\nfunction cacheTerminalStatus(\n fetch_impl: FetchFunction,\n origin: string,\n status: SillyTavernProxyTerminalStatus\n): void {\n const entry = getProbeEntry(fetch_impl, origin);\n entry.status = status;\n entry.promise = undefined;\n}\n\n/**\n * 创建受单一服务商基础地址约束的代理 fetch,转发认证头、JSON 正文和取消信号。\n * 发送前确认代理可用,目标越界或载荷不兼容时拒绝转发。\n */\nexport function createSillyTavernProxyFetch(options: SillyTavernProxyFetchOptions): FetchFunction {\n const base = parseHttpUrl(options.baseUrl, 'baseUrl');\n\n return async (input: FetchInput, init?: RequestInit): Promise => {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n throw new PiProxyUnavailableError('unavailable');\n }\n\n const request = requestFromInput(input);\n const target = parseHttpUrl(requestUrl(input, request), 'proxy target');\n assertTargetAllowed(target, base);\n\n const method = requestMethod(request, init);\n const headers = requestHeaders(request, init);\n const signal = requestSignal(request, init);\n throwIfAborted(signal);\n const body = await requestBodyText(request, init);\n assertJsonBody(method, headers, body);\n\n await assertSillyTavernProxyAvailable({\n fetch: fetch_impl,\n origin,\n ...(signal == null ? {} : { signal }),\n });\n throwIfAborted(signal);\n\n const response = await fetch_impl(proxyUrl(target.href), {\n method,\n ...(headers === undefined ? {} : { headers }),\n ...(body === undefined ? {} : { body }),\n ...(signal === undefined ? {} : { signal }),\n credentials: 'same-origin',\n });\n void debugProxyResponse(response, target, method, headers, signal).catch(() => {});\n if (await isDisabledProxyResponse(response)) {\n cacheTerminalStatus(fetch_impl, origin, 'disabled');\n throw new PiProxyUnavailableError('disabled');\n }\n return response;\n };\n}\n\n/** @internal 仅供测试清空模块级探测缓存,避免用例之间共享状态。 */\nexport function resetSillyTavernProxyStatusForTests(): void {\n probe_cache = new WeakMap();\n}\n","import { clampThinkingLevel, type Api, type Model } from './pi_gateway';\nimport { normalizePiThinkingLevel, type PiThinkingLevel } from './thinking_setting';\n\n/** 按可信目录或协议适配元数据解析生效等级;缺少元数据时保留显式选择。 */\nexport function resolvePiThinkingLevel(value: unknown, model?: Model): PiThinkingLevel {\n const level = normalizePiThinkingLevel(value);\n if (level === 'default' || level === 'off' || !model) return level;\n return model.reasoning ? clampThinkingLevel(model, level) : 'off';\n}\n\n/** 为动态模型补充显式启用信息,避免 SDK 再次把支持 effort 的协议降为 high。 */\nexport function preparePiThinkingModel(\n model: Model,\n catalogHit: boolean,\n level: PiThinkingLevel\n): Model {\n if (catalogHit || level === 'default') return model;\n const extendedEffort =\n (level === 'xhigh' || level === 'max') &&\n [\n 'openai-completions',\n 'openai-responses',\n 'openai-codex-responses',\n 'mistral-conversations',\n ].includes(model.api);\n return {\n ...model,\n reasoning: true,\n ...(extendedEffort ? { thinkingLevelMap: { xhigh: 'xhigh', max: 'max' } } : {}),\n };\n}\n","/** 默认值沿用原始协议行为;显式等级由 Pi 按模型能力映射。 */\nexport const PI_THINKING_LEVELS = [\n 'default',\n 'off',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n] as const;\nexport type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];\n\nexport function normalizePiThinkingLevel(value: unknown): PiThinkingLevel {\n return PI_THINKING_LEVELS.includes(value as PiThinkingLevel)\n ? (value as PiThinkingLevel)\n : 'default';\n}\n\nexport function isPiThinkingEnabled(value: unknown): boolean {\n const level = normalizePiThinkingLevel(value);\n return level !== 'default' && level !== 'off';\n}\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const commonMessages = defineMessages({\n 'common.enabled': {\n 'zh-CN': '启用',\n en: 'Enabled',\n },\n 'common.disabled': {\n 'zh-CN': '关闭',\n en: 'Disabled',\n },\n 'common.yes': {\n 'zh-CN': '是',\n en: 'Yes',\n },\n 'common.no': {\n 'zh-CN': '否',\n en: 'No',\n },\n 'common.confirm': {\n 'zh-CN': '确认',\n en: 'Confirm',\n },\n 'common.cancel': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const panelMessages = defineMessages({\n 'panel.title': {\n 'zh-CN': 'MVU 变量框架',\n en: 'MVU Variable Framework',\n },\n 'panel.version.section': {\n 'zh-CN': '当前版本',\n en: 'Current version',\n },\n 'panel.version.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n 'panel.notification.section': {\n 'zh-CN': '通知设置',\n en: 'Notifications',\n },\n 'panel.notification.frameworkLoaded': {\n 'zh-CN': 'MVU 框架加载成功时通知',\n en: 'Notify when the MVU framework loads successfully',\n },\n 'panel.notification.variablesInitialized': {\n 'zh-CN': '变量初始化成功时通知',\n en: 'Notify when variables are initialized successfully',\n },\n 'panel.notification.variableError': {\n 'zh-CN': '变量初始化/更新出错时通知',\n en: 'Notify when variable initialization or updates fail',\n },\n 'panel.notification.extraModelParsing': {\n 'zh-CN': '额外模型解析中通知',\n en: 'Notify while extra-model parsing is running',\n },\n 'panel.cleanup.section': {\n 'zh-CN': '自动清理变量',\n en: 'Automatic variable cleanup',\n },\n 'panel.cleanup.enable': {\n 'zh-CN': '启用自动清理变量',\n en: 'Enable automatic variable cleanup',\n },\n 'panel.cleanup.strategy': {\n 'zh-CN': '清理策略',\n en: 'Cleanup strategy',\n },\n 'panel.cleanup.snapshotInterval': {\n 'zh-CN': '快照保留间隔',\n en: 'Snapshot retention interval',\n },\n 'panel.cleanup.recentFloors': {\n 'zh-CN': '要保留变量的最近楼层数',\n en: 'Recent floors whose variables are retained',\n },\n 'panel.cleanup.restoreFloors': {\n 'zh-CN': '触发恢复变量的最近楼层数',\n en: 'Recent floors that trigger variable restoration',\n },\n 'panel.compatibility.section': {\n 'zh-CN': '兼容性',\n en: 'Compatibility',\n },\n 'panel.compatibility.updateChatVariables': {\n 'zh-CN': '变量更新到聊天变量',\n en: 'Write variable updates to chat variables',\n },\n 'panel.compatibility.updateChatVariablesHelp': {\n 'zh-CN':\n '启用后,所有变量更新结果也会输出到聊天变量中。如果部分老角色卡无法正常游玩,可以开启这个开关。',\n en: 'When enabled, every variable update is also written to chat variables. Enable this if some older character cards do not work correctly.',\n },\n 'panel.compatibility.showLegacy': {\n 'zh-CN': '显示老旧功能',\n en: 'Show legacy features',\n },\n 'panel.compatibility.sendasNotUser': {\n 'zh-CN': 'sendas 不视为 user 消息',\n en: 'Do not treat sendas as a user message',\n },\n 'panel.compatibility.nonblockingExtraAnalysis': {\n 'zh-CN': '额外模型解析不阻塞正文显示(实验性)',\n en: 'Show replies without waiting for extra model analysis (experimental)',\n },\n 'panel.compatibility.nonblockingExtraAnalysisHelp': {\n 'zh-CN':\n '默认关闭。开启后,角色卡的部分默认前端页面会立即显示,其他插件的额外分析流程也可同时执行,无需等待变量解析完成。仅对非工具调用的自动解析生效。',\n en: \"Off by default. When enabled, some of a character card's default frontend pages appear immediately, and other plugins can run their additional analysis at the same time, without waiting for variable analysis to finish. Applies only to automatic analysis without tool calling.\",\n },\n 'panel.compatibility.license': {\n 'zh-CN': 'LICENSE',\n en: 'LICENSE',\n },\n 'panel.compatibility.licenseIntro': {\n 'zh-CN': 'MVU 采用 MIT 许可证。以下是运行时使用或随扩展打包的第三方开源组件:',\n en: 'MVU is licensed under MIT. The following third-party components are used at runtime or bundled with the extension:',\n },\n 'panel.compatibility.licenseComponent': {\n 'zh-CN': '组件',\n en: 'Component',\n },\n 'panel.compatibility.licenseIdentifier': {\n 'zh-CN': '许可证',\n en: 'License',\n },\n 'panel.compatibility.licenseDetails': {\n 'zh-CN':\n '许可证标识来自当前依赖元数据;点击组件名称可查看项目,完整条款以各项目 LICENSE 为准。',\n en: 'License identifiers come from current dependency metadata. Select a component name to view its project; the project LICENSE contains the complete terms.',\n },\n 'panel.button.section': {\n 'zh-CN': '修复按钮',\n en: 'Repair actions',\n },\n 'panel.button.reprocessVariables': {\n 'zh-CN': '重新处理变量',\n en: 'Reprocess variables',\n },\n 'panel.button.reloadInitialVariables': {\n 'zh-CN': '重新读取初始变量',\n en: 'Reload initial variables',\n },\n 'panel.button.snapshotFloor': {\n 'zh-CN': '快照楼层',\n en: 'Snapshot floor',\n },\n 'panel.button.replayFloor': {\n 'zh-CN': '重演楼层',\n en: 'Replay floor',\n },\n 'panel.button.retryExtraModelParsing': {\n 'zh-CN': '重试额外模型解析',\n en: 'Retry extra-model parsing',\n },\n 'panel.button.incrementalRepairExtraModelParsing': {\n 'zh-CN': '增量校正额外模型解析',\n en: 'Incrementally repair extra-model parsing',\n },\n 'panel.button.clearOldFloorVariables': {\n 'zh-CN': '清除旧楼层变量',\n en: 'Clear variables from old floors',\n },\n 'panel.help.ariaLabel': {\n 'zh-CN': '帮助',\n en: 'Help',\n },\n 'panel.badge.override': {\n 'zh-CN': '角色卡覆盖',\n en: 'Character-card override',\n },\n 'panel.badge.overrideWithValue': {\n 'zh-CN': '角色卡覆盖:{value}',\n en: 'Character-card override: {value}',\n },\n 'panel.badge.additive': {\n 'zh-CN': '角色卡规则叠加',\n en: 'Character-card rule added',\n },\n 'panel.update.section': {\n 'zh-CN': '变量更新方式',\n en: 'Variable update method',\n },\n 'panel.update.method.aiOutput': {\n 'zh-CN': '随 AI 输出',\n en: 'Alongside AI output',\n },\n 'panel.update.method.extraModel': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.update.method.unsupportedWarning': {\n 'zh-CN':\n '世界书 [{worldbooks}] 未适配额外模型解析,视为 [mvu_plot] 条目(只会发给剧情 AI、不会发给变量更新 AI)。',\n en: 'World book [{worldbooks}] is not adapted for extra-model parsing and will be treated as an [mvu_plot] entry (sent only to the story AI, not the variable-update AI).',\n },\n 'panel.modelSelect.ariaLabel': {\n 'zh-CN': '模型列表',\n en: 'Model list',\n },\n 'panel.modelSelect.chooseFromList': {\n 'zh-CN': '(从列表选择)',\n en: '(Choose from list)',\n },\n 'panel.modelSelect.loading': {\n 'zh-CN': '获取中…',\n en: 'Fetching…',\n },\n 'panel.modelSelect.fetch': {\n 'zh-CN': '获取模型列表',\n en: 'Fetch model list',\n },\n 'panel.modelSelect.empty': {\n 'zh-CN': '模型列表为空或获取失败',\n en: 'The model list is empty or could not be fetched',\n },\n 'panel.modelSelect.fetchTitle': {\n 'zh-CN': '[MVU]获取模型列表',\n en: '[MVU] Fetch model list',\n },\n 'panel.modelSelect.fetchFailureTitle': {\n 'zh-CN': '[MVU]获取模型列表失败',\n en: '[MVU] Failed to fetch model list',\n },\n 'panel.prompt.apiOptionsMigrated': {\n 'zh-CN': '破限/应答格式等配置 已迁移到 API 方案,与方案配置绑定',\n en: 'Jailbreak, response format, and related settings have moved to API profiles and are saved with each profile.',\n },\n 'panel.source.pi.auth.accountLogin': {\n 'zh-CN': '账号登录',\n en: 'Account sign-in',\n },\n 'panel.source.pi.contextWindowHelp': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。请按服务商说明填写;数值过小会使较长的请求无法发送。',\n en: 'The total length the model can read and generate, in tokens. Enter the limit given by your provider; setting it too low prevents longer requests from being sent.',\n },\n 'panel.prompt.section': {\n 'zh-CN': '请求内容',\n en: 'Request content',\n },\n 'panel.prompt.jailbreakStrategy': {\n 'zh-CN': '破限方案',\n en: 'Jailbreak strategy',\n },\n 'panel.prompt.jailbreak.builtin': {\n 'zh-CN': '使用内置破限',\n en: 'Use built-in jailbreak',\n },\n 'panel.prompt.jailbreak.currentPreset': {\n 'zh-CN': '使用当前预设',\n en: 'Use current preset',\n },\n 'panel.prompt.jailbreak.otherPreset': {\n 'zh-CN': '使用其他预设',\n en: 'Use another preset',\n },\n 'panel.prompt.targetPreset': {\n 'zh-CN': '目标预设',\n en: 'Target preset',\n },\n 'panel.prompt.noSavedPreset': {\n 'zh-CN': '未检测到可用的已保存预设',\n en: 'No available saved presets detected',\n },\n 'panel.prompt.randomHeader': {\n 'zh-CN': '随机头部',\n en: 'Randomized header',\n },\n 'panel.prompt.randomHeaderHelp': {\n 'zh-CN':\n 'Gemini 系模型会记录破限头部,因此需要在头部增加随机数。如果你使用的不是 Gemini 系模型,请关闭这个功能,以免缓存失效。',\n en: 'Gemini-family models may record the jailbreak header, so a random number is added to it. Disable this for non-Gemini models to avoid invalidating the prompt cache.',\n },\n 'panel.prompt.responseFormat': {\n 'zh-CN': '应答格式',\n en: 'Response format',\n },\n 'panel.prompt.response.chatMessage': {\n 'zh-CN': '聊天消息',\n en: 'Chat message',\n },\n 'panel.prompt.response.toolCall': {\n 'zh-CN': '工具调用',\n en: 'Tool call',\n },\n 'panel.prompt.response.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.prompt.response.structuredV4': {\n 'zh-CN': '格式化输出(v4 兼容)',\n en: 'Structured output (v4 compatible)',\n },\n 'panel.prompt.disableThinking': {\n 'zh-CN': '关闭 thinking',\n en: 'Disable thinking',\n },\n 'panel.prompt.disableThinkingHelp': {\n 'zh-CN': '关闭后可避免部分空回复问题。',\n en: 'Disabling thinking can prevent some empty responses.',\n },\n 'panel.prompt.disable': {\n 'zh-CN': '关闭',\n en: 'Disable',\n },\n 'panel.prompt.fakeStreaming': {\n 'zh-CN': '兼容假流式',\n en: 'Pseudo-streaming compatibility',\n },\n 'panel.prompt.fakeStreamingHelp': {\n 'zh-CN':\n '默认关闭,等待模型一次返回完整结果。若渠道要求流式请求,或等待时经常超时,可尝试勾选。开启后仍会等回复完整后再更新变量。OpenAI Codex 只支持流式传输,会始终使用流式。',\n en: 'Disabled by default: wait for the complete response. Enable this if your provider requires streaming or requests often time out while waiting. Variables are still updated after the full response arrives. OpenAI Codex requires streaming and always uses it.',\n },\n 'panel.prompt.whitelist': {\n 'zh-CN': '世界书条目白名单正则',\n en: 'World-book entry whitelist regex',\n },\n 'panel.prompt.whitelistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段只保留 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing retains only world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.whitelistPlaceholder': {\n 'zh-CN': '角色{or}地点 或 /角色{or}地点/i',\n en: 'character{or}location or /character{or}location/i',\n },\n 'panel.prompt.blacklist': {\n 'zh-CN': '世界书条目黑名单正则',\n en: 'World-book entry blacklist regex',\n },\n 'panel.prompt.blacklistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段会排除 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing excludes world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.blacklistPlaceholder': {\n 'zh-CN': '临时{or}禁用 或 /临时{or}禁用/i',\n en: 'temporary{or}disabled or /temporary{or}disabled/i',\n },\n 'panel.prompt.regexInvalid': {\n 'zh-CN': '正则无效:{error}',\n en: 'Invalid regex: {error}',\n },\n 'panel.prompt.filtered.title': {\n 'zh-CN': '上次分析被筛选的条目',\n en: 'Entries filtered from the last analysis',\n },\n 'panel.prompt.filtered.empty': {\n 'zh-CN': '上次分析没有被黑/白名单筛选掉的条目。',\n en: 'No entries were filtered by the whitelist or blacklist in the last analysis.',\n },\n 'panel.prompt.filtered.entrySource': {\n 'zh-CN': '条目来源',\n en: 'Entry source',\n },\n 'panel.prompt.filtered.worldBook': {\n 'zh-CN': '世界书',\n en: 'World book',\n },\n 'panel.prompt.filtered.reason': {\n 'zh-CN': '原因',\n en: 'Reason',\n },\n 'panel.prompt.filtered.configSource': {\n 'zh-CN': '配置来源',\n en: 'Configuration source',\n },\n 'panel.prompt.filtered.comment': {\n 'zh-CN': '条目备注',\n en: 'Comment',\n },\n 'panel.prompt.filtered.globalLore': {\n 'zh-CN': '全局世界书',\n en: 'Global world book',\n },\n 'panel.prompt.filtered.characterLore': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.prompt.filtered.chatLore': {\n 'zh-CN': '聊天世界书',\n en: 'Chat world book',\n },\n 'panel.prompt.filtered.personaLore': {\n 'zh-CN': '用户世界书',\n en: 'Persona world book',\n },\n 'panel.prompt.filtered.whitelistReason': {\n 'zh-CN': '白名单',\n en: 'Whitelist',\n },\n 'panel.prompt.filtered.blacklistReason': {\n 'zh-CN': '黑名单',\n en: 'Blacklist',\n },\n 'panel.prompt.filtered.globalConfig': {\n 'zh-CN': '用户全局配置',\n en: 'User global settings',\n },\n 'panel.prompt.filtered.characterConfig': {\n 'zh-CN': '角色卡配置',\n en: 'Character-card settings',\n },\n 'panel.prompt.toolCallUnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'工具调用'\",\n en: \"[MVU] Cannot use 'Tool call'\",\n },\n 'panel.prompt.toolCallUnsupported': {\n 'zh-CN': '当前 API 源不支持工具调用,请换用支持 tools 的渠道模型或改用其他应答格式',\n en: 'The current API source does not support tool calls. Choose a provider/model that supports tools, or use another response format.',\n },\n 'panel.prompt.structuredV4UnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'格式化输出(v4兼容)'\",\n en: \"[MVU] Cannot use 'Structured output (v4 compatible)'\",\n },\n 'panel.prompt.structuredV4RequiresCustom': {\n 'zh-CN': '格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同',\n en: 'Structured output (v4 compatible) requires a custom extra-model source and cannot use the current connection.',\n },\n 'panel.request.section': {\n 'zh-CN': '请求策略',\n en: 'Request strategy',\n },\n 'panel.request.method': {\n 'zh-CN': '请求方式',\n en: 'Request method',\n },\n 'panel.request.sequential': {\n 'zh-CN': '依次请求,失败后重试',\n en: 'Request sequentially and retry on failure',\n },\n 'panel.request.parallel': {\n 'zh-CN': '同时请求多次',\n en: 'Send multiple requests in parallel',\n },\n 'panel.request.onceThenParallel': {\n 'zh-CN': '先请求一次,失败后再同时请求多次',\n en: 'Request once, then retry in parallel on failure',\n },\n 'panel.request.count': {\n 'zh-CN': '请求次数',\n en: 'Number of requests',\n },\n 'panel.request.auto': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.request.autoHelp': {\n 'zh-CN':\n '关闭后,AI 回复完成时不会自动触发额外模型解析;你需要主动点击“重试额外模型解析”按钮,才会执行解析并添加状态栏占位符 <StatusPlaceHolderImpl/>。',\n en: 'When disabled, extra-model parsing is not triggered automatically after the AI finishes replying. Click “Retry extra-model parsing” to run it and add the status-bar placeholder <StatusPlaceHolderImpl/>.',\n },\n 'panel.request.minLength': {\n 'zh-CN': '最小请求字数',\n en: 'Minimum reply length',\n },\n 'panel.request.minLengthHelp': {\n 'zh-CN':\n 'AI 回复达到此字数才自动请求额外模型解析。按整条回复去除首尾空白后的字符数计算(包含标签,不是 token 数);0 表示不限制。若同时设置触发标签,两个条件都须满足。手动重试不受限制。',\n en: 'Automatically request extra-model parsing only when the AI reply reaches this many characters. Counts the entire reply, including tags, after trimming leading and trailing whitespace (not tokens). Set to 0 for no minimum. If a trigger tag is also set, both conditions must be met. Manual retries bypass these conditions.',\n },\n 'panel.request.triggerTag': {\n 'zh-CN': '触发标签',\n en: 'Trigger tag',\n },\n 'panel.request.triggerTagHelp': {\n 'zh-CN':\n 'AI 回复匹配此正则表达式才自动请求额外模型解析。可直接填写 </content>,或用 </(content|正文)> 匹配多个标签;也支持 /pattern/flags 格式,例如 /content/i 忽略大小写。默认区分大小写,特殊字符需用反斜杠转义。忽略配置首尾空白,留空表示不限制;正则无效时跳过自动请求。若同时设置最小请求字数,两个条件都须满足。手动重试不受限制。',\n en: 'Automatically request extra-model parsing only when the AI reply matches this regular expression. Enter </content> directly, or use </(content|正文)> to match either tag. The /pattern/flags format is also supported, such as /content/i for case-insensitive matching. Matching is case-sensitive by default; escape special characters with a backslash. Leading and trailing whitespace in this setting is ignored; leave empty for no restriction. Invalid regexes skip automatic requests. If a minimum reply length is also set, both conditions must be met. Manual retries bypass these conditions.',\n },\n 'panel.request.triggerTagInvalid': {\n 'zh-CN': '触发标签正则无效,将跳过自动请求:{error}',\n en: 'Invalid trigger-tag regex; automatic requests will be skipped: {error}',\n },\n 'panel.request.batchWarning': {\n 'zh-CN':\n '请升级酒馆助手到 4.4.3 或更高版本,否则批量请求功能可能让预设的“流式传输”设置失效',\n en: \"Upgrade Tavern Helper to version 4.4.3 or later. Otherwise, batch requests may override the preset's streaming setting.\",\n },\n 'panel.request.batchWarningTitle': {\n 'zh-CN': '[MVU]批量请求可能有问题',\n en: '[MVU] Batch requests may not work correctly',\n },\n 'panel.source.section': {\n 'zh-CN': '模型来源',\n en: 'Model source',\n },\n 'panel.source.sameAsConnection': {\n 'zh-CN': '与插头相同',\n en: 'Same as current connection',\n },\n 'panel.source.custom': {\n 'zh-CN': '自定义',\n en: 'Custom',\n },\n 'panel.source.more': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.profile.section': {\n 'zh-CN': 'API 方案',\n en: 'API profiles',\n },\n 'panel.source.profile.current': {\n 'zh-CN': '当前方案',\n en: 'Current profile',\n },\n 'panel.source.profile.ariaLabel': {\n 'zh-CN': 'API 方案',\n en: 'API profile',\n },\n 'panel.source.profile.manual': {\n 'zh-CN': '(手动编辑,未绑定方案)',\n en: '(Manual settings, not bound to a profile)',\n },\n 'panel.source.profile.newName': {\n 'zh-CN': '新方案名称',\n en: 'New profile name',\n },\n 'panel.source.profile.save': {\n 'zh-CN': '保存当前方案',\n en: 'Save current profile',\n },\n 'panel.source.profile.saveAs': {\n 'zh-CN': '另存为新方案',\n en: 'Save as new profile',\n },\n 'panel.source.profile.delete': {\n 'zh-CN': '删除当前方案',\n en: 'Delete current profile',\n },\n 'panel.source.profile.backendCustom': {\n 'zh-CN': '自定义 API',\n en: 'Custom API',\n },\n 'panel.source.profile.backendPi': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.apiAddress': {\n 'zh-CN': 'API 地址',\n en: 'API address',\n },\n 'panel.source.apiKey': {\n 'zh-CN': 'API 密钥',\n en: 'API key',\n },\n 'panel.source.apiKeyPlaceholder': {\n 'zh-CN': '留空表示无需密钥',\n en: 'Leave blank when no key is required',\n },\n 'panel.source.modelName': {\n 'zh-CN': '模型名称',\n en: 'Model name',\n },\n // “更多”来源的连接、协议能力和认证界面文案;底层 Pi 标识不作为产品名称展示。\n 'panel.source.pi.provider': {\n 'zh-CN': '来源',\n en: 'Provider',\n },\n 'panel.source.pi.endpoint': {\n 'zh-CN': 'API 基础地址',\n en: 'API base URL',\n },\n 'panel.source.pi.endpointDefault': {\n 'zh-CN': '留空使用默认地址:{endpoint}',\n en: 'Leave blank to use the default: {endpoint}',\n },\n 'panel.source.pi.endpointPlaceholder': {\n 'zh-CN': '请输入 API 基础地址',\n en: 'Enter an API base URL',\n },\n 'panel.source.pi.endpointHelp': {\n 'zh-CN':\n '填写服务商提供的 API 地址,留空使用默认地址。也可以直接粘贴完整的聊天或 Responses 接口地址,MVU 会自动处理末尾路径。',\n en: 'Enter the API URL supplied by your provider, or leave it blank to use the default. You can also paste a full chat or Responses URL; MVU adjusts its ending automatically.',\n },\n 'panel.source.pi.proxy.use': {\n 'zh-CN': '使用 Proxy',\n en: 'Use Proxy',\n },\n 'panel.source.pi.proxy.help': {\n 'zh-CN':\n '如果获取模型列表或生成请求因 CORS(浏览器跨域限制)失败,可以尝试勾选,通过 SillyTavern 转发请求。需要先在 SillyTavern 中开启 Proxy。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'If fetching models or generating a response fails because of CORS (browser cross-origin restrictions), try enabling this to forward requests through SillyTavern. Proxy must also be enabled in SillyTavern. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.proxy.notEnabled': {\n 'zh-CN': '当前来源需要 Proxy,但 SillyTavern 的 Proxy 未开启或无法连接。',\n en: 'This provider needs Proxy, but SillyTavern Proxy is disabled or unavailable.',\n },\n 'panel.source.pi.proxy.notEnabledHelp': {\n 'zh-CN':\n '打开 SillyTavern 的 config.yaml,将 enableCorsProxy 设为 true,保存后重启 SillyTavern。如果酒馆由他人托管,请联系管理员开启。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'Open SillyTavern config.yaml, set enableCorsProxy to true, save, and restart SillyTavern. If someone else hosts your server, ask its administrator to enable it. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.model': {\n 'zh-CN': '模型',\n en: 'Model',\n },\n 'panel.source.pi.catalogModel': {\n 'zh-CN': '模型目录',\n en: 'Model catalog',\n },\n 'panel.source.pi.customModel': {\n 'zh-CN': '(手动填写模型)',\n en: '(Enter a model manually)',\n },\n 'panel.source.pi.api.openaiResponses': {\n 'zh-CN': 'OpenAI Responses',\n en: 'OpenAI Responses',\n },\n 'panel.source.pi.api.openaiCompletions': {\n 'zh-CN': 'OpenAI Chat Completions',\n en: 'OpenAI Chat Completions',\n },\n 'panel.source.pi.api.openaiCodexResponses': {\n 'zh-CN': 'OpenAI Codex Responses',\n en: 'OpenAI Codex Responses',\n },\n 'panel.source.pi.api.anthropicMessages': {\n 'zh-CN': 'Anthropic Messages',\n en: 'Anthropic Messages',\n },\n 'panel.source.pi.api.googleGenerativeAi': {\n 'zh-CN': 'Google Generative AI',\n en: 'Google Generative AI',\n },\n 'panel.source.pi.api.mistralConversations': {\n 'zh-CN': 'Mistral Conversations',\n en: 'Mistral Conversations',\n },\n 'panel.source.pi.auth.apiKey': {\n 'zh-CN': 'API Key',\n en: 'API key',\n },\n 'panel.source.pi.capabilities': {\n 'zh-CN': '可尝试的功能:{capabilities}。具体是否可用,以你使用的模型和服务商为准。',\n en: 'Features you can try: {capabilities}. Availability depends on your model and provider.',\n },\n 'panel.source.pi.capability.tools': {\n 'zh-CN': '工具调用',\n en: 'tools',\n },\n 'panel.source.pi.capability.images': {\n 'zh-CN': '读取图片',\n en: 'Read images',\n },\n 'panel.source.pi.capability.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.source.pi.thinkingLevel': {\n 'zh-CN': '思考等级',\n en: 'Thinking level',\n },\n 'panel.source.pi.thinkingLevelHelp': {\n 'zh-CN':\n '默认保持现有请求行为;其他等级按模型能力映射,部分模型无法完全关闭思考。未知模型按协议转换所选等级;OpenAI/Mistral 的极高、最高会原样尝试,Google/预算式思考仍按协议映射。Anthropic 开启思考时不支持强制工具调用,旧模型至少需要 2048 回复 token。自定义请求体覆盖仍优先。',\n en: 'Default preserves existing requests. Other levels are mapped to model capabilities; some models cannot fully disable thinking. Unknown models use protocol-specific mappings: OpenAI/Mistral attempt xhigh/max unchanged, while Google and budget-based thinking use protocol mappings. Anthropic thinking cannot force tool calls; older models need at least 2048 reply tokens. Custom body overrides take precedence.',\n },\n 'panel.source.pi.thinking.default': {\n 'zh-CN': '默认(保持原行为)',\n en: 'Default (unchanged behavior)',\n },\n 'panel.source.pi.thinking.off': {\n 'zh-CN': '关闭 / 最低',\n en: 'Off / minimum',\n },\n 'panel.source.pi.thinking.minimal': {\n 'zh-CN': '极低',\n en: 'Minimal',\n },\n 'panel.source.pi.thinking.low': {\n 'zh-CN': '低',\n en: 'Low',\n },\n 'panel.source.pi.thinking.medium': {\n 'zh-CN': '中',\n en: 'Medium',\n },\n 'panel.source.pi.thinking.high': {\n 'zh-CN': '高',\n en: 'High',\n },\n 'panel.source.pi.thinking.xhigh': {\n 'zh-CN': '极高',\n en: 'Extra high',\n },\n 'panel.source.pi.thinking.max': {\n 'zh-CN': '最高',\n en: 'Maximum',\n },\n 'panel.source.pi.contextWindow': {\n 'zh-CN': '上下文窗口',\n en: 'Context window',\n },\n 'panel.source.pi.contextWindowCatalog': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。已自动填入 {value};如果服务商给出的限制不同,可手动修改,清空后恢复自动值。',\n en: 'The total length the model can read and generate, in tokens. Automatically set to {value}. Edit it if your provider gives a different limit; clear it to restore the automatic value.',\n },\n 'panel.source.pi.contextWindowOverride': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。当前使用你填写的值;清空后会尝试自动填写已知模型的限制。',\n en: 'The total length the model can read and generate, in tokens. Using your entered value; clear it to try the known model limit.',\n },\n 'panel.source.pi.contextWindowRequired': {\n 'zh-CN': '请输入正整数上下文窗口;模型目录未提供该值时必须手动填写。',\n en: 'Enter a positive integer context window. It is required when the model catalog has no value.',\n },\n 'panel.source.pi.error.unknownProvider': {\n 'zh-CN': '“更多”中的未知来源“{provider}”;请重新选择来源。',\n en: 'Unknown provider \"{provider}\" under More. Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedApi': {\n 'zh-CN': '已保存的来源“{provider}”不支持接口“{api}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support API \"{api}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedAuth': {\n 'zh-CN': '已保存的来源“{provider}”不支持认证方式“{auth}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support authentication type \"{auth}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedEndpoint': {\n 'zh-CN': '当前来源不支持自定义 API 地址,请重新选择来源或切换 API 方案。',\n en: 'This provider does not support a custom API URL. Select a provider again or switch API profiles.',\n },\n 'panel.source.pi.featureDisabled': {\n 'zh-CN':\n '此构建已关闭“更多”模型来源。已保存的“更多”配置仍会保留;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Saved More-source settings are preserved; select Same as current connection or Custom.',\n },\n 'panel.source.pi.maxTokensPositive': {\n 'zh-CN': '最大回复 token 必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'panel.source.pi.maxTokensExceedContext': {\n 'zh-CN': '最大回复 token 不能大于上下文窗口。',\n en: 'Maximum response tokens cannot exceed the context window.',\n },\n 'panel.source.pi.customHeaders': {\n 'zh-CN': '自定义请求头',\n en: 'Custom request headers',\n },\n 'panel.source.pi.customHeadersHelp': {\n 'zh-CN':\n '仅在服务商要求额外请求头时填写,格式为 YAML 或 JSON,例如 X-App: MVU。留空即可;API 密钥请填在上方的密钥栏。',\n en: 'Fill this in only if your provider requires extra headers. Use YAML or JSON, for example X-App: MVU. Otherwise leave it blank. Enter your API key in the key field above.',\n },\n 'panel.source.pi.customOverridesSwitchHelp': {\n 'zh-CN':\n '下方的自定义请求头、附加字段和排除字段会随 API 方案保存。手动切换来源或 API 地址时会清空这三项;切换方案会恢复该方案保存的内容。',\n en: 'Custom headers, additional fields, and excluded fields below are saved with the API profile. Manually changing the provider or API URL clears these three fields; switching profiles restores their saved values.',\n },\n 'panel.source.pi.customIncludeBody': {\n 'zh-CN': '请求体附加字段',\n en: 'Additional request body fields',\n },\n 'panel.source.pi.customIncludeBodyHelp': {\n 'zh-CN':\n '服务商要求额外参数时在这里填写,使用 YAML 或 JSON,例如 seed: 123。同名参数会采用这里的值。Google 参数请放在 config 下。不需要时留空;密钥、模型和流式开关请使用对应选项。',\n en: 'Add parameters required by your provider as YAML or JSON, for example seed: 123. These values replace matching parameters. Put Google parameters under config. Leave blank when unnecessary; use the dedicated controls for keys, models, and streaming.',\n },\n 'panel.source.pi.customExcludeBody': {\n 'zh-CN': '请求体排除字段',\n en: 'Excluded request body fields',\n },\n 'panel.source.pi.customExcludeBodyHelp': {\n 'zh-CN':\n '如果服务商提示某个参数不被支持,可在这里填写参数名,使请求不再携带它。多个名称用换行或逗号分隔,也支持 YAML 列表。Google 参数写为 config.<参数名>。不需要时留空。',\n en: 'If your provider rejects a parameter, enter its name here to leave it out of requests. Separate names with newlines or commas, or use a YAML list. For Google, use config.<parameter>. Otherwise leave blank.',\n },\n 'panel.source.pi.clearCustomField': {\n 'zh-CN': '清空',\n en: 'Clear',\n },\n 'panel.source.pi.oauth.section': {\n 'zh-CN': 'OAuth 登录',\n en: 'OAuth login',\n },\n 'panel.source.pi.oauth.status': {\n 'zh-CN': '状态',\n en: 'Status',\n },\n 'panel.source.pi.oauth.checking': {\n 'zh-CN': '检查中…',\n en: 'Checking…',\n },\n 'panel.source.pi.oauth.loggedIn': {\n 'zh-CN': '已登录',\n en: 'Signed in',\n },\n 'panel.source.pi.oauth.loggedOut': {\n 'zh-CN': '未登录',\n en: 'Not signed in',\n },\n 'panel.source.pi.oauth.expiresAt': {\n 'zh-CN': '凭据有效期:{time}',\n en: 'Credential expiry: {time}',\n },\n 'panel.source.pi.oauth.login': {\n 'zh-CN': '登录',\n en: 'Sign in',\n },\n 'panel.source.pi.oauth.refresh': {\n 'zh-CN': '刷新凭证',\n en: 'Refresh credentials',\n },\n 'panel.source.pi.oauth.refreshing': {\n 'zh-CN': '正在刷新凭证…',\n en: 'Refreshing credentials…',\n },\n 'panel.source.pi.oauth.refreshSucceeded': {\n 'zh-CN': '凭证已刷新。',\n en: 'Credentials refreshed.',\n },\n 'panel.source.pi.oauth.cancel': {\n 'zh-CN': '取消登录',\n en: 'Cancel sign-in',\n },\n 'panel.source.pi.oauth.logout': {\n 'zh-CN': '登出',\n en: 'Sign out',\n },\n 'panel.source.pi.oauth.authorizationUrl': {\n 'zh-CN': '授权链接',\n en: 'Authorization URL',\n },\n 'panel.source.pi.oauth.openAuthorization': {\n 'zh-CN': '打开授权页',\n en: 'Open authorization page',\n },\n 'panel.source.pi.oauth.copyAuthorization': {\n 'zh-CN': '复制授权链接',\n en: 'Copy authorization URL',\n },\n 'panel.source.pi.oauth.copySucceeded': {\n 'zh-CN': '已复制授权链接',\n en: 'Authorization URL copied',\n },\n 'panel.source.pi.oauth.copyFailed': {\n 'zh-CN': '无法复制授权链接,请手动选择并复制。',\n en: 'Could not copy the authorization URL. Select and copy it manually.',\n },\n 'panel.source.pi.oauth.callbackUrl': {\n 'zh-CN': '回调链接',\n en: 'Callback URL',\n },\n 'panel.source.pi.oauth.callbackHelp': {\n 'zh-CN':\n '完成授权后,如果跳转到的页面打不开,不影响登录。请复制浏览器地址栏中以 127.0.0.1 或 localhost 开头的完整链接,粘贴到这里,再点击“完成登录”。',\n en: 'After authorization, the redirected page may fail to open; you can still finish signing in. Copy the full URL starting with 127.0.0.1 or localhost from the address bar, paste it here, and click Complete sign-in.',\n },\n 'panel.source.pi.oauth.complete': {\n 'zh-CN': '完成登录',\n en: 'Complete sign-in',\n },\n 'panel.source.pi.oauth.preparing': {\n 'zh-CN': '正在准备安全授权链接…',\n en: 'Preparing a secure authorization URL…',\n },\n 'panel.source.pi.oauth.waitingCallback': {\n 'zh-CN': '请在授权页完成登录,然后粘贴完整回调链接。',\n en: 'Finish signing in on the authorization page, then paste the complete callback URL.',\n },\n 'panel.source.pi.oauth.exchanging': {\n 'zh-CN': '正在验证回调并交换凭据…',\n en: 'Validating the callback and exchanging credentials…',\n },\n 'panel.source.pi.oauth.loginSucceeded': {\n 'zh-CN': 'OAuth 登录成功。',\n en: 'OAuth sign-in succeeded.',\n },\n 'panel.source.pi.oauth.logoutSucceeded': {\n 'zh-CN': '已登出。',\n en: 'Signed out.',\n },\n 'panel.source.pi.oauth.logoutConfirm': {\n 'zh-CN': '确定在当前配置中登出 {provider} 吗?之后可以登录其他账号。',\n en: 'Sign out of {provider} for the current configuration? You can then sign in with another account.',\n },\n 'panel.source.pi.oauth.failureTitle': {\n 'zh-CN': '[MVU]OAuth 操作失败',\n en: '[MVU] OAuth operation failed',\n },\n 'panel.source.advanced': {\n 'zh-CN': '高级参数',\n en: 'Advanced parameters',\n },\n 'panel.source.unsupportedAdvanced': {\n 'zh-CN': '⚠️酒馆助手版本过低,不支持以下配置',\n en: '⚠️ Your Tavern Helper version is too old to support the settings below',\n },\n 'panel.source.maxTokens': {\n 'zh-CN': '最大回复 token',\n en: 'Maximum response tokens',\n },\n 'panel.source.chatHistory': {\n 'zh-CN': '聊天历史条数',\n en: 'Chat history entries',\n },\n 'panel.source.temperature': {\n 'zh-CN': '温度',\n en: 'Temperature',\n },\n 'panel.source.frequencyPenalty': {\n 'zh-CN': '频率惩罚',\n en: 'Frequency penalty',\n },\n 'panel.source.presencePenalty': {\n 'zh-CN': '存在惩罚',\n en: 'Presence penalty',\n },\n 'panel.source.topP': {\n 'zh-CN': 'Top P',\n en: 'Top P',\n },\n 'panel.source.topK': {\n 'zh-CN': 'Top K',\n en: 'Top K',\n },\n 'panel.source.switchDirty': {\n 'zh-CN': '当前方案有未保存的修改,切换将丢弃这些修改。是否继续?',\n en: 'The current profile has unsaved changes. Switching will discard them. Continue?',\n },\n 'panel.source.continue': {\n 'zh-CN': '继续',\n en: 'Continue',\n },\n 'panel.source.discardChanges': {\n 'zh-CN': '丢弃修改',\n en: 'Discard changes',\n },\n 'panel.source.delete': {\n 'zh-CN': '删除',\n en: 'Delete',\n },\n 'panel.source.deleteDirty': {\n 'zh-CN': '当前方案“{name}”有未保存的修改,删除将丢弃这些修改。是否继续?',\n en: 'Profile “{name}” has unsaved changes. Deleting it will discard them. Continue?',\n },\n 'panel.source.deleteConfirm': {\n 'zh-CN': '确定删除 API 方案“{name}”吗?此操作不可撤销。',\n en: 'Delete API profile “{name}”? This action cannot be undone.',\n },\n 'panel.source.profileSaved': {\n 'zh-CN': '已保存 API 方案“{name}”',\n en: 'Saved API profile “{name}”',\n },\n 'panel.source.profileSavedAs': {\n 'zh-CN': '已另存为 API 方案“{name}”',\n en: 'Saved as API profile “{name}”',\n },\n 'panel.source.profileDeleted': {\n 'zh-CN': '已删除 API 方案“{name}”',\n en: 'Deleted API profile “{name}”',\n },\n 'panel.source.enterProfileName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a new profile name first',\n },\n 'panel.source.keepTwoProfiles': {\n 'zh-CN': '至少保留两个 API 方案时才可删除',\n en: 'At least two API profiles are required before one can be deleted',\n },\n 'panel.source.switchFailureTitle': {\n 'zh-CN': '[MVU]切换 API 方案失败',\n en: '[MVU] Failed to switch API profile',\n },\n 'panel.source.saveFailureTitle': {\n 'zh-CN': '[MVU]保存 API 方案失败',\n en: '[MVU] Failed to save API profile',\n },\n 'panel.source.deleteFailureTitle': {\n 'zh-CN': '[MVU]删除 API 方案失败',\n en: '[MVU] Failed to delete API profile',\n },\n 'panel.character.titleActive': {\n 'zh-CN': '当前角色卡配置(覆盖中)',\n en: 'Current character-card settings (overriding)',\n },\n 'panel.character.titleInactive': {\n 'zh-CN': '当前角色卡配置(未启用)',\n en: 'Current character-card settings (not enabled)',\n },\n 'panel.character.worldBook': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.character.reading': {\n 'zh-CN': '正在读取…',\n en: 'Reading…',\n },\n 'panel.character.unbound': {\n 'zh-CN': '未绑定',\n en: 'Not bound',\n },\n 'panel.character.inherit': {\n 'zh-CN': '跟随用户配置',\n en: 'Inherit user settings',\n },\n 'panel.character.extraModelGroup': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.character.autoRequest': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.character.whitelist': {\n 'zh-CN': '角色卡世界书条目白名单正则',\n en: 'Character-card world-book entry whitelist regex',\n },\n 'panel.character.blacklist': {\n 'zh-CN': '角色卡世界书条目黑名单正则',\n en: 'Character-card world-book entry blacklist regex',\n },\n 'panel.character.regexInvalid': {\n 'zh-CN': '角色卡配置正则无效:{error}',\n en: 'Invalid character-card regex: {error}',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const runtimeMessages = defineMessages({\n 'runtime.common.mvuTitle': {\n 'zh-CN': '[MVU]',\n en: '[MVU]',\n },\n 'runtime.common.errorCause': {\n 'zh-CN': '错误详情:{cause}',\n en: 'Error details: {cause}',\n },\n 'runtime.common.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n\n 'runtime.main.chatReinitializeFailedLog': {\n 'zh-CN': '[MVU]切换聊天后重新初始化失败',\n en: '[MVU] Failed to reinitialize after switching chats',\n },\n 'runtime.main.reinitializeFailedTitle': {\n 'zh-CN': '[MVU]重新初始化失败',\n en: '[MVU] Reinitialization failed',\n },\n 'runtime.main.minimumVersionRequired': {\n 'zh-CN': 'MVU 变量框架需要酒馆助手版本不低于 {version}',\n en: 'MVU Variable Framework requires Tavern Helper {version} or later',\n },\n 'runtime.main.versionIncompatibleTitle': {\n 'zh-CN': '版本不兼容',\n en: 'Incompatible version',\n },\n\n 'runtime.parseString.invalidFormat': {\n 'zh-CN': '要解析的字符串不是有效的 YAML/JSON/JSON5 格式',\n en: 'The input is not valid YAML, JSON, or JSON5',\n },\n 'runtime.parseString.content': {\n 'zh-CN': '字符串内容',\n en: 'Input',\n },\n 'runtime.parseString.yamlError': {\n 'zh-CN': 'YAML 错误信息',\n en: 'YAML error',\n },\n 'runtime.parseString.json5Error': {\n 'zh-CN': 'JSON5 错误信息',\n en: 'JSON5 error',\n },\n 'runtime.parseString.jsonError': {\n 'zh-CN': 'JSON 错误信息',\n en: 'JSON error',\n },\n\n 'runtime.button.initVarDataNotFound': {\n 'zh-CN': '没有找到 InitVar 数据',\n en: 'No InitVar data was found',\n },\n 'runtime.button.initVarDataLoadFailed': {\n 'zh-CN': '加载 InitVar 数据失败:',\n en: 'Failed to load InitVar data',\n },\n 'runtime.button.messageNotFound': {\n 'zh-CN': '没有找到消息',\n en: 'No message was found',\n },\n 'runtime.button.initVarDescriptionsUpdated': {\n 'zh-CN': 'InitVar描述已更新',\n en: 'InitVar descriptions updated',\n },\n 'runtime.button.snapshotPrompt': {\n 'zh-CN':\n '

设置快照楼层可以避免指定的楼层在清理操作中被移除变量信息

请填写要保留变量信息的楼层(如 10 为第 10 层)
后续楼层的重演将可以从这一层开始',\n en: '

Marking a floor as a snapshot prevents its variable data from being removed during cleanup.

Enter the floor whose variable data should be retained (for example, 10 for floor 10).
Later floors can then be replayed starting from this floor.',\n },\n 'runtime.button.invalidFloorInput': {\n 'zh-CN': '请输入有效的楼层数,你输入的是“{value}”',\n en: 'Enter a valid floor number. You entered “{value}”.',\n },\n 'runtime.button.invalidFloor': {\n 'zh-CN': '无效的楼层“{value}”',\n en: 'Invalid floor “{value}”.',\n },\n 'runtime.button.snapshotConfigureFailedTitle': {\n 'zh-CN': '[MVU]配置楼层快照失败',\n en: '[MVU] Failed to configure floor snapshot',\n },\n 'runtime.button.snapshotConfigureTitle': {\n 'zh-CN': '[MVU]配置楼层快照',\n en: '[MVU] Configure floor snapshot',\n },\n 'runtime.button.snapshotConfigured': {\n 'zh-CN': '已将 {floor} 层配置为快照楼层',\n en: 'Floor {floor} is now a snapshot floor',\n },\n 'runtime.button.replayPrompt': {\n 'zh-CN':\n '

当变量更新出现 required/extensible 相关问题时,可以尝试从过去的楼层重演

请填写要进行重演的楼层(如 10 为第 10 层,-1 为最新楼层)
也就是出现问题的楼层',\n en: '

If variable updates have required/extensible issues, replaying from an earlier floor may help.

Enter the floor to replay (for example, 10 for floor 10, or -1 for the latest floor).
This should be the floor where the problem occurred.',\n },\n 'runtime.button.replayFailedTitle': {\n 'zh-CN': '[MVU]楼层重演失败',\n en: '[MVU] Floor replay failed',\n },\n 'runtime.button.replayNoAvailableFloor': {\n 'zh-CN': '无法找到可以进行重演的楼层',\n en: 'No floor with replayable variable data was found',\n },\n 'runtime.button.replayStartPrompt': {\n 'zh-CN': '请填写从哪个楼层开始重演,找到最近的支持重演楼层为 [{floor}]',\n en: 'Enter the floor from which to start replaying. The nearest replayable floor is [{floor}].',\n },\n 'runtime.button.replayNeedsVariables': {\n 'zh-CN': '请输入含变量信息的楼层,你输入的是“{value}”',\n en: 'Enter a floor that contains variable data. You entered “{value}”.',\n },\n 'runtime.button.replayProgress': {\n 'zh-CN': '处理变量中({processed} / {total})',\n en: 'Processing variables ({processed} / {total})',\n },\n 'runtime.button.replayingFloorLog': {\n 'zh-CN': '正在重演 {index},内容:{content}',\n en: 'Replaying {index}. Content: {content}',\n },\n 'runtime.button.replayTitle': {\n 'zh-CN': '[MVU]楼层重演',\n en: '[MVU] Floor replay',\n },\n 'runtime.button.replayCompleted': {\n 'zh-CN': '已将 {floor} 层变量状态重演完毕,共重演 {count} 楼',\n en: 'Variable state replay for floor {floor} is complete ({count} floors replayed)',\n },\n 'runtime.button.extraModelNotEnabled': {\n 'zh-CN': '当前配置没有启用额外模型解析,不需要进行此操作',\n en: 'Extra-model parsing is not enabled by the current configuration',\n },\n 'runtime.button.extraModelToolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或配置指定的 LLM 不支持工具调用,请调整额外模型解析设置',\n en: 'The current TavernHelper version or configured LLM does not support tool calling. Adjust the extra-model parsing settings.',\n },\n 'runtime.button.extraModelUnsupportedByCard': {\n 'zh-CN': '当前角色卡不支持额外模型解析,无法进行此操作',\n en: 'The current character card does not support extra-model parsing',\n },\n 'runtime.button.extraModelRetryTitle': {\n 'zh-CN': '[MVU]重试额外模型解析',\n en: '[MVU] Retry extra-model parsing',\n },\n 'runtime.button.extraModelParsingCompleted': {\n 'zh-CN': '解析完成',\n en: 'Parsing completed',\n },\n 'runtime.incrementalRepair.title': {\n 'zh-CN': '[MVU]增量校正变量',\n en: '[MVU] Incremental variable repair',\n },\n 'runtime.incrementalRepair.alreadyRunning': {\n 'zh-CN': '已有一项增量校正正在运行',\n en: 'An incremental repair is already running',\n },\n 'runtime.incrementalRepair.noUsableFloor': {\n 'zh-CN': '最新楼层或上一楼没有可用的 MVU 状态,无法进行增量校正',\n en: 'The latest or previous floor has no usable MVU state',\n },\n 'runtime.incrementalRepair.directionPrompt': {\n 'zh-CN':\n '可选:补充本次增量校正方向;直接确认留空则自动审计。\\n例如:核对生命值归零后的即时后果,或补齐本轮明确获得的物品。',\n en: 'Optional: add a direction for this incremental repair. Confirm with an empty input for automatic audit.\\nFor example: check the immediate zero-HP aftermath or add an item clearly obtained this floor.',\n },\n 'runtime.incrementalRepair.requestFailed': {\n 'zh-CN': '增量校正请求失败或已被中止,未修改变量',\n en: 'The incremental repair failed or was cancelled; variables were not changed',\n },\n 'runtime.incrementalRepair.replayFailed': {\n 'zh-CN': '整楼重放失败,未提交校正:\\n{detail}',\n en: 'Full-floor replay failed. No repair was committed:\\n{detail}',\n },\n 'runtime.incrementalRepair.sourceChanged': {\n 'zh-CN': '等待期间聊天、楼层、回复版本或变量已经变化,本次结果已作废',\n en: 'The chat, floor, reply version, or variables changed while waiting; this result was discarded',\n },\n 'runtime.incrementalRepair.noEffectiveChanges': {\n 'zh-CN': '校正补丁没有产生实际变化,未写入正文或变量',\n en: 'The repair patch made no effective changes and was not written',\n },\n 'runtime.incrementalRepair.previewTitle': {\n 'zh-CN': '增量校正预览',\n en: 'Incremental repair preview',\n },\n 'runtime.incrementalRepair.previewDescription': {\n 'zh-CN':\n '模型提出 {count} 项修正。确认后合并本楼更新块,并从上一楼变量完整重算本楼,统一保存正文与变量。',\n en: 'The model proposed {count} repairs. The merged floor will be replayed from the preceding variables, then saved together with its resulting state.',\n },\n 'runtime.incrementalRepair.showRawPatch': {\n 'zh-CN': '查看原始补丁',\n en: 'Show raw patch',\n },\n 'runtime.incrementalRepair.applyButton': {\n 'zh-CN': '应用修正',\n en: 'Apply repairs',\n },\n 'runtime.incrementalRepair.cancelButton': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n 'runtime.incrementalRepair.appliedClickToUndo': {\n 'zh-CN': '增量校正已应用并写回正文;点击此通知可撤销',\n en: 'Incremental repair applied and written back; click this notice to undo',\n },\n 'runtime.incrementalRepair.undoStateChanged': {\n 'zh-CN': '变量或回复在校正后又发生了变化,为避免覆盖新进度,不能直接撤销',\n en: 'Variables or the reply changed after repair; undo was blocked to protect newer progress',\n },\n 'runtime.incrementalRepair.undone': {\n 'zh-CN': '已撤销此次增量校正',\n en: 'The incremental repair was undone',\n },\n 'runtime.button.cleanupPrompt': {\n 'zh-CN':\n '

清除旧楼层变量信息以减小聊天文件大小,避免手机崩溃

请填写要保留变量信息的楼层数(如 10 为保留最后 10 层,每 [{interval}] 层保留一层作为快照)
注意:你需要通过重演才能回退游玩到未保留变量信息的楼层',\n en: '

Remove variable data from old floors to reduce chat file size and help prevent mobile crashes.

Enter how many recent floors to retain (for example, 10 retains the latest 10 floors, with one snapshot every [{interval}] floors).
Note: you must replay variables before returning to floors whose variable data was removed.',\n },\n 'runtime.button.cleanupFailedTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量失败',\n en: '[MVU] Failed to clean old floor variables',\n },\n 'runtime.button.cleanupSucceededTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量成功',\n en: '[MVU] Old floor variables cleaned',\n },\n 'runtime.button.cleanupCompleted': {\n 'zh-CN': '已清理旧变量,保留了最后 {depth} 层的变量',\n en: 'Old variables were cleaned; variables for the latest {depth} floors were retained',\n },\n 'runtime.button.snapshotMarkedLog': {\n 'zh-CN': '将 [{floor}] 层作为快照楼层',\n en: 'Marked floor [{floor}] as a snapshot floor',\n },\n\n 'runtime.characterOverride.worldbookReadFailed': {\n 'zh-CN': '无法读取角色世界书“{worldbook}”',\n en: 'Unable to read character lorebook “{worldbook}”',\n },\n 'runtime.characterOverride.contentMustBeObject': {\n 'zh-CN': '配置正文必须是 JSON 对象',\n en: 'The settings content must be a JSON object',\n },\n 'runtime.characterOverride.conflictPrompt': {\n 'zh-CN':\n '角色世界书“{worldbook}”中的 {entry} 已被其他来源修改。是否使用待保存的角色卡配置覆盖它?',\n en: '{entry} in character lorebook “{worldbook}” was modified by another source. Overwrite it with the pending character-card configuration?',\n },\n 'runtime.characterOverride.overwriteButton': {\n 'zh-CN': '覆盖',\n en: 'Overwrite',\n },\n 'runtime.characterOverride.loadLatestButton': {\n 'zh-CN': '加载最新配置',\n en: 'Load latest configuration',\n },\n 'runtime.characterOverride.bindingReadFailedLog': {\n 'zh-CN': '[MVU]读取角色世界书绑定失败',\n en: '[MVU] Failed to read the character lorebook binding',\n },\n 'runtime.characterOverride.readFailedLog': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read the character-card configuration',\n },\n 'runtime.characterOverride.readFailedTitle': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read character-card configuration',\n },\n 'runtime.characterOverride.stopWithUnsavedChangesLog': {\n 'zh-CN': '[MVU]停止角色卡配置控制器时仍未能保存“{worldbook}”中的修改',\n en: '[MVU] Changes in “{worldbook}” were still unsaved when the character-card configuration controller stopped',\n },\n 'runtime.characterOverride.reloadFailedLog': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload the character-card configuration',\n },\n 'runtime.characterOverride.reloadFailedTitle': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload character-card configuration',\n },\n 'runtime.characterOverride.duplicateEntries': {\n 'zh-CN':\n '角色世界书“{worldbook}”中存在 {count} 个关闭的 {entry} 条目,仅使用扫描到的第一个。',\n en: 'Character lorebook “{worldbook}” contains {count} disabled {entry} entries. Only the first scanned entry will be used.',\n },\n 'runtime.characterOverride.duplicateEntriesTitle': {\n 'zh-CN': '[MVU]角色卡配置重复',\n en: '[MVU] Duplicate character-card configuration',\n },\n 'runtime.characterOverride.invalidWorldbookData': {\n 'zh-CN': '角色世界书“{worldbook}”的数据格式无效',\n en: 'Character lorebook “{worldbook}” has an invalid data format',\n },\n 'runtime.characterOverride.invalidConfigurationLog': {\n 'zh-CN': '[MVU]角色世界书“{worldbook}”的 {entry} 配置无效',\n en: '[MVU] The {entry} configuration in character lorebook “{worldbook}” is invalid',\n },\n 'runtime.characterOverride.invalidConfiguration': {\n 'zh-CN': '角色世界书“{worldbook}”中的配置无效,已按不存在处理:{cause}',\n en: 'The configuration in character lorebook “{worldbook}” is invalid and will be treated as absent: {cause}',\n },\n 'runtime.characterOverride.invalidConfigurationTitle': {\n 'zh-CN': '[MVU]角色卡配置无效',\n en: '[MVU] Invalid character-card configuration',\n },\n 'runtime.characterOverride.saveFailedLog': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save the character-card configuration',\n },\n 'runtime.characterOverride.saveFailedTitle': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save character-card configuration',\n },\n\n 'runtime.cleanup.title': {\n 'zh-CN': '[MVU]自动清理',\n en: '[MVU] Automatic cleanup',\n },\n 'runtime.cleanup.legacyPrompt': {\n 'zh-CN':\n '检测到可以清理本聊天文件中的旧变量以减小文件体积,是否清理?(备份会消耗较多内存,手机上建议关闭其他后台应用后进行,或在计算机上备份)',\n en: 'Old variables can be removed from this chat to reduce its file size. Clean them now? (Creating a backup uses considerable memory; on mobile, close other background apps first or create the backup on a computer.)',\n },\n 'runtime.cleanup.cleanOnlyButton': {\n 'zh-CN': '仅清理',\n en: 'Clean only',\n },\n 'runtime.cleanup.doNotRemindButton': {\n 'zh-CN': '不再提醒',\n en: 'Do not remind me again',\n },\n 'runtime.cleanup.backupAndCleanButton': {\n 'zh-CN': '备份并清理',\n en: 'Back up and clean',\n },\n 'runtime.cleanup.starting': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量…',\n en: 'Cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.startingWithBackup': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量,并自动生成备份…',\n en: 'Creating a backup, then cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.exportFailed': {\n 'zh-CN': '聊天记录导出失败,放弃清理:{cause}',\n en: 'Chat export failed; cleanup was cancelled: {cause}',\n },\n 'runtime.cleanup.exportSucceeded': {\n 'zh-CN': '聊天记录导出成功:{message}',\n en: 'Chat exported successfully: {message}',\n },\n 'runtime.cleanup.cleanedMessages': {\n 'zh-CN': '已清理旧聊天记录中的 {count} 条消息',\n en: 'Cleaned {count} messages from the old chat history',\n },\n 'runtime.cleanup.cleanedFloorsLog': {\n 'zh-CN': '[MVU]已清理 {count} 层的消息',\n en: '[MVU] Cleaned messages on {count} floors',\n },\n 'runtime.cleanup.restoreNotNeededLog': {\n 'zh-CN': '最近 {count} 层都包含变量数据,不需要进行恢复。',\n en: 'All of the latest {count} floors contain variable data; restoration is not needed.',\n },\n 'runtime.cleanup.restoreUnavailable': {\n 'zh-CN': '在 0~{floor} 层找不到有效的变量信息,无法进行楼层变量恢复',\n en: 'No valid variable data was found between floors 0 and {floor}; floor variables cannot be restored',\n },\n 'runtime.cleanup.restoreTitle': {\n 'zh-CN': '[MVU]恢复旧楼层变量',\n en: '[MVU] Restore old floor variables',\n },\n\n 'runtime.initvar.noMessagesLog': {\n 'zh-CN': '不存在任何一条消息,退出',\n en: 'No messages exist; initialization stopped',\n },\n 'runtime.initvar.greetingRequired': {\n 'zh-CN': '需要有开场白才能初始化变量',\n en: 'A greeting message is required to initialize variables',\n },\n 'runtime.initvar.initializationFailedTitle': {\n 'zh-CN': '[MVU]变量初始化失败',\n en: '[MVU] Variable initialization failed',\n },\n 'runtime.initvar.oldLorebookFormatLog': {\n 'zh-CN': '检测到旧的 initialized_lorebooks 数组格式,正在迁移到新的对象格式。',\n en: 'Old initialized_lorebooks array format detected. Migrating to the new object format.',\n },\n 'runtime.initvar.chatVariablesInitializedLog': {\n 'zh-CN': '聊天变量已初始化。',\n en: 'Chat variables initialized.',\n },\n 'runtime.initvar.blockParseFailedLog': {\n 'zh-CN': '解析 initvar 块失败:{cause}',\n en: 'Failed to parse initvar block: {cause}',\n },\n 'runtime.initvar.initializationCompletedLog': {\n 'zh-CN': '变量初始化完成',\n en: 'Variable initialization completed',\n },\n 'runtime.initvar.initializationSucceeded': {\n 'zh-CN': '已加载新的世界书初始化变量,当前使用世界书:
{books}',\n en: 'New lorebook initialization variables were loaded. Active lorebooks:
{books}',\n },\n 'runtime.initvar.initializationSucceededTitle': {\n 'zh-CN': '[MVU]变量初始化成功',\n en: '[MVU] Variable initialization succeeded',\n },\n 'runtime.initvar.primaryLorebookReadFailedLog': {\n 'zh-CN': '获取角色主 lorebook 失败,已忽略',\n en: 'Failed to obtain the primary character lorebook; ignoring it',\n },\n 'runtime.initvar.entryParseFailedLog': {\n 'zh-CN': '解析世界书条目“{comment}”失败:{cause}',\n en: 'Failed to parse lorebook entry “{comment}”: {cause}',\n },\n 'runtime.initvar.entryParseFailedTitle': {\n 'zh-CN': '[MVU]解析世界书条目“{comment}”失败',\n en: '[MVU] Failed to parse lorebook entry “{comment}”',\n },\n 'runtime.initvar.noMessages': {\n 'zh-CN': '不存在任何一条消息',\n en: 'No messages exist',\n },\n\n 'runtime.notification.settingsUiUpdatedTitle': {\n 'zh-CN': '[MVU]已更新独立配置界面',\n en: '[MVU] Dedicated settings panel updated',\n },\n 'runtime.notification.settingsUiUpdatedBody': {\n 'zh-CN': '配置界面位于酒馆扩展界面中的“正则”下方,请点开了解新功能或自定义配置',\n en: 'The settings panel is located below “Regex” in SillyTavern Extensions. Open it to review new features or customize MVU.',\n },\n 'runtime.notification.autoCleanupFeatureTitle': {\n 'zh-CN': '[MVU]已更新自动清理旧变量功能',\n en: '[MVU] Automatic cleanup updated',\n },\n 'runtime.notification.autoCleanupFeatureBody': {\n 'zh-CN':\n 'MVU 现在可以自动清理旧变量来减少聊天文件大小;这不会影响你回退游玩以前的楼层。可在设置中开启“变量自动清理”。',\n en: 'MVU can now automatically remove old variable data to reduce chat file size without preventing you from returning to earlier floors. Enable Automatic Variable Cleanup in settings.',\n },\n 'runtime.notification.customApiOptionsTitle': {\n 'zh-CN': '[MVU]已更新更多自定义 API 配置',\n en: '[MVU] More custom API options added',\n },\n 'runtime.notification.customApiOptionsBody': {\n 'zh-CN':\n 'MVU 现在可以自定义 API 的温度、频率惩罚、存在惩罚和最大回复 token 数;需要酒馆助手版本 >= 4.0.14。',\n en: 'MVU now supports custom API temperature, frequency penalty, presence penalty, and maximum response tokens. TavernHelper >= 4.0.14 is required.',\n },\n 'runtime.notification.cleanupDefaultTitle': {\n 'zh-CN': '[MVU]已更新自动清理配置',\n en: '[MVU] Automatic cleanup settings updated',\n },\n 'runtime.notification.cleanupDefaultBody': {\n 'zh-CN': 'MVU 现在会自动清理较老楼层上的变量信息,以降低聊天文件大小。',\n en: 'MVU now automatically removes variable data from older floors to reduce chat file size.',\n },\n 'runtime.notification.builtinJailbreakTitle': {\n 'zh-CN': '[MVU]已内置破限',\n en: '[MVU] Built-in jailbreak prompt added',\n },\n 'runtime.notification.builtinJailbreakBody': {\n 'zh-CN':\n '现在,额外模型解析如果取消“发送预设”,则会使用内置的破限提示词——既避免写作任务又避免道歉。',\n en: 'When Send Preset is disabled, extra-model parsing now uses a built-in jailbreak prompt designed to avoid both writing tasks and refusals.',\n },\n 'runtime.notification.concurrentRequestsTitle': {\n 'zh-CN': '[MVU]已支持同时多次请求变量更新',\n en: '[MVU] Concurrent variable-update requests added',\n },\n 'runtime.notification.concurrentRequestsBody': {\n 'zh-CN':\n '现在,你可以在“变量更新方式 → 请求策略 → 请求方式”中选择并发请求,提高额外模型解析成功率并节省时间。',\n en: 'You can now choose concurrent requests under Variable Update Method → Request Strategy → Request Method to improve extra-model parsing success rates and save time.',\n },\n 'runtime.notification.buildInfo': {\n 'zh-CN': '构建信息:{date}({commit})',\n en: 'Build: {date} ({commit})',\n },\n 'runtime.notification.scriptLoadedTitle': {\n 'zh-CN': '[MVU]脚本加载成功',\n en: '[MVU] Script loaded',\n },\n\n 'runtime.filter.toolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或预设/API 不支持工具调用,已退化回 `随AI输出`',\n en: 'The current TavernHelper version or preset/API does not support tool calling. Falling back to Follow AI Output.',\n },\n 'runtime.filter.toolCallingUnsupportedTitle': {\n 'zh-CN': '[MVU]无法使用工具调用',\n en: '[MVU] Tool calling unavailable',\n },\n 'runtime.filter.globalSettingsSource': {\n 'zh-CN': '用户全局配置',\n en: 'global user settings',\n },\n 'runtime.filter.characterSettingsSource': {\n 'zh-CN': '角色卡配置',\n en: 'character-card settings',\n },\n 'runtime.filter.whitelistRegexLabel': {\n 'zh-CN': '白名单正则',\n en: 'whitelist regex',\n },\n 'runtime.filter.blacklistRegexLabel': {\n 'zh-CN': '黑名单正则',\n en: 'blacklist regex',\n },\n 'runtime.filter.invalidRegex': {\n 'zh-CN': '{source}的{label}无效,已在本轮世界书条目过滤中忽略:{cause}',\n en: 'The {label} from {source} is invalid and was ignored for this lorebook filtering pass: {cause}',\n },\n 'runtime.filter.invalidRegexTitle': {\n 'zh-CN': '[MVU]世界书条目过滤正则无效',\n en: '[MVU] Invalid lorebook entry filter regex',\n },\n 'runtime.filter.logTitle': {\n 'zh-CN': '[MVU]世界书条目黑/白名单筛选结果',\n en: '[MVU] Lorebook entry allowlist/blocklist filtering results',\n },\n 'runtime.regex.jsStyleSyntax': {\n 'zh-CN': 'JS 风格正则需要以 /pattern/flags 形式书写',\n en: 'JavaScript-style regular expressions must use the /pattern/flags form',\n },\n\n 'runtime.functionCalling.versionUnsupported': {\n 'zh-CN': '当前酒馆助手版本为 {current},工具调用需要酒馆助手 {required} 或更高版本',\n en: 'The current TavernHelper version is {current}. Tool calling requires TavernHelper {required} or later.',\n },\n 'runtime.extraModel.characterLorebookUnavailableLog': {\n 'zh-CN': '无法找到角色世界书,在多人聊天下不支持额外模型解析。',\n en: 'The character lorebook could not be found. Extra-model parsing is unavailable in group chats.',\n },\n 'runtime.extraModel.v4RequiresCustomSource': {\n 'zh-CN': '[MVU额外模型解析]格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同。',\n en: '[MVU extra-model parsing] Formatted Output (v4 Compatible) requires a Custom model source and cannot use Same as Primary Connection.',\n },\n 'runtime.extraModel.customIncludeBodyInvalid': {\n 'zh-CN': '[MVU额外模型解析]custom_include_body 不是 YAML object,无法合并配置。',\n en: '[MVU extra-model parsing] custom_include_body is not a YAML object, so the configuration cannot be merged.',\n },\n 'runtime.extraModel.saveSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern saveSettings,不能临时更新配置。',\n en: '[MVU extra-model parsing] SillyTavern saveSettings is unavailable; settings cannot be updated temporarily.',\n },\n 'runtime.extraModel.openAiSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable.',\n },\n 'runtime.extraModel.openAiSettingsRestoreUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置,不能恢复配置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable; the configuration cannot be restored.',\n },\n 'runtime.extraModel.requesting': {\n 'zh-CN': '正在请求 AI 回复…',\n en: 'Requesting an AI response…',\n },\n 'runtime.extraModel.retrying': {\n 'zh-CN': '正在重试({attempt} / {total})…',\n en: 'Retrying ({attempt} / {total})…',\n },\n 'runtime.extraModel.updateInProgressTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新中',\n en: '[MVU extra-model parsing] Updating variables',\n },\n 'runtime.extraModel.concurrentRequests': {\n 'zh-CN': '将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.firstRequest': {\n 'zh-CN': '将先请求一次,尝试是否能成功…',\n en: 'Trying one request first…',\n },\n 'runtime.extraModel.firstRequestFailed': {\n 'zh-CN': '首次请求失败,将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'The first request failed. Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.updateTagMissing': {\n 'zh-CN': '[MVU额外模型解析]没有能从回复中找到标签',\n en: '[MVU extra-model parsing] No tag was found in the response',\n },\n 'runtime.extraModel.updateCommandsInvalid': {\n 'zh-CN': '[MVU额外模型解析]从回复找到了标签,但其内的更新命令无效',\n en: '[MVU extra-model parsing] An tag was found, but its update commands are invalid',\n },\n 'runtime.extraModel.firstFloorSkippedLog': {\n 'zh-CN': '[MVU]对第一层永不进行额外模型解析',\n en: '[MVU] Extra-model parsing is never performed on the first floor',\n },\n 'runtime.extraModel.autoRequestDisabledLog': {\n 'zh-CN': '[MVU]不自动触发额外模型解析',\n en: '[MVU] Automatic extra-model parsing is disabled',\n },\n 'runtime.extraModel.belowMinLengthLog': {\n 'zh-CN': '[MVU]回复未达到最小请求字数,跳过自动额外模型解析',\n en: '[MVU] Reply is below the minimum length; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.triggerTagMissingLog': {\n 'zh-CN': '[MVU]回复未匹配触发标签正则,跳过自动额外模型解析',\n en: '[MVU] Reply does not match the trigger-tag regex; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.triggerTagInvalidLog': {\n 'zh-CN': '[MVU]触发标签正则无效,跳过自动额外模型解析',\n en: '[MVU] Invalid trigger-tag regex; skipping automatic extra-model parsing',\n },\n 'runtime.extraModel.updateFailed': {\n 'zh-CN': '建议调整变量更新方式;可在“输入框左下角魔棒 → 日志查看器”查看具体情况',\n en: 'Try adjusting the variable update method. Open Magic Wand → Log Viewer at the lower-left of the input box for details.',\n },\n 'runtime.extraModel.updateFailedTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新失败',\n en: '[MVU extra-model parsing] Variable update failed',\n },\n\n // Pi 运行时统一错误文案:只插入经过分类的参数,不拼接服务商原始响应或认证信息。\n 'runtime.pi.invalidConfig': {\n 'zh-CN': '“更多”配置无效,请检查其中的来源、API 和认证设置。',\n en: 'The More-source configuration is invalid. Check its provider, API, and authentication settings.',\n },\n 'runtime.pi.unknownProvider': {\n 'zh-CN': '“更多”中所选来源不可用,请重新选择来源。',\n en: 'The selected provider under More is unavailable. Select a provider again.',\n },\n 'runtime.pi.unsupportedApi': {\n 'zh-CN': '“更多”中所选来源不支持当前 API,请改用该来源支持的 API。',\n en: 'The selected provider under More does not support the current API. Choose an API supported by this provider.',\n },\n 'runtime.pi.unsupportedAuth': {\n 'zh-CN': '“更多”中所选来源不支持当前认证方式,请改用该来源支持的认证方式。',\n en: 'The selected provider under More does not support the current authentication method. Choose a supported method.',\n },\n 'runtime.pi.missingApiKey': {\n 'zh-CN': '请输入“更多”中所选来源的 API Key。',\n en: 'Enter an API key for the selected provider under More.',\n },\n 'runtime.pi.missingOAuthCredential': {\n 'zh-CN': '“更多”中所选来源尚未完成 OAuth 登录,请先登录。',\n en: 'The selected provider under More is not signed in with OAuth. Sign in before retrying.',\n },\n 'runtime.pi.oauthCredentialExpired': {\n 'zh-CN': '“更多”的 OAuth 凭据已过期且无法刷新,请登出后再登录。',\n en: 'The OAuth credential for More has expired and could not be refreshed. Sign out, then sign in again.',\n },\n 'runtime.pi.invalidEndpoint': {\n 'zh-CN':\n '自定义 endpoint 必须使用 HTTPS;HTTP 仅允许 localhost、127.0.0.1 或 [::1],且不能包含凭据、查询参数或片段。',\n en: 'The custom endpoint must use HTTPS. HTTP is limited to localhost, 127.0.0.1, or [::1]. Credentials, query parameters, and fragments are not allowed.',\n },\n 'runtime.pi.customEndpointNotAllowed': {\n 'zh-CN': '“更多”中所选来源使用固定 endpoint,不允许自定义。',\n en: 'The selected provider under More uses a fixed endpoint and does not allow a custom one.',\n },\n 'runtime.pi.oauthEndpointNotAllowed': {\n 'zh-CN': 'OAuth 来源必须使用注册的固定 endpoint,请清除自定义 endpoint。',\n en: 'OAuth providers must use their registered fixed endpoint. Clear the custom endpoint.',\n },\n 'runtime.pi.oauthApiMismatch': {\n 'zh-CN': '当前 OAuth 来源只能使用其锁定的兼容 API,请重新选择来源或认证方式。',\n en: 'This OAuth provider can only use its locked compatible API. Reselect the provider or authentication method.',\n },\n 'runtime.pi.missingModel': {\n 'zh-CN': '请填写“更多”来源的模型名称。',\n en: 'Enter a model name for the More source.',\n },\n 'runtime.pi.invalidContextWindow': {\n 'zh-CN': 'contextWindow 必须是正整数。',\n en: 'contextWindow must be a positive integer.',\n },\n 'runtime.pi.missingContextWindow': {\n 'zh-CN': '当前模型不在“更多”的内置目录中,请手动填写有效的 contextWindow。',\n en: 'The current model is not in the built-in catalog under More. Enter a valid contextWindow manually.',\n },\n 'runtime.pi.invalidMaxTokens': {\n 'zh-CN': '“最大回复 token 数”必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'runtime.pi.maxTokensExceedContext': {\n 'zh-CN': '“最大回复 token 数”不能大于 contextWindow。',\n en: 'Maximum response tokens must not exceed contextWindow.',\n },\n 'runtime.pi.anthropicSamplingConflict': {\n 'zh-CN': 'Anthropic 的温度和 top_p 只能调整其中一个,请将另一个恢复为默认值 1。',\n en: 'Adjust either temperature or top_p for Anthropic. Reset the other to its default value of 1.',\n },\n 'runtime.pi.customHeadersInvalid': {\n 'zh-CN': '“更多”的自定义请求头必须是 YAML/JSON 对象,且值只能是字符串或 null。',\n en: 'Custom headers under More must be a YAML/JSON object whose values are strings or null.',\n },\n 'runtime.pi.customConfigParseFailed': {\n 'zh-CN': '“更多”的自定义请求配置不是有效的 YAML 或 JSON,请检查格式。',\n en: 'The custom request settings under More are not valid YAML or JSON. Check their formatting.',\n },\n 'runtime.pi.customIncludeBodyInvalid': {\n 'zh-CN': '“更多”的自定义请求体必须是 YAML/JSON 对象。',\n en: 'The custom request body under More must be a YAML/JSON object.',\n },\n 'runtime.pi.customExcludeBodyInvalid': {\n 'zh-CN': '“更多”的排除请求字段必须是 YAML/JSON 字段名数组。',\n en: 'Excluded request fields under More must be a YAML/JSON array of field names.',\n },\n 'runtime.pi.customPayloadProtectedField': {\n 'zh-CN': '“更多”的自定义请求配置不能覆盖、排除或设置认证及关键协议字段。',\n en: 'Custom request settings under More cannot override, exclude, or set authentication or other protected protocol fields.',\n },\n 'runtime.pi.payloadInvalid': {\n 'zh-CN': '“更多”来源的请求体必须是对象,请检查 API 配置。',\n en: 'The provider payload under More must be an object. Check the API configuration.',\n },\n 'runtime.pi.structuredOutputSchemaMissing': {\n 'zh-CN': '“更多”的格式化输出需要有效的 JSON Schema。',\n en: 'Formatted output under More requires a valid JSON Schema.',\n },\n 'runtime.pi.structuredOutputUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持原生格式化输出,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support native formatted output. Change the configuration and retry.',\n },\n 'runtime.pi.toolCallingUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持所需的工具调用模式,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support the required tool-calling mode. Change the configuration and retry.',\n },\n 'runtime.pi.toolRequestRejected': {\n 'zh-CN':\n '目标端点未接受“工具调用”请求。请确认该端点和模型支持工具调用,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the tool-calling request. Confirm that the endpoint and model support tool calling, and check the API type, URL, and model name.',\n },\n 'runtime.pi.structuredOutputRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出”请求。请确认该端点和模型支持 JSON Schema 格式化输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the formatted-output request. Confirm that the endpoint and model support JSON Schema formatted output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.jsonObjectRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出(v4兼容)”请求。请确认该端点和模型支持 JSON Object 输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the v4-compatible formatted-output request. Confirm that the endpoint and model support JSON Object output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.namedToolChoiceUnsupported': {\n 'zh-CN': '“更多”中的当前 API 不支持指定工具调用,请更换 API 或调整工具选择方式。',\n en: 'The current API under More does not support named tool choice. Choose another API or adjust the tool-choice mode.',\n },\n 'runtime.pi.invalidToolDefinition': {\n 'zh-CN':\n '“更多”来源的工具定义无效:function name 不能为空,且 parameters 根节点必须是 object。',\n en: 'The tool definition under More is invalid: function name cannot be empty, and the parameters root must be an object.',\n },\n 'runtime.pi.invalidToolCall': {\n 'zh-CN': '“更多”来源返回了无效的工具调用,请重试或更换模型。',\n en: 'The More source returned an invalid tool call. Retry or choose another model.',\n },\n 'runtime.pi.toolUseMissingCall': {\n 'zh-CN': '“更多”来源以工具调用结束,但没有返回工具调用内容,请重试或更换模型。',\n en: 'The More source stopped for tool use without returning a tool call. Retry or choose another model.',\n },\n 'runtime.pi.imageInputUnsupported': {\n 'zh-CN': '“更多”中的当前模型不支持图片输入,请移除图片或更换模型。',\n en: 'The current model under More does not support image input. Remove the image or choose another model.',\n },\n 'runtime.pi.contextInvalidImage': {\n 'zh-CN': '“更多”的图片输入必须是可解码的 image/* base64 data URL。',\n en: 'Image input under More must be a decodable image/* base64 data URL.',\n },\n 'runtime.pi.remoteImageUnsupported': {\n 'zh-CN': '“更多”当前不支持远程图片 URL,请改用 base64 data URL。',\n en: 'The More source does not currently support remote image URLs. Use a base64 data URL instead.',\n },\n 'runtime.pi.videoUnsupported': {\n 'zh-CN': '“更多”链路当前不支持视频内容,请移除视频后重试。',\n en: 'The More path does not currently support video content. Remove the video and retry.',\n },\n 'runtime.pi.contextSystemPayloadMismatch': {\n 'zh-CN': '无法在服务商请求中完整恢复 system 消息,已停止发送。请更新脚本或更换协议后重试。',\n en: 'Could not restore all system messages in the provider payload. Sending was stopped. Update the script or select another API and try again.',\n },\n 'runtime.pi.contextMissingToolCall': {\n 'zh-CN': '第 {index} 条历史工具结果无法找到对应的工具调用,请检查聊天历史。',\n en: 'Historical tool result {index} has no matching tool call. Check the chat history.',\n },\n 'runtime.pi.contextUnsupportedContent': {\n 'zh-CN': '第 {index} 条消息包含“更多”当前不支持的内容,请移除该内容后重试。',\n en: 'Message {index} contains content that More does not currently support. Remove it and retry.',\n },\n 'runtime.pi.promptCaptureFailed': {\n 'zh-CN': '未能捕获 SillyTavern 构建后的最终提示词,请重试并查看日志。',\n en: 'Could not capture the final prompt built by SillyTavern. Retry and check the logs.',\n },\n 'runtime.pi.requestAlreadyActive': {\n 'zh-CN': '同一“更多”请求已在运行,请等待它结束或先停止该请求。',\n en: 'The same More-source request is already running. Wait for it to finish or stop it first.',\n },\n 'runtime.pi.tokenBudgetExceeded': {\n 'zh-CN':\n '请求预计占用 {estimatedInput} 个输入 token;加上 {maxTokens} 个回复 token 和 {reserve} 个预留 token 后,超过 {contextWindow} 的上下文窗口。请缩短聊天历史、降低最大回复 token 数,或修正 contextWindow。',\n en: 'The request is estimated to use {estimatedInput} input tokens. Together with {maxTokens} response tokens and {reserve} reserved tokens, it exceeds the {contextWindow}-token context window. Shorten chat history, reduce maximum response tokens, or correct contextWindow.',\n },\n 'runtime.pi.requestAborted': {\n 'zh-CN': '“更多”请求已取消。',\n en: 'The More-source request was cancelled.',\n },\n 'runtime.pi.browserNetworkError': {\n 'zh-CN':\n '浏览器无法连接“更多”中所选来源。请检查网络、地址配置,并确认来源允许浏览器 CORS 请求。',\n en: 'The browser could not reach the selected provider under More. Check the network and address settings, and confirm that the provider allows browser CORS requests.',\n },\n 'runtime.pi.proxyUnavailable': {\n 'zh-CN':\n '没有开启Proxy。请在 SillyTavern 的 config.yaml 中开启 enableCorsProxy,或使用 --corsProxy 启动参数,然后重启 SillyTavern。',\n en: 'Proxy is not enabled. Enable enableCorsProxy in SillyTavern config.yaml, or start SillyTavern with --corsProxy, then restart SillyTavern.',\n },\n 'runtime.pi.requestFailed': {\n 'zh-CN': '“更多”来源请求失败。请检查来源配置、凭据和网络后重试。',\n en: 'The provider request under More failed. Check the provider settings, credentials, and network, then retry.',\n },\n 'runtime.pi.featureDisabled': {\n 'zh-CN': '此构建已关闭“更多”模型来源;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Select Same as current connection or Custom.',\n },\n 'runtime.pi.protocolError': {\n 'zh-CN': '“更多”来源返回了无效的协议数据,请重试或更换来源/API。',\n en: 'The provider under More returned invalid protocol data. Retry or choose another provider/API.',\n },\n 'runtime.pi.lengthTruncated': {\n 'zh-CN': '“更多”回复因达到长度上限而被截断,请增加最大回复 token 数或缩短请求内容。',\n en: 'The response from More was truncated at the length limit. Increase maximum response tokens or shorten the request content.',\n },\n 'runtime.pi.emptyResponse': {\n 'zh-CN': '“更多”来源返回了空回复,请重试或更换模型。',\n en: 'The More source returned an empty response. Retry or choose another model.',\n },\n 'runtime.pi.thinkingOnlyResponse': {\n 'zh-CN': '“更多”回复仅包含 thinking,没有可用的业务内容,请重试或更换模型。',\n en: 'The response from More contained only thinking and no usable content. Retry or choose another model.',\n },\n 'runtime.pi.deferredResponse': {\n 'zh-CN': '“更多”回复尚未完成,当前不支持 deferred 结果。',\n en: 'The response from More is not complete; deferred results are not currently supported.',\n },\n 'runtime.pi.oauth.cancelled': {\n 'zh-CN': '“更多”的 OAuth 操作已取消,原凭据未更改。',\n en: 'The OAuth operation under More was cancelled. The existing credential was not changed.',\n },\n 'runtime.pi.oauth.browserUnavailable': {\n 'zh-CN':\n '当前页面缺少 OAuth 所需的 Fetch 或 Web Crypto 能力,请在支持的安全浏览器环境中重试。',\n en: 'Fetch or Web Crypto required for OAuth is unavailable. Retry in a supported secure browser context.',\n },\n 'runtime.pi.oauth.unsupportedProvider': {\n 'zh-CN': '“更多”中所选来源不支持浏览器 OAuth 登录,请改用 API Key 或其他来源。',\n en: 'The selected provider under More does not support browser OAuth sign-in. Use an API key or another provider.',\n },\n 'runtime.pi.oauth.invalidCallback': {\n 'zh-CN':\n 'OAuth callback URL 无效。请从浏览器地址栏复制完整的 loopback callback URL 后重试。',\n en: 'The OAuth callback URL is invalid. Copy the complete loopback callback URL from the browser address bar and retry.',\n },\n 'runtime.pi.oauth.stateMismatch': {\n 'zh-CN': 'OAuth callback 与当前登录尝试不匹配。请取消当前尝试并重新登录。',\n en: 'The OAuth callback does not match the current sign-in attempt. Cancel it and start a new sign-in.',\n },\n 'runtime.pi.oauth.authorizationFailed': {\n 'zh-CN': 'OAuth 授权未完成或被来源拒绝,请重新发起登录。',\n en: 'OAuth authorization was not completed or was rejected by the provider. Start a new sign-in.',\n },\n 'runtime.pi.oauth.browserNetwork': {\n 'zh-CN':\n '浏览器无法连接 OAuth token 服务。请检查网络,并确认来源允许浏览器 CORS 请求;MVU 不会自动改用代理。',\n en: 'The browser could not reach the OAuth token service. Check the network and confirm that the provider allows browser CORS requests; MVU will not silently use a proxy.',\n },\n 'runtime.pi.oauth.tokenHttp': {\n 'zh-CN': 'OAuth token 交换被来源拒绝。请重新发起登录,不要重用之前的 callback URL。',\n en: 'The provider rejected the OAuth token exchange. Start a new sign-in and do not reuse the previous callback URL.',\n },\n 'runtime.pi.oauth.tokenResponse': {\n 'zh-CN': 'OAuth token 服务返回了无效响应,请稍后重新登录。',\n en: 'The OAuth token service returned an invalid response. Sign in again later.',\n },\n 'runtime.pi.oauth.accountId': {\n 'zh-CN': '无法从 OAuth 凭据中确认账户,请重新登录或选择其他账户。',\n en: 'The account could not be identified from the OAuth credential. Sign in again or choose another account.',\n },\n 'runtime.pi.oauth.attemptExpired': {\n 'zh-CN': 'OAuth 登录尝试已过期,请重新点击登录并使用新的授权链接。',\n en: 'The OAuth sign-in attempt has expired. Start a new sign-in and use the new authorization link.',\n },\n 'runtime.pi.oauth.attemptUsed': {\n 'zh-CN': 'OAuth 登录尝试已结束或 callback URL 已使用,请重新发起登录。',\n en: 'The OAuth sign-in attempt has ended or its callback URL was already used. Start a new sign-in.',\n },\n 'runtime.pi.oauth.credentialStore': {\n 'zh-CN': '无法读取或保存“更多”的 OAuth 凭据,请检查浏览器存储后重试。',\n en: 'The OAuth credential for More could not be read or saved. Check browser storage and retry.',\n },\n\n 'runtime.variableUpdate.unknownCommand': {\n 'zh-CN': '未知命令',\n en: 'unknown command',\n },\n 'runtime.variableUpdate.errorTitle': {\n 'zh-CN': '[MVU]发生变量更新错误,可能需要重 Roll:{command}',\n en: '[MVU] Variable update error; rerolling may help: {command}',\n },\n 'runtime.variableUpdate.errorDetail': {\n 'zh-CN': '错误详情:{detail}',\n en: 'Error details: {detail}',\n },\n 'runtime.variableUpdate.messageOverwrittenTitle': {\n 'zh-CN': '[MVU]消息内容被覆盖',\n en: '[MVU] Message content overwritten',\n },\n 'runtime.variableUpdate.messageOverwritten': {\n 'zh-CN':\n '第 {messageId} 层消息中其他插件的修改被覆盖,可尝试重新运行相关插件对此楼层的解析。',\n en: \"Changes another plugin made to message #{messageId} were overwritten. Try rerunning that plugin's analysis for this message.\",\n },\n 'runtime.variableUpdate.setPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 set 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the set command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.assignPrimitive': {\n 'zh-CN': '路径“{path}”保存的是原始值({type}),无法向其中 assign;已跳过操作。{reason}',\n en: \"Cannot assign into path '{path}' because it holds a primitive value ({type}). The operation was skipped. {reason}\",\n },\n 'runtime.variableUpdate.mergeNonExtensibleObject': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象合并数据。{reason}',\n en: \"SCHEMA VIOLATION: Cannot merge data into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignUnknownKey': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象写入新键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign the new key '{key}' into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的数组写入元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign elements into the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignMissingParent': {\n 'zh-CN': '路径“{path}”不存在,且其父级不可扩展,无法向其中 assign。{reason}',\n en: \"Cannot assign into the non-existent path '{path}' without an extensible parent. {reason}\",\n },\n 'runtime.variableUpdate.mergeArrayIntoObject': {\n 'zh-CN': '无法将数组合并到路径“{path}”处的对象中。',\n en: \"Cannot merge an array into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.mergeNonObjectIntoObject': {\n 'zh-CN': '无法将非对象值合并到路径“{path}”处的对象中。',\n en: \"Cannot merge a non-object value into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.templateResolutionFailed': {\n 'zh-CN': '解析路径“{path}”处的模板元数据失败:{cause}',\n en: \"Failed to resolve template metadata at '{path}': {cause}\",\n },\n 'runtime.variableUpdate.assignInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.assign 参数无效。',\n en: \"Invalid arguments for _.assign at path '{path}'.\",\n },\n 'runtime.variableUpdate.removePathUndefined': {\n 'zh-CN': '_.remove 命令中的路径“{path}”未定义。',\n en: \"Path '{path}' is undefined in the _.remove command.\",\n },\n 'runtime.variableUpdate.deleteTargetUndetermined': {\n 'zh-CN': '无法确定路径“{path}”上的命令要删除的目标。{reason}',\n en: \"Could not determine the deletion target for the command at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removePathMissing': {\n 'zh-CN': '无法从不存在的路径“{path}”中删除内容。{reason}',\n en: \"Cannot remove from the non-existent path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”处不可扩展的数组中删除元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove an element from the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeRequiredKey': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”中删除必需键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove the required key '{key}' from path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonCollection': {\n 'zh-CN': '路径“{path}”处的值不是数组或对象,无法从中删除内容;已跳过命令。{reason}',\n en: \"Cannot remove from path '{path}' because it is not an array or object. The command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.removeExecutionFailed': {\n 'zh-CN': '无法在路径“{path}”上执行 remove。',\n en: \"Failed to execute remove at path '{path}'.\",\n },\n 'runtime.variableUpdate.addPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 add 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.dateDeltaNotNumber': {\n 'zh-CN': '日期操作的增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' for the date operation is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.deltaNotNumber': {\n 'zh-CN': '增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addUnsupportedValue': {\n 'zh-CN': '路径“{path}”处的值不是日期或数字,已跳过 add 命令。{reason}',\n en: \"The value at path '{path}' is not a date or number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.add 参数数量无效。{reason}',\n en: \"Invalid number of arguments for _.add at path '{path}'. {reason}\",\n },\n\n 'runtime.apiProfile.nameRequired': {\n 'zh-CN': 'API 方案名称不能为空',\n en: 'API profile name cannot be empty',\n },\n 'runtime.apiProfile.notFound': {\n 'zh-CN': '未找到 API 方案:{name}',\n en: 'API profile not found: {name}',\n },\n 'runtime.apiProfile.selectOrEnterName': {\n 'zh-CN': '请先输入或选择一个 API 方案名称',\n en: 'Enter or select an API profile name first',\n },\n 'runtime.apiProfile.alreadyExists': {\n 'zh-CN': 'API 方案「{name}」已存在',\n en: 'API profile “{name}” already exists',\n },\n 'runtime.apiProfile.piConfigRequired': {\n 'zh-CN': '“更多”API 方案缺少完整的连接配置,无法保存。',\n en: 'A More-source API profile requires a complete connection snapshot before it can be saved.',\n },\n 'runtime.apiProfile.enterNewName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a name for the new API profile first',\n },\n 'runtime.preset.notSelected': {\n 'zh-CN': '未选择额外模型预设',\n en: 'No extra-model preset was selected',\n },\n 'runtime.preset.apiUnavailable': {\n 'zh-CN': '当前环境未提供 getPreset 接口',\n en: 'The getPreset API is unavailable in the current environment',\n },\n 'runtime.preset.readFailed': {\n 'zh-CN': '无法读取预设“{name}”',\n en: 'Unable to read preset “{name}”',\n },\n\n 'runtime.functionCall.invalidJsonPatch': {\n 'zh-CN': '不是有效的 json patch',\n en: 'Invalid JSON Patch',\n },\n 'runtime.functionCall.updateBlockParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]无法解析变量更新块。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the variable update block. Content: {content}; error: {cause}',\n },\n 'runtime.functionCall.resultParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]函数调用结果解析失败:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the function-call result: {cause}',\n },\n 'runtime.functionCall.formattedOutputParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]格式化输出解析失败。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse formatted output. Content: {content}; error: {cause}',\n },\n});\n","export function getDevtoolsGlobalHook() {\n return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;\n}\nexport function getTarget() {\n // @ts-expect-error navigator and windows are not available in all environments\n return (typeof navigator !== 'undefined' && typeof window !== 'undefined')\n ? window\n : typeof globalThis !== 'undefined'\n ? globalThis\n : {};\n}\nexport const isProxyAvailable = typeof Proxy === 'function';\n","let supported;\nlet perf;\nexport function isPerformanceSupported() {\n var _a;\n if (supported !== undefined) {\n return supported;\n }\n if (typeof window !== 'undefined' && window.performance) {\n supported = true;\n perf = window.performance;\n }\n else if (typeof globalThis !== 'undefined' && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {\n supported = true;\n perf = globalThis.perf_hooks.performance;\n }\n else {\n supported = false;\n }\n return supported;\n}\nexport function now() {\n return isPerformanceSupported() ? perf.now() : Date.now();\n}\n","import { HOOK_PLUGIN_SETTINGS_SET } from './const.js';\nimport { now } from './time.js';\nexport class ApiProxy {\n constructor(plugin, hook) {\n this.target = null;\n this.targetQueue = [];\n this.onQueue = [];\n this.plugin = plugin;\n this.hook = hook;\n const defaultSettings = {};\n if (plugin.settings) {\n for (const id in plugin.settings) {\n const item = plugin.settings[id];\n defaultSettings[id] = item.defaultValue;\n }\n }\n const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;\n let currentSettings = Object.assign({}, defaultSettings);\n try {\n const raw = localStorage.getItem(localSettingsSaveId);\n const data = JSON.parse(raw);\n Object.assign(currentSettings, data);\n }\n catch (e) {\n // noop\n }\n this.fallbacks = {\n getSettings() {\n return currentSettings;\n },\n setSettings(value) {\n try {\n localStorage.setItem(localSettingsSaveId, JSON.stringify(value));\n }\n catch (e) {\n // noop\n }\n currentSettings = value;\n },\n now() {\n return now();\n },\n };\n if (hook) {\n hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {\n if (pluginId === this.plugin.id) {\n this.fallbacks.setSettings(value);\n }\n });\n }\n this.proxiedOn = new Proxy({}, {\n get: (_target, prop) => {\n if (this.target) {\n return this.target.on[prop];\n }\n else {\n return (...args) => {\n this.onQueue.push({\n method: prop,\n args,\n });\n };\n }\n },\n });\n this.proxiedTarget = new Proxy({}, {\n get: (_target, prop) => {\n if (this.target) {\n return this.target[prop];\n }\n else if (prop === 'on') {\n return this.proxiedOn;\n }\n else if (Object.keys(this.fallbacks).includes(prop)) {\n return (...args) => {\n this.targetQueue.push({\n method: prop,\n args,\n resolve: () => { },\n });\n return this.fallbacks[prop](...args);\n };\n }\n else {\n return (...args) => {\n return new Promise((resolve) => {\n this.targetQueue.push({\n method: prop,\n args,\n resolve,\n });\n });\n };\n }\n },\n });\n }\n async setRealTarget(target) {\n this.target = target;\n for (const item of this.onQueue) {\n this.target.on[item.method](...item.args);\n }\n for (const item of this.targetQueue) {\n item.resolve(await this.target[item.method](...item.args));\n }\n }\n}\n","export const HOOK_SETUP = 'devtools-plugin:setup';\nexport const HOOK_PLUGIN_SETTINGS_SET = 'plugin:settings:set';\n","import { getDevtoolsGlobalHook, getTarget, isProxyAvailable } from './env.js';\nimport { HOOK_SETUP } from './const.js';\nimport { ApiProxy } from './proxy.js';\nexport * from './api/index.js';\nexport * from './plugin.js';\nexport * from './time.js';\nexport function setupDevtoolsPlugin(pluginDescriptor, setupFn) {\n const descriptor = pluginDescriptor;\n const target = getTarget();\n const hook = getDevtoolsGlobalHook();\n const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;\n if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {\n hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);\n }\n else {\n const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;\n const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];\n list.push({\n pluginDescriptor: descriptor,\n setupFn,\n proxy,\n });\n if (proxy) {\n setupFn(proxy.proxiedTarget);\n }\n }\n}\n","/*!\n * vue-i18n v11.1.12\n * (c) 2025 kazuya kawaguchi\n * Released under the MIT License.\n */\nimport { CORE_ERROR_CODES_EXTEND_POINT, createCompileError, CORE_WARN_CODES_EXTEND_POINT, isMessageAST, AST_NODE_PROPS_KEYS, DEFAULT_LOCALE, updateFallbackLocale, setFallbackContext, createCoreContext, clearDateTimeFormat, clearNumberFormat, setAdditionalMeta, getFallbackContext, NOT_REOSLVED, isTranslateFallbackWarn, isTranslateMissingWarn, parseTranslateArgs, translate, MISSING_RESOLVE_VALUE, parseDateTimeArgs, datetime, parseNumberArgs, number, isMessageFunction, fallbackWithLocaleChain, NUMBER_FORMAT_OPTIONS_KEYS, DATETIME_FORMAT_OPTIONS_KEYS, registerMessageCompiler, compile, registerMessageResolver, resolveValue, registerLocaleFallbacker, setDevToolsHook } from '@intlify/core-base';\nimport { getGlobalThis, makeSymbol, format, isObject, create, isPlainObject, isArray, deepCopy, isString, hasOwn, warn, isBoolean, isRegExp, isFunction, inBrowser, assign, isNumber, createEmitter, warnOnce, isEmptyObject } from '@intlify/shared';\nimport { createVNode, Text, computed, watch, getCurrentInstance, ref, shallowRef, Fragment, defineComponent, h, effectScope, inject, onMounted, onUnmounted, isRef } from 'vue';\nimport { setupDevtoolsPlugin } from '@vue/devtools-api';\n\n/**\n * Vue I18n Version\n *\n * @remarks\n * Semver format. Same format as the package.json `version` field.\n *\n * @VueI18nGeneral\n */\nconst VERSION = '11.1.12';\n/**\n * This is only called in esm-bundler builds.\n * istanbul-ignore-next\n */\nfunction initFeatureFlags() {\n if (typeof __VUE_I18N_FULL_INSTALL__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_FULL_INSTALL__ = true;\n }\n if (typeof __VUE_I18N_LEGACY_API__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_LEGACY_API__ = true;\n }\n if (typeof __INTLIFY_DROP_MESSAGE_COMPILER__ !== 'boolean') {\n getGlobalThis().__INTLIFY_DROP_MESSAGE_COMPILER__ = false;\n }\n if (typeof __INTLIFY_PROD_DEVTOOLS__ !== 'boolean') {\n getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false;\n }\n}\n\nconst I18nErrorCodes = {\n // composer module errors\n UNEXPECTED_RETURN_TYPE: CORE_ERROR_CODES_EXTEND_POINT, // 24\n // legacy module errors\n INVALID_ARGUMENT: 25,\n // i18n module errors\n MUST_BE_CALL_SETUP_TOP: 26,\n NOT_INSTALLED: 27,\n // directive module errors\n REQUIRED_VALUE: 28,\n INVALID_VALUE: 29,\n // vue-devtools errors\n CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: 30,\n NOT_INSTALLED_WITH_PROVIDE: 31,\n // unexpected error\n UNEXPECTED_ERROR: 32,\n // not compatible legacy vue-i18n constructor\n NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,\n // Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly\n NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34\n};\nfunction createI18nError(code, ...args) {\n return createCompileError(code, null, (process.env.NODE_ENV !== 'production') ? { messages: errorMessages, args } : undefined);\n}\nconst errorMessages = {\n [I18nErrorCodes.UNEXPECTED_RETURN_TYPE]: 'Unexpected return type in composer',\n [I18nErrorCodes.INVALID_ARGUMENT]: 'Invalid argument',\n [I18nErrorCodes.MUST_BE_CALL_SETUP_TOP]: 'Must be called at the top of a `setup` function',\n [I18nErrorCodes.NOT_INSTALLED]: 'Need to install with `app.use` function',\n [I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpected error',\n [I18nErrorCodes.REQUIRED_VALUE]: `Required in value: {0}`,\n [I18nErrorCodes.INVALID_VALUE]: `Invalid value`,\n [I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN]: `Cannot setup vue-devtools plugin`,\n [I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE]: 'Need to install with `provide` function',\n [I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]: 'Not compatible legacy VueI18n.',\n [I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY]: 'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly'\n};\n\nconst TranslateVNodeSymbol = \n/* #__PURE__*/ makeSymbol('__translateVNode');\nconst DatetimePartsSymbol = /* #__PURE__*/ makeSymbol('__datetimeParts');\nconst NumberPartsSymbol = /* #__PURE__*/ makeSymbol('__numberParts');\nconst EnableEmitter = /* #__PURE__*/ makeSymbol('__enableEmitter');\nconst DisableEmitter = /* #__PURE__*/ makeSymbol('__disableEmitter');\nconst SetPluralRulesSymbol = makeSymbol('__setPluralRules');\nmakeSymbol('__intlifyMeta');\nconst InejctWithOptionSymbol = \n/* #__PURE__*/ makeSymbol('__injectWithOption');\nconst DisposeSymbol = /* #__PURE__*/ makeSymbol('__dispose');\n\nconst I18nWarnCodes = {\n FALLBACK_TO_ROOT: CORE_WARN_CODES_EXTEND_POINT, // 8\n NOT_FOUND_PARENT_SCOPE: 9,\n IGNORE_OBJ_FLATTEN: 10,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_LEGACY_MODE: 11,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12,\n // duplicate `useI18n` calling\n DUPLICATE_USE_I18N_CALLING: 13\n};\nconst warnMessages = {\n [I18nWarnCodes.FALLBACK_TO_ROOT]: `Fall back to {type} '{key}' with root locale.`,\n [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.`,\n [I18nWarnCodes.IGNORE_OBJ_FLATTEN]: `Ignore object flatten: '{key}' key has an string value`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_LEGACY_MODE]: `Legacy API mode has been deprecated in v11. Use Composition API mode instead.\\nAbout how to use the Composition API mode, see https://vue-i18n.intlify.dev/guide/advanced/composition.html`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.`,\n [I18nWarnCodes.DUPLICATE_USE_I18N_CALLING]: \"Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component.\"\n};\nfunction getWarnMessage(code, ...args) {\n return format(warnMessages[code], ...args);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Transform flat json in obj to normal json in obj\n */\nfunction handleFlatJson(obj) {\n // check obj\n if (!isObject(obj)) {\n return obj;\n }\n if (isMessageAST(obj)) {\n return obj;\n }\n for (const key in obj) {\n // check key\n if (!hasOwn(obj, key)) {\n continue;\n }\n // handle for normal json\n if (!key.includes('.')) {\n // recursive process value if value is also a object\n if (isObject(obj[key])) {\n handleFlatJson(obj[key]);\n }\n }\n // handle for flat json, transform to normal json\n else {\n // go to the last object\n const subKeys = key.split('.');\n const lastIndex = subKeys.length - 1;\n let currentObj = obj;\n let hasStringValue = false;\n for (let i = 0; i < lastIndex; i++) {\n if (subKeys[i] === '__proto__') {\n throw new Error(`unsafe key: ${subKeys[i]}`);\n }\n if (!(subKeys[i] in currentObj)) {\n currentObj[subKeys[i]] = create();\n }\n if (!isObject(currentObj[subKeys[i]])) {\n (process.env.NODE_ENV !== 'production') &&\n warn(getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, {\n key: subKeys[i]\n }));\n hasStringValue = true;\n break;\n }\n currentObj = currentObj[subKeys[i]];\n }\n // update last object value, delete old property\n if (!hasStringValue) {\n if (!isMessageAST(currentObj)) {\n currentObj[subKeys[lastIndex]] = obj[key];\n delete obj[key];\n }\n else {\n /**\n * NOTE:\n * if the last object is a message AST and subKeys[lastIndex] has message AST prop key, ignore to copy and key deletion\n */\n if (!AST_NODE_PROPS_KEYS.includes(subKeys[lastIndex])) {\n delete obj[key];\n }\n }\n }\n // recursive process value if value is also a object\n if (!isMessageAST(currentObj)) {\n const target = currentObj[subKeys[lastIndex]];\n if (isObject(target)) {\n handleFlatJson(target);\n }\n }\n }\n }\n return obj;\n}\nfunction getLocaleMessages(locale, options) {\n const { messages, __i18n, messageResolver, flatJson } = options;\n // prettier-ignore\n const ret = (isPlainObject(messages)\n ? messages\n : isArray(__i18n)\n ? create()\n : { [locale]: create() });\n // merge locale messages of i18n custom block\n if (isArray(__i18n)) {\n __i18n.forEach(custom => {\n if ('locale' in custom && 'resource' in custom) {\n const { locale, resource } = custom;\n if (locale) {\n ret[locale] = ret[locale] || create();\n deepCopy(resource, ret[locale]);\n }\n else {\n deepCopy(resource, ret);\n }\n }\n else {\n isString(custom) && deepCopy(JSON.parse(custom), ret);\n }\n });\n }\n // handle messages for flat json\n if (messageResolver == null && flatJson) {\n for (const key in ret) {\n if (hasOwn(ret, key)) {\n handleFlatJson(ret[key]);\n }\n }\n }\n return ret;\n}\nfunction getComponentOptions(instance) {\n return instance.type;\n}\nfunction adjustI18nResources(gl, options, componentOptions) {\n // prettier-ignore\n let messages = isObject(options.messages)\n ? options.messages\n : create();\n if ('__i18nGlobal' in componentOptions) {\n messages = getLocaleMessages(gl.locale.value, {\n messages,\n __i18n: componentOptions.__i18nGlobal\n });\n }\n // merge locale messages\n const locales = Object.keys(messages);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeLocaleMessage(locale, messages[locale]);\n });\n }\n {\n // merge datetime formats\n if (isObject(options.datetimeFormats)) {\n const locales = Object.keys(options.datetimeFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeDateTimeFormat(locale, options.datetimeFormats[locale]);\n });\n }\n }\n // merge number formats\n if (isObject(options.numberFormats)) {\n const locales = Object.keys(options.numberFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeNumberFormat(locale, options.numberFormats[locale]);\n });\n }\n }\n }\n}\nfunction createTextNode(key) {\n return createVNode(Text, null, key, 0);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// extend VNode interface\nconst DEVTOOLS_META = '__INTLIFY_META__';\nconst NOOP_RETURN_ARRAY = () => [];\nconst NOOP_RETURN_FALSE = () => false;\nlet composerID = 0;\nfunction defineCoreMissingHandler(missing) {\n return ((ctx, locale, key, type) => {\n return missing(locale, key, getCurrentInstance() || undefined, type);\n });\n}\n// for Intlify DevTools\n/* #__NO_SIDE_EFFECTS__ */\nconst getMetaInfo = () => {\n const instance = getCurrentInstance();\n let meta = null;\n return instance && (meta = getComponentOptions(instance)[DEVTOOLS_META])\n ? { [DEVTOOLS_META]: meta }\n : null;\n};\n/**\n * Create composer interface factory\n *\n * @internal\n */\nfunction createComposer(options = {}) {\n const { __root, __injectWithOption } = options;\n const _isGlobal = __root === undefined;\n const flatJson = options.flatJson;\n const _ref = inBrowser ? ref : shallowRef;\n let _inheritLocale = isBoolean(options.inheritLocale)\n ? options.inheritLocale\n : true;\n const _locale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.locale.value\n : isString(options.locale)\n ? options.locale\n : DEFAULT_LOCALE);\n const _fallbackLocale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.fallbackLocale.value\n : isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : _locale.value);\n const _messages = _ref(getLocaleMessages(_locale.value, options));\n // prettier-ignore\n const _datetimeFormats = _ref(isPlainObject(options.datetimeFormats)\n ? options.datetimeFormats\n : { [_locale.value]: {} })\n ;\n // prettier-ignore\n const _numberFormats = _ref(isPlainObject(options.numberFormats)\n ? options.numberFormats\n : { [_locale.value]: {} })\n ;\n // warning suppress options\n // prettier-ignore\n let _missingWarn = __root\n ? __root.missingWarn\n : isBoolean(options.missingWarn) || isRegExp(options.missingWarn)\n ? options.missingWarn\n : true;\n // prettier-ignore\n let _fallbackWarn = __root\n ? __root.fallbackWarn\n : isBoolean(options.fallbackWarn) || isRegExp(options.fallbackWarn)\n ? options.fallbackWarn\n : true;\n // prettier-ignore\n let _fallbackRoot = __root\n ? __root.fallbackRoot\n : isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n // configure fall back to root\n let _fallbackFormat = !!options.fallbackFormat;\n // runtime missing\n let _missing = isFunction(options.missing) ? options.missing : null;\n let _runtimeMissing = isFunction(options.missing)\n ? defineCoreMissingHandler(options.missing)\n : null;\n // postTranslation handler\n let _postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : null;\n // prettier-ignore\n let _warnHtmlMessage = __root\n ? __root.warnHtmlMessage\n : isBoolean(options.warnHtmlMessage)\n ? options.warnHtmlMessage\n : true;\n let _escapeParameter = !!options.escapeParameter;\n // custom linked modifiers\n // prettier-ignore\n const _modifiers = __root\n ? __root.modifiers\n : isPlainObject(options.modifiers)\n ? options.modifiers\n : {};\n // pluralRules\n let _pluralRules = options.pluralRules || (__root && __root.pluralRules);\n // runtime context\n // eslint-disable-next-line prefer-const\n let _context;\n const getCoreContext = () => {\n _isGlobal && setFallbackContext(null);\n const ctxOptions = {\n version: VERSION,\n locale: _locale.value,\n fallbackLocale: _fallbackLocale.value,\n messages: _messages.value,\n modifiers: _modifiers,\n pluralRules: _pluralRules,\n missing: _runtimeMissing === null ? undefined : _runtimeMissing,\n missingWarn: _missingWarn,\n fallbackWarn: _fallbackWarn,\n fallbackFormat: _fallbackFormat,\n unresolving: true,\n postTranslation: _postTranslation === null ? undefined : _postTranslation,\n warnHtmlMessage: _warnHtmlMessage,\n escapeParameter: _escapeParameter,\n messageResolver: options.messageResolver,\n messageCompiler: options.messageCompiler,\n __meta: { framework: 'vue' }\n };\n {\n ctxOptions.datetimeFormats = _datetimeFormats.value;\n ctxOptions.numberFormats = _numberFormats.value;\n ctxOptions.__datetimeFormatters = isPlainObject(_context)\n ? _context.__datetimeFormatters\n : undefined;\n ctxOptions.__numberFormatters = isPlainObject(_context)\n ? _context.__numberFormatters\n : undefined;\n }\n if ((process.env.NODE_ENV !== 'production')) {\n ctxOptions.__v_emitter = isPlainObject(_context)\n ? _context.__v_emitter\n : undefined;\n }\n const ctx = createCoreContext(ctxOptions);\n _isGlobal && setFallbackContext(ctx);\n return ctx;\n };\n _context = getCoreContext();\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n // track reactivity\n function trackReactivityValues() {\n return [\n _locale.value,\n _fallbackLocale.value,\n _messages.value,\n _datetimeFormats.value,\n _numberFormats.value\n ]\n ;\n }\n // locale\n const locale = computed({\n get: () => _locale.value,\n set: val => {\n _context.locale = val;\n _locale.value = val;\n }\n });\n // fallbackLocale\n const fallbackLocale = computed({\n get: () => _fallbackLocale.value,\n set: val => {\n _context.fallbackLocale = val;\n _fallbackLocale.value = val;\n updateFallbackLocale(_context, _locale.value, val);\n }\n });\n // messages\n const messages = computed(() => _messages.value);\n // datetimeFormats\n const datetimeFormats = /* #__PURE__*/ computed(() => _datetimeFormats.value);\n // numberFormats\n const numberFormats = /* #__PURE__*/ computed(() => _numberFormats.value);\n // getPostTranslationHandler\n function getPostTranslationHandler() {\n return isFunction(_postTranslation) ? _postTranslation : null;\n }\n // setPostTranslationHandler\n function setPostTranslationHandler(handler) {\n _postTranslation = handler;\n _context.postTranslation = handler;\n }\n // getMissingHandler\n function getMissingHandler() {\n return _missing;\n }\n // setMissingHandler\n function setMissingHandler(handler) {\n if (handler !== null) {\n _runtimeMissing = defineCoreMissingHandler(handler);\n }\n _missing = handler;\n _context.missing = _runtimeMissing;\n }\n function isResolvedTranslateMessage(type, arg) {\n return type !== 'translate' || !arg.resolvedMessage;\n }\n const wrapWithDeps = (fn, argumentParser, warnType, fallbackSuccess, fallbackFail, successCondition) => {\n trackReactivityValues(); // track reactive dependency\n // NOTE: experimental !!\n let ret;\n try {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(getMetaInfo());\n }\n if (!_isGlobal) {\n _context.fallbackContext = __root\n ? getFallbackContext()\n : undefined;\n }\n ret = fn(_context);\n }\n finally {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(null);\n }\n if (!_isGlobal) {\n _context.fallbackContext = undefined;\n }\n }\n if ((warnType !== 'translate exists' && // for not `te` (e.g `t`)\n isNumber(ret) &&\n ret === NOT_REOSLVED) ||\n (warnType === 'translate exists' && !ret) // for `te`\n ) {\n const [key, arg2] = argumentParser();\n if ((process.env.NODE_ENV !== 'production') &&\n __root &&\n isString(key) &&\n isResolvedTranslateMessage(warnType, arg2)) {\n if (_fallbackRoot &&\n (isTranslateFallbackWarn(_fallbackWarn, key) ||\n isTranslateMissingWarn(_missingWarn, key))) {\n warn(getWarnMessage(I18nWarnCodes.FALLBACK_TO_ROOT, {\n key,\n type: warnType\n }));\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n const { __v_emitter: emitter } = _context;\n if (emitter && _fallbackRoot) {\n emitter.emit('fallback', {\n type: warnType,\n key,\n to: 'global',\n groupId: `${warnType}:${key}`\n });\n }\n }\n }\n return __root && _fallbackRoot\n ? fallbackSuccess(__root)\n : fallbackFail(key);\n }\n else if (successCondition(ret)) {\n return ret;\n }\n else {\n /* istanbul ignore next */\n throw createI18nError(I18nErrorCodes.UNEXPECTED_RETURN_TYPE);\n }\n };\n // t\n function t(...args) {\n return wrapWithDeps(context => Reflect.apply(translate, null, [context, ...args]), () => parseTranslateArgs(...args), 'translate', root => Reflect.apply(root.t, root, [...args]), key => key, val => isString(val));\n }\n // rt\n function rt(...args) {\n const [arg1, arg2, arg3] = args;\n if (arg3 && !isObject(arg3)) {\n throw createI18nError(I18nErrorCodes.INVALID_ARGUMENT);\n }\n return t(...[arg1, arg2, assign({ resolvedMessage: true }, arg3 || {})]);\n }\n // d\n function d(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => Reflect.apply(root.d, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // n\n function n(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => Reflect.apply(root.n, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // for custom processor\n function normalize(values) {\n return values.map(val => isString(val) || isNumber(val) || isBoolean(val)\n ? createTextNode(String(val))\n : val);\n }\n const interpolate = (val) => val;\n const processor = {\n normalize,\n interpolate,\n type: 'vnode'\n };\n // translateVNode, using for `i18n-t` component\n function translateVNode(...args) {\n return wrapWithDeps(context => {\n let ret;\n const _context = context;\n try {\n _context.processor = processor;\n ret = Reflect.apply(translate, null, [_context, ...args]);\n }\n finally {\n _context.processor = null;\n }\n return ret;\n }, () => parseTranslateArgs(...args), 'translate', root => root[TranslateVNodeSymbol](...args), key => [createTextNode(key)], val => isArray(val));\n }\n // numberParts, using for `i18n-n` component\n function numberParts(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => root[NumberPartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n // datetimeParts, using for `i18n-d` component\n function datetimeParts(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => root[DatetimePartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n function setPluralRules(rules) {\n _pluralRules = rules;\n _context.pluralRules = _pluralRules;\n }\n // te\n function te(key, locale) {\n return wrapWithDeps(() => {\n if (!key) {\n return false;\n }\n const targetLocale = isString(locale) ? locale : _locale.value;\n const message = getLocaleMessage(targetLocale);\n const resolved = _context.messageResolver(message, key);\n return (isMessageAST(resolved) ||\n isMessageFunction(resolved) ||\n isString(resolved));\n }, () => [key], 'translate exists', root => {\n return Reflect.apply(root.te, root, [key, locale]);\n }, NOOP_RETURN_FALSE, val => isBoolean(val));\n }\n function resolveMessages(key) {\n let messages = null;\n const locales = fallbackWithLocaleChain(_context, _fallbackLocale.value, _locale.value);\n for (let i = 0; i < locales.length; i++) {\n const targetLocaleMessages = _messages.value[locales[i]] || {};\n const messageValue = _context.messageResolver(targetLocaleMessages, key);\n if (messageValue != null) {\n messages = messageValue;\n break;\n }\n }\n return messages;\n }\n // tm\n function tm(key) {\n const messages = resolveMessages(key);\n // prettier-ignore\n return messages != null\n ? messages\n : __root\n ? __root.tm(key) || {}\n : {};\n }\n // getLocaleMessage\n function getLocaleMessage(locale) {\n return (_messages.value[locale] || {});\n }\n // setLocaleMessage\n function setLocaleMessage(locale, message) {\n if (flatJson) {\n const _message = { [locale]: message };\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n message = _message[locale];\n }\n _messages.value[locale] = message;\n _context.messages = _messages.value;\n }\n // mergeLocaleMessage\n function mergeLocaleMessage(locale, message) {\n _messages.value[locale] = _messages.value[locale] || {};\n const _message = { [locale]: message };\n if (flatJson) {\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n }\n message = _message[locale];\n deepCopy(message, _messages.value[locale]);\n _context.messages = _messages.value;\n }\n // getDateTimeFormat\n function getDateTimeFormat(locale) {\n return _datetimeFormats.value[locale] || {};\n }\n // setDateTimeFormat\n function setDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = format;\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // mergeDateTimeFormat\n function mergeDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = assign(_datetimeFormats.value[locale] || {}, format);\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // getNumberFormat\n function getNumberFormat(locale) {\n return _numberFormats.value[locale] || {};\n }\n // setNumberFormat\n function setNumberFormat(locale, format) {\n _numberFormats.value[locale] = format;\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // mergeNumberFormat\n function mergeNumberFormat(locale, format) {\n _numberFormats.value[locale] = assign(_numberFormats.value[locale] || {}, format);\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // for debug\n composerID++;\n // watch root locale & fallbackLocale\n if (__root && inBrowser) {\n watch(__root.locale, (val) => {\n if (_inheritLocale) {\n _locale.value = val;\n _context.locale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n watch(__root.fallbackLocale, (val) => {\n if (_inheritLocale) {\n _fallbackLocale.value = val;\n _context.fallbackLocale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n }\n // define basic composition API!\n const composer = {\n id: composerID,\n locale,\n fallbackLocale,\n get inheritLocale() {\n return _inheritLocale;\n },\n set inheritLocale(val) {\n _inheritLocale = val;\n if (val && __root) {\n _locale.value = __root.locale.value;\n _fallbackLocale.value = __root.fallbackLocale.value;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n },\n get availableLocales() {\n return Object.keys(_messages.value).sort();\n },\n messages,\n get modifiers() {\n return _modifiers;\n },\n get pluralRules() {\n return _pluralRules || {};\n },\n get isGlobal() {\n return _isGlobal;\n },\n get missingWarn() {\n return _missingWarn;\n },\n set missingWarn(val) {\n _missingWarn = val;\n _context.missingWarn = _missingWarn;\n },\n get fallbackWarn() {\n return _fallbackWarn;\n },\n set fallbackWarn(val) {\n _fallbackWarn = val;\n _context.fallbackWarn = _fallbackWarn;\n },\n get fallbackRoot() {\n return _fallbackRoot;\n },\n set fallbackRoot(val) {\n _fallbackRoot = val;\n },\n get fallbackFormat() {\n return _fallbackFormat;\n },\n set fallbackFormat(val) {\n _fallbackFormat = val;\n _context.fallbackFormat = _fallbackFormat;\n },\n get warnHtmlMessage() {\n return _warnHtmlMessage;\n },\n set warnHtmlMessage(val) {\n _warnHtmlMessage = val;\n _context.warnHtmlMessage = val;\n },\n get escapeParameter() {\n return _escapeParameter;\n },\n set escapeParameter(val) {\n _escapeParameter = val;\n _context.escapeParameter = val;\n },\n t,\n getLocaleMessage,\n setLocaleMessage,\n mergeLocaleMessage,\n getPostTranslationHandler,\n setPostTranslationHandler,\n getMissingHandler,\n setMissingHandler,\n [SetPluralRulesSymbol]: setPluralRules\n };\n {\n composer.datetimeFormats = datetimeFormats;\n composer.numberFormats = numberFormats;\n composer.rt = rt;\n composer.te = te;\n composer.tm = tm;\n composer.d = d;\n composer.n = n;\n composer.getDateTimeFormat = getDateTimeFormat;\n composer.setDateTimeFormat = setDateTimeFormat;\n composer.mergeDateTimeFormat = mergeDateTimeFormat;\n composer.getNumberFormat = getNumberFormat;\n composer.setNumberFormat = setNumberFormat;\n composer.mergeNumberFormat = mergeNumberFormat;\n composer[InejctWithOptionSymbol] = __injectWithOption;\n composer[TranslateVNodeSymbol] = translateVNode;\n composer[DatetimePartsSymbol] = datetimeParts;\n composer[NumberPartsSymbol] = numberParts;\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n composer[EnableEmitter] = (emitter) => {\n _context.__v_emitter = emitter;\n };\n composer[DisableEmitter] = () => {\n _context.__v_emitter = undefined;\n };\n }\n return composer;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst VUE_I18N_COMPONENT_TYPES = 'vue-i18n: composer properties';\nconst VueDevToolsLabels = {\n 'vue-devtools-plugin-vue-i18n': 'Vue I18n DevTools',\n 'vue-i18n-resource-inspector': 'Vue I18n DevTools',\n 'vue-i18n-timeline': 'Vue I18n'\n};\nconst VueDevToolsPlaceholders = {\n 'vue-i18n-resource-inspector': 'Search for scopes ...'\n};\nconst VueDevToolsTimelineColors = {\n 'vue-i18n-timeline': 0xffcd19\n};\nlet devtoolsApi;\nasync function enableDevTools(app, i18n) {\n return new Promise((resolve, reject) => {\n try {\n setupDevtoolsPlugin({\n id: 'vue-devtools-plugin-vue-i18n',\n label: VueDevToolsLabels['vue-devtools-plugin-vue-i18n'],\n packageName: 'vue-i18n',\n homepage: 'https://vue-i18n.intlify.dev',\n logo: 'https://vue-i18n.intlify.dev/vue-i18n-devtools-logo.png',\n componentStateTypes: [VUE_I18N_COMPONENT_TYPES],\n app: app // eslint-disable-line @typescript-eslint/no-explicit-any\n }, api => {\n devtoolsApi = api;\n api.on.visitComponentTree(({ componentInstance, treeNode }) => {\n updateComponentTreeTags(componentInstance, treeNode, i18n);\n });\n api.on.inspectComponent(({ componentInstance, instanceData }) => {\n if (componentInstance.vnode.el &&\n componentInstance.vnode.el.__VUE_I18N__ &&\n instanceData) {\n if (i18n.mode === 'legacy') {\n // ignore global scope on legacy mode\n if (componentInstance.vnode.el.__VUE_I18N__ !==\n i18n.global.__composer) {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n else {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n });\n api.addInspector({\n id: 'vue-i18n-resource-inspector',\n label: VueDevToolsLabels['vue-i18n-resource-inspector'],\n icon: 'language',\n treeFilterPlaceholder: VueDevToolsPlaceholders['vue-i18n-resource-inspector']\n });\n api.on.getInspectorTree(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n registerScope(payload, i18n);\n }\n });\n const roots = new Map();\n api.on.getInspectorState(async (payload) => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n api.unhighlightElement();\n inspectScope(payload, i18n);\n if (payload.nodeId === 'global') {\n if (!roots.has(payload.app)) {\n const [root] = await api.getComponentInstances(payload.app);\n roots.set(payload.app, root);\n }\n api.highlightElement(roots.get(payload.app));\n }\n else {\n const instance = getComponentInstance(payload.nodeId, i18n);\n instance && api.highlightElement(instance);\n }\n }\n });\n api.on.editInspectorState(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n editScope(payload, i18n);\n }\n });\n api.addTimelineLayer({\n id: 'vue-i18n-timeline',\n label: VueDevToolsLabels['vue-i18n-timeline'],\n color: VueDevToolsTimelineColors['vue-i18n-timeline']\n });\n resolve(true);\n });\n }\n catch (e) {\n console.error(e);\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n reject(false);\n }\n });\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getI18nScopeLable(instance) {\n return (instance.type.name ||\n instance.type.displayName ||\n instance.type.__file ||\n 'Anonymous');\n}\nfunction updateComponentTreeTags(instance, // eslint-disable-line @typescript-eslint/no-explicit-any\ntreeNode, i18n) {\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n if (instance && instance.vnode.el && instance.vnode.el.__VUE_I18N__) {\n // add custom tags local scope only\n if (instance.vnode.el.__VUE_I18N__ !== global) {\n const tag = {\n label: `i18n (${getI18nScopeLable(instance)} Scope)`,\n textColor: 0x000000,\n backgroundColor: 0xffcd19\n };\n treeNode.tags.push(tag);\n }\n }\n}\nfunction inspectComposer(instanceData, composer) {\n const type = VUE_I18N_COMPONENT_TYPES;\n instanceData.state.push({\n type,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n });\n instanceData.state.push({\n type,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n });\n instanceData.state.push({\n type,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n });\n instanceData.state.push({\n type,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n });\n instanceData.state.push({\n type,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n });\n {\n instanceData.state.push({\n type,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n });\n instanceData.state.push({\n type,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n });\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getLocaleMessageValue(messages) {\n const value = {};\n Object.keys(messages).forEach((key) => {\n const v = messages[key];\n if (isFunction(v) && 'source' in v) {\n value[key] = getMessageFunctionDetails(v);\n }\n else if (isMessageAST(v) && v.loc && v.loc.source) {\n value[key] = v.loc.source;\n }\n else if (isObject(v)) {\n value[key] = getLocaleMessageValue(v);\n }\n else {\n value[key] = v;\n }\n });\n return value;\n}\nconst ESC = {\n '<': '<',\n '>': '>',\n '\"': '"',\n '&': '&'\n};\nfunction escape(s) {\n return s.replace(/[<>\"&]/g, escapeChar);\n}\nfunction escapeChar(a) {\n return ESC[a] || a;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getMessageFunctionDetails(func) {\n const argString = func.source ? `(\"${escape(func.source)}\")` : `(?)`;\n return {\n _custom: {\n type: 'function',\n display: `ƒ ${argString}`\n }\n };\n}\nfunction registerScope(payload, i18n) {\n payload.rootNodes.push({\n id: 'global',\n label: 'Global Scope'\n });\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n for (const [keyInstance, instance] of i18n.__instances) {\n // prettier-ignore\n const composer = i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n if (global === composer) {\n continue;\n }\n payload.rootNodes.push({\n id: composer.id.toString(),\n label: `${getI18nScopeLable(keyInstance)} Scope`\n });\n }\n}\nfunction getComponentInstance(nodeId, i18n) {\n let instance = null;\n if (nodeId !== 'global') {\n for (const [component, composer] of i18n.__instances.entries()) {\n if (composer.id.toString() === nodeId) {\n instance = component;\n break;\n }\n }\n }\n return instance;\n}\nfunction getComposer$2(nodeId, i18n) {\n if (nodeId === 'global') {\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n }\n else {\n const instance = Array.from(i18n.__instances.values()).find(item => item.id.toString() === nodeId);\n if (instance) {\n return i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n }\n else {\n return null;\n }\n }\n}\nfunction inspectScope(payload, i18n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n // TODO:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n payload.state = makeScopeInspectState(composer);\n }\n return null;\n}\nfunction makeScopeInspectState(composer) {\n const state = {};\n const localeType = 'Locale related info';\n const localeStates = [\n {\n type: localeType,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n },\n {\n type: localeType,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n },\n {\n type: localeType,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n },\n {\n type: localeType,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n }\n ];\n state[localeType] = localeStates;\n const localeMessagesType = 'Locale messages info';\n const localeMessagesStates = [\n {\n type: localeMessagesType,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n }\n ];\n state[localeMessagesType] = localeMessagesStates;\n {\n const datetimeFormatsType = 'Datetime formats info';\n const datetimeFormatsStates = [\n {\n type: datetimeFormatsType,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n }\n ];\n state[datetimeFormatsType] = datetimeFormatsStates;\n const numberFormatsType = 'Datetime formats info';\n const numberFormatsStates = [\n {\n type: numberFormatsType,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n }\n ];\n state[numberFormatsType] = numberFormatsStates;\n }\n return state;\n}\nfunction addTimelineEvent(event, payload) {\n if (devtoolsApi) {\n let groupId;\n if (payload && 'groupId' in payload) {\n groupId = payload.groupId;\n delete payload.groupId;\n }\n devtoolsApi.addTimelineEvent({\n layerId: 'vue-i18n-timeline',\n event: {\n title: event,\n groupId,\n time: Date.now(),\n meta: {},\n data: payload || {},\n logType: event === 'compile-error'\n ? 'error'\n : event === 'fallback' || event === 'missing'\n ? 'warning'\n : 'default'\n }\n });\n }\n}\nfunction editScope(payload, i18n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n const [field] = payload.path;\n if (field === 'locale' && isString(payload.state.value)) {\n composer.locale.value = payload.state.value;\n }\n else if (field === 'fallbackLocale' &&\n (isString(payload.state.value) ||\n isArray(payload.state.value) ||\n isObject(payload.state.value))) {\n composer.fallbackLocale.value = payload.state.value;\n }\n else if (field === 'inheritLocale' && isBoolean(payload.state.value)) {\n composer.inheritLocale = payload.state.value;\n }\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Convert to I18n Composer Options from VueI18n Options\n *\n * @internal\n */\nfunction convertComposerOptions(options) {\n const locale = isString(options.locale) ? options.locale : DEFAULT_LOCALE;\n const fallbackLocale = isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : locale;\n const missing = isFunction(options.missing) ? options.missing : undefined;\n const missingWarn = isBoolean(options.silentTranslationWarn) ||\n isRegExp(options.silentTranslationWarn)\n ? !options.silentTranslationWarn\n : true;\n const fallbackWarn = isBoolean(options.silentFallbackWarn) ||\n isRegExp(options.silentFallbackWarn)\n ? !options.silentFallbackWarn\n : true;\n const fallbackRoot = isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n const fallbackFormat = !!options.formatFallbackMessages;\n const modifiers = isPlainObject(options.modifiers) ? options.modifiers : {};\n const pluralizationRules = options.pluralizationRules;\n const postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : undefined;\n const warnHtmlMessage = isString(options.warnHtmlInMessage)\n ? options.warnHtmlInMessage !== 'off'\n : true;\n const escapeParameter = !!options.escapeParameterHtml;\n const inheritLocale = isBoolean(options.sync) ? options.sync : true;\n let messages = options.messages;\n if (isPlainObject(options.sharedMessages)) {\n const sharedMessages = options.sharedMessages;\n const locales = Object.keys(sharedMessages);\n messages = locales.reduce((messages, locale) => {\n const message = messages[locale] || (messages[locale] = {});\n assign(message, sharedMessages[locale]);\n return messages;\n }, (messages || {}));\n }\n const { __i18n, __root, __injectWithOption } = options;\n const datetimeFormats = options.datetimeFormats;\n const numberFormats = options.numberFormats;\n const flatJson = options.flatJson;\n return {\n locale,\n fallbackLocale,\n messages,\n flatJson,\n datetimeFormats,\n numberFormats,\n missing,\n missingWarn,\n fallbackWarn,\n fallbackRoot,\n fallbackFormat,\n modifiers,\n pluralRules: pluralizationRules,\n postTranslation,\n warnHtmlMessage,\n escapeParameter,\n messageResolver: options.messageResolver,\n inheritLocale,\n __i18n,\n __root,\n __injectWithOption\n };\n}\n/**\n * create VueI18n interface factory\n *\n * @internal\n *\n * @deprecated will be removed at vue-i18n v12\n */\nfunction createVueI18n(options = {}) {\n const composer = createComposer(convertComposerOptions(options));\n const { __extender } = options;\n // defines VueI18n\n const vueI18n = {\n // id\n id: composer.id,\n // locale\n get locale() {\n return composer.locale.value;\n },\n set locale(val) {\n composer.locale.value = val;\n },\n // fallbackLocale\n get fallbackLocale() {\n return composer.fallbackLocale.value;\n },\n set fallbackLocale(val) {\n composer.fallbackLocale.value = val;\n },\n // messages\n get messages() {\n return composer.messages.value;\n },\n // datetimeFormats\n get datetimeFormats() {\n return composer.datetimeFormats.value;\n },\n // numberFormats\n get numberFormats() {\n return composer.numberFormats.value;\n },\n // availableLocales\n get availableLocales() {\n return composer.availableLocales;\n },\n // missing\n get missing() {\n return composer.getMissingHandler();\n },\n set missing(handler) {\n composer.setMissingHandler(handler);\n },\n // silentTranslationWarn\n get silentTranslationWarn() {\n return isBoolean(composer.missingWarn)\n ? !composer.missingWarn\n : composer.missingWarn;\n },\n set silentTranslationWarn(val) {\n composer.missingWarn = isBoolean(val) ? !val : val;\n },\n // silentFallbackWarn\n get silentFallbackWarn() {\n return isBoolean(composer.fallbackWarn)\n ? !composer.fallbackWarn\n : composer.fallbackWarn;\n },\n set silentFallbackWarn(val) {\n composer.fallbackWarn = isBoolean(val) ? !val : val;\n },\n // modifiers\n get modifiers() {\n return composer.modifiers;\n },\n // formatFallbackMessages\n get formatFallbackMessages() {\n return composer.fallbackFormat;\n },\n set formatFallbackMessages(val) {\n composer.fallbackFormat = val;\n },\n // postTranslation\n get postTranslation() {\n return composer.getPostTranslationHandler();\n },\n set postTranslation(handler) {\n composer.setPostTranslationHandler(handler);\n },\n // sync\n get sync() {\n return composer.inheritLocale;\n },\n set sync(val) {\n composer.inheritLocale = val;\n },\n // warnInHtmlMessage\n get warnHtmlInMessage() {\n return composer.warnHtmlMessage ? 'warn' : 'off';\n },\n set warnHtmlInMessage(val) {\n composer.warnHtmlMessage = val !== 'off';\n },\n // escapeParameterHtml\n get escapeParameterHtml() {\n return composer.escapeParameter;\n },\n set escapeParameterHtml(val) {\n composer.escapeParameter = val;\n },\n // pluralizationRules\n get pluralizationRules() {\n return composer.pluralRules || {};\n },\n // for internal\n __composer: composer,\n // t\n t(...args) {\n return Reflect.apply(composer.t, composer, [...args]);\n },\n // rt\n rt(...args) {\n return Reflect.apply(composer.rt, composer, [...args]);\n },\n // te\n te(key, locale) {\n return composer.te(key, locale);\n },\n // tm\n tm(key) {\n return composer.tm(key);\n },\n // getLocaleMessage\n getLocaleMessage(locale) {\n return composer.getLocaleMessage(locale);\n },\n // setLocaleMessage\n setLocaleMessage(locale, message) {\n composer.setLocaleMessage(locale, message);\n },\n // mergeLocaleMessage\n mergeLocaleMessage(locale, message) {\n composer.mergeLocaleMessage(locale, message);\n },\n // d\n d(...args) {\n return Reflect.apply(composer.d, composer, [...args]);\n },\n // getDateTimeFormat\n getDateTimeFormat(locale) {\n return composer.getDateTimeFormat(locale);\n },\n // setDateTimeFormat\n setDateTimeFormat(locale, format) {\n composer.setDateTimeFormat(locale, format);\n },\n // mergeDateTimeFormat\n mergeDateTimeFormat(locale, format) {\n composer.mergeDateTimeFormat(locale, format);\n },\n // n\n n(...args) {\n return Reflect.apply(composer.n, composer, [...args]);\n },\n // getNumberFormat\n getNumberFormat(locale) {\n return composer.getNumberFormat(locale);\n },\n // setNumberFormat\n setNumberFormat(locale, format) {\n composer.setNumberFormat(locale, format);\n },\n // mergeNumberFormat\n mergeNumberFormat(locale, format) {\n composer.mergeNumberFormat(locale, format);\n }\n };\n vueI18n.__extender = __extender;\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n vueI18n.__enableEmitter = (emitter) => {\n const __composer = composer;\n __composer[EnableEmitter] && __composer[EnableEmitter](emitter);\n };\n vueI18n.__disableEmitter = () => {\n const __composer = composer;\n __composer[DisableEmitter] && __composer[DisableEmitter]();\n };\n }\n return vueI18n;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Supports compatibility for legacy vue-i18n APIs\n * This mixin is used when we use vue-i18n@v9.x or later\n */\nfunction defineMixin(vuei18n, composer, i18n) {\n return {\n beforeCreate() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const options = this.$options;\n if (options.i18n) {\n const optionsI18n = options.i18n;\n if (options.__i18n) {\n optionsI18n.__i18n = options.__i18n;\n }\n optionsI18n.__root = composer;\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, optionsI18n);\n }\n else {\n optionsI18n.__injectWithOption = true;\n optionsI18n.__extender = i18n.__vueI18nExtend;\n // atttach local VueI18n instance\n this.$i18n = createVueI18n(optionsI18n);\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else if (options.__i18n) {\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, options);\n }\n else {\n // atttach local VueI18n instance\n this.$i18n = createVueI18n({\n __i18n: options.__i18n,\n __injectWithOption: true,\n __extender: i18n.__vueI18nExtend,\n __root: composer\n });\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else {\n // attach global VueI18n instance\n this.$i18n = vuei18n;\n }\n if (options.__i18nGlobal) {\n adjustI18nResources(composer, options, options);\n }\n // defines vue-i18n legacy APIs\n this.$t = (...args) => this.$i18n.t(...args);\n this.$rt = (...args) => this.$i18n.rt(...args);\n this.$te = (key, locale) => this.$i18n.te(key, locale);\n this.$d = (...args) => this.$i18n.d(...args);\n this.$n = (...args) => this.$i18n.n(...args);\n this.$tm = (key) => this.$i18n.tm(key);\n i18n.__setInstance(instance, this.$i18n);\n },\n mounted() {\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$i18n) {\n const _vueI18n = this.$i18n;\n this.$el.__VUE_I18N__ = _vueI18n.__composer;\n const emitter = (this.__v_emitter =\n createEmitter());\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n emitter.on('*', addTimelineEvent);\n }\n },\n unmounted() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const _vueI18n = this.$i18n;\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$el.__VUE_I18N__) {\n if (this.__v_emitter) {\n this.__v_emitter.off('*', addTimelineEvent);\n delete this.__v_emitter;\n }\n if (this.$i18n) {\n _vueI18n.__disableEmitter && _vueI18n.__disableEmitter();\n delete this.$el.__VUE_I18N__;\n }\n }\n delete this.$t;\n delete this.$rt;\n delete this.$te;\n delete this.$d;\n delete this.$n;\n delete this.$tm;\n if (_vueI18n.__disposer) {\n _vueI18n.__disposer();\n delete _vueI18n.__disposer;\n delete _vueI18n.__extender;\n }\n i18n.__deleteInstance(instance);\n delete this.$i18n;\n }\n };\n}\nfunction mergeToGlobal(g, options) {\n g.locale = options.locale || g.locale;\n g.fallbackLocale = options.fallbackLocale || g.fallbackLocale;\n g.missing = options.missing || g.missing;\n g.silentTranslationWarn =\n options.silentTranslationWarn || g.silentFallbackWarn;\n g.silentFallbackWarn = options.silentFallbackWarn || g.silentFallbackWarn;\n g.formatFallbackMessages =\n options.formatFallbackMessages || g.formatFallbackMessages;\n g.postTranslation = options.postTranslation || g.postTranslation;\n g.warnHtmlInMessage = options.warnHtmlInMessage || g.warnHtmlInMessage;\n g.escapeParameterHtml = options.escapeParameterHtml || g.escapeParameterHtml;\n g.sync = options.sync || g.sync;\n g.__composer[SetPluralRulesSymbol](options.pluralizationRules || g.pluralizationRules);\n const messages = getLocaleMessages(g.locale, {\n messages: options.messages,\n __i18n: options.__i18n\n });\n Object.keys(messages).forEach(locale => g.mergeLocaleMessage(locale, messages[locale]));\n if (options.datetimeFormats) {\n Object.keys(options.datetimeFormats).forEach(locale => g.mergeDateTimeFormat(locale, options.datetimeFormats[locale]));\n }\n if (options.numberFormats) {\n Object.keys(options.numberFormats).forEach(locale => g.mergeNumberFormat(locale, options.numberFormats[locale]));\n }\n return g;\n}\n\nconst baseFormatProps = {\n tag: {\n type: [String, Object]\n },\n locale: {\n type: String\n },\n scope: {\n type: String,\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n validator: (val /* ComponentI18nScope */) => val === 'parent' || val === 'global',\n default: 'parent' /* ComponentI18nScope */\n },\n i18n: {\n type: Object\n }\n};\n\nfunction getInterpolateArg(\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n{ slots }, // SetupContext,\nkeys) {\n if (keys.length === 1 && keys[0] === 'default') {\n // default slot with list\n const ret = slots.default ? slots.default() : [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return ret.reduce((slot, current) => {\n return [\n ...slot,\n // prettier-ignore\n ...(current.type === Fragment ? current.children : [current])\n ];\n }, []);\n }\n else {\n // named slots\n return keys.reduce((arg, key) => {\n const slot = slots[key];\n if (slot) {\n arg[key] = slot();\n }\n return arg;\n }, create());\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getFragmentableTag() {\n return Fragment;\n}\n\nconst TranslationImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-t',\n props: assign({\n keypath: {\n type: String,\n required: true\n },\n plural: {\n type: [Number, String],\n validator: (val) => isNumber(val) || !isNaN(val)\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const { slots, attrs } = context;\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return () => {\n const keys = Object.keys(slots).filter(key => key[0] !== '_');\n const options = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (props.plural !== undefined) {\n options.plural = isString(props.plural) ? +props.plural : props.plural;\n }\n const arg = getInterpolateArg(context, keys);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const children = i18n[TranslateVNodeSymbol](props.keypath, arg, options);\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Translation Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [TranslationProps](component#translationprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Component Interpolation](../guide/advanced/component)\n *\n * @example\n * ```html\n *
\n * \n * \n * {{ $t('tos') }}\n * \n * \n *
\n * ```\n * ```js\n * import { createApp } from 'vue'\n * import { createI18n } from 'vue-i18n'\n *\n * const messages = {\n * en: {\n * tos: 'Term of Service',\n * term: 'I accept xxx {0}.'\n * },\n * ja: {\n * tos: '利用規約',\n * term: '私は xxx の{0}に同意します。'\n * }\n * }\n *\n * const i18n = createI18n({\n * locale: 'en',\n * messages\n * })\n *\n * const app = createApp({\n * data: {\n * url: '/term'\n * }\n * }).use(i18n).mount('#app')\n * ```\n *\n * @VueI18nComponent\n */\nconst Translation = TranslationImpl;\nconst I18nT = Translation;\n\nfunction isVNode(target) {\n return isArray(target) && !isString(target[0]);\n}\nfunction renderFormatter(props, context, slotKeys, partFormatter) {\n const { slots, attrs } = context;\n return () => {\n const options = { part: true };\n let overrides = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (isString(props.format)) {\n options.key = props.format;\n }\n else if (isObject(props.format)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (isString(props.format.key)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options.key = props.format.key;\n }\n // Filter out number format options only\n overrides = Object.keys(props.format).reduce((options, prop) => {\n return slotKeys.includes(prop)\n ? assign(create(), options, { [prop]: props.format[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any\n : options;\n }, create());\n }\n const parts = partFormatter(...[props.value, options, overrides]);\n let children = [options.key];\n if (isArray(parts)) {\n children = parts.map((part, index) => {\n const slot = slots[part.type];\n const node = slot\n ? slot({ [part.type]: part.value, index, parts })\n : [part.value];\n if (isVNode(node)) {\n node[0].key = `${part.type}-${index}`;\n }\n return node;\n });\n }\n else if (isString(parts)) {\n children = [parts];\n }\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n}\n\nconst NumberFormatImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-n',\n props: assign({\n value: {\n type: Number,\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, NUMBER_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[NumberPartsSymbol](...args));\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Number Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/number#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.NumberFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-numberformat)\n *\n * @VueI18nComponent\n */\nconst NumberFormat = NumberFormatImpl;\nconst I18nN = NumberFormat;\n\nfunction getComposer$1(i18n, instance) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n return (i18nInternal.__getInstance(instance) || i18n.global);\n }\n else {\n const vueI18n = i18nInternal.__getInstance(instance);\n return vueI18n != null\n ? vueI18n.__composer\n : i18n.global.__composer;\n }\n}\n/**\n * @deprecated will be removed at vue-i18n v12\n */\nfunction vTDirective(i18n) {\n const _process = (binding) => {\n if ((process.env.NODE_ENV !== 'production')) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE));\n }\n const { instance, value } = binding;\n /* istanbul ignore if */\n if (!instance || !instance.$) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const composer = getComposer$1(i18n, instance.$);\n const parsedValue = parseValue(value);\n return [\n Reflect.apply(composer.t, composer, [...makeParams(parsedValue)]),\n composer\n ];\n };\n const register = (el, binding) => {\n const [textContent, composer] = _process(binding);\n if (inBrowser && i18n.global === composer) {\n // global scope only\n el.__i18nWatcher = watch(composer.locale, () => {\n binding.instance && binding.instance.$forceUpdate();\n });\n }\n el.__composer = composer;\n el.textContent = textContent;\n };\n const unregister = (el) => {\n if (inBrowser && el.__i18nWatcher) {\n el.__i18nWatcher();\n el.__i18nWatcher = undefined;\n delete el.__i18nWatcher;\n }\n if (el.__composer) {\n el.__composer = undefined;\n delete el.__composer;\n }\n };\n const update = (el, { value }) => {\n if (el.__composer) {\n const composer = el.__composer;\n const parsedValue = parseValue(value);\n el.textContent = Reflect.apply(composer.t, composer, [\n ...makeParams(parsedValue)\n ]);\n }\n };\n const getSSRProps = (binding) => {\n const [textContent] = _process(binding);\n return { textContent };\n };\n return {\n created: register,\n unmounted: unregister,\n beforeUpdate: update,\n getSSRProps\n };\n}\nfunction parseValue(value) {\n if (isString(value)) {\n return { path: value };\n }\n else if (isPlainObject(value)) {\n if (!('path' in value)) {\n throw createI18nError(I18nErrorCodes.REQUIRED_VALUE, 'path');\n }\n return value;\n }\n else {\n throw createI18nError(I18nErrorCodes.INVALID_VALUE);\n }\n}\nfunction makeParams(value) {\n const { path, locale, args, choice, plural } = value;\n const options = {};\n const named = args || {};\n if (isString(locale)) {\n options.locale = locale;\n }\n if (isNumber(choice)) {\n options.plural = choice;\n }\n if (isNumber(plural)) {\n options.plural = plural;\n }\n return [path, named, options];\n}\n\nfunction apply(app, i18n, ...options) {\n const pluginOptions = isPlainObject(options[0])\n ? options[0]\n : {};\n const globalInstall = isBoolean(pluginOptions.globalInstall)\n ? pluginOptions.globalInstall\n : true;\n if (globalInstall) {\n [Translation.name, 'I18nT'].forEach(name => app.component(name, Translation));\n [NumberFormat.name, 'I18nN'].forEach(name => app.component(name, NumberFormat));\n [DatetimeFormat.name, 'I18nD'].forEach(name => app.component(name, DatetimeFormat));\n }\n // install directive\n {\n app.directive('t', vTDirective(i18n));\n }\n}\n\n/**\n * Injection key for {@link useI18n}\n *\n * @remarks\n * The global injection key for I18n instances with `useI18n`. this injection key is used in Web Components.\n * Specify the i18n instance created by {@link createI18n} together with `provide` function.\n *\n * @VueI18nGeneral\n */\nconst I18nInjectionKey = \n/* #__PURE__*/ makeSymbol('global-vue-i18n');\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction createI18n(options = {}) {\n // prettier-ignore\n const __legacyMode = __VUE_I18N_LEGACY_API__ && isBoolean(options.legacy)\n ? options.legacy\n : __VUE_I18N_LEGACY_API__;\n if ((process.env.NODE_ENV !== 'production') && __legacyMode) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_LEGACY_MODE));\n }\n // prettier-ignore\n const __globalInjection = isBoolean(options.globalInjection)\n ? options.globalInjection\n : true;\n const __instances = new Map();\n const [globalScope, __global] = createGlobal(options, __legacyMode);\n const symbol = /* #__PURE__*/ makeSymbol((process.env.NODE_ENV !== 'production') ? 'vue-i18n' : '');\n function __getInstance(component) {\n return __instances.get(component) || null;\n }\n function __setInstance(component, instance) {\n __instances.set(component, instance);\n }\n function __deleteInstance(component) {\n __instances.delete(component);\n }\n const i18n = {\n // mode\n get mode() {\n return __VUE_I18N_LEGACY_API__ && __legacyMode\n ? 'legacy'\n : 'composition';\n },\n // install plugin\n async install(app, ...options) {\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n app.__VUE_I18N__ = i18n;\n }\n // setup global provider\n app.__VUE_I18N_SYMBOL__ = symbol;\n app.provide(app.__VUE_I18N_SYMBOL__, i18n);\n // set composer & vuei18n extend hook options from plugin options\n if (isPlainObject(options[0])) {\n const opts = options[0];\n i18n.__composerExtend =\n opts.__composerExtend;\n i18n.__vueI18nExtend =\n opts.__vueI18nExtend;\n }\n // global method and properties injection for Composition API\n let globalReleaseHandler = null;\n if (!__legacyMode && __globalInjection) {\n globalReleaseHandler = injectGlobalFields(app, i18n.global);\n }\n // install built-in components and directive\n if (__VUE_I18N_FULL_INSTALL__) {\n apply(app, i18n, ...options);\n }\n // setup mixin for Legacy API\n if (__VUE_I18N_LEGACY_API__ && __legacyMode) {\n app.mixin(defineMixin(__global, __global.__composer, i18n));\n }\n // release global scope\n const unmountApp = app.unmount;\n app.unmount = () => {\n globalReleaseHandler && globalReleaseHandler();\n i18n.dispose();\n unmountApp();\n };\n // setup vue-devtools plugin\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n const ret = await enableDevTools(app, i18n);\n if (!ret) {\n throw createI18nError(I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN);\n }\n const emitter = createEmitter();\n if (__legacyMode) {\n const _vueI18n = __global;\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = __global;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n }\n emitter.on('*', addTimelineEvent);\n }\n },\n // global accessor\n get global() {\n return __global;\n },\n dispose() {\n globalScope.stop();\n },\n // @internal\n __instances,\n // @internal\n __getInstance,\n // @internal\n __setInstance,\n // @internal\n __deleteInstance\n };\n return i18n;\n}\nfunction useI18n(options = {}) {\n const instance = getCurrentInstance();\n if (instance == null) {\n throw createI18nError(I18nErrorCodes.MUST_BE_CALL_SETUP_TOP);\n }\n if (!instance.isCE &&\n instance.appContext.app != null &&\n !instance.appContext.app.__VUE_I18N_SYMBOL__) {\n throw createI18nError(I18nErrorCodes.NOT_INSTALLED);\n }\n const i18n = getI18nInstance(instance);\n const gl = getGlobalComposer(i18n);\n const componentOptions = getComponentOptions(instance);\n const scope = getScope(options, componentOptions);\n if (scope === 'global') {\n adjustI18nResources(gl, options, componentOptions);\n return gl;\n }\n if (scope === 'parent') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let composer = getComposer(i18n, instance, options.__useComponent);\n if (composer == null) {\n if ((process.env.NODE_ENV !== 'production')) {\n warn(getWarnMessage(I18nWarnCodes.NOT_FOUND_PARENT_SCOPE));\n }\n composer = gl;\n }\n return composer;\n }\n const i18nInternal = i18n;\n let composer = i18nInternal.__getInstance(instance);\n if (composer == null) {\n const composerOptions = assign({}, options);\n if ('__i18n' in componentOptions) {\n composerOptions.__i18n = componentOptions.__i18n;\n }\n if (gl) {\n composerOptions.__root = gl;\n }\n composer = createComposer(composerOptions);\n if (i18nInternal.__composerExtend) {\n composer[DisposeSymbol] =\n i18nInternal.__composerExtend(composer);\n }\n setupLifeCycle(i18nInternal, instance, composer);\n i18nInternal.__setInstance(instance, composer);\n }\n else {\n if ((process.env.NODE_ENV !== 'production') && scope === 'local') {\n warn(getWarnMessage(I18nWarnCodes.DUPLICATE_USE_I18N_CALLING));\n }\n }\n return composer;\n}\nfunction createGlobal(options, legacyMode) {\n const scope = effectScope();\n const obj = __VUE_I18N_LEGACY_API__ && legacyMode\n ? scope.run(() => createVueI18n(options))\n : scope.run(() => createComposer(options));\n if (obj == null) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n return [scope, obj];\n}\nfunction getI18nInstance(instance) {\n const i18n = inject(!instance.isCE\n ? instance.appContext.app.__VUE_I18N_SYMBOL__\n : I18nInjectionKey);\n /* istanbul ignore if */\n if (!i18n) {\n throw createI18nError(!instance.isCE\n ? I18nErrorCodes.UNEXPECTED_ERROR\n : I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE);\n }\n return i18n;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getScope(options, componentOptions) {\n // prettier-ignore\n return isEmptyObject(options)\n ? ('__i18n' in componentOptions)\n ? 'local'\n : 'global'\n : !options.useScope\n ? 'local'\n : options.useScope;\n}\nfunction getGlobalComposer(i18n) {\n // prettier-ignore\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n}\nfunction getComposer(i18n, target, useComponent = false) {\n let composer = null;\n const root = target.root;\n let current = getParentComponentInstance(target, useComponent);\n while (current != null) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n composer = i18nInternal.__getInstance(current);\n }\n else {\n if (__VUE_I18N_LEGACY_API__) {\n const vueI18n = i18nInternal.__getInstance(current);\n if (vueI18n != null) {\n composer = vueI18n\n .__composer;\n if (useComponent &&\n composer &&\n !composer[InejctWithOptionSymbol] // eslint-disable-line @typescript-eslint/no-explicit-any\n ) {\n composer = null;\n }\n }\n }\n }\n if (composer != null) {\n break;\n }\n if (root === current) {\n break;\n }\n current = current.parent;\n }\n return composer;\n}\nfunction getParentComponentInstance(target, useComponent = false) {\n if (target == null) {\n return null;\n }\n // if `useComponent: true` will be specified, we get lexical scope owner instance for use-case slots\n return !useComponent\n ? target.parent\n : target.vnode.ctx || target.parent; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\nfunction setupLifeCycle(i18n, target, composer) {\n let emitter = null;\n onMounted(() => {\n // inject composer instance to DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el) {\n target.vnode.el.__VUE_I18N__ = composer;\n emitter = createEmitter();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n emitter.on('*', addTimelineEvent);\n }\n }, target);\n onUnmounted(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n // remove composer instance from DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el &&\n target.vnode.el.__VUE_I18N__) {\n emitter && emitter.off('*', addTimelineEvent);\n _composer[DisableEmitter] && _composer[DisableEmitter]();\n delete target.vnode.el.__VUE_I18N__;\n }\n i18n.__deleteInstance(target);\n // dispose extended resources\n const dispose = _composer[DisposeSymbol];\n if (dispose) {\n dispose();\n delete _composer[DisposeSymbol];\n }\n }, target);\n}\nconst globalExportProps = [\n 'locale',\n 'fallbackLocale',\n 'availableLocales'\n];\nconst globalExportMethods = ['t', 'rt', 'd', 'n', 'tm', 'te']\n ;\nfunction injectGlobalFields(app, composer) {\n const i18n = Object.create(null);\n globalExportProps.forEach(prop => {\n const desc = Object.getOwnPropertyDescriptor(composer, prop);\n if (!desc) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const wrap = isRef(desc.value) // check computed props\n ? {\n get() {\n return desc.value.value;\n },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n set(val) {\n desc.value.value = val;\n }\n }\n : {\n get() {\n return desc.get && desc.get();\n }\n };\n Object.defineProperty(i18n, prop, wrap);\n });\n app.config.globalProperties.$i18n = i18n;\n globalExportMethods.forEach(method => {\n const desc = Object.getOwnPropertyDescriptor(composer, method);\n if (!desc || !desc.value) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n Object.defineProperty(app.config.globalProperties, `$${method}`, desc);\n });\n const dispose = () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties.$i18n;\n globalExportMethods.forEach(method => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties[`$${method}`];\n });\n };\n return dispose;\n}\n\nconst DatetimeFormatImpl = /* #__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-d',\n props: assign({\n value: {\n type: [Number, Date],\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, DATETIME_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[DatetimePartsSymbol](...args));\n }\n});\n/**\n * Datetime Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/datetime#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.DateTimeFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-datetimeformat)\n *\n * @VueI18nComponent\n */\nconst DatetimeFormat = DatetimeFormatImpl;\nconst I18nD = DatetimeFormat;\n\n{\n initFeatureFlags();\n}\n// register message compiler at vue-i18n\nregisterMessageCompiler(compile);\n// register message resolver at vue-i18n\nregisterMessageResolver(resolveValue);\n// register fallback locale at vue-i18n\nregisterLocaleFallbacker(fallbackWithLocaleChain);\n// NOTE: experimental !!\nif ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n const target = getGlobalThis();\n target.__INTLIFY__ = true;\n setDevToolsHook(target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__);\n}\nif ((process.env.NODE_ENV !== 'production')) ;\n\nexport { DatetimeFormat, I18nD, I18nInjectionKey, I18nN, I18nT, NumberFormat, Translation, VERSION, createI18n, useI18n, vTDirective };\n","import { commonMessages } from '@/i18n/messages/common';\nimport { panelMessages } from '@/i18n/messages/panel';\nimport { runtimeMessages } from '@/i18n/messages/runtime';\nimport type { AppLocale, MessageModule } from '@/i18n/messages/types';\nimport { createI18n, type LocaleMessageValue, useI18n } from 'vue-i18n';\n\nconst allMessages = {\n ...commonMessages,\n ...panelMessages,\n ...runtimeMessages,\n};\n\nexport type MessageKey = keyof typeof allMessages;\nexport type TranslationParams = Record;\n\nexport function resolveMvuLocale(locale: string): AppLocale {\n return locale.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';\n}\n\nfunction getHostLocale(): string {\n try {\n return SillyTavern.getCurrentLocale();\n } catch {\n return document.documentElement.lang || navigator.language || 'en';\n }\n}\n\ntype LocaleMessages = Record;\n\nfunction buildLocaleMessages(locale: AppLocale): LocaleMessages {\n // 消息源使用便于类型检查的扁平键,这里再展开成 vue-i18n 所需的嵌套对象。\n const result: LocaleMessages = {};\n for (const [key, translations] of Object.entries(allMessages) as [\n MessageKey,\n MessageModule[string],\n ][]) {\n const path = key.split('.');\n const leaf = path.pop()!;\n let parent = result;\n for (const segment of path) {\n const existing = parent[segment];\n if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {\n parent[segment] = {};\n }\n parent = parent[segment] as LocaleMessages;\n }\n parent[leaf] = translations[locale];\n }\n return result;\n}\n\nexport const i18n = createI18n({\n legacy: false,\n globalInjection: false,\n locale: resolveMvuLocale(getHostLocale()),\n fallbackLocale: 'zh-CN',\n messages: {\n 'zh-CN': buildLocaleMessages('zh-CN'),\n en: buildLocaleMessages('en'),\n },\n missingWarn: false,\n fallbackWarn: false,\n warnHtmlMessage: false,\n});\n\nfunction translate(key: MessageKey, params?: TranslationParams): string {\n return params ? i18n.global.t(key, params) : i18n.global.t(key);\n}\n\nexport function tr(key: MessageKey, params?: TranslationParams): string {\n return translate(key, params);\n}\n\nexport function useMvuI18n() {\n const composer = useI18n({ useScope: 'global' });\n return {\n locale: composer.locale,\n t: (key: MessageKey, params?: TranslationParams) =>\n params ? composer.t(key, params) : composer.t(key),\n };\n}\n","declare const jest: any;\ndeclare const process: any;\n\nexport const is_jest_environment =\n typeof jest !== 'undefined' ||\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test');\n","import {\n migrateExtraModelApiProfiles,\n normalizeExtraModelPiProfileContextWindow,\n} from '@/function/update/extra_model_api_profiles';\nimport {\n CharacterSettingsOverride,\n CharacterSettingsOverridePath,\n getCharacterSettingsOverrideValue,\n hasActiveCharacterSettingsOverride,\n hasOwnCharacterSettingsOverride,\n} from '@/function/character_override/schema';\nimport { PI_THINKING_LEVELS } from '@/function/update/pi/thinking_setting';\nimport { is_jest_environment } from '@/jest';\nimport { registerAsUniqueScript } from '@util/script';\nimport { klona } from 'klona';\nimport { defineStore } from 'pinia';\nimport { computed, ref, toRaw, watch } from 'vue';\nimport * as z from 'zod';\n\n/**\n * 保留导入或界面中的无效窗口值,使其与合法的“0 = 使用目录值”明确区分。\n * 请求预检只接受 0 或正整数,避免错误配置静默回退到目录元数据。\n */\nconst ExtraModelPiContextWindow = z\n .union([z.number(), z.string()])\n .catch('__invalid_context_window__')\n .default(0)\n .transform((value): number | string => {\n if (typeof value === 'number') {\n return value;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : value;\n });\n\n// 方案快照使用规范化整数;无效值使整个连接快照失效,而不污染其余设置。\nconst ExtraModelPiProfileContextWindow = z\n .union([z.number(), z.string()])\n .transform(value => normalizeExtraModelPiProfileContextWindow(value))\n .pipe(z.number());\n\n// 方案只保存连接与请求覆盖,OAuth 凭证和按目标缓存的密钥由活动设置独立持有。\nconst ExtraModelPiProfileSnapshot = z\n .object({\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n provider: z.string().trim(),\n api: z.string().trim(),\n authType: z\n .string()\n .trim()\n .pipe(z.enum(['api_key', 'oauth'])),\n endpoint: z.string().trim(),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim(),\n contextWindow: ExtraModelPiProfileContextWindow,\n customHeaders: z.string(),\n customIncludeBody: z.string(),\n customExcludeBody: z.string(),\n })\n .loose()\n .transform(({ credentials: _credentials, apiKeys: _apiKeys, ...connection }) => connection);\n\n// 导入的残缺 Pi 快照不能靠默认服务商或协议补成可发送连接。\n// 保留外层方案,只丢弃无效连接,应用时再要求用户补全。\nconst ExtraModelPiProfile = z.union([ExtraModelPiProfileSnapshot, z.undefined()]).catch(undefined);\n\n// 活动连接容许保留未知字段,实际可发送性由统一预检决定。\nconst ExtraModelPiSettings = z\n .object({\n provider: z.string().trim().default('openai'),\n api: z.string().trim().default('openai-responses'),\n // 保留未知认证标识供用户查看;请求和方案保存会拒绝它,迁移会清除无归属的活动密钥。\n authType: z.string().trim().default('api_key'),\n endpoint: z.string().trim().default(''),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim().default(''),\n contextWindow: ExtraModelPiContextWindow,\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n credentials: z.record(z.string(), z.unknown()).catch({}).default({}),\n apiKeys: z.record(z.string(), z.string()).catch({}).default({}),\n customHeaders: z.string().default(''),\n customIncludeBody: z.string().default(''),\n customExcludeBody: z.string().default(''),\n })\n .loose()\n .prefault({});\n\nexport const EXTRA_MODEL_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\nconst ExtraModelResponseFormat = z.enum(EXTRA_MODEL_RESPONSE_FORMATS);\nconst ExtraModelJailbreakStrategy = z.enum(['使用内置破限', '使用当前预设', '使用其他预设']);\n\nconst ExtraModelApiProfile = z\n .object({\n 名称: z.string().trim().min(1),\n backend: z.enum(['custom', 'pi']).default('custom'),\n api地址: z.string().default(''),\n 密钥: z.string().default(''),\n 模型名称: z.string().default(''),\n pi: ExtraModelPiProfile.optional(),\n // 缺失选项保持未设置,使旧自定义方案可以一次性继承当前请求选项。\n 破限方案: ExtraModelJailbreakStrategy.optional().catch(undefined),\n 其他预设名称: z.string().optional().catch(undefined),\n 随机头部: z.boolean().optional().catch(undefined),\n 应答格式: ExtraModelResponseFormat.optional().catch(undefined),\n 关闭thinking: z.boolean().optional().catch(undefined),\n 兼容假流式: z.boolean().optional().catch(undefined),\n })\n .loose()\n .transform(({ customApiKey: _customApiKey, ...profile }) => profile);\n\n// 逐项容错,单个损坏方案只被过滤,不使整份用户设置重置。\nconst ExtraModelApiProfileList = z\n .array(ExtraModelApiProfile.nullable().catch(null))\n .catch([])\n .transform(profiles =>\n profiles.filter(\n (profile): profile is z.infer => profile !== null\n )\n );\n\nconst OldSettings = z\n .object({\n 通知: z\n .object({\n 变量更新出错: z.boolean(),\n 额外模型解析中: z.boolean(),\n })\n .loose(),\n 更新方式: z.enum(['随AI输出', '额外模型解析']),\n 自动触发额外模型解析: z.boolean(),\n 额外模型解析配置: z\n .object({\n 发送预设: z.boolean(),\n 使用函数调用: z.boolean(),\n 模型来源: z.enum(['与插头相同', '自定义']),\n api地址: z.string(),\n 密钥: z.string(),\n 模型名称: z.string(),\n 温度: z.coerce.number(),\n 频率惩罚: z.coerce.number(),\n 存在惩罚: z.coerce.number(),\n top_p: z.coerce.number(),\n 最大回复token数: z.coerce.number(),\n })\n .loose(),\n 快照保留间隔: z.number(),\n 更新到聊天变量: z.boolean(),\n legacy: z\n .object({\n 显示老旧功能: z.boolean(),\n })\n .loose(),\n auto_cleanup: z\n .object({\n 启用: z.boolean(),\n 要保留变量的最近楼层数: z.number(),\n 触发恢复变量的最近楼层数: z.number(),\n })\n .loose(),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean(),\n 已提醒自动清理旧变量功能: z.boolean(),\n 已提醒更新了API温度等配置: z.boolean(),\n 已默认开启自动清理旧变量功能: z.boolean(),\n })\n .loose(),\n })\n .loose()\n .transform(data => {\n const {\n 自动触发额外模型解析,\n 额外模型解析配置: { 发送预设, 使用函数调用, ...extra_model_settings },\n 快照保留间隔,\n 更新到聊天变量,\n legacy,\n auto_cleanup,\n 自动清理变量: existing_auto_cleanup,\n 兼容性: existing_compatibility,\n ...settings\n } = data;\n const existing_auto_cleanup_settings = _.isPlainObject(existing_auto_cleanup)\n ? (existing_auto_cleanup as Record)\n : {};\n const existing_compatibility_settings = _.isPlainObject(existing_compatibility)\n ? (existing_compatibility as Record)\n : {};\n\n return NewSettings.decode({\n ...settings,\n 额外模型解析配置: {\n 破限方案: 发送预设 ? '使用当前预设' : '使用内置破限',\n 启用自动请求: 自动触发额外模型解析,\n 应答格式: 使用函数调用 ? '工具调用' : '聊天消息',\n ...extra_model_settings,\n },\n 自动清理变量: {\n ...auto_cleanup,\n 快照保留间隔,\n ...existing_auto_cleanup_settings,\n },\n 兼容性: {\n ...legacy,\n 更新到聊天变量,\n ...existing_compatibility_settings,\n },\n });\n });\n\nconst NewSettings = z\n .object({\n 通知: z\n .object({\n MVU框架加载成功: z.boolean().default(true),\n 变量初始化成功: z.boolean().default(true),\n 变量更新出错: z.boolean().default(false),\n 额外模型解析中: z.boolean().default(true),\n })\n .loose()\n .prefault({}),\n 更新方式: z.enum(['随AI输出', '额外模型解析']).default('随AI输出'),\n 额外模型解析配置: z\n .object({\n 破限方案: ExtraModelJailbreakStrategy.default('使用内置破限'),\n 其他预设名称: z.string().default(''),\n 使用函数调用: z.boolean().optional(),\n 应答格式: ExtraModelResponseFormat.optional(),\n 关闭thinking: z.boolean().default(false),\n 兼容假流式: z.boolean().default(false),\n 随机头部: z.boolean().default(true),\n\n 启用自动请求: z.boolean().default(true),\n 最小请求字数: z.coerce\n .number()\n .catch(0)\n .default(0)\n .transform(value => Math.max(0, Math.floor(value))),\n 触发标签: z.string().default(''),\n 请求方式: z\n .enum([\n '依次请求,失败后重试',\n '同时请求多次',\n '先请求一次, 失败后再同时请求多次',\n ])\n .default('依次请求,失败后重试'),\n 请求次数: z.number().default(3),\n 世界书条目白名单正则: z.string().default(''),\n 世界书条目黑名单正则: z.string().default(''),\n\n 模型来源: z.enum(['与插头相同', '自定义', '更多']).default('与插头相同'),\n api地址: z.string().default('http://localhost:1234/v1'),\n 密钥: z.string().default(''),\n customApiKey: z.string().catch('').default(''),\n 模型名称: z.string().default('gemini-2.5-flash-nothinking'),\n pi: ExtraModelPiSettings,\n 温度: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 2)),\n 频率惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n 存在惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n top_p: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 1)),\n top_k: z.coerce\n .number()\n .default(0)\n .transform(value => _.clamp(value, 0, 500)),\n max_chat_history: z.coerce\n .number()\n .default(2)\n .transform(value => _.clamp(Math.round(value), 2, 100)),\n 最大回复token数: z.coerce\n .number()\n // 回复 token 的 0 没有默认值语义,可保留为阻止请求的无效导入值。\n .catch(0)\n .default(4096)\n .transform(value => Math.max(0, value)),\n api方案列表: ExtraModelApiProfileList.default([]),\n 当前api方案: z.string().catch('').default(''),\n })\n .loose()\n .transform(({ 使用函数调用, 应答格式, ...data }) =>\n migrateExtraModelApiProfiles({\n ...data,\n 应答格式: 应答格式 ?? (使用函数调用 ? '工具调用' : '聊天消息'),\n })\n )\n .prefault({}),\n 自动清理变量: z\n .object({\n 启用: z.boolean().default(true),\n 快照保留间隔: z.number().default(50),\n 要保留变量的最近楼层数: z.number().default(20),\n 触发恢复变量的最近楼层数: z.number().default(10),\n })\n .loose()\n .prefault({}),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().default(false),\n 显示老旧功能: z.boolean().default(false),\n 额外模型解析非阻塞: z.boolean().default(false),\n sendas不视为user消息: z.boolean().optional(),\n sandas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .transform(({ sendas不视为user消息, sandas不视为user消息, ...data }) => ({\n ...data,\n 更新到聊天变量: data.更新到聊天变量,\n 显示老旧功能: data.显示老旧功能,\n sendas不视为user消息: sendas不视为user消息 ?? sandas不视为user消息 ?? false,\n }))\n .prefault({}),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean().default(false),\n 已提醒自动清理旧变量功能: z.boolean().default(false),\n 已提醒更新了API温度等配置: z.boolean().default(false),\n 已默认开启自动清理旧变量功能: z.boolean().default(false),\n 已提醒内置破限: z.boolean().default(false),\n 已提醒额外模型同时请求: z.boolean().default(false),\n 已开启默认不兼容假流式: z.boolean().default(false),\n })\n .loose()\n .prefault({}),\n })\n .loose()\n .transform(data => {\n // 新安装由 Schema 提供 false;迁移时保留已保存的流式选项及方案继承值。\n data.internal.已开启默认不兼容假流式 = true;\n return data;\n })\n .prefault({});\n\nconst Settings = z.union([OldSettings, NewSettings]).catch(() => NewSettings.parse({}));\n\nexport type MvuSettings = z.infer;\n\nexport type CharacterSettingsState = {\n status: 'loading' | 'unbound' | 'ready' | 'error';\n character_name: string;\n worldbook_name: string | null;\n entry_uid: number | null;\n expected_content: string | null;\n draft: CharacterSettingsOverride;\n is_valid: boolean;\n revision: number;\n has_pending_save: boolean;\n is_saving: boolean;\n};\n\nfunction createCharacterSettingsState(): CharacterSettingsState {\n return {\n status: 'loading',\n character_name: '',\n worldbook_name: null,\n entry_uid: null,\n expected_content: null,\n draft: {},\n is_valid: true,\n revision: 0,\n has_pending_save: false,\n is_saving: false,\n };\n}\n\nconst Runtimes = z\n .object({\n unsupported_warnings: z.string().default(''),\n is_during_extra_analysis: z.boolean().default(false),\n is_function_call_enabled: z.boolean().default(false),\n 上次世界书条目过滤结果: z\n .array(\n z.object({\n lore: z.enum(['globalLore', 'characterLore', 'chatLore', 'personaLore']),\n world: z.string(),\n comment: z.string(),\n reason: z.enum(['白名单', '黑名单']),\n sources: z.array(z.enum(['用户全局配置', '角色卡配置'])).default([]),\n })\n )\n .default([]),\n debug: z\n .object({\n 首次额外请求必失败: z.boolean().default(false),\n })\n .prefault({}),\n })\n .prefault({});\n\nexport const useDataStore = defineStore('MVU变量框架', () => {\n const settings = ref(Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {})));\n watch(\n settings,\n new_settings => {\n _.set(SillyTavern.extensionSettings, 'mvu_settings', toRaw(new_settings));\n if (!is_jest_environment) SillyTavern.saveSettingsDebounced();\n },\n { deep: true }\n );\n const _reload_settings = () => {\n settings.value = Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {}));\n };\n\n const character_settings = ref(createCharacterSettingsState());\n const effective_settings = computed(() => {\n // 运行时读取合并后的配置,但不修改用户持久化的全局设置。\n const result = klona(settings.value);\n if (!character_settings.value.is_valid) {\n return result;\n }\n\n const draft = character_settings.value.draft;\n // 正则采用“全局 + 角色卡”的叠加语义,由 filterEntries 单独合并,不在此处覆盖。\n if (_.has(draft, '更新方式')) {\n result.更新方式 = draft.更新方式!;\n }\n if (_.has(draft, '额外模型解析配置.启用自动请求')) {\n result.额外模型解析配置.启用自动请求 = draft.额外模型解析配置!.启用自动请求!;\n }\n if (_.has(draft, '兼容性.更新到聊天变量')) {\n result.兼容性.更新到聊天变量 = draft.兼容性!.更新到聊天变量!;\n }\n if (_.has(draft, '兼容性.sendas不视为user消息')) {\n result.兼容性.sendas不视为user消息 = draft.兼容性!.sendas不视为user消息!;\n }\n return result;\n });\n const has_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid &&\n hasOwnCharacterSettingsOverride(character_settings.value.draft, path);\n const get_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid\n ? getCharacterSettingsOverrideValue(character_settings.value.draft, path)\n : undefined;\n const is_character_settings_override_active = computed(\n () =>\n character_settings.value.is_valid &&\n hasActiveCharacterSettingsOverride(character_settings.value.draft)\n );\n const resetCharacterSettings = () => {\n character_settings.value = createCharacterSettingsState();\n };\n\n const runtimes = ref(Runtimes.parse({}));\n watch(\n () => runtimes.value.is_during_extra_analysis,\n new_value => insertOrAssignVariables({ extra_analysis: new_value }, { type: 'global' }),\n { immediate: true }\n );\n const resetRuntimes = () => {\n runtimes.value = Runtimes.parse({});\n };\n\n const versions = ref<{ sillytavern: string; tavernhelper: string }>({\n sillytavern: '',\n tavernhelper: '',\n });\n const _wait_init = async () => {\n versions.value.sillytavern = await fetch('/version')\n .then(res => res.json())\n .then(data => data.pkgVersion)\n .catch(() => '1.0.0');\n versions.value.tavernhelper = await getTavernHelperVersion();\n };\n\n const should_enable = ref(false);\n watch(should_enable, (new_enable, old_enable) => {\n if (new_enable && !old_enable) {\n _reload_settings();\n }\n });\n\n // 当存在多个 MVU 脚本实例时,仅优先实例应启用运行逻辑。\n registerAsUniqueScript('MVU变量框架').listenPreferenceState(preferred_script_id => {\n should_enable.value = preferred_script_id === getScriptId();\n });\n\n return {\n settings,\n _reload_settings,\n character_settings,\n effective_settings,\n has_character_settings_override,\n get_character_settings_override,\n is_character_settings_override_active,\n resetCharacterSettings,\n runtimes,\n resetRuntimes,\n versions,\n _wait_init,\n should_enable,\n };\n});\n","import { tr } from '@/i18n';\n\nexport type RegexCompileResult = {\n regex?: RegExp;\n error?: string;\n};\n\nfunction isEscaped(value: string, index: number): boolean {\n let backslash_count = 0;\n for (let i = index - 1; i >= 0 && value[i] === '\\\\'; i--) {\n backslash_count++;\n }\n return backslash_count % 2 === 1;\n}\n\nfunction findClosingSlash(value: string): number {\n for (let i = value.length - 1; i > 0; i--) {\n if (value[i] === '/' && !isEscaped(value, i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function compileRegex(value: string): RegexCompileResult {\n const trimmed = value.trim();\n if (trimmed === '') {\n return {};\n }\n\n try {\n if (trimmed.startsWith('/')) {\n const closing_slash = findClosingSlash(trimmed);\n if (closing_slash <= 0) {\n throw new Error(tr('runtime.regex.jsStyleSyntax'));\n }\n return {\n regex: new RegExp(\n trimmed.slice(1, closing_slash),\n trimmed.slice(closing_slash + 1)\n ),\n };\n }\n\n return { regex: new RegExp(trimmed) };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n}\n","import { tr } from '@/i18n';\nimport { compare } from 'compare-versions';\nimport JSON5 from 'json5';\nimport { jsonrepair } from 'jsonrepair';\n\n// 修正 _.merge 对数组的合并逻辑, [1, 2, 3] 和 [4, 5] 合并后变成 [4, 5] 而不是 [4, 5, 3]\nexport function correctlyMerge(lhs: TObject, rhs: TSource): TObject & TSource {\n return _.mergeWith(lhs, rhs, (_lhs, rhs) => (_.isArray(rhs) ? rhs : undefined));\n}\n\nexport function uuidv4(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nexport async function checkMinimumVersion(\n expected: string,\n notice: { message: string; title: string }\n) {\n if (compare(await getTavernHelperVersion(), expected, '<')) {\n toastr.error(notice.message, notice.title);\n }\n}\nexport function literalYamlify(value: any) {\n return YAML.stringify(value, { blockQuote: 'literal' });\n}\n\nexport function parseString(content: string): any {\n const json_first = /^[[{]/s.test(content.trimStart());\n try {\n if (json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error1) {\n try {\n // eslint-disable-next-line import-x/no-named-as-default-member\n return JSON5.parse(content);\n } catch (json5_error) {\n try {\n return JSON.parse(jsonrepair(content));\n } catch (json_error) {\n try {\n if (!json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error2) {\n const toError = (error: unknown) =>\n error instanceof Error\n ? `${error.stack ? error.stack : error.message}`\n : String(error);\n\n throw new Error(\n literalYamlify({\n [tr('runtime.parseString.invalidFormat')]: {\n [tr('runtime.parseString.content')]: content,\n [tr('runtime.parseString.yamlError')]: toError(\n json_first ? yaml_error2 : yaml_error1\n ),\n [tr('runtime.parseString.json5Error')]: toError(json5_error),\n [tr('runtime.parseString.jsonError')]: toError(json_error),\n },\n })\n );\n }\n }\n }\n }\n}\n","export function teleportStyle(\n append_to:\n | JQuery.Selector\n | JQuery.htmlString\n | JQuery.TypeOrArray\n | JQuery = 'head'\n): { destroy: () => void } {\n const $div = $(`
`)\n .attr('script_id', getScriptId())\n .append($(`head > style`, document).clone())\n .appendTo(append_to);\n\n return {\n destroy: () => $div.remove(),\n };\n}\n\nexport function createScriptIdDiv(): JQuery {\n return $('
').attr('script_id', getScriptId()) as JQuery;\n}\n\n/**\n * 将当前脚本实例注册到共享的“唯一脚本”命名空间。\n *\n * 当同一脚本被重复加载时,优先实例为:仍存在于 TavernHelper 脚本列表中、\n * 且在注册顺序上最后出现的 script id。\n * 调用方可通过 `listenPreferenceState` 订阅优先实例变化,并仅在\n * `getScriptId()` 与优先实例一致时启用功能。\n */\nexport function registerAsUniqueScript(id: string): {\n unregister: () => void;\n getPreferredScriptId: () => string | undefined;\n listenPreferenceState: (callback: (perferred_script_id: string) => void) => EventOnReturn;\n} {\n // 当前实例在 TavernHelper 中的唯一脚本 ID。\n const script_id = getScriptId();\n // 以业务 id 作为命名空间,避免不同功能之间冲突。\n const path = `th_unique_check.${id}`;\n\n const getPreferredScriptId = () => {\n // 从共享状态中取出已注册实例集合(跨脚本实例共享在 window.parent)。\n const registered_scripts = _.get(window.parent, path, new Set());\n // 以页面上实际存在的脚本顺序为准,选出“最后一个仍有效”的实例作为优先实例。\n return _($('#tavern_helper').find('div[data-script-id]').toArray())\n .map(element => String($(element).attr('data-script-id')))\n .filter(element => registered_scripts.has(element))\n .last();\n };\n\n // 将当前实例加入注册集合。不存在的场合创建。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value === undefined) {\n return new Set([script_id]);\n }\n //避免重复添加\n if (value.has(script_id)) return value;\n value.add(script_id);\n return value;\n });\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n\n return {\n unregister: () => {\n // 卸载时从注册集合移除,并重新广播优先实例。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value !== undefined) {\n value.delete(script_id);\n }\n return value;\n });\n eventEmit(path, getPreferredScriptId());\n },\n getPreferredScriptId,\n // 监听优先实例变化(回调入参是当前优先实例 script id)。\n listenPreferenceState: (callback: (enabled_script_id: string) => void) => {\n const ret = eventOn(path, callback);\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n return ret;\n },\n };\n}\n","/* unplugin-vue-components disabled */// style-loader: Adds some css to the DOM by adding a \\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-character-override__metadata[data-v-ac919d4c]{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:0.25rem 0.7rem;margin:0;padding:0.35rem 0.55rem;border-radius:8px;background-color:rgba(0,0,0,0.08)}.mvu-character-override__metadata dt[data-v-ac919d4c]{font-weight:600;opacity:0.82}.mvu-character-override__metadata dd[data-v-ac919d4c]{min-width:0;margin:0;overflow-wrap:anywhere}.mvu-character-override__fieldset[data-v-ac919d4c]{min-inline-size:0;margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:0.45rem}.mvu-character-override__fieldset[data-v-ac919d4c]:disabled{opacity:0.6}.mvu-character-override__group[data-v-ac919d4c]{display:flex;flex-direction:column;gap:0.3rem;padding-top:0.1rem}.mvu-character-override__group-title[data-v-ac919d4c]{padding:0 0.15rem}.mvu-character-override__select-field.mvu-field[data-v-ac919d4c]{flex-flow:row nowrap;align-items:center;gap:0.4rem;padding:0.3rem 0.5rem}.mvu-character-override__select-field>.text_pole[data-v-ac919d4c]{flex:1 1 auto;width:auto;min-width:0;margin:0}.mvu-character-override__regex-error[data-v-ac919d4c]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/CharacterOverride.vue\"],\"names\":[],\"mappings\":\"AAsMA,mDACI,YAAa,CACb,+CAAiD,CACjD,kBAAmB,CACnB,QAAS,CACT,uBAAwB,CACxB,iBAAkB,CAClB,iCACJ,CAEA,sDACI,eAAgB,CAChB,YACJ,CAEA,sDACI,WAAY,CACZ,QAAS,CACT,sBACJ,CAEA,mDACI,iBAAkB,CAClB,QAAS,CACT,SAAU,CACV,QAAS,CACT,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,4DACI,WACJ,CAEA,gDACI,YAAa,CACb,qBAAsB,CACtB,UAAW,CACX,kBACJ,CAEA,sDACI,iBACJ,CAEA,iEACI,oBAAqB,CACrB,kBAAmB,CACnB,UAAW,CACX,qBACJ,CAEA,kEACI,aAAc,CACd,UAAW,CACX,WAAY,CACZ,QACJ,CAEA,sDACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-license-table-wrap[data-v-67402f54]{max-height:18rem;overflow:auto;border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:6px}.mvu-license-table[data-v-67402f54]{width:100%;border-collapse:collapse;font-size:0.9em}.mvu-license-table th[data-v-67402f54],.mvu-license-table td[data-v-67402f54]{padding:0.35rem 0.5rem;text-align:start;vertical-align:top;border-bottom:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,0.45))}.mvu-license-table th[data-v-67402f54]:last-child,.mvu-license-table td[data-v-67402f54]:last-child{width:7.5rem;white-space:nowrap}.mvu-license-table tbody tr:last-child td[data-v-67402f54]{border-bottom:0}.mvu-license-table a[data-v-67402f54]{overflow-wrap:anywhere}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Compatibility.vue\"],\"names\":[],\"mappings\":\"AA0GA,yCACI,gBAAiB,CACjB,aAAc,CACd,8DAAmE,CACnE,iBACJ,CAEA,oCACI,UAAW,CACX,wBAAyB,CACzB,eACJ,CAEA,8EAEI,sBAAuB,CACvB,gBAAiB,CACjB,kBAAmB,CACnB,wEACJ,CAEA,oGAEI,YAAa,CACb,kBACJ,CAEA,2DACI,eACJ,CAEA,sCACI,sBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.inline-drawer-content[data-v-7a58cca8]{flex-direction:column;gap:0.75rem;padding-top:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/Panel.vue\"],\"names\":[],\"mappings\":\"AAmCA,wCACI,qBAAsB,CACtB,WAAY,CACZ,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-details[data-v-3a7b912a]{border:1px dashed var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.5rem 0.7rem;background-color:rgba(0,0,0,0.06);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 70%,transparent)}.mvu-details__summary[data-v-3a7b912a]{cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;opacity:0.95}.mvu-details__title-suffix[data-v-3a7b912a]{display:inline-flex;align-items:center;margin-inline-start:0.35rem;vertical-align:middle}.mvu-details__content[data-v-3a7b912a]{margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Detail.vue\"],\"names\":[],\"mappings\":\"AAwBA,8BACI,+DAAoE,CACpE,kBAAmB,CACnB,qBAAsB,CACtB,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,cAAe,CACf,wBAAiB,CAAjB,gBAAiB,CACjB,eAAgB,CAChB,YACJ,CAEA,4CACI,mBAAoB,CACpB,kBAAmB,CACnB,2BAA4B,CAC5B,qBACJ,CAEA,uCACI,iBAAkB,CAClB,YAAa,CACb,qBAAsB,CACtB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field[data-v-1bd30ada]{padding:0.45rem 0.6rem;gap:0.25rem;border:1px solid color-mix(in srgb,var(--SmartThemeBorderColor,rgba(45,45,45,1)) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 55%,transparent)}.mvu-field__label[data-v-1bd30ada]{display:inline-flex;align-items:center;gap:0.35rem;font-weight:600;opacity:0.95}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Field.vue\"],\"names\":[],\"mappings\":\"AAeA,4BACI,sBAAuB,CACvB,WAAY,CACZ,iGAAwG,CACxG,kBAAmB,CACnB,mGAKJ,CAEA,mCACI,mBAAoB,CACpB,kBAAmB,CACnB,WAAY,CACZ,eAAgB,CAChB,YACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-help-icon[data-v-7a641daa]{cursor:pointer}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/HelpIcon.vue\"],\"names\":[],\"mappings\":\"AAoBA,gCACI,cACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-model-select[data-v-7b6ca08a]{display:flex;flex-direction:column;gap:0.5rem}.mvu-model-select__row[data-v-7b6ca08a]{width:100%}.mvu-model-select__row--controls[data-v-7b6ca08a]{display:grid;grid-template-columns:1fr auto;gap:0.5rem;align-items:center}.mvu-model-select__btn[data-v-7b6ca08a]{white-space:nowrap;text-align:left;padding:0.35rem 0.6rem;min-height:unset;height:2.05rem;line-height:1.1}@media (max-width:520px){.mvu-model-select__row--controls[data-v-7b6ca08a]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/ModelSelect.vue\"],\"names\":[],\"mappings\":\"AA4JA,mCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,wCACI,UACJ,CAEA,kDACI,YAAa,CACb,8BAA+B,CAC/B,UAAW,CACX,kBACJ,CAEA,wCACI,kBAAmB,CACnB,eAAgB,CAChB,sBAAuB,CACvB,gBAAiB,CACjB,cAAe,CACf,eACJ,CAEA,yBACI,kDACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-override-badge[data-v-14bdab4e]{display:inline-flex;flex:0 1 auto;align-items:center;max-width:100%;padding:0.08rem 0.38rem;border:1px solid currentColor;border-radius:999px;font-size:calc(var(--mainFontSize,1rem) * 0.76);font-weight:600;line-height:1.25;text-align:center;overflow-wrap:anywhere}.mvu-override-badge--override[data-v-14bdab4e]{color:var(--SmartThemeEmColor,#d39e00);background-color:color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 12%,transparent)}.mvu-override-badge--additive[data-v-14bdab4e]{color:var(--SmartThemeQuoteColor,#6ba7ff);background-color:color-mix(in srgb,var(--SmartThemeQuoteColor,#6ba7ff) 12%,transparent)}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/OverrideBadge.vue\"],\"names\":[],\"mappings\":\"AAuBA,qCACI,mBAAoB,CACpB,aAAc,CACd,kBAAmB,CACnB,cAAe,CACf,uBAAwB,CACxB,6BAA8B,CAC9B,mBAAoB,CACpB,+CAAiD,CACjD,eAAgB,CAChB,gBAAiB,CACjB,iBAAkB,CAClB,sBACJ,CAEA,+CACI,sCAAwC,CACxC,oFACJ,CAEA,+CACI,yCAA2C,CAC3C,uFACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-range-number[data-v-48562df7]{display:grid;grid-template-columns:1fr 7.5rem;gap:0.5rem;align-items:center}.mvu-range-number__range[data-v-48562df7]{width:100%}.mvu-range-number__number[data-v-48562df7]{text-align:left;padding-top:0.3rem;padding-bottom:0.3rem;background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 33%,transparent)}@media (max-width:420px){.mvu-range-number[data-v-48562df7]{grid-template-columns:1fr 6.5rem}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/RangeNumber.vue\"],\"names\":[],\"mappings\":\"AA2CA,mCACI,YAAa,CACb,gCAAiC,CACjC,UAAW,CACX,kBACJ,CAEA,0CACI,UACJ,CAEA,2CACI,eAAgB,CAChB,kBAAmB,CACnB,qBAAsB,CACtB,mGAKJ,CAEA,yBACI,mCACI,gCACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-section[data-v-ab897094]{border:1px solid var(--SmartThemeBorderColor,rgba(45,45,45,1));border-radius:10px;padding:0.6rem 0.75rem;gap:0.45rem;background-color:rgba(0,0,0,0.08);background-color:color-mix(in srgb,var(--SmartThemeBlurTintColor,rgba(31,31,31,1)) 75%,transparent)}.mvu-section__content[data-v-ab897094]{gap:0.5rem}.mvu-section__title[data-v-ab897094]{display:flex;flex-wrap:wrap;align-items:center;gap:0.3rem}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/component/Section.vue\"],\"names\":[],\"mappings\":\"AAmBA,8BACI,8DAAmE,CACnE,kBAAmB,CACnB,sBAAuB,CACvB,WAAY,CACZ,iCAAqC,CACrC,mGAKJ,CAEA,uCACI,UACJ,CAEA,qCACI,YAAa,CACb,cAAe,CACf,kBAAmB,CACnB,UACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-warning[data-v-58f80c4e]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-58f80c4e]{line-height:1}.mvu-warning__text[data-v-58f80c4e]{word-break:break-word}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Method.vue\"],\"names\":[],\"mappings\":\"AA4CA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-note[data-v-7ae42438]{padding:0 0.6rem 0.45rem;opacity:0.8;font-size:calc(var(--mainFontSize,1rem) * 0.9)}.mvu-regex-error[data-v-7ae42438]{color:var(--SmartThemeQuoteColor,#ff6b6b);font-size:calc(var(--mainFontSize,1rem) * 0.9);line-height:1.35;word-break:break-word}.mvu-regex-actions[data-v-7ae42438]{display:flex;padding:0 0.6rem 0.45rem}.mvu-regex-actions__button[data-v-7ae42438]{min-height:2rem;white-space:normal}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Prompt.vue\"],\"names\":[],\"mappings\":\"AA0MA,2BACI,wBAAyB,CACzB,WAAY,CACZ,8CACJ,CAEA,kCACI,yCAA2C,CAC3C,8CAAgD,CAChD,gBAAiB,CACjB,qBACJ,CAEA,oCACI,YAAa,CACb,wBACJ,CAEA,4CACI,eAAgB,CAChB,kBACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.mvu-field-grid[data-v-0edef090]{display:flex;flex-direction:column;gap:0.5rem}.mvu-api-profile-controls[data-v-0edef090]{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem}.mvu-api-profile-actions[data-v-0edef090]{display:flex;flex-wrap:wrap;gap:0.5rem}.mvu-note[data-v-0edef090]{opacity:0.85;color:var(--SmartThemeEmColor,inherit)}.mvu-pi-proxy-option[data-v-0edef090]{padding:0 0.6rem}.mvu-warning[data-v-0edef090]{margin-top:0.5rem;padding:0.55rem 0.7rem;border:1px solid color-mix(in srgb,var(--SmartThemeEmColor,#d39e00) 35%,transparent);border-radius:10px;background-color:color-mix(in srgb,var(--SmartThemeEmColor,#fff3cd) 15%,transparent);color:var(--SmartThemeEmColor,#856404);display:grid;grid-template-columns:auto 1fr;-moz-column-gap:0.5rem;column-gap:0.5rem;align-items:center}.mvu-warning__icon[data-v-0edef090]{line-height:1}.mvu-warning__text[data-v-0edef090]{word-break:break-word}.mvu-pi-advanced-textarea[data-v-0edef090]{width:100%;min-height:5rem;resize:vertical;font-family:var(--monoFontFamily,monospace);white-space:pre}.mvu-field-error[data-v-0edef090]{color:var(--SmartThemeQuoteColor,#ff6b6b);line-height:1.35;word-break:break-word}.mvu-oauth-status[data-v-0edef090]{display:flex;flex-direction:column;gap:0.25rem}.mvu-link-button[data-v-0edef090]{display:inline-flex;align-items:center;text-decoration:none}@media (max-width:520px){.mvu-api-profile-controls[data-v-0edef090]{grid-template-columns:1fr}}\n`, \"\",{\"version\":3,\"sources\":[\"webpack://./src/panel/update/Source.vue\"],\"names\":[],\"mappings\":\"AA0wDA,iCACI,YAAa,CACb,qBAAsB,CACtB,UACJ,CAEA,2CACI,YAAa,CACb,6BAA8B,CAC9B,UACJ,CAEA,0CACI,YAAa,CACb,cAAe,CACf,UACJ,CAEA,2BACI,YAAa,CACb,sCACJ,CAEA,sCACI,gBACJ,CAEA,8BACI,iBAAkB,CAClB,sBAAuB,CACvB,oFAAwF,CACxF,kBAAmB,CACnB,oFAAwF,CACxF,sCAAwC,CACxC,YAAa,CACb,8BAA+B,CAC/B,sBAAkB,CAAlB,iBAAkB,CAClB,kBACJ,CAEA,oCACI,aACJ,CAEA,oCACI,qBACJ,CAEA,2CACI,UAAW,CACX,eAAgB,CAChB,eAAgB,CAChB,2CAA6C,CAC7C,eACJ,CAEA,kCACI,yCAA2C,CAC3C,gBAAiB,CACjB,qBACJ,CAEA,mCACI,YAAa,CACb,qBAAsB,CACtB,WACJ,CAEA,kCACI,mBAAoB,CACpB,kBAAmB,CACnB,oBACJ,CAEA,yBACI,2CACI,yBACJ,CACJ\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = [];\n\n // return the list of modules as css string\n list.toString = function toString() {\n return this.map(function (item) {\n var content = \"\";\n var needLayer = typeof item[5] !== \"undefined\";\n if (item[4]) {\n content += \"@supports (\".concat(item[4], \") {\");\n }\n if (item[2]) {\n content += \"@media \".concat(item[2], \" {\");\n }\n if (needLayer) {\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\n }\n content += cssWithMappingToString(item);\n if (needLayer) {\n content += \"}\";\n }\n if (item[2]) {\n content += \"}\";\n }\n if (item[4]) {\n content += \"}\";\n }\n return content;\n }).join(\"\");\n };\n\n // import a list of modules into the list\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === \"string\") {\n modules = [[null, modules, undefined]];\n }\n var alreadyImportedModules = {};\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n if (typeof layer !== \"undefined\") {\n if (typeof item[5] === \"undefined\") {\n item[5] = layer;\n } else {\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\n item[5] = layer;\n }\n }\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\n item[2] = media;\n }\n }\n if (supports) {\n if (!item[4]) {\n item[4] = \"\".concat(supports);\n } else {\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\n item[4] = supports;\n }\n }\n list.push(item);\n }\n };\n return list;\n};","\"use strict\";\n\nmodule.exports = function (item) {\n var content = item[1];\n var cssMapping = item[3];\n if (!cssMapping) {\n return content;\n }\n if (typeof btoa === \"function\") {\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n var sourceMapping = \"/*# \".concat(data, \" */\");\n return [content].concat([sourceMapping]).join(\"\\n\");\n }\n return [content].join(\"\\n\");\n};","import { compileEntryCommentRegex } from '@/function/request/entry_comment_regex';\nimport { tr } from '@/i18n';\nimport { parseString } from '@util/common';\nimport { klona } from 'klona';\nimport * as z from 'zod';\n\nexport const CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME = '[config_override]';\n\n// loose 模式用于保留未来版本或其他扩展写入的未知配置字段。\nexport const CharacterSettingsOverrideSchema = z\n .object({\n 更新方式: z.enum(['随AI输出', '额外模型解析']).optional(),\n 额外模型解析配置: z\n .object({\n 启用自动请求: z.boolean().optional(),\n 世界书条目白名单正则: z.string().optional(),\n 世界书条目黑名单正则: z.string().optional(),\n })\n .loose()\n .optional(),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().optional(),\n sendas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .optional(),\n })\n .loose();\n\nexport type CharacterSettingsOverride = z.infer;\n\nexport type CharacterSettingsOverridePath =\n | '更新方式'\n | '额外模型解析配置.启用自动请求'\n | '额外模型解析配置.世界书条目白名单正则'\n | '额外模型解析配置.世界书条目黑名单正则'\n | '兼容性.更新到聊天变量'\n | '兼容性.sendas不视为user消息';\n\nexport type CharacterSettingsOverrideValue = string | boolean | undefined;\n\nexport function isCharacterSettingsOverrideEntryName(name: unknown): boolean {\n return (\n typeof name === 'string' &&\n name.trim().toLowerCase() === CHARACTER_SETTINGS_OVERRIDE_ENTRY_NAME\n );\n}\n\nexport function hasOwnCharacterSettingsOverride(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): boolean {\n return _.has(draft, path);\n}\n\nexport function getCharacterSettingsOverrideValue(\n draft: CharacterSettingsOverride,\n path: CharacterSettingsOverridePath\n): CharacterSettingsOverrideValue {\n return _.get(draft, path) as CharacterSettingsOverrideValue;\n}\n\nfunction removeEmptyKnownValues(draft: CharacterSettingsOverride): CharacterSettingsOverride {\n // “继承全局配置”在文件中表示字段不存在,而不是保存空字符串或空对象。\n const result = klona(draft);\n const extra_model = result.额外模型解析配置;\n if (extra_model) {\n for (const key of ['世界书条目白名单正则', '世界书条目黑名单正则'] as const) {\n if (extra_model[key]?.trim() === '') {\n delete extra_model[key];\n }\n }\n if (Object.keys(extra_model).length === 0) {\n delete result.额外模型解析配置;\n }\n }\n if (result.兼容性 && Object.keys(result.兼容性).length === 0) {\n delete result.兼容性;\n }\n return result;\n}\n\nexport function normalizeCharacterSettingsOverride(\n draft: CharacterSettingsOverride\n): CharacterSettingsOverride {\n return CharacterSettingsOverrideSchema.parse(removeEmptyKnownValues(draft));\n}\n\nfunction makeAdditionalPropertiesExplicit(schema: unknown): unknown {\n // Zod 会用空对象表示允许附加属性;显式改为 true 便于其他 JSON Schema 工具识别。\n if (Array.isArray(schema)) {\n return schema.map(makeAdditionalPropertiesExplicit);\n }\n if (!_.isPlainObject(schema)) {\n return schema;\n }\n\n const result = Object.fromEntries(\n Object.entries(schema as Record).map(([key, value]) => [\n key,\n key === 'additionalProperties' && _.isPlainObject(value) && _.isEmpty(value)\n ? true\n : makeAdditionalPropertiesExplicit(value),\n ])\n );\n return result;\n}\n\nexport function getCharacterSettingsOverrideJsonSchema(): Record {\n const generated = z.toJSONSchema(CharacterSettingsOverrideSchema, {\n target: 'draft-2020-12',\n });\n return {\n ...(makeAdditionalPropertiesExplicit(generated) as Record),\n title: 'CharacterSettingsOverride',\n };\n}\n\nexport function parseCharacterSettingsOverrideContent(content: string): CharacterSettingsOverride {\n const document = JSON.parse(content) as unknown;\n if (!_.isPlainObject(document)) {\n throw new Error(tr('runtime.characterOverride.contentMustBeObject'));\n }\n\n // schema 仅供角色卡编辑器展示,不属于实际覆盖配置。\n const { schema: _embedded_schema, ...draft } = document as Record;\n return normalizeCharacterSettingsOverride(CharacterSettingsOverrideSchema.parse(draft));\n}\n\nexport function recoverCharacterSettingsOverridePassthrough(\n content: string\n): CharacterSettingsOverride {\n const document = parseString(content) as unknown;\n if (!_.isPlainObject(document)) {\n return {};\n }\n\n // 丢弃可能损坏的已知字段,只抢救能够安全原样写回的未知扩展字段。\n const recovered = klona(document) as Record;\n delete recovered.schema;\n delete recovered.更新方式;\n\n const remove_known_nested_fields = (key: '额外模型解析配置' | '兼容性', fields: string[]) => {\n const nested = recovered[key];\n if (!_.isPlainObject(nested)) {\n delete recovered[key];\n return;\n }\n const nested_record = nested as Record;\n fields.forEach(field => delete nested_record[field]);\n if (_.isEmpty(nested_record)) {\n delete recovered[key];\n }\n };\n remove_known_nested_fields('额外模型解析配置', [\n '启用自动请求',\n '世界书条目白名单正则',\n '世界书条目黑名单正则',\n ]);\n remove_known_nested_fields('兼容性', ['更新到聊天变量', 'sendas不视为user消息']);\n\n return CharacterSettingsOverrideSchema.parse(recovered);\n}\n\nexport function serializeCharacterSettingsOverride(draft: CharacterSettingsOverride): string {\n const normalized = normalizeCharacterSettingsOverride(draft);\n const { schema: _existing_schema, ...config } = normalized;\n // schema 固定放在最后,兼顾人工阅读和 SillyTavern 的编辑体验。\n const document = {\n ...config,\n schema: getCharacterSettingsOverrideJsonSchema(),\n };\n return JSON.stringify(document, null, 4);\n}\n\nexport function hasActiveCharacterSettingsOverride(draft: CharacterSettingsOverride): boolean {\n const ordinary_paths: CharacterSettingsOverridePath[] = [\n '更新方式',\n '额外模型解析配置.启用自动请求',\n '兼容性.更新到聊天变量',\n '兼容性.sendas不视为user消息',\n ];\n if (ordinary_paths.some(path => hasOwnCharacterSettingsOverride(draft, path))) {\n return true;\n }\n\n // 空白或无效正则不会参与过滤,也不应让界面显示为“覆盖中”。\n return (\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目白名单正则 ?? '').regex !==\n undefined ||\n compileEntryCommentRegex(draft.额外模型解析配置?.世界书条目黑名单正则 ?? '').regex !==\n undefined\n );\n}\n","import { tr } from '@/i18n';\n\nexport type EntryCommentRegexCompileResult = {\n regex?: RegExp;\n error?: string;\n};\n\nexport type EntryCommentFilterSource = '用户全局配置' | '角色卡配置';\n\nexport type EntryCommentFilterResult = {\n lore: 'globalLore' | 'characterLore' | 'chatLore' | 'personaLore';\n world: string;\n comment: string;\n reason: '白名单' | '黑名单';\n sources: EntryCommentFilterSource[];\n};\n\nexport const ENTRY_COMMENT_FILTER_LOG_TITLE = tr('runtime.filter.logTitle');\n\nexport function logEntryCommentFilterResult(result: EntryCommentFilterResult[]) {\n console.log(\n ENTRY_COMMENT_FILTER_LOG_TITLE,\n result.map(entry => ({ ...entry }))\n );\n}\n\nfunction isEscaped(value: string, index: number): boolean {\n let backslash_count = 0;\n for (let i = index - 1; i >= 0 && value[i] === '\\\\'; i--) {\n backslash_count++;\n }\n return backslash_count % 2 === 1;\n}\n\nfunction findClosingSlash(value: string): number {\n for (let i = value.length - 1; i > 0; i--) {\n if (value[i] === '/' && !isEscaped(value, i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function compileEntryCommentRegex(value: string): EntryCommentRegexCompileResult {\n const trimmed = value.trim();\n if (trimmed === '') {\n return {};\n }\n\n try {\n if (trimmed.startsWith('/')) {\n const closing_slash = findClosingSlash(trimmed);\n if (closing_slash <= 0) {\n throw new Error(tr('runtime.regex.jsStyleSyntax'));\n }\n return {\n regex: new RegExp(\n trimmed.slice(1, closing_slash),\n trimmed.slice(closing_slash + 1)\n ),\n };\n }\n\n return { regex: new RegExp(trimmed) };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n}\n\nexport function testEntryCommentRegex(regex: RegExp, comment: string): boolean {\n regex.lastIndex = 0;\n return regex.test(comment);\n}\n","import { normalizePiThinkingLevel, type PiThinkingLevel } from './pi/thinking_setting';\nimport { tr } from '@/i18n';\nimport {\n getPiProviderTargetDefinition,\n resolvePiApiKeyScope,\n} from '@/function/update/pi/provider_target';\nimport { klona } from 'klona';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './pi/credential_refs';\n\nexport type ExtraModelApiProfileBackend = 'custom' | 'pi';\n\nexport type ExtraModelApiRequestFields = {\n 破限方案: '使用内置破限' | '使用当前预设' | '使用其他预设';\n 其他预设名称: string;\n 随机头部: boolean;\n 应答格式: '聊天消息' | '工具调用' | '格式化输出' | '格式化输出(v4兼容)';\n 关闭thinking: boolean;\n 兼容假流式: boolean;\n};\n\nconst API_REQUEST_FIELDS = [\n '破限方案',\n '其他预设名称',\n '随机头部',\n '应答格式',\n '关闭thinking',\n '兼容假流式',\n] as const satisfies readonly (keyof ExtraModelApiRequestFields)[];\n\n/** 提取已配置的请求选项,保留缺失字段,供方案保存和旧方案补全使用。 */\nfunction extractApiRequestFields(\n config: Partial\n): Partial {\n return Object.fromEntries(\n API_REQUEST_FIELDS.filter(field => config[field] !== undefined).map(field => [\n field,\n config[field],\n ])\n );\n}\n\nexport type ExtraModelPiConnectionFields = {\n [key: string]: unknown;\n /** 各服务商绑定的集中 OAuth 凭证编号;空映射表示明确未登录。 */\n credentialIds?: PiCredentialReferences;\n provider: string;\n api: string;\n authType: string;\n endpoint: string;\n useProxy: boolean;\n thinkingLevel?: PiThinkingLevel;\n model: string;\n contextWindow: number | string;\n customHeaders: string;\n customIncludeBody: string;\n customExcludeBody: string;\n};\n\nexport type ExtraModelPiSettings = ExtraModelPiConnectionFields & {\n credentials: Record;\n apiKeys?: Record;\n};\n\nexport type ExtraModelApiProfile = Partial & {\n [key: string]: unknown;\n 名称: string;\n backend?: ExtraModelApiProfileBackend;\n api地址: string;\n 密钥: string;\n 模型名称: string;\n pi?: ExtraModelPiConnectionFields;\n};\n\nexport type ExtraModelApiProfileFields = Partial & {\n 模型来源?: '与插头相同' | '自定义' | '更多';\n api地址: string;\n 密钥: string;\n customApiKey?: string;\n 模型名称: string;\n pi?: ExtraModelPiSettings;\n api方案列表: ExtraModelApiProfile[];\n 当前api方案: string;\n};\n\nexport const DEFAULT_EXTRA_MODEL_API_PROFILE_NAME = '默认';\n\n/** 统一方案名两端空白,避免查找、去重和保存使用不同的名称。 */\nfunction normalizeExtraModelApiProfileName(name: string): string {\n return name.trim();\n}\n\n/**\n * 将方案中的上下文窗口规范为非负整数,0 表示使用目录值。\n * 返回 undefined 表示配置无效,调用方不得将其当作目录默认值。\n */\nexport function normalizeExtraModelPiProfileContextWindow(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isInteger(value) && value >= 0 ? value : undefined;\n }\n if (typeof value !== 'string') {\n return undefined;\n }\n\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;\n}\n\n/** 根据当前模型来源确定方案保存的是自定义连接还是 Pi 连接。 */\nfunction getExtraModelApiProfileBackend(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileBackend {\n return config.模型来源 === '更多' ? 'pi' : 'custom';\n}\n\n/**\n * 生成脱离响应式代理的连接快照,并规范连接标识与上下文窗口。\n * 快照不包含 OAuth 凭证或按目标缓存的 API Key,避免方案复制这些共享状态。\n */\nfunction clonePiConnectionFields(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): ExtraModelPiConnectionFields | undefined {\n if (pi === undefined) {\n return undefined;\n }\n\n // Settings and profile entries normally arrive as Vue/Pinia reactive proxies. Native\n // structuredClone rejects proxies with DataCloneError, while klona traverses them into\n // detached plain objects and still preserves unknown forward-compatible fields.\n const cloned = klona(pi);\n delete cloned.credentials;\n delete cloned.apiKeys;\n if (pi.credentialIds !== undefined || pi.credentials !== undefined) {\n cloned.credentialIds = resolvePiCredentialReferences(\n pi,\n (pi.credentials as Record | undefined) ?? {}\n );\n }\n // Runtime resolution treats surrounding whitespace on connection identifiers as insignificant.\n // Persist the same canonical representation so profile validation, the Source form, dirty\n // checks, and runtime resolution cannot disagree about a target after import.\n for (const field of ['provider', 'api', 'authType', 'endpoint', 'model'] as const) {\n if (typeof cloned[field] === 'string') {\n cloned[field] = cloned[field].trim();\n }\n }\n // Profiles saved before this option existed, and malformed imported values, must retain the\n // direct-transport default rather than becoming enabled through truthiness coercion.\n cloned.useProxy = cloned.useProxy === true;\n cloned.thinkingLevel = normalizePiThinkingLevel(cloned.thinkingLevel);\n const context_window = normalizeExtraModelPiProfileContextWindow(cloned.contextWindow);\n if (context_window !== undefined) {\n cloned.contextWindow = context_window;\n }\n return cloned;\n}\n\n/** 仅为完整且受支持的 API Key 目标解析密钥缓存位置;无效连接返回空字符串。 */\nfunction resolvePiConnectionApiKeyScope(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): string {\n if (\n pi === undefined ||\n typeof pi.provider !== 'string' ||\n typeof pi.api !== 'string' ||\n typeof pi.authType !== 'string' ||\n typeof pi.endpoint !== 'string'\n ) {\n return '';\n }\n\n return resolvePiApiKeyScope(\n getPiProviderTargetDefinition(pi.provider.trim()),\n pi.api.trim(),\n pi.authType.trim(),\n pi.endpoint\n );\n}\n\n/** 检查导入快照是否具备全部连接字段及合法类型,避免用默认值补成可发送连接。 */\nfunction hasCompletePiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n pi !== undefined &&\n typeof pi.provider === 'string' &&\n typeof pi.api === 'string' &&\n typeof pi.authType === 'string' &&\n (pi.authType.trim() === 'api_key' || pi.authType.trim() === 'oauth') &&\n typeof pi.endpoint === 'string' &&\n typeof pi.useProxy === 'boolean' &&\n typeof pi.model === 'string' &&\n typeof pi.contextWindow === 'number' &&\n Number.isInteger(pi.contextWindow) &&\n pi.contextWindow >= 0 &&\n typeof pi.customHeaders === 'string' &&\n typeof pi.customIncludeBody === 'string' &&\n typeof pi.customExcludeBody === 'string'\n );\n}\n\n/** 在结构完整的基础上检查服务商、协议和模型是否已填写。 */\nfunction hasConfiguredPiConnectionSnapshot(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): pi is ExtraModelPiConnectionFields | ExtraModelPiSettings {\n return (\n hasCompletePiConnectionSnapshot(pi) &&\n pi.provider.trim().length > 0 &&\n pi.api.trim().length > 0 &&\n pi.model.trim().length > 0\n );\n}\n\n/** 判断连接能否归属到有效的 API Key 目标,作为保留方案密钥的条件。 */\nfunction hasValidPiApiKeyTarget(\n pi: ExtraModelPiConnectionFields | ExtraModelPiSettings | undefined\n): boolean {\n return hasConfiguredPiConnectionSnapshot(pi) && resolvePiConnectionApiKeyScope(pi) !== '';\n}\n\n/** 规范单个方案的名称、后端和连接快照,并清除不属于当前认证方式的密钥。 */\nfunction normalizeExtraModelApiProfile(profile: ExtraModelApiProfile): ExtraModelApiProfile {\n const cloned = klona(profile);\n delete cloned.customApiKey;\n cloned.名称 = normalizeExtraModelApiProfileName(cloned.名称);\n const backend: ExtraModelApiProfileBackend = cloned.backend === 'pi' ? 'pi' : 'custom';\n cloned.backend = backend;\n\n if (backend === 'pi' && cloned.pi !== undefined) {\n cloned.pi = clonePiConnectionFields(cloned.pi);\n } else {\n delete cloned.pi;\n }\n if (backend === 'pi') {\n // These legacy top-level fields belong to the hidden Custom source. Pi owns its endpoint\n // and model inside `pi`, so importing or saving them here must never overwrite that cache.\n cloned.api地址 = '';\n cloned.模型名称 = '';\n\n // A profile-level key has wire meaning only for a registry-backed, normalizable API-key\n // target. OAuth and malformed/future targets retain metadata but never a dormant secret.\n if (!hasValidPiApiKeyTarget(cloned.pi)) {\n cloned.密钥 = '';\n }\n }\n return cloned;\n}\n\n/** 规范方案列表,剔除空名称和重复名称,同名方案只保留首次出现的条目。 */\nfunction normalizeExtraModelApiProfileList(\n profiles: ExtraModelApiProfile[]\n): ExtraModelApiProfile[] {\n const names = new Set();\n const normalized_profiles: ExtraModelApiProfile[] = [];\n for (const profile of profiles) {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n if (normalized_profile.名称 === '' || names.has(normalized_profile.名称)) {\n continue;\n }\n names.add(normalized_profile.名称);\n normalized_profiles.push(normalized_profile);\n }\n return normalized_profiles;\n}\n\n/** 将方案连接合并到运行设置,同时保留当前凭证仓库和目标密钥缓存。 */\nfunction mergePiConnectionFields(\n current: ExtraModelPiSettings | undefined,\n profile: ExtraModelPiConnectionFields\n): ExtraModelPiSettings {\n const current_clone = current === undefined ? undefined : klona(current);\n const profile_clone = clonePiConnectionFields(profile)!;\n return {\n ...current_clone,\n ...profile_clone,\n credentialIds: resolvePiCredentialReferences(profile, current_clone?.credentials),\n credentials: klona(current_clone?.credentials ?? {}),\n apiKeys: klona(current_clone?.apiKeys ?? {}),\n } as ExtraModelPiSettings;\n}\n\n/** 清空可发送的连接字段,保留共享凭证和密钥缓存,等待用户重新配置。 */\nfunction clearPiConnectionFields(pi: ExtraModelPiSettings): ExtraModelPiSettings {\n const cloned = klona(pi);\n return {\n ...cloned,\n credentialIds: {},\n provider: '',\n api: '',\n authType: 'api_key',\n endpoint: '',\n useProxy: false,\n thinkingLevel: 'default',\n model: '',\n contextWindow: 0,\n credentials: klona(cloned.credentials),\n apiKeys: klona(cloned.apiKeys ?? {}),\n customHeaders: '',\n customIncludeBody: '',\n customExcludeBody: '',\n };\n}\n\n/** 从当前设置提取可保存的连接和请求选项;Pi 方案必须先通过完整性校验。 */\nexport function extractExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfile {\n const backend = getExtraModelApiProfileBackend(config);\n const pi_snapshot = backend === 'pi' ? clonePiConnectionFields(config.pi) : undefined;\n if (backend === 'pi' && !hasConfiguredPiConnectionSnapshot(pi_snapshot)) {\n throw new Error(tr('runtime.apiProfile.piConfigRequired'));\n }\n return {\n 名称:\n normalizeExtraModelApiProfileName(config.当前api方案) ||\n DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n backend,\n api地址: backend === 'pi' ? '' : config.api地址,\n 密钥: backend === 'pi' && !hasValidPiApiKeyTarget(pi_snapshot) ? '' : config.密钥,\n 模型名称: backend === 'pi' ? '' : config.模型名称,\n ...extractApiRequestFields(config),\n ...(backend === 'pi' && pi_snapshot !== undefined ? { pi: pi_snapshot } : {}),\n };\n}\n\n/**\n * 应用方案的连接及请求选项,并切换到相应模型来源。\n * 无效 Pi 快照会清空活动连接,不能借用其他来源的密钥继续发送。\n */\nexport function applyExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile: ExtraModelApiProfile\n): ExtraModelApiProfileFields {\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const result: ExtraModelApiProfileFields = {\n ...config,\n ...extractApiRequestFields(normalized_profile),\n 模型来源: normalized_profile.backend === 'pi' ? '更多' : '自定义',\n 密钥: normalized_profile.密钥,\n 当前api方案: normalized_profile.名称,\n ...(normalized_profile.backend === 'custom'\n ? {\n api地址: normalized_profile.api地址,\n 模型名称: normalized_profile.模型名称,\n }\n : {}),\n };\n if (normalized_profile.backend === 'pi') {\n if (hasConfiguredPiConnectionSnapshot(normalized_profile.pi)) {\n result.pi = mergePiConnectionFields(config.pi, normalized_profile.pi);\n } else {\n // Never combine a malformed Pi profile's key with the previously active provider,\n // endpoint, or model. Preserve credentials, but disable the connection until the\n // user repairs and saves a complete snapshot.\n result.密钥 = '';\n if (config.pi !== undefined) {\n result.pi = clearPiConnectionFields(config.pi);\n }\n }\n }\n return result;\n}\n\n/** 按规范化名称新增或更新方案,更新时保留已有条目的其他字段。 */\nexport function upsertExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile: ExtraModelApiProfile\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile.名称);\n if (!normalized_name) {\n throw new Error(tr('runtime.apiProfile.nameRequired'));\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(profiles);\n const next_profile = {\n ...normalizeExtraModelApiProfile(profile),\n 名称: normalized_name,\n };\n const existing_index = normalized_profiles.findIndex(item => item.名称 === normalized_name);\n if (existing_index === -1) {\n return [...normalized_profiles, next_profile];\n }\n\n const next_profiles = [...normalized_profiles];\n next_profiles[existing_index] = normalizeExtraModelApiProfile({\n ...normalized_profiles[existing_index],\n ...next_profile,\n });\n return next_profiles;\n}\n\n/** 返回删除指定方案后的规范化列表,不直接修改原列表。 */\nexport function removeExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): ExtraModelApiProfile[] {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).filter(\n profile => profile.名称 !== normalized_name\n );\n}\n\n/** 按规范化名称检查方案是否存在,供保存和删除操作校验。 */\nexport function hasExtraModelApiProfile(\n profiles: ExtraModelApiProfile[],\n profile_name: string\n): boolean {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n return normalizeExtraModelApiProfileList(profiles).some(\n profile => profile.名称 === normalized_name\n );\n}\n\n/** 比较活动方案与当前连接、密钥及请求选项,判断是否存在未保存修改。 */\nexport function isActiveExtraModelApiProfileDirty(config: ExtraModelApiProfileFields): boolean {\n const active_name = normalizeExtraModelApiProfileName(config.当前api方案);\n if (!active_name) {\n return false;\n }\n\n const profile = normalizeExtraModelApiProfileList(config.api方案列表).find(\n item => item.名称 === active_name\n );\n if (!profile) {\n return false;\n }\n\n const normalized_profile = normalizeExtraModelApiProfile(profile);\n const backend = getExtraModelApiProfileBackend(config);\n if (normalized_profile.backend !== backend || normalized_profile.密钥 !== config.密钥) {\n return true;\n }\n\n if (\n API_REQUEST_FIELDS.some(\n field =>\n normalized_profile[field] !== undefined &&\n normalized_profile[field] !== config[field]\n )\n ) {\n return true;\n }\n\n if (\n backend === 'custom' &&\n (normalized_profile.api地址 !== config.api地址 ||\n normalized_profile.模型名称 !== config.模型名称)\n ) {\n return true;\n }\n\n return (\n backend === 'pi' && !_.isEqual(normalized_profile.pi, clonePiConnectionFields(config.pi))\n );\n}\n\n/** 解除方案绑定并清空活动连接;使用 Pi 时保留隐藏的自定义连接字段。 */\nexport function clearUnboundExtraModelApiProfileFields(\n config: ExtraModelApiProfileFields\n): ExtraModelApiProfileFields {\n const preserve_hidden_custom_fields = config.模型来源 === '更多';\n return {\n ...config,\n 当前api方案: '',\n api地址: preserve_hidden_custom_fields ? config.api地址 : '',\n 密钥: '',\n 模型名称: preserve_hidden_custom_fields ? config.模型名称 : '',\n ...(config.模型来源 === '更多' && config.pi !== undefined\n ? { pi: clearPiConnectionFields(config.pi) }\n : {}),\n };\n}\n\n/** 校正活动方案引用,使列表、当前名称和连接状态保持一致。 */\nexport function reconcileExtraModelApiProfileSelection(\n config: T\n): T {\n const normalized_config = {\n ...config,\n api方案列表: normalizeExtraModelApiProfileList(config.api方案列表),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n const active_name = normalized_config.当前api方案;\n if (!active_name) {\n return normalized_config;\n }\n\n if (hasExtraModelApiProfile(normalized_config.api方案列表, active_name)) {\n return normalized_config;\n }\n\n if (normalized_config.api方案列表.length === 0) {\n return {\n ...normalized_config,\n 当前api方案: '',\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...normalized_config,\n api方案列表: normalized_config.api方案列表,\n },\n normalized_config.api方案列表[0]\n ) as T;\n}\n\n/** 删除指定的活动方案,并同步清理方案选择及相关连接字段。 */\nexport function deleteActiveExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n const remaining = removeExtraModelApiProfile(config.api方案列表, profile_name);\n if (remaining.length === 0) {\n return {\n ...clearUnboundExtraModelApiProfileFields(config),\n api方案列表: [],\n };\n }\n\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: remaining,\n },\n remaining[0]\n );\n}\n\nexport type ExtraModelApiProfileDeletionConfirmation = 'discard_unsaved_changes' | 'delete_profile';\n\n/**\n * 有未保存修改时先确认丢弃,再确认删除,任一步取消都返回 null。\n * 删除前再次核对活动方案,避免等待弹窗期间误删用户新选中的方案。\n */\nexport async function deleteActiveExtraModelApiProfileWithConfirmation(\n config: ExtraModelApiProfileFields,\n profile_name: string,\n confirm: (confirmation: ExtraModelApiProfileDeletionConfirmation) => Promise\n): Promise {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n if (\n normalized_name === '' ||\n normalized_name !== normalizeExtraModelApiProfileName(config.当前api方案) ||\n !hasExtraModelApiProfile(config.api方案列表, normalized_name)\n ) {\n return config;\n }\n\n if (isActiveExtraModelApiProfileDirty(config) && !(await confirm('discard_unsaved_changes'))) {\n return null;\n }\n\n if (!(await confirm('delete_profile'))) {\n return null;\n }\n\n return deleteActiveExtraModelApiProfile(config, profile_name);\n}\n\n/**\n * 补全方案中缺失的请求选项,并校正方案选择与密钥归属。\n * 不完整的 Pi 快照保持不可发送,不能从遗留自定义字段推导出 Pi 连接。\n */\nexport function migrateExtraModelApiProfiles(config: T): T {\n let migrated = {\n ...config,\n ...(config.pi === undefined\n ? {}\n : {\n pi: {\n ...config.pi,\n credentialIds: resolvePiCredentialReferences(\n config.pi,\n config.pi.credentials\n ),\n },\n }),\n // Old profiles only stored connection fields. Copy the current request options into\n // every profile once; saved per-profile values must survive subsequent loads/imports.\n api方案列表: normalizeExtraModelApiProfileList(\n config.api方案列表.map(profile => ({\n ...profile,\n ...(profile.backend === 'pi' && profile.pi !== undefined\n ? {\n pi: {\n ...profile.pi,\n credentialIds: resolvePiCredentialReferences(\n profile.pi,\n config.pi?.credentials\n ),\n },\n }\n : {}),\n ...extractApiRequestFields(config),\n ...extractApiRequestFields(profile),\n }))\n ),\n 当前api方案: normalizeExtraModelApiProfileName(config.当前api方案),\n } as T;\n\n // The shared root key has wire meaning only for a registry-backed, normalizable API-key target.\n // Clear stale imported values even when there is no active profile (and before legacy-profile\n // detection), while retaining the source-specific Custom/API-key caches and OAuth store.\n if (\n migrated.模型来源 === '更多' &&\n resolvePiConnectionApiKeyScope(migrated.pi) === '' &&\n migrated.密钥 !== ''\n ) {\n migrated = {\n ...migrated,\n 密钥: '',\n } as T;\n }\n\n const active_profile = migrated.api方案列表.find(\n profile => profile.名称 === migrated.当前api方案\n );\n if (active_profile?.backend === 'pi' && !hasConfiguredPiConnectionSnapshot(active_profile.pi)) {\n migrated = applyExtraModelApiProfile(migrated, active_profile) as T;\n }\n\n if (migrated.api方案列表.length === 0) {\n const has_legacy_custom_api =\n migrated.api地址.trim().length > 0 ||\n migrated.密钥.trim().length > 0 ||\n migrated.模型名称.trim().length > 0;\n if (has_legacy_custom_api) {\n // A newly selected but not yet configured Pi source may still carry legacy root\n // Custom fields. Do not turn those unrelated fields into a sendable Pi profile.\n if (\n getExtraModelApiProfileBackend(migrated) === 'pi' &&\n !hasConfiguredPiConnectionSnapshot(migrated.pi)\n ) {\n return reconcileExtraModelApiProfileSelection(migrated);\n }\n const profile = extractExtraModelApiProfileFields(migrated);\n migrated = {\n ...migrated,\n api方案列表: [\n {\n ...profile,\n 名称: DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n },\n ],\n 当前api方案: migrated.当前api方案 || DEFAULT_EXTRA_MODEL_API_PROFILE_NAME,\n };\n }\n }\n\n return reconcileExtraModelApiProfileSelection(migrated);\n}\n\n/** 按名称查找并应用方案;不存在时抛出可展示的配置错误。 */\nexport function selectExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const normalized_name = normalizeExtraModelApiProfileName(profile_name);\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const profile = normalized_profiles.find(item => item.名称 === normalized_name);\n if (!profile) {\n throw new Error(\n tr('runtime.apiProfile.notFound', {\n name: normalized_name,\n })\n );\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: normalized_profiles,\n },\n profile\n );\n}\n\n/** 保存当前连接和请求选项,支持更新活动方案或指定名称,并防止覆盖其他同名方案。 */\nexport function saveCurrentExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name?: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name ?? config.当前api方案);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.selectOrEnterName'));\n }\n\n if (\n hasExtraModelApiProfile(config.api方案列表, target_name) &&\n normalizeExtraModelApiProfileName(config.当前api方案) !== target_name\n ) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n\n const normalized_profiles = normalizeExtraModelApiProfileList(config.api方案列表);\n const source_profile = normalized_profiles.find(\n item => item.名称 === normalizeExtraModelApiProfileName(config.当前api方案)\n );\n const normalized_source_profile =\n source_profile === undefined ? undefined : normalizeExtraModelApiProfile(source_profile);\n const profile: ExtraModelApiProfile = {\n ...normalized_source_profile,\n ...extractExtraModelApiProfileFields(config),\n 名称: target_name,\n };\n if (profile.backend !== 'pi') {\n delete profile.pi;\n }\n return applyExtraModelApiProfile(\n {\n ...config,\n api方案列表: upsertExtraModelApiProfile(normalized_profiles, profile),\n },\n profile\n );\n}\n\n/** 校验新名称不为空且未占用,再将当前配置另存为独立方案。 */\nexport function saveAsNewExtraModelApiProfile(\n config: ExtraModelApiProfileFields,\n profile_name: string\n): ExtraModelApiProfileFields {\n const target_name = normalizeExtraModelApiProfileName(profile_name);\n if (!target_name) {\n throw new Error(tr('runtime.apiProfile.enterNewName'));\n }\n if (hasExtraModelApiProfile(config.api方案列表, target_name)) {\n throw new Error(\n tr('runtime.apiProfile.alreadyExists', {\n name: target_name,\n })\n );\n }\n return saveCurrentExtraModelApiProfile(config, target_name);\n}\n","/** 兼容旧浏览器的 AbortSignal 静态接口;可选方法由运行时检测后补齐。 */\ntype AbortSignalConstructorWithPolyfills = {\n any?: (signals: Iterable) => AbortSignal;\n timeout?: (milliseconds: number) => AbortSignal;\n};\n\n/**\n * 为旧版酒馆浏览器补齐 Pi OAuth 刷新所需的 AbortSignal.timeout 和 any。\n * 仅在原生实现缺失时安装,并在取消后解除组合信号的监听。\n */\nexport function installPiAbortSignalPolyfills(): void {\n const constructor = AbortSignal as unknown as AbortSignalConstructorWithPolyfills;\n if (typeof constructor.timeout !== 'function') {\n Object.defineProperty(constructor, 'timeout', {\n configurable: true,\n writable: true,\n /** 创建到期自动取消的信号,拒绝负数和非有限超时时间。 */\n value(milliseconds: number): AbortSignal {\n if (!Number.isFinite(milliseconds) || milliseconds < 0) {\n throw new RangeError(\n 'AbortSignal timeout must be a non-negative finite number'\n );\n }\n const controller = new AbortController();\n setTimeout(\n () =>\n controller.abort(\n new DOMException('The operation timed out', 'TimeoutError')\n ),\n milliseconds\n );\n return controller.signal;\n },\n });\n }\n\n if (typeof constructor.any === 'function') {\n return;\n }\n\n Object.defineProperty(constructor, 'any', {\n configurable: true,\n writable: true,\n /** 合并多个取消信号,首次取消时保留原始原因并清理其他监听。 */\n value(signals: Iterable): AbortSignal {\n const controller = new AbortController();\n const listeners = new Map void>();\n /** 解除已注册的取消监听,避免组合信号结束后继续持有输入信号。 */\n const cleanup = () => {\n for (const [signal, listener] of listeners) {\n signal.removeEventListener('abort', listener);\n }\n listeners.clear();\n };\n /** 只转发首次取消原因,并统一释放组合信号的监听。 */\n const forward = (signal: AbortSignal) => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n cleanup();\n };\n\n for (const signal of signals) {\n if (listeners.has(signal)) {\n continue;\n }\n if (signal.aborted) {\n forward(signal);\n break;\n }\n /** 将当前输入信号的取消传递给组合信号。 */\n const listener = () => forward(signal);\n listeners.set(signal, listener);\n signal.addEventListener('abort', listener, { once: true });\n }\n return controller.signal;\n },\n });\n}\n","import type { ProviderHeaders } from '@earendil-works/pi-ai';\nimport YAML from 'yaml';\n\n/** 解析用户输入的 YAML 或 JSON;空输入表示未配置,语法错误只返回字段级提示。 */\nfunction parseYaml(value: string, label: string): unknown {\n if (!value.trim()) {\n return undefined;\n }\n try {\n return YAML.parse(value);\n } catch {\n throw new Error(`More source ${label} is not valid YAML or JSON`);\n }\n}\n\n/** 判断解析结果是否为非空、非数组对象,供请求配置的结构校验使用。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 解析自定义请求头,允许字符串或 null,并禁止覆盖由认证层管理的密钥头。 */\nexport function parsePiCustomHeaders(value: string): ProviderHeaders | undefined {\n const parsed = parseYaml(value, 'customHeaders');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customHeaders must be an object');\n }\n const headers: ProviderHeaders = {};\n for (const [name, header_value] of Object.entries(parsed)) {\n const normalized_name = name.trim();\n if (!normalized_name || (typeof header_value !== 'string' && header_value !== null)) {\n throw new Error('More source customHeaders values must be strings or null');\n }\n if (\n /^(authorization|proxy-authorization|x-api-key|x-goog-api-key)$/i.test(normalized_name)\n ) {\n throw new Error(\n `More source customHeaders cannot override authentication header '${normalized_name}'`\n );\n }\n headers[normalized_name] = header_value;\n }\n return headers;\n}\n\n/** 解析要合并到请求体的对象;空输入不产生覆盖。 */\nexport function parsePiCustomIncludeBody(value: string): Record | undefined {\n const parsed = parseYaml(value, 'customIncludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n if (!isPlainObject(parsed)) {\n throw new Error('More source customIncludeBody must be an object');\n }\n return parsed;\n}\n\n/** 将数组或逗号、换行分隔的字段名规范为去重列表,拒绝空名称和非字符串。 */\nexport function parsePiCustomExcludeBody(value: string): string[] | undefined {\n const parsed = parseYaml(value, 'customExcludeBody');\n if (parsed === undefined) {\n return undefined;\n }\n const fields =\n typeof parsed === 'string'\n ? parsed\n .split(/[\\n,]/)\n .map(item => item.trim())\n .filter(Boolean)\n : parsed;\n if (\n !Array.isArray(fields) ||\n fields.some(field => typeof field !== 'string' || !field.trim())\n ) {\n throw new Error('More source customExcludeBody must be an array of field names');\n }\n return [...new Set(fields.map(field => field.trim()))];\n}\n","import { getPendingPromptCaptureDiagnostics } from './prompt_capture';\n\nconst pi_request_controllers = new Map();\nconst pi_request_attempts = new Map();\n\nexport class PiRequestAbortedError extends Error {\n readonly generationId: string;\n\n /** 将取消原因与生成编号绑定,供重试策略和界面识别主动停止。 */\n constructor(generationId: string, reason?: unknown) {\n const detail =\n reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';\n super(\n detail\n ? `More source request '${generationId}' was aborted: ${detail}`\n : `More source request '${generationId}' was aborted`\n );\n this.name = 'PiRequestAbortedError';\n this.generationId = generationId;\n }\n}\n\n/** 识别 Pi 的主动取消错误,避免将其当作普通请求失败重试。 */\nexport function isPiRequestAbortedError(error: unknown): error is PiRequestAbortedError {\n return error instanceof PiRequestAbortedError;\n}\n\n/** 实际服务商请求的取消句柄,连接本次请求、用户停止和完整尝试的取消信号。 */\nexport type PiRequestControllerRegistration = {\n controller: AbortController;\n signal: AbortSignal;\n /** 移除本次登记和关联监听;释放本身不会取消请求。 */\n release: () => void;\n};\n\n/** 跨越提示词捕获与服务商请求的尝试句柄,用于记住两阶段之间发生的取消。 */\nexport type PiRequestAttemptRegistration = {\n signal: AbortSignal;\n /** 完整尝试结束后移除登记;调用多次不会影响同编号的新尝试。 */\n release: () => void;\n};\n\n/**\n * 为“提示词捕获 → 服务商请求”的完整尝试保留生成编号和取消信号。\n * 该记录在捕获结束后仍保留停止标记,防止间隙中的取消被后续请求注册绕过。\n */\nexport function beginPiRequestAttempt(generation_id: string): PiRequestAttemptRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_attempts.has(generation_id) || pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n pi_request_attempts.set(generation_id, controller);\n let released = false;\n /** 只释放本次尝试登记的记录,避免清理同编号的新实例。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n if (pi_request_attempts.get(generation_id) === controller) {\n pi_request_attempts.delete(generation_id);\n }\n };\n\n return { signal: controller.signal, release };\n}\n\n/** 登记实际请求控制器,并继承完整尝试已发生或随后发生的取消。 */\nexport function registerPiRequestController(\n generation_id: string,\n caller_signal?: AbortSignal\n): PiRequestControllerRegistration {\n if (!generation_id.trim()) {\n throw new Error('More source request generation_id must not be empty');\n }\n if (pi_request_controllers.has(generation_id)) {\n throw new Error(`More source request generation_id '${generation_id}' is already active`);\n }\n\n const controller = new AbortController();\n const attempt_signal = pi_request_attempts.get(generation_id)?.signal;\n const abort_sources = [\n ...new Set(\n [attempt_signal, caller_signal].filter(\n (signal): signal is AbortSignal => signal !== undefined\n )\n ),\n ];\n const abort_listeners = abort_sources.map(signal => {\n /** 把调用方或尝试层信号的取消原因传给实际请求控制器。 */\n const forward_abort = () => {\n if (!controller.signal.aborted) {\n controller.abort(signal.reason);\n }\n };\n signal.addEventListener('abort', forward_abort, { once: true });\n if (signal.aborted) {\n forward_abort();\n }\n return { signal, forward_abort };\n });\n\n pi_request_controllers.set(generation_id, controller);\n let released = false;\n /** 解除上游取消监听,并只移除当前请求的登记项。 */\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n for (const { signal, forward_abort } of abort_listeners) {\n signal.removeEventListener('abort', forward_abort);\n }\n if (pi_request_controllers.get(generation_id) === controller) {\n pi_request_controllers.delete(generation_id);\n }\n };\n\n return { controller, signal: controller.signal, release };\n}\n\n/** 在实际请求执行期间登记控制器,无论成功、失败还是取消都释放登记。 */\nexport async function withPiRequestController(\n generation_id: string,\n run: (signal: AbortSignal) => Promise,\n caller_signal?: AbortSignal\n): Promise {\n const registration = registerPiRequestController(generation_id, caller_signal);\n try {\n return await run(registration.signal);\n } finally {\n registration.release();\n }\n}\n\n/** 按生成编号取消 Pi 尝试和实际请求,保留可供后续注册检查的停止状态。 */\nexport function stopPiRequestById(generation_id: string, reason?: unknown): boolean {\n const controllers = [\n pi_request_attempts.get(generation_id),\n pi_request_controllers.get(generation_id),\n ];\n let stopped = false;\n for (const controller of controllers) {\n if (!controller || controller.signal.aborted) {\n continue;\n }\n controller.abort(reason ?? new PiRequestAbortedError(generation_id));\n stopped = true;\n }\n return stopped;\n}\n\n/** 同时停止同一编号的 Slash 捕获和 Pi 请求,覆盖生成链路的各个阶段。 */\nexport function stopExtraModelRequestById(generation_id: string, reason?: unknown): boolean {\n const slash_stopped = stopGenerationById(generation_id);\n const pi_stopped = stopPiRequestById(generation_id, reason);\n return slash_stopped || pi_stopped;\n}\n\n/** 取消所有仍活动的 Pi 尝试与请求,并返回受影响的生成编号数量。 */\nexport function stopAllPiRequests(reason?: unknown): number {\n const ids = [\n ...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()]),\n ].filter(id => {\n const attempt = pi_request_attempts.get(id);\n const runtime = pi_request_controllers.get(id);\n return (\n (attempt !== undefined && !attempt.signal.aborted) ||\n (runtime !== undefined && !runtime.signal.aborted)\n );\n });\n for (const id of ids) {\n stopPiRequestById(id, reason);\n }\n return ids.length;\n}\n\n/** 停止全部额外模型尝试,包括仍处于 Slash 提示词捕获阶段的请求。 */\nexport function stopAllExtraModelRequests(reason?: unknown): number {\n const ids = new Set([\n ...getPendingPromptCaptureDiagnostics().map(capture => capture.generationId),\n ...pi_request_attempts.keys(),\n ...pi_request_controllers.keys(),\n ]);\n let stopped = 0;\n for (const id of ids) {\n if (stopExtraModelRequestById(id, reason)) {\n stopped += 1;\n }\n }\n return stopped;\n}\n\n/** 返回当前登记的生成编号,供取消操作及生命周期诊断使用。 */\nexport function getActivePiRequestIds(): readonly string[] {\n return [...new Set([...pi_request_attempts.keys(), ...pi_request_controllers.keys()])];\n}\n\n/**\n * 脚本卸载时取消全部请求,但保留登记项直到各自结束。\n * 尝试记录仍充当停止标记,防止并发完成的捕获在卸载后发起新请求。\n */\nexport function clearPiRequestControllers(): void {\n stopAllExtraModelRequests(new Error('More source request registry disposed'));\n}\n","/** 连接与方案仅保存服务商到集中凭证记录的引用,不复制会轮换的 refresh token。 */\nexport type PiCredentialReferences = Record;\n\n/** 接受旧版服务商键和带服务商命名空间的新凭证编号,拒绝跨服务商引用。 */\nexport function isPiCredentialId(providerId: string, credentialId: string): boolean {\n return (\n credentialId === providerId ||\n credentialId.startsWith(`oauth:${encodeURIComponent(providerId)}:`)\n );\n}\n\n/** 规范显式引用;缺少字段的旧设置才从原 providerId 记录一次性继承登录态。 */\nexport function resolvePiCredentialReferences(\n pi: { credentialIds?: unknown },\n credentials: Record = {}\n): PiCredentialReferences {\n if (pi.credentialIds !== undefined) {\n if (\n typeof pi.credentialIds !== 'object' ||\n pi.credentialIds === null ||\n Array.isArray(pi.credentialIds)\n ) {\n return {};\n }\n return Object.fromEntries(\n Object.entries(pi.credentialIds).filter(\n ([providerId, id]) => typeof id === 'string' && isPiCredentialId(providerId, id)\n )\n );\n }\n return Object.fromEntries(\n Object.entries(credentials)\n .filter(\n ([id, value]) =>\n !id.startsWith('oauth:') &&\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'oauth'\n )\n .map(([providerId]) => [providerId, providerId])\n );\n}\n","import { useDataStore } from '@/store';\nimport { resolvePiCredentialReferences, type PiCredentialReferences } from './credential_refs';\nimport type {\n AuthOperationOptions,\n Credential,\n CredentialInfo,\n CredentialStore,\n OAuthCredential,\n} from './pi_gateway';\n\n/** 按凭证编号排队执行的凭证操作,进入队列前不提前读取或修改凭证。 */\ntype CredentialTask = () => Promise;\n\n/** 复制 OAuth 凭证,避免调用方修改持久化对象本身。 */\nfunction cloneOAuthCredential(credential: OAuthCredential): OAuthCredential {\n return { ...credential };\n}\n\n/**\n * 从宽松保存的设置中读取符合 Pi 契约的 OAuth 凭证。\n * 未知字段可以保留,但缺失或无效的 access、refresh 和 expires 不进入运行时。\n */\nfunction readOAuthCredential(value: unknown): OAuthCredential | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return undefined;\n }\n\n const candidate = value as Record;\n if (\n candidate.type !== 'oauth' ||\n typeof candidate.access !== 'string' ||\n candidate.access.trim().length === 0 ||\n typeof candidate.refresh !== 'string' ||\n candidate.refresh.trim().length === 0 ||\n typeof candidate.expires !== 'number' ||\n !Number.isFinite(candidate.expires)\n ) {\n return undefined;\n }\n\n return cloneOAuthCredential(candidate as OAuthCredential);\n}\n\n/** 所有捕获同一设置仓库的适配器共用真实凭证编号的队列。 */\nconst storeChains = new WeakMap>>();\n/** 登录和解绑的代次按所属连接记录,阻止已经退出的待完成登录重新绑定。 */\nconst bindingGenerations = new WeakMap>();\n\ntype DataStore = ReturnType;\ntype CredentialStoreHooks = {\n beforeWrite?: (providerId: string, credentialId: string) => void;\n /** 返回 false 时仅解除绑定,保留其他方案仍使用的集中凭证。 */\n beforeDelete?: (providerId: string, credentialId: string | undefined) => boolean;\n};\n\n/** 在读取或写入凭证前检查调用方是否已取消操作。 */\nfunction throwIfAborted(options?: AuthOperationOptions): void {\n options?.signal?.throwIfAborted();\n}\n\n/** 取消时立即结束调用方等待,同时允许内部串行任务安全收尾。 */\nfunction raceWithAbort(task: Promise, signal?: AbortSignal): Promise {\n if (signal === undefined) {\n return task;\n }\n\n if (signal.aborted) {\n return Promise.reject(signal.reason);\n }\n return new Promise((resolve, reject) => {\n /** 解除取消监听并使用原始取消原因拒绝等待中的调用。 */\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort);\n reject(signal.reason);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n\n task.then(\n value => {\n signal.removeEventListener('abort', onAbort);\n resolve(value);\n },\n error => {\n signal.removeEventListener('abort', onAbort);\n reject(error);\n }\n );\n });\n}\n\n/**\n * 创建基于设置持久化的 OAuth 凭证仓库。\n * 同一凭证编号的修改和删除串行执行,写入前复查取消状态,避免刷新覆盖登出或新凭证。\n */\nexport function createPiCredentialStore(\n credentialIds?: PiCredentialReferences,\n dataStore: DataStore = useDataStore(),\n hooks: CredentialStoreHooks = {}\n): CredentialStore {\n const references = credentialIds === undefined ? undefined : { ...credentialIds };\n const getPersistedCredentials = () => dataStore.settings.额外模型解析配置.pi.credentials;\n const resolveId = (providerId: string) =>\n references === undefined ? providerId : references[providerId];\n const chains = storeChains.get(dataStore) ?? new Map>();\n storeChains.set(dataStore, chains);\n\n /** 按凭证编号串行安排凭证修改;前序失败不阻塞后续任务,取消后也保持队列顺序。 */\n const enqueue = (\n providerId: string,\n task: CredentialTask,\n options?: AuthOperationOptions\n ): Promise => {\n const previous = chains.get(providerId) ?? Promise.resolve();\n const queued = (async () => {\n throwIfAborted(options);\n await previous.catch(() => undefined);\n throwIfAborted(options);\n return task();\n })();\n const tail = queued.then(\n () => undefined,\n () => undefined\n );\n chains.set(providerId, tail);\n void tail.then(() => {\n if (chains.get(providerId) === tail) {\n chains.delete(providerId);\n }\n });\n\n return raceWithAbort(queued, options?.signal);\n };\n\n return {\n /** 读取并返回有效凭证的副本,忽略仓库中无法识别的记录。 */\n async read(\n providerId: string,\n options?: AuthOperationOptions\n ): Promise {\n throwIfAborted(options);\n const credentialId = resolveId(providerId);\n const credential =\n credentialId === undefined\n ? undefined\n : readOAuthCredential(getPersistedCredentials()[credentialId]);\n return credential === undefined ? undefined : cloneOAuthCredential(credential);\n },\n\n /** 列出有效 OAuth 凭证的服务商和类型,不向列表调用方暴露令牌内容。 */\n async list(options?: AuthOperationOptions): Promise {\n throwIfAborted(options);\n const ids =\n references ??\n Object.fromEntries(Object.keys(getPersistedCredentials()).map(id => [id, id]));\n return Object.entries(ids).flatMap(([providerId, credentialId]) =>\n readOAuthCredential(getPersistedCredentials()[credentialId]) === undefined\n ? []\n : [{ providerId, type: 'oauth' as const }]\n );\n },\n\n /** 在凭证队列内读取、更新并保存凭证,提交前再次检查取消信号。 */\n modify(\n providerId: string,\n fn: (current: Credential | undefined) => Promise,\n options?: AuthOperationOptions\n ): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.reject(new Error('No OAuth credential is bound to this connection'));\n }\n return enqueue(\n credentialId,\n async () => {\n const current = readOAuthCredential(getPersistedCredentials()[credentialId]);\n const next = await fn(\n current === undefined ? undefined : cloneOAuthCredential(current)\n );\n throwIfAborted(options);\n\n if (next === undefined) {\n return current === undefined ? undefined : cloneOAuthCredential(current);\n }\n\n const oauth = readOAuthCredential(next);\n if (oauth === undefined) {\n throw new TypeError(\n 'More source credential store only persists valid OAuth credentials'\n );\n }\n\n const persisted = cloneOAuthCredential(oauth);\n hooks.beforeWrite?.(providerId, credentialId);\n getPersistedCredentials()[credentialId] = persisted;\n return cloneOAuthCredential(persisted);\n },\n options\n );\n },\n\n /** 在同一凭证队列中删除凭证,与自动或手动刷新保持确定的先后顺序。 */\n delete(providerId: string, options?: AuthOperationOptions): Promise {\n const credentialId = resolveId(providerId);\n if (credentialId === undefined) {\n return Promise.resolve().then(() => {\n throwIfAborted(options);\n hooks.beforeDelete?.(providerId, undefined);\n });\n }\n return enqueue(\n credentialId,\n async () => {\n throwIfAborted(options);\n if (hooks.beforeDelete?.(providerId, credentialId) !== false) {\n delete getPersistedCredentials()[credentialId];\n }\n },\n options\n );\n },\n };\n}\n\n/** 捕获操作发起时所属的方案及活动连接,回调提交前复核原引用。 */\nfunction captureBinding(dataStore: DataStore) {\n const config = dataStore.settings.额外模型解析配置;\n const profileName = config.当前api方案;\n const profile = config.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const activeReferences = resolvePiCredentialReferences(config.pi, config.pi.credentials);\n const profileReferences =\n profile?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(profile.pi, config.pi.credentials);\n const source = config.模型来源;\n const provider = config.pi.provider;\n const authType = config.pi.authType;\n const generations = bindingGenerations.get(dataStore) ?? new Map();\n bindingGenerations.set(dataStore, generations);\n const scope = JSON.stringify([profileName, source, provider, authType]);\n const observedGenerations = new Map(generations);\n\n /** 仅修改发起操作时的所属方案;活动连接只有仍绑定原值时才同步更新。 */\n const replace = (providerId: string, nextId: string | undefined) => {\n const generationKey = `${scope}:${providerId}`;\n const generation = observedGenerations.get(generationKey) ?? 0;\n if ((generations.get(generationKey) ?? 0) !== generation) {\n throw new Error('The OAuth connection was changed by another authentication operation');\n }\n const current = dataStore.settings.额外模型解析配置;\n const target =\n profileReferences === undefined\n ? undefined\n : current.api方案列表.find(\n item => item.名称 === profileName && item.backend === 'pi'\n );\n const isActive =\n current.当前api方案 === profileName &&\n current.模型来源 === source &&\n current.pi.provider === provider &&\n current.pi.authType === authType;\n const currentRefs = resolvePiCredentialReferences(current.pi, current.pi.credentials);\n const targetRefs =\n target?.pi === undefined\n ? undefined\n : resolvePiCredentialReferences(target.pi, current.pi.credentials);\n if (isActive && currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The active OAuth credential changed while authentication was pending');\n }\n if (profileReferences !== undefined) {\n if (!target?.pi || targetRefs?.[providerId] !== profileReferences[providerId]) {\n throw new Error('The OAuth profile changed while authentication was pending');\n }\n } else if (!isActive || currentRefs[providerId] !== activeReferences[providerId]) {\n throw new Error('The OAuth connection changed while authentication was pending');\n }\n const updated = (refs: PiCredentialReferences) => {\n const result = { ...refs };\n if (nextId === undefined) delete result[providerId];\n else result[providerId] = nextId;\n return result;\n };\n generations.set(generationKey, generation + 1);\n if (target?.pi && targetRefs) target.pi.credentialIds = updated(targetRefs);\n if (isActive && currentRefs[providerId] === activeReferences[providerId]) {\n current.pi.credentialIds = updated(currentRefs);\n }\n };\n return { references: activeReferences, replace };\n}\n\n/** 是否仍有方案或活动连接引用该凭证;未迁移的旧方案也参与引用检查。 */\nfunction isCredentialReferenced(dataStore: DataStore, credentialId: string): boolean {\n const config = dataStore.settings.额外模型解析配置;\n const connections = [\n config.pi,\n ...config.api方案列表.flatMap(profile =>\n profile.backend === 'pi' && profile.pi ? [profile.pi] : []\n ),\n ];\n return connections.some(pi =>\n Object.values(resolvePiCredentialReferences(pi, config.pi.credentials)).includes(\n credentialId\n )\n );\n}\n\n/** 捕获当前连接的凭证引用;随后切换方案不会改变此对象读写的账号。 */\nexport function getPiCredentialStore(pi?: { credentialIds?: unknown }): CredentialStore {\n const dataStore = useDataStore();\n const settings = dataStore.settings.额外模型解析配置.pi;\n const references = resolvePiCredentialReferences(pi ?? settings, settings.credentials);\n return createPiCredentialStore(references, dataStore);\n}\n\n/** 捕获退出登录的所属方案,仅解绑该方案,最后一个引用消失后才删除集中凭证。 */\nexport function createPiOAuthLogoutStore(): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n return createPiCredentialStore(binding.references, dataStore, {\n beforeDelete(providerId, credentialId) {\n binding.replace(providerId, undefined);\n return credentialId !== undefined && !isCredentialReferenced(dataStore, credentialId);\n },\n });\n}\n\n/** 为登录结果分配全新记录,成功提交时才绑定原方案,避免覆盖其他账号或失败时丢失旧登录态。 */\nexport function createPiOAuthLoginStore(providerId: string, nonce: string): CredentialStore {\n const dataStore = useDataStore();\n const binding = captureBinding(dataStore);\n const id = `oauth:${encodeURIComponent(providerId)}:${nonce}`;\n return createPiCredentialStore({ [providerId]: id }, dataStore, {\n beforeWrite(writtenProvider, credentialId) {\n binding.replace(writtenProvider, credentialId);\n },\n });\n}\n","import {\n GoogleGenAI,\n type GenerateContentConfig,\n type GenerateContentParameters,\n type ThinkingConfig,\n} from '@google/genai';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n buildBaseOptions,\n calculateCost,\n clampThinkingLevel,\n collapseSystemMessages,\n convertGoogleMessages,\n convertGoogleTools,\n createAssistantMessageEventStream,\n getCurrentSystemPrompt,\n getCurrentTools,\n isGoogleThinkingPart,\n mapGoogleStopReason,\n resolveGoogleFunctionCallingMode,\n resolveGoogleThinkingLevel,\n retainGoogleThoughtSignature,\n retryGoogleRequest,\n supportsGoogleStrictToolSampling,\n type Api,\n type AssistantMessage,\n type AssistantMessageEventStream,\n type FetchFunction,\n type GoogleApiThinkingLevel,\n type Model,\n type ProviderHeaders,\n type ProviderStreams,\n type ResolvedGoogleThinkingLevel,\n type SimpleStreamOptions,\n type StreamOptions,\n type TextContent,\n type ThinkingBudgets,\n type ThinkingContent,\n type ToolCall,\n type TranscriptContext,\n} from './pi_gateway';\n\n/**\n * 当前锁定的 Google SDK 未公开实例级 fetch 选项,但内部 apiClient 保留了 HTTP 入口。\n * 只声明经过核对的最小结构;SDK 升级后若不再匹配,发送带凭证请求前即停止。\n * 该入口只替换当前实例的传输,不修改全局 fetch 或 SDK 原型。\n */\ntype InjectableGoogleClient = {\n apiClient?: {\n apiCall?: (\n url: string,\n init: RequestInit,\n retryOptions?: unknown,\n timeout?: number,\n abortSignal?: AbortSignal\n ) => Promise;\n };\n};\n\nconst GOOGLE_TRANSPORT_PROBE_KEY = 'mvu-google-transport-compatibility-check';\n\nexport class GoogleProxyAdapterCompatibilityError extends Error {\n readonly code = 'google_proxy_adapter_incompatible';\n readonly retryable = false;\n\n /** 将 SDK 传输注入点不兼容标记为不可重试错误,避免被包装成普通服务商失败。 */\n constructor() {\n super(\n 'The installed Google Generative AI SDK no longer exposes the audited per-client HTTP transport required by the SillyTavern proxy.'\n );\n this.name = 'GoogleProxyAdapterCompatibilityError';\n }\n}\n\n/** Google 自定义传输路径的请求选项,补齐通用流选项之外的工具选择和思考控制。 */\nexport interface GoogleProxyOptions extends StreamOptions {\n toolChoice?: 'auto' | 'none' | 'any';\n thinking?: {\n enabled: boolean;\n /** -1 由 Google 动态分配预算;0 关闭基于预算的思考。 */\n budgetTokens?: number;\n level?: GoogleApiThinkingLevel;\n };\n}\n\n/**\n * 仅替换单个 Google 客户端的 HTTP 调用实现,保留取消和超时信号。\n * 先验证当前 SDK 注入点;不修改全局 fetch 或客户端原型。\n */\nexport function installGoogleClientFetch(client: unknown, fetch_impl: FetchFunction): void {\n const api_client = (client as InjectableGoogleClient | null)?.apiClient;\n if (\n !api_client ||\n typeof api_client.apiCall !== 'function' ||\n api_client.apiCall.length !== 5\n ) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n\n installPiAbortSignalPolyfills();\n // GenAI 2.x moved abort/timeout out of RequestInit into apiCall arguments. Pi owns retries,\n // but the injected transport must preserve those signals through response-body consumption.\n /** 合并 RequestInit、SDK 参数和超时产生的信号,再交给本次请求的传输实现。 */\n const injected = (\n url: string,\n init: RequestInit,\n _retry_options?: unknown,\n timeout?: number,\n abort_signal?: AbortSignal\n ) => {\n const signals = [init.signal, abort_signal].filter(\n (signal): signal is AbortSignal => signal != null\n );\n if (timeout !== undefined && timeout > 0) {\n signals.push(AbortSignal.timeout(timeout));\n }\n if (signals.length === 0) {\n return fetch_impl(url, init);\n }\n const signal = AbortSignal.any(signals);\n signal.throwIfAborted();\n return fetch_impl(url, { ...init, signal });\n };\n try {\n api_client.apiCall = injected;\n } catch {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n if (api_client.apiCall !== injected) {\n throw new GoogleProxyAdapterCompatibilityError();\n }\n}\n\n/**\n * 在 Pi 懒加载流边界之前验证 SDK 注入点,保留明确的兼容错误。\n * 构造客户端与校验过程不发送网络请求。\n */\nexport function assertGoogleProxyAdapterCompatible(): void {\n const client = new GoogleGenAI({ apiKey: GOOGLE_TRANSPORT_PROBE_KEY });\n installGoogleClientFetch(client, async () => {\n throw new GoogleProxyAdapterCompatibilityError();\n });\n}\n\n/** 仅在请求指定了不同于全局 fetch 的传输实现时启用本地桥接。 */\nfunction shouldUseInjectedFetch(\n fetch_impl: FetchFunction | undefined\n): fetch_impl is FetchFunction {\n return fetch_impl !== undefined && fetch_impl !== globalThis.fetch;\n}\n\n/** 移除表示删除操作的 null 请求头,转换为 Google SDK 接受的字符串映射。 */\nfunction providerHeadersToRecord(\n headers: ProviderHeaders | undefined\n): Record | undefined {\n if (!headers) {\n return undefined;\n }\n const result: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== null) {\n result[key] = value;\n }\n }\n return Object.keys(result).length === 0 ? undefined : result;\n}\n\n/** 删除孤立的 UTF-16 代理码元,避免系统提示词编码失败。 */\nfunction sanitizeSurrogates(text: string): string {\n return text.replace(\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?): AssistantMessage {\n return {\n role: 'assistant',\n content: [],\n api: 'google-generative-ai' as Api,\n provider: model.provider,\n model: model.id,\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'pending',\n timestamp: Date.now(),\n };\n}\n\n/** 按本次模型端点和请求头创建 Google 客户端,并安装仅作用于该实例的 fetch。 */\nfunction createClient(\n model: Model<'google-generative-ai'>,\n api_key: string,\n options_headers: ProviderHeaders | undefined,\n fetch_impl: FetchFunction\n): GoogleGenAI {\n const http_options: {\n baseUrl?: string;\n apiVersion?: string;\n headers?: Record;\n } = {};\n if (model.baseUrl) {\n http_options.baseUrl = model.baseUrl;\n // Pi model base URLs already include their API version path.\n http_options.apiVersion = '';\n }\n const headers = providerHeadersToRecord({\n 'User-Agent': 'pi (browser)',\n ...model.headers,\n ...options_headers,\n });\n if (headers) {\n http_options.headers = headers;\n }\n\n const client = new GoogleGenAI({\n apiKey: api_key,\n httpOptions: Object.keys(http_options).length === 0 ? undefined : http_options,\n });\n installGoogleClientFetch(client, fetch_impl);\n return client;\n}\n\n/** 将 Pi 上下文、工具、采样和思考配置转换为 Google 生成参数,并保留取消信号。 */\nfunction buildParams(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions = {}\n): GenerateContentParameters {\n // Pi 1.x 将系统提示词和工具声明放进 transcript;Google 使用折叠后的系统消息。\n const normalized_context = collapseSystemMessages(context);\n const contents = convertGoogleMessages(model, normalized_context);\n const system_prompt = getCurrentSystemPrompt(normalized_context.messages);\n const tools = getCurrentTools(normalized_context.messages);\n const generation_config: GenerateContentConfig = {};\n if (options.temperature !== undefined) {\n generation_config.temperature = options.temperature;\n }\n if (options.maxTokens !== undefined) {\n generation_config.maxOutputTokens = options.maxTokens;\n }\n\n const supports_strict_mode = supportsGoogleStrictToolSampling(model.id);\n const function_calling_mode = tools.length\n ? resolveGoogleFunctionCallingMode(tools, options.toolChoice, supports_strict_mode)\n : undefined;\n const config: GenerateContentConfig = {\n ...(Object.keys(generation_config).length === 0 ? {} : generation_config),\n ...(system_prompt ? { systemInstruction: sanitizeSurrogates(system_prompt) } : {}),\n ...(tools.length ? { tools: convertGoogleTools(tools, false, supports_strict_mode) } : {}),\n ...(function_calling_mode === undefined\n ? {}\n : { toolConfig: { functionCallingConfig: { mode: function_calling_mode } } }),\n };\n\n if (options.thinking?.enabled && model.reasoning) {\n const thinking_config: ThinkingConfig = { includeThoughts: true };\n if (options.thinking.level !== undefined) {\n thinking_config.thinkingLevel = options.thinking\n .level as ThinkingConfig['thinkingLevel'];\n } else if (options.thinking.budgetTokens !== undefined) {\n thinking_config.thinkingBudget = options.thinking.budgetTokens;\n }\n config.thinkingConfig = thinking_config;\n } else if (model.reasoning && options.thinking && !options.thinking.enabled) {\n config.thinkingConfig = getDisabledThinkingConfig(model);\n }\n\n if (options.signal) {\n if (options.signal.aborted) {\n throw new Error('Request aborted');\n }\n config.abortSignal = options.signal;\n }\n\n return {\n model: model.id,\n contents,\n config,\n };\n}\n\nlet tool_call_counter = 0;\n\n/**\n * 通过注入的传输执行 Google 生成,并转换为 Pi 的文本、思考、工具和用量事件。\n * 客户端兼容性同步校验,避免初始化失败被吞为普通模型响应。\n */\nfunction streamWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: GoogleProxyOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const output = createOutput(model);\n const stream = createAssistantMessageEventStream();\n const api_key = options.apiKey;\n\n // Construct and verify the instance-local injection point synchronously. This cannot perform\n // network I/O, and preserves the compatibility error instead of converting it to provider data.\n const client = api_key ? createClient(model, api_key, options.headers, fetch_impl) : undefined;\n\n void (async () => {\n try {\n if (!api_key || !client) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n\n let params = buildParams(model, context, options);\n const next_params = await options.onPayload?.(params, model);\n if (next_params !== undefined) {\n params = next_params as GenerateContentParameters;\n }\n const google_stream = await retryGoogleRequest(\n () => client.models.generateContentStream(params),\n options\n );\n\n stream.push({ type: 'start', partial: output });\n let current_block: TextContent | ThinkingContent | null = null;\n const blocks = output.content;\n /** 取得当前输出块下标,使增量事件始终指向最新内容块。 */\n const block_index = () => blocks.length - 1;\n\n for await (const chunk of google_stream) {\n output.responseId ||= chunk.responseId;\n const candidate = chunk.candidates?.[0];\n if (candidate?.content?.parts) {\n for (const part of candidate.content.parts) {\n if (part.text !== undefined) {\n const is_thinking = isGoogleThinkingPart(part);\n if (\n !current_block ||\n (is_thinking && current_block.type !== 'thinking') ||\n (!is_thinking && current_block.type !== 'text')\n ) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n if (is_thinking) {\n current_block = {\n type: 'thinking',\n thinking: '',\n thinkingSignature: undefined,\n };\n output.content.push(current_block);\n stream.push({\n type: 'thinking_start',\n contentIndex: block_index(),\n partial: output,\n });\n } else {\n current_block = { type: 'text', text: '' };\n output.content.push(current_block);\n stream.push({\n type: 'text_start',\n contentIndex: block_index(),\n partial: output,\n });\n }\n }\n\n if (current_block.type === 'thinking') {\n current_block.thinking += part.text;\n current_block.thinkingSignature = retainGoogleThoughtSignature(\n current_block.thinkingSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'thinking_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n } else {\n current_block.text += part.text;\n current_block.textSignature = retainGoogleThoughtSignature(\n current_block.textSignature,\n part.thoughtSignature\n );\n stream.push({\n type: 'text_delta',\n contentIndex: block_index(),\n delta: part.text,\n partial: output,\n });\n }\n }\n\n if (part.functionCall) {\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n current_block = null;\n }\n\n const provided_id = part.functionCall.id;\n const needs_new_id =\n !provided_id ||\n output.content.some(\n block => block.type === 'toolCall' && block.id === provided_id\n );\n const tool_call_id = needs_new_id\n ? `${part.functionCall.name}_${Date.now()}_${++tool_call_counter}`\n : provided_id;\n const tool_call: ToolCall = {\n type: 'toolCall',\n id: tool_call_id,\n name: part.functionCall.name || '',\n arguments: (part.functionCall.args as ToolCall['arguments']) ?? {},\n ...(part.thoughtSignature\n ? { thoughtSignature: part.thoughtSignature }\n : {}),\n };\n output.content.push(tool_call);\n stream.push({\n type: 'toolcall_start',\n contentIndex: block_index(),\n partial: output,\n });\n stream.push({\n type: 'toolcall_delta',\n contentIndex: block_index(),\n delta: JSON.stringify(tool_call.arguments),\n partial: output,\n });\n stream.push({\n type: 'toolcall_end',\n contentIndex: block_index(),\n toolCall: tool_call,\n partial: output,\n });\n }\n }\n }\n\n if (candidate?.finishReason) {\n output.rawStopReason = candidate.finishReason;\n output.stopReason = mapGoogleStopReason(candidate.finishReason);\n if (\n output.stopReason === 'stop' &&\n output.content.some(block => block.type === 'toolCall')\n ) {\n output.stopReason = 'toolUse';\n }\n }\n\n if (chunk.usageMetadata) {\n output.usage = {\n input:\n (chunk.usageMetadata.promptTokenCount || 0) -\n (chunk.usageMetadata.cachedContentTokenCount || 0),\n output:\n (chunk.usageMetadata.candidatesTokenCount || 0) +\n (chunk.usageMetadata.thoughtsTokenCount || 0),\n cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,\n cacheWrite: 0,\n reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,\n totalTokens: chunk.usageMetadata.totalTokenCount || 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n calculateCost(model, output.usage);\n }\n }\n\n if (current_block) {\n if (current_block.type === 'text') {\n stream.push({\n type: 'text_end',\n contentIndex: block_index(),\n content: current_block.text,\n partial: output,\n });\n } else {\n stream.push({\n type: 'thinking_end',\n contentIndex: block_index(),\n content: current_block.thinking,\n partial: output,\n });\n }\n }\n\n if (options.signal?.aborted) {\n throw new Error('Request was aborted');\n }\n if (output.stopReason === 'pending') {\n throw new Error('Google stream ended without a finish reason');\n }\n if (output.stopReason === 'aborted' || output.stopReason === 'error') {\n throw new Error(\n output.rawStopReason\n ? `Provider stopped with: ${output.rawStopReason}`\n : 'An unknown error occurred'\n );\n }\n\n stream.push({ type: 'done', reason: output.stopReason, message: output });\n stream.end();\n } catch (error) {\n for (const block of output.content) {\n if ('index' in block) {\n delete (block as { index?: number }).index;\n }\n }\n output.stopReason = options.signal?.aborted ? 'aborted' : 'error';\n output.errorMessage = formatGoogleError(error);\n stream.push({ type: 'error', reason: output.stopReason, error: output });\n stream.end();\n }\n })();\n\n return stream;\n}\n\n/** 识别使用思考等级配置的 Gemma 4 模型名称。 */\nfunction isGemma4Model(model: Model<'google-generative-ai'>): boolean {\n return /gemma-?4/.test(model.id.toLowerCase());\n}\n\n/** 识别 Gemini 3 Pro 系列,以使用其支持的思考等级范围。 */\nfunction isGemini3ProModel(model: Model<'google-generative-ai'>): boolean {\n return /gemini-3(?:\\.\\d+)?-pro/.test(model.id.toLowerCase());\n}\n\n/** 识别 Gemini 3 Flash 系列及对应 latest 别名。 */\nfunction isGemini3FlashModel(model: Model<'google-generative-ai'>): boolean {\n const id = model.id.toLowerCase();\n return (\n /gemini-3(?:\\.\\d+)?-flash/.test(id) ||\n id === 'gemini-flash-latest' ||\n id === 'gemini-flash-lite-latest'\n );\n}\n\n/** 为不同 Google 模型选择关闭或最低允许思考配置,避免发送不支持的零预算。 */\nfunction getDisabledThinkingConfig(model: Model<'google-generative-ai'>): ThinkingConfig {\n if (isGemini3ProModel(model)) {\n return { thinkingLevel: 'LOW' as ThinkingConfig['thinkingLevel'] };\n }\n if (isGemini3FlashModel(model) || isGemma4Model(model)) {\n return { thinkingLevel: 'MINIMAL' as ThinkingConfig['thinkingLevel'] };\n }\n return { thinkingBudget: 0 };\n}\n\n/** 把 Pi 思考等级映射到当前 Google 模型实际支持的等级。 */\nfunction getThinkingLevel(\n effort: ResolvedGoogleThinkingLevel,\n model: Model<'google-generative-ai'>\n): GoogleApiThinkingLevel {\n if (isGemini3ProModel(model)) {\n return effort === 'minimal' || effort === 'low' ? 'LOW' : 'HIGH';\n }\n if (isGemma4Model(model)) {\n return effort === 'minimal' || effort === 'low' ? 'MINIMAL' : 'HIGH';\n }\n switch (effort) {\n case 'minimal':\n return 'MINIMAL';\n case 'low':\n return 'LOW';\n case 'medium':\n return 'MEDIUM';\n case 'high':\n return 'HIGH';\n }\n}\n\n/** 优先使用自定义思考预算,否则采用相应模型的预算映射;未知模型交由服务端动态分配。 */\nfunction getGoogleBudget(\n model: Model<'google-generative-ai'>,\n level: ResolvedGoogleThinkingLevel,\n custom_budgets?: ThinkingBudgets\n): number {\n if (custom_budgets?.[level] !== undefined) {\n return custom_budgets[level];\n }\n if (model.id.includes('2.5-pro')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 32768 }[level];\n }\n if (model.id.includes('2.5-flash-lite')) {\n return { minimal: 512, low: 2048, medium: 8192, high: 24576 }[level];\n }\n if (model.id.includes('2.5-flash')) {\n return { minimal: 128, low: 2048, medium: 8192, high: 24576 }[level];\n }\n return -1;\n}\n\n/** 将 Pi 简化选项解析为模型适用的思考等级或预算,再复用注入传输的流实现。 */\nfunction streamSimpleWithInjectedFetch(\n model: Model<'google-generative-ai'>,\n context: TranscriptContext,\n options: SimpleStreamOptions,\n fetch_impl: FetchFunction\n): AssistantMessageEventStream {\n const api_key = options.apiKey;\n if (!api_key) {\n throw new Error(`No API key for provider: ${model.provider}`);\n }\n const base: GoogleProxyOptions = {\n ...buildBaseOptions(model, context, options, api_key),\n toolChoice: options.toolChoice,\n };\n if (!options.reasoning) {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n\n const clamped_reasoning = clampThinkingLevel(model, options.reasoning);\n if (clamped_reasoning === 'off') {\n return streamWithInjectedFetch(\n model,\n context,\n { ...base, thinking: { enabled: false } },\n fetch_impl\n );\n }\n const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);\n if (isGemini3ProModel(model) || isGemini3FlashModel(model) || isGemma4Model(model)) {\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: { enabled: true, level: getThinkingLevel(resolved_level, model) },\n },\n fetch_impl\n );\n }\n return streamWithInjectedFetch(\n model,\n context,\n {\n ...base,\n thinking: {\n enabled: true,\n budgetTokens: getGoogleBudget(model, resolved_level, options.thinkingBudgets),\n },\n },\n fetch_impl\n );\n}\n\n/** 普通浏览器请求沿用 Pi 上游实现;具有独立 fetch 的代理或非流式请求走本地桥接。 */\nexport function createGoogleProxyAwareApi(upstream: ProviderStreams): ProviderStreams {\n return {\n /** 为完整流选项选择上游实现或带请求级传输的 Google 桥接。 */\n stream(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.stream(model, context, options);\n }\n return streamWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options as GoogleProxyOptions,\n options.fetch\n );\n },\n /** 为简化流选项选择上游实现或带请求级传输的 Google 桥接。 */\n streamSimple(model, context, options) {\n if (!shouldUseInjectedFetch(options?.fetch)) {\n return upstream.streamSimple(model, context, options);\n }\n return streamSimpleWithInjectedFetch(\n model as Model<'google-generative-ai'>,\n context,\n options,\n options.fetch\n );\n },\n };\n}\n","import { type Api, type Model } from './pi_gateway';\nimport {\n getPiCatalogModels,\n getPiProviderDefinition,\n isPiCatalogModelApiCompatible,\n type PiAuthType,\n type PiProviderDefinition,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_registry';\nimport {\n getPiProviderApiBaseUrl,\n normalizePiApiBaseEndpoint,\n normalizePiTargetEndpoint,\n PiEndpointValidationError,\n} from './provider_target';\n\nexport { resolvePiApiKeyScope } from './provider_target';\n\n/** 设置中保存的 Pi 连接与模型选择,字符串取值仍需通过注册表校验。 */\nexport interface PiModelConfiguration {\n provider: string;\n api: string;\n authType: string;\n endpoint?: string;\n model: string;\n /** 0 或未填写表示使用目录窗口;正值覆盖目录,目录外模型必须显式填写。 */\n contextWindow?: number;\n}\n\n/** 模型解析边界的原始输入,接受 unknown 以校验旧设置和外部调用。 */\nexport interface ResolvePiModelInput {\n piConfig: PiModelConfiguration | unknown;\n /** 用户配置的最大回复 token 数,尚未应用模型目录的输出上限。 */\n maxTokens: number | unknown;\n apiKey?: string | unknown;\n}\n\n/** 连接、认证和模型预算校验的失败分类,供预检及本地化提示使用。 */\nexport type PiModelResolutionErrorCode =\n | 'invalid_config'\n | 'unknown_provider'\n | 'unsupported_api'\n | 'unsupported_auth'\n | 'missing_api_key'\n | 'invalid_endpoint'\n | 'custom_endpoint_not_allowed'\n | 'oauth_endpoint_not_allowed'\n | 'oauth_api_mismatch'\n | 'missing_model'\n | 'invalid_context_window'\n | 'missing_context_window'\n | 'invalid_max_tokens'\n | 'max_tokens_exceed_context';\n\nexport class PiModelResolutionError extends Error {\n /** 保留模型配置错误码,供预检和界面给出稳定的错误分类。 */\n constructor(\n public readonly code: PiModelResolutionErrorCode,\n message: string\n ) {\n super(message);\n this.name = 'PiModelResolutionError';\n }\n}\n\n/** 校验并规范化后的连接配置,保留目录依据与最终预算,供构造本次 Pi Model 使用。 */\nexport interface ValidatedPiConfiguration {\n definition: PiProviderDefinition;\n provider: PiProviderKey;\n api: PiWireApi;\n authType: PiAuthType;\n endpoint: string;\n modelId: string;\n /** 用户显式窗口;0 表示采用匹配目录项的上下文窗口。 */\n manualContextWindow: number;\n configuredMaxTokens: number;\n apiKey?: string;\n /** 仅在默认端点且协议兼容时使用,避免套用自定义端点的同名模型。 */\n catalogModel?: Model;\n effectiveContextWindow: number;\n /** 用户回复额度与适用目录输出上限的较小值。 */\n effectiveMaxTokens: number;\n}\n\n/** 交给运行时的模型解析结果,model 是本次请求独立的目录副本或动态元数据。 */\nexport interface ResolvedPiModel {\n definition: PiProviderDefinition;\n model: Model;\n /** 是否实际采用了匹配目录的元数据,而非仅命中同名模型。 */\n catalogHit: boolean;\n effectiveContextWindow: number;\n effectiveMaxTokens: number;\n authType: PiAuthType;\n apiKey?: string;\n}\n\n/** 判断输入是否为可读取配置字段的非空、非数组对象。 */\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 读取并规范必填字符串,缺失时抛出指定的模型配置错误。 */\nfunction requiredString(\n source: Record,\n field: string,\n errorCode: PiModelResolutionErrorCode,\n label: string\n): string {\n const value = source[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw new PiModelResolutionError(errorCode, `${label} must be a non-empty string`);\n }\n return value.trim();\n}\n\n/** 规范自定义端点并执行浏览器传输限制,将端点错误转换为模型配置错误。 */\nexport function normalizePiEndpoint(endpoint: string): string {\n try {\n return normalizePiTargetEndpoint(endpoint);\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n}\n\n/** 判断端点是否等同服务商的协议默认地址,空值与显式填写默认地址视为相同。 */\nexport function isPiDefaultProviderEndpoint(\n definition: PiProviderDefinition,\n endpoint: string,\n api: PiWireApi = definition.defaultApi\n): boolean {\n const requested = endpoint.trim();\n if (requested === '') {\n return true;\n }\n try {\n return (\n normalizePiApiBaseEndpoint(api, requested) ===\n normalizePiApiBaseEndpoint(api, getPiProviderApiBaseUrl(definition, api))\n );\n } catch {\n return false;\n }\n}\n\n/** 校验允许 0 哨兵值的配置整数,拒绝无效值隐式回退。 */\nfunction validateNonNegativeInteger(value: unknown, field: string): number {\n if (value === undefined) {\n return 0;\n }\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n `${field} must be a non-negative integer`\n );\n }\n return value;\n}\n\n/** 校验必须大于 0 的配置整数,供回复 token 等硬性预算使用。 */\nfunction validatePositiveInteger(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new PiModelResolutionError(\n 'invalid_max_tokens',\n `${field} must be a positive integer`\n );\n }\n return value;\n}\n\n/** 从所选服务商目录中查找模型,并检查目录项与当前协议是否兼容。 */\nfunction findCatalogModel(\n definition: PiProviderDefinition,\n modelId: string\n): Model | undefined {\n return getPiCatalogModels(definition.key).find(model => model.id === modelId);\n}\n\n/**\n * 校验服务商、协议、认证、端点、模型和 token 参数的组合。\n * 只使用适用于当前端点的目录元数据,不修改设置或共享目录对象。\n */\nexport function validatePiConfiguration(input: ResolvePiModelInput): ValidatedPiConfiguration {\n if (!isRecord(input.piConfig)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'More source configuration must be an object'\n );\n }\n\n const provider = requiredString(\n input.piConfig,\n 'provider',\n 'unknown_provider',\n 'More source provider'\n );\n const definition = getPiProviderDefinition(provider);\n if (!definition) {\n throw new PiModelResolutionError(\n 'unknown_provider',\n `Unknown More source provider: ${provider}`\n );\n }\n\n const api = requiredString(input.piConfig, 'api', 'unsupported_api', 'More source API');\n if (!definition.allowedApis.some(candidate => candidate === api)) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Provider ${definition.key} does not support API ${api}`\n );\n }\n\n const authType = requiredString(\n input.piConfig,\n 'authType',\n 'unsupported_auth',\n 'More source auth type'\n );\n if (!definition.allowedAuthTypes.some(candidate => candidate === authType)) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} does not support ${authType} authentication`\n );\n }\n\n const rawEndpoint = input.piConfig.endpoint;\n if (rawEndpoint !== undefined && typeof rawEndpoint !== 'string') {\n throw new PiModelResolutionError(\n 'invalid_endpoint',\n 'More source endpoint must be a string'\n );\n }\n const requestedEndpoint = (rawEndpoint ?? '').trim();\n\n if (authType === 'oauth') {\n if (!definition.oauth) {\n throw new PiModelResolutionError(\n 'unsupported_auth',\n `Provider ${definition.key} has no OAuth registration`\n );\n }\n if (requestedEndpoint) {\n throw new PiModelResolutionError(\n 'oauth_endpoint_not_allowed',\n 'OAuth providers cannot use a custom endpoint'\n );\n }\n if (api !== definition.oauth.api) {\n throw new PiModelResolutionError(\n 'oauth_api_mismatch',\n `OAuth for ${definition.key} requires API ${definition.oauth.api}`\n );\n }\n } else if (requestedEndpoint && !definition.allowCustomEndpoint) {\n throw new PiModelResolutionError(\n 'custom_endpoint_not_allowed',\n `Provider ${definition.key} does not allow a custom endpoint`\n );\n }\n\n let endpoint: string;\n const defaultEndpoint = getPiProviderApiBaseUrl(definition, api as PiWireApi);\n try {\n endpoint = requestedEndpoint\n ? normalizePiApiBaseEndpoint(api as PiWireApi, requestedEndpoint)\n : defaultEndpoint;\n } catch (error) {\n if (error instanceof PiEndpointValidationError) {\n throw new PiModelResolutionError('invalid_endpoint', error.message);\n }\n throw error;\n }\n const usesDefaultEndpoint = endpoint === normalizePiEndpoint(defaultEndpoint);\n const modelId = requiredString(input.piConfig, 'model', 'missing_model', 'More source model');\n const manualContextWindow = validateNonNegativeInteger(\n input.piConfig.contextWindow,\n 'More source contextWindow'\n );\n const configuredMaxTokens = validatePositiveInteger(input.maxTokens, 'maximum reply tokens');\n const catalogModelById = findCatalogModel(definition, modelId);\n const catalogModelMatchesApi =\n catalogModelById !== undefined &&\n isPiCatalogModelApiCompatible(definition, catalogModelById, api as PiWireApi);\n if (catalogModelById && !catalogModelMatchesApi && usesDefaultEndpoint) {\n throw new PiModelResolutionError(\n 'unsupported_api',\n `Model ${modelId} is registered for ${catalogModelById.api}, not ${api}`\n );\n }\n // On a custom OpenAI-compatible endpoint the same identifier may refer to an entirely\n // different model. Require manual metadata and treat it as dynamic instead of replaying\n // catalog metadata from another wire API.\n const catalogModel =\n catalogModelMatchesApi && usesDefaultEndpoint ? catalogModelById : undefined;\n\n if (!catalogModel && manualContextWindow === 0) {\n throw new PiModelResolutionError(\n 'missing_context_window',\n `Model ${modelId} is not in the ${definition.key} catalog; contextWindow is required`\n );\n }\n\n const effectiveContextWindow =\n manualContextWindow > 0 ? manualContextWindow : catalogModel!.contextWindow;\n if (!Number.isInteger(effectiveContextWindow) || effectiveContextWindow <= 0) {\n throw new PiModelResolutionError(\n 'invalid_context_window',\n 'effective contextWindow must be a positive integer'\n );\n }\n if (configuredMaxTokens > effectiveContextWindow) {\n throw new PiModelResolutionError(\n 'max_tokens_exceed_context',\n 'maximum reply tokens must not exceed contextWindow'\n );\n }\n\n const effectiveMaxTokens = catalogModel\n ? Math.min(configuredMaxTokens, catalogModel.maxTokens)\n : configuredMaxTokens;\n\n let apiKey: string | undefined;\n if (authType === 'api_key') {\n if (typeof input.apiKey !== 'string' || input.apiKey.trim() === '') {\n throw new PiModelResolutionError(\n 'missing_api_key',\n `An API key is required for ${definition.key}`\n );\n }\n apiKey = input.apiKey.trim();\n }\n\n return {\n definition,\n provider: definition.providerId,\n api: api as PiWireApi,\n authType: authType as PiAuthType,\n endpoint,\n modelId,\n manualContextWindow,\n configuredMaxTokens,\n apiKey,\n catalogModel,\n effectiveContextWindow,\n effectiveMaxTokens,\n };\n}\n\n/** 为目录外或自定义端点模型构造独立元数据,使用显式窗口与保守能力默认值。 */\nfunction createDynamicModel(configuration: ValidatedPiConfiguration): Model {\n return {\n id: configuration.modelId,\n name: configuration.modelId,\n api: configuration.api,\n provider: configuration.provider,\n baseUrl: configuration.endpoint,\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: configuration.effectiveContextWindow,\n maxTokens: configuration.effectiveMaxTokens,\n };\n}\n\n/** 复制目录模型后应用本次端点和预算,防止请求修改共享目录元数据。 */\nfunction cloneCatalogModel(configuration: ValidatedPiConfiguration): Model {\n const original = configuration.catalogModel!;\n const model = structuredClone(original) as Model;\n\n model.api = configuration.api;\n model.provider = configuration.provider;\n model.baseUrl = configuration.endpoint;\n model.contextWindow = configuration.effectiveContextWindow;\n model.maxTokens = configuration.effectiveMaxTokens;\n\n // Compatibility metadata is API-shaped. Do not replay Responses flags into Completions.\n if (original.api !== configuration.api) {\n delete model.compat;\n }\n return model;\n}\n\n/** 在配置校验后选择目录模型或动态模型,并返回本次请求的有效预算及认证信息。 */\nexport function resolvePiModel(input: ResolvePiModelInput): ResolvedPiModel {\n const configuration = validatePiConfiguration(input);\n const catalogHit = configuration.catalogModel !== undefined;\n return {\n definition: configuration.definition,\n model: catalogHit ? cloneCatalogModel(configuration) : createDynamicModel(configuration),\n catalogHit,\n effectiveContextWindow: configuration.effectiveContextWindow,\n effectiveMaxTokens: configuration.effectiveMaxTokens,\n authType: configuration.authType,\n apiKey: configuration.apiKey,\n };\n}\n\n/** 从中文字段的额外模型设置中提取 Pi 连接和回复预算,再调用统一模型解析。 */\nexport function resolvePiModelFromExtraModelSettings(settings: unknown): ResolvedPiModel {\n if (!isRecord(settings)) {\n throw new PiModelResolutionError(\n 'invalid_config',\n 'extra-model settings must be an object'\n );\n }\n return resolvePiModel({\n piConfig: settings.pi,\n maxTokens: settings['最大回复token数'],\n apiKey: settings['密钥'],\n });\n}\n","import {\n createPiOAuthLoginStore,\n createPiOAuthLogoutStore,\n getPiCredentialStore,\n} from './credential_store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport type { CredentialStore, ModelAuth, OAuthAuth, OAuthCredential } from './pi_gateway';\nimport { getPiProviderRegistration, type PiOAuthDefinition } from './provider_registry';\n\nconst DEFAULT_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst CLOSED_ATTEMPT_TTL_MS = 10 * 60 * 1000;\nconst MAX_CLOSED_ATTEMPTS = 128;\nconst REFRESH_TIMEOUT_MS = 15_000;\nconst OPENAI_CODEX_PROVIDER_ID = 'openai-codex';\nconst ANTHROPIC_PROVIDER_ID = 'anthropic';\nconst OPENAI_ACCOUNT_CLAIM = 'https://api.openai.com/auth';\n\n/** 浏览器登录、回调校验、令牌交换及持久化的失败分类,供界面和取消逻辑使用。 */\nexport type PiOAuthErrorCode =\n | 'unsupported_provider'\n | 'browser_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_failed'\n | 'attempt_expired'\n | 'attempt_used'\n | 'cancelled'\n | 'browser_network'\n | 'token_http'\n | 'token_response'\n | 'account_id'\n | 'missing_credential'\n | 'credential_store';\n\nexport class PiOAuthError extends Error {\n readonly code: PiOAuthErrorCode;\n\n /** 为浏览器 OAuth 失败保留稳定错误码,供界面和取消逻辑分类处理。 */\n constructor(code: PiOAuthErrorCode, message: string) {\n super(message);\n this.name = 'PiOAuthError';\n this.code = code;\n }\n}\n\n/** 可展示给界面的登录尝试信息,不包含 PKCE verifier 或令牌。 */\nexport type PiOAuthAttemptView = {\n id: string;\n providerId: string;\n authorizationUrl: string;\n /** 本次回调可被接受的截止时间,使用 Unix 毫秒时间戳。 */\n expiresAt: number;\n};\n\n/** 对外展示的 OAuth 凭证摘要;loggedIn 表示已存有凭证,过期凭证仍可尝试刷新。 */\nexport type PiOAuthCredentialStatus = {\n loggedIn: boolean;\n type?: 'oauth';\n /** 已保存访问令牌的过期时间,使用 Unix 毫秒时间戳。 */\n expiresAt?: number;\n};\n\n/** 令牌请求需要的最小 fetch 契约,便于注入测试传输。 */\ntype FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise;\n\n/** 生成 state、PKCE verifier 和 SHA-256 challenge 所需的最小 Web Crypto 接口。 */\ntype CryptoLike = {\n getRandomValues(array: T): T;\n subtle: {\n digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise;\n };\n};\n\n/** 单次登录引用的只读服务商 OAuth 注册信息。 */\ntype OAuthMetadata = Readonly;\n\n/** 可替换的浏览器与存储依赖,未传入时使用页面环境和共享凭证仓库。 */\ntype PiOAuthDependencies = {\n fetch?: FetchLike;\n crypto?: CryptoLike;\n /** 返回 Unix 毫秒时间戳,用于登录有效期和令牌过期计算。 */\n now?: () => number;\n credentialStore?: CredentialStore;\n};\n\n/** 发起登录时的依赖、取消信号及回调等待期限。 */\nexport type BeginPiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n /** 从创建授权链接起计算的有效时长,单位为毫秒。 */\n attemptTtlMs?: number;\n};\n\n/** 完成回调校验和授权码交换时使用的依赖与取消信号。 */\nexport type CompletePiOAuthOptions = PiOAuthDependencies & {\n signal?: AbortSignal;\n};\n\n/** 凭证查询、删除等操作共享的取消与存储配置。 */\nexport type PiOAuthOperationOptions = {\n signal?: AbortSignal;\n credentialStore?: CredentialStore;\n};\n\n/** 刷新令牌所需的操作选项,只额外依赖网络和时钟,不需要重新生成 PKCE。 */\nexport type RefreshPiOAuthOptions = PiOAuthOperationOptions &\n Pick;\n\n/** 构建 Pi OAuthAuth 桥接对象时注入的浏览器能力与登录有效时长。 */\nexport type BrowserOAuthAuthOptions = Pick & {\n attemptTtlMs?: number;\n};\n\n/** 仅保存在内存中的登录尝试,负责回调防重放、有效期和交换请求取消。 */\ntype PendingAttempt = {\n id: string;\n providerId: string;\n metadata: OAuthMetadata;\n /** 登录开始时捕获的所属方案仓库,回调不再解析当前界面。 */\n credentialStore: CredentialStore;\n /** PKCE 原始验证串,只在交换授权码时提交给令牌端点。 */\n verifier: string;\n /** 与回调严格匹配的随机 state,用于绑定本次授权请求。 */\n state: string;\n expiresAt: number;\n /** exchanging 表示授权码已被领取处理,阻止重复提交同一回调。 */\n phase: 'pending' | 'exchanging';\n controller: AbortController;\n timeout: ReturnType;\n /** 清理调用方取消监听,避免尝试结束后继续持有外部信号。 */\n detachCallerAbort?: () => void;\n};\n\n/** 短期保留已关闭尝试的原因,使迟到或重复回调得到明确错误。 */\ntype ClosedAttemptReason = 'used' | 'expired' | 'cancelled';\n\nconst pendingAttempts = new Map();\nconst closedAttempts = new Map();\n\n/** 构造带稳定错误码的 OAuth 错误。 */\nfunction oauthError(code: PiOAuthErrorCode, message: string): PiOAuthError {\n return new PiOAuthError(code, message);\n}\n\n/** 创建统一的登录取消错误,供各异步阶段使用。 */\nfunction cancellationError(): PiOAuthError {\n return oauthError('cancelled', 'More source OAuth login was cancelled.');\n}\n\n/** 将已取消信号转换为 OAuth 取消错误,阻止继续交换或写入令牌。 */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw cancellationError();\n }\n}\n\n/** 解析可注入的时钟,使登录有效期和刷新测试可以精确控制时间。 */\nfunction getNow(options?: Pick): () => number {\n return options?.now ?? Date.now;\n}\n\n/** 解析浏览器令牌请求使用的 fetch,缺少能力时返回明确的环境错误。 */\nfunction getFetch(options?: Pick): FetchLike {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires the browser Fetch API for token requests.'\n );\n }\n return fetchImpl.bind(globalThis) as FetchLike;\n}\n\n/** 确认 Web Crypto 提供随机数和 SHA-256,满足 OAuth state 与 PKCE 的生成要求。 */\nfunction getCrypto(options?: Pick): CryptoLike {\n const cryptoImpl = options?.crypto ?? (globalThis.crypto as CryptoLike | undefined);\n if (\n !cryptoImpl ||\n typeof cryptoImpl.getRandomValues !== 'function' ||\n typeof cryptoImpl.subtle?.digest !== 'function'\n ) {\n throw oauthError(\n 'browser_unavailable',\n 'More source OAuth requires Web Crypto with SHA-256 support.'\n );\n }\n return cryptoImpl;\n}\n\n/** 优先使用注入的凭证仓库,否则复用运行时的共享仓库。 */\nfunction getCredentialStore(\n options?: Pick\n): CredentialStore {\n return options?.credentialStore ?? getPiCredentialStore();\n}\n\n/** 只解析已注册且支持浏览器登录的 OAuth 服务商,拒绝其他授权流程。 */\nfunction getOAuthMetadata(providerId: string): OAuthMetadata {\n const registration = getPiProviderRegistration(providerId);\n const metadata = registration?.oauth;\n if (!metadata || metadata.providerId !== providerId) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n if (providerId !== ANTHROPIC_PROVIDER_ID && providerId !== OPENAI_CODEX_PROVIDER_ID) {\n throw oauthError(\n 'unsupported_provider',\n `More source provider \"${providerId}\" does not support browser OAuth.`\n );\n }\n return metadata;\n}\n\n/** 将随机字节或摘要编码为无填充的 Base64URL,供 PKCE 和 state 使用。 */\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return globalThis.btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\n/** 用密码学随机数生成指定字节长度的 Base64URL 标识。 */\nfunction randomBase64Url(cryptoImpl: CryptoLike, length: number): string {\n return bytesToBase64Url(cryptoImpl.getRandomValues(new Uint8Array(length)));\n}\n\n/** 生成仅保留在内存中的 verifier,并计算 S256 challenge。 */\nasync function createPkce(cryptoImpl: CryptoLike): Promise<{\n verifier: string;\n challenge: string;\n}> {\n const verifier = randomBase64Url(cryptoImpl, 32);\n const verifierBytes = Uint8Array.from(verifier, character => character.charCodeAt(0));\n const digest = await cryptoImpl.subtle.digest('SHA-256', verifierBytes);\n return {\n verifier,\n challenge: bytesToBase64Url(new Uint8Array(digest)),\n };\n}\n\n/** 按服务商元数据组装授权地址,附带回调地址、scope、PKCE 和 state。 */\nfunction createAuthorizationUrl(metadata: OAuthMetadata, challenge: string, state: string): string {\n const url = new URL(metadata.authorizeUrl);\n for (const [name, value] of Object.entries(metadata.authorizeParams)) {\n url.searchParams.set(name, value);\n }\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', metadata.clientId);\n url.searchParams.set('redirect_uri', metadata.redirectUri);\n url.searchParams.set('scope', metadata.scope);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n return url.toString();\n}\n\n/** 短期记录已关闭尝试的原因,并限制记录数量,使重复回调可得到准确错误。 */\nfunction rememberClosedAttempt(id: string, reason: ClosedAttemptReason): void {\n const now = Date.now();\n for (const [closedId, closed] of closedAttempts) {\n if (closed.removeAfter <= now) {\n closedAttempts.delete(closedId);\n }\n }\n while (closedAttempts.size >= MAX_CLOSED_ATTEMPTS) {\n const oldestId = closedAttempts.keys().next().value as string | undefined;\n if (oldestId === undefined) {\n break;\n }\n closedAttempts.delete(oldestId);\n }\n closedAttempts.set(id, { reason, removeAfter: now + CLOSED_ATTEMPT_TTL_MS });\n}\n\n/** 关闭当前登录尝试,释放计时器和监听,清除 verifier 与 state,并记录关闭原因。 */\nfunction closeAttempt(attempt: PendingAttempt, reason: ClosedAttemptReason, abort: boolean): void {\n if (pendingAttempts.get(attempt.id) !== attempt) {\n return;\n }\n pendingAttempts.delete(attempt.id);\n clearTimeout(attempt.timeout);\n attempt.detachCallerAbort?.();\n if (abort && !attempt.controller.signal.aborted) {\n attempt.controller.abort();\n }\n attempt.verifier = '';\n attempt.state = '';\n rememberClosedAttempt(attempt.id, reason);\n}\n\n/** 使超时尝试失效并取消进行中的令牌交换。 */\nfunction expireAttempt(attempt: PendingAttempt): void {\n closeAttempt(attempt, 'expired', true);\n}\n\n/** 将调用方取消传递给内部控制器,并返回解绑函数。 */\nfunction linkAbortSignal(signal: AbortSignal | undefined, controller: AbortController): () => void {\n if (!signal) {\n return () => undefined;\n }\n /** 将外部取消转发给当前令牌操作。 */\n const onAbort = () => controller.abort();\n if (signal.aborted) {\n onAbort();\n } else {\n signal.addEventListener('abort', onAbort, { once: true });\n }\n return () => signal.removeEventListener('abort', onAbort);\n}\n\n/** 根据已关闭原因区分过期、取消和回调重复使用。 */\nfunction errorForClosedAttempt(reason: ClosedAttemptReason): PiOAuthError {\n if (reason === 'expired') {\n return oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (reason === 'cancelled') {\n return cancellationError();\n }\n return oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n}\n\n/** 取得仍处于待回调阶段的有效尝试,拒绝过期、已消费或已取消的记录。 */\nfunction getPendingAttempt(attemptId: string, now: number): PendingAttempt {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n const closed = closedAttempts.get(attemptId);\n if (closed) {\n throw errorForClosedAttempt(closed.reason);\n }\n throw oauthError(\n 'attempt_used',\n 'No active More source OAuth login attempt matches this callback.'\n );\n }\n if (now >= attempt.expiresAt) {\n expireAttempt(attempt);\n throw oauthError('attempt_expired', 'This More source OAuth login attempt has expired.');\n }\n if (attempt.phase !== 'pending') {\n throw oauthError('attempt_used', 'This More source OAuth callback has already been used.');\n }\n if (attempt.controller.signal.aborted) {\n closeAttempt(attempt, 'cancelled', false);\n throw cancellationError();\n }\n return attempt;\n}\n\n/** 通过地址与 state 校验后提取的授权码回调,不再携带完整 URL。 */\ntype ParsedCallback = { code: string; state: string };\n\n/** 补齐 URL 的默认端口,使回调地址校验不受显式端口写法影响。 */\nfunction normalizePort(url: URL): string {\n if (url.port) {\n return url.port;\n }\n return url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '';\n}\n\n/**\n * 校验完整回环回调地址的协议、主机、端口、路径和 state,再提取授权码。\n * 回调必须属于当前尝试;服务商返回的授权错误也在此转换为固定错误。\n */\nfunction parseAndValidateCallback(callbackUrl: string, attempt: PendingAttempt): ParsedCallback {\n let callback: URL;\n let redirect: URL;\n try {\n callback = new URL(callbackUrl.trim());\n redirect = new URL(attempt.metadata.redirectUri);\n } catch {\n throw oauthError(\n 'invalid_callback',\n 'Paste the complete loopback callback URL from the browser address bar.'\n );\n }\n\n const allowedHosts = new Set(\n attempt.metadata.allowedCallbackHosts.map(host => host.toLowerCase())\n );\n allowedHosts.add(redirect.hostname.toLowerCase());\n if (\n callback.protocol !== 'http:' ||\n callback.username !== '' ||\n callback.password !== '' ||\n callback.hash !== '' ||\n !allowedHosts.has(callback.hostname.toLowerCase()) ||\n normalizePort(callback) !== normalizePort(redirect) ||\n callback.pathname !== redirect.pathname\n ) {\n throw oauthError(\n 'invalid_callback',\n 'The More source OAuth callback does not match the registered loopback address.'\n );\n }\n\n const state = callback.searchParams.get('state') ?? '';\n if (!state || state !== attempt.state) {\n throw oauthError('state_mismatch', 'The More source OAuth callback state does not match.');\n }\n if (callback.searchParams.has('error')) {\n throw oauthError(\n 'authorization_failed',\n 'The OAuth provider did not authorize the More source login.'\n );\n }\n const code = callback.searchParams.get('code') ?? '';\n if (!code) {\n throw oauthError('invalid_callback', 'The More source OAuth callback is missing its code.');\n }\n return { code, state };\n}\n\n/** 综合信号和 OAuth 错误码识别取消,避免误报为网络或存储故障。 */\nfunction isCancellation(error: unknown, signal: AbortSignal): boolean {\n return signal.aborted || (error instanceof PiOAuthError && error.code === 'cancelled');\n}\n\n/** 区分主动取消与浏览器网络失败,不透传可能含敏感请求数据的底层错误。 */\nfunction normalizeFetchFailure(error: unknown, signal: AbortSignal): never {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n if (error instanceof TypeError) {\n throw oauthError(\n 'browser_network',\n 'The browser could not reach the More source OAuth token endpoint. Check network access and whether the provider allows browser CORS requests.'\n );\n }\n throw oauthError(\n 'browser_network',\n 'The More source OAuth token request failed in the browser.'\n );\n}\n\n/** 令牌交换的两种输入:首次登录使用授权码和 PKCE,续期使用刷新令牌。 */\ntype TokenGrant =\n | { type: 'authorization_code'; code: string; verifier: string; state: string }\n | { type: 'refresh_token'; refreshToken: string };\n\n/** 已校验并统一字段名的令牌响应,持久化前还需计算过期时间和补齐账号信息。 */\ntype TokenData = {\n access: string;\n refresh: string;\n /** 服务商返回的有效时长,单位为秒,并非绝对时间戳。 */\n expiresInSeconds: number;\n};\n\n/** 按授权码或刷新令牌两种 grant 生成令牌请求字段,并遵循服务商的 state 约定。 */\nfunction createTokenFields(metadata: OAuthMetadata, grant: TokenGrant): Record {\n const fields: Record = { ...metadata.tokenParams };\n fields.client_id = metadata.clientId;\n if (grant.type === 'authorization_code') {\n fields.grant_type = 'authorization_code';\n fields.code = grant.code;\n fields.code_verifier = grant.verifier;\n fields.redirect_uri = metadata.redirectUri;\n if (metadata.includeStateInTokenRequest) {\n fields.state = grant.state;\n }\n } else {\n fields.grant_type = 'refresh_token';\n fields.refresh_token = grant.refreshToken;\n }\n return fields;\n}\n\n/** 校验令牌接口状态和必需字段;刷新响应未返回 refresh_token 时保留原刷新令牌。 */\nasync function readTokenResponse(\n response: Response,\n fallbackRefreshToken?: string\n): Promise {\n if (!response.ok) {\n throw oauthError(\n 'token_http',\n `The More source OAuth token endpoint rejected the request (HTTP ${response.status}).`\n );\n }\n\n let body: unknown;\n try {\n body = JSON.parse(await response.text());\n } catch {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n if (!body || typeof body !== 'object') {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint returned an invalid response.'\n );\n }\n const token = body as Record;\n const access = token.access_token;\n const refresh = token.refresh_token ?? fallbackRefreshToken;\n const expiresIn = token.expires_in;\n if (\n typeof access !== 'string' ||\n access.length === 0 ||\n typeof refresh !== 'string' ||\n refresh.length === 0 ||\n typeof expiresIn !== 'number' ||\n !Number.isFinite(expiresIn) ||\n expiresIn <= 0\n ) {\n throw oauthError(\n 'token_response',\n 'The More source OAuth token endpoint response is missing required fields.'\n );\n }\n return { access, refresh, expiresInSeconds: expiresIn };\n}\n\n/** 按服务商要求用 JSON 或表单交换令牌,并在请求前后检查取消。 */\nasync function requestTokens(\n metadata: OAuthMetadata,\n grant: TokenGrant,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n const fields = createTokenFields(metadata, grant);\n const headers: Record = { Accept: 'application/json' };\n let body: string;\n if (metadata.exchangeKind === 'json') {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(fields);\n } else {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n body = new URLSearchParams(fields).toString();\n }\n\n let response: Response;\n try {\n response = await getFetch(options)(metadata.tokenUrl, {\n method: 'POST',\n headers,\n body,\n signal,\n });\n } catch (error) {\n normalizeFetchFailure(error, signal);\n }\n throwIfAborted(signal);\n return readTokenResponse(\n response!,\n grant.type === 'refresh_token' ? grant.refreshToken : undefined\n );\n}\n\n/** 解码 JWT 中的 Base64URL JSON 数据,兼容缺少 TextDecoder 的浏览器。 */\nfunction decodeBase64UrlJson(value: string): unknown {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const binary = globalThis.atob(padded);\n const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));\n const json =\n typeof globalThis.TextDecoder === 'function'\n ? new globalThis.TextDecoder().decode(bytes)\n : decodeURIComponent(\n [...bytes].map(byte => `%${byte.toString(16).padStart(2, '0')}`).join('')\n );\n return JSON.parse(json);\n}\n\n/** 从访问令牌的账号声明中读取 Codex 所需账号标识;缺失时拒绝建立凭证。 */\nfunction extractOpenAIAccountId(accessToken: string): string {\n try {\n const parts = accessToken.split('.');\n if (parts.length !== 3) {\n throw new Error('invalid JWT');\n }\n const payload = decodeBase64UrlJson(parts[1]);\n if (!payload || typeof payload !== 'object') {\n throw new Error('invalid JWT payload');\n }\n const authClaim = (payload as Record)[OPENAI_ACCOUNT_CLAIM];\n if (!authClaim || typeof authClaim !== 'object') {\n throw new Error('missing auth claim');\n }\n const accountId = (authClaim as Record).chatgpt_account_id;\n if (typeof accountId !== 'string' || accountId.length === 0) {\n throw new Error('missing account ID');\n }\n return accountId;\n } catch {\n throw oauthError(\n 'account_id',\n 'The OpenAI Codex OAuth token does not contain a ChatGPT account ID.'\n );\n }\n}\n\n/** 将令牌响应转换为 Pi OAuth 凭证,预留过期时间偏差,并补齐 Codex 账号标识。 */\nfunction credentialFromTokens(\n metadata: OAuthMetadata,\n token: TokenData,\n now: number\n): OAuthCredential {\n const credential: OAuthCredential = {\n type: 'oauth',\n access: token.access,\n refresh: token.refresh,\n expires: now + token.expiresInSeconds * 1000 - metadata.expirySkewMs,\n };\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n credential.accountId = extractOpenAIAccountId(token.access);\n }\n return credential;\n}\n\n/** 通过共享凭证队列保存登录结果,分别处理取消与存储失败。 */\nasync function persistCredential(\n providerId: string,\n credential: OAuthCredential,\n signal: AbortSignal,\n options?: Pick\n): Promise {\n throwIfAborted(signal);\n try {\n await getCredentialStore(options).modify(providerId, async () => credential, { signal });\n } catch (error) {\n if (isCancellation(error, signal)) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'More source OAuth login succeeded, but the credential could not be saved.'\n );\n }\n}\n\n/**\n * 创建带有效期的浏览器登录尝试,生成 PKCE 和 state,返回供界面打开的授权地址。\n * 秘密校验信息留在内存中,调用方取消或超时会关闭该尝试。\n */\nexport async function beginPiOAuth(\n providerId: string,\n options: BeginPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const metadata = getOAuthMetadata(providerId);\n const ttl = options.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new RangeError('More source OAuth attemptTtlMs must be a positive number.');\n }\n const cryptoImpl = getCrypto(options);\n const credentialStore =\n options.credentialStore ??\n createPiOAuthLoginStore(providerId, randomBase64Url(cryptoImpl, 18));\n const { verifier, challenge } = await createPkce(cryptoImpl);\n throwIfAborted(options.signal);\n\n const state = randomBase64Url(cryptoImpl, 32);\n const id = randomBase64Url(cryptoImpl, 18);\n const expiresAt = getNow(options)() + ttl;\n const controller = new AbortController();\n const attempt: PendingAttempt = {\n id,\n providerId,\n metadata,\n credentialStore,\n verifier,\n state,\n expiresAt,\n phase: 'pending' as const,\n controller,\n timeout: undefined as unknown as ReturnType,\n };\n attempt.timeout = setTimeout(() => expireAttempt(attempt), ttl);\n pendingAttempts.set(id, attempt);\n if (options.signal) {\n /** 关闭已取消的登录尝试,阻止随后到达的回调继续交换令牌。 */\n const onAbort = () => closeAttempt(attempt, 'cancelled', true);\n options.signal.addEventListener('abort', onAbort, { once: true });\n attempt.detachCallerAbort = () => options.signal?.removeEventListener('abort', onAbort);\n if (options.signal.aborted) {\n onAbort();\n throw cancellationError();\n }\n }\n\n return {\n id,\n providerId,\n authorizationUrl: createAuthorizationUrl(metadata, challenge, state),\n expiresAt,\n };\n}\n\n/**\n * 校验并一次性消费回调,交换授权码后保存凭证,最终清理尝试状态。\n * 重复回调或交换期间取消都不能产生第二次有效登录。\n */\nexport async function completePiOAuth(\n attemptId: string,\n callbackUrl: string,\n options: CompletePiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const now = getNow(options);\n const attempt = getPendingAttempt(attemptId, now());\n let parsed: ParsedCallback;\n try {\n parsed = parseAndValidateCallback(callbackUrl, attempt);\n } catch (error) {\n if (error instanceof PiOAuthError && error.code === 'authorization_failed') {\n closeAttempt(attempt, 'used', true);\n }\n throw error;\n }\n attempt.phase = 'exchanging';\n const detachCompleteAbort = linkAbortSignal(options.signal, attempt.controller);\n\n try {\n throwIfAborted(attempt.controller.signal);\n const token = await requestTokens(\n attempt.metadata,\n {\n type: 'authorization_code',\n code: parsed.code,\n verifier: attempt.verifier,\n state: parsed.state,\n },\n attempt.controller.signal,\n options\n );\n const credential = credentialFromTokens(attempt.metadata, token, now());\n await persistCredential(attempt.providerId, credential, attempt.controller.signal, {\n credentialStore: options.credentialStore ?? attempt.credentialStore,\n });\n closeAttempt(attempt, 'used', false);\n return credential;\n } catch (error) {\n const cancelled = isCancellation(error, attempt.controller.signal);\n closeAttempt(attempt, cancelled ? 'cancelled' : 'used', true);\n if (cancelled) {\n throw cancellationError();\n }\n throw error;\n } finally {\n detachCompleteAbort();\n }\n}\n\n/** 按尝试编号取消登录,清除回调校验状态并中止未完成的交换。 */\nexport function cancelPiOAuth(attemptId: string): boolean {\n const attempt = pendingAttempts.get(attemptId);\n if (!attempt) {\n return false;\n }\n closeAttempt(attempt, 'cancelled', true);\n return true;\n}\n\n/** 取消全部待完成的登录尝试,供界面卸载和脚本清理使用。 */\nexport function cancelAllPiOAuth(): void {\n for (const attempt of [...pendingAttempts.values()]) {\n closeAttempt(attempt, 'cancelled', true);\n }\n}\n\n/** 解除捕获方案的登录绑定;其他方案仍引用的集中凭证会继续保留。 */\nexport async function logoutPiOAuth(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n try {\n const store = options.credentialStore ?? createPiOAuthLogoutStore();\n await store.delete(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential could not be removed.'\n );\n }\n}\n\n/** 返回是否已登录及过期时间等界面状态,不暴露访问令牌和刷新令牌。 */\nexport async function getPiOAuthCredentialStatus(\n providerId: string,\n options: PiOAuthOperationOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n getOAuthMetadata(providerId);\n let credential;\n try {\n credential = await getCredentialStore(options).read(providerId, { signal: options.signal });\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'credential_store',\n 'The More source OAuth credential status could not be read.'\n );\n }\n if (!credential || credential.type !== 'oauth') {\n return { loggedIn: false };\n }\n return {\n loggedIn: true,\n type: 'oauth',\n expiresAt: credential.expires,\n };\n}\n\n/**\n * 由界面强制刷新现有 OAuth 凭证,与 Pi 自动刷新共用同一服务商修改锁。\n * 操作使用捕获的凭证仓库和取消信号,刷新完成后再提交新凭证。\n */\nexport async function refreshPiOAuth(\n providerId: string,\n options: RefreshPiOAuthOptions = {}\n): Promise {\n throwIfAborted(options.signal);\n const auth = getBrowserOAuthAuth(providerId, options);\n const store = getCredentialStore(options);\n installPiAbortSignalPolyfills();\n const signal = AbortSignal.any([\n ...(options.signal ? [options.signal] : []),\n AbortSignal.timeout(REFRESH_TIMEOUT_MS),\n ]);\n /** 生成缺少可刷新 OAuth 凭证的错误,供刷新入口统一报告。 */\n const missingCredential = () =>\n oauthError('missing_credential', 'Sign in before refreshing OAuth credentials.');\n try {\n const observed = await store.read(providerId, { signal });\n if (observed?.type !== 'oauth') {\n throw missingCredential();\n }\n const refreshed = await store.modify(\n providerId,\n async current => {\n throwIfAborted(signal);\n if (current?.type !== 'oauth') {\n throw missingCredential();\n }\n // An automatic refresh or another login may have completed while we queued.\n // Keep that result instead of reusing or rotating its token a second time.\n if (\n current.access !== observed.access ||\n current.refresh !== observed.refresh ||\n current.expires !== observed.expires\n ) {\n return undefined;\n }\n return auth.refresh(current, signal);\n },\n { signal }\n );\n if (refreshed?.type !== 'oauth') {\n throw missingCredential();\n }\n return { loggedIn: true, type: 'oauth', expiresAt: refreshed.expires };\n } catch (error) {\n if (options.signal?.aborted) {\n throw cancellationError();\n }\n if (signal.aborted) {\n throw oauthError('browser_network', 'The OAuth credential refresh timed out.');\n }\n if (error instanceof PiOAuthError) {\n throw error;\n }\n throw oauthError('credential_store', 'The refreshed OAuth credential could not be saved.');\n }\n}\n\n/**\n * 将浏览器授权、刷新和凭证转换接入 Pi 的 OAuthAuth 契约。\n * 登录通过授权地址与手动回调交互完成,不启动 Node 回环服务器。\n */\nfunction createBrowserOAuthAuth(\n metadata: OAuthMetadata,\n options: BrowserOAuthAuthOptions\n): OAuthAuth {\n const providerName =\n metadata.providerId === ANTHROPIC_PROVIDER_ID\n ? 'Anthropic (Claude Pro/Max)'\n : 'OpenAI (ChatGPT Plus/Pro)';\n return {\n name: providerName,\n isSubscription: true,\n /** 通过 Pi 认证交互展示授权地址、接收回调,并在结束时释放登录尝试。 */\n async login(interaction) {\n const attempt = await beginPiOAuth(metadata.providerId, {\n ...options,\n signal: interaction.signal,\n });\n interaction.notify({\n type: 'auth_url',\n url: attempt.authorizationUrl,\n instructions: 'Complete login, then paste the complete loopback callback URL here.',\n });\n let callbackUrl: string;\n try {\n callbackUrl = await interaction.prompt({\n type: 'manual_code',\n message: 'Paste the complete loopback callback URL:',\n placeholder: metadata.redirectUri.replace('localhost', '127.0.0.1'),\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n if (interaction.signal.aborted) {\n throw cancellationError();\n }\n throw oauthError(\n 'invalid_callback',\n 'More source OAuth callback input was cancelled.'\n );\n }\n try {\n return await completePiOAuth(attempt.id, callbackUrl, {\n ...options,\n signal: interaction.signal,\n });\n } catch (error) {\n cancelPiOAuth(attempt.id);\n throw error;\n }\n },\n /** 为 Pi 自动续期交换 refresh_token,保留请求的取消信号并更新过期时间。 */\n async refresh(credential, signal) {\n const token = await requestTokens(\n metadata,\n { type: 'refresh_token', refreshToken: credential.refresh },\n signal,\n options\n );\n return credentialFromTokens(metadata, token, getNow(options)());\n },\n /** 把持久化 OAuth 凭证转换为 Pi 请求认证信息,保留服务商需要的账号字段。 */\n async toAuth(credential): Promise {\n if (metadata.providerId === OPENAI_CODEX_PROVIDER_ID) {\n extractOpenAIAccountId(credential.access);\n }\n return { apiKey: credential.access };\n },\n };\n}\n\n/** 按服务商解析浏览器 OAuth 实现,并传入可替换的请求和时钟依赖。 */\nexport function getBrowserOAuthAuth(\n providerId: string,\n options: BrowserOAuthAuthOptions = {}\n): OAuthAuth {\n return createBrowserOAuthAuth(getOAuthMetadata(providerId), options);\n}\n","import { uuidv4 } from '@util/common';\n\nexport const PI_PROMPT_CAPTURE_MODEL_PREFIX = 'mvu-pi-prompt-capture:';\nexport const PI_PROMPT_CAPTURE_API_URL =\n 'https://mvu-pi-prompt-capture.invalid/v1/chat/completions';\n\n/** 一次提示词捕获的诊断快照;只含状态和标记,不暴露完整提示词。 */\nexport type PromptCaptureDiagnostics = {\n generationId: string;\n /** 写入占位请求 model 字段的唯一标记,用于隔离并发生成事件。 */\n marker: string;\n markerMatched: boolean;\n captured: boolean;\n /** 占位生成是否已被成功停止,避免捕获后继续请求酒馆后端。 */\n stopSucceeded: boolean;\n /** 首次失败原因;清理阶段不会覆盖它。 */\n captureError?: string;\n};\n\n/** 从酒馆设置就绪事件复制出的最终消息及其所属生成编号。 */\nexport type CapturedPrompt = {\n generationId: string;\n messages: SillyTavern.SendingMessage[];\n};\n\n/** 捕获完成后的消息、诊断信息和接管回调的返回值。 */\nexport type PromptCaptureResult = PromptCaptureDiagnostics &\n CapturedPrompt & {\n result?: T;\n };\n\n/** 接管捕获结果并接收原生停止事件的回调,使 Pi 请求沿用酒馆的生成生命周期。 */\nexport type PromptCaptureOptions = {\n /** 在提示词就绪监听内等待此回调完成,期间保留酒馆原生停止按钮。 */\n onCaptured: (prompt: CapturedPrompt) => Promise;\n /** 仅转发用户停止当前生成的事件,不包含内部占位请求清理。 */\n onStopped: (generation_id: string) => void;\n};\n\n/** 进行中捕获的内部可变状态,messages 仅在成功复制提示词后存在。 */\ntype MutablePromptCaptureState = PromptCaptureDiagnostics & {\n messages?: SillyTavern.SendingMessage[];\n};\n\n/** 酒馆设置就绪事件中本适配层需要读取的字段,其余请求参数保持透明。 */\ntype SettingsReadyData = {\n messages: SillyTavern.SendingMessage[];\n model: string;\n [key: string]: unknown;\n};\n\n/** generate / generateRaw 的共同调用形状;捕获只依赖事件,不使用它们的返回结果。 */\ntype GenerateRunner = (config: TConfig) => Promise;\n\nconst pending_prompt_captures = new Map();\n\n/** 将非空生成编号编码为专用模型标记,用于从设置就绪事件中识别本次捕获。 */\nexport function encodePromptCaptureMarker(generation_id: string): string {\n if (generation_id.length === 0) {\n throw new Error('Prompt capture generation_id cannot be empty');\n }\n\n try {\n return `${PI_PROMPT_CAPTURE_MODEL_PREFIX}${encodeURIComponent(generation_id)}`;\n } catch (error) {\n throw new Error(`Prompt capture generation_id cannot be encoded: ${generation_id}`, {\n cause: error,\n });\n }\n}\n\n/** 只接受可规范往返编码的捕获标记,普通模型名或畸形标记返回 null。 */\nexport function decodePromptCaptureMarker(model: unknown): string | null {\n if (typeof model !== 'string' || !model.startsWith(PI_PROMPT_CAPTURE_MODEL_PREFIX)) {\n return null;\n }\n\n const encoded_generation_id = model.slice(PI_PROMPT_CAPTURE_MODEL_PREFIX.length);\n if (encoded_generation_id.length === 0) {\n return null;\n }\n\n try {\n const generation_id = decodeURIComponent(encoded_generation_id);\n if (\n generation_id.length === 0 ||\n encodeURIComponent(generation_id) !== encoded_generation_id\n ) {\n return null;\n }\n return generation_id;\n } catch {\n return null;\n }\n}\n\n/**\n * 将生成配置转换为专用捕获请求,移除业务工具、Schema、凭证和自定义覆盖。\n * 请求指向固定无效地址,并应在最终提示词捕获后被停止,避免误发到主模型。\n */\nexport function buildPromptCaptureConfig(\n config: TConfig,\n generation_id: string\n): TConfig {\n const marker = encodePromptCaptureMarker(generation_id);\n\n return {\n ...config,\n generation_id,\n should_stream: false,\n should_silence: config.should_silence ?? true,\n tools: [],\n tool_choice: 'none',\n json_schema: undefined,\n custom_api: {\n source: 'custom',\n apiurl: PI_PROMPT_CAPTURE_API_URL,\n key: '',\n model: marker,\n custom_include_body: {},\n custom_exclude_body: [],\n custom_include_headers: {},\n },\n } as TConfig;\n}\n\n/** 返回进行中捕获的诊断快照,不暴露完整提示词内容。 */\nexport function getPendingPromptCaptureDiagnostics(): PromptCaptureDiagnostics[] {\n return Array.from(pending_prompt_captures.values(), state => ({\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n ...(state.captureError === undefined ? {} : { captureError: state.captureError }),\n }));\n}\n\n/** 只有标记匹配、提示词复制成功且生成已停止,才认定捕获完整成功。 */\nfunction hasCompletedCapture(state: MutablePromptCaptureState): boolean {\n return state.markerMatched && state.captured && state.stopSucceeded;\n}\n\n/** 只记录首次捕获失败原因,避免清理阶段覆盖最有用的错误。 */\nfunction recordCaptureError(state: MutablePromptCaptureState, message: string): void {\n state.captureError ??= message;\n}\n\n/**\n * 在酒馆设置就绪事件的最后阶段复制最终提示词,并以生成编号隔离并发捕获。\n * 可等待 Pi 回调结束以维持原生停止按钮;随后停止占位请求,最终释放全部监听和登记。\n */\nexport async function capturePrompt(\n run: GenerateRunner,\n config: TConfig,\n options?: PromptCaptureOptions\n): Promise> {\n const generation_id = config.generation_id || uuidv4();\n const marker = encodePromptCaptureMarker(generation_id);\n\n if (pending_prompt_captures.has(generation_id)) {\n throw new Error(`Prompt capture generation_id is already pending: ${generation_id}`);\n }\n\n const state: MutablePromptCaptureState = {\n generationId: generation_id,\n marker,\n markerMatched: false,\n captured: false,\n stopSucceeded: false,\n };\n const capture_config = buildPromptCaptureConfig(config, generation_id);\n let captured_result: T | undefined;\n let callback_failed = false;\n let callback_error: unknown;\n let callback_started = false;\n let listening_for_stop = options !== undefined;\n /** 仅将当前生成编号的用户停止事件传给 Pi 回调,不影响其他并发请求。 */\n const stop_listener = (stopped_id?: string) => {\n if (listening_for_stop && stopped_id === generation_id) {\n options?.onStopped(generation_id);\n }\n };\n /** 关闭并移除停止监听,防止内部终止占位请求被误判为用户取消。 */\n const remove_stop_listener = () => {\n listening_for_stop = false;\n if (options) {\n eventRemoveListener(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n };\n\n /** 匹配当前捕获标记后复制最终提示词,等待接管回调,再阻止占位请求进入酒馆后端。 */\n const listener = async (generate_data: SettingsReadyData) => {\n const event_generation_id = decodePromptCaptureMarker(generate_data?.model);\n if (\n event_generation_id !== generation_id ||\n pending_prompt_captures.get(event_generation_id) !== state\n ) {\n return;\n }\n\n state.markerMatched = true;\n if (state.captureError !== undefined || state.captured) {\n return;\n }\n if (!Array.isArray(generate_data.messages)) {\n recordCaptureError(\n state,\n `Prompt capture event has no messages array: ${generation_id}`\n );\n return;\n }\n\n try {\n state.messages = structuredClone(generate_data.messages);\n } catch {\n recordCaptureError(state, `Failed to clone prompt capture messages: ${generation_id}`);\n return;\n }\n state.captured = true;\n if (options) {\n callback_started = true;\n try {\n // Settings-ready listeners are awaited by Slash. Keep generation (and its\n // native Stop button) active until the provider request has settled.\n captured_result = await options.onCaptured({\n generationId: generation_id,\n messages: state.messages,\n });\n } catch (error) {\n callback_failed = true;\n callback_error = error;\n }\n }\n // This stop only prevents the fixed capture request from reaching ST's backend.\n // It must not be mistaken for a user cancellation of the successful Pi request.\n remove_stop_listener();\n try {\n state.stopSucceeded = stopGenerationById(generation_id);\n } catch {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n return;\n }\n if (!state.stopSucceeded) {\n recordCaptureError(state, `Failed to stop prompt capture generation: ${generation_id}`);\n }\n };\n\n let subscription: { stop: () => void } | undefined;\n pending_prompt_captures.set(generation_id, state);\n\n try {\n if (options) {\n eventOn(tavern_events.GENERATION_STOPPED, stop_listener);\n }\n subscription = eventMakeLast(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n\n let runner_failed = false;\n let runner_error: unknown;\n try {\n await run(capture_config);\n } catch (error) {\n runner_failed = true;\n runner_error = error;\n }\n\n if (state.captureError !== undefined) {\n throw new Error(state.captureError);\n }\n if (callback_failed) {\n throw callback_error;\n }\n if (runner_failed && !hasCompletedCapture(state)) {\n throw runner_error;\n }\n\n if (!hasCompletedCapture(state) || state.messages === undefined) {\n throw new Error(`Generation ended without a captured prompt: ${generation_id}`);\n }\n\n return {\n generationId: state.generationId,\n marker: state.marker,\n markerMatched: state.markerMatched,\n captured: state.captured,\n stopSucceeded: state.stopSucceeded,\n messages: state.messages,\n ...(callback_started ? { result: captured_result } : {}),\n };\n } finally {\n remove_stop_listener();\n try {\n subscription?.stop();\n } finally {\n try {\n // Slash-Runner 4.9.1's EventOnReturn.stop() cannot reliably remove\n // its wrapped listener, so always remove the original listener too.\n eventRemoveListener(tavern_events.CHAT_COMPLETION_SETTINGS_READY, listener);\n } finally {\n if (pending_prompt_captures.get(generation_id) === state) {\n pending_prompt_captures.delete(generation_id);\n }\n }\n }\n }\n}\n\n/** 使用当前预设的 generate 流程构造并捕获最终提示词。 */\nexport function captureGeneratePrompt(\n config: GenerateConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generate, config, options);\n}\n\n/** 使用显式顺序的 generateRaw 流程构造并捕获最终提示词。 */\nexport function captureGenerateRawPrompt(\n config: GenerateRawConfig,\n options?: PromptCaptureOptions\n): Promise> {\n return capturePrompt(generateRaw, config, options);\n}\n","import { resolvePiThinkingLevel } from './thinking';\nimport { isPiThinkingEnabled } from './thinking_setting';\nimport {\n ANT_LING_MODELS,\n ANTHROPIC_MODELS,\n BASETEN_MODELS,\n CEREBRAS_MODELS,\n DEEPSEEK_MODELS,\n FIREWORKS_MODELS,\n GITHUB_COPILOT_MODELS,\n GOOGLE_MODELS,\n GROQ_MODELS,\n HUGGINGFACE_MODELS,\n KIMI_CODING_MODELS,\n MINIMAX_CN_MODELS,\n MINIMAX_MODELS,\n MISTRAL_MODELS,\n MOONSHOTAI_CN_MODELS,\n MOONSHOTAI_MODELS,\n NVIDIA_MODELS,\n OPENAI_CODEX_MODELS,\n OPENAI_MODELS,\n OPENCODE_GO_MODELS,\n OPENCODE_MODELS,\n OPENROUTER_MODELS,\n QWEN_TOKEN_PLAN_CN_MODELS,\n QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n QWEN_TOKEN_PLAN_MODELS,\n TOGETHER_MODELS,\n VERCEL_AI_GATEWAY_MODELS,\n XAI_MODELS,\n XIAOMI_MODELS,\n XIAOMI_TOKEN_PLAN_AMS_MODELS,\n XIAOMI_TOKEN_PLAN_CN_MODELS,\n XIAOMI_TOKEN_PLAN_SGP_MODELS,\n ZAI_CODING_CN_MODELS,\n ZAI_MODELS,\n anthropicMessagesApi,\n googleGenerativeAIApi,\n mistralConversationsApi,\n openAICodexResponsesApi,\n openAICompletionsApi,\n openAIResponsesApi,\n type Api,\n type Model,\n type ProviderStreams,\n} from './pi_gateway';\nimport { createGoogleProxyAwareApi } from './google_proxy_adapter';\nimport {\n getPiProviderApiBaseUrl,\n PI_PROVIDER_KEYS,\n PI_PROVIDER_TARGET_REGISTRY,\n type PiAuthType,\n type PiProviderKey,\n type PiWireApi,\n} from './provider_target';\n\nexport {\n isPiCorsProxyRequired,\n PI_AUTH_TYPES,\n PI_PROVIDER_KEYS,\n PI_WIRE_APIS,\n shouldUsePiCorsProxy,\n} from './provider_target';\nexport type { PiAuthType, PiProviderKey, PiWireApi } from './provider_target';\n\n/**\n * Codex protocol level implemented by the pinned pi-ai adapter/catalog. Keep this tied to an\n * adapter upgrade rather than MVU's unrelated application version: the models endpoint uses it to\n * hide entries whose request shape the active adapter cannot yet support.\n */\nexport const OPENAI_CODEX_ADAPTER_CLIENT_VERSION = '0.144.0';\n\n/** 来源面板单个配置字段的呈现与编辑方式。 */\nexport type PiFieldMode = 'hidden' | 'readonly' | 'select' | 'editable';\n\n/** 服务商对应的面板字段规则,认证相关字段按所选认证方式显示。 */\nexport interface PiProviderFieldVisibility {\n api: PiFieldMode;\n authType: PiFieldMode;\n endpoint: PiFieldMode;\n apiKey: 'hidden' | 'when-api-key';\n oauth: 'hidden' | 'when-oauth';\n model: 'editable';\n contextWindow: 'editable';\n}\n\n/** 协议与模型组合可用的请求能力,供面板显隐和发送前参数校验共用。 */\nexport interface PiApiCapabilities {\n /** 是否支持 MVU 当前采用的 Pi 流式执行链路。 */\n streaming: boolean;\n tools: boolean;\n imageInput: boolean;\n /** 是否支持服务商原生 JSON Schema 输出约束。 */\n structuredOutput: boolean;\n /** 是否支持不指定 Schema 的原生 JSON 对象输出。 */\n jsonObjectOutput: boolean;\n temperature: boolean;\n /** temperature 可配置的闭区间,仅在支持该参数时生效。 */\n temperatureRange: readonly [minimum: number, maximum: number];\n /** 可直接传给当前协议的采样参数;未支持的配置在预检时拒绝。 */\n sampling: Readonly<{\n topP: boolean;\n topK: boolean;\n frequencyPenalty: boolean;\n presencePenalty: boolean;\n }>;\n}\n\n/** 令牌端点要求的请求正文编码:JSON 或 URL 编码表单。 */\nexport type PiOAuthExchangeKind = 'json' | 'form';\n\n/**\n * 来源面板与浏览器 OAuth 流程共享的公共客户端配置。\n * 每次登录独有的 PKCE verifier 和 state 保存在尝试状态中,不属于注册信息。\n */\nexport interface PiOAuthDefinition {\n providerId: PiProviderKey;\n api: PiWireApi;\n clientId: string;\n authorizeUrl: string;\n tokenUrl: string;\n redirectUri: string;\n /** 粘贴回调允许使用的回环主机名,端口和路径仍需与 redirectUri 匹配。 */\n allowedCallbackHosts: readonly ('localhost' | '127.0.0.1')[];\n scope: string;\n exchangeKind: PiOAuthExchangeKind;\n authorizeParams: Readonly>;\n tokenParams: Readonly>;\n /** 部分服务商要求交换授权码时同时提交本次 state。 */\n includeStateInTokenRequest: boolean;\n /** 持久化时从服务商有效期扣除的毫秒数,使刷新提前于实际过期。 */\n expirySkewMs: number;\n}\n\n/** 完整服务商注册信息,在轻量连接规则上加入能力表、界面配置和 OAuth 元数据。 */\nexport interface PiProviderDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n displayName: Readonly<{\n 'zh-CN': string;\n en: string;\n }>;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须经酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n /** 按协议登记的能力基线,查询具体模型时还会应用模型限制。 */\n apiCapabilities: Readonly>>>;\n fields: Readonly;\n oauth?: Readonly;\n}\n\n/** 按模型目录键索引的只读 Pi 元数据,不可直接修改为本次请求的配置。 */\ntype Catalog = Readonly>>;\n/** 创建协议流适配器的工厂,供 Provider 构建时选择实际协议实现。 */\ntype ApiLoader = () => ProviderStreams;\n\nconst CATALOGS: Readonly> = {\n 'ant-ling': ANT_LING_MODELS,\n anthropic: ANTHROPIC_MODELS,\n baseten: BASETEN_MODELS,\n cerebras: CEREBRAS_MODELS,\n deepseek: DEEPSEEK_MODELS,\n fireworks: FIREWORKS_MODELS,\n 'github-copilot': GITHUB_COPILOT_MODELS,\n google: GOOGLE_MODELS,\n groq: GROQ_MODELS,\n huggingface: HUGGINGFACE_MODELS,\n 'kimi-coding': KIMI_CODING_MODELS,\n minimax: MINIMAX_MODELS,\n 'minimax-cn': MINIMAX_CN_MODELS,\n mistral: MISTRAL_MODELS,\n moonshotai: MOONSHOTAI_MODELS,\n 'moonshotai-cn': MOONSHOTAI_CN_MODELS,\n nvidia: NVIDIA_MODELS,\n openai: OPENAI_MODELS,\n 'openai-codex': OPENAI_CODEX_MODELS,\n opencode: OPENCODE_MODELS,\n 'opencode-go': OPENCODE_GO_MODELS,\n openrouter: OPENROUTER_MODELS,\n 'qwen-token-plan': QWEN_TOKEN_PLAN_MODELS,\n 'qwen-token-plan-cn': QWEN_TOKEN_PLAN_CN_MODELS,\n 'qwen-token-plan-individual': QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS,\n together: TOGETHER_MODELS,\n 'vercel-ai-gateway': VERCEL_AI_GATEWAY_MODELS,\n xai: XAI_MODELS,\n xiaomi: XIAOMI_MODELS,\n 'xiaomi-token-plan-ams': XIAOMI_TOKEN_PLAN_AMS_MODELS,\n 'xiaomi-token-plan-cn': XIAOMI_TOKEN_PLAN_CN_MODELS,\n 'xiaomi-token-plan-sgp': XIAOMI_TOKEN_PLAN_SGP_MODELS,\n zai: ZAI_MODELS,\n 'zai-coding-cn': ZAI_CODING_CN_MODELS,\n};\n\n/** Provider catalog entries that are known to have been shut down upstream. */\nconst GOOGLE_RETIRED_MODEL_IDS: ReadonlySet = new Set(['gemini-3.1-flash-lite-preview']);\n\nconst API_LOADERS: Readonly> = {\n 'openai-responses': openAIResponsesApi,\n 'openai-completions': openAICompletionsApi,\n 'openai-codex-responses': openAICodexResponsesApi,\n 'anthropic-messages': anthropicMessagesApi,\n 'google-generative-ai': () => createGoogleProxyAwareApi(googleGenerativeAIApi()),\n 'mistral-conversations': mistralConversationsApi,\n};\n\nconst TOP_P_ONLY = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TOP_P_TOP_K = Object.freeze({\n topP: true,\n topK: true,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst OPENAI_COMPLETIONS_SAMPLING = Object.freeze({\n topP: true,\n topK: false,\n frequencyPenalty: true,\n presencePenalty: true,\n});\n\nconst NO_SAMPLING = Object.freeze({\n topP: false,\n topK: false,\n frequencyPenalty: false,\n presencePenalty: false,\n});\n\nconst TEMPERATURE_ZERO_TO_TWO = Object.freeze([0, 2] as const);\nconst TEMPERATURE_ZERO_TO_ONE = Object.freeze([0, 1] as const);\n\nconst OPENAI_RESPONSES_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_ONLY,\n});\n\nconst OPENAI_COMPLETIONS_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst CODEX_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: false,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: NO_SAMPLING,\n});\n\nconst ANTHROPIC_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: false,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: TOP_P_TOP_K,\n});\n\nconst GOOGLE_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_TWO,\n sampling: TOP_P_TOP_K,\n});\n\nconst MISTRAL_CAPABILITIES: Readonly = Object.freeze({\n streaming: true,\n tools: true,\n imageInput: true,\n structuredOutput: true,\n jsonObjectOutput: true,\n temperature: true,\n temperatureRange: TEMPERATURE_ZERO_TO_ONE,\n sampling: OPENAI_COMPLETIONS_SAMPLING,\n});\n\nconst API_CAPABILITIES: Readonly>> = Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n 'openai-codex-responses': CODEX_CAPABILITIES,\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n 'mistral-conversations': MISTRAL_CAPABILITIES,\n});\n\nconst ANTHROPIC_OAUTH: Readonly = Object.freeze({\n providerId: 'anthropic',\n api: 'anthropic-messages',\n clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',\n authorizeUrl: 'https://claude.ai/oauth/authorize',\n tokenUrl: 'https://platform.claude.com/v1/oauth/token',\n redirectUri: 'http://localhost:53692/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',\n exchangeKind: 'json',\n authorizeParams: Object.freeze({ code: 'true' }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: true,\n expirySkewMs: 5 * 60 * 1000,\n});\n\nconst OPENAI_CODEX_OAUTH: Readonly = Object.freeze({\n providerId: 'openai-codex',\n api: 'openai-codex-responses',\n clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',\n authorizeUrl: 'https://auth.openai.com/oauth/authorize',\n tokenUrl: 'https://auth.openai.com/oauth/token',\n redirectUri: 'http://localhost:1455/auth/callback',\n allowedCallbackHosts: Object.freeze(['localhost', '127.0.0.1'] as const),\n scope: 'openid profile email offline_access',\n exchangeKind: 'form',\n authorizeParams: Object.freeze({\n id_token_add_organizations: 'true',\n codex_cli_simplified_flow: 'true',\n originator: 'pi',\n }),\n tokenParams: Object.freeze({}),\n includeStateInTokenRequest: false,\n expirySkewMs: 0,\n});\n\nconst EDITABLE_OPENAI_FIELDS: Readonly = Object.freeze({\n api: 'select',\n authType: 'readonly',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst OAUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'hidden',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MIXED_AUTH_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'select',\n endpoint: 'editable',\n apiKey: 'when-api-key',\n oauth: 'when-oauth',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst API_KEY_FIELDS: Readonly = Object.freeze({\n api: 'readonly',\n authType: 'readonly',\n endpoint: 'hidden',\n apiKey: 'when-api-key',\n oauth: 'hidden',\n model: 'editable',\n contextWindow: 'editable',\n});\n\nconst MULTI_API_KEY_FIELDS: Readonly = Object.freeze({\n ...API_KEY_FIELDS,\n api: 'select',\n});\n\n/** 从轻量目标定义构造固定 API Key 服务商,补充展示信息、目录和协议能力。 */\nfunction fixedApiKeyProvider(\n key: PiProviderKey,\n name: string,\n chineseName = name\n): Readonly {\n const target = PI_PROVIDER_TARGET_REGISTRY[key];\n const apiCapabilities = Object.freeze(\n Object.fromEntries(target.allowedApis.map(api => [api, API_CAPABILITIES[api]]))\n ) as Readonly>>>;\n return Object.freeze({\n ...target,\n displayName: Object.freeze({ 'zh-CN': chineseName, en: name }),\n apiCapabilities,\n fields: target.allowedApis.length > 1 ? MULTI_API_KEY_FIELDS : API_KEY_FIELDS,\n });\n}\n\nexport const PI_PROVIDER_REGISTRY: Readonly> =\n Object.freeze({\n 'ant-ling': fixedApiKeyProvider('ant-ling', 'Ant Ling'),\n anthropic: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.anthropic,\n displayName: Object.freeze({ 'zh-CN': 'Anthropic', en: 'Anthropic' }),\n apiCapabilities: Object.freeze({\n 'anthropic-messages': ANTHROPIC_CAPABILITIES,\n }),\n fields: MIXED_AUTH_FIELDS,\n oauth: ANTHROPIC_OAUTH,\n }),\n baseten: fixedApiKeyProvider('baseten', 'Baseten'),\n cerebras: fixedApiKeyProvider('cerebras', 'Cerebras'),\n deepseek: fixedApiKeyProvider('deepseek', 'DeepSeek'),\n fireworks: fixedApiKeyProvider('fireworks', 'Fireworks'),\n 'github-copilot': fixedApiKeyProvider('github-copilot', 'GitHub Copilot'),\n google: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.google,\n displayName: Object.freeze({ 'zh-CN': 'Google Gemini', en: 'Google Gemini' }),\n apiCapabilities: Object.freeze({\n 'google-generative-ai': GOOGLE_CAPABILITIES,\n }),\n fields: API_KEY_FIELDS,\n }),\n groq: fixedApiKeyProvider('groq', 'Groq'),\n huggingface: fixedApiKeyProvider('huggingface', 'Hugging Face'),\n 'kimi-coding': fixedApiKeyProvider('kimi-coding', 'Kimi For Coding'),\n minimax: fixedApiKeyProvider('minimax', 'MiniMax'),\n 'minimax-cn': fixedApiKeyProvider('minimax-cn', 'MiniMax CN', 'MiniMax(中国)'),\n mistral: fixedApiKeyProvider('mistral', 'Mistral'),\n moonshotai: fixedApiKeyProvider('moonshotai', 'Moonshot AI'),\n 'moonshotai-cn': fixedApiKeyProvider(\n 'moonshotai-cn',\n 'Moonshot AI CN',\n 'Moonshot AI(中国)'\n ),\n nvidia: fixedApiKeyProvider('nvidia', 'NVIDIA'),\n openai: Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY.openai,\n displayName: Object.freeze({ 'zh-CN': 'OpenAI', en: 'OpenAI' }),\n apiCapabilities: Object.freeze({\n 'openai-responses': OPENAI_RESPONSES_CAPABILITIES,\n 'openai-completions': OPENAI_COMPLETIONS_CAPABILITIES,\n }),\n fields: EDITABLE_OPENAI_FIELDS,\n }),\n 'openai-codex': Object.freeze({\n ...PI_PROVIDER_TARGET_REGISTRY['openai-codex'],\n displayName: Object.freeze({ 'zh-CN': 'OpenAI Codex', en: 'OpenAI Codex' }),\n apiCapabilities: Object.freeze({\n 'openai-codex-responses': CODEX_CAPABILITIES,\n }),\n fields: OAUTH_FIELDS,\n oauth: OPENAI_CODEX_OAUTH,\n }),\n opencode: fixedApiKeyProvider('opencode', 'OpenCode Zen'),\n 'opencode-go': fixedApiKeyProvider('opencode-go', 'OpenCode Go'),\n openrouter: fixedApiKeyProvider('openrouter', 'OpenRouter'),\n 'qwen-token-plan': fixedApiKeyProvider('qwen-token-plan', 'Qwen Token Plan'),\n 'qwen-token-plan-cn': fixedApiKeyProvider(\n 'qwen-token-plan-cn',\n 'Qwen Token Plan CN',\n 'Qwen Token Plan(中国)'\n ),\n 'qwen-token-plan-individual': fixedApiKeyProvider(\n 'qwen-token-plan-individual',\n 'Qwen Token Plan Individual'\n ),\n together: fixedApiKeyProvider('together', 'Together'),\n 'vercel-ai-gateway': fixedApiKeyProvider('vercel-ai-gateway', 'Vercel AI Gateway'),\n xai: fixedApiKeyProvider('xai', 'xAI'),\n xiaomi: fixedApiKeyProvider('xiaomi', 'Xiaomi'),\n 'xiaomi-token-plan-ams': fixedApiKeyProvider(\n 'xiaomi-token-plan-ams',\n 'Xiaomi Token Plan AMS'\n ),\n 'xiaomi-token-plan-cn': fixedApiKeyProvider(\n 'xiaomi-token-plan-cn',\n 'Xiaomi Token Plan CN',\n 'Xiaomi Token Plan(中国)'\n ),\n 'xiaomi-token-plan-sgp': fixedApiKeyProvider(\n 'xiaomi-token-plan-sgp',\n 'Xiaomi Token Plan SGP'\n ),\n zai: fixedApiKeyProvider('zai', 'Z.AI'),\n 'zai-coding-cn': fixedApiKeyProvider(\n 'zai-coding-cn',\n 'Z.AI Coding CN',\n 'Z.AI Coding(中国)'\n ),\n } satisfies Record);\n\n/** 按注册顺序返回支持的服务商定义,供来源界面生成选项。 */\nexport function listPiProviderDefinitions(): readonly PiProviderDefinition[] {\n return PI_PROVIDER_KEYS.map(key => PI_PROVIDER_REGISTRY[key]);\n}\n\n/** 按注册键读取服务商定义,拒绝命中对象原型上的同名属性。 */\nexport function getPiProviderDefinition(key: string): PiProviderDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_REGISTRY, key)\n ? PI_PROVIDER_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** Compatibility spelling used by the OAuth/UI integration. */\nexport const getPiProviderRegistration = getPiProviderDefinition;\n\n/** 取得服务商的固定模型目录,未知服务商返回空列表。 */\nexport function getPiCatalogModels(key: PiProviderKey | string): readonly Model[] {\n const definition = getPiProviderDefinition(key);\n if (!definition) {\n return [];\n }\n const models = Object.values(CATALOGS[definition.key]);\n return definition.key === 'google'\n ? models.filter(model => !GOOGLE_RETIRED_MODEL_IDS.has(model.id))\n : models;\n}\n\n/** 综合模型元数据和已知模型限制判断是否可发送 temperature。 */\nfunction modelSupportsTemperature(model: Model | undefined): boolean {\n const compat = model?.compat;\n return (\n compat === undefined ||\n !('supportsTemperature' in compat) ||\n compat.supportsTemperature !== false\n );\n}\n\n/**\n * Anthropic rejects every sampling control (temperature, top_p and top_k) for these catalog\n * models. Keep the explicit IDs alongside the provider registry so a missing compat flag cannot\n * accidentally make a newly published catalog entry permissive.\n */\nconst ANTHROPIC_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'claude-fable-5',\n 'claude-opus-4-7',\n 'claude-opus-4-8',\n 'claude-opus-5',\n 'claude-sonnet-5',\n]);\n\n/** Catalog models explicitly available through OpenAI Chat Completions as well as Responses. */\nconst OPENAI_CHAT_COMPLETIONS_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-4',\n 'gpt-4-turbo',\n 'gpt-4.1',\n 'gpt-4.1-mini',\n 'gpt-4.1-nano',\n 'gpt-4o',\n 'gpt-4o-2024-05-13',\n 'gpt-4o-2024-08-06',\n 'gpt-4o-2024-11-20',\n 'gpt-4o-mini',\n 'gpt-5',\n 'gpt-5-chat-latest',\n 'gpt-5-mini',\n 'gpt-5-nano',\n 'gpt-5.1',\n 'gpt-5.2',\n 'gpt-5.2-chat-latest',\n 'gpt-5.3-chat-latest',\n 'gpt-5.4',\n 'gpt-5.4-mini',\n 'gpt-5.4-nano',\n 'gpt-5.5',\n 'gpt-5.6-luna',\n 'gpt-5.6-sol',\n 'gpt-5.6-terra',\n 'o1',\n 'o3',\n 'o3-mini',\n 'o4-mini',\n]);\n\nconst OPENAI_NON_STREAMING_MODEL_IDS: ReadonlySet = new Set([\n 'gpt-5.5-pro',\n 'gpt-realtime-2.1',\n 'o1-pro',\n 'o3-pro',\n]);\n\nconst GOOGLE_NO_SAMPLING_MODEL_IDS: ReadonlySet = new Set([\n 'gemini-3.6-flash',\n 'gemini-3.7-flash',\n]);\n\n/** 验证固定目录模型是否适用于当前服务商协议,避免借用另一协议的能力元数据。 */\nexport function isPiCatalogModelApiCompatible(\n definition: PiProviderDefinition,\n model: Model,\n api: PiWireApi\n): boolean {\n if (model.api === api) {\n return true;\n }\n return (\n definition.key === 'openai' &&\n model.api === 'openai-responses' &&\n api === 'openai-completions' &&\n OPENAI_CHAT_COMPLETIONS_MODEL_IDS.has(model.id)\n );\n}\n\n/**\n * 重新查找并校验目录项,不直接信任调用方的 catalogHit 标记。\n * 只有确认匹配的元数据才能影响图片、流式和采样能力。\n */\nfunction verifiedCatalogModel(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean }\n): Model | undefined {\n const model = options.model;\n if (!options.catalogHit || !model) {\n return undefined;\n }\n\n const catalogModel = getPiCatalogModels(definition.key).find(\n candidate => candidate.id === model.id\n );\n if (\n !catalogModel ||\n catalogModel.provider !== definition.providerId ||\n model.provider !== definition.providerId ||\n !isPiCatalogModelApiCompatible(definition, catalogModel, api) ||\n (model.api !== api && model.api !== catalogModel.api) ||\n catalogModel.baseUrl !== getPiProviderApiBaseUrl(definition, api) ||\n model.baseUrl !== getPiProviderApiBaseUrl(definition, api)\n ) {\n return undefined;\n }\n\n return catalogModel;\n}\n\n/** 将服务商协议能力与可信目录模型限制合并,给出当前请求可用的能力。 */\nexport function resolvePiCapabilities(\n definition: PiProviderDefinition,\n api: PiWireApi,\n options: { model?: Model; catalogHit: boolean; thinkingLevel?: string }\n): Readonly | undefined {\n const registered = definition.apiCapabilities[api];\n if (!registered) {\n return undefined;\n }\n\n const catalogModel = verifiedCatalogModel(definition, api, options);\n const samplingModel = catalogModel ?? options.model;\n const thinkingEnabled = isPiThinkingEnabled(\n resolvePiThinkingLevel(options.thinkingLevel, catalogModel)\n );\n const anthropicSamplingAllowed =\n definition.key !== 'anthropic' ||\n catalogModel === undefined ||\n (samplingModel !== undefined &&\n modelSupportsTemperature(samplingModel) &&\n !ANTHROPIC_NO_SAMPLING_MODEL_IDS.has(samplingModel.id));\n // pi's Responses adapter selects `reasoning.effort: none` when the catalog explicitly\n // exposes it. Older GPT-5/o-series/pro models cannot turn reasoning off and reject\n // temperature/top_p. Dynamic OpenAI-compatible models remain API-level configurable.\n const openAISamplingAllowed =\n definition.key !== 'openai' ||\n !catalogModel?.reasoning ||\n catalogModel.thinkingLevelMap?.off === 'none';\n const googleSamplingAllowed =\n definition.key !== 'google' ||\n !catalogModel ||\n !GOOGLE_NO_SAMPLING_MODEL_IDS.has(catalogModel.id);\n const samplingAllowed =\n anthropicSamplingAllowed &&\n openAISamplingAllowed &&\n googleSamplingAllowed &&\n !(thinkingEnabled && (api === 'anthropic-messages' || definition.key === 'openai'));\n const streamingAllowed =\n definition.key !== 'openai' ||\n !catalogModel ||\n !OPENAI_NON_STREAMING_MODEL_IDS.has(catalogModel.id);\n\n return Object.freeze({\n ...registered,\n // These are adapter-level promises. Any narrower endpoint/model support is reported by\n // the provider request itself instead of being guessed from a pinned catalog.\n streaming: registered.streaming && streamingAllowed,\n tools: registered.tools,\n structuredOutput: registered.structuredOutput,\n jsonObjectOutput: registered.jsonObjectOutput,\n imageInput: registered.imageInput && catalogModel?.input.includes('image') === true,\n temperature:\n registered.temperature && modelSupportsTemperature(samplingModel) && samplingAllowed,\n sampling: samplingAllowed ? registered.sampling : NO_SAMPLING,\n });\n}\n\n/** 仅创建当前来源明确允许的协议懒加载适配器,避免引入无关服务商实现。 */\nexport function createPiApiImplementations(\n definitionOrKey: PiProviderDefinition | PiProviderKey | string\n): Partial> {\n const definition =\n typeof definitionOrKey === 'string'\n ? getPiProviderDefinition(definitionOrKey)\n : definitionOrKey;\n if (!definition) {\n return {};\n }\n\n return Object.fromEntries(\n definition.allowedApis.map(api => [api, API_LOADERS[api]()])\n ) as Partial>;\n}\n","export const PI_PROVIDER_KEYS = [\n 'ant-ling',\n 'anthropic',\n 'baseten',\n 'cerebras',\n 'deepseek',\n 'fireworks',\n 'github-copilot',\n 'google',\n 'groq',\n 'huggingface',\n 'kimi-coding',\n 'minimax',\n 'minimax-cn',\n 'mistral',\n 'moonshotai',\n 'moonshotai-cn',\n 'nvidia',\n 'openai',\n 'openai-codex',\n 'opencode',\n 'opencode-go',\n 'openrouter',\n 'qwen-token-plan',\n 'qwen-token-plan-cn',\n 'qwen-token-plan-individual',\n 'together',\n 'vercel-ai-gateway',\n 'xai',\n 'xiaomi',\n 'xiaomi-token-plan-ams',\n 'xiaomi-token-plan-cn',\n 'xiaomi-token-plan-sgp',\n 'zai',\n 'zai-coding-cn',\n] as const;\n/** MVU 当前注册的服务商标识,用于设置、连接规则与 Pi 模型目录之间的关联。 */\nexport type PiProviderKey = (typeof PI_PROVIDER_KEYS)[number];\n\nexport const PI_WIRE_APIS = [\n 'openai-responses',\n 'openai-completions',\n 'openai-codex-responses',\n 'anthropic-messages',\n 'google-generative-ai',\n 'mistral-conversations',\n] as const;\n/** 实际发送请求所用的协议,与服务商名称分开选择。 */\nexport type PiWireApi = (typeof PI_WIRE_APIS)[number];\n\n/** 标记必须使用流式传输的 Codex 账号接口,该接口没有 JSON 非流式模式。 */\nexport function isPiStreamingRequired(api: string): boolean {\n return api === 'openai-codex-responses';\n}\n\nexport const PI_AUTH_TYPES = ['api_key', 'oauth'] as const;\n/** Pi 连接的凭证来源:手填 API Key 或浏览器 OAuth 登录凭证。 */\nexport type PiAuthType = (typeof PI_AUTH_TYPES)[number];\n\n/** 不依赖 SDK 的连接规则,供设置迁移、凭证目标隔离和运行时校验共用。 */\nexport interface PiProviderTargetDefinition {\n key: PiProviderKey;\n providerId: PiProviderKey;\n defaultApi: PiWireApi;\n allowedApis: readonly PiWireApi[];\n defaultAuthType: PiAuthType;\n allowedAuthTypes: readonly PiAuthType[];\n defaultBaseUrl: string;\n /** 同一服务商不同协议的专用地址;缺省时使用 defaultBaseUrl。 */\n apiBaseUrls?: Readonly>>;\n /** 内置端点在浏览器中必须通过酒馆 CORS 代理访问的协议。 */\n readonly corsProxyRequiredApis: readonly PiWireApi[];\n allowCustomEndpoint: boolean;\n}\n\n/** 注册连接规则时允许省略代理列表,由定义函数补齐并冻结为空数组。 */\ntype PiProviderTargetDefinitionInput = Omit & {\n readonly corsProxyRequiredApis?: readonly PiWireApi[];\n};\n\nconst NO_CORS_PROXY_REQUIRED_APIS: readonly PiWireApi[] = Object.freeze([]);\n\n/** 补齐并冻结目标的代理策略,使共享注册信息不能被请求修改。 */\nfunction definePiProviderTarget(\n input: PiProviderTargetDefinitionInput\n): Readonly {\n return Object.freeze({\n ...input,\n corsProxyRequiredApis:\n input.corsProxyRequiredApis === undefined\n ? NO_CORS_PROXY_REQUIRED_APIS\n : Object.freeze([...input.corsProxyRequiredApis]),\n });\n}\n\n/**\n * Lightweight source of truth for provider/API/auth/endpoint compatibility. Keeping this separate\n * from catalogs and adapters lets settings migration validate credential targets without loading\n * the provider SDK boundary.\n */\nexport const PI_PROVIDER_TARGET_REGISTRY: Readonly<\n Record>\n> = Object.freeze({\n 'ant-ling': definePiProviderTarget({\n key: 'ant-ling',\n providerId: 'ant-ling',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.ant-ling.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n anthropic: definePiProviderTarget({\n key: 'anthropic',\n providerId: 'anthropic',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key', 'oauth'] as const),\n defaultBaseUrl: 'https://api.anthropic.com',\n allowCustomEndpoint: true,\n }),\n baseten: definePiProviderTarget({\n key: 'baseten',\n providerId: 'baseten',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://inference.baseten.co/v1',\n allowCustomEndpoint: false,\n }),\n cerebras: definePiProviderTarget({\n key: 'cerebras',\n providerId: 'cerebras',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.cerebras.ai/v1',\n allowCustomEndpoint: false,\n }),\n deepseek: definePiProviderTarget({\n key: 'deepseek',\n providerId: 'deepseek',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.deepseek.com',\n allowCustomEndpoint: false,\n }),\n fireworks: definePiProviderTarget({\n key: 'fireworks',\n providerId: 'fireworks',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.fireworks.ai/inference',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://api.fireworks.ai/inference',\n 'openai-completions': 'https://api.fireworks.ai/inference/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n 'github-copilot': definePiProviderTarget({\n key: 'github-copilot',\n providerId: 'github-copilot',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.individual.githubcopilot.com',\n corsProxyRequiredApis: ['anthropic-messages', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n google: definePiProviderTarget({\n key: 'google',\n providerId: 'google',\n defaultApi: 'google-generative-ai',\n allowedApis: Object.freeze(['google-generative-ai'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n allowCustomEndpoint: false,\n }),\n groq: definePiProviderTarget({\n key: 'groq',\n providerId: 'groq',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.groq.com/openai/v1',\n allowCustomEndpoint: false,\n }),\n huggingface: definePiProviderTarget({\n key: 'huggingface',\n providerId: 'huggingface',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://router.huggingface.co/v1',\n allowCustomEndpoint: false,\n }),\n 'kimi-coding': definePiProviderTarget({\n key: 'kimi-coding',\n providerId: 'kimi-coding',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.kimi.com/coding',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n minimax: definePiProviderTarget({\n key: 'minimax',\n providerId: 'minimax',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimax.io/anthropic',\n allowCustomEndpoint: false,\n }),\n 'minimax-cn': definePiProviderTarget({\n key: 'minimax-cn',\n providerId: 'minimax-cn',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.minimaxi.com/anthropic',\n corsProxyRequiredApis: ['anthropic-messages'],\n allowCustomEndpoint: false,\n }),\n mistral: definePiProviderTarget({\n key: 'mistral',\n providerId: 'mistral',\n defaultApi: 'mistral-conversations',\n allowedApis: Object.freeze(['mistral-conversations'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.mistral.ai',\n allowCustomEndpoint: false,\n }),\n moonshotai: definePiProviderTarget({\n key: 'moonshotai',\n providerId: 'moonshotai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'moonshotai-cn': definePiProviderTarget({\n key: 'moonshotai-cn',\n providerId: 'moonshotai-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.moonshot.cn/v1',\n allowCustomEndpoint: false,\n }),\n nvidia: definePiProviderTarget({\n key: 'nvidia',\n providerId: 'nvidia',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://integrate.api.nvidia.com/v1',\n corsProxyRequiredApis: ['openai-completions'],\n allowCustomEndpoint: false,\n }),\n openai: definePiProviderTarget({\n key: 'openai',\n providerId: 'openai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses', 'openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.openai.com/v1',\n allowCustomEndpoint: true,\n }),\n 'openai-codex': definePiProviderTarget({\n key: 'openai-codex',\n providerId: 'openai-codex',\n defaultApi: 'openai-codex-responses',\n allowedApis: Object.freeze(['openai-codex-responses'] as const),\n defaultAuthType: 'oauth',\n allowedAuthTypes: Object.freeze(['oauth'] as const),\n defaultBaseUrl: 'https://chatgpt.com/backend-api',\n corsProxyRequiredApis: ['openai-codex-responses'],\n allowCustomEndpoint: false,\n }),\n opencode: definePiProviderTarget({\n key: 'opencode',\n providerId: 'opencode',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/zen',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/zen',\n 'google-generative-ai': 'https://opencode.ai/zen/v1',\n 'openai-completions': 'https://opencode.ai/zen/v1',\n 'openai-responses': 'https://opencode.ai/zen/v1',\n }),\n corsProxyRequiredApis: [\n 'anthropic-messages',\n 'google-generative-ai',\n 'openai-completions',\n 'openai-responses',\n ],\n allowCustomEndpoint: false,\n }),\n 'opencode-go': definePiProviderTarget({\n key: 'opencode-go',\n providerId: 'opencode-go',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze([\n 'anthropic-messages',\n 'openai-completions',\n 'openai-responses',\n ] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://opencode.ai/zen/go',\n apiBaseUrls: Object.freeze({\n 'anthropic-messages': 'https://opencode.ai/zen/go',\n 'openai-completions': 'https://opencode.ai/zen/go/v1',\n 'openai-responses': 'https://opencode.ai/zen/go/v1',\n }),\n corsProxyRequiredApis: ['anthropic-messages', 'openai-completions', 'openai-responses'],\n allowCustomEndpoint: false,\n }),\n openrouter: definePiProviderTarget({\n key: 'openrouter',\n providerId: 'openrouter',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions', 'anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://openrouter.ai/api/v1',\n apiBaseUrls: Object.freeze({\n 'openai-completions': 'https://openrouter.ai/api/v1',\n 'anthropic-messages': 'https://openrouter.ai/api',\n }),\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan': definePiProviderTarget({\n key: 'qwen-token-plan',\n providerId: 'qwen-token-plan',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-cn': definePiProviderTarget({\n key: 'qwen-token-plan-cn',\n providerId: 'qwen-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n 'qwen-token-plan-individual': definePiProviderTarget({\n key: 'qwen-token-plan-individual',\n providerId: 'qwen-token-plan-individual',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1',\n allowCustomEndpoint: false,\n }),\n together: definePiProviderTarget({\n key: 'together',\n providerId: 'together',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.together.ai/v1',\n allowCustomEndpoint: false,\n }),\n 'vercel-ai-gateway': definePiProviderTarget({\n key: 'vercel-ai-gateway',\n providerId: 'vercel-ai-gateway',\n defaultApi: 'anthropic-messages',\n allowedApis: Object.freeze(['anthropic-messages'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://ai-gateway.vercel.sh',\n allowCustomEndpoint: false,\n }),\n xai: definePiProviderTarget({\n key: 'xai',\n providerId: 'xai',\n defaultApi: 'openai-responses',\n allowedApis: Object.freeze(['openai-responses'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.x.ai/v1',\n allowCustomEndpoint: false,\n }),\n xiaomi: definePiProviderTarget({\n key: 'xiaomi',\n providerId: 'xiaomi',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-ams': definePiProviderTarget({\n key: 'xiaomi-token-plan-ams',\n providerId: 'xiaomi-token-plan-ams',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-ams.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-cn': definePiProviderTarget({\n key: 'xiaomi-token-plan-cn',\n providerId: 'xiaomi-token-plan-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-cn.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n 'xiaomi-token-plan-sgp': definePiProviderTarget({\n key: 'xiaomi-token-plan-sgp',\n providerId: 'xiaomi-token-plan-sgp',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1',\n allowCustomEndpoint: false,\n }),\n zai: definePiProviderTarget({\n key: 'zai',\n providerId: 'zai',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://api.z.ai/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n 'zai-coding-cn': definePiProviderTarget({\n key: 'zai-coding-cn',\n providerId: 'zai-coding-cn',\n defaultApi: 'openai-completions',\n allowedApis: Object.freeze(['openai-completions'] as const),\n defaultAuthType: 'api_key',\n allowedAuthTypes: Object.freeze(['api_key'] as const),\n defaultBaseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4',\n allowCustomEndpoint: false,\n }),\n});\n\n/** 读取轻量目标注册信息,供配置和密钥归属校验使用,无需加载 SDK 目录。 */\nexport function getPiProviderTargetDefinition(\n key: string\n): Readonly | undefined {\n return Object.prototype.hasOwnProperty.call(PI_PROVIDER_TARGET_REGISTRY, key)\n ? PI_PROVIDER_TARGET_REGISTRY[key as PiProviderKey]\n : undefined;\n}\n\n/** 判断指定服务商的内置协议端点是否必须经酒馆 CORS 代理访问。 */\nexport function isPiCorsProxyRequired(\n definition: Readonly | undefined,\n api: string\n): boolean {\n return definition?.corsProxyRequiredApis.includes(api as PiWireApi) ?? false;\n}\n\n/**\n * 内置端点遵循注册的代理策略,自定义端点遵循用户勾选状态。\n * 等价写法先规范化,避免把显式填写的默认地址误认作自定义目标。\n */\nexport function shouldUsePiCorsProxy(\n definition: Readonly | undefined,\n api: string,\n endpoint: string,\n customEndpointUseProxy: unknown\n): boolean {\n const explicit_endpoint = endpoint.trim();\n if (\n !definition ||\n explicit_endpoint === '' ||\n !definition.allowedApis.includes(api as PiWireApi)\n ) {\n return isPiCorsProxyRequired(definition, api);\n }\n try {\n const wire_api = api as PiWireApi;\n const configured_base = normalizePiApiBaseEndpoint(wire_api, explicit_endpoint);\n const registered_base = normalizePiApiBaseEndpoint(\n wire_api,\n getPiProviderApiBaseUrl(definition, wire_api)\n );\n if (configured_base === registered_base) {\n return isPiCorsProxyRequired(definition, api);\n }\n } catch {\n // Target validation reports malformed endpoints. Until then, keep the explicit checkbox\n // semantics so the UI does not mislabel an invalid value as a registered route.\n }\n return customEndpointUseProxy === true;\n}\n\n/** 优先读取协议专用基础地址,否则使用服务商默认地址。 */\nexport function getPiProviderApiBaseUrl(\n definition: Readonly,\n api: PiWireApi\n): string {\n return definition.apiBaseUrls?.[api] ?? definition.defaultBaseUrl;\n}\n\nexport class PiEndpointValidationError extends Error {\n /** 将端点格式或传输限制失败标记为独立的端点校验错误。 */\n constructor(message: string) {\n super(message);\n this.name = 'PiEndpointValidationError';\n }\n}\n\nconst HTTP_LOOPBACK_HOSTNAMES: ReadonlySet = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/** 规范端点地址,要求 HTTPS 或回环 HTTP,并拒绝内嵌凭证、查询参数和片段。 */\nexport function normalizePiTargetEndpoint(endpoint: string): string {\n let parsed: URL;\n try {\n parsed = new URL(endpoint.trim());\n } catch {\n throw new PiEndpointValidationError('More source endpoint must be a valid URL');\n }\n\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new PiEndpointValidationError('More source endpoint must use http or https');\n }\n if (\n parsed.protocol === 'http:' &&\n !HTTP_LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase())\n ) {\n throw new PiEndpointValidationError(\n 'More source HTTP endpoint must use localhost, 127.0.0.1, or [::1]'\n );\n }\n if (parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new PiEndpointValidationError(\n 'More source endpoint must not contain credentials, a query, or a fragment'\n );\n }\n\n const normalized_path = parsed.pathname.replace(/\\/+$/, '');\n return `${parsed.origin}${normalized_path}`;\n}\n\nconst PI_API_OPERATION_PATH_SUFFIXES: Partial> = {\n 'openai-responses': ['/responses'],\n 'openai-completions': ['/chat/completions', '/completions'],\n 'anthropic-messages': ['/v1/messages', '/messages'],\n};\n\n/**\n * 将安全端点转换为 SDK 需要的 API 基础地址。\n * 最多移除一次当前协议会自行追加的操作路径,避免重复拼接或过度裁剪。\n */\nexport function normalizePiApiBaseEndpoint(api: PiWireApi, endpoint: string): string {\n const normalized_endpoint = normalizePiTargetEndpoint(endpoint);\n const parsed = new URL(normalized_endpoint);\n const normalized_path_lower = parsed.pathname.toLowerCase();\n const operation_path = PI_API_OPERATION_PATH_SUFFIXES[api]?.find(suffix =>\n normalized_path_lower.endsWith(suffix)\n );\n if (!operation_path) {\n return normalized_endpoint;\n }\n\n const base_path = parsed.pathname.slice(0, -operation_path.length).replace(/\\/+$/, '');\n return normalizePiTargetEndpoint(`${parsed.origin}${base_path}`);\n}\n\n/**\n * 仅为完整、合法的 API Key 目标生成服务商与端点对应的缓存键。\n * 未知协议、OAuth、无效地址或不允许自定义端点时返回空值。\n */\nexport function resolvePiApiKeyScope(\n definition: Readonly | undefined,\n api: string,\n auth_type: string,\n endpoint: string\n): string {\n if (\n !definition ||\n auth_type !== 'api_key' ||\n !definition.allowedAuthTypes.includes('api_key') ||\n !definition.allowedApis.includes(api as PiWireApi) ||\n (endpoint.trim() !== '' && !definition.allowCustomEndpoint)\n ) {\n return '';\n }\n\n try {\n const effective_endpoint = normalizePiApiBaseEndpoint(\n api as PiWireApi,\n endpoint.trim() === ''\n ? getPiProviderApiBaseUrl(definition, api as PiWireApi)\n : endpoint\n );\n return `${definition.key}\\n${effective_endpoint}`;\n } catch {\n return '';\n }\n}\n","import type {\n Api,\n AssistantMessage,\n Context,\n ImageContent,\n Message,\n TextContent,\n ToolCall,\n ToolResultMessage,\n Usage,\n UserMessage,\n} from '@earendil-works/pi-ai';\n\n/** 需要按酒馆原始位置保留的系统消息,交由请求载荷 hook 在原位置恢复。 */\nexport type PiLateSystemMessage = {\n sourceIndex: number;\n /** 插在转换后第几条普通消息之前;等于 messages.length 表示位于末尾。 */\n beforeMessageIndex: number;\n text: string;\n};\n\n/** 上下文转换的内容保留及空消息诊断,不包含完整提示词。 */\nexport type PiContextAdapterDiagnostics = {\n /** 保留待处理的 system 数量;最终角色由协议适配层自动决定。 */\n preservedLateSystemCount: number;\n droppedEmptyMessageIndexes: number[];\n};\n\n/** 标准 Pi 上下文与必须一并传给载荷适配层的原生 system 消息。 */\nexport type PiContextAdapterResult = {\n context: Context;\n lateSystemMessages: readonly PiLateSystemMessage[];\n diagnostics: PiContextAdapterDiagnostics;\n};\n\n/** 提示词转换或原生 system 恢复失败的稳定分类。 */\nexport type PiContextAdapterErrorCode =\n | 'invalid-image'\n | 'invalid-tool-call'\n | 'system-payload-mismatch'\n | 'missing-tool-call'\n | 'unsupported-content';\n\nexport class PiContextAdapterError extends Error {\n /** 保留消息下标和错误类别,便于将上下文转换失败定位到原始提示词。 */\n constructor(\n message: string,\n readonly code: PiContextAdapterErrorCode,\n readonly sourceIndex: number\n ) {\n super(message);\n this.name = 'PiContextAdapterError';\n }\n}\n\n/** 酒馆捕获消息,加上历史工具结果可能携带的错误标记。 */\ntype SendingMessage = SillyTavern.SendingMessage & {\n name?: string;\n is_error?: boolean;\n isError?: boolean;\n};\n\n/** 酒馆多模态内容数组中的单个块。 */\ntype ContentBlock = NonNullable>[number];\n/** 当前转换层允许交给 Pi 的文本或图片输入。 */\ntype PiInputContent = TextContent | ImageContent;\n\nconst IMPORTED_ASSISTANT_API = 'sillytavern-import' as Api;\nconst IMPORTED_ASSISTANT_PROVIDER = 'sillytavern';\nconst IMPORTED_ASSISTANT_MODEL = 'prepared-prompt';\n\nconst SUPPORTED_IMAGE_MIME_TYPES = new Set(['image/gif', 'image/jpeg', 'image/png', 'image/webp']);\nconst MEBIBYTE = 1024 * 1024;\nconst MAX_DATA_URL_HEADER_CHARACTERS = 64;\nconst MAX_DECODED_IMAGE_HEADER_CHARACTERS = 768;\nconst MAX_ENCODED_IMAGE_HEADER_CHARACTERS = Math.ceil(MAX_DECODED_IMAGE_HEADER_CHARACTERS / 3) * 4;\n\nexport const PI_IMAGE_INPUT_LIMITS = Object.freeze({\n // Five MiB matches the strictest supported provider's common per-image cap.\n // The aggregate/count ceilings bound base64 copies on mobile browsers.\n maxDecodedBytesPerImage: 5 * MEBIBYTE,\n maxDecodedBytesPerContext: 16 * MEBIBYTE,\n maxImagesPerContext: 20,\n});\n\nconst MAX_ENCODED_CHARACTERS_PER_IMAGE =\n Math.ceil(PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / 3) * 4;\n\n/** 图片校验时提取的预算元数据,避免 token 预检重复解码图片。 */\nexport type PiImageMetadata = Readonly<{\n decodedBytes: number;\n dimensions?: Readonly<{\n width: number;\n height: number;\n }>;\n}>;\n\n/** 单次上下文转换累计使用的图片数量与解码字节数。 */\ntype ImageInputBudget = {\n decodedBytes: number;\n imageCount: number;\n};\n\nconst imageMetadata = new WeakMap();\n\n/** 读取转换时记录的图片大小和尺寸,供预算检查复用,避免再次解码。 */\nexport function getPiImageMetadata(image: ImageContent): PiImageMetadata | undefined {\n return imageMetadata.get(image);\n}\n\n/** 为导入的历史助手消息补齐零用量结构,避免把历史内容记为本次生成消耗。 */\nfunction makeZeroUsage(): Usage {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n}\n\n/** 转义消息名称中的 XML 特殊字符,防止名称破坏注入标记结构。 */\nfunction escapeXmlText(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');\n}\n\n/** 将有效的酒馆消息名称转换为可拼接的文本前缀。 */\nexport function formatSendingMessageName(name: string | undefined): string {\n if (typeof name !== 'string' || name.trim() === '') {\n return '';\n }\n return `${escapeXmlText(name)}`;\n}\n\n/** 识别仅含空白的内容,供严格校验和空消息处理使用。 */\nfunction isBlank(value: string): boolean {\n return value.trim().length === 0;\n}\n\n/** 仅接受普通对象或空原型对象,避免把类实例作为结构化消息内容。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 检查指定偏移处的文件头字节,用于验证声明的图片类型。 */\nfunction hasBytesAt(bytes: string, offset: number, expected: readonly number[]): boolean {\n return expected.every((value, index) => bytes.charCodeAt(offset + index) === value);\n}\n\n/** 核对 PNG、JPEG、GIF 或 WebP 文件签名,拒绝伪装为图片的数据。 */\nfunction hasRecognizableImageSignature(mimeType: string, bytes: string): boolean {\n if (mimeType === 'image/png') {\n return hasBytesAt(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n }\n if (mimeType === 'image/jpeg') {\n return hasBytesAt(bytes, 0, [0xff, 0xd8, 0xff]);\n }\n if (mimeType === 'image/gif') {\n return bytes.startsWith('GIF87a') || bytes.startsWith('GIF89a');\n }\n if (mimeType === 'image/webp') {\n return bytes.startsWith('RIFF') && bytes.slice(8, 12) === 'WEBP';\n }\n return false;\n}\n\n/** 判断字符是否属于标准 Base64 字母表,供图片载荷逐字符校验使用。 */\nfunction isBase64Character(code: number): boolean {\n return (\n (code >= 0x41 && code <= 0x5a) ||\n (code >= 0x61 && code <= 0x7a) ||\n (code >= 0x30 && code <= 0x39) ||\n code === 0x2b ||\n code === 0x2f\n );\n}\n\n/** 按大端读取图片头中的 16 位整数。 */\nfunction readUint16BigEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) * 0x100 + bytes.charCodeAt(offset + 1);\n}\n\n/** 按小端读取图片头中的 16 位整数。 */\nfunction readUint16LittleEndian(bytes: string, offset: number): number {\n return bytes.charCodeAt(offset) + bytes.charCodeAt(offset + 1) * 0x100;\n}\n\n/** 按小端读取 WebP 图片头中的 24 位整数。 */\nfunction readUint24LittleEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) +\n bytes.charCodeAt(offset + 1) * 0x100 +\n bytes.charCodeAt(offset + 2) * 0x10000\n );\n}\n\n/** 按大端读取图片头中的 32 位整数,并保持无符号取值。 */\nfunction readUint32BigEndian(bytes: string, offset: number): number {\n return (\n bytes.charCodeAt(offset) * 0x1000000 +\n bytes.charCodeAt(offset + 1) * 0x10000 +\n bytes.charCodeAt(offset + 2) * 0x100 +\n bytes.charCodeAt(offset + 3)\n );\n}\n\nconst JPEG_START_OF_FRAME_MARKERS = new Set([\n 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,\n]);\n\n/** 扫描已解码的 JPEG 头部标记以提取尺寸;信息不足时不推测尺寸。 */\nfunction readJpegDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 4 || bytes.charCodeAt(0) !== 0xff || bytes.charCodeAt(1) !== 0xd8) {\n return undefined;\n }\n\n let offset = 2;\n while (offset + 3 < bytes.length) {\n if (bytes.charCodeAt(offset) !== 0xff) {\n offset += 1;\n continue;\n }\n while (offset < bytes.length && bytes.charCodeAt(offset) === 0xff) {\n offset += 1;\n }\n const marker = bytes.charCodeAt(offset);\n offset += 1;\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {\n continue;\n }\n if (marker === 0xd9 || marker === 0xda || offset + 1 >= bytes.length) {\n break;\n }\n const segmentLength = readUint16BigEndian(bytes, offset);\n if (segmentLength < 2 || offset + segmentLength > bytes.length) {\n break;\n }\n if (JPEG_START_OF_FRAME_MARKERS.has(marker) && segmentLength >= 7) {\n return {\n height: readUint16BigEndian(bytes, offset + 3),\n width: readUint16BigEndian(bytes, offset + 5),\n };\n }\n offset += segmentLength;\n }\n return undefined;\n}\n\n/** 按 WebP 头部编码类型提取尺寸,兼容其不同帧头布局。 */\nfunction readWebpDimensions(bytes: string): PiImageMetadata['dimensions'] {\n if (bytes.length < 30 || !bytes.startsWith('RIFF') || bytes.slice(8, 12) !== 'WEBP') {\n return undefined;\n }\n\n const chunkType = bytes.slice(12, 16);\n if (chunkType === 'VP8X') {\n return {\n width: readUint24LittleEndian(bytes, 24) + 1,\n height: readUint24LittleEndian(bytes, 27) + 1,\n };\n }\n if (chunkType === 'VP8 ' && bytes.slice(23, 26) === '\\x9d\\x01\\x2a') {\n return {\n width: readUint16LittleEndian(bytes, 26) & 0x3fff,\n height: readUint16LittleEndian(bytes, 28) & 0x3fff,\n };\n }\n if (chunkType === 'VP8L' && bytes.charCodeAt(20) === 0x2f) {\n const b1 = bytes.charCodeAt(21);\n const b2 = bytes.charCodeAt(22);\n const b3 = bytes.charCodeAt(23);\n const b4 = bytes.charCodeAt(24);\n return {\n width: 1 + b1 + ((b2 & 0x3f) << 8),\n height: 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10),\n };\n }\n return undefined;\n}\n\n/** 根据已验证的图片类型分派尺寸读取,无法读取时返回 undefined。 */\nfunction readImageDimensions(bytes: string, mimeType: string): PiImageMetadata['dimensions'] {\n if (mimeType === 'image/png' && bytes.length >= 24) {\n return {\n width: readUint32BigEndian(bytes, 16),\n height: readUint32BigEndian(bytes, 20),\n };\n }\n if (mimeType === 'image/gif' && bytes.length >= 10) {\n return {\n width: readUint16LittleEndian(bytes, 6),\n height: readUint16LittleEndian(bytes, 8),\n };\n }\n if (mimeType === 'image/jpeg') {\n return readJpegDimensions(bytes);\n }\n if (mimeType === 'image/webp') {\n return readWebpDimensions(bytes);\n }\n return undefined;\n}\n\n/** 在受限的头部长度内查找 data URL 分隔符,避免对异常长头部做无界扫描。 */\nfunction findDataUrlSeparator(url: string): number {\n const maximumOffset = Math.min(url.length, MAX_DATA_URL_HEADER_CHARACTERS + 1);\n for (let offset = 5; offset < maximumOffset; offset++) {\n if (url.charCodeAt(offset) === 0x2c) {\n return offset;\n }\n }\n return -1;\n}\n\n/** 只解码识别图片类型和尺寸所需的有限头部,控制大图转换的内存开销。 */\nfunction decodeImageHeader(url: string, dataStart: number, encodedLength: number): string {\n const prefixLength = Math.min(encodedLength, MAX_ENCODED_IMAGE_HEADER_CHARACTERS);\n return atob(url.slice(dataStart, dataStart + prefixLength));\n}\n\n/**\n * 校验 data URL、Base64 编码、文件签名和图片预算,并记录元数据。\n * 仅接收受支持的内嵌图片,失败时携带原始消息下标,不自动下载远程地址。\n */\nfunction parseBase64Image(\n url: string,\n sourceIndex: number,\n imageBudget: ImageInputBudget\n): ImageContent {\n if (url.slice(0, 5).toLowerCase() !== 'data:') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含远程图片 URL;更多来源当前只接受 data URL 图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n\n if (url.length > MAX_DATA_URL_HEADER_CHARACTERS + 1 + MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const separator = findDataUrlSeparator(url);\n if (separator === -1) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const headerMatch = /^data:([^;,]+);base64$/i.exec(url.slice(0, separator));\n if (!headerMatch) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片 data URL`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const [, rawMimeType] = headerMatch;\n const mimeType = rawMimeType.toLowerCase();\n if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片 MIME 类型不受支持;只接受 PNG、JPEG、GIF 或 WebP`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const dataStart = separator + 1;\n const encodedLength = url.length - dataStart;\n if (encodedLength > MAX_ENCODED_CHARACTERS_PER_IMAGE) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (encodedLength === 0 || encodedLength % 4 !== 0) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n let padding = 0;\n if (url.charCodeAt(url.length - 1) === 0x3d) {\n padding += 1;\n if (url.charCodeAt(url.length - 2) === 0x3d) {\n padding += 1;\n }\n }\n const decodedBytes = (encodedLength / 4) * 3 - padding;\n if (decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片超过单张 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.imageCount + 1 > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求最多接受 ${PI_IMAGE_INPUT_LIMITS.maxImagesPerContext} 张图片`,\n 'invalid-image',\n sourceIndex\n );\n }\n if (imageBudget.decodedBytes + decodedBytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new PiContextAdapterError(\n `更多来源单次请求的图片总量超过 ${PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext / MEBIBYTE} MiB 上限`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n const contentEnd = url.length - padding;\n for (let offset = dataStart; offset < contentEnd; offset++) {\n if (!isBase64Character(url.charCodeAt(offset))) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n for (let offset = contentEnd; offset < url.length; offset++) {\n if (url.charCodeAt(offset) !== 0x3d) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无效的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n }\n\n let decodedHeader: string;\n try {\n decodedHeader = decodeImageHeader(url, dataStart, encodedLength);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含无法解码的 base64 图片数据`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n if (!hasRecognizableImageSignature(mimeType, decodedHeader)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息的图片数据与声明的 MIME 类型不匹配或缺少有效文件头`,\n 'invalid-image',\n sourceIndex\n );\n }\n\n imageBudget.imageCount += 1;\n imageBudget.decodedBytes += decodedBytes;\n\n const image: ImageContent = {\n type: 'image',\n data: url.slice(dataStart),\n mimeType,\n };\n const dimensions = readImageDimensions(decodedHeader, mimeType);\n imageMetadata.set(image, {\n decodedBytes,\n ...(dimensions === undefined ? {} : { dimensions }),\n });\n return image;\n}\n\n/** 将酒馆文本或图片块转换为 Pi 内容块,并执行对应的结构与图片校验。 */\nfunction convertContentBlock(\n block: ContentBlock,\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent {\n if (block.type === 'text') {\n return { type: 'text', text: block.text };\n }\n if (block.type === 'video_url') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含更多来源不支持的视频内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n if (block.type === 'image_url') {\n if (!allowImages) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法导入更多来源历史的图片`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return parseBase64Image(block.image_url.url, sourceIndex, imageBudget);\n }\n\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条消息包含未知内容块`,\n 'unsupported-content',\n sourceIndex\n );\n}\n\n/** 统一处理字符串和多块消息内容,逐块转换为 Pi 输入格式。 */\nfunction convertContent(\n content: SendingMessage['content'],\n sourceIndex: number,\n allowImages: boolean,\n imageBudget: ImageInputBudget\n): PiInputContent[] {\n if (typeof content === 'string') {\n return [{ type: 'text', text: content }];\n }\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map(block => convertContentBlock(block, sourceIndex, allowImages, imageBudget));\n}\n\n/** 判断内容块是否包含可发送的文本或图片。 */\nfunction contentHasValue(content: PiInputContent[]): boolean {\n return content.some(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 拼接文本内容块,供只允许纯文本的消息角色使用。 */\nfunction compactTextBlocks(content: PiInputContent[]): PiInputContent[] {\n return content.filter(block => block.type === 'image' || !isBlank(block.text));\n}\n\n/** 提取 system 消息的纯文本,统一拒绝不支持的系统内容。 */\nfunction extractSystemText(message: SendingMessage, sourceIndex: number): string {\n const content = message.content;\n let text: string;\n if (typeof content === 'string') {\n text = content;\n } else if (Array.isArray(content)) {\n const textBlocks = content.map(block => {\n if (block.type !== 'text') {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 system 消息包含非文本内容`,\n 'unsupported-content',\n sourceIndex\n );\n }\n return block.text;\n });\n text = textBlocks.join('');\n } else {\n text = '';\n }\n\n const name = formatSendingMessageName(message.name);\n return [name, text].filter(value => value !== '').join('\\n');\n}\n\n/** 在消息正文前保留酒馆角色名,并避免生成空的名称前缀。 */\nfunction addNamePrefix(content: PiInputContent[], name: string | undefined): PiInputContent[] {\n const prefix = formatSendingMessageName(name);\n if (!prefix) {\n return content;\n }\n return [{ type: 'text', text: prefix }, ...content];\n}\n\n/** 将历史工具参数解析为普通对象,拒绝无效 JSON 或非对象参数。 */\nfunction parseToolArguments(argumentsValue: string, sourceIndex: number): ToolCall['arguments'] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(argumentsValue);\n } catch {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无法解析的工具参数`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n if (!isPlainObject(parsed)) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息的工具参数必须是 JSON object`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n // JSON.parse 已排除非 JSON 值,且上方校验保证参数是对象而非数组或标量。\n return parsed as ToolCall['arguments'];\n}\n\n/** 校验历史工具调用标识和参数,并转换为 Pi 工具调用块。 */\nfunction convertToolCalls(\n message: SendingMessage,\n sourceIndex: number,\n toolNamesById: Map\n): ToolCall[] {\n return (message.tool_calls ?? []).map(call => {\n if (!call.id || !call.function?.name) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 assistant 消息包含无效工具调用`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n const existingName = toolNamesById.get(call.id);\n if (existingName && existingName !== call.function.name) {\n throw new PiContextAdapterError(\n `工具调用 ID ${call.id} 对应了多个工具名`,\n 'invalid-tool-call',\n sourceIndex\n );\n }\n toolNamesById.set(call.id, call.function.name);\n return {\n type: 'toolCall',\n id: call.id,\n name: call.function.name,\n arguments: parseToolArguments(call.function.arguments, sourceIndex),\n };\n });\n}\n\n/** 转换历史助手文本和工具调用,并补齐 Pi 要求的历史消息元数据。 */\nfunction convertAssistantMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): AssistantMessage {\n const textContent = compactTextBlocks(\n addNamePrefix(\n convertContent(message.content, sourceIndex, false, imageBudget),\n message.name\n )\n ) as TextContent[];\n const toolCalls = convertToolCalls(message, sourceIndex, toolNamesById);\n return {\n role: 'assistant',\n content: [...textContent, ...toolCalls],\n api: IMPORTED_ASSISTANT_API,\n provider: IMPORTED_ASSISTANT_PROVIDER,\n model: IMPORTED_ASSISTANT_MODEL,\n usage: makeZeroUsage(),\n stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',\n timestamp: now(),\n };\n}\n\n/** 将工具返回值关联到已知调用,保留工具名、错误标记及多模态结果。 */\nfunction convertToolResultMessage(\n message: SendingMessage,\n sourceIndex: number,\n now: () => number,\n toolNamesById: Map,\n imageBudget: ImageInputBudget\n): ToolResultMessage {\n const toolCallId = message.tool_call_id;\n if (!toolCallId) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息缺少 tool_call_id`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n const toolName = toolNamesById.get(toolCallId);\n if (!toolName) {\n throw new PiContextAdapterError(\n `第 ${sourceIndex} 条 tool 消息找不到对应的历史工具调用`,\n 'missing-tool-call',\n sourceIndex\n );\n }\n return {\n role: 'toolResult',\n toolCallId,\n toolName,\n content: compactTextBlocks(convertContent(message.content, sourceIndex, true, imageBudget)),\n isError: message.is_error === true || message.isError === true,\n timestamp: now(),\n };\n}\n\n/** 所有来源统一丢弃空消息,并保留原始下标用于诊断。 */\nfunction recordEmptyMessage(sourceIndex: number, diagnostics: PiContextAdapterDiagnostics): void {\n diagnostics.droppedEmptyMessageIndexes.push(sourceIndex);\n}\n\n/**\n * 把酒馆最终提示词转换为 Pi 上下文,并返回转换诊断。\n * 前置 system 合并为系统提示词;后置 system 单独保留内容及原位置,由载荷 hook 恢复。\n * 同时校验工具关联和图片预算、清理空消息;渠道兼容由载荷适配层处理。\n * now 只供测试注入稳定时间,不提供渠道相关的转换策略开关。\n */\nexport function toPiContext(\n input: readonly SendingMessage[],\n now: () => number = Date.now\n): PiContextAdapterResult {\n const messages = Array.from(input);\n const diagnostics: PiContextAdapterDiagnostics = {\n preservedLateSystemCount: 0,\n droppedEmptyMessageIndexes: [],\n };\n\n let leadingSystemEnd = 0;\n while (leadingSystemEnd < messages.length && messages[leadingSystemEnd].role === 'system') {\n leadingSystemEnd++;\n }\n\n const systemPromptParts: string[] = [];\n for (let index = 0; index < leadingSystemEnd; index++) {\n const text = extractSystemText(messages[index], index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n systemPromptParts.push(text);\n }\n\n const lateSystemMessages: PiLateSystemMessage[] = [];\n const piMessages: Message[] = [];\n const toolNamesById = new Map();\n const imageBudget: ImageInputBudget = { decodedBytes: 0, imageCount: 0 };\n for (let index = leadingSystemEnd; index < messages.length; index++) {\n const message = messages[index];\n if (message.role === 'system') {\n const text = extractSystemText(message, index);\n if (isBlank(text)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n lateSystemMessages.push({\n sourceIndex: index,\n beforeMessageIndex: piMessages.length,\n text,\n });\n continue;\n }\n if (message.role === 'user') {\n const content = addNamePrefix(\n convertContent(message.content, index, true, imageBudget),\n message.name\n );\n if (!contentHasValue(content)) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n const compactContent = compactTextBlocks(content);\n const userMessage: UserMessage = {\n role: 'user',\n content:\n compactContent.length === 1 && compactContent[0].type === 'text'\n ? compactContent[0].text\n : compactContent,\n timestamp: now(),\n };\n piMessages.push(userMessage);\n continue;\n }\n if (message.role === 'assistant') {\n const assistantMessage = convertAssistantMessage(\n message,\n index,\n now,\n toolNamesById,\n imageBudget\n );\n if (assistantMessage.content.length === 0) {\n recordEmptyMessage(index, diagnostics);\n continue;\n }\n piMessages.push(assistantMessage);\n continue;\n }\n if (message.role === 'tool') {\n piMessages.push(\n convertToolResultMessage(message, index, now, toolNamesById, imageBudget)\n );\n continue;\n }\n throw new PiContextAdapterError(\n `第 ${index} 条消息包含未知角色`,\n 'unsupported-content',\n index\n );\n }\n diagnostics.droppedEmptyMessageIndexes.sort((left, right) => left - right);\n diagnostics.preservedLateSystemCount = lateSystemMessages.length;\n\n return {\n context: {\n ...(systemPromptParts.length > 0\n ? { systemPrompt: systemPromptParts.join('\\n\\n') }\n : {}),\n messages: piMessages,\n },\n lateSystemMessages,\n diagnostics,\n };\n}\n","import type { FetchFunction } from './pi_gateway';\nimport type { PiWireApi } from './provider_target';\n\n/** 尚待逐字段校验的协议 JSON 对象,用于非流式响应和本地合成的流事件。 */\ntype JsonObject = Record;\n\n/** 校验响应结构为非空、非数组对象。 */\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 拒绝不符合协议的非流式响应,不将可能包含提示词或凭证的响应正文写入错误。 */\nfunction invalidResponse(): never {\n // Do not include provider bodies: they can echo prompts, headers, or credentials.\n throw new Error('More source received an invalid non-streaming response');\n}\n\n/** 把单个协议事件编码为 SSE 文本,供 Pi 现有流解析器消费。 */\nfunction event(data: JsonObject): string {\n return `${typeof data.type === 'string' ? `event: ${data.type}\\n` : ''}data: ${JSON.stringify(data)}\\n\\n`;\n}\n\n/** 把 Chat Completions 完整响应转换为增量结束事件,补齐工具调用下标。 */\nfunction completionEvents(response: JsonObject): string {\n if (!Array.isArray(response.choices) || response.choices.length === 0) {\n return invalidResponse();\n }\n const choices = response.choices.map((choice: unknown, index) => {\n if (\n !isObject(choice) ||\n !isObject(choice.message) ||\n typeof choice.finish_reason !== 'string' ||\n !choice.finish_reason\n ) {\n return invalidResponse();\n }\n const delta = { ...choice.message };\n if (Array.isArray(delta.tool_calls)) {\n delta.tool_calls = delta.tool_calls.map((call: unknown, index) => {\n if (!isObject(call)) {\n return invalidResponse();\n }\n return { ...call, index };\n });\n }\n const { message: _message, ...rest } = choice;\n return { ...rest, index, delta };\n });\n return event({ ...response, object: 'chat.completion.chunk', choices }) + 'data: [DONE]\\n\\n';\n}\n\n/** 把 Responses 完整输出转换为输出项事件和终态事件,保留失败与截断状态。 */\nfunction responsesEvents(response: JsonObject): string {\n const status = response.status;\n if (!['completed', 'incomplete', 'failed', 'cancelled'].includes(String(status))) {\n return invalidResponse();\n }\n if (!Array.isArray(response.output)) {\n return invalidResponse();\n }\n return (\n response.output\n .map((item: unknown, output_index) => {\n if (!isObject(item)) {\n return invalidResponse();\n }\n return event({ type: 'response.output_item.done', output_index, item });\n })\n .join('') +\n event({\n type: `response.${status === 'cancelled' ? 'failed' : status}`,\n response,\n })\n );\n}\n\n/** 把 Anthropic 完整响应转换为消息和内容块事件,工具参数通过增量事件交给 Pi 解析。 */\nfunction anthropicEvents(response: JsonObject): string {\n if (!Array.isArray(response.content) || typeof response.stop_reason !== 'string') {\n return invalidResponse();\n }\n const usage = isObject(response.usage) ? response.usage : {};\n let result = event({\n type: 'message_start',\n message: { ...response, content: [], usage },\n });\n response.content.forEach((block: unknown, index) => {\n if (!isObject(block)) {\n return invalidResponse();\n }\n result += event({\n type: 'content_block_start',\n index,\n content_block: block.type === 'tool_use' ? { ...block, input: {} } : block,\n });\n // Pi finalizes tool arguments from the delta buffer, even when start has an input.\n if (block.type === 'tool_use') {\n result += event({\n type: 'content_block_delta',\n index,\n delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) },\n });\n }\n result += event({ type: 'content_block_stop', index });\n });\n return (\n result +\n event({\n type: 'message_delta',\n delta: {\n stop_reason: response.stop_reason,\n stop_sequence: response.stop_sequence,\n stop_details: response.stop_details,\n },\n usage,\n }) +\n event({ type: 'message_stop' })\n );\n}\n\n/** 按请求协议选择非流式响应转换器,拒绝错误对象和不支持的响应结构。 */\nfunction toEvents(api: PiWireApi, response: unknown): string {\n if (!isObject(response) || response.error) {\n return invalidResponse();\n }\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return completionEvents(response);\n case 'openai-responses':\n return responsesEvents(response);\n case 'anthropic-messages':\n return anthropicEvents(response);\n case 'google-generative-ai':\n if (!Array.isArray(response.candidates)) {\n return invalidResponse();\n }\n return event(response);\n default:\n return invalidResponse();\n }\n}\n\n/** 识别需要改写传输方式的生成接口,避免影响认证和模型列表等其他请求。 */\nfunction isGenerationUrl(api: PiWireApi, url: URL): boolean {\n switch (api) {\n case 'openai-completions':\n case 'mistral-conversations':\n return url.pathname.endsWith('/chat/completions');\n case 'openai-responses':\n return url.pathname.endsWith('/responses');\n case 'anthropic-messages':\n return url.pathname.endsWith('/messages');\n case 'google-generative-ai':\n return url.pathname.endsWith(':streamGenerateContent');\n default:\n return false;\n }\n}\n\n/**\n * 保留 Pi 的请求构造、认证和结果解析,只将支持的生成请求改为真正的非流式 HTTP。\n * 返回的 JSON 在本地转换为 Pi 可消费的事件流;Codex 的强制流式协议保持原样。\n */\nexport function createPiNonStreamingFetch(\n api: PiWireApi,\n fetch_impl?: FetchFunction\n): FetchFunction {\n return async (input, init) => {\n const send = fetch_impl ?? globalThis.fetch;\n const request = new Request(input, init);\n const url = new URL(request.url);\n if (request.method !== 'POST' || !isGenerationUrl(api, url)) {\n return send(input, init);\n }\n request.signal.throwIfAborted();\n const body: unknown = await request.json();\n if (!isObject(body)) {\n throw new Error('More source requires a JSON request body');\n }\n if (api === 'google-generative-ai') {\n url.pathname = url.pathname.replace(/:streamGenerateContent$/, ':generateContent');\n if (url.searchParams.get('alt') === 'sse') {\n url.searchParams.delete('alt');\n }\n } else {\n body.stream = false;\n delete body.stream_options;\n delete body.tool_stream;\n if (api === 'anthropic-messages' && Array.isArray(body.tools)) {\n body.tools.forEach((tool: unknown) => {\n if (isObject(tool)) {\n delete tool.eager_input_streaming;\n }\n });\n }\n }\n const headers = new Headers(request.headers);\n headers.set('accept', 'application/json');\n headers.delete('content-length');\n const response = await send(\n new Request(url, {\n ...init,\n method: request.method,\n headers,\n body: JSON.stringify(body),\n signal: request.signal,\n credentials: request.credentials,\n redirect: request.redirect,\n })\n );\n request.signal.throwIfAborted();\n // Keep provider HTTP errors intact for Pi's existing retry/error handling. Some gateways\n // ignore stream:false and still return SSE; their valid stream can be consumed as-is.\n if (!response.ok || response.headers.get('content-type')?.includes('text/event-stream')) {\n return response;\n }\n const data: unknown = await response.json();\n request.signal.throwIfAborted();\n const response_headers = new Headers(response.headers);\n response_headers.set('content-type', 'text/event-stream');\n response_headers.delete('content-length');\n response_headers.delete('content-encoding');\n return new Response(toEvents(api, data), {\n status: response.status,\n statusText: response.statusText,\n headers: response_headers,\n });\n };\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** 需要映射到服务商原生 JSON 输出配置的两种应答格式。 */\nexport type PiStructuredResponseFormat = '格式化输出' | '格式化输出(v4兼容)';\n\n/** MVU 的结构化输出 Schema,包含协议需要的名称及可选严格校验标记。 */\nexport type PiJsonSchema = {\n name: string;\n description?: string;\n value: Record;\n strict?: boolean;\n};\n\n/** 单次请求的原生载荷转换选项;消息角色由独立的 system 桥接层恢复。 */\nexport type PiPayloadTransformOptions = {\n api: Api;\n responseFormat?: PiStructuredResponseFormat;\n jsonSchema?: PiJsonSchema;\n customIncludeBody?: Record;\n customExcludeBody?: readonly string[];\n sampling?: {\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n };\n};\n\nconst PROTECTED_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'config',\n 'instructions',\n 'model',\n 'messages',\n 'input',\n 'contents',\n 'system',\n 'stream',\n 'tools',\n 'tool_choice',\n 'toolChoice',\n 'toolConfig',\n]);\n\nconst GOOGLE_PROTECTED_CONFIG_FIELDS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n 'abortSignal',\n 'automaticFunctionCalling',\n 'httpOptions',\n 'responseJsonSchema',\n 'responseMimeType',\n 'responseSchema',\n 'systemInstruction',\n 'temperature',\n 'thinkingConfig',\n 'toolConfig',\n 'tools',\n 'topK',\n 'topP',\n]);\n\n/** 判断请求体或自定义配置是否为非空、非数组对象。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * 先删除再合并自定义字段,同时阻止修改受保护的请求字段。\n * 只浅拷贝原请求体以保留 AbortSignal 等浏览器对象,用户覆盖值单独深拷贝。\n */\nfunction applyCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n // Provider payloads may contain live browser objects (notably Google's AbortSignal).\n // A deep structuredClone turns those into plain objects and breaks cancellation. Only clone\n // the top-level payload here; user-supplied include values are cloned individually below.\n const result = { ...payload };\n for (const name of exclude) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot exclude protected field '${name}'`);\n }\n delete result[name];\n }\n for (const [name, value] of Object.entries(include)) {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(`More source custom body cannot override protected field '${name}'`);\n }\n result[name] = structuredClone(value);\n }\n return result;\n}\n\n/** 只接受 config.<字段> 形式的 Google 删除路径,避免歧义或删除整个生成配置。 */\nfunction googleConfigField(path: string): string {\n const match = /^config\\.([^.]+)$/.exec(path);\n if (!match) {\n throw new Error(\n `More source customExcludeBody for Google must use a direct 'config.' path, received '${path}'`\n );\n }\n return match[1];\n}\n\n/** 阻止自定义配置覆盖或删除由运行时管理的 Google 核心字段。 */\nfunction assertGoogleConfigFieldAllowed(field: string, operation: 'override' | 'exclude'): void {\n if (GOOGLE_PROTECTED_CONFIG_FIELDS.has(field)) {\n throw new Error(\n `More source custom body cannot ${operation} protected field 'config.${field}'`\n );\n }\n}\n\n/** 将 Google 自定义参数限制在 config 内,保留现有配置中的取消信号等对象。 */\nfunction applyGoogleCustomFields(\n payload: Record,\n include: Record,\n exclude: readonly string[]\n): Record {\n const existing_config = isPlainObject(payload.config) ? payload.config : {};\n const config = { ...existing_config };\n\n for (const path of exclude) {\n if (path === 'config' || PROTECTED_FIELDS.has(path)) {\n throw new Error(`More source custom body cannot exclude protected field '${path}'`);\n }\n const field = googleConfigField(path);\n assertGoogleConfigFieldAllowed(field, 'exclude');\n delete config[field];\n }\n\n for (const [name, value] of Object.entries(include)) {\n if (name !== 'config') {\n if (PROTECTED_FIELDS.has(name)) {\n throw new Error(\n `More source custom body cannot override protected field '${name}'`\n );\n }\n throw new Error(\n `More source customIncludeBody for Google must place '${name}' inside the 'config' object`\n );\n }\n if (!isPlainObject(value)) {\n throw new Error(\n \"More source customIncludeBody for Google requires 'config' to be an object\"\n );\n }\n for (const [field, field_value] of Object.entries(value)) {\n assertGoogleConfigFieldAllowed(field, 'override');\n config[field] = structuredClone(field_value);\n }\n }\n\n return { ...payload, config };\n}\n\n/** 按协议写入原生 JSON Schema 或 JSON 对象输出配置,不支持的组合直接报错。 */\nfunction applyNativeStructuredOutput(\n payload: Record,\n api: Api,\n response_format: PiStructuredResponseFormat,\n schema: PiJsonSchema | undefined\n): Record {\n if (response_format === '格式化输出' && !schema) {\n throw new Error('More source structured output requires a JSON schema');\n }\n\n const is_json_schema = response_format === '格式化输出';\n if (api === 'openai-completions') {\n return {\n ...payload,\n response_format: is_json_schema\n ? {\n type: 'json_schema',\n json_schema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n if (api === 'openai-responses' || api === 'openai-codex-responses') {\n const existing_text = isPlainObject(payload.text) ? payload.text : {};\n return {\n ...payload,\n text: {\n ...existing_text,\n format: is_json_schema\n ? {\n type: 'json_schema',\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schema: schema!.value,\n strict: schema!.strict ?? true,\n }\n : { type: 'json_object' },\n },\n };\n }\n\n if (api === 'anthropic-messages') {\n if (!is_json_schema) {\n throw new Error(`More source API '${api}' does not support native JSON-object output`);\n }\n const existing_output_config = isPlainObject(payload.output_config)\n ? payload.output_config\n : {};\n return {\n ...payload,\n output_config: {\n ...existing_output_config,\n format: {\n type: 'json_schema',\n schema: schema!.value,\n },\n },\n };\n }\n\n if (api === 'google-generative-ai') {\n if (!is_json_schema) {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n },\n };\n }\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n responseMimeType: 'application/json',\n responseJsonSchema: schema!.value,\n },\n };\n }\n\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n responseFormat: is_json_schema\n ? {\n type: 'json_schema',\n jsonSchema: {\n name: schema!.name,\n ...(schema!.description ? { description: schema!.description } : {}),\n schemaDefinition: schema!.value,\n strict: schema!.strict ?? true,\n },\n }\n : { type: 'json_object' },\n };\n }\n\n throw new Error(`More source API '${api}' does not support native structured output`);\n}\n\n/** 将统一采样选项转换为各协议的字段名称和嵌套位置。 */\nfunction applyApiSampling(\n payload: Record,\n api: Api,\n sampling: PiPayloadTransformOptions['sampling']\n): Record {\n if (!sampling) {\n return payload;\n }\n if (api === 'openai-completions') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequency_penalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presence_penalty: sampling.presencePenalty }),\n };\n }\n if (api === 'openai-responses') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n };\n }\n if (api === 'anthropic-messages') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { top_p: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { top_k: sampling.topK }),\n };\n }\n if (api === 'google-generative-ai') {\n return {\n ...payload,\n config: {\n ...(isPlainObject(payload.config) ? payload.config : {}),\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.topK === undefined ? {} : { topK: sampling.topK }),\n },\n };\n }\n if (api === 'mistral-conversations') {\n return {\n ...payload,\n ...(sampling.topP === undefined ? {} : { topP: sampling.topP }),\n ...(sampling.frequencyPenalty === undefined\n ? {}\n : { frequencyPenalty: sampling.frequencyPenalty }),\n ...(sampling.presencePenalty === undefined\n ? {}\n : { presencePenalty: sampling.presencePenalty }),\n };\n }\n return payload;\n}\n\n/** 统一应用请求覆盖、采样和原生应答格式,并保留协议核心字段的控制权。 */\nexport function transformPiPayload(payload: unknown, options: PiPayloadTransformOptions): unknown {\n if (!isPlainObject(payload)) {\n throw new Error('More source provider payload must be an object');\n }\n let result = applyApiSampling(payload, options.api, options.sampling);\n result =\n options.api === 'google-generative-ai'\n ? applyGoogleCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n )\n : applyCustomFields(\n result,\n options.customIncludeBody ?? {},\n options.customExcludeBody ?? []\n );\n if (options.responseFormat) {\n result = applyNativeStructuredOutput(\n result,\n options.api,\n options.responseFormat,\n options.jsonSchema\n );\n }\n return result;\n}\n\n/** 将本轮转换选项绑定为 Pi 的 onPayload 回调。 */\nexport function createPiPayloadTransform(options: PiPayloadTransformOptions) {\n return (payload: unknown) => transformPiPayload(payload, options);\n}\n","import type { AssistantMessage, Tool, ToolCall } from '@earendil-works/pi-ai';\n\n/** 模型输出无法交回原有变量更新流程的原因,区分取消、截断和无效结果。 */\nexport type PiResultAdapterErrorCode =\n | 'aborted'\n | 'deferred'\n | 'empty-response'\n | 'invalid-tool-call'\n | 'length'\n | 'network'\n | 'provider-error';\n\nexport class PiResultAdapterError extends Error {\n /** 保留结果转换错误码,供运行时决定取消、重试及界面提示。 */\n constructor(\n message: string,\n readonly code: PiResultAdapterErrorCode\n ) {\n super(message);\n this.name = 'PiResultAdapterError';\n }\n}\n\n/** 将酒馆函数工具转为 Pi Tool 时附加的协议选项。 */\nexport type PiToolDefinitionOptions = {\n /** 服务商端的 Schema 或语法约束配置;false 显式关闭约束采样。 */\n constrainedSampling?: Tool['constrainedSampling'];\n};\n\n/** 仅接受普通或空原型对象,作为工具参数与 Schema 的结构边界。 */\nfunction isPlainObject(value: unknown): value is Record {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** 复制工具 Schema,无法复制时转换为工具定义错误,避免共享结构被修改。 */\nfunction cloneSchema(schema: Record): Record {\n try {\n return structuredClone(schema);\n } catch (cause) {\n throw new PiResultAdapterError(\n `工具参数 schema 无法复制:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n}\n\n/** 校验函数工具名称和对象 Schema,再转换为 Pi 工具定义及约束采样配置。 */\nexport function toPiToolDefinition(\n definition: ToolDefinition,\n options: PiToolDefinitionOptions = {}\n): Tool {\n if (definition?.type !== 'function' || !definition.function?.name?.trim()) {\n throw new PiResultAdapterError('工具定义缺少有效的 function name', 'invalid-tool-call');\n }\n\n const parameters = definition.function.parameters ?? {\n type: 'object',\n properties: {},\n };\n if (!isPlainObject(parameters) || parameters.type !== 'object') {\n throw new PiResultAdapterError(\n '工具参数 schema 的根节点必须是 object',\n 'invalid-tool-call'\n );\n }\n\n return {\n name: definition.function.name,\n description: definition.function.description ?? '',\n parameters: cloneSchema(parameters) as Tool['parameters'],\n ...(options.constrainedSampling === undefined\n ? {}\n : { constrainedSampling: options.constrainedSampling }),\n };\n}\n\nexport const toPiTool = toPiToolDefinition;\n\n/** 把 Pi 工具调用转换为酒馆结果结构,序列化参数并保留思考签名。 */\nfunction normalizeToolCall(call: ToolCall): GenerateToolCallResult['tool_calls'][number] {\n if (!call.id?.trim() || !call.name?.trim() || !isPlainObject(call.arguments)) {\n throw new PiResultAdapterError('更多来源返回了无效的工具调用', 'invalid-tool-call');\n }\n\n let argumentsValue: string;\n try {\n argumentsValue = JSON.stringify(call.arguments);\n } catch (cause) {\n throw new PiResultAdapterError(\n `更多来源工具参数无法序列化:${cause instanceof Error ? cause.message : String(cause)}`,\n 'invalid-tool-call'\n );\n }\n\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: argumentsValue,\n },\n ...(call.thoughtSignature ? { thought_signature: call.thoughtSignature } : {}),\n };\n}\n\n/** 只允许完整文本或工具调用结果继续转换,分别拒绝截断、取消、延迟和服务商失败。 */\nfunction assertSuccessfulStopReason(message: AssistantMessage): void {\n switch (message.stopReason) {\n case 'stop':\n case 'toolUse':\n return;\n case 'length':\n throw new PiResultAdapterError('更多来源回复因达到长度上限而被截断', 'length');\n case 'aborted':\n throw new PiResultAdapterError('更多来源请求已取消', 'aborted');\n case 'error': {\n // Provider errorMessage values are untrusted response data. They have been observed to\n // contain echoed Authorization headers/API keys, so retain only a coarse classification.\n const isNetworkError =\n typeof message.errorMessage === 'string' &&\n /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message.errorMessage\n );\n throw new PiResultAdapterError(\n isNetworkError\n ? 'The browser could not complete the More source request.'\n : 'More source request failed.',\n isNetworkError ? 'network' : 'provider-error'\n );\n }\n case 'pending':\n case 'deferred':\n throw new PiResultAdapterError(\n '更多来源回复尚未完成,当前不支持 deferred 结果',\n 'deferred'\n );\n default: {\n const exhaustive: never = message.stopReason;\n void exhaustive;\n throw new PiResultAdapterError(\n 'More source provider returned an unknown stop reason.',\n 'provider-error'\n );\n }\n }\n}\n\n/**\n * 提取助手文本及工具调用并返回 MVU 可消费的结果。\n * 空响应、仅有思考或工具结束却没有调用都视为协议失败,不写入聊天上下文。\n */\nexport function fromPiAssistantMessage(message: AssistantMessage): string | GenerateToolCallResult {\n assertSuccessfulStopReason(message);\n\n const text = message.content\n .filter(content => content.type === 'text')\n .map(content => content.text)\n .join('');\n const toolCalls = message.content\n .filter((content): content is ToolCall => content.type === 'toolCall')\n .map(normalizeToolCall);\n\n if (message.stopReason === 'toolUse' && toolCalls.length === 0) {\n throw new PiResultAdapterError(\n '更多来源以 toolUse 结束,但没有返回工具调用',\n 'invalid-tool-call'\n );\n }\n if (toolCalls.length > 0) {\n return {\n content: text,\n tool_calls: toolCalls,\n };\n }\n if (text.length === 0) {\n const hasThinking = message.content.some(content => content.type === 'thinking');\n throw new PiResultAdapterError(\n hasThinking ? '更多来源回复仅包含 thinking,没有业务内容' : '更多来源返回了空回复',\n 'empty-response'\n );\n }\n return text;\n}\n","import type { Context, ImageContent, Message } from '@earendil-works/pi-ai';\nimport {\n getPiImageMetadata,\n PI_IMAGE_INPUT_LIMITS,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 发送前的保守预算结果,用于解释输入是否超出模型窗口,不代表计费 token 数。 */\nexport type PiTokenPreflightResult = {\n estimatedInputTokens: number;\n /** 额外预留的安全余量,不包含回复 token。 */\n reservedTokens: number;\n /** 上下文窗口扣除回复额度及安全余量后允许的输入上限。 */\n maxInputTokens: number;\n};\n\nconst MIN_IMAGE_TOKEN_ESTIMATE = 1024;\nconst ESTIMATED_IMAGE_BYTES_PER_TOKEN = 64;\n// Common multimodal APIs estimate image tokens from tiled/pixel area. Using a\n// lower pixels-per-token ratio leaves deliberate cross-provider headroom.\nconst ESTIMATED_IMAGE_PIXELS_PER_TOKEN = 500;\n\n/**\n * 按 UTF-8 字节保守估算文本 token,兼顾中文与非基本平面字符。\n * 这是发送前的预算保护,不用于精确计费。\n */\nfunction estimateTextTokens(value: string): number {\n if (!value) {\n return 0;\n }\n // This is a deliberately conservative boundary guard, not billing-grade\n // tokenization. UTF-8 bytes avoid undercounting CJK and astral characters\n // while ASCII/JSON remains cheaper than non-ASCII text.\n let ascii_bytes = 0;\n let non_ascii_bytes = 0;\n for (const character of value) {\n const code_point = character.codePointAt(0)!;\n if (code_point <= 0x7f) {\n ascii_bytes += 1;\n } else if (code_point <= 0x7ff) {\n non_ascii_bytes += 2;\n } else if (code_point <= 0xffff) {\n non_ascii_bytes += 3;\n } else {\n non_ascii_bytes += 4;\n }\n }\n return Math.ceil(ascii_bytes / 3 + non_ascii_bytes / 2);\n}\n\n/** 根据 Base64 长度与填充估算解码字节数,避免为预算检查复制或解码整张图片。 */\nfunction estimateDecodedBase64Bytes(data: string): number {\n if (!data) {\n return 0;\n }\n // Adapter-produced blocks are canonical base64. Counting whitespace in an\n // external block overestimates safely and avoids an image-sized copy.\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding);\n}\n\n/** 优先使用上下文转换时记录的图片大小,外部内容块再按 Base64 长度估算。 */\nfunction getEstimatedImageBytes(image: ImageContent): number {\n return getPiImageMetadata(image)?.decodedBytes ?? estimateDecodedBase64Bytes(image.data);\n}\n\n/** 取图片最小预算、字节估算和像素面积估算中的最大值,保留跨服务商余量。 */\nfunction estimateImageTokens(image: ImageContent): number {\n const metadata = getPiImageMetadata(image);\n const estimated_bytes = getEstimatedImageBytes(image);\n const dimensions = metadata?.dimensions;\n\n let pixel_tokens = 0;\n if (\n dimensions !== undefined &&\n Number.isSafeInteger(dimensions.width) &&\n dimensions.width > 0 &&\n Number.isSafeInteger(dimensions.height) &&\n dimensions.height > 0\n ) {\n pixel_tokens = Math.ceil(\n (dimensions.width * dimensions.height) / ESTIMATED_IMAGE_PIXELS_PER_TOKEN\n );\n }\n\n return Math.max(\n MIN_IMAGE_TOKEN_ESTIMATE,\n Math.ceil(estimated_bytes / ESTIMATED_IMAGE_BYTES_PER_TOKEN),\n pixel_tokens\n );\n}\n\n/** 检查单图、总图片数量和累计解码大小,即使外部绕过上下文转换也执行预算限制。 */\nfunction assertPiImageInputBudget(context: Context): void {\n let image_count = 0;\n let decoded_bytes = 0;\n /** 累计单个图片块并立即检查各项图片限制,超限时停止遍历。 */\n const countImage = (image: ImageContent): void => {\n const image_bytes = getEstimatedImageBytes(image);\n if (image_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerImage) {\n throw new Error('More source image exceeds the per-image input limit');\n }\n image_count += 1;\n decoded_bytes += image_bytes;\n if (image_count > PI_IMAGE_INPUT_LIMITS.maxImagesPerContext) {\n throw new Error('More source image count exceeds the per-context input limit');\n }\n if (decoded_bytes > PI_IMAGE_INPUT_LIMITS.maxDecodedBytesPerContext) {\n throw new Error('More source image total exceeds the per-context input limit');\n }\n };\n\n for (const message of context.messages) {\n if (message.role === 'user' && Array.isArray(message.content)) {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n } else if (message.role === 'toolResult') {\n for (const block of message.content) {\n if (block.type === 'image') {\n countImage(block);\n }\n }\n }\n }\n}\n\n/** 按消息角色估算文本、思考、工具参数和图片,并计入消息结构开销。 */\nfunction estimateMessageTokens(message: Message): number {\n let tokens = 6;\n if (message.role === 'system') {\n tokens += estimateTextTokens(\n typeof message.content === 'string'\n ? message.content\n : message.content.map(block => block.text).join('')\n );\n for (const value of [message.sections, message.toolsAdded, message.toolsRemoved]) {\n if (value) tokens += estimateTextTokens(JSON.stringify(value));\n }\n return tokens;\n }\n if (message.role === 'user') {\n if (typeof message.content === 'string') {\n return tokens + estimateTextTokens(message.content);\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens;\n }\n if (message.role === 'assistant') {\n for (const block of message.content) {\n if (block.type === 'text') {\n tokens += estimateTextTokens(block.text);\n } else if (block.type === 'thinking') {\n tokens += estimateTextTokens(block.thinking);\n } else {\n tokens +=\n estimateTextTokens(block.name) +\n estimateTextTokens(JSON.stringify(block.arguments));\n }\n }\n return tokens;\n }\n for (const block of message.content) {\n tokens +=\n block.type === 'text' ? estimateTextTokens(block.text) : estimateImageTokens(block);\n }\n return tokens + estimateTextTokens(message.toolName);\n}\n\n/** 汇总系统提示词、历史消息和工具定义的输入 token 估算。 */\nexport function estimatePiContextTokens(\n context: Context,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): number {\n let tokens = estimateTextTokens(context.systemPrompt ?? '');\n for (const message of context.messages) {\n tokens += estimateMessageTokens(message);\n }\n if (context.tools?.length) {\n tokens += estimateTextTokens(JSON.stringify(context.tools));\n }\n // 后置 system 尚未写入 Pi Context,但最终请求会恢复其文本和独立消息开销。\n for (const message of lateSystemMessages) {\n tokens += estimateTextTokens(message.text) + 8;\n }\n return Math.max(1, tokens);\n}\n\n/**\n * 预留回复 token 和安全余量后检查完整输入预算,不在此截断提示词。\n * 同时校验图片限制;超限错误携带估算明细,供界面解释原因。\n */\nexport function assertPiTokenBudget(\n context: Context,\n context_window: number,\n max_tokens: number,\n reserve_ratio = 0.07,\n lateSystemMessages: readonly PiLateSystemMessage[] = []\n): PiTokenPreflightResult {\n if (!Number.isInteger(context_window) || context_window <= 0) {\n throw new Error('More source contextWindow must be a positive integer');\n }\n if (!Number.isInteger(max_tokens) || max_tokens <= 0) {\n throw new Error('More source maxTokens must be a positive integer');\n }\n if (max_tokens > context_window) {\n throw new Error('More source maxTokens must not exceed contextWindow');\n }\n assertPiImageInputBudget(context);\n\n const normalized_reserve_ratio = Math.min(0.1, Math.max(0.05, reserve_ratio));\n const reserved_tokens = Math.ceil(context_window * normalized_reserve_ratio);\n const max_input_tokens = context_window - max_tokens - reserved_tokens;\n const estimated_input_tokens = estimatePiContextTokens(context, lateSystemMessages);\n if (max_input_tokens <= 0 || estimated_input_tokens > max_input_tokens) {\n throw new Error(\n `More source prompt is too long: estimated ${estimated_input_tokens} input tokens, ` +\n `limit ${Math.max(0, max_input_tokens)} after reserving ${max_tokens} reply tokens ` +\n `and ${reserved_tokens} safety tokens`\n );\n }\n\n return {\n estimatedInputTokens: estimated_input_tokens,\n reservedTokens: reserved_tokens,\n maxInputTokens: max_input_tokens,\n };\n}\n","import type { Api, Context, Message, TextContent } from './pi_gateway';\nimport {\n PiContextAdapterError,\n type PiContextAdapterResult,\n type PiLateSystemMessage,\n} from './context_adapter';\n\n/** 一次请求的 system 恢复桥接;context 仅供 Pi 构造请求,restore 必须用于 onPayload。 */\nexport interface PiSystemMessageBridge {\n context: Context;\n restore: (payload: unknown) => unknown;\n /** Pi 会把 hook 异常转为普通服务商错误,保留原始分类供运行时取回。 */\n readonly failure: PiContextAdapterError | undefined;\n assertRestored: () => void;\n}\n\n/** 同一原始位置的一组 system,锚定到下一条实际消息,不额外伪造对话回合。 */\ntype SystemAnchor = {\n marker: string;\n messages: readonly PiLateSystemMessage[];\n};\n\n/** 对服务商 JSON 节点移除锚点后的结果;remove 仅用于锚点独占的临时文本块。 */\ntype CleanedNode = { value: unknown; anchors: SystemAnchor[]; remove?: boolean };\n\nlet nextAnchorNamespace = 0;\n\n/** 选用原始内容中不存在的请求级标记,防止用户文本被误识别为内部锚点。 */\nfunction containsMarker(value: unknown, prefix: string): boolean {\n if (typeof value === 'string') return value.includes(prefix);\n if (Array.isArray(value)) return value.some(item => containsMarker(item, prefix));\n if (value && typeof value === 'object') {\n return Object.values(value).some(item => containsMarker(item, prefix));\n }\n return false;\n}\n\n/** 在已有消息的首个文本块前加锚点;保留消息角色,避免打断 Pi 的工具调用关联。 */\nfunction anchorMessage(message: Message, marker: string, api: Api): Message {\n if (typeof message.content === 'string') {\n return { ...message, role: 'user', content: marker + message.content };\n }\n const content = [...message.content];\n const index = content.findIndex(block => block.type === 'text');\n let fallback = '';\n if (\n message.role === 'toolResult' &&\n !message.content\n .filter((block): block is TextContent => block.type === 'text')\n .map(block => block.text)\n .join('\\n')\n ) {\n const hasImages = message.content.some(block => block.type === 'image');\n // 锚点不能抑制 SDK 原本为无文本工具结果生成的占位说明。\n if (api === 'openai-completions' || api === 'mistral-conversations') {\n fallback = hasImages ? '(see attached image)' : '(no tool output)';\n } else if (!hasImages && ['openai-responses', 'openai-codex-responses'].includes(api)) {\n fallback = '(no tool output)';\n }\n }\n if (index === -1) {\n content.unshift({ type: 'text', text: marker + fallback });\n } else {\n const block = content[index] as TextContent;\n content[index] = { ...block, text: marker + block.text + fallback };\n }\n return { ...message, content } as Message;\n}\n\n/** 按实际协议和消息位置自动选择角色,不暴露额外的渠道配置。 */\nfunction systemMessageRole(\n context: Context,\n api: Api,\n message: PiLateSystemMessage\n): 'system' | 'user' {\n if (api === 'google-generative-ai') return 'user';\n if (api === 'anthropic-messages') {\n const previous = context.messages[message.beforeMessageIndex - 1];\n const next = context.messages[message.beforeMessageIndex];\n // toolResult 在 Anthropic 协议里是携带工具结果的 user 回合。\n return (previous?.role === 'user' || previous?.role === 'toolResult') &&\n (!next || next.role === 'assistant')\n ? 'system'\n : 'user';\n }\n return 'system';\n}\n\n/**\n * 保留普通消息角色,在服务商完成拆分/合并之后按内容锚点恢复 system;不符合协议约束时转为 user。\n * 锚点只进入待转换的文本,发送前必须恰好移除一次;不修改捕获输入、共享上下文或全局 fetch。\n */\nexport function createPiSystemMessageBridge(\n adapted: PiContextAdapterResult,\n api: Api\n): PiSystemMessageBridge {\n const { context, lateSystemMessages } = adapted;\n if (!lateSystemMessages.length) {\n return {\n context,\n restore: payload => payload,\n failure: undefined,\n assertRestored: () => {},\n };\n }\n\n let prefix: string;\n do {\n prefix = `__mvu_pi_system_anchor_${++nextAnchorNamespace}_`;\n } while (containsMarker(adapted, prefix));\n\n const groups = new Map();\n for (const message of lateSystemMessages) {\n const group = groups.get(message.beforeMessageIndex) ?? [];\n group.push(message);\n groups.set(message.beforeMessageIndex, group);\n }\n const anchors: SystemAnchor[] = [];\n const messages = context.messages.map((message, index) => {\n const group = groups.get(index);\n if (!group) return message;\n const anchor = { marker: `${prefix}${index}__`, messages: group };\n anchors.push(anchor);\n return anchorMessage(message, anchor.marker, api);\n });\n const tail = groups.get(context.messages.length) ?? [];\n const field =\n api === 'google-generative-ai'\n ? 'contents'\n : api === 'openai-responses' || api === 'openai-codex-responses'\n ? 'input'\n : 'messages';\n const hasUserFallback = lateSystemMessages.some(\n message => systemMessageRole(context, api, message) === 'user'\n );\n let failure: PiContextAdapterError | undefined;\n let applied = false;\n const restoredPayloads = new WeakSet();\n\n const mismatch = () =>\n new PiContextAdapterError(\n 'More source could not restore intermediate system messages in the provider payload.',\n 'system-payload-mismatch',\n lateSystemMessages[0].sourceIndex\n );\n const nativeMessages = (group: readonly PiLateSystemMessage[]) =>\n group.map(message =>\n api === 'google-generative-ai'\n ? { role: 'user', parts: [{ text: message.text }] }\n : { role: systemMessageRole(context, api, message), content: message.text }\n );\n\n /** 工具结果可能被 SDK 合并进同一 user;按工具结果块拆出插入边界,避免把指令提前到整组结果前。 */\n const splitToolResultGroups = (items: unknown[]): unknown[] =>\n items.flatMap(item => {\n if (!item || typeof item !== 'object' || !hasUserFallback) return [item];\n const message = item as Record;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const blocks = message[key];\n if (message.role !== 'user' || !Array.isArray(blocks)) return [item];\n const result: unknown[] = [];\n let current: unknown[] = [];\n for (const block of blocks) {\n const isToolResult =\n block &&\n typeof block === 'object' &&\n (block.type === 'tool_result' || block.functionResponse);\n if (isToolResult && current.length && containsMarker(block, prefix)) {\n result.push({ ...message, [key]: current });\n current = [];\n }\n current.push(block);\n }\n result.push({ ...message, [key]: current });\n return result;\n });\n\n /** 与酒馆兼容处理一致,user 回退后合并相邻 user,同时保留内容块、图片及工具结果的顺序。 */\n const mergeFallbackUsers = (items: unknown[]): unknown[] => {\n if (!hasUserFallback || (api !== 'anthropic-messages' && api !== 'google-generative-ai'))\n return items;\n const key = api === 'google-generative-ai' ? 'parts' : 'content';\n const asBlocks = (value: unknown) =>\n typeof value === 'string' ? [{ type: 'text', text: value }] : value;\n const merged: unknown[] = [];\n for (const item of items) {\n const current = item as Record;\n const previous = merged.at(-1) as Record | undefined;\n if (current?.role === 'user' && previous?.role === 'user') {\n const before = asBlocks(previous[key]);\n const after = asBlocks(current[key]);\n if (Array.isArray(before) && Array.isArray(after)) {\n merged[merged.length - 1] = { ...previous, [key]: [...before, ...after] };\n continue;\n }\n }\n merged.push(item);\n }\n return merged;\n };\n\n /** 只遍历原生消息字段;完整保留工具 ID、图片、缓存标记和其他服务商元数据。 */\n const clean = (value: unknown): CleanedNode => {\n if (typeof value === 'string') {\n if (!value.includes(prefix)) return { value, anchors: [] };\n const found: SystemAnchor[] = [];\n let text = value;\n for (const anchor of anchors) {\n const parts = text.split(anchor.marker);\n for (let index = 1; index < parts.length; index++) found.push(anchor);\n text = parts.join('');\n }\n return { value: text, anchors: found };\n }\n if (Array.isArray(value)) {\n const children = value.map(clean);\n return {\n value: children.filter(child => !child.remove).map(child => child.value),\n anchors: children.flatMap(child => child.anchors),\n };\n }\n if (!value || typeof value !== 'object') return { value, anchors: [] };\n const entries = Object.entries(value).map(([key, item]) => [key, clean(item)] as const);\n const found = entries.flatMap(([, child]) => child.anchors);\n if (!found.length) return { value, anchors: [] };\n const result = Object.fromEntries(entries.map(([key, child]) => [key, child.value]));\n const emptyAnchorBlock =\n result.text === '' &&\n (api === 'google-generative-ai' ||\n (typeof result.type === 'string' &&\n ['text', 'input_text', 'output_text'].includes(result.type)));\n // Responses 会把仅含工具调用的 assistant 上新增的锚点文本拆成单独消息,恢复后移除它。\n const emptyAnchorMessage =\n result.type === 'message' &&\n Array.isArray(result.content) &&\n result.content.length === 0;\n // Mistral 的纯工具助手消息省略 content,不能把临时定位块变成空内容数组。\n if (\n api === 'mistral-conversations' &&\n result.role === 'assistant' &&\n Array.isArray(result.content) &&\n !result.content.length\n ) {\n delete result.content;\n }\n return { value: result, anchors: found, remove: emptyAnchorBlock || emptyAnchorMessage };\n };\n\n return {\n context: { ...context, messages },\n get failure() {\n return failure;\n },\n restore(payload) {\n try {\n if (!payload || typeof payload !== 'object') throw mismatch();\n if (restoredPayloads.has(payload)) return payload;\n const original = payload as Record;\n const items = original[field];\n if (!Array.isArray(items)) throw mismatch();\n const seen = new Set();\n const restored: unknown[] = [];\n for (const item of splitToolResultGroups(items)) {\n const result = clean(item);\n if (result.anchors.length > 1) throw mismatch();\n const anchor = result.anchors[0];\n if (anchor) {\n if (seen.has(anchor) || anchors[seen.size] !== anchor) throw mismatch();\n seen.add(anchor);\n restored.push(...nativeMessages(anchor.messages));\n }\n if (!result.remove) restored.push(result.value);\n }\n if (seen.size !== anchors.length) throw mismatch();\n restored.push(...nativeMessages(tail));\n const result = { ...original, [field]: mergeFallbackUsers(restored) };\n if (containsMarker(result, prefix)) throw mismatch();\n restoredPayloads.add(result);\n applied = true;\n return result;\n } catch (error) {\n failure = error instanceof PiContextAdapterError ? error : mismatch();\n throw failure;\n }\n },\n assertRestored() {\n if (failure) throw failure;\n if (!applied) throw mismatch();\n },\n };\n}\n","import {\n normalizePiThinkingLevel,\n isPiThinkingEnabled,\n type PiThinkingLevel,\n} from './thinking_setting';\nimport { preparePiThinkingModel, resolvePiThinkingLevel } from './thinking';\nimport type { MvuSettings } from '@/store';\nimport { installPiAbortSignalPolyfills } from './abort_signal';\nimport {\n parsePiCustomExcludeBody,\n parsePiCustomHeaders,\n parsePiCustomIncludeBody,\n} from './config_parser';\nimport { PiContextAdapterError, toPiContext } from './context_adapter';\nimport { PiRequestAbortedError, registerPiRequestController } from './controller_registry';\nimport { getPiCredentialStore } from './credential_store';\nimport { assertGoogleProxyAdapterCompatible } from './google_proxy_adapter';\nimport {\n isPiDefaultProviderEndpoint,\n PiModelResolutionError,\n resolvePiModelFromExtraModelSettings,\n type ResolvedPiModel,\n} from './model_resolver';\nimport { getBrowserOAuthAuth } from './oauth';\nimport { createPiNonStreamingFetch } from './non_streaming_fetch';\nimport { isPiStreamingRequired } from './provider_target';\nimport { createPiPayloadTransform, transformPiPayload, type PiJsonSchema } from './payload';\nimport {\n createModels,\n createProvider,\n type Api,\n type ApiStreamOptions,\n type AssistantMessageEvent,\n type AuthContext,\n type CredentialStore,\n type FetchFunction,\n type SimpleStreamOptions,\n type ProviderAuth,\n type ProviderHeaders,\n type Tool,\n} from './pi_gateway';\nimport {\n createPiApiImplementations,\n resolvePiCapabilities,\n shouldUsePiCorsProxy,\n type PiApiCapabilities,\n type PiWireApi,\n} from './provider_registry';\nimport { fromPiAssistantMessage, PiResultAdapterError, toPiToolDefinition } from './result_adapter';\nimport { assertPiTokenBudget } from './token_preflight';\nimport { resolvePiToolChoice, type MvuToolChoice } from './tool_choice';\nimport { createPiSystemMessageBridge, type PiSystemMessageBridge } from './system_messages';\nimport {\n assertSillyTavernProxyAvailable,\n createSillyTavernProxyFetch,\n getSillyTavernProxyStatus,\n PiProxyUnavailableError,\n} from './sillytavern_proxy';\n\ninstallPiAbortSignalPolyfills();\n\nexport const PI_RUNTIME_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\n/** 额外解析允许使用的应答格式,供预检和原生载荷转换共同约束。 */\nexport type PiRuntimeResponseFormat = (typeof PI_RUNTIME_RESPONSE_FORMATS)[number];\n/** 一次额外模型请求读取的完整设置结构。 */\nexport type PiExtraModelSettings = MvuSettings['额外模型解析配置'];\n\n/** 运行时失败分类;配合 retryable 决定请求策略与界面提示。 */\nexport type PiRuntimeErrorCode =\n | 'invalid_configuration'\n | 'invalid_prompt'\n | 'missing_oauth_credential'\n | 'request_already_active'\n | 'unsupported_capability'\n | 'unsupported_image_input'\n | 'token_budget'\n | 'proxy_unavailable'\n | 'network'\n | 'provider'\n | 'protocol';\n\nexport class PiRuntimeError extends Error {\n /** 为运行时错误保存稳定类别和重试标记,供额外模型策略统一决策。 */\n constructor(\n readonly code: PiRuntimeErrorCode,\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'PiRuntimeError';\n }\n}\n\n/** 为串行和并发策略提供统一的不可重试错误判断。 */\nexport function isNonRetryablePiRuntimeError(error: unknown): boolean {\n return (\n (error instanceof PiRuntimeError && !error.retryable) ||\n error instanceof PiModelResolutionError ||\n error instanceof PiContextAdapterError\n );\n}\n\n/** 已通过服务商能力校验的采样选项,不含独立传入 SDK 的 temperature。 */\nexport type PiRuntimeSampling = Readonly<{\n topP?: number;\n topK?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n}>;\n\n/** 在重试批次开始前进行静态预检所需的设置、工具和可注入依赖。 */\nexport interface AssertPiRuntimeConfigurationInput {\n /** The whole persisted `额外模型解析配置` object. */\n settings: unknown;\n /** Defaults to `settings.应答格式`. */\n responseFormat?: unknown;\n /** Required by `格式化输出`; ignored by non-structured response modes. */\n jsonSchema?: PiJsonSchema;\n /** Required by `工具调用`; the MVU ToolDefinition constants can be passed directly. */\n tools?: readonly ToolDefinition[];\n /** Defaults to `required` in tool mode. */\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n signal?: AbortSignal;\n}\n\n/**\n * 重试前解析并冻结的连接、认证、能力与载荷配置。\n * 不包含提示词或请求级 system 锚点,可在同批次的每次尝试中复用。\n */\nexport interface PiRuntimePreflight {\n readonly resolution: ResolvedPiModel;\n readonly responseFormat: PiRuntimeResponseFormat;\n readonly capabilities: Readonly;\n readonly headers?: Readonly;\n readonly customIncludeBody?: Readonly>;\n readonly customExcludeBody?: readonly string[];\n readonly temperature?: number;\n readonly sampling: PiRuntimeSampling;\n readonly tools?: readonly Tool[];\n readonly toolChoice?: unknown;\n readonly jsonSchema?: PiJsonSchema;\n readonly credentialStore: CredentialStore;\n readonly fetch?: FetchFunction;\n readonly useCorsProxy: boolean;\n readonly streaming: boolean;\n readonly thinkingLevel: PiThinkingLevel;\n}\n\n/** 可选的 Pi 进度监听,运行时不会向此回调转发未经处理的错误事件。 */\nexport type PiRuntimeProgressCallback = (event: AssistantMessageEvent) => void | Promise;\n\n/** 执行单次请求的输入:预检快照、捕获消息、取消编号及可选进度监听。 */\nexport interface RunPiRequestInput {\n /** Supply this when configuration was validated once outside the retry loop. */\n preflight?: PiRuntimePreflight;\n /** Required only when `preflight` is omitted. */\n settings?: unknown;\n responseFormat?: unknown;\n jsonSchema?: PiJsonSchema;\n tools?: readonly ToolDefinition[];\n toolChoice?: MvuToolChoice;\n credentialStore?: CredentialStore;\n fetch?: FetchFunction;\n messages: readonly SillyTavern.SendingMessage[];\n generationId: string;\n signal?: AbortSignal;\n onProgress?: PiRuntimeProgressCallback;\n}\n\n/** 设置根对象完成形状检查后、各字段尚未解析时的中间结构。 */\ntype ExtraModelSettingsRecord = Record & {\n pi?: unknown;\n};\n\nconst BROWSER_AUTH_CONTEXT: AuthContext = Object.freeze({\n env: async () => undefined,\n fileExists: async () => false,\n});\n\n/** 确认设置是非空、非数组对象,再读取请求字段。 */\nfunction isPlainObject(value: unknown): value is Record {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** 校验额外模型设置的根结构,结构错误在构造请求前失败。 */\nfunction requireSettings(value: unknown): ExtraModelSettingsRecord {\n if (!isPlainObject(value)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source extra-model settings must be an object'\n );\n }\n return value;\n}\n\n/** 优先采用本次显式应答格式,否则读取设置,并拒绝未知格式。 */\nfunction resolveResponseFormat(\n settings: ExtraModelSettingsRecord,\n override: unknown\n): PiRuntimeResponseFormat {\n const value = override ?? settings['应答格式'];\n if (\n typeof value !== 'string' ||\n !PI_RUNTIME_RESPONSE_FORMATS.some(candidate => candidate === value)\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source response format is missing or unsupported'\n );\n }\n return value as PiRuntimeResponseFormat;\n}\n\n/** 读取可选字符串配置;缺失返回空字符串,类型错误不能隐式转换。 */\nfunction optionalStringField(source: Record, name: string): string {\n const value = source[name];\n if (value === undefined) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new PiRuntimeError('invalid_configuration', `More source ${name} must be a string`);\n }\n return value;\n}\n\n/** 校验可选数值的范围、有限性及整数要求,缺失字段保持未设置。 */\nfunction optionalNumberField(\n source: Record,\n name: string,\n minimum: number,\n maximum: number,\n integer = false\n): number | undefined {\n const value = source[name];\n if (value === undefined) {\n return undefined;\n }\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n value < minimum ||\n value > maximum ||\n (integer && !Number.isInteger(value))\n ) {\n const kind = integer ? 'integer' : 'number';\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source ${name} must be a ${kind} between ${minimum} and ${maximum}`\n );\n }\n return value;\n}\n\n/** 仅解析当前模型支持的采样字段,并处理协议或模型特有的组合限制。 */\nfunction resolveSampling(\n settings: ExtraModelSettingsRecord,\n capabilities: Readonly,\n resolution: ResolvedPiModel\n): {\n temperature?: number;\n sampling: PiRuntimeSampling;\n} {\n let temperature = capabilities.temperature\n ? optionalNumberField(\n settings,\n '温度',\n capabilities.temperatureRange[0],\n capabilities.temperatureRange[1]\n )\n : undefined;\n let topP = capabilities.sampling.topP\n ? optionalNumberField(settings, 'top_p', 0, 1)\n : undefined;\n const topK = capabilities.sampling.topK\n ? optionalNumberField(settings, 'top_k', 0, 500, true)\n : undefined;\n const frequencyPenalty = capabilities.sampling.frequencyPenalty\n ? optionalNumberField(settings, '频率惩罚', -2, 2)\n : undefined;\n const presencePenalty = capabilities.sampling.presencePenalty\n ? optionalNumberField(settings, '存在惩罚', -2, 2)\n : undefined;\n if (\n resolution.definition.key === 'anthropic' &&\n isPiDefaultProviderEndpoint(resolution.definition, resolution.model.baseUrl) &&\n temperature !== undefined &&\n topP !== undefined\n ) {\n // Native Claude models require at most one of these samplers. MVU persists both\n // controls with 1 as their default, so omit the untouched one from the wire request.\n if (topP === 1) {\n topP = undefined;\n } else if (temperature === 1) {\n temperature = undefined;\n } else {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source Anthropic sampling requires temperature or top_p; reset one to 1.'\n );\n }\n }\n return {\n temperature,\n sampling: Object.freeze({\n ...(topP === undefined ? {} : { topP }),\n // zero is the existing \"unset\" value and is invalid for APIs that send top_k.\n ...(topK === undefined || topK === 0 ? {} : { topK }),\n ...(frequencyPenalty === undefined ? {} : { frequencyPenalty }),\n ...(presencePenalty === undefined ? {} : { presencePenalty }),\n }),\n };\n}\n\n/** 复制本次结构化输出 Schema,避免执行时修改调用方定义。 */\nfunction cloneJsonSchema(schema: PiJsonSchema | undefined): PiJsonSchema | undefined {\n return schema === undefined ? undefined : structuredClone(schema);\n}\n\n/** 解析当前模型的可信协议能力,注册信息缺失时终止配置预检。 */\nfunction capabilityFor(\n resolution: ResolvedPiModel,\n thinkingLevel: PiThinkingLevel\n): Readonly {\n const capability = resolvePiCapabilities(\n resolution.definition,\n resolution.model.api as PiWireApi,\n {\n model: resolution.model,\n catalogHit: resolution.catalogHit,\n thinkingLevel,\n }\n );\n if (!capability) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n `More source API '${resolution.model.api}' has no registered runtime capability metadata`\n );\n }\n return capability;\n}\n\n/** 在捕获提示词前确认所选应答格式受到支持,避免重复发送必然失败的请求。 */\nfunction assertResponseCapability(\n responseFormat: PiRuntimeResponseFormat,\n capabilities: Readonly,\n api: Api\n): void {\n if (responseFormat === '工具调用' && !capabilities.tools) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support tool calls`\n );\n }\n const structuredOutputUnsupported =\n responseFormat === '格式化输出' && !capabilities.structuredOutput;\n const jsonObjectOutputUnsupported =\n responseFormat === '格式化输出(v4兼容)' && !capabilities.jsonObjectOutput;\n if (structuredOutputUnsupported || jsonObjectOutputUnsupported) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source API '${api}' does not support native structured output`\n );\n }\n}\n\n/** 仅在工具调用模式校验并复制工具定义,其他模式不携带业务工具。 */\nfunction prepareTools(\n responseFormat: PiRuntimeResponseFormat,\n definitions: readonly ToolDefinition[] | undefined\n): readonly Tool[] | undefined {\n if (responseFormat !== '工具调用') {\n return undefined;\n }\n if (!definitions?.length) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source tool-call mode requires at least one tool definition'\n );\n }\n return Object.freeze(\n definitions.map(definition =>\n toPiToolDefinition(definition, {\n constrainedSampling: { type: 'json_schema', strict: 'prefer' },\n })\n )\n );\n}\n\n/** JSON Schema 模式要求显式结构定义,其他应答格式不继承残留 Schema。 */\nfunction prepareJsonSchema(\n responseFormat: PiRuntimeResponseFormat,\n schema: PiJsonSchema | undefined\n): PiJsonSchema | undefined {\n if (responseFormat === '格式化输出' && schema === undefined) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source structured output requires a JSON schema'\n );\n }\n return responseFormat === '格式化输出' ? cloneJsonSchema(schema) : undefined;\n}\n\n/** 用空载荷预演请求转换,提前发现受保护字段覆盖及格式配置错误。 */\nfunction validatePayloadConfiguration(preflight: {\n resolution: ResolvedPiModel;\n responseFormat: PiRuntimeResponseFormat;\n customIncludeBody?: Readonly>;\n customExcludeBody?: readonly string[];\n sampling: PiRuntimeSampling;\n jsonSchema?: PiJsonSchema;\n}): void {\n try {\n transformPiPayload(\n {},\n {\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n }\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source payload configuration is invalid'\n );\n }\n}\n\n/** 确认 OAuth 来源已有可用类型的凭证;到期刷新由后续 Pi 认证流程处理。 */\nasync function assertOAuthCredential(\n resolution: ResolvedPiModel,\n credentialStore: CredentialStore,\n signal?: AbortSignal\n): Promise {\n if (resolution.authType !== 'oauth') {\n return;\n }\n signal?.throwIfAborted();\n const credential = await credentialStore.read(resolution.definition.providerId, { signal });\n signal?.throwIfAborted();\n if (!credential || credential.type !== 'oauth') {\n throw new PiRuntimeError(\n 'missing_oauth_credential',\n `More source provider '${resolution.definition.providerId}' is not logged in`\n );\n }\n}\n\n/**\n * 一次性校验不依赖提示词的连接、认证、能力、预算参数和请求覆盖,并冻结本轮快照。\n * 应在串行或并发重试前调用,避免把固定配置错误误判为可重试的服务商失败。\n */\nexport async function assertPiRuntimeConfiguration(\n input: AssertPiRuntimeConfigurationInput\n): Promise {\n input.signal?.throwIfAborted();\n const settings = requireSettings(input.settings);\n const resolution = resolvePiModelFromExtraModelSettings(settings);\n const responseFormat = resolveResponseFormat(settings, input.responseFormat);\n const requestedThinkingLevel = normalizePiThinkingLevel(\n isPlainObject(settings.pi) ? settings.pi.thinkingLevel : undefined\n );\n resolution.model = preparePiThinkingModel(\n resolution.model,\n resolution.catalogHit,\n requestedThinkingLevel\n );\n // Validation, sampling and dispatch must all use the same effective level.\n const thinkingLevel = resolvePiThinkingLevel(requestedThinkingLevel, resolution.model);\n const capabilities = capabilityFor(resolution, thinkingLevel);\n const streaming =\n settings['兼容假流式'] === true || isPiStreamingRequired(resolution.model.api);\n if (streaming && !capabilities.streaming) {\n throw new PiRuntimeError(\n 'unsupported_capability',\n `More source model '${resolution.model.id}' does not support streaming requests`\n );\n }\n assertResponseCapability(responseFormat, capabilities, resolution.model.api);\n\n const piSettings = settings.pi;\n if (!isPlainObject(piSettings)) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source connection settings must be an object'\n );\n }\n const useCorsProxy = shouldUsePiCorsProxy(\n resolution.definition,\n resolution.model.api,\n optionalStringField(piSettings, 'endpoint'),\n piSettings.useProxy\n );\n let headers: ProviderHeaders | undefined;\n let customIncludeBody: Record | undefined;\n let customExcludeBody: string[] | undefined;\n try {\n headers = parsePiCustomHeaders(optionalStringField(piSettings, 'customHeaders'));\n customIncludeBody = parsePiCustomIncludeBody(\n optionalStringField(piSettings, 'customIncludeBody')\n );\n customExcludeBody = parsePiCustomExcludeBody(\n optionalStringField(piSettings, 'customExcludeBody')\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error\n ? error.message\n : 'More source custom request configuration is invalid'\n );\n }\n const { temperature, sampling } = resolveSampling(settings, capabilities, resolution);\n let tools: readonly Tool[] | undefined;\n let toolChoice: unknown;\n try {\n tools = prepareTools(responseFormat, input.tools);\n toolChoice =\n responseFormat === '工具调用'\n ? resolvePiToolChoice(resolution.model.api, input.toolChoice ?? 'required')\n : undefined;\n } catch (error) {\n if (error instanceof PiRuntimeError) {\n throw error;\n }\n throw new PiRuntimeError(\n 'invalid_configuration',\n error instanceof Error ? error.message : 'More source tool configuration is invalid'\n );\n }\n if (isPiThinkingEnabled(thinkingLevel) && resolution.model.api === 'anthropic-messages') {\n if (toolChoice !== undefined && toolChoice !== 'auto' && toolChoice !== 'none') {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic thinking does not support forced tool calls. Use chat/structured output or turn thinking off.'\n );\n }\n if (\n !(\n resolution.model.compat &&\n 'forceAdaptiveThinking' in resolution.model.compat &&\n resolution.model.compat.forceAdaptiveThinking === true\n ) &&\n resolution.effectiveMaxTokens < 2048\n ) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'Anthropic budget-based thinking requires at least 2048 max reply tokens.'\n );\n }\n }\n const jsonSchema = prepareJsonSchema(responseFormat, input.jsonSchema);\n const credentialStore = input.credentialStore ?? getPiCredentialStore(piSettings);\n await assertOAuthCredential(resolution, credentialStore, input.signal);\n\n const preflight: PiRuntimePreflight = Object.freeze({\n resolution,\n responseFormat,\n capabilities,\n ...(headers === undefined ? {} : { headers: Object.freeze({ ...headers }) }),\n ...(customIncludeBody === undefined\n ? {}\n : { customIncludeBody: Object.freeze(structuredClone(customIncludeBody)) }),\n ...(customExcludeBody === undefined\n ? {}\n : { customExcludeBody: Object.freeze([...customExcludeBody]) }),\n temperature: capabilities.temperature ? temperature : undefined,\n sampling,\n tools,\n toolChoice,\n jsonSchema,\n credentialStore,\n fetch: input.fetch,\n useCorsProxy,\n streaming,\n thinkingLevel,\n });\n validatePayloadConfiguration(preflight);\n return preflight;\n}\n\n/** 只为本次解析出的模型和协议创建 Pi 服务商,并接入浏览器 OAuth 或请求密钥。 */\nfunction createRuntimeProvider(preflight: PiRuntimePreflight) {\n const { resolution } = preflight;\n let auth: ProviderAuth;\n if (resolution.authType === 'oauth') {\n auth = {\n oauth: getBrowserOAuthAuth(resolution.definition.providerId, {\n fetch: preflight.fetch,\n }),\n };\n } else {\n auth = {\n apiKey: {\n name: `${resolution.definition.displayName.en} API key`,\n /** 将本次设置提供的 API Key 转为 Pi 认证结果,解析前检查请求是否取消。 */\n async resolve({ credential, signal }) {\n signal.throwIfAborted();\n if (!credential?.key) {\n return undefined;\n }\n return {\n auth: { apiKey: credential.key },\n source: 'extra-model settings',\n };\n },\n },\n };\n }\n\n return createProvider({\n id: resolution.definition.providerId,\n name: resolution.definition.displayName.en,\n baseUrl: resolution.model.baseUrl,\n auth,\n models: [resolution.model],\n api: createPiApiImplementations(resolution.definition),\n });\n}\n\n/** 检查用户消息和工具返回值是否包含图片,供能力预检使用。 */\nfunction contextHasImages(context: ReturnType['context']): boolean {\n return context.messages.some(message => {\n if (message.role === 'user') {\n return (\n Array.isArray(message.content) &&\n message.content.some(content => content.type === 'image')\n );\n }\n if (message.role === 'toolResult') {\n return message.content.some(content => content.type === 'image');\n }\n return false;\n });\n}\n\n/** 提示词含图片时同时核对协议能力和模型输入类型,发送前拒绝不支持的组合。 */\nfunction assertImageCapability(\n preflight: PiRuntimePreflight,\n context: ReturnType['context']\n): void {\n if (!contextHasImages(context)) {\n return;\n }\n if (!preflight.capabilities.imageInput || !preflight.resolution.model.input.includes('image')) {\n throw new PiRuntimeError(\n 'unsupported_image_input',\n `More source model '${preflight.resolution.model.id}' does not support image input`\n );\n }\n}\n\n/** 仅保留需要写入原生载荷的两种格式化输出模式。 */\nfunction nativeStructuredFormat(\n responseFormat: PiRuntimeResponseFormat\n): '格式化输出' | '格式化输出(v4兼容)' | undefined {\n return responseFormat === '格式化输出' || responseFormat === '格式化输出(v4兼容)'\n ? responseFormat\n : undefined;\n}\n\n/**\n * 按本轮快照组合认证、采样、代理、非流式传输及载荷转换回调。\n * Codex 经代理时强制使用 SSE,所有请求层共享同一取消信号。\n */\nfunction createStreamOptions(\n preflight: PiRuntimePreflight,\n signal: AbortSignal,\n systemMessages: PiSystemMessageBridge\n): ApiStreamOptions {\n const providerFetch = preflight.useCorsProxy\n ? createSillyTavernProxyFetch({\n baseUrl: preflight.resolution.model.baseUrl,\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : preflight.fetch;\n return {\n signal,\n apiKey: preflight.resolution.apiKey,\n fetch: preflight.streaming\n ? providerFetch\n : createPiNonStreamingFetch(preflight.resolution.model.api as PiWireApi, providerFetch),\n headers: preflight.headers === undefined ? undefined : { ...preflight.headers },\n temperature: preflight.capabilities.temperature ? preflight.temperature : undefined,\n maxTokens: preflight.resolution.effectiveMaxTokens,\n ...(preflight.toolChoice === undefined ? {} : { toolChoice: preflight.toolChoice }),\n ...(preflight.useCorsProxy && preflight.resolution.model.api === 'openai-codex-responses'\n ? { transport: 'sse' as const }\n : {}),\n onPayload: payload =>\n systemMessages.restore(\n createPiPayloadTransform({\n api: preflight.resolution.model.api,\n responseFormat: nativeStructuredFormat(preflight.responseFormat),\n jsonSchema: preflight.jsonSchema,\n customIncludeBody:\n preflight.customIncludeBody === undefined\n ? undefined\n : { ...preflight.customIncludeBody },\n customExcludeBody: preflight.customExcludeBody,\n sampling: preflight.sampling,\n })(payload)\n ),\n };\n}\n\n/** 识别常见浏览器 fetch、网络和 CORS 失败,供运行时选择网络错误类别。 */\nfunction isNetworkFailure(error: unknown): boolean {\n const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';\n return /failed to fetch|fetch failed|network\\s*error|networkerror|load failed|cors/i.test(\n message\n );\n}\n\n/** 通过稳定错误码识别 Google 注入点不兼容,避免直接导入适配器产生循环依赖。 */\nfunction isGoogleProxyAdapterCompatibilityFailure(error: unknown): boolean {\n // Keep the runtime boundary independent from the Google adapter module: provider registration\n // imports the runtime-facing gateway, so importing the adapter here would create a cycle.\n return (\n error instanceof Error &&\n (error as Error & { code?: unknown }).code === 'google_proxy_adapter_incompatible'\n );\n}\n\n/**\n * 将已知底层失败归类为运行时错误并保留重试语义。\n * 未知错误可能附带请求头或正文,只返回固定提示,不保留原始 cause。\n */\nfunction normalizeRuntimeFailure(error: unknown): Error {\n if (error instanceof PiRuntimeError || error instanceof PiContextAdapterError) {\n return error;\n }\n if (error instanceof PiProxyUnavailableError) {\n return new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n );\n }\n if (isGoogleProxyAdapterCompatibilityFailure(error)) {\n return new PiRuntimeError(\n 'protocol',\n 'The installed Google Generative AI SDK is incompatible with the browser proxy transport required by this build.'\n );\n }\n if (error instanceof PiResultAdapterError && error.code === 'aborted') {\n // The generation id is supplied by the request-level abort conversion below.\n return error;\n }\n if (\n (error instanceof PiResultAdapterError && error.code === 'network') ||\n isNetworkFailure(error)\n ) {\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n if (error instanceof PiResultAdapterError) {\n switch (error.code) {\n case 'provider-error':\n return new PiRuntimeError('provider', 'More source request failed.', true);\n case 'deferred':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned a deferred response, which is not supported.'\n );\n case 'empty-response':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned no usable response content.'\n );\n case 'invalid-tool-call':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider returned an invalid tool call.'\n );\n case 'length':\n return new PiRuntimeError(\n 'protocol',\n 'More source provider response was truncated because it reached the output limit.'\n );\n case 'aborted':\n return error;\n case 'network':\n return new PiRuntimeError(\n 'network',\n 'The browser could not complete the More source request. Check the endpoint, network access, and CORS policy.',\n true\n );\n }\n }\n // SDK/fetch implementations may attach response bodies, headers, or request configuration to\n // arbitrary errors. Do not surface the unknown error or retain it as a cause.\n return new PiRuntimeError('provider', 'More source request failed.', true);\n}\n\n/** 要求非空生成编号,使取消登记与提示词捕获可以准确关联。 */\nfunction assertGenerationId(generationId: string): void {\n if (!generationId.trim()) {\n throw new PiRuntimeError(\n 'invalid_configuration',\n 'More source request generationId must not be empty'\n );\n }\n}\n\n/**\n * 将捕获的酒馆提示词转换为 Pi 上下文,检查图片及 token 预算后执行所选服务商请求。\n * 统一处理代理检查、取消登记、错误分类和结果转换;返回值不自动追加到聊天或上下文。\n */\nexport async function runPiRequest(\n input: RunPiRequestInput\n): Promise {\n assertGenerationId(input.generationId);\n let preflight: PiRuntimePreflight;\n try {\n preflight =\n input.preflight ??\n (await assertPiRuntimeConfiguration({\n settings: input.settings,\n responseFormat: input.responseFormat,\n jsonSchema: input.jsonSchema,\n tools: input.tools,\n toolChoice: input.toolChoice,\n credentialStore: input.credentialStore,\n fetch: input.fetch,\n signal: input.signal,\n }));\n } catch (error) {\n if (input.signal?.aborted) {\n throw new PiRequestAbortedError(input.generationId, input.signal.reason ?? error);\n }\n throw error;\n }\n\n let registration: ReturnType;\n try {\n registration = registerPiRequestController(input.generationId, input.signal);\n } catch (error) {\n throw new PiRuntimeError(\n 'request_already_active',\n error instanceof Error ? error.message : 'More source request is already active'\n );\n }\n let systemMessages: PiSystemMessageBridge | undefined;\n try {\n if (registration.signal.aborted) {\n throw new PiRequestAbortedError(input.generationId, registration.signal.reason);\n }\n\n if (preflight.useCorsProxy) {\n try {\n await assertSillyTavernProxyAvailable({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n signal: registration.signal,\n force: true,\n });\n } catch (error) {\n throw normalizeRuntimeFailure(error);\n }\n }\n\n let adapted: ReturnType;\n try {\n adapted = toPiContext(input.messages);\n } catch (error) {\n if (error instanceof PiContextAdapterError) {\n throw new PiRuntimeError('invalid_prompt', error.message);\n }\n throw error;\n }\n const { context, lateSystemMessages } = adapted;\n if (preflight.tools !== undefined) {\n context.tools = [...preflight.tools];\n }\n assertImageCapability(preflight, context);\n systemMessages = createPiSystemMessageBridge(adapted, preflight.resolution.model.api);\n try {\n assertPiTokenBudget(\n context,\n preflight.resolution.effectiveContextWindow,\n preflight.resolution.effectiveMaxTokens,\n undefined,\n lateSystemMessages\n );\n } catch (error) {\n throw new PiRuntimeError(\n 'token_budget',\n error instanceof Error ? error.message : 'More source token preflight failed'\n );\n }\n\n const models = createModels({\n credentials: preflight.credentialStore,\n authContext: BROWSER_AUTH_CONTEXT,\n });\n models.setProvider(createRuntimeProvider(preflight));\n const options = createStreamOptions(preflight, registration.signal, systemMessages);\n if (\n preflight.resolution.model.api === 'google-generative-ai' &&\n options.fetch &&\n options.fetch !== globalThis.fetch\n ) {\n // Pi wraps provider setup in lazyStream and would otherwise erase the compatibility\n // error code. Check the instance-only SDK seam before entering that boundary.\n assertGoogleProxyAdapterCompatible();\n }\n let stream;\n if (preflight.thinkingLevel === 'default') {\n stream = models.stream(preflight.resolution.model, systemMessages.context, options);\n } else {\n const model = {\n ...preflight.resolution.model,\n // The shared answer/thinking budget must stay inside the preflight reservation.\n maxTokens: preflight.resolution.effectiveMaxTokens,\n };\n // Pi 1.0's simple adapters forward native toolChoice unchanged, although the public\n // simple type only declares auto/none. Keep required/named choices from our resolver.\n // Real-adapter transport tests cover this compatibility boundary.\n const simpleOptions = {\n ...options,\n reasoning: preflight.thinkingLevel === 'off' ? undefined : preflight.thinkingLevel,\n } as SimpleStreamOptions;\n stream = models.streamSimple(model, systemMessages.context, simpleOptions);\n }\n\n for await (const event of stream) {\n // Provider error events can contain raw response bodies, request headers, or echoed\n // credentials. The normalized terminal error below is the public failure boundary.\n if (event.type !== 'error') {\n await input.onProgress?.(event);\n }\n }\n const message = await stream.result();\n if (registration.signal.aborted || message.stopReason === 'aborted') {\n throw new PiRequestAbortedError(\n input.generationId,\n registration.signal.aborted ? registration.signal.reason : undefined\n );\n }\n if (systemMessages.failure) throw systemMessages.failure;\n const result = fromPiAssistantMessage(message);\n systemMessages.assertRestored();\n return result;\n } catch (error) {\n const wasAborted =\n registration.signal.aborted ||\n error instanceof PiRequestAbortedError ||\n (error instanceof PiResultAdapterError && error.code === 'aborted');\n if (wasAborted) {\n throw error instanceof PiRequestAbortedError\n ? error\n : new PiRequestAbortedError(input.generationId, error);\n }\n const requestError = systemMessages?.failure ?? error;\n const proxy_status = preflight.useCorsProxy\n ? getSillyTavernProxyStatus({\n ...(preflight.fetch === undefined ? {} : { fetch: preflight.fetch }),\n })\n : 'unchecked';\n const normalizedError =\n proxy_status === 'disabled' || proxy_status === 'unavailable'\n ? new PiRuntimeError(\n 'proxy_unavailable',\n 'SillyTavern CORS proxy is not enabled or unavailable.'\n )\n : normalizeRuntimeFailure(requestError);\n if (!registration.signal.aborted) {\n // Abort listeners must not observe the raw provider error either.\n registration.controller.abort(normalizedError);\n }\n throw normalizedError;\n } finally {\n registration.release();\n }\n}\n","import type { Api } from '@earendil-works/pi-ai';\n\n/** MVU 接受的工具选择规则;required/any 均表示必须调用工具,发送前按协议转换。 */\nexport type MvuToolChoice =\n | 'auto'\n | 'none'\n | 'required'\n | 'any'\n | { type: 'function'; function: { name: string } };\n\n/**\n * 把 MVU 工具选择转换为当前协议接受的形式。\n * 保留 auto/none,转换 required/any 与具名调用;不支持具名选择的协议提前拒绝。\n */\nexport function resolvePiToolChoice(api: Api, choice: MvuToolChoice | undefined): unknown {\n const normalized = choice ?? 'auto';\n if (normalized === 'auto' || normalized === 'none') {\n return normalized;\n }\n\n const named = typeof normalized === 'object' ? normalized.function.name.trim() : undefined;\n if (named !== undefined && !named) {\n throw new Error('More source named tool choice requires a tool name');\n }\n\n if (api === 'openai-completions') {\n return named\n ? { type: 'function', function: { name: named } }\n : normalized === 'any'\n ? 'required'\n : normalized;\n }\n if (api === 'openai-responses') {\n return named ? { type: 'function', name: named } : 'required';\n }\n if (api === 'openai-codex-responses') {\n if (named) {\n throw new Error(\n \"More source API 'openai-codex-responses' does not support a named tool choice\"\n );\n }\n return 'required';\n }\n if (api === 'anthropic-messages') {\n return named ? { type: 'tool', name: named } : 'any';\n }\n if (api === 'google-generative-ai') {\n if (named) {\n throw new Error(\n \"More source API 'google-generative-ai' does not support a named tool choice\"\n );\n }\n return 'any';\n }\n if (api === 'mistral-conversations') {\n return named ? { type: 'function', function: { name: named } } : normalized;\n }\n throw new Error(`More source API '${api}' does not support required tool choice`);\n}\n","import type { FetchFunction } from './pi_gateway';\n\nconst PROXY_DISABLED_MESSAGE =\n 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';\nconst PROXY_PROBE_BODY = 'mvu-st-cors-proxy-probe';\nconst PROXY_PROBE_TARGET = `data:text/plain,${PROXY_PROBE_BODY}`;\nconst PROXY_PROBE_TIMEOUT_MS = 5_000;\n\n/** 酒馆 CORS 代理的探测状态,区分未检查、检查中和三种已完成结果。 */\nexport type SillyTavernProxyStatus =\n | 'unchecked'\n | 'checking'\n | 'enabled'\n | 'disabled'\n | 'unavailable';\n\n/** 一次探测完成后可缓存的结果:已启用、明确关闭或当前不可用。 */\nexport type SillyTavernProxyTerminalStatus = Extract<\n SillyTavernProxyStatus,\n 'enabled' | 'disabled' | 'unavailable'\n>;\n\n/** 代理探测的环境与缓存选项,允许测试注入传输和酒馆源地址。 */\nexport interface SillyTavernProxyProbeOptions {\n /** 默认使用页面的 fetch 实现。 */\n fetch?: FetchFunction;\n /** 默认从页面基础地址或 location 解析酒馆源地址。 */\n origin?: string;\n /** 仅取消当前调用方的等待,不中止其他调用方共用的探测。 */\n signal?: AbortSignal;\n /** 忽略已缓存的完成结果,重新检查酒馆代理路由。 */\n force?: boolean;\n}\n\n/** 创建代理 fetch 所需的配置,限定携带凭证请求能够访问的目标范围。 */\nexport interface SillyTavernProxyFetchOptions extends SillyTavernProxyProbeOptions {\n /**\n * 服务商 API 基础地址;返回的 fetch 仅允许向同源、同路径或下级路径转发凭证。\n */\n baseUrl: string | URL;\n}\n\nexport class PiProxyUnavailableError extends Error {\n readonly code = 'proxy_unavailable';\n readonly retryable = false;\n\n /** 区分代理未启用和不可用,并标记为发送前应终止的配置类失败。 */\n constructor(readonly status: Exclude) {\n super(\n status === 'disabled'\n ? 'SillyTavern CORS proxy is not enabled'\n : 'SillyTavern CORS proxy is unavailable'\n );\n this.name = 'PiProxyUnavailableError';\n }\n}\n\n/** 与 Pi fetch 契约保持一致的请求目标,可为 URL、字符串或 Request。 */\ntype FetchInput = Parameters[0];\n\n/** 按 fetch 实现及酒馆源地址共享的探测记录,用 promise 合并同时发起的检查。 */\ntype ProbeEntry = {\n status: SillyTavernProxyStatus;\n promise?: Promise;\n};\n\nlet probe_cache = new WeakMap>();\n\n/** 读取注入或全局 fetch;浏览器缺少请求能力时返回 undefined。 */\nfunction resolveFetch(fetch_override?: FetchFunction): FetchFunction | undefined {\n const candidate = fetch_override ?? globalThis.fetch;\n return typeof candidate === 'function' ? candidate : undefined;\n}\n\n/** 从覆盖值或文档基础地址解析酒馆源地址,兼容 origin 为 null 的 srcdoc 脚本。 */\nfunction resolveOrigin(origin_override?: string): string | undefined {\n // Slash normally hosts scripts in srcdoc, where location.origin is \"null\" despite\n // inheriting the parent's origin. Use the same document base as relative browser fetch;\n // Slash's Blob mode also supplies a pointing to SillyTavern.\n const value = origin_override ?? globalThis.document?.baseURI ?? globalThis.location?.origin;\n if (!value) {\n return undefined;\n }\n try {\n const origin = new URL(value).origin;\n return origin === 'null' ? undefined : origin;\n } catch {\n return undefined;\n }\n}\n\n/** 按 fetch 实现与酒馆源地址隔离探测缓存,复用同一目标的进行中检查。 */\nfunction getProbeEntry(fetch_impl: FetchFunction, origin: string): ProbeEntry {\n let entries = probe_cache.get(fetch_impl);\n if (!entries) {\n entries = new Map();\n probe_cache.set(fetch_impl, entries);\n }\n let entry = entries.get(origin);\n if (!entry) {\n entry = { status: 'unchecked' };\n entries.set(origin, entry);\n }\n return entry;\n}\n\n/** 将完整目标地址编码进酒馆通用 CORS 代理路径。 */\nfunction proxyUrl(target: string): string {\n return `/proxy/${encodeURIComponent(target)}`;\n}\n\n/** 忽略首尾空白后精确核对探测正文,避免把其他页面误识别为代理结果。 */\nfunction exactResponseText(actual: string, expected: string): boolean {\n return actual.trim() === expected;\n}\n\n/** 通过本地 data URL 和超时限制探测代理,区分启用、关闭和暂时不可达。 */\nasync function performProbe(fetch_impl: FetchFunction): Promise {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROXY_PROBE_TIMEOUT_MS);\n try {\n // node-fetch resolves this data URL locally. Unlike a same-origin circular sentinel, it\n // remains reliable when SillyTavern is behind an HTTPS reverse proxy.\n const response = await fetch_impl(proxyUrl(PROXY_PROBE_TARGET), {\n method: 'GET',\n credentials: 'same-origin',\n cache: 'no-store',\n signal: controller.signal,\n });\n const body = await response.text();\n if (response.status === 200 && exactResponseText(body, PROXY_PROBE_BODY)) {\n return 'enabled';\n }\n if (response.status === 404 && exactResponseText(body, PROXY_DISABLED_MESSAGE)) {\n return 'disabled';\n }\n return 'unavailable';\n } catch {\n return 'unavailable';\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/** 保留信号原始取消原因,缺失时补齐标准 AbortError。 */\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n}\n\n/** 允许调用方取消对共享探测的等待,不中断其他调用方共用的探测请求。 */\nfunction waitForProbe(\n promise: Promise,\n signal?: AbortSignal\n): Promise {\n if (!signal) {\n return promise;\n }\n if (signal.aborted) {\n return Promise.reject(abortReason(signal));\n }\n return new Promise((resolve, reject) => {\n /** 解除当前等待者的监听并拒绝其 Promise,保留共享探测继续运行。 */\n const on_abort = () => {\n cleanup();\n reject(abortReason(signal));\n };\n /** 移除当前等待者的取消监听,避免探测结束后残留引用。 */\n const cleanup = () => signal.removeEventListener('abort', on_abort);\n signal.addEventListener('abort', on_abort, { once: true });\n promise.then(\n status => {\n cleanup();\n resolve(status);\n },\n error => {\n cleanup();\n reject(error);\n }\n );\n });\n}\n\n/** 只读取缓存中的代理状态,不触发新的网络探测。 */\nexport function getSillyTavernProxyStatus(\n options: SillyTavernProxyProbeOptions = {}\n): SillyTavernProxyStatus {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return 'unavailable';\n }\n return getProbeEntry(fetch_impl, origin).status;\n}\n\n/**\n * 合并并发探测并缓存确定的启用或关闭状态,暂时失败允许重试。\n * force 可重新核对服务端状态;单个等待者取消不影响共享探测。\n */\nexport function probeSillyTavernProxy(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n return Promise.resolve('unavailable');\n }\n\n const entry = getProbeEntry(fetch_impl, origin);\n if (entry.promise) {\n return waitForProbe(entry.promise, options.signal);\n }\n if (!options.force && (entry.status === 'enabled' || entry.status === 'disabled')) {\n return waitForProbe(Promise.resolve(entry.status), options.signal);\n }\n\n entry.status = 'checking';\n entry.promise = performProbe(fetch_impl).then(status => {\n entry.status = status;\n entry.promise = undefined;\n return status;\n });\n return waitForProbe(entry.promise, options.signal);\n}\n\n/** 在发送带凭证的服务商请求前确认代理可用,不可用时立即报错。 */\nexport async function assertSillyTavernProxyAvailable(\n options: SillyTavernProxyProbeOptions = {}\n): Promise {\n const status = await probeSillyTavernProxy(options);\n if (status !== 'enabled') {\n throw new PiProxyUnavailableError(status);\n }\n}\n\n/** 要求不含内嵌凭证的绝对 HTTP(S) 地址,作为代理目标的第一层校验。 */\nfunction parseHttpUrl(value: string | URL, name: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL`);\n }\n if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {\n throw new TypeError(`${name} must be an absolute HTTP(S) URL without credentials`);\n }\n return url;\n}\n\n/** 去除基础路径末尾斜杠,便于严格比较目标路径边界。 */\nfunction normalizeBasePath(pathname: string): string {\n return pathname === '/' ? '' : pathname.replace(/\\/+$/, '');\n}\n\n/** 限制请求必须位于配置服务商的同源基础路径内,避免凭证随代理流向其他目标。 */\nfunction assertTargetAllowed(target: URL, base: URL): void {\n const base_path = normalizeBasePath(base.pathname);\n const in_base_path =\n base_path === '' ||\n target.pathname === base_path ||\n target.pathname.startsWith(`${base_path}/`);\n if (target.origin !== base.origin || !in_base_path) {\n throw new TypeError('SillyTavern proxy target is outside the configured provider base URL');\n }\n}\n\n/** 识别 Request 输入,供后续按 fetch 语义合并 init 覆盖值。 */\nfunction requestFromInput(input: FetchInput): Request | undefined {\n return typeof Request !== 'undefined' && input instanceof Request ? input : undefined;\n}\n\n/** 统一取得字符串、URL 或 Request 输入对应的目标地址。 */\nfunction requestUrl(input: FetchInput, request: Request | undefined): string {\n return request?.url ?? input.toString();\n}\n\n/** 按 init 优先、Request 次之的顺序解析 HTTP 方法并统一大写。 */\nfunction requestMethod(request: Request | undefined, init?: RequestInit): string {\n return (init?.method ?? request?.method ?? 'GET').toUpperCase();\n}\n\n/** 遵循 fetch 的覆盖顺序解析本次请求头。 */\nfunction requestHeaders(request: Request | undefined, init?: RequestInit): HeadersInit | undefined {\n return init?.headers ?? request?.headers;\n}\n\n/** 保留 init 显式传入的取消信号或 null,否则继承 Request 的信号。 */\nfunction requestSignal(\n request: Request | undefined,\n init?: RequestInit\n): AbortSignal | null | undefined {\n if (init && 'signal' in init) {\n return init.signal;\n }\n return request?.signal;\n}\n\n/** 将可重放的请求体转换为文本,拒绝无法安全复用的流或其他载荷。 */\nasync function bodyToText(body: BodyInit): Promise {\n if (typeof body === 'string') {\n return body;\n }\n if (body instanceof URLSearchParams) {\n return body.toString();\n }\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return body.text();\n }\n if (body instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(body));\n }\n if (ArrayBuffer.isView(body)) {\n return new TextDecoder().decode(\n new Uint8Array(body.buffer, body.byteOffset, body.byteLength)\n );\n }\n throw new TypeError('SillyTavern proxy only supports replayable JSON request bodies');\n}\n\n/** 优先读取 init 请求体,必要时克隆 Request 读取,避免消费原始输入。 */\nasync function requestBodyText(\n request: Request | undefined,\n init?: RequestInit\n): Promise {\n if (init?.body !== undefined && init.body !== null) {\n return bodyToText(init.body);\n }\n if (init && 'body' in init && init.body === null) {\n return undefined;\n }\n if (request?.body !== null && request !== undefined) {\n return request.clone().text();\n }\n return undefined;\n}\n\n/** 从 Headers、键值数组或对象中按大小写不敏感名称查找请求头。 */\nfunction getHeader(headers: HeadersInit | undefined, wanted_name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const wanted = wanted_name.toLowerCase();\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(wanted_name) ?? undefined;\n }\n if (Array.isArray(headers)) {\n return headers.find(([name]) => name.toLowerCase() === wanted)?.[1];\n }\n const entry = Object.entries(headers).find(([name]) => name.toLowerCase() === wanted);\n return entry?.[1] === undefined ? undefined : String(entry[1]);\n}\n\n/** 确认带正文的代理请求使用受支持的方法、JSON 类型和合法 JSON 内容。 */\nfunction assertJsonBody(method: string, headers: HeadersInit | undefined, body?: string): void {\n if (body === undefined) {\n return;\n }\n if (!['POST', 'PUT', 'PATCH'].includes(method)) {\n throw new TypeError(`SillyTavern proxy does not forward request bodies for ${method}`);\n }\n const content_type = getHeader(headers, 'content-type') ?? '';\n if (!/^application\\/json(?:\\s*;|$)/i.test(content_type)) {\n throw new TypeError('SillyTavern proxy requires Content-Type: application/json');\n }\n try {\n JSON.parse(body);\n } catch {\n throw new TypeError('SillyTavern proxy request body must be valid JSON');\n }\n}\n\n/** 在准备和转发请求的边界检查取消状态,保留原始取消原因。 */\nfunction throwIfAborted(signal: AbortSignal | null | undefined): void {\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');\n }\n}\n\n/** 保留原始应答格式,仅遮盖上游可能在错误正文中回显的本次请求凭证。 */\nfunction redactProxyResponseBody(\n body: string,\n headers: HeadersInit | undefined,\n target: URL\n): string {\n const credentials = new Set();\n for (const name of ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key']) {\n const value = getHeader(headers, name)?.trim();\n if (value) {\n credentials.add(value);\n credentials.add(value.replace(/^Bearer\\s+/i, ''));\n }\n }\n for (const [name, value] of target.searchParams) {\n if (/^(?:key|api[_-]?key|(?:access[_-]?)?token)$/i.test(name) && value) {\n credentials.add(value);\n }\n }\n for (const credential of [...credentials].filter(Boolean).sort((a, b) => b.length - a.length)) {\n body = body.split(credential).join('[REDACTED]');\n }\n return body;\n}\n\n/** 异步读取克隆应答,在流结束或中断后输出 debug 日志,不占用 SDK 的正文。 */\nasync function debugProxyResponse(\n response: Response,\n target: URL,\n method: string,\n headers: HeadersInit | undefined,\n signal: AbortSignal | null | undefined\n): Promise {\n const copy = response.clone();\n const reader = copy.body?.getReader();\n let body = '';\n let complete = false;\n // 不等待 tee 分支的 cancel:其 Promise 可能要等 SDK 也结束读取才完成。\n const on_abort = () => {\n void reader?.cancel().catch(() => {});\n };\n try {\n if (reader) {\n const decoder = new TextDecoder();\n signal?.addEventListener('abort', on_abort, { once: true });\n if (signal?.aborted) {\n on_abort();\n }\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) {\n body += decoder.decode();\n complete = !signal?.aborted;\n break;\n }\n body += decoder.decode(chunk.value, { stream: true });\n }\n } else {\n body = await copy.text();\n complete = !signal?.aborted;\n }\n } catch {\n // 读取失败时仍记录已收到的部分;日志失败不能改变请求结果。\n } finally {\n signal?.removeEventListener('abort', on_abort);\n reader?.releaseLock();\n }\n console.debug(\n '[MVU Pi Proxy] raw response',\n { method, url: `${target.origin}${target.pathname}`, status: response.status, complete },\n redactProxyResponseBody(body, headers, target)\n );\n}\n\n/** 检查返回值是否为酒馆明确的代理关闭提示,不把普通上游 404 误判为代理关闭。 */\nasync function isDisabledProxyResponse(response: Response): Promise {\n if (response.status !== 404) {\n return false;\n }\n try {\n return exactResponseText(await response.clone().text(), PROXY_DISABLED_MESSAGE);\n } catch {\n return false;\n }\n}\n\n/** 将实际请求发现的代理终态写回对应探测缓存。 */\nfunction cacheTerminalStatus(\n fetch_impl: FetchFunction,\n origin: string,\n status: SillyTavernProxyTerminalStatus\n): void {\n const entry = getProbeEntry(fetch_impl, origin);\n entry.status = status;\n entry.promise = undefined;\n}\n\n/**\n * 创建受单一服务商基础地址约束的代理 fetch,转发认证头、JSON 正文和取消信号。\n * 发送前确认代理可用,目标越界或载荷不兼容时拒绝转发。\n */\nexport function createSillyTavernProxyFetch(options: SillyTavernProxyFetchOptions): FetchFunction {\n const base = parseHttpUrl(options.baseUrl, 'baseUrl');\n\n return async (input: FetchInput, init?: RequestInit): Promise => {\n const fetch_impl = resolveFetch(options.fetch);\n const origin = resolveOrigin(options.origin);\n if (!fetch_impl || !origin) {\n throw new PiProxyUnavailableError('unavailable');\n }\n\n const request = requestFromInput(input);\n const target = parseHttpUrl(requestUrl(input, request), 'proxy target');\n assertTargetAllowed(target, base);\n\n const method = requestMethod(request, init);\n const headers = requestHeaders(request, init);\n const signal = requestSignal(request, init);\n throwIfAborted(signal);\n const body = await requestBodyText(request, init);\n assertJsonBody(method, headers, body);\n\n await assertSillyTavernProxyAvailable({\n fetch: fetch_impl,\n origin,\n ...(signal == null ? {} : { signal }),\n });\n throwIfAborted(signal);\n\n const response = await fetch_impl(proxyUrl(target.href), {\n method,\n ...(headers === undefined ? {} : { headers }),\n ...(body === undefined ? {} : { body }),\n ...(signal === undefined ? {} : { signal }),\n credentials: 'same-origin',\n });\n void debugProxyResponse(response, target, method, headers, signal).catch(() => {});\n if (await isDisabledProxyResponse(response)) {\n cacheTerminalStatus(fetch_impl, origin, 'disabled');\n throw new PiProxyUnavailableError('disabled');\n }\n return response;\n };\n}\n\n/** @internal 仅供测试清空模块级探测缓存,避免用例之间共享状态。 */\nexport function resetSillyTavernProxyStatusForTests(): void {\n probe_cache = new WeakMap();\n}\n","import { clampThinkingLevel, type Api, type Model } from './pi_gateway';\nimport { normalizePiThinkingLevel, type PiThinkingLevel } from './thinking_setting';\n\n/** 按可信目录或协议适配元数据解析生效等级;缺少元数据时保留显式选择。 */\nexport function resolvePiThinkingLevel(value: unknown, model?: Model): PiThinkingLevel {\n const level = normalizePiThinkingLevel(value);\n if (level === 'default' || level === 'off' || !model) return level;\n return model.reasoning ? clampThinkingLevel(model, level) : 'off';\n}\n\n/** 为动态模型补充显式启用信息,避免 SDK 再次把支持 effort 的协议降为 high。 */\nexport function preparePiThinkingModel(\n model: Model,\n catalogHit: boolean,\n level: PiThinkingLevel\n): Model {\n if (catalogHit || level === 'default') return model;\n const extendedEffort =\n (level === 'xhigh' || level === 'max') &&\n [\n 'openai-completions',\n 'openai-responses',\n 'openai-codex-responses',\n 'mistral-conversations',\n ].includes(model.api);\n return {\n ...model,\n reasoning: true,\n ...(extendedEffort ? { thinkingLevelMap: { xhigh: 'xhigh', max: 'max' } } : {}),\n };\n}\n","/** 默认值沿用原始协议行为;显式等级由 Pi 按模型能力映射。 */\nexport const PI_THINKING_LEVELS = [\n 'default',\n 'off',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n] as const;\nexport type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];\n\nexport function normalizePiThinkingLevel(value: unknown): PiThinkingLevel {\n return PI_THINKING_LEVELS.includes(value as PiThinkingLevel)\n ? (value as PiThinkingLevel)\n : 'default';\n}\n\nexport function isPiThinkingEnabled(value: unknown): boolean {\n const level = normalizePiThinkingLevel(value);\n return level !== 'default' && level !== 'off';\n}\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const commonMessages = defineMessages({\n 'common.enabled': {\n 'zh-CN': '启用',\n en: 'Enabled',\n },\n 'common.disabled': {\n 'zh-CN': '关闭',\n en: 'Disabled',\n },\n 'common.yes': {\n 'zh-CN': '是',\n en: 'Yes',\n },\n 'common.no': {\n 'zh-CN': '否',\n en: 'No',\n },\n 'common.confirm': {\n 'zh-CN': '确认',\n en: 'Confirm',\n },\n 'common.cancel': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const panelMessages = defineMessages({\n 'panel.title': {\n 'zh-CN': 'MVU 变量框架',\n en: 'MVU Variable Framework',\n },\n 'panel.version.section': {\n 'zh-CN': '当前版本',\n en: 'Current version',\n },\n 'panel.version.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n 'panel.notification.section': {\n 'zh-CN': '通知设置',\n en: 'Notifications',\n },\n 'panel.notification.frameworkLoaded': {\n 'zh-CN': 'MVU 框架加载成功时通知',\n en: 'Notify when the MVU framework loads successfully',\n },\n 'panel.notification.variablesInitialized': {\n 'zh-CN': '变量初始化成功时通知',\n en: 'Notify when variables are initialized successfully',\n },\n 'panel.notification.variableError': {\n 'zh-CN': '变量初始化/更新出错时通知',\n en: 'Notify when variable initialization or updates fail',\n },\n 'panel.notification.extraModelParsing': {\n 'zh-CN': '额外模型解析中通知',\n en: 'Notify while extra-model parsing is running',\n },\n 'panel.cleanup.section': {\n 'zh-CN': '自动清理变量',\n en: 'Automatic variable cleanup',\n },\n 'panel.cleanup.enable': {\n 'zh-CN': '启用自动清理变量',\n en: 'Enable automatic variable cleanup',\n },\n 'panel.cleanup.strategy': {\n 'zh-CN': '清理策略',\n en: 'Cleanup strategy',\n },\n 'panel.cleanup.snapshotInterval': {\n 'zh-CN': '快照保留间隔',\n en: 'Snapshot retention interval',\n },\n 'panel.cleanup.recentFloors': {\n 'zh-CN': '要保留变量的最近楼层数',\n en: 'Recent floors whose variables are retained',\n },\n 'panel.cleanup.restoreFloors': {\n 'zh-CN': '触发恢复变量的最近楼层数',\n en: 'Recent floors that trigger variable restoration',\n },\n 'panel.compatibility.section': {\n 'zh-CN': '兼容性',\n en: 'Compatibility',\n },\n 'panel.compatibility.updateChatVariables': {\n 'zh-CN': '变量更新到聊天变量',\n en: 'Write variable updates to chat variables',\n },\n 'panel.compatibility.updateChatVariablesHelp': {\n 'zh-CN':\n '启用后,所有变量更新结果也会输出到聊天变量中。如果部分老角色卡无法正常游玩,可以开启这个开关。',\n en: 'When enabled, every variable update is also written to chat variables. Enable this if some older character cards do not work correctly.',\n },\n 'panel.compatibility.showLegacy': {\n 'zh-CN': '显示老旧功能',\n en: 'Show legacy features',\n },\n 'panel.compatibility.sendasNotUser': {\n 'zh-CN': 'sendas 不视为 user 消息',\n en: 'Do not treat sendas as a user message',\n },\n 'panel.compatibility.nonblockingExtraAnalysis': {\n 'zh-CN': '额外模型解析不阻塞正文显示(实验性)',\n en: 'Show replies without waiting for extra model analysis (experimental)',\n },\n 'panel.compatibility.nonblockingExtraAnalysisHelp': {\n 'zh-CN':\n '默认关闭。开启后,角色卡的部分默认前端页面会立即显示,其他插件的额外分析流程也可同时执行,无需等待变量解析完成。仅对非工具调用的自动解析生效。',\n en: \"Off by default. When enabled, some of a character card's default frontend pages appear immediately, and other plugins can run their additional analysis at the same time, without waiting for variable analysis to finish. Applies only to automatic analysis without tool calling.\",\n },\n 'panel.compatibility.license': {\n 'zh-CN': 'LICENSE',\n en: 'LICENSE',\n },\n 'panel.compatibility.licenseIntro': {\n 'zh-CN': 'MVU 采用 MIT 许可证。以下是运行时使用或随扩展打包的第三方开源组件:',\n en: 'MVU is licensed under MIT. The following third-party components are used at runtime or bundled with the extension:',\n },\n 'panel.compatibility.licenseComponent': {\n 'zh-CN': '组件',\n en: 'Component',\n },\n 'panel.compatibility.licenseIdentifier': {\n 'zh-CN': '许可证',\n en: 'License',\n },\n 'panel.compatibility.licenseDetails': {\n 'zh-CN':\n '许可证标识来自当前依赖元数据;点击组件名称可查看项目,完整条款以各项目 LICENSE 为准。',\n en: 'License identifiers come from current dependency metadata. Select a component name to view its project; the project LICENSE contains the complete terms.',\n },\n 'panel.button.section': {\n 'zh-CN': '修复按钮',\n en: 'Repair actions',\n },\n 'panel.button.reprocessVariables': {\n 'zh-CN': '重新处理变量',\n en: 'Reprocess variables',\n },\n 'panel.button.reloadInitialVariables': {\n 'zh-CN': '重新读取初始变量',\n en: 'Reload initial variables',\n },\n 'panel.button.snapshotFloor': {\n 'zh-CN': '快照楼层',\n en: 'Snapshot floor',\n },\n 'panel.button.replayFloor': {\n 'zh-CN': '重演楼层',\n en: 'Replay floor',\n },\n 'panel.button.retryExtraModelParsing': {\n 'zh-CN': '重试额外模型解析',\n en: 'Retry extra-model parsing',\n },\n 'panel.button.incrementalRepairExtraModelParsing': {\n 'zh-CN': '增量校正额外模型解析',\n en: 'Incrementally repair extra-model parsing',\n },\n 'panel.button.clearOldFloorVariables': {\n 'zh-CN': '清除旧楼层变量',\n en: 'Clear variables from old floors',\n },\n 'panel.help.ariaLabel': {\n 'zh-CN': '帮助',\n en: 'Help',\n },\n 'panel.badge.override': {\n 'zh-CN': '角色卡覆盖',\n en: 'Character-card override',\n },\n 'panel.badge.overrideWithValue': {\n 'zh-CN': '角色卡覆盖:{value}',\n en: 'Character-card override: {value}',\n },\n 'panel.badge.additive': {\n 'zh-CN': '角色卡规则叠加',\n en: 'Character-card rule added',\n },\n 'panel.update.section': {\n 'zh-CN': '变量更新方式',\n en: 'Variable update method',\n },\n 'panel.update.method.aiOutput': {\n 'zh-CN': '随 AI 输出',\n en: 'Alongside AI output',\n },\n 'panel.update.method.extraModel': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.update.method.unsupportedWarning': {\n 'zh-CN':\n '世界书 [{worldbooks}] 未适配额外模型解析,视为 [mvu_plot] 条目(只会发给剧情 AI、不会发给变量更新 AI)。',\n en: 'World book [{worldbooks}] is not adapted for extra-model parsing and will be treated as an [mvu_plot] entry (sent only to the story AI, not the variable-update AI).',\n },\n 'panel.modelSelect.ariaLabel': {\n 'zh-CN': '模型列表',\n en: 'Model list',\n },\n 'panel.modelSelect.chooseFromList': {\n 'zh-CN': '(从列表选择)',\n en: '(Choose from list)',\n },\n 'panel.modelSelect.loading': {\n 'zh-CN': '获取中…',\n en: 'Fetching…',\n },\n 'panel.modelSelect.fetch': {\n 'zh-CN': '获取模型列表',\n en: 'Fetch model list',\n },\n 'panel.modelSelect.empty': {\n 'zh-CN': '模型列表为空或获取失败',\n en: 'The model list is empty or could not be fetched',\n },\n 'panel.modelSelect.fetchTitle': {\n 'zh-CN': '[MVU]获取模型列表',\n en: '[MVU] Fetch model list',\n },\n 'panel.modelSelect.fetchFailureTitle': {\n 'zh-CN': '[MVU]获取模型列表失败',\n en: '[MVU] Failed to fetch model list',\n },\n 'panel.prompt.apiOptionsMigrated': {\n 'zh-CN': '破限/应答格式等配置 已迁移到 API 方案,与方案配置绑定',\n en: 'Jailbreak, response format, and related settings have moved to API profiles and are saved with each profile.',\n },\n 'panel.source.pi.auth.accountLogin': {\n 'zh-CN': '账号登录',\n en: 'Account sign-in',\n },\n 'panel.source.pi.contextWindowHelp': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。请按服务商说明填写;数值过小会使较长的请求无法发送。',\n en: 'The total length the model can read and generate, in tokens. Enter the limit given by your provider; setting it too low prevents longer requests from being sent.',\n },\n 'panel.prompt.section': {\n 'zh-CN': '请求内容',\n en: 'Request content',\n },\n 'panel.prompt.jailbreakStrategy': {\n 'zh-CN': '破限方案',\n en: 'Jailbreak strategy',\n },\n 'panel.prompt.jailbreak.builtin': {\n 'zh-CN': '使用内置破限',\n en: 'Use built-in jailbreak',\n },\n 'panel.prompt.jailbreak.currentPreset': {\n 'zh-CN': '使用当前预设',\n en: 'Use current preset',\n },\n 'panel.prompt.jailbreak.otherPreset': {\n 'zh-CN': '使用其他预设',\n en: 'Use another preset',\n },\n 'panel.prompt.targetPreset': {\n 'zh-CN': '目标预设',\n en: 'Target preset',\n },\n 'panel.prompt.noSavedPreset': {\n 'zh-CN': '未检测到可用的已保存预设',\n en: 'No available saved presets detected',\n },\n 'panel.prompt.randomHeader': {\n 'zh-CN': '随机头部',\n en: 'Randomized header',\n },\n 'panel.prompt.randomHeaderHelp': {\n 'zh-CN':\n 'Gemini 系模型会记录破限头部,因此需要在头部增加随机数。如果你使用的不是 Gemini 系模型,请关闭这个功能,以免缓存失效。',\n en: 'Gemini-family models may record the jailbreak header, so a random number is added to it. Disable this for non-Gemini models to avoid invalidating the prompt cache.',\n },\n 'panel.prompt.responseFormat': {\n 'zh-CN': '应答格式',\n en: 'Response format',\n },\n 'panel.prompt.response.chatMessage': {\n 'zh-CN': '聊天消息',\n en: 'Chat message',\n },\n 'panel.prompt.response.toolCall': {\n 'zh-CN': '工具调用',\n en: 'Tool call',\n },\n 'panel.prompt.response.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.prompt.response.structuredV4': {\n 'zh-CN': '格式化输出(v4 兼容)',\n en: 'Structured output (v4 compatible)',\n },\n 'panel.prompt.disableThinking': {\n 'zh-CN': '关闭 thinking',\n en: 'Disable thinking',\n },\n 'panel.prompt.disableThinkingHelp': {\n 'zh-CN': '关闭后可避免部分空回复问题。',\n en: 'Disabling thinking can prevent some empty responses.',\n },\n 'panel.prompt.disable': {\n 'zh-CN': '关闭',\n en: 'Disable',\n },\n 'panel.prompt.fakeStreaming': {\n 'zh-CN': '兼容假流式',\n en: 'Pseudo-streaming compatibility',\n },\n 'panel.prompt.fakeStreamingHelp': {\n 'zh-CN':\n '默认关闭,等待模型一次返回完整结果。若渠道要求流式请求,或等待时经常超时,可尝试勾选。开启后仍会等回复完整后再更新变量。OpenAI Codex 只支持流式传输,会始终使用流式。',\n en: 'Disabled by default: wait for the complete response. Enable this if your provider requires streaming or requests often time out while waiting. Variables are still updated after the full response arrives. OpenAI Codex requires streaming and always uses it.',\n },\n 'panel.prompt.whitelist': {\n 'zh-CN': '世界书条目白名单正则',\n en: 'World-book entry whitelist regex',\n },\n 'panel.prompt.whitelistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段只保留 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing retains only world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.whitelistPlaceholder': {\n 'zh-CN': '角色{or}地点 或 /角色{or}地点/i',\n en: 'character{or}location or /character{or}location/i',\n },\n 'panel.prompt.blacklist': {\n 'zh-CN': '世界书条目黑名单正则',\n en: 'World-book entry blacklist regex',\n },\n 'panel.prompt.blacklistHelp': {\n 'zh-CN':\n '留空则关闭;非空时,额外模型解析阶段会排除 comment 匹配该正则的世界书条目。支持 {example}。',\n en: 'Leave blank to disable. When set, extra-model parsing excludes world-book entries whose comment matches this regex. Supports {example}.',\n },\n 'panel.prompt.blacklistPlaceholder': {\n 'zh-CN': '临时{or}禁用 或 /临时{or}禁用/i',\n en: 'temporary{or}disabled or /temporary{or}disabled/i',\n },\n 'panel.prompt.regexInvalid': {\n 'zh-CN': '正则无效:{error}',\n en: 'Invalid regex: {error}',\n },\n 'panel.prompt.filtered.title': {\n 'zh-CN': '上次分析被筛选的条目',\n en: 'Entries filtered from the last analysis',\n },\n 'panel.prompt.filtered.empty': {\n 'zh-CN': '上次分析没有被黑/白名单筛选掉的条目。',\n en: 'No entries were filtered by the whitelist or blacklist in the last analysis.',\n },\n 'panel.prompt.filtered.entrySource': {\n 'zh-CN': '条目来源',\n en: 'Entry source',\n },\n 'panel.prompt.filtered.worldBook': {\n 'zh-CN': '世界书',\n en: 'World book',\n },\n 'panel.prompt.filtered.reason': {\n 'zh-CN': '原因',\n en: 'Reason',\n },\n 'panel.prompt.filtered.configSource': {\n 'zh-CN': '配置来源',\n en: 'Configuration source',\n },\n 'panel.prompt.filtered.comment': {\n 'zh-CN': '条目备注',\n en: 'Comment',\n },\n 'panel.prompt.filtered.globalLore': {\n 'zh-CN': '全局世界书',\n en: 'Global world book',\n },\n 'panel.prompt.filtered.characterLore': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.prompt.filtered.chatLore': {\n 'zh-CN': '聊天世界书',\n en: 'Chat world book',\n },\n 'panel.prompt.filtered.personaLore': {\n 'zh-CN': '用户世界书',\n en: 'Persona world book',\n },\n 'panel.prompt.filtered.whitelistReason': {\n 'zh-CN': '白名单',\n en: 'Whitelist',\n },\n 'panel.prompt.filtered.blacklistReason': {\n 'zh-CN': '黑名单',\n en: 'Blacklist',\n },\n 'panel.prompt.filtered.globalConfig': {\n 'zh-CN': '用户全局配置',\n en: 'User global settings',\n },\n 'panel.prompt.filtered.characterConfig': {\n 'zh-CN': '角色卡配置',\n en: 'Character-card settings',\n },\n 'panel.prompt.toolCallUnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'工具调用'\",\n en: \"[MVU] Cannot use 'Tool call'\",\n },\n 'panel.prompt.toolCallUnsupported': {\n 'zh-CN': '当前 API 源不支持工具调用,请换用支持 tools 的渠道模型或改用其他应答格式',\n en: 'The current API source does not support tool calls. Choose a provider/model that supports tools, or use another response format.',\n },\n 'panel.prompt.structuredV4UnavailableTitle': {\n 'zh-CN': \"[MVU]无法使用'格式化输出(v4兼容)'\",\n en: \"[MVU] Cannot use 'Structured output (v4 compatible)'\",\n },\n 'panel.prompt.structuredV4RequiresCustom': {\n 'zh-CN': '格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同',\n en: 'Structured output (v4 compatible) requires a custom extra-model source and cannot use the current connection.',\n },\n 'panel.request.section': {\n 'zh-CN': '请求策略',\n en: 'Request strategy',\n },\n 'panel.request.method': {\n 'zh-CN': '请求方式',\n en: 'Request method',\n },\n 'panel.request.sequential': {\n 'zh-CN': '依次请求,失败后重试',\n en: 'Request sequentially and retry on failure',\n },\n 'panel.request.parallel': {\n 'zh-CN': '同时请求多次',\n en: 'Send multiple requests in parallel',\n },\n 'panel.request.onceThenParallel': {\n 'zh-CN': '先请求一次,失败后再同时请求多次',\n en: 'Request once, then retry in parallel on failure',\n },\n 'panel.request.count': {\n 'zh-CN': '请求次数',\n en: 'Number of requests',\n },\n 'panel.request.auto': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.request.autoHelp': {\n 'zh-CN':\n '关闭后,AI 回复完成时不会自动触发额外模型解析;你需要主动点击“重试额外模型解析”按钮,才会执行解析并添加状态栏占位符 <StatusPlaceHolderImpl/>。',\n en: 'When disabled, extra-model parsing is not triggered automatically after the AI finishes replying. Click “Retry extra-model parsing” to run it and add the status-bar placeholder <StatusPlaceHolderImpl/>.',\n },\n 'panel.request.batchWarning': {\n 'zh-CN':\n '请升级酒馆助手到 4.4.3 或更高版本,否则批量请求功能可能让预设的“流式传输”设置失效',\n en: \"Upgrade Tavern Helper to version 4.4.3 or later. Otherwise, batch requests may override the preset's streaming setting.\",\n },\n 'panel.request.batchWarningTitle': {\n 'zh-CN': '[MVU]批量请求可能有问题',\n en: '[MVU] Batch requests may not work correctly',\n },\n 'panel.source.section': {\n 'zh-CN': '模型来源',\n en: 'Model source',\n },\n 'panel.source.sameAsConnection': {\n 'zh-CN': '与插头相同',\n en: 'Same as current connection',\n },\n 'panel.source.custom': {\n 'zh-CN': '自定义',\n en: 'Custom',\n },\n 'panel.source.more': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.profile.section': {\n 'zh-CN': 'API 方案',\n en: 'API profiles',\n },\n 'panel.source.profile.current': {\n 'zh-CN': '当前方案',\n en: 'Current profile',\n },\n 'panel.source.profile.ariaLabel': {\n 'zh-CN': 'API 方案',\n en: 'API profile',\n },\n 'panel.source.profile.manual': {\n 'zh-CN': '(手动编辑,未绑定方案)',\n en: '(Manual settings, not bound to a profile)',\n },\n 'panel.source.profile.newName': {\n 'zh-CN': '新方案名称',\n en: 'New profile name',\n },\n 'panel.source.profile.save': {\n 'zh-CN': '保存当前方案',\n en: 'Save current profile',\n },\n 'panel.source.profile.saveAs': {\n 'zh-CN': '另存为新方案',\n en: 'Save as new profile',\n },\n 'panel.source.profile.delete': {\n 'zh-CN': '删除当前方案',\n en: 'Delete current profile',\n },\n 'panel.source.profile.backendCustom': {\n 'zh-CN': '自定义 API',\n en: 'Custom API',\n },\n 'panel.source.profile.backendPi': {\n 'zh-CN': '更多',\n en: 'More',\n },\n 'panel.source.apiAddress': {\n 'zh-CN': 'API 地址',\n en: 'API address',\n },\n 'panel.source.apiKey': {\n 'zh-CN': 'API 密钥',\n en: 'API key',\n },\n 'panel.source.apiKeyPlaceholder': {\n 'zh-CN': '留空表示无需密钥',\n en: 'Leave blank when no key is required',\n },\n 'panel.source.modelName': {\n 'zh-CN': '模型名称',\n en: 'Model name',\n },\n // “更多”来源的连接、协议能力和认证界面文案;底层 Pi 标识不作为产品名称展示。\n 'panel.source.pi.provider': {\n 'zh-CN': '来源',\n en: 'Provider',\n },\n 'panel.source.pi.endpoint': {\n 'zh-CN': 'API 基础地址',\n en: 'API base URL',\n },\n 'panel.source.pi.endpointDefault': {\n 'zh-CN': '留空使用默认地址:{endpoint}',\n en: 'Leave blank to use the default: {endpoint}',\n },\n 'panel.source.pi.endpointPlaceholder': {\n 'zh-CN': '请输入 API 基础地址',\n en: 'Enter an API base URL',\n },\n 'panel.source.pi.endpointHelp': {\n 'zh-CN':\n '填写服务商提供的 API 地址,留空使用默认地址。也可以直接粘贴完整的聊天或 Responses 接口地址,MVU 会自动处理末尾路径。',\n en: 'Enter the API URL supplied by your provider, or leave it blank to use the default. You can also paste a full chat or Responses URL; MVU adjusts its ending automatically.',\n },\n 'panel.source.pi.proxy.use': {\n 'zh-CN': '使用 Proxy',\n en: 'Use Proxy',\n },\n 'panel.source.pi.proxy.help': {\n 'zh-CN':\n '如果获取模型列表或生成请求因 CORS(浏览器跨域限制)失败,可以尝试勾选,通过 SillyTavern 转发请求。需要先在 SillyTavern 中开启 Proxy。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'If fetching models or generating a response fails because of CORS (browser cross-origin restrictions), try enabling this to forward requests through SillyTavern. Proxy must also be enabled in SillyTavern. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.proxy.notEnabled': {\n 'zh-CN': '当前来源需要 Proxy,但 SillyTavern 的 Proxy 未开启或无法连接。',\n en: 'This provider needs Proxy, but SillyTavern Proxy is disabled or unavailable.',\n },\n 'panel.source.pi.proxy.notEnabledHelp': {\n 'zh-CN':\n '打开 SillyTavern 的 config.yaml,将 enableCorsProxy 设为 true,保存后重启 SillyTavern。如果酒馆由他人托管,请联系管理员开启。开启这个功能后,任意前端脚本都可以无障碍地访问任意站点,请在了解相关的风险后决定是否打开这个功能。',\n en: 'Open SillyTavern config.yaml, set enableCorsProxy to true, save, and restart SillyTavern. If someone else hosts your server, ask its administrator to enable it. Once enabled, any frontend script can freely access any website. Decide whether to enable this feature only after understanding the risks.',\n },\n 'panel.source.pi.model': {\n 'zh-CN': '模型',\n en: 'Model',\n },\n 'panel.source.pi.catalogModel': {\n 'zh-CN': '模型目录',\n en: 'Model catalog',\n },\n 'panel.source.pi.customModel': {\n 'zh-CN': '(手动填写模型)',\n en: '(Enter a model manually)',\n },\n 'panel.source.pi.api.openaiResponses': {\n 'zh-CN': 'OpenAI Responses',\n en: 'OpenAI Responses',\n },\n 'panel.source.pi.api.openaiCompletions': {\n 'zh-CN': 'OpenAI Chat Completions',\n en: 'OpenAI Chat Completions',\n },\n 'panel.source.pi.api.openaiCodexResponses': {\n 'zh-CN': 'OpenAI Codex Responses',\n en: 'OpenAI Codex Responses',\n },\n 'panel.source.pi.api.anthropicMessages': {\n 'zh-CN': 'Anthropic Messages',\n en: 'Anthropic Messages',\n },\n 'panel.source.pi.api.googleGenerativeAi': {\n 'zh-CN': 'Google Generative AI',\n en: 'Google Generative AI',\n },\n 'panel.source.pi.api.mistralConversations': {\n 'zh-CN': 'Mistral Conversations',\n en: 'Mistral Conversations',\n },\n 'panel.source.pi.auth.apiKey': {\n 'zh-CN': 'API Key',\n en: 'API key',\n },\n 'panel.source.pi.capabilities': {\n 'zh-CN': '可尝试的功能:{capabilities}。具体是否可用,以你使用的模型和服务商为准。',\n en: 'Features you can try: {capabilities}. Availability depends on your model and provider.',\n },\n 'panel.source.pi.capability.tools': {\n 'zh-CN': '工具调用',\n en: 'tools',\n },\n 'panel.source.pi.capability.images': {\n 'zh-CN': '读取图片',\n en: 'Read images',\n },\n 'panel.source.pi.capability.structured': {\n 'zh-CN': '格式化输出',\n en: 'Structured output',\n },\n 'panel.source.pi.thinkingLevel': {\n 'zh-CN': '思考等级',\n en: 'Thinking level',\n },\n 'panel.source.pi.thinkingLevelHelp': {\n 'zh-CN':\n '默认保持现有请求行为;其他等级按模型能力映射,部分模型无法完全关闭思考。未知模型按协议转换所选等级;OpenAI/Mistral 的极高、最高会原样尝试,Google/预算式思考仍按协议映射。Anthropic 开启思考时不支持强制工具调用,旧模型至少需要 2048 回复 token。自定义请求体覆盖仍优先。',\n en: 'Default preserves existing requests. Other levels are mapped to model capabilities; some models cannot fully disable thinking. Unknown models use protocol-specific mappings: OpenAI/Mistral attempt xhigh/max unchanged, while Google and budget-based thinking use protocol mappings. Anthropic thinking cannot force tool calls; older models need at least 2048 reply tokens. Custom body overrides take precedence.',\n },\n 'panel.source.pi.thinking.default': {\n 'zh-CN': '默认(保持原行为)',\n en: 'Default (unchanged behavior)',\n },\n 'panel.source.pi.thinking.off': {\n 'zh-CN': '关闭 / 最低',\n en: 'Off / minimum',\n },\n 'panel.source.pi.thinking.minimal': {\n 'zh-CN': '极低',\n en: 'Minimal',\n },\n 'panel.source.pi.thinking.low': {\n 'zh-CN': '低',\n en: 'Low',\n },\n 'panel.source.pi.thinking.medium': {\n 'zh-CN': '中',\n en: 'Medium',\n },\n 'panel.source.pi.thinking.high': {\n 'zh-CN': '高',\n en: 'High',\n },\n 'panel.source.pi.thinking.xhigh': {\n 'zh-CN': '极高',\n en: 'Extra high',\n },\n 'panel.source.pi.thinking.max': {\n 'zh-CN': '最高',\n en: 'Maximum',\n },\n 'panel.source.pi.contextWindow': {\n 'zh-CN': '上下文窗口',\n en: 'Context window',\n },\n 'panel.source.pi.contextWindowCatalog': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。已自动填入 {value};如果服务商给出的限制不同,可手动修改,清空后恢复自动值。',\n en: 'The total length the model can read and generate, in tokens. Automatically set to {value}. Edit it if your provider gives a different limit; clear it to restore the automatic value.',\n },\n 'panel.source.pi.contextWindowOverride': {\n 'zh-CN':\n '模型一次能读取和生成的总长度,单位为 token。当前使用你填写的值;清空后会尝试自动填写已知模型的限制。',\n en: 'The total length the model can read and generate, in tokens. Using your entered value; clear it to try the known model limit.',\n },\n 'panel.source.pi.contextWindowRequired': {\n 'zh-CN': '请输入正整数上下文窗口;模型目录未提供该值时必须手动填写。',\n en: 'Enter a positive integer context window. It is required when the model catalog has no value.',\n },\n 'panel.source.pi.error.unknownProvider': {\n 'zh-CN': '“更多”中的未知来源“{provider}”;请重新选择来源。',\n en: 'Unknown provider \"{provider}\" under More. Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedApi': {\n 'zh-CN': '已保存的来源“{provider}”不支持接口“{api}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support API \"{api}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedAuth': {\n 'zh-CN': '已保存的来源“{provider}”不支持认证方式“{auth}”,请重新选择来源。',\n en: 'The saved provider \"{provider}\" does not support authentication type \"{auth}\". Select a provider again.',\n },\n 'panel.source.pi.error.unsupportedEndpoint': {\n 'zh-CN': '当前来源不支持自定义 API 地址,请重新选择来源或切换 API 方案。',\n en: 'This provider does not support a custom API URL. Select a provider again or switch API profiles.',\n },\n 'panel.source.pi.featureDisabled': {\n 'zh-CN':\n '此构建已关闭“更多”模型来源。已保存的“更多”配置仍会保留;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Saved More-source settings are preserved; select Same as current connection or Custom.',\n },\n 'panel.source.pi.maxTokensPositive': {\n 'zh-CN': '最大回复 token 必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'panel.source.pi.maxTokensExceedContext': {\n 'zh-CN': '最大回复 token 不能大于上下文窗口。',\n en: 'Maximum response tokens cannot exceed the context window.',\n },\n 'panel.source.pi.customHeaders': {\n 'zh-CN': '自定义请求头',\n en: 'Custom request headers',\n },\n 'panel.source.pi.customHeadersHelp': {\n 'zh-CN':\n '仅在服务商要求额外请求头时填写,格式为 YAML 或 JSON,例如 X-App: MVU。留空即可;API 密钥请填在上方的密钥栏。',\n en: 'Fill this in only if your provider requires extra headers. Use YAML or JSON, for example X-App: MVU. Otherwise leave it blank. Enter your API key in the key field above.',\n },\n 'panel.source.pi.customOverridesSwitchHelp': {\n 'zh-CN':\n '下方的自定义请求头、附加字段和排除字段会随 API 方案保存。手动切换来源或 API 地址时会清空这三项;切换方案会恢复该方案保存的内容。',\n en: 'Custom headers, additional fields, and excluded fields below are saved with the API profile. Manually changing the provider or API URL clears these three fields; switching profiles restores their saved values.',\n },\n 'panel.source.pi.customIncludeBody': {\n 'zh-CN': '请求体附加字段',\n en: 'Additional request body fields',\n },\n 'panel.source.pi.customIncludeBodyHelp': {\n 'zh-CN':\n '服务商要求额外参数时在这里填写,使用 YAML 或 JSON,例如 seed: 123。同名参数会采用这里的值。Google 参数请放在 config 下。不需要时留空;密钥、模型和流式开关请使用对应选项。',\n en: 'Add parameters required by your provider as YAML or JSON, for example seed: 123. These values replace matching parameters. Put Google parameters under config. Leave blank when unnecessary; use the dedicated controls for keys, models, and streaming.',\n },\n 'panel.source.pi.customExcludeBody': {\n 'zh-CN': '请求体排除字段',\n en: 'Excluded request body fields',\n },\n 'panel.source.pi.customExcludeBodyHelp': {\n 'zh-CN':\n '如果服务商提示某个参数不被支持,可在这里填写参数名,使请求不再携带它。多个名称用换行或逗号分隔,也支持 YAML 列表。Google 参数写为 config.<参数名>。不需要时留空。',\n en: 'If your provider rejects a parameter, enter its name here to leave it out of requests. Separate names with newlines or commas, or use a YAML list. For Google, use config.<parameter>. Otherwise leave blank.',\n },\n 'panel.source.pi.clearCustomField': {\n 'zh-CN': '清空',\n en: 'Clear',\n },\n 'panel.source.pi.oauth.section': {\n 'zh-CN': 'OAuth 登录',\n en: 'OAuth login',\n },\n 'panel.source.pi.oauth.status': {\n 'zh-CN': '状态',\n en: 'Status',\n },\n 'panel.source.pi.oauth.checking': {\n 'zh-CN': '检查中…',\n en: 'Checking…',\n },\n 'panel.source.pi.oauth.loggedIn': {\n 'zh-CN': '已登录',\n en: 'Signed in',\n },\n 'panel.source.pi.oauth.loggedOut': {\n 'zh-CN': '未登录',\n en: 'Not signed in',\n },\n 'panel.source.pi.oauth.expiresAt': {\n 'zh-CN': '凭据有效期:{time}',\n en: 'Credential expiry: {time}',\n },\n 'panel.source.pi.oauth.login': {\n 'zh-CN': '登录',\n en: 'Sign in',\n },\n 'panel.source.pi.oauth.refresh': {\n 'zh-CN': '刷新凭证',\n en: 'Refresh credentials',\n },\n 'panel.source.pi.oauth.refreshing': {\n 'zh-CN': '正在刷新凭证…',\n en: 'Refreshing credentials…',\n },\n 'panel.source.pi.oauth.refreshSucceeded': {\n 'zh-CN': '凭证已刷新。',\n en: 'Credentials refreshed.',\n },\n 'panel.source.pi.oauth.cancel': {\n 'zh-CN': '取消登录',\n en: 'Cancel sign-in',\n },\n 'panel.source.pi.oauth.logout': {\n 'zh-CN': '登出',\n en: 'Sign out',\n },\n 'panel.source.pi.oauth.authorizationUrl': {\n 'zh-CN': '授权链接',\n en: 'Authorization URL',\n },\n 'panel.source.pi.oauth.openAuthorization': {\n 'zh-CN': '打开授权页',\n en: 'Open authorization page',\n },\n 'panel.source.pi.oauth.copyAuthorization': {\n 'zh-CN': '复制授权链接',\n en: 'Copy authorization URL',\n },\n 'panel.source.pi.oauth.copySucceeded': {\n 'zh-CN': '已复制授权链接',\n en: 'Authorization URL copied',\n },\n 'panel.source.pi.oauth.copyFailed': {\n 'zh-CN': '无法复制授权链接,请手动选择并复制。',\n en: 'Could not copy the authorization URL. Select and copy it manually.',\n },\n 'panel.source.pi.oauth.callbackUrl': {\n 'zh-CN': '回调链接',\n en: 'Callback URL',\n },\n 'panel.source.pi.oauth.callbackHelp': {\n 'zh-CN':\n '完成授权后,如果跳转到的页面打不开,不影响登录。请复制浏览器地址栏中以 127.0.0.1 或 localhost 开头的完整链接,粘贴到这里,再点击“完成登录”。',\n en: 'After authorization, the redirected page may fail to open; you can still finish signing in. Copy the full URL starting with 127.0.0.1 or localhost from the address bar, paste it here, and click Complete sign-in.',\n },\n 'panel.source.pi.oauth.complete': {\n 'zh-CN': '完成登录',\n en: 'Complete sign-in',\n },\n 'panel.source.pi.oauth.preparing': {\n 'zh-CN': '正在准备安全授权链接…',\n en: 'Preparing a secure authorization URL…',\n },\n 'panel.source.pi.oauth.waitingCallback': {\n 'zh-CN': '请在授权页完成登录,然后粘贴完整回调链接。',\n en: 'Finish signing in on the authorization page, then paste the complete callback URL.',\n },\n 'panel.source.pi.oauth.exchanging': {\n 'zh-CN': '正在验证回调并交换凭据…',\n en: 'Validating the callback and exchanging credentials…',\n },\n 'panel.source.pi.oauth.loginSucceeded': {\n 'zh-CN': 'OAuth 登录成功。',\n en: 'OAuth sign-in succeeded.',\n },\n 'panel.source.pi.oauth.logoutSucceeded': {\n 'zh-CN': '已登出。',\n en: 'Signed out.',\n },\n 'panel.source.pi.oauth.logoutConfirm': {\n 'zh-CN': '确定在当前配置中登出 {provider} 吗?之后可以登录其他账号。',\n en: 'Sign out of {provider} for the current configuration? You can then sign in with another account.',\n },\n 'panel.source.pi.oauth.failureTitle': {\n 'zh-CN': '[MVU]OAuth 操作失败',\n en: '[MVU] OAuth operation failed',\n },\n 'panel.source.advanced': {\n 'zh-CN': '高级参数',\n en: 'Advanced parameters',\n },\n 'panel.source.unsupportedAdvanced': {\n 'zh-CN': '⚠️酒馆助手版本过低,不支持以下配置',\n en: '⚠️ Your Tavern Helper version is too old to support the settings below',\n },\n 'panel.source.maxTokens': {\n 'zh-CN': '最大回复 token',\n en: 'Maximum response tokens',\n },\n 'panel.source.chatHistory': {\n 'zh-CN': '聊天历史条数',\n en: 'Chat history entries',\n },\n 'panel.source.temperature': {\n 'zh-CN': '温度',\n en: 'Temperature',\n },\n 'panel.source.frequencyPenalty': {\n 'zh-CN': '频率惩罚',\n en: 'Frequency penalty',\n },\n 'panel.source.presencePenalty': {\n 'zh-CN': '存在惩罚',\n en: 'Presence penalty',\n },\n 'panel.source.topP': {\n 'zh-CN': 'Top P',\n en: 'Top P',\n },\n 'panel.source.topK': {\n 'zh-CN': 'Top K',\n en: 'Top K',\n },\n 'panel.source.switchDirty': {\n 'zh-CN': '当前方案有未保存的修改,切换将丢弃这些修改。是否继续?',\n en: 'The current profile has unsaved changes. Switching will discard them. Continue?',\n },\n 'panel.source.continue': {\n 'zh-CN': '继续',\n en: 'Continue',\n },\n 'panel.source.discardChanges': {\n 'zh-CN': '丢弃修改',\n en: 'Discard changes',\n },\n 'panel.source.delete': {\n 'zh-CN': '删除',\n en: 'Delete',\n },\n 'panel.source.deleteDirty': {\n 'zh-CN': '当前方案“{name}”有未保存的修改,删除将丢弃这些修改。是否继续?',\n en: 'Profile “{name}” has unsaved changes. Deleting it will discard them. Continue?',\n },\n 'panel.source.deleteConfirm': {\n 'zh-CN': '确定删除 API 方案“{name}”吗?此操作不可撤销。',\n en: 'Delete API profile “{name}”? This action cannot be undone.',\n },\n 'panel.source.profileSaved': {\n 'zh-CN': '已保存 API 方案“{name}”',\n en: 'Saved API profile “{name}”',\n },\n 'panel.source.profileSavedAs': {\n 'zh-CN': '已另存为 API 方案“{name}”',\n en: 'Saved as API profile “{name}”',\n },\n 'panel.source.profileDeleted': {\n 'zh-CN': '已删除 API 方案“{name}”',\n en: 'Deleted API profile “{name}”',\n },\n 'panel.source.enterProfileName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a new profile name first',\n },\n 'panel.source.keepTwoProfiles': {\n 'zh-CN': '至少保留两个 API 方案时才可删除',\n en: 'At least two API profiles are required before one can be deleted',\n },\n 'panel.source.switchFailureTitle': {\n 'zh-CN': '[MVU]切换 API 方案失败',\n en: '[MVU] Failed to switch API profile',\n },\n 'panel.source.saveFailureTitle': {\n 'zh-CN': '[MVU]保存 API 方案失败',\n en: '[MVU] Failed to save API profile',\n },\n 'panel.source.deleteFailureTitle': {\n 'zh-CN': '[MVU]删除 API 方案失败',\n en: '[MVU] Failed to delete API profile',\n },\n 'panel.character.titleActive': {\n 'zh-CN': '当前角色卡配置(覆盖中)',\n en: 'Current character-card settings (overriding)',\n },\n 'panel.character.titleInactive': {\n 'zh-CN': '当前角色卡配置(未启用)',\n en: 'Current character-card settings (not enabled)',\n },\n 'panel.character.worldBook': {\n 'zh-CN': '角色世界书',\n en: 'Character world book',\n },\n 'panel.character.reading': {\n 'zh-CN': '正在读取…',\n en: 'Reading…',\n },\n 'panel.character.unbound': {\n 'zh-CN': '未绑定',\n en: 'Not bound',\n },\n 'panel.character.inherit': {\n 'zh-CN': '跟随用户配置',\n en: 'Inherit user settings',\n },\n 'panel.character.extraModelGroup': {\n 'zh-CN': '额外模型解析',\n en: 'Extra-model parsing',\n },\n 'panel.character.autoRequest': {\n 'zh-CN': '自动请求',\n en: 'Automatic requests',\n },\n 'panel.character.whitelist': {\n 'zh-CN': '角色卡世界书条目白名单正则',\n en: 'Character-card world-book entry whitelist regex',\n },\n 'panel.character.blacklist': {\n 'zh-CN': '角色卡世界书条目黑名单正则',\n en: 'Character-card world-book entry blacklist regex',\n },\n 'panel.character.regexInvalid': {\n 'zh-CN': '角色卡配置正则无效:{error}',\n en: 'Invalid character-card regex: {error}',\n },\n});\n","import { defineMessages } from '@/i18n/messages/types';\n\nexport const runtimeMessages = defineMessages({\n 'runtime.common.mvuTitle': {\n 'zh-CN': '[MVU]',\n en: '[MVU]',\n },\n 'runtime.common.errorCause': {\n 'zh-CN': '错误详情:{cause}',\n en: 'Error details: {cause}',\n },\n 'runtime.common.unknown': {\n 'zh-CN': '未知',\n en: 'Unknown',\n },\n\n 'runtime.main.chatReinitializeFailedLog': {\n 'zh-CN': '[MVU]切换聊天后重新初始化失败',\n en: '[MVU] Failed to reinitialize after switching chats',\n },\n 'runtime.main.reinitializeFailedTitle': {\n 'zh-CN': '[MVU]重新初始化失败',\n en: '[MVU] Reinitialization failed',\n },\n 'runtime.main.minimumVersionRequired': {\n 'zh-CN': 'MVU 变量框架需要酒馆助手版本不低于 {version}',\n en: 'MVU Variable Framework requires Tavern Helper {version} or later',\n },\n 'runtime.main.versionIncompatibleTitle': {\n 'zh-CN': '版本不兼容',\n en: 'Incompatible version',\n },\n\n 'runtime.parseString.invalidFormat': {\n 'zh-CN': '要解析的字符串不是有效的 YAML/JSON/JSON5 格式',\n en: 'The input is not valid YAML, JSON, or JSON5',\n },\n 'runtime.parseString.content': {\n 'zh-CN': '字符串内容',\n en: 'Input',\n },\n 'runtime.parseString.yamlError': {\n 'zh-CN': 'YAML 错误信息',\n en: 'YAML error',\n },\n 'runtime.parseString.json5Error': {\n 'zh-CN': 'JSON5 错误信息',\n en: 'JSON5 error',\n },\n 'runtime.parseString.jsonError': {\n 'zh-CN': 'JSON 错误信息',\n en: 'JSON error',\n },\n\n 'runtime.button.initVarDataNotFound': {\n 'zh-CN': '没有找到 InitVar 数据',\n en: 'No InitVar data was found',\n },\n 'runtime.button.initVarDataLoadFailed': {\n 'zh-CN': '加载 InitVar 数据失败:',\n en: 'Failed to load InitVar data',\n },\n 'runtime.button.messageNotFound': {\n 'zh-CN': '没有找到消息',\n en: 'No message was found',\n },\n 'runtime.button.initVarDescriptionsUpdated': {\n 'zh-CN': 'InitVar描述已更新',\n en: 'InitVar descriptions updated',\n },\n 'runtime.button.snapshotPrompt': {\n 'zh-CN':\n '

设置快照楼层可以避免指定的楼层在清理操作中被移除变量信息

请填写要保留变量信息的楼层(如 10 为第 10 层)
后续楼层的重演将可以从这一层开始',\n en: '

Marking a floor as a snapshot prevents its variable data from being removed during cleanup.

Enter the floor whose variable data should be retained (for example, 10 for floor 10).
Later floors can then be replayed starting from this floor.',\n },\n 'runtime.button.invalidFloorInput': {\n 'zh-CN': '请输入有效的楼层数,你输入的是“{value}”',\n en: 'Enter a valid floor number. You entered “{value}”.',\n },\n 'runtime.button.invalidFloor': {\n 'zh-CN': '无效的楼层“{value}”',\n en: 'Invalid floor “{value}”.',\n },\n 'runtime.button.snapshotConfigureFailedTitle': {\n 'zh-CN': '[MVU]配置楼层快照失败',\n en: '[MVU] Failed to configure floor snapshot',\n },\n 'runtime.button.snapshotConfigureTitle': {\n 'zh-CN': '[MVU]配置楼层快照',\n en: '[MVU] Configure floor snapshot',\n },\n 'runtime.button.snapshotConfigured': {\n 'zh-CN': '已将 {floor} 层配置为快照楼层',\n en: 'Floor {floor} is now a snapshot floor',\n },\n 'runtime.button.replayPrompt': {\n 'zh-CN':\n '

当变量更新出现 required/extensible 相关问题时,可以尝试从过去的楼层重演

请填写要进行重演的楼层(如 10 为第 10 层,-1 为最新楼层)
也就是出现问题的楼层',\n en: '

If variable updates have required/extensible issues, replaying from an earlier floor may help.

Enter the floor to replay (for example, 10 for floor 10, or -1 for the latest floor).
This should be the floor where the problem occurred.',\n },\n 'runtime.button.replayFailedTitle': {\n 'zh-CN': '[MVU]楼层重演失败',\n en: '[MVU] Floor replay failed',\n },\n 'runtime.button.replayNoAvailableFloor': {\n 'zh-CN': '无法找到可以进行重演的楼层',\n en: 'No floor with replayable variable data was found',\n },\n 'runtime.button.replayStartPrompt': {\n 'zh-CN': '请填写从哪个楼层开始重演,找到最近的支持重演楼层为 [{floor}]',\n en: 'Enter the floor from which to start replaying. The nearest replayable floor is [{floor}].',\n },\n 'runtime.button.replayNeedsVariables': {\n 'zh-CN': '请输入含变量信息的楼层,你输入的是“{value}”',\n en: 'Enter a floor that contains variable data. You entered “{value}”.',\n },\n 'runtime.button.replayProgress': {\n 'zh-CN': '处理变量中({processed} / {total})',\n en: 'Processing variables ({processed} / {total})',\n },\n 'runtime.button.replayingFloorLog': {\n 'zh-CN': '正在重演 {index},内容:{content}',\n en: 'Replaying {index}. Content: {content}',\n },\n 'runtime.button.replayTitle': {\n 'zh-CN': '[MVU]楼层重演',\n en: '[MVU] Floor replay',\n },\n 'runtime.button.replayCompleted': {\n 'zh-CN': '已将 {floor} 层变量状态重演完毕,共重演 {count} 楼',\n en: 'Variable state replay for floor {floor} is complete ({count} floors replayed)',\n },\n 'runtime.button.extraModelNotEnabled': {\n 'zh-CN': '当前配置没有启用额外模型解析,不需要进行此操作',\n en: 'Extra-model parsing is not enabled by the current configuration',\n },\n 'runtime.button.extraModelToolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或配置指定的 LLM 不支持工具调用,请调整额外模型解析设置',\n en: 'The current TavernHelper version or configured LLM does not support tool calling. Adjust the extra-model parsing settings.',\n },\n 'runtime.button.extraModelUnsupportedByCard': {\n 'zh-CN': '当前角色卡不支持额外模型解析,无法进行此操作',\n en: 'The current character card does not support extra-model parsing',\n },\n 'runtime.button.extraModelRetryTitle': {\n 'zh-CN': '[MVU]重试额外模型解析',\n en: '[MVU] Retry extra-model parsing',\n },\n 'runtime.button.extraModelParsingCompleted': {\n 'zh-CN': '解析完成',\n en: 'Parsing completed',\n },\n 'runtime.incrementalRepair.title': {\n 'zh-CN': '[MVU]增量校正变量',\n en: '[MVU] Incremental variable repair',\n },\n 'runtime.incrementalRepair.alreadyRunning': {\n 'zh-CN': '已有一项增量校正正在运行',\n en: 'An incremental repair is already running',\n },\n 'runtime.incrementalRepair.noUsableFloor': {\n 'zh-CN': '最新楼层或上一楼没有可用的 MVU 状态,无法进行增量校正',\n en: 'The latest or previous floor has no usable MVU state',\n },\n 'runtime.incrementalRepair.directionPrompt': {\n 'zh-CN':\n '可选:补充本次增量校正方向;直接确认留空则自动审计。\\n例如:核对生命值归零后的即时后果,或补齐本轮明确获得的物品。',\n en: 'Optional: add a direction for this incremental repair. Confirm with an empty input for automatic audit.\\nFor example: check the immediate zero-HP aftermath or add an item clearly obtained this floor.',\n },\n 'runtime.incrementalRepair.requestFailed': {\n 'zh-CN': '增量校正请求失败或已被中止,未修改变量',\n en: 'The incremental repair failed or was cancelled; variables were not changed',\n },\n 'runtime.incrementalRepair.replayFailed': {\n 'zh-CN': '整楼重放失败,未提交校正:\\n{detail}',\n en: 'Full-floor replay failed. No repair was committed:\\n{detail}',\n },\n 'runtime.incrementalRepair.sourceChanged': {\n 'zh-CN': '等待期间聊天、楼层、回复版本或变量已经变化,本次结果已作废',\n en: 'The chat, floor, reply version, or variables changed while waiting; this result was discarded',\n },\n 'runtime.incrementalRepair.noEffectiveChanges': {\n 'zh-CN': '校正补丁没有产生实际变化,未写入正文或变量',\n en: 'The repair patch made no effective changes and was not written',\n },\n 'runtime.incrementalRepair.previewTitle': {\n 'zh-CN': '增量校正预览',\n en: 'Incremental repair preview',\n },\n 'runtime.incrementalRepair.previewDescription': {\n 'zh-CN':\n '模型提出 {count} 项修正。确认后合并本楼更新块,并从上一楼变量完整重算本楼,统一保存正文与变量。',\n en: 'The model proposed {count} repairs. The merged floor will be replayed from the preceding variables, then saved together with its resulting state.',\n },\n 'runtime.incrementalRepair.showRawPatch': {\n 'zh-CN': '查看原始补丁',\n en: 'Show raw patch',\n },\n 'runtime.incrementalRepair.applyButton': {\n 'zh-CN': '应用修正',\n en: 'Apply repairs',\n },\n 'runtime.incrementalRepair.cancelButton': {\n 'zh-CN': '取消',\n en: 'Cancel',\n },\n 'runtime.incrementalRepair.appliedClickToUndo': {\n 'zh-CN': '增量校正已应用并写回正文;点击此通知可撤销',\n en: 'Incremental repair applied and written back; click this notice to undo',\n },\n 'runtime.incrementalRepair.undoStateChanged': {\n 'zh-CN': '变量或回复在校正后又发生了变化,为避免覆盖新进度,不能直接撤销',\n en: 'Variables or the reply changed after repair; undo was blocked to protect newer progress',\n },\n 'runtime.incrementalRepair.undone': {\n 'zh-CN': '已撤销此次增量校正',\n en: 'The incremental repair was undone',\n },\n 'runtime.button.cleanupPrompt': {\n 'zh-CN':\n '

清除旧楼层变量信息以减小聊天文件大小,避免手机崩溃

请填写要保留变量信息的楼层数(如 10 为保留最后 10 层,每 [{interval}] 层保留一层作为快照)
注意:你需要通过重演才能回退游玩到未保留变量信息的楼层',\n en: '

Remove variable data from old floors to reduce chat file size and help prevent mobile crashes.

Enter how many recent floors to retain (for example, 10 retains the latest 10 floors, with one snapshot every [{interval}] floors).
Note: you must replay variables before returning to floors whose variable data was removed.',\n },\n 'runtime.button.cleanupFailedTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量失败',\n en: '[MVU] Failed to clean old floor variables',\n },\n 'runtime.button.cleanupSucceededTitle': {\n 'zh-CN': '[MVU]清理旧楼层变量成功',\n en: '[MVU] Old floor variables cleaned',\n },\n 'runtime.button.cleanupCompleted': {\n 'zh-CN': '已清理旧变量,保留了最后 {depth} 层的变量',\n en: 'Old variables were cleaned; variables for the latest {depth} floors were retained',\n },\n 'runtime.button.snapshotMarkedLog': {\n 'zh-CN': '将 [{floor}] 层作为快照楼层',\n en: 'Marked floor [{floor}] as a snapshot floor',\n },\n\n 'runtime.characterOverride.worldbookReadFailed': {\n 'zh-CN': '无法读取角色世界书“{worldbook}”',\n en: 'Unable to read character lorebook “{worldbook}”',\n },\n 'runtime.characterOverride.contentMustBeObject': {\n 'zh-CN': '配置正文必须是 JSON 对象',\n en: 'The settings content must be a JSON object',\n },\n 'runtime.characterOverride.conflictPrompt': {\n 'zh-CN':\n '角色世界书“{worldbook}”中的 {entry} 已被其他来源修改。是否使用待保存的角色卡配置覆盖它?',\n en: '{entry} in character lorebook “{worldbook}” was modified by another source. Overwrite it with the pending character-card configuration?',\n },\n 'runtime.characterOverride.overwriteButton': {\n 'zh-CN': '覆盖',\n en: 'Overwrite',\n },\n 'runtime.characterOverride.loadLatestButton': {\n 'zh-CN': '加载最新配置',\n en: 'Load latest configuration',\n },\n 'runtime.characterOverride.bindingReadFailedLog': {\n 'zh-CN': '[MVU]读取角色世界书绑定失败',\n en: '[MVU] Failed to read the character lorebook binding',\n },\n 'runtime.characterOverride.readFailedLog': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read the character-card configuration',\n },\n 'runtime.characterOverride.readFailedTitle': {\n 'zh-CN': '[MVU]读取角色卡配置失败',\n en: '[MVU] Failed to read character-card configuration',\n },\n 'runtime.characterOverride.stopWithUnsavedChangesLog': {\n 'zh-CN': '[MVU]停止角色卡配置控制器时仍未能保存“{worldbook}”中的修改',\n en: '[MVU] Changes in “{worldbook}” were still unsaved when the character-card configuration controller stopped',\n },\n 'runtime.characterOverride.reloadFailedLog': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload the character-card configuration',\n },\n 'runtime.characterOverride.reloadFailedTitle': {\n 'zh-CN': '[MVU]重新读取角色卡配置失败',\n en: '[MVU] Failed to reload character-card configuration',\n },\n 'runtime.characterOverride.duplicateEntries': {\n 'zh-CN':\n '角色世界书“{worldbook}”中存在 {count} 个关闭的 {entry} 条目,仅使用扫描到的第一个。',\n en: 'Character lorebook “{worldbook}” contains {count} disabled {entry} entries. Only the first scanned entry will be used.',\n },\n 'runtime.characterOverride.duplicateEntriesTitle': {\n 'zh-CN': '[MVU]角色卡配置重复',\n en: '[MVU] Duplicate character-card configuration',\n },\n 'runtime.characterOverride.invalidWorldbookData': {\n 'zh-CN': '角色世界书“{worldbook}”的数据格式无效',\n en: 'Character lorebook “{worldbook}” has an invalid data format',\n },\n 'runtime.characterOverride.invalidConfigurationLog': {\n 'zh-CN': '[MVU]角色世界书“{worldbook}”的 {entry} 配置无效',\n en: '[MVU] The {entry} configuration in character lorebook “{worldbook}” is invalid',\n },\n 'runtime.characterOverride.invalidConfiguration': {\n 'zh-CN': '角色世界书“{worldbook}”中的配置无效,已按不存在处理:{cause}',\n en: 'The configuration in character lorebook “{worldbook}” is invalid and will be treated as absent: {cause}',\n },\n 'runtime.characterOverride.invalidConfigurationTitle': {\n 'zh-CN': '[MVU]角色卡配置无效',\n en: '[MVU] Invalid character-card configuration',\n },\n 'runtime.characterOverride.saveFailedLog': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save the character-card configuration',\n },\n 'runtime.characterOverride.saveFailedTitle': {\n 'zh-CN': '[MVU]保存角色卡配置失败',\n en: '[MVU] Failed to save character-card configuration',\n },\n\n 'runtime.cleanup.title': {\n 'zh-CN': '[MVU]自动清理',\n en: '[MVU] Automatic cleanup',\n },\n 'runtime.cleanup.legacyPrompt': {\n 'zh-CN':\n '检测到可以清理本聊天文件中的旧变量以减小文件体积,是否清理?(备份会消耗较多内存,手机上建议关闭其他后台应用后进行,或在计算机上备份)',\n en: 'Old variables can be removed from this chat to reduce its file size. Clean them now? (Creating a backup uses considerable memory; on mobile, close other background apps first or create the backup on a computer.)',\n },\n 'runtime.cleanup.cleanOnlyButton': {\n 'zh-CN': '仅清理',\n en: 'Clean only',\n },\n 'runtime.cleanup.doNotRemindButton': {\n 'zh-CN': '不再提醒',\n en: 'Do not remind me again',\n },\n 'runtime.cleanup.backupAndCleanButton': {\n 'zh-CN': '备份并清理',\n en: 'Back up and clean',\n },\n 'runtime.cleanup.starting': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量…',\n en: 'Cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.startingWithBackup': {\n 'zh-CN': '即将开始清理旧聊天记录中的变量,并自动生成备份…',\n en: 'Creating a backup, then cleaning variables from old chat messages…',\n },\n 'runtime.cleanup.exportFailed': {\n 'zh-CN': '聊天记录导出失败,放弃清理:{cause}',\n en: 'Chat export failed; cleanup was cancelled: {cause}',\n },\n 'runtime.cleanup.exportSucceeded': {\n 'zh-CN': '聊天记录导出成功:{message}',\n en: 'Chat exported successfully: {message}',\n },\n 'runtime.cleanup.cleanedMessages': {\n 'zh-CN': '已清理旧聊天记录中的 {count} 条消息',\n en: 'Cleaned {count} messages from the old chat history',\n },\n 'runtime.cleanup.cleanedFloorsLog': {\n 'zh-CN': '[MVU]已清理 {count} 层的消息',\n en: '[MVU] Cleaned messages on {count} floors',\n },\n 'runtime.cleanup.restoreNotNeededLog': {\n 'zh-CN': '最近 {count} 层都包含变量数据,不需要进行恢复。',\n en: 'All of the latest {count} floors contain variable data; restoration is not needed.',\n },\n 'runtime.cleanup.restoreUnavailable': {\n 'zh-CN': '在 0~{floor} 层找不到有效的变量信息,无法进行楼层变量恢复',\n en: 'No valid variable data was found between floors 0 and {floor}; floor variables cannot be restored',\n },\n 'runtime.cleanup.restoreTitle': {\n 'zh-CN': '[MVU]恢复旧楼层变量',\n en: '[MVU] Restore old floor variables',\n },\n\n 'runtime.initvar.noMessagesLog': {\n 'zh-CN': '不存在任何一条消息,退出',\n en: 'No messages exist; initialization stopped',\n },\n 'runtime.initvar.greetingRequired': {\n 'zh-CN': '需要有开场白才能初始化变量',\n en: 'A greeting message is required to initialize variables',\n },\n 'runtime.initvar.initializationFailedTitle': {\n 'zh-CN': '[MVU]变量初始化失败',\n en: '[MVU] Variable initialization failed',\n },\n 'runtime.initvar.oldLorebookFormatLog': {\n 'zh-CN': '检测到旧的 initialized_lorebooks 数组格式,正在迁移到新的对象格式。',\n en: 'Old initialized_lorebooks array format detected. Migrating to the new object format.',\n },\n 'runtime.initvar.chatVariablesInitializedLog': {\n 'zh-CN': '聊天变量已初始化。',\n en: 'Chat variables initialized.',\n },\n 'runtime.initvar.blockParseFailedLog': {\n 'zh-CN': '解析 initvar 块失败:{cause}',\n en: 'Failed to parse initvar block: {cause}',\n },\n 'runtime.initvar.initializationCompletedLog': {\n 'zh-CN': '变量初始化完成',\n en: 'Variable initialization completed',\n },\n 'runtime.initvar.initializationSucceeded': {\n 'zh-CN': '已加载新的世界书初始化变量,当前使用世界书:
{books}',\n en: 'New lorebook initialization variables were loaded. Active lorebooks:
{books}',\n },\n 'runtime.initvar.initializationSucceededTitle': {\n 'zh-CN': '[MVU]变量初始化成功',\n en: '[MVU] Variable initialization succeeded',\n },\n 'runtime.initvar.primaryLorebookReadFailedLog': {\n 'zh-CN': '获取角色主 lorebook 失败,已忽略',\n en: 'Failed to obtain the primary character lorebook; ignoring it',\n },\n 'runtime.initvar.entryParseFailedLog': {\n 'zh-CN': '解析世界书条目“{comment}”失败:{cause}',\n en: 'Failed to parse lorebook entry “{comment}”: {cause}',\n },\n 'runtime.initvar.entryParseFailedTitle': {\n 'zh-CN': '[MVU]解析世界书条目“{comment}”失败',\n en: '[MVU] Failed to parse lorebook entry “{comment}”',\n },\n 'runtime.initvar.noMessages': {\n 'zh-CN': '不存在任何一条消息',\n en: 'No messages exist',\n },\n\n 'runtime.notification.settingsUiUpdatedTitle': {\n 'zh-CN': '[MVU]已更新独立配置界面',\n en: '[MVU] Dedicated settings panel updated',\n },\n 'runtime.notification.settingsUiUpdatedBody': {\n 'zh-CN': '配置界面位于酒馆扩展界面中的“正则”下方,请点开了解新功能或自定义配置',\n en: 'The settings panel is located below “Regex” in SillyTavern Extensions. Open it to review new features or customize MVU.',\n },\n 'runtime.notification.autoCleanupFeatureTitle': {\n 'zh-CN': '[MVU]已更新自动清理旧变量功能',\n en: '[MVU] Automatic cleanup updated',\n },\n 'runtime.notification.autoCleanupFeatureBody': {\n 'zh-CN':\n 'MVU 现在可以自动清理旧变量来减少聊天文件大小;这不会影响你回退游玩以前的楼层。可在设置中开启“变量自动清理”。',\n en: 'MVU can now automatically remove old variable data to reduce chat file size without preventing you from returning to earlier floors. Enable Automatic Variable Cleanup in settings.',\n },\n 'runtime.notification.customApiOptionsTitle': {\n 'zh-CN': '[MVU]已更新更多自定义 API 配置',\n en: '[MVU] More custom API options added',\n },\n 'runtime.notification.customApiOptionsBody': {\n 'zh-CN':\n 'MVU 现在可以自定义 API 的温度、频率惩罚、存在惩罚和最大回复 token 数;需要酒馆助手版本 >= 4.0.14。',\n en: 'MVU now supports custom API temperature, frequency penalty, presence penalty, and maximum response tokens. TavernHelper >= 4.0.14 is required.',\n },\n 'runtime.notification.cleanupDefaultTitle': {\n 'zh-CN': '[MVU]已更新自动清理配置',\n en: '[MVU] Automatic cleanup settings updated',\n },\n 'runtime.notification.cleanupDefaultBody': {\n 'zh-CN': 'MVU 现在会自动清理较老楼层上的变量信息,以降低聊天文件大小。',\n en: 'MVU now automatically removes variable data from older floors to reduce chat file size.',\n },\n 'runtime.notification.builtinJailbreakTitle': {\n 'zh-CN': '[MVU]已内置破限',\n en: '[MVU] Built-in jailbreak prompt added',\n },\n 'runtime.notification.builtinJailbreakBody': {\n 'zh-CN':\n '现在,额外模型解析如果取消“发送预设”,则会使用内置的破限提示词——既避免写作任务又避免道歉。',\n en: 'When Send Preset is disabled, extra-model parsing now uses a built-in jailbreak prompt designed to avoid both writing tasks and refusals.',\n },\n 'runtime.notification.concurrentRequestsTitle': {\n 'zh-CN': '[MVU]已支持同时多次请求变量更新',\n en: '[MVU] Concurrent variable-update requests added',\n },\n 'runtime.notification.concurrentRequestsBody': {\n 'zh-CN':\n '现在,你可以在“变量更新方式 → 请求策略 → 请求方式”中选择并发请求,提高额外模型解析成功率并节省时间。',\n en: 'You can now choose concurrent requests under Variable Update Method → Request Strategy → Request Method to improve extra-model parsing success rates and save time.',\n },\n 'runtime.notification.buildInfo': {\n 'zh-CN': '构建信息:{date}({commit})',\n en: 'Build: {date} ({commit})',\n },\n 'runtime.notification.scriptLoadedTitle': {\n 'zh-CN': '[MVU]脚本加载成功',\n en: '[MVU] Script loaded',\n },\n\n 'runtime.filter.toolCallingUnsupported': {\n 'zh-CN': '当前 TavernHelper 版本或预设/API 不支持工具调用,已退化回 `随AI输出`',\n en: 'The current TavernHelper version or preset/API does not support tool calling. Falling back to Follow AI Output.',\n },\n 'runtime.filter.toolCallingUnsupportedTitle': {\n 'zh-CN': '[MVU]无法使用工具调用',\n en: '[MVU] Tool calling unavailable',\n },\n 'runtime.filter.globalSettingsSource': {\n 'zh-CN': '用户全局配置',\n en: 'global user settings',\n },\n 'runtime.filter.characterSettingsSource': {\n 'zh-CN': '角色卡配置',\n en: 'character-card settings',\n },\n 'runtime.filter.whitelistRegexLabel': {\n 'zh-CN': '白名单正则',\n en: 'whitelist regex',\n },\n 'runtime.filter.blacklistRegexLabel': {\n 'zh-CN': '黑名单正则',\n en: 'blacklist regex',\n },\n 'runtime.filter.invalidRegex': {\n 'zh-CN': '{source}的{label}无效,已在本轮世界书条目过滤中忽略:{cause}',\n en: 'The {label} from {source} is invalid and was ignored for this lorebook filtering pass: {cause}',\n },\n 'runtime.filter.invalidRegexTitle': {\n 'zh-CN': '[MVU]世界书条目过滤正则无效',\n en: '[MVU] Invalid lorebook entry filter regex',\n },\n 'runtime.filter.logTitle': {\n 'zh-CN': '[MVU]世界书条目黑/白名单筛选结果',\n en: '[MVU] Lorebook entry allowlist/blocklist filtering results',\n },\n 'runtime.regex.jsStyleSyntax': {\n 'zh-CN': 'JS 风格正则需要以 /pattern/flags 形式书写',\n en: 'JavaScript-style regular expressions must use the /pattern/flags form',\n },\n\n 'runtime.functionCalling.versionUnsupported': {\n 'zh-CN': '当前酒馆助手版本为 {current},工具调用需要酒馆助手 {required} 或更高版本',\n en: 'The current TavernHelper version is {current}. Tool calling requires TavernHelper {required} or later.',\n },\n 'runtime.extraModel.characterLorebookUnavailableLog': {\n 'zh-CN': '无法找到角色世界书,在多人聊天下不支持额外模型解析。',\n en: 'The character lorebook could not be found. Extra-model parsing is unavailable in group chats.',\n },\n 'runtime.extraModel.v4RequiresCustomSource': {\n 'zh-CN': '[MVU额外模型解析]格式化输出(v4兼容)需要额外模型来源为自定义,不能与插头相同。',\n en: '[MVU extra-model parsing] Formatted Output (v4 Compatible) requires a Custom model source and cannot use Same as Primary Connection.',\n },\n 'runtime.extraModel.customIncludeBodyInvalid': {\n 'zh-CN': '[MVU额外模型解析]custom_include_body 不是 YAML object,无法合并配置。',\n en: '[MVU extra-model parsing] custom_include_body is not a YAML object, so the configuration cannot be merged.',\n },\n 'runtime.extraModel.saveSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern saveSettings,不能临时更新配置。',\n en: '[MVU extra-model parsing] SillyTavern saveSettings is unavailable; settings cannot be updated temporarily.',\n },\n 'runtime.extraModel.openAiSettingsUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable.',\n },\n 'runtime.extraModel.openAiSettingsRestoreUnavailable': {\n 'zh-CN': '[MVU额外模型解析]无法获取 SillyTavern OpenAI 设置,不能恢复配置。',\n en: '[MVU extra-model parsing] SillyTavern OpenAI settings are unavailable; the configuration cannot be restored.',\n },\n 'runtime.extraModel.requesting': {\n 'zh-CN': '正在请求 AI 回复…',\n en: 'Requesting an AI response…',\n },\n 'runtime.extraModel.retrying': {\n 'zh-CN': '正在重试({attempt} / {total})…',\n en: 'Retrying ({attempt} / {total})…',\n },\n 'runtime.extraModel.updateInProgressTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新中',\n en: '[MVU extra-model parsing] Updating variables',\n },\n 'runtime.extraModel.concurrentRequests': {\n 'zh-CN': '将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.firstRequest': {\n 'zh-CN': '将先请求一次,尝试是否能成功…',\n en: 'Trying one request first…',\n },\n 'runtime.extraModel.firstRequestFailed': {\n 'zh-CN': '首次请求失败,将同时请求 {count} 次 AI 回复以提高成功率…',\n en: 'The first request failed. Requesting {count} AI responses concurrently to improve the success rate…',\n },\n 'runtime.extraModel.updateTagMissing': {\n 'zh-CN': '[MVU额外模型解析]没有能从回复中找到标签',\n en: '[MVU extra-model parsing] No tag was found in the response',\n },\n 'runtime.extraModel.updateCommandsInvalid': {\n 'zh-CN': '[MVU额外模型解析]从回复找到了标签,但其内的更新命令无效',\n en: '[MVU extra-model parsing] An tag was found, but its update commands are invalid',\n },\n 'runtime.extraModel.firstFloorSkippedLog': {\n 'zh-CN': '[MVU]对第一层永不进行额外模型解析',\n en: '[MVU] Extra-model parsing is never performed on the first floor',\n },\n 'runtime.extraModel.autoRequestDisabledLog': {\n 'zh-CN': '[MVU]不自动触发额外模型解析',\n en: '[MVU] Automatic extra-model parsing is disabled',\n },\n 'runtime.extraModel.updateFailed': {\n 'zh-CN': '建议调整变量更新方式;可在“输入框左下角魔棒 → 日志查看器”查看具体情况',\n en: 'Try adjusting the variable update method. Open Magic Wand → Log Viewer at the lower-left of the input box for details.',\n },\n 'runtime.extraModel.updateFailedTitle': {\n 'zh-CN': '[MVU额外模型解析]变量更新失败',\n en: '[MVU extra-model parsing] Variable update failed',\n },\n\n // Pi 运行时统一错误文案:只插入经过分类的参数,不拼接服务商原始响应或认证信息。\n 'runtime.pi.invalidConfig': {\n 'zh-CN': '“更多”配置无效,请检查其中的来源、API 和认证设置。',\n en: 'The More-source configuration is invalid. Check its provider, API, and authentication settings.',\n },\n 'runtime.pi.unknownProvider': {\n 'zh-CN': '“更多”中所选来源不可用,请重新选择来源。',\n en: 'The selected provider under More is unavailable. Select a provider again.',\n },\n 'runtime.pi.unsupportedApi': {\n 'zh-CN': '“更多”中所选来源不支持当前 API,请改用该来源支持的 API。',\n en: 'The selected provider under More does not support the current API. Choose an API supported by this provider.',\n },\n 'runtime.pi.unsupportedAuth': {\n 'zh-CN': '“更多”中所选来源不支持当前认证方式,请改用该来源支持的认证方式。',\n en: 'The selected provider under More does not support the current authentication method. Choose a supported method.',\n },\n 'runtime.pi.missingApiKey': {\n 'zh-CN': '请输入“更多”中所选来源的 API Key。',\n en: 'Enter an API key for the selected provider under More.',\n },\n 'runtime.pi.missingOAuthCredential': {\n 'zh-CN': '“更多”中所选来源尚未完成 OAuth 登录,请先登录。',\n en: 'The selected provider under More is not signed in with OAuth. Sign in before retrying.',\n },\n 'runtime.pi.oauthCredentialExpired': {\n 'zh-CN': '“更多”的 OAuth 凭据已过期且无法刷新,请登出后再登录。',\n en: 'The OAuth credential for More has expired and could not be refreshed. Sign out, then sign in again.',\n },\n 'runtime.pi.invalidEndpoint': {\n 'zh-CN':\n '自定义 endpoint 必须使用 HTTPS;HTTP 仅允许 localhost、127.0.0.1 或 [::1],且不能包含凭据、查询参数或片段。',\n en: 'The custom endpoint must use HTTPS. HTTP is limited to localhost, 127.0.0.1, or [::1]. Credentials, query parameters, and fragments are not allowed.',\n },\n 'runtime.pi.customEndpointNotAllowed': {\n 'zh-CN': '“更多”中所选来源使用固定 endpoint,不允许自定义。',\n en: 'The selected provider under More uses a fixed endpoint and does not allow a custom one.',\n },\n 'runtime.pi.oauthEndpointNotAllowed': {\n 'zh-CN': 'OAuth 来源必须使用注册的固定 endpoint,请清除自定义 endpoint。',\n en: 'OAuth providers must use their registered fixed endpoint. Clear the custom endpoint.',\n },\n 'runtime.pi.oauthApiMismatch': {\n 'zh-CN': '当前 OAuth 来源只能使用其锁定的兼容 API,请重新选择来源或认证方式。',\n en: 'This OAuth provider can only use its locked compatible API. Reselect the provider or authentication method.',\n },\n 'runtime.pi.missingModel': {\n 'zh-CN': '请填写“更多”来源的模型名称。',\n en: 'Enter a model name for the More source.',\n },\n 'runtime.pi.invalidContextWindow': {\n 'zh-CN': 'contextWindow 必须是正整数。',\n en: 'contextWindow must be a positive integer.',\n },\n 'runtime.pi.missingContextWindow': {\n 'zh-CN': '当前模型不在“更多”的内置目录中,请手动填写有效的 contextWindow。',\n en: 'The current model is not in the built-in catalog under More. Enter a valid contextWindow manually.',\n },\n 'runtime.pi.invalidMaxTokens': {\n 'zh-CN': '“最大回复 token 数”必须是正整数。',\n en: 'Maximum response tokens must be a positive integer.',\n },\n 'runtime.pi.maxTokensExceedContext': {\n 'zh-CN': '“最大回复 token 数”不能大于 contextWindow。',\n en: 'Maximum response tokens must not exceed contextWindow.',\n },\n 'runtime.pi.anthropicSamplingConflict': {\n 'zh-CN': 'Anthropic 的温度和 top_p 只能调整其中一个,请将另一个恢复为默认值 1。',\n en: 'Adjust either temperature or top_p for Anthropic. Reset the other to its default value of 1.',\n },\n 'runtime.pi.customHeadersInvalid': {\n 'zh-CN': '“更多”的自定义请求头必须是 YAML/JSON 对象,且值只能是字符串或 null。',\n en: 'Custom headers under More must be a YAML/JSON object whose values are strings or null.',\n },\n 'runtime.pi.customConfigParseFailed': {\n 'zh-CN': '“更多”的自定义请求配置不是有效的 YAML 或 JSON,请检查格式。',\n en: 'The custom request settings under More are not valid YAML or JSON. Check their formatting.',\n },\n 'runtime.pi.customIncludeBodyInvalid': {\n 'zh-CN': '“更多”的自定义请求体必须是 YAML/JSON 对象。',\n en: 'The custom request body under More must be a YAML/JSON object.',\n },\n 'runtime.pi.customExcludeBodyInvalid': {\n 'zh-CN': '“更多”的排除请求字段必须是 YAML/JSON 字段名数组。',\n en: 'Excluded request fields under More must be a YAML/JSON array of field names.',\n },\n 'runtime.pi.customPayloadProtectedField': {\n 'zh-CN': '“更多”的自定义请求配置不能覆盖、排除或设置认证及关键协议字段。',\n en: 'Custom request settings under More cannot override, exclude, or set authentication or other protected protocol fields.',\n },\n 'runtime.pi.payloadInvalid': {\n 'zh-CN': '“更多”来源的请求体必须是对象,请检查 API 配置。',\n en: 'The provider payload under More must be an object. Check the API configuration.',\n },\n 'runtime.pi.structuredOutputSchemaMissing': {\n 'zh-CN': '“更多”的格式化输出需要有效的 JSON Schema。',\n en: 'Formatted output under More requires a valid JSON Schema.',\n },\n 'runtime.pi.structuredOutputUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持原生格式化输出,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support native formatted output. Change the configuration and retry.',\n },\n 'runtime.pi.toolCallingUnsupported': {\n 'zh-CN': '“更多”中当前来源、API 或模型不支持所需的工具调用模式,请更换配置后重试。',\n en: 'The current provider, API, or model under More does not support the required tool-calling mode. Change the configuration and retry.',\n },\n 'runtime.pi.toolRequestRejected': {\n 'zh-CN':\n '目标端点未接受“工具调用”请求。请确认该端点和模型支持工具调用,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the tool-calling request. Confirm that the endpoint and model support tool calling, and check the API type, URL, and model name.',\n },\n 'runtime.pi.structuredOutputRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出”请求。请确认该端点和模型支持 JSON Schema 格式化输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the formatted-output request. Confirm that the endpoint and model support JSON Schema formatted output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.jsonObjectRequestRejected': {\n 'zh-CN':\n '目标端点未接受“格式化输出(v4兼容)”请求。请确认该端点和模型支持 JSON Object 输出,并检查 API 类型、地址和模型名。',\n en: 'The target endpoint did not accept the v4-compatible formatted-output request. Confirm that the endpoint and model support JSON Object output, and check the API type, URL, and model name.',\n },\n 'runtime.pi.namedToolChoiceUnsupported': {\n 'zh-CN': '“更多”中的当前 API 不支持指定工具调用,请更换 API 或调整工具选择方式。',\n en: 'The current API under More does not support named tool choice. Choose another API or adjust the tool-choice mode.',\n },\n 'runtime.pi.invalidToolDefinition': {\n 'zh-CN':\n '“更多”来源的工具定义无效:function name 不能为空,且 parameters 根节点必须是 object。',\n en: 'The tool definition under More is invalid: function name cannot be empty, and the parameters root must be an object.',\n },\n 'runtime.pi.invalidToolCall': {\n 'zh-CN': '“更多”来源返回了无效的工具调用,请重试或更换模型。',\n en: 'The More source returned an invalid tool call. Retry or choose another model.',\n },\n 'runtime.pi.toolUseMissingCall': {\n 'zh-CN': '“更多”来源以工具调用结束,但没有返回工具调用内容,请重试或更换模型。',\n en: 'The More source stopped for tool use without returning a tool call. Retry or choose another model.',\n },\n 'runtime.pi.imageInputUnsupported': {\n 'zh-CN': '“更多”中的当前模型不支持图片输入,请移除图片或更换模型。',\n en: 'The current model under More does not support image input. Remove the image or choose another model.',\n },\n 'runtime.pi.contextInvalidImage': {\n 'zh-CN': '“更多”的图片输入必须是可解码的 image/* base64 data URL。',\n en: 'Image input under More must be a decodable image/* base64 data URL.',\n },\n 'runtime.pi.remoteImageUnsupported': {\n 'zh-CN': '“更多”当前不支持远程图片 URL,请改用 base64 data URL。',\n en: 'The More source does not currently support remote image URLs. Use a base64 data URL instead.',\n },\n 'runtime.pi.videoUnsupported': {\n 'zh-CN': '“更多”链路当前不支持视频内容,请移除视频后重试。',\n en: 'The More path does not currently support video content. Remove the video and retry.',\n },\n 'runtime.pi.contextSystemPayloadMismatch': {\n 'zh-CN': '无法在服务商请求中完整恢复 system 消息,已停止发送。请更新脚本或更换协议后重试。',\n en: 'Could not restore all system messages in the provider payload. Sending was stopped. Update the script or select another API and try again.',\n },\n 'runtime.pi.contextMissingToolCall': {\n 'zh-CN': '第 {index} 条历史工具结果无法找到对应的工具调用,请检查聊天历史。',\n en: 'Historical tool result {index} has no matching tool call. Check the chat history.',\n },\n 'runtime.pi.contextUnsupportedContent': {\n 'zh-CN': '第 {index} 条消息包含“更多”当前不支持的内容,请移除该内容后重试。',\n en: 'Message {index} contains content that More does not currently support. Remove it and retry.',\n },\n 'runtime.pi.promptCaptureFailed': {\n 'zh-CN': '未能捕获 SillyTavern 构建后的最终提示词,请重试并查看日志。',\n en: 'Could not capture the final prompt built by SillyTavern. Retry and check the logs.',\n },\n 'runtime.pi.requestAlreadyActive': {\n 'zh-CN': '同一“更多”请求已在运行,请等待它结束或先停止该请求。',\n en: 'The same More-source request is already running. Wait for it to finish or stop it first.',\n },\n 'runtime.pi.tokenBudgetExceeded': {\n 'zh-CN':\n '请求预计占用 {estimatedInput} 个输入 token;加上 {maxTokens} 个回复 token 和 {reserve} 个预留 token 后,超过 {contextWindow} 的上下文窗口。请缩短聊天历史、降低最大回复 token 数,或修正 contextWindow。',\n en: 'The request is estimated to use {estimatedInput} input tokens. Together with {maxTokens} response tokens and {reserve} reserved tokens, it exceeds the {contextWindow}-token context window. Shorten chat history, reduce maximum response tokens, or correct contextWindow.',\n },\n 'runtime.pi.requestAborted': {\n 'zh-CN': '“更多”请求已取消。',\n en: 'The More-source request was cancelled.',\n },\n 'runtime.pi.browserNetworkError': {\n 'zh-CN':\n '浏览器无法连接“更多”中所选来源。请检查网络、地址配置,并确认来源允许浏览器 CORS 请求。',\n en: 'The browser could not reach the selected provider under More. Check the network and address settings, and confirm that the provider allows browser CORS requests.',\n },\n 'runtime.pi.proxyUnavailable': {\n 'zh-CN':\n '没有开启Proxy。请在 SillyTavern 的 config.yaml 中开启 enableCorsProxy,或使用 --corsProxy 启动参数,然后重启 SillyTavern。',\n en: 'Proxy is not enabled. Enable enableCorsProxy in SillyTavern config.yaml, or start SillyTavern with --corsProxy, then restart SillyTavern.',\n },\n 'runtime.pi.requestFailed': {\n 'zh-CN': '“更多”来源请求失败。请检查来源配置、凭据和网络后重试。',\n en: 'The provider request under More failed. Check the provider settings, credentials, and network, then retry.',\n },\n 'runtime.pi.featureDisabled': {\n 'zh-CN': '此构建已关闭“更多”模型来源;请选择“与插头相同”或“自定义”。',\n en: 'The More model source is disabled in this build. Select Same as current connection or Custom.',\n },\n 'runtime.pi.protocolError': {\n 'zh-CN': '“更多”来源返回了无效的协议数据,请重试或更换来源/API。',\n en: 'The provider under More returned invalid protocol data. Retry or choose another provider/API.',\n },\n 'runtime.pi.lengthTruncated': {\n 'zh-CN': '“更多”回复因达到长度上限而被截断,请增加最大回复 token 数或缩短请求内容。',\n en: 'The response from More was truncated at the length limit. Increase maximum response tokens or shorten the request content.',\n },\n 'runtime.pi.emptyResponse': {\n 'zh-CN': '“更多”来源返回了空回复,请重试或更换模型。',\n en: 'The More source returned an empty response. Retry or choose another model.',\n },\n 'runtime.pi.thinkingOnlyResponse': {\n 'zh-CN': '“更多”回复仅包含 thinking,没有可用的业务内容,请重试或更换模型。',\n en: 'The response from More contained only thinking and no usable content. Retry or choose another model.',\n },\n 'runtime.pi.deferredResponse': {\n 'zh-CN': '“更多”回复尚未完成,当前不支持 deferred 结果。',\n en: 'The response from More is not complete; deferred results are not currently supported.',\n },\n 'runtime.pi.oauth.cancelled': {\n 'zh-CN': '“更多”的 OAuth 操作已取消,原凭据未更改。',\n en: 'The OAuth operation under More was cancelled. The existing credential was not changed.',\n },\n 'runtime.pi.oauth.browserUnavailable': {\n 'zh-CN':\n '当前页面缺少 OAuth 所需的 Fetch 或 Web Crypto 能力,请在支持的安全浏览器环境中重试。',\n en: 'Fetch or Web Crypto required for OAuth is unavailable. Retry in a supported secure browser context.',\n },\n 'runtime.pi.oauth.unsupportedProvider': {\n 'zh-CN': '“更多”中所选来源不支持浏览器 OAuth 登录,请改用 API Key 或其他来源。',\n en: 'The selected provider under More does not support browser OAuth sign-in. Use an API key or another provider.',\n },\n 'runtime.pi.oauth.invalidCallback': {\n 'zh-CN':\n 'OAuth callback URL 无效。请从浏览器地址栏复制完整的 loopback callback URL 后重试。',\n en: 'The OAuth callback URL is invalid. Copy the complete loopback callback URL from the browser address bar and retry.',\n },\n 'runtime.pi.oauth.stateMismatch': {\n 'zh-CN': 'OAuth callback 与当前登录尝试不匹配。请取消当前尝试并重新登录。',\n en: 'The OAuth callback does not match the current sign-in attempt. Cancel it and start a new sign-in.',\n },\n 'runtime.pi.oauth.authorizationFailed': {\n 'zh-CN': 'OAuth 授权未完成或被来源拒绝,请重新发起登录。',\n en: 'OAuth authorization was not completed or was rejected by the provider. Start a new sign-in.',\n },\n 'runtime.pi.oauth.browserNetwork': {\n 'zh-CN':\n '浏览器无法连接 OAuth token 服务。请检查网络,并确认来源允许浏览器 CORS 请求;MVU 不会自动改用代理。',\n en: 'The browser could not reach the OAuth token service. Check the network and confirm that the provider allows browser CORS requests; MVU will not silently use a proxy.',\n },\n 'runtime.pi.oauth.tokenHttp': {\n 'zh-CN': 'OAuth token 交换被来源拒绝。请重新发起登录,不要重用之前的 callback URL。',\n en: 'The provider rejected the OAuth token exchange. Start a new sign-in and do not reuse the previous callback URL.',\n },\n 'runtime.pi.oauth.tokenResponse': {\n 'zh-CN': 'OAuth token 服务返回了无效响应,请稍后重新登录。',\n en: 'The OAuth token service returned an invalid response. Sign in again later.',\n },\n 'runtime.pi.oauth.accountId': {\n 'zh-CN': '无法从 OAuth 凭据中确认账户,请重新登录或选择其他账户。',\n en: 'The account could not be identified from the OAuth credential. Sign in again or choose another account.',\n },\n 'runtime.pi.oauth.attemptExpired': {\n 'zh-CN': 'OAuth 登录尝试已过期,请重新点击登录并使用新的授权链接。',\n en: 'The OAuth sign-in attempt has expired. Start a new sign-in and use the new authorization link.',\n },\n 'runtime.pi.oauth.attemptUsed': {\n 'zh-CN': 'OAuth 登录尝试已结束或 callback URL 已使用,请重新发起登录。',\n en: 'The OAuth sign-in attempt has ended or its callback URL was already used. Start a new sign-in.',\n },\n 'runtime.pi.oauth.credentialStore': {\n 'zh-CN': '无法读取或保存“更多”的 OAuth 凭据,请检查浏览器存储后重试。',\n en: 'The OAuth credential for More could not be read or saved. Check browser storage and retry.',\n },\n\n 'runtime.variableUpdate.unknownCommand': {\n 'zh-CN': '未知命令',\n en: 'unknown command',\n },\n 'runtime.variableUpdate.errorTitle': {\n 'zh-CN': '[MVU]发生变量更新错误,可能需要重 Roll:{command}',\n en: '[MVU] Variable update error; rerolling may help: {command}',\n },\n 'runtime.variableUpdate.errorDetail': {\n 'zh-CN': '错误详情:{detail}',\n en: 'Error details: {detail}',\n },\n 'runtime.variableUpdate.messageOverwrittenTitle': {\n 'zh-CN': '[MVU]消息内容被覆盖',\n en: '[MVU] Message content overwritten',\n },\n 'runtime.variableUpdate.messageOverwritten': {\n 'zh-CN':\n '第 {messageId} 层消息中其他插件的修改被覆盖,可尝试重新运行相关插件对此楼层的解析。',\n en: \"Changes another plugin made to message #{messageId} were overwritten. Try rerunning that plugin's analysis for this message.\",\n },\n 'runtime.variableUpdate.setPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 set 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the set command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.assignPrimitive': {\n 'zh-CN': '路径“{path}”保存的是原始值({type}),无法向其中 assign;已跳过操作。{reason}',\n en: \"Cannot assign into path '{path}' because it holds a primitive value ({type}). The operation was skipped. {reason}\",\n },\n 'runtime.variableUpdate.mergeNonExtensibleObject': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象合并数据。{reason}',\n en: \"SCHEMA VIOLATION: Cannot merge data into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignUnknownKey': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的对象写入新键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign the new key '{key}' into the non-extensible object at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法向路径“{path}”处不可扩展的数组写入元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot assign elements into the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.assignMissingParent': {\n 'zh-CN': '路径“{path}”不存在,且其父级不可扩展,无法向其中 assign。{reason}',\n en: \"Cannot assign into the non-existent path '{path}' without an extensible parent. {reason}\",\n },\n 'runtime.variableUpdate.mergeArrayIntoObject': {\n 'zh-CN': '无法将数组合并到路径“{path}”处的对象中。',\n en: \"Cannot merge an array into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.mergeNonObjectIntoObject': {\n 'zh-CN': '无法将非对象值合并到路径“{path}”处的对象中。',\n en: \"Cannot merge a non-object value into the object at '{path}'.\",\n },\n 'runtime.variableUpdate.templateResolutionFailed': {\n 'zh-CN': '解析路径“{path}”处的模板元数据失败:{cause}',\n en: \"Failed to resolve template metadata at '{path}': {cause}\",\n },\n 'runtime.variableUpdate.assignInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.assign 参数无效。',\n en: \"Invalid arguments for _.assign at path '{path}'.\",\n },\n 'runtime.variableUpdate.removePathUndefined': {\n 'zh-CN': '_.remove 命令中的路径“{path}”未定义。',\n en: \"Path '{path}' is undefined in the _.remove command.\",\n },\n 'runtime.variableUpdate.deleteTargetUndetermined': {\n 'zh-CN': '无法确定路径“{path}”上的命令要删除的目标。{reason}',\n en: \"Could not determine the deletion target for the command at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removePathMissing': {\n 'zh-CN': '无法从不存在的路径“{path}”中删除内容。{reason}',\n en: \"Cannot remove from the non-existent path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonExtensibleArray': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”处不可扩展的数组中删除元素。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove an element from the non-extensible array at path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeRequiredKey': {\n 'zh-CN': 'SCHEMA 违规:无法从路径“{path}”中删除必需键“{key}”。{reason}',\n en: \"SCHEMA VIOLATION: Cannot remove the required key '{key}' from path '{path}'. {reason}\",\n },\n 'runtime.variableUpdate.removeNonCollection': {\n 'zh-CN': '路径“{path}”处的值不是数组或对象,无法从中删除内容;已跳过命令。{reason}',\n en: \"Cannot remove from path '{path}' because it is not an array or object. The command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.removeExecutionFailed': {\n 'zh-CN': '无法在路径“{path}”上执行 remove。',\n en: \"Failed to execute remove at path '{path}'.\",\n },\n 'runtime.variableUpdate.addPathMissing': {\n 'zh-CN': 'stat_data 中不存在路径“{path}”,已跳过 add 命令。{reason}',\n en: \"Path '{path}' does not exist in stat_data; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.dateDeltaNotNumber': {\n 'zh-CN': '日期操作的增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' for the date operation is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.deltaNotNumber': {\n 'zh-CN': '增量“{delta}”不是数字,已跳过 add 命令。{reason}',\n en: \"Delta '{delta}' is not a number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addUnsupportedValue': {\n 'zh-CN': '路径“{path}”处的值不是日期或数字,已跳过 add 命令。{reason}',\n en: \"The value at path '{path}' is not a date or number; the add command was skipped. {reason}\",\n },\n 'runtime.variableUpdate.addInvalidArguments': {\n 'zh-CN': '路径“{path}”上的 _.add 参数数量无效。{reason}',\n en: \"Invalid number of arguments for _.add at path '{path}'. {reason}\",\n },\n\n 'runtime.apiProfile.nameRequired': {\n 'zh-CN': 'API 方案名称不能为空',\n en: 'API profile name cannot be empty',\n },\n 'runtime.apiProfile.notFound': {\n 'zh-CN': '未找到 API 方案:{name}',\n en: 'API profile not found: {name}',\n },\n 'runtime.apiProfile.selectOrEnterName': {\n 'zh-CN': '请先输入或选择一个 API 方案名称',\n en: 'Enter or select an API profile name first',\n },\n 'runtime.apiProfile.alreadyExists': {\n 'zh-CN': 'API 方案「{name}」已存在',\n en: 'API profile “{name}” already exists',\n },\n 'runtime.apiProfile.piConfigRequired': {\n 'zh-CN': '“更多”API 方案缺少完整的连接配置,无法保存。',\n en: 'A More-source API profile requires a complete connection snapshot before it can be saved.',\n },\n 'runtime.apiProfile.enterNewName': {\n 'zh-CN': '请先输入新方案名称',\n en: 'Enter a name for the new API profile first',\n },\n 'runtime.preset.notSelected': {\n 'zh-CN': '未选择额外模型预设',\n en: 'No extra-model preset was selected',\n },\n 'runtime.preset.apiUnavailable': {\n 'zh-CN': '当前环境未提供 getPreset 接口',\n en: 'The getPreset API is unavailable in the current environment',\n },\n 'runtime.preset.readFailed': {\n 'zh-CN': '无法读取预设“{name}”',\n en: 'Unable to read preset “{name}”',\n },\n\n 'runtime.functionCall.invalidJsonPatch': {\n 'zh-CN': '不是有效的 json patch',\n en: 'Invalid JSON Patch',\n },\n 'runtime.functionCall.updateBlockParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]无法解析变量更新块。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the variable update block. Content: {content}; error: {cause}',\n },\n 'runtime.functionCall.resultParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]函数调用结果解析失败:{cause}',\n en: '[MVU extra-model parsing] Failed to parse the function-call result: {cause}',\n },\n 'runtime.functionCall.formattedOutputParseFailedLog': {\n 'zh-CN': '[MVU额外模型解析]格式化输出解析失败。内容:{content};错误:{cause}',\n en: '[MVU extra-model parsing] Failed to parse formatted output. Content: {content}; error: {cause}',\n },\n});\n","/*!\n * vue-i18n v11.1.12\n * (c) 2025 kazuya kawaguchi\n * Released under the MIT License.\n */\nimport { CORE_ERROR_CODES_EXTEND_POINT, createCompileError, CORE_WARN_CODES_EXTEND_POINT, isMessageAST, AST_NODE_PROPS_KEYS, DEFAULT_LOCALE, updateFallbackLocale, setFallbackContext, createCoreContext, clearDateTimeFormat, clearNumberFormat, setAdditionalMeta, getFallbackContext, NOT_REOSLVED, isTranslateFallbackWarn, isTranslateMissingWarn, parseTranslateArgs, translate, MISSING_RESOLVE_VALUE, parseDateTimeArgs, datetime, parseNumberArgs, number, isMessageFunction, fallbackWithLocaleChain, NUMBER_FORMAT_OPTIONS_KEYS, DATETIME_FORMAT_OPTIONS_KEYS, registerMessageCompiler, compile, registerMessageResolver, resolveValue, registerLocaleFallbacker, setDevToolsHook } from '@intlify/core-base';\nimport { getGlobalThis, makeSymbol, format, isObject, create, isPlainObject, isArray, deepCopy, isString, hasOwn, warn, isBoolean, isRegExp, isFunction, inBrowser, assign, isNumber, createEmitter, warnOnce, isEmptyObject } from '@intlify/shared';\nimport { createVNode, Text, computed, watch, getCurrentInstance, ref, shallowRef, Fragment, defineComponent, h, effectScope, inject, onMounted, onUnmounted, isRef } from 'vue';\nimport { setupDevtoolsPlugin } from '@vue/devtools-api';\n\n/**\n * Vue I18n Version\n *\n * @remarks\n * Semver format. Same format as the package.json `version` field.\n *\n * @VueI18nGeneral\n */\nconst VERSION = '11.1.12';\n/**\n * This is only called in esm-bundler builds.\n * istanbul-ignore-next\n */\nfunction initFeatureFlags() {\n if (typeof __VUE_I18N_FULL_INSTALL__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_FULL_INSTALL__ = true;\n }\n if (typeof __VUE_I18N_LEGACY_API__ !== 'boolean') {\n getGlobalThis().__VUE_I18N_LEGACY_API__ = true;\n }\n if (typeof __INTLIFY_DROP_MESSAGE_COMPILER__ !== 'boolean') {\n getGlobalThis().__INTLIFY_DROP_MESSAGE_COMPILER__ = false;\n }\n if (typeof __INTLIFY_PROD_DEVTOOLS__ !== 'boolean') {\n getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false;\n }\n}\n\nconst I18nErrorCodes = {\n // composer module errors\n UNEXPECTED_RETURN_TYPE: CORE_ERROR_CODES_EXTEND_POINT, // 24\n // legacy module errors\n INVALID_ARGUMENT: 25,\n // i18n module errors\n MUST_BE_CALL_SETUP_TOP: 26,\n NOT_INSTALLED: 27,\n // directive module errors\n REQUIRED_VALUE: 28,\n INVALID_VALUE: 29,\n // vue-devtools errors\n CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: 30,\n NOT_INSTALLED_WITH_PROVIDE: 31,\n // unexpected error\n UNEXPECTED_ERROR: 32,\n // not compatible legacy vue-i18n constructor\n NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,\n // Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly\n NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34\n};\nfunction createI18nError(code, ...args) {\n return createCompileError(code, null, (process.env.NODE_ENV !== 'production') ? { messages: errorMessages, args } : undefined);\n}\nconst errorMessages = {\n [I18nErrorCodes.UNEXPECTED_RETURN_TYPE]: 'Unexpected return type in composer',\n [I18nErrorCodes.INVALID_ARGUMENT]: 'Invalid argument',\n [I18nErrorCodes.MUST_BE_CALL_SETUP_TOP]: 'Must be called at the top of a `setup` function',\n [I18nErrorCodes.NOT_INSTALLED]: 'Need to install with `app.use` function',\n [I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpected error',\n [I18nErrorCodes.REQUIRED_VALUE]: `Required in value: {0}`,\n [I18nErrorCodes.INVALID_VALUE]: `Invalid value`,\n [I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN]: `Cannot setup vue-devtools plugin`,\n [I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE]: 'Need to install with `provide` function',\n [I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]: 'Not compatible legacy VueI18n.',\n [I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY]: 'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly'\n};\n\nconst TranslateVNodeSymbol = \n/* #__PURE__*/ makeSymbol('__translateVNode');\nconst DatetimePartsSymbol = /* #__PURE__*/ makeSymbol('__datetimeParts');\nconst NumberPartsSymbol = /* #__PURE__*/ makeSymbol('__numberParts');\nconst EnableEmitter = /* #__PURE__*/ makeSymbol('__enableEmitter');\nconst DisableEmitter = /* #__PURE__*/ makeSymbol('__disableEmitter');\nconst SetPluralRulesSymbol = makeSymbol('__setPluralRules');\nmakeSymbol('__intlifyMeta');\nconst InejctWithOptionSymbol = \n/* #__PURE__*/ makeSymbol('__injectWithOption');\nconst DisposeSymbol = /* #__PURE__*/ makeSymbol('__dispose');\n\nconst I18nWarnCodes = {\n FALLBACK_TO_ROOT: CORE_WARN_CODES_EXTEND_POINT, // 8\n NOT_FOUND_PARENT_SCOPE: 9,\n IGNORE_OBJ_FLATTEN: 10,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_LEGACY_MODE: 11,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12,\n // duplicate `useI18n` calling\n DUPLICATE_USE_I18N_CALLING: 13\n};\nconst warnMessages = {\n [I18nWarnCodes.FALLBACK_TO_ROOT]: `Fall back to {type} '{key}' with root locale.`,\n [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.`,\n [I18nWarnCodes.IGNORE_OBJ_FLATTEN]: `Ignore object flatten: '{key}' key has an string value`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_LEGACY_MODE]: `Legacy API mode has been deprecated in v11. Use Composition API mode instead.\\nAbout how to use the Composition API mode, see https://vue-i18n.intlify.dev/guide/advanced/composition.html`,\n /**\n * @deprecated will be removed at vue-i18n v12\n */\n [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.`,\n [I18nWarnCodes.DUPLICATE_USE_I18N_CALLING]: \"Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component.\"\n};\nfunction getWarnMessage(code, ...args) {\n return format(warnMessages[code], ...args);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Transform flat json in obj to normal json in obj\n */\nfunction handleFlatJson(obj) {\n // check obj\n if (!isObject(obj)) {\n return obj;\n }\n if (isMessageAST(obj)) {\n return obj;\n }\n for (const key in obj) {\n // check key\n if (!hasOwn(obj, key)) {\n continue;\n }\n // handle for normal json\n if (!key.includes('.')) {\n // recursive process value if value is also a object\n if (isObject(obj[key])) {\n handleFlatJson(obj[key]);\n }\n }\n // handle for flat json, transform to normal json\n else {\n // go to the last object\n const subKeys = key.split('.');\n const lastIndex = subKeys.length - 1;\n let currentObj = obj;\n let hasStringValue = false;\n for (let i = 0; i < lastIndex; i++) {\n if (subKeys[i] === '__proto__') {\n throw new Error(`unsafe key: ${subKeys[i]}`);\n }\n if (!(subKeys[i] in currentObj)) {\n currentObj[subKeys[i]] = create();\n }\n if (!isObject(currentObj[subKeys[i]])) {\n (process.env.NODE_ENV !== 'production') &&\n warn(getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, {\n key: subKeys[i]\n }));\n hasStringValue = true;\n break;\n }\n currentObj = currentObj[subKeys[i]];\n }\n // update last object value, delete old property\n if (!hasStringValue) {\n if (!isMessageAST(currentObj)) {\n currentObj[subKeys[lastIndex]] = obj[key];\n delete obj[key];\n }\n else {\n /**\n * NOTE:\n * if the last object is a message AST and subKeys[lastIndex] has message AST prop key, ignore to copy and key deletion\n */\n if (!AST_NODE_PROPS_KEYS.includes(subKeys[lastIndex])) {\n delete obj[key];\n }\n }\n }\n // recursive process value if value is also a object\n if (!isMessageAST(currentObj)) {\n const target = currentObj[subKeys[lastIndex]];\n if (isObject(target)) {\n handleFlatJson(target);\n }\n }\n }\n }\n return obj;\n}\nfunction getLocaleMessages(locale, options) {\n const { messages, __i18n, messageResolver, flatJson } = options;\n // prettier-ignore\n const ret = (isPlainObject(messages)\n ? messages\n : isArray(__i18n)\n ? create()\n : { [locale]: create() });\n // merge locale messages of i18n custom block\n if (isArray(__i18n)) {\n __i18n.forEach(custom => {\n if ('locale' in custom && 'resource' in custom) {\n const { locale, resource } = custom;\n if (locale) {\n ret[locale] = ret[locale] || create();\n deepCopy(resource, ret[locale]);\n }\n else {\n deepCopy(resource, ret);\n }\n }\n else {\n isString(custom) && deepCopy(JSON.parse(custom), ret);\n }\n });\n }\n // handle messages for flat json\n if (messageResolver == null && flatJson) {\n for (const key in ret) {\n if (hasOwn(ret, key)) {\n handleFlatJson(ret[key]);\n }\n }\n }\n return ret;\n}\nfunction getComponentOptions(instance) {\n return instance.type;\n}\nfunction adjustI18nResources(gl, options, componentOptions) {\n // prettier-ignore\n let messages = isObject(options.messages)\n ? options.messages\n : create();\n if ('__i18nGlobal' in componentOptions) {\n messages = getLocaleMessages(gl.locale.value, {\n messages,\n __i18n: componentOptions.__i18nGlobal\n });\n }\n // merge locale messages\n const locales = Object.keys(messages);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeLocaleMessage(locale, messages[locale]);\n });\n }\n {\n // merge datetime formats\n if (isObject(options.datetimeFormats)) {\n const locales = Object.keys(options.datetimeFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeDateTimeFormat(locale, options.datetimeFormats[locale]);\n });\n }\n }\n // merge number formats\n if (isObject(options.numberFormats)) {\n const locales = Object.keys(options.numberFormats);\n if (locales.length) {\n locales.forEach(locale => {\n gl.mergeNumberFormat(locale, options.numberFormats[locale]);\n });\n }\n }\n }\n}\nfunction createTextNode(key) {\n return createVNode(Text, null, key, 0);\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// extend VNode interface\nconst DEVTOOLS_META = '__INTLIFY_META__';\nconst NOOP_RETURN_ARRAY = () => [];\nconst NOOP_RETURN_FALSE = () => false;\nlet composerID = 0;\nfunction defineCoreMissingHandler(missing) {\n return ((ctx, locale, key, type) => {\n return missing(locale, key, getCurrentInstance() || undefined, type);\n });\n}\n// for Intlify DevTools\n/* #__NO_SIDE_EFFECTS__ */\nconst getMetaInfo = () => {\n const instance = getCurrentInstance();\n let meta = null;\n return instance && (meta = getComponentOptions(instance)[DEVTOOLS_META])\n ? { [DEVTOOLS_META]: meta }\n : null;\n};\n/**\n * Create composer interface factory\n *\n * @internal\n */\nfunction createComposer(options = {}) {\n const { __root, __injectWithOption } = options;\n const _isGlobal = __root === undefined;\n const flatJson = options.flatJson;\n const _ref = inBrowser ? ref : shallowRef;\n let _inheritLocale = isBoolean(options.inheritLocale)\n ? options.inheritLocale\n : true;\n const _locale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.locale.value\n : isString(options.locale)\n ? options.locale\n : DEFAULT_LOCALE);\n const _fallbackLocale = _ref(\n // prettier-ignore\n __root && _inheritLocale\n ? __root.fallbackLocale.value\n : isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : _locale.value);\n const _messages = _ref(getLocaleMessages(_locale.value, options));\n // prettier-ignore\n const _datetimeFormats = _ref(isPlainObject(options.datetimeFormats)\n ? options.datetimeFormats\n : { [_locale.value]: {} })\n ;\n // prettier-ignore\n const _numberFormats = _ref(isPlainObject(options.numberFormats)\n ? options.numberFormats\n : { [_locale.value]: {} })\n ;\n // warning suppress options\n // prettier-ignore\n let _missingWarn = __root\n ? __root.missingWarn\n : isBoolean(options.missingWarn) || isRegExp(options.missingWarn)\n ? options.missingWarn\n : true;\n // prettier-ignore\n let _fallbackWarn = __root\n ? __root.fallbackWarn\n : isBoolean(options.fallbackWarn) || isRegExp(options.fallbackWarn)\n ? options.fallbackWarn\n : true;\n // prettier-ignore\n let _fallbackRoot = __root\n ? __root.fallbackRoot\n : isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n // configure fall back to root\n let _fallbackFormat = !!options.fallbackFormat;\n // runtime missing\n let _missing = isFunction(options.missing) ? options.missing : null;\n let _runtimeMissing = isFunction(options.missing)\n ? defineCoreMissingHandler(options.missing)\n : null;\n // postTranslation handler\n let _postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : null;\n // prettier-ignore\n let _warnHtmlMessage = __root\n ? __root.warnHtmlMessage\n : isBoolean(options.warnHtmlMessage)\n ? options.warnHtmlMessage\n : true;\n let _escapeParameter = !!options.escapeParameter;\n // custom linked modifiers\n // prettier-ignore\n const _modifiers = __root\n ? __root.modifiers\n : isPlainObject(options.modifiers)\n ? options.modifiers\n : {};\n // pluralRules\n let _pluralRules = options.pluralRules || (__root && __root.pluralRules);\n // runtime context\n // eslint-disable-next-line prefer-const\n let _context;\n const getCoreContext = () => {\n _isGlobal && setFallbackContext(null);\n const ctxOptions = {\n version: VERSION,\n locale: _locale.value,\n fallbackLocale: _fallbackLocale.value,\n messages: _messages.value,\n modifiers: _modifiers,\n pluralRules: _pluralRules,\n missing: _runtimeMissing === null ? undefined : _runtimeMissing,\n missingWarn: _missingWarn,\n fallbackWarn: _fallbackWarn,\n fallbackFormat: _fallbackFormat,\n unresolving: true,\n postTranslation: _postTranslation === null ? undefined : _postTranslation,\n warnHtmlMessage: _warnHtmlMessage,\n escapeParameter: _escapeParameter,\n messageResolver: options.messageResolver,\n messageCompiler: options.messageCompiler,\n __meta: { framework: 'vue' }\n };\n {\n ctxOptions.datetimeFormats = _datetimeFormats.value;\n ctxOptions.numberFormats = _numberFormats.value;\n ctxOptions.__datetimeFormatters = isPlainObject(_context)\n ? _context.__datetimeFormatters\n : undefined;\n ctxOptions.__numberFormatters = isPlainObject(_context)\n ? _context.__numberFormatters\n : undefined;\n }\n if ((process.env.NODE_ENV !== 'production')) {\n ctxOptions.__v_emitter = isPlainObject(_context)\n ? _context.__v_emitter\n : undefined;\n }\n const ctx = createCoreContext(ctxOptions);\n _isGlobal && setFallbackContext(ctx);\n return ctx;\n };\n _context = getCoreContext();\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n // track reactivity\n function trackReactivityValues() {\n return [\n _locale.value,\n _fallbackLocale.value,\n _messages.value,\n _datetimeFormats.value,\n _numberFormats.value\n ]\n ;\n }\n // locale\n const locale = computed({\n get: () => _locale.value,\n set: val => {\n _context.locale = val;\n _locale.value = val;\n }\n });\n // fallbackLocale\n const fallbackLocale = computed({\n get: () => _fallbackLocale.value,\n set: val => {\n _context.fallbackLocale = val;\n _fallbackLocale.value = val;\n updateFallbackLocale(_context, _locale.value, val);\n }\n });\n // messages\n const messages = computed(() => _messages.value);\n // datetimeFormats\n const datetimeFormats = /* #__PURE__*/ computed(() => _datetimeFormats.value);\n // numberFormats\n const numberFormats = /* #__PURE__*/ computed(() => _numberFormats.value);\n // getPostTranslationHandler\n function getPostTranslationHandler() {\n return isFunction(_postTranslation) ? _postTranslation : null;\n }\n // setPostTranslationHandler\n function setPostTranslationHandler(handler) {\n _postTranslation = handler;\n _context.postTranslation = handler;\n }\n // getMissingHandler\n function getMissingHandler() {\n return _missing;\n }\n // setMissingHandler\n function setMissingHandler(handler) {\n if (handler !== null) {\n _runtimeMissing = defineCoreMissingHandler(handler);\n }\n _missing = handler;\n _context.missing = _runtimeMissing;\n }\n function isResolvedTranslateMessage(type, arg) {\n return type !== 'translate' || !arg.resolvedMessage;\n }\n const wrapWithDeps = (fn, argumentParser, warnType, fallbackSuccess, fallbackFail, successCondition) => {\n trackReactivityValues(); // track reactive dependency\n // NOTE: experimental !!\n let ret;\n try {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(getMetaInfo());\n }\n if (!_isGlobal) {\n _context.fallbackContext = __root\n ? getFallbackContext()\n : undefined;\n }\n ret = fn(_context);\n }\n finally {\n if ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n setAdditionalMeta(null);\n }\n if (!_isGlobal) {\n _context.fallbackContext = undefined;\n }\n }\n if ((warnType !== 'translate exists' && // for not `te` (e.g `t`)\n isNumber(ret) &&\n ret === NOT_REOSLVED) ||\n (warnType === 'translate exists' && !ret) // for `te`\n ) {\n const [key, arg2] = argumentParser();\n if ((process.env.NODE_ENV !== 'production') &&\n __root &&\n isString(key) &&\n isResolvedTranslateMessage(warnType, arg2)) {\n if (_fallbackRoot &&\n (isTranslateFallbackWarn(_fallbackWarn, key) ||\n isTranslateMissingWarn(_missingWarn, key))) {\n warn(getWarnMessage(I18nWarnCodes.FALLBACK_TO_ROOT, {\n key,\n type: warnType\n }));\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n const { __v_emitter: emitter } = _context;\n if (emitter && _fallbackRoot) {\n emitter.emit('fallback', {\n type: warnType,\n key,\n to: 'global',\n groupId: `${warnType}:${key}`\n });\n }\n }\n }\n return __root && _fallbackRoot\n ? fallbackSuccess(__root)\n : fallbackFail(key);\n }\n else if (successCondition(ret)) {\n return ret;\n }\n else {\n /* istanbul ignore next */\n throw createI18nError(I18nErrorCodes.UNEXPECTED_RETURN_TYPE);\n }\n };\n // t\n function t(...args) {\n return wrapWithDeps(context => Reflect.apply(translate, null, [context, ...args]), () => parseTranslateArgs(...args), 'translate', root => Reflect.apply(root.t, root, [...args]), key => key, val => isString(val));\n }\n // rt\n function rt(...args) {\n const [arg1, arg2, arg3] = args;\n if (arg3 && !isObject(arg3)) {\n throw createI18nError(I18nErrorCodes.INVALID_ARGUMENT);\n }\n return t(...[arg1, arg2, assign({ resolvedMessage: true }, arg3 || {})]);\n }\n // d\n function d(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => Reflect.apply(root.d, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // n\n function n(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => Reflect.apply(root.n, root, [...args]), () => MISSING_RESOLVE_VALUE, val => isString(val) || isArray(val));\n }\n // for custom processor\n function normalize(values) {\n return values.map(val => isString(val) || isNumber(val) || isBoolean(val)\n ? createTextNode(String(val))\n : val);\n }\n const interpolate = (val) => val;\n const processor = {\n normalize,\n interpolate,\n type: 'vnode'\n };\n // translateVNode, using for `i18n-t` component\n function translateVNode(...args) {\n return wrapWithDeps(context => {\n let ret;\n const _context = context;\n try {\n _context.processor = processor;\n ret = Reflect.apply(translate, null, [_context, ...args]);\n }\n finally {\n _context.processor = null;\n }\n return ret;\n }, () => parseTranslateArgs(...args), 'translate', root => root[TranslateVNodeSymbol](...args), key => [createTextNode(key)], val => isArray(val));\n }\n // numberParts, using for `i18n-n` component\n function numberParts(...args) {\n return wrapWithDeps(context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => root[NumberPartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n // datetimeParts, using for `i18n-d` component\n function datetimeParts(...args) {\n return wrapWithDeps(context => Reflect.apply(datetime, null, [context, ...args]), () => parseDateTimeArgs(...args), 'datetime format', root => root[DatetimePartsSymbol](...args), NOOP_RETURN_ARRAY, val => isString(val) || isArray(val));\n }\n function setPluralRules(rules) {\n _pluralRules = rules;\n _context.pluralRules = _pluralRules;\n }\n // te\n function te(key, locale) {\n return wrapWithDeps(() => {\n if (!key) {\n return false;\n }\n const targetLocale = isString(locale) ? locale : _locale.value;\n const message = getLocaleMessage(targetLocale);\n const resolved = _context.messageResolver(message, key);\n return (isMessageAST(resolved) ||\n isMessageFunction(resolved) ||\n isString(resolved));\n }, () => [key], 'translate exists', root => {\n return Reflect.apply(root.te, root, [key, locale]);\n }, NOOP_RETURN_FALSE, val => isBoolean(val));\n }\n function resolveMessages(key) {\n let messages = null;\n const locales = fallbackWithLocaleChain(_context, _fallbackLocale.value, _locale.value);\n for (let i = 0; i < locales.length; i++) {\n const targetLocaleMessages = _messages.value[locales[i]] || {};\n const messageValue = _context.messageResolver(targetLocaleMessages, key);\n if (messageValue != null) {\n messages = messageValue;\n break;\n }\n }\n return messages;\n }\n // tm\n function tm(key) {\n const messages = resolveMessages(key);\n // prettier-ignore\n return messages != null\n ? messages\n : __root\n ? __root.tm(key) || {}\n : {};\n }\n // getLocaleMessage\n function getLocaleMessage(locale) {\n return (_messages.value[locale] || {});\n }\n // setLocaleMessage\n function setLocaleMessage(locale, message) {\n if (flatJson) {\n const _message = { [locale]: message };\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n message = _message[locale];\n }\n _messages.value[locale] = message;\n _context.messages = _messages.value;\n }\n // mergeLocaleMessage\n function mergeLocaleMessage(locale, message) {\n _messages.value[locale] = _messages.value[locale] || {};\n const _message = { [locale]: message };\n if (flatJson) {\n for (const key in _message) {\n if (hasOwn(_message, key)) {\n handleFlatJson(_message[key]);\n }\n }\n }\n message = _message[locale];\n deepCopy(message, _messages.value[locale]);\n _context.messages = _messages.value;\n }\n // getDateTimeFormat\n function getDateTimeFormat(locale) {\n return _datetimeFormats.value[locale] || {};\n }\n // setDateTimeFormat\n function setDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = format;\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // mergeDateTimeFormat\n function mergeDateTimeFormat(locale, format) {\n _datetimeFormats.value[locale] = assign(_datetimeFormats.value[locale] || {}, format);\n _context.datetimeFormats = _datetimeFormats.value;\n clearDateTimeFormat(_context, locale, format);\n }\n // getNumberFormat\n function getNumberFormat(locale) {\n return _numberFormats.value[locale] || {};\n }\n // setNumberFormat\n function setNumberFormat(locale, format) {\n _numberFormats.value[locale] = format;\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // mergeNumberFormat\n function mergeNumberFormat(locale, format) {\n _numberFormats.value[locale] = assign(_numberFormats.value[locale] || {}, format);\n _context.numberFormats = _numberFormats.value;\n clearNumberFormat(_context, locale, format);\n }\n // for debug\n composerID++;\n // watch root locale & fallbackLocale\n if (__root && inBrowser) {\n watch(__root.locale, (val) => {\n if (_inheritLocale) {\n _locale.value = val;\n _context.locale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n watch(__root.fallbackLocale, (val) => {\n if (_inheritLocale) {\n _fallbackLocale.value = val;\n _context.fallbackLocale = val;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n });\n }\n // define basic composition API!\n const composer = {\n id: composerID,\n locale,\n fallbackLocale,\n get inheritLocale() {\n return _inheritLocale;\n },\n set inheritLocale(val) {\n _inheritLocale = val;\n if (val && __root) {\n _locale.value = __root.locale.value;\n _fallbackLocale.value = __root.fallbackLocale.value;\n updateFallbackLocale(_context, _locale.value, _fallbackLocale.value);\n }\n },\n get availableLocales() {\n return Object.keys(_messages.value).sort();\n },\n messages,\n get modifiers() {\n return _modifiers;\n },\n get pluralRules() {\n return _pluralRules || {};\n },\n get isGlobal() {\n return _isGlobal;\n },\n get missingWarn() {\n return _missingWarn;\n },\n set missingWarn(val) {\n _missingWarn = val;\n _context.missingWarn = _missingWarn;\n },\n get fallbackWarn() {\n return _fallbackWarn;\n },\n set fallbackWarn(val) {\n _fallbackWarn = val;\n _context.fallbackWarn = _fallbackWarn;\n },\n get fallbackRoot() {\n return _fallbackRoot;\n },\n set fallbackRoot(val) {\n _fallbackRoot = val;\n },\n get fallbackFormat() {\n return _fallbackFormat;\n },\n set fallbackFormat(val) {\n _fallbackFormat = val;\n _context.fallbackFormat = _fallbackFormat;\n },\n get warnHtmlMessage() {\n return _warnHtmlMessage;\n },\n set warnHtmlMessage(val) {\n _warnHtmlMessage = val;\n _context.warnHtmlMessage = val;\n },\n get escapeParameter() {\n return _escapeParameter;\n },\n set escapeParameter(val) {\n _escapeParameter = val;\n _context.escapeParameter = val;\n },\n t,\n getLocaleMessage,\n setLocaleMessage,\n mergeLocaleMessage,\n getPostTranslationHandler,\n setPostTranslationHandler,\n getMissingHandler,\n setMissingHandler,\n [SetPluralRulesSymbol]: setPluralRules\n };\n {\n composer.datetimeFormats = datetimeFormats;\n composer.numberFormats = numberFormats;\n composer.rt = rt;\n composer.te = te;\n composer.tm = tm;\n composer.d = d;\n composer.n = n;\n composer.getDateTimeFormat = getDateTimeFormat;\n composer.setDateTimeFormat = setDateTimeFormat;\n composer.mergeDateTimeFormat = mergeDateTimeFormat;\n composer.getNumberFormat = getNumberFormat;\n composer.setNumberFormat = setNumberFormat;\n composer.mergeNumberFormat = mergeNumberFormat;\n composer[InejctWithOptionSymbol] = __injectWithOption;\n composer[TranslateVNodeSymbol] = translateVNode;\n composer[DatetimePartsSymbol] = datetimeParts;\n composer[NumberPartsSymbol] = numberParts;\n }\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n composer[EnableEmitter] = (emitter) => {\n _context.__v_emitter = emitter;\n };\n composer[DisableEmitter] = () => {\n _context.__v_emitter = undefined;\n };\n }\n return composer;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst VUE_I18N_COMPONENT_TYPES = 'vue-i18n: composer properties';\nconst VueDevToolsLabels = {\n 'vue-devtools-plugin-vue-i18n': 'Vue I18n DevTools',\n 'vue-i18n-resource-inspector': 'Vue I18n DevTools',\n 'vue-i18n-timeline': 'Vue I18n'\n};\nconst VueDevToolsPlaceholders = {\n 'vue-i18n-resource-inspector': 'Search for scopes ...'\n};\nconst VueDevToolsTimelineColors = {\n 'vue-i18n-timeline': 0xffcd19\n};\nlet devtoolsApi;\nasync function enableDevTools(app, i18n) {\n return new Promise((resolve, reject) => {\n try {\n setupDevtoolsPlugin({\n id: 'vue-devtools-plugin-vue-i18n',\n label: VueDevToolsLabels['vue-devtools-plugin-vue-i18n'],\n packageName: 'vue-i18n',\n homepage: 'https://vue-i18n.intlify.dev',\n logo: 'https://vue-i18n.intlify.dev/vue-i18n-devtools-logo.png',\n componentStateTypes: [VUE_I18N_COMPONENT_TYPES],\n app: app // eslint-disable-line @typescript-eslint/no-explicit-any\n }, api => {\n devtoolsApi = api;\n api.on.visitComponentTree(({ componentInstance, treeNode }) => {\n updateComponentTreeTags(componentInstance, treeNode, i18n);\n });\n api.on.inspectComponent(({ componentInstance, instanceData }) => {\n if (componentInstance.vnode.el &&\n componentInstance.vnode.el.__VUE_I18N__ &&\n instanceData) {\n if (i18n.mode === 'legacy') {\n // ignore global scope on legacy mode\n if (componentInstance.vnode.el.__VUE_I18N__ !==\n i18n.global.__composer) {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n else {\n inspectComposer(instanceData, componentInstance.vnode.el.__VUE_I18N__);\n }\n }\n });\n api.addInspector({\n id: 'vue-i18n-resource-inspector',\n label: VueDevToolsLabels['vue-i18n-resource-inspector'],\n icon: 'language',\n treeFilterPlaceholder: VueDevToolsPlaceholders['vue-i18n-resource-inspector']\n });\n api.on.getInspectorTree(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n registerScope(payload, i18n);\n }\n });\n const roots = new Map();\n api.on.getInspectorState(async (payload) => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n api.unhighlightElement();\n inspectScope(payload, i18n);\n if (payload.nodeId === 'global') {\n if (!roots.has(payload.app)) {\n const [root] = await api.getComponentInstances(payload.app);\n roots.set(payload.app, root);\n }\n api.highlightElement(roots.get(payload.app));\n }\n else {\n const instance = getComponentInstance(payload.nodeId, i18n);\n instance && api.highlightElement(instance);\n }\n }\n });\n api.on.editInspectorState(payload => {\n if (payload.app === app &&\n payload.inspectorId === 'vue-i18n-resource-inspector') {\n editScope(payload, i18n);\n }\n });\n api.addTimelineLayer({\n id: 'vue-i18n-timeline',\n label: VueDevToolsLabels['vue-i18n-timeline'],\n color: VueDevToolsTimelineColors['vue-i18n-timeline']\n });\n resolve(true);\n });\n }\n catch (e) {\n console.error(e);\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n reject(false);\n }\n });\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getI18nScopeLable(instance) {\n return (instance.type.name ||\n instance.type.displayName ||\n instance.type.__file ||\n 'Anonymous');\n}\nfunction updateComponentTreeTags(instance, // eslint-disable-line @typescript-eslint/no-explicit-any\ntreeNode, i18n) {\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n if (instance && instance.vnode.el && instance.vnode.el.__VUE_I18N__) {\n // add custom tags local scope only\n if (instance.vnode.el.__VUE_I18N__ !== global) {\n const tag = {\n label: `i18n (${getI18nScopeLable(instance)} Scope)`,\n textColor: 0x000000,\n backgroundColor: 0xffcd19\n };\n treeNode.tags.push(tag);\n }\n }\n}\nfunction inspectComposer(instanceData, composer) {\n const type = VUE_I18N_COMPONENT_TYPES;\n instanceData.state.push({\n type,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n });\n instanceData.state.push({\n type,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n });\n instanceData.state.push({\n type,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n });\n instanceData.state.push({\n type,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n });\n instanceData.state.push({\n type,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n });\n {\n instanceData.state.push({\n type,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n });\n instanceData.state.push({\n type,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n });\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getLocaleMessageValue(messages) {\n const value = {};\n Object.keys(messages).forEach((key) => {\n const v = messages[key];\n if (isFunction(v) && 'source' in v) {\n value[key] = getMessageFunctionDetails(v);\n }\n else if (isMessageAST(v) && v.loc && v.loc.source) {\n value[key] = v.loc.source;\n }\n else if (isObject(v)) {\n value[key] = getLocaleMessageValue(v);\n }\n else {\n value[key] = v;\n }\n });\n return value;\n}\nconst ESC = {\n '<': '<',\n '>': '>',\n '\"': '"',\n '&': '&'\n};\nfunction escape(s) {\n return s.replace(/[<>\"&]/g, escapeChar);\n}\nfunction escapeChar(a) {\n return ESC[a] || a;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getMessageFunctionDetails(func) {\n const argString = func.source ? `(\"${escape(func.source)}\")` : `(?)`;\n return {\n _custom: {\n type: 'function',\n display: `ƒ ${argString}`\n }\n };\n}\nfunction registerScope(payload, i18n) {\n payload.rootNodes.push({\n id: 'global',\n label: 'Global Scope'\n });\n // prettier-ignore\n const global = i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n for (const [keyInstance, instance] of i18n.__instances) {\n // prettier-ignore\n const composer = i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n if (global === composer) {\n continue;\n }\n payload.rootNodes.push({\n id: composer.id.toString(),\n label: `${getI18nScopeLable(keyInstance)} Scope`\n });\n }\n}\nfunction getComponentInstance(nodeId, i18n) {\n let instance = null;\n if (nodeId !== 'global') {\n for (const [component, composer] of i18n.__instances.entries()) {\n if (composer.id.toString() === nodeId) {\n instance = component;\n break;\n }\n }\n }\n return instance;\n}\nfunction getComposer$2(nodeId, i18n) {\n if (nodeId === 'global') {\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n }\n else {\n const instance = Array.from(i18n.__instances.values()).find(item => item.id.toString() === nodeId);\n if (instance) {\n return i18n.mode === 'composition'\n ? instance\n : instance.__composer;\n }\n else {\n return null;\n }\n }\n}\nfunction inspectScope(payload, i18n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n // TODO:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n payload.state = makeScopeInspectState(composer);\n }\n return null;\n}\nfunction makeScopeInspectState(composer) {\n const state = {};\n const localeType = 'Locale related info';\n const localeStates = [\n {\n type: localeType,\n key: 'locale',\n editable: true,\n value: composer.locale.value\n },\n {\n type: localeType,\n key: 'fallbackLocale',\n editable: true,\n value: composer.fallbackLocale.value\n },\n {\n type: localeType,\n key: 'availableLocales',\n editable: false,\n value: composer.availableLocales\n },\n {\n type: localeType,\n key: 'inheritLocale',\n editable: true,\n value: composer.inheritLocale\n }\n ];\n state[localeType] = localeStates;\n const localeMessagesType = 'Locale messages info';\n const localeMessagesStates = [\n {\n type: localeMessagesType,\n key: 'messages',\n editable: false,\n value: getLocaleMessageValue(composer.messages.value)\n }\n ];\n state[localeMessagesType] = localeMessagesStates;\n {\n const datetimeFormatsType = 'Datetime formats info';\n const datetimeFormatsStates = [\n {\n type: datetimeFormatsType,\n key: 'datetimeFormats',\n editable: false,\n value: composer.datetimeFormats.value\n }\n ];\n state[datetimeFormatsType] = datetimeFormatsStates;\n const numberFormatsType = 'Datetime formats info';\n const numberFormatsStates = [\n {\n type: numberFormatsType,\n key: 'numberFormats',\n editable: false,\n value: composer.numberFormats.value\n }\n ];\n state[numberFormatsType] = numberFormatsStates;\n }\n return state;\n}\nfunction addTimelineEvent(event, payload) {\n if (devtoolsApi) {\n let groupId;\n if (payload && 'groupId' in payload) {\n groupId = payload.groupId;\n delete payload.groupId;\n }\n devtoolsApi.addTimelineEvent({\n layerId: 'vue-i18n-timeline',\n event: {\n title: event,\n groupId,\n time: Date.now(),\n meta: {},\n data: payload || {},\n logType: event === 'compile-error'\n ? 'error'\n : event === 'fallback' || event === 'missing'\n ? 'warning'\n : 'default'\n }\n });\n }\n}\nfunction editScope(payload, i18n) {\n const composer = getComposer$2(payload.nodeId, i18n);\n if (composer) {\n const [field] = payload.path;\n if (field === 'locale' && isString(payload.state.value)) {\n composer.locale.value = payload.state.value;\n }\n else if (field === 'fallbackLocale' &&\n (isString(payload.state.value) ||\n isArray(payload.state.value) ||\n isObject(payload.state.value))) {\n composer.fallbackLocale.value = payload.state.value;\n }\n else if (field === 'inheritLocale' && isBoolean(payload.state.value)) {\n composer.inheritLocale = payload.state.value;\n }\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Convert to I18n Composer Options from VueI18n Options\n *\n * @internal\n */\nfunction convertComposerOptions(options) {\n const locale = isString(options.locale) ? options.locale : DEFAULT_LOCALE;\n const fallbackLocale = isString(options.fallbackLocale) ||\n isArray(options.fallbackLocale) ||\n isPlainObject(options.fallbackLocale) ||\n options.fallbackLocale === false\n ? options.fallbackLocale\n : locale;\n const missing = isFunction(options.missing) ? options.missing : undefined;\n const missingWarn = isBoolean(options.silentTranslationWarn) ||\n isRegExp(options.silentTranslationWarn)\n ? !options.silentTranslationWarn\n : true;\n const fallbackWarn = isBoolean(options.silentFallbackWarn) ||\n isRegExp(options.silentFallbackWarn)\n ? !options.silentFallbackWarn\n : true;\n const fallbackRoot = isBoolean(options.fallbackRoot)\n ? options.fallbackRoot\n : true;\n const fallbackFormat = !!options.formatFallbackMessages;\n const modifiers = isPlainObject(options.modifiers) ? options.modifiers : {};\n const pluralizationRules = options.pluralizationRules;\n const postTranslation = isFunction(options.postTranslation)\n ? options.postTranslation\n : undefined;\n const warnHtmlMessage = isString(options.warnHtmlInMessage)\n ? options.warnHtmlInMessage !== 'off'\n : true;\n const escapeParameter = !!options.escapeParameterHtml;\n const inheritLocale = isBoolean(options.sync) ? options.sync : true;\n let messages = options.messages;\n if (isPlainObject(options.sharedMessages)) {\n const sharedMessages = options.sharedMessages;\n const locales = Object.keys(sharedMessages);\n messages = locales.reduce((messages, locale) => {\n const message = messages[locale] || (messages[locale] = {});\n assign(message, sharedMessages[locale]);\n return messages;\n }, (messages || {}));\n }\n const { __i18n, __root, __injectWithOption } = options;\n const datetimeFormats = options.datetimeFormats;\n const numberFormats = options.numberFormats;\n const flatJson = options.flatJson;\n return {\n locale,\n fallbackLocale,\n messages,\n flatJson,\n datetimeFormats,\n numberFormats,\n missing,\n missingWarn,\n fallbackWarn,\n fallbackRoot,\n fallbackFormat,\n modifiers,\n pluralRules: pluralizationRules,\n postTranslation,\n warnHtmlMessage,\n escapeParameter,\n messageResolver: options.messageResolver,\n inheritLocale,\n __i18n,\n __root,\n __injectWithOption\n };\n}\n/**\n * create VueI18n interface factory\n *\n * @internal\n *\n * @deprecated will be removed at vue-i18n v12\n */\nfunction createVueI18n(options = {}) {\n const composer = createComposer(convertComposerOptions(options));\n const { __extender } = options;\n // defines VueI18n\n const vueI18n = {\n // id\n id: composer.id,\n // locale\n get locale() {\n return composer.locale.value;\n },\n set locale(val) {\n composer.locale.value = val;\n },\n // fallbackLocale\n get fallbackLocale() {\n return composer.fallbackLocale.value;\n },\n set fallbackLocale(val) {\n composer.fallbackLocale.value = val;\n },\n // messages\n get messages() {\n return composer.messages.value;\n },\n // datetimeFormats\n get datetimeFormats() {\n return composer.datetimeFormats.value;\n },\n // numberFormats\n get numberFormats() {\n return composer.numberFormats.value;\n },\n // availableLocales\n get availableLocales() {\n return composer.availableLocales;\n },\n // missing\n get missing() {\n return composer.getMissingHandler();\n },\n set missing(handler) {\n composer.setMissingHandler(handler);\n },\n // silentTranslationWarn\n get silentTranslationWarn() {\n return isBoolean(composer.missingWarn)\n ? !composer.missingWarn\n : composer.missingWarn;\n },\n set silentTranslationWarn(val) {\n composer.missingWarn = isBoolean(val) ? !val : val;\n },\n // silentFallbackWarn\n get silentFallbackWarn() {\n return isBoolean(composer.fallbackWarn)\n ? !composer.fallbackWarn\n : composer.fallbackWarn;\n },\n set silentFallbackWarn(val) {\n composer.fallbackWarn = isBoolean(val) ? !val : val;\n },\n // modifiers\n get modifiers() {\n return composer.modifiers;\n },\n // formatFallbackMessages\n get formatFallbackMessages() {\n return composer.fallbackFormat;\n },\n set formatFallbackMessages(val) {\n composer.fallbackFormat = val;\n },\n // postTranslation\n get postTranslation() {\n return composer.getPostTranslationHandler();\n },\n set postTranslation(handler) {\n composer.setPostTranslationHandler(handler);\n },\n // sync\n get sync() {\n return composer.inheritLocale;\n },\n set sync(val) {\n composer.inheritLocale = val;\n },\n // warnInHtmlMessage\n get warnHtmlInMessage() {\n return composer.warnHtmlMessage ? 'warn' : 'off';\n },\n set warnHtmlInMessage(val) {\n composer.warnHtmlMessage = val !== 'off';\n },\n // escapeParameterHtml\n get escapeParameterHtml() {\n return composer.escapeParameter;\n },\n set escapeParameterHtml(val) {\n composer.escapeParameter = val;\n },\n // pluralizationRules\n get pluralizationRules() {\n return composer.pluralRules || {};\n },\n // for internal\n __composer: composer,\n // t\n t(...args) {\n return Reflect.apply(composer.t, composer, [...args]);\n },\n // rt\n rt(...args) {\n return Reflect.apply(composer.rt, composer, [...args]);\n },\n // te\n te(key, locale) {\n return composer.te(key, locale);\n },\n // tm\n tm(key) {\n return composer.tm(key);\n },\n // getLocaleMessage\n getLocaleMessage(locale) {\n return composer.getLocaleMessage(locale);\n },\n // setLocaleMessage\n setLocaleMessage(locale, message) {\n composer.setLocaleMessage(locale, message);\n },\n // mergeLocaleMessage\n mergeLocaleMessage(locale, message) {\n composer.mergeLocaleMessage(locale, message);\n },\n // d\n d(...args) {\n return Reflect.apply(composer.d, composer, [...args]);\n },\n // getDateTimeFormat\n getDateTimeFormat(locale) {\n return composer.getDateTimeFormat(locale);\n },\n // setDateTimeFormat\n setDateTimeFormat(locale, format) {\n composer.setDateTimeFormat(locale, format);\n },\n // mergeDateTimeFormat\n mergeDateTimeFormat(locale, format) {\n composer.mergeDateTimeFormat(locale, format);\n },\n // n\n n(...args) {\n return Reflect.apply(composer.n, composer, [...args]);\n },\n // getNumberFormat\n getNumberFormat(locale) {\n return composer.getNumberFormat(locale);\n },\n // setNumberFormat\n setNumberFormat(locale, format) {\n composer.setNumberFormat(locale, format);\n },\n // mergeNumberFormat\n mergeNumberFormat(locale, format) {\n composer.mergeNumberFormat(locale, format);\n }\n };\n vueI18n.__extender = __extender;\n // for vue-devtools timeline event\n if ((process.env.NODE_ENV !== 'production')) {\n vueI18n.__enableEmitter = (emitter) => {\n const __composer = composer;\n __composer[EnableEmitter] && __composer[EnableEmitter](emitter);\n };\n vueI18n.__disableEmitter = () => {\n const __composer = composer;\n __composer[DisableEmitter] && __composer[DisableEmitter]();\n };\n }\n return vueI18n;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Supports compatibility for legacy vue-i18n APIs\n * This mixin is used when we use vue-i18n@v9.x or later\n */\nfunction defineMixin(vuei18n, composer, i18n) {\n return {\n beforeCreate() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const options = this.$options;\n if (options.i18n) {\n const optionsI18n = options.i18n;\n if (options.__i18n) {\n optionsI18n.__i18n = options.__i18n;\n }\n optionsI18n.__root = composer;\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, optionsI18n);\n }\n else {\n optionsI18n.__injectWithOption = true;\n optionsI18n.__extender = i18n.__vueI18nExtend;\n // atttach local VueI18n instance\n this.$i18n = createVueI18n(optionsI18n);\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else if (options.__i18n) {\n if (this === this.$root) {\n // merge option and gttach global\n this.$i18n = mergeToGlobal(vuei18n, options);\n }\n else {\n // atttach local VueI18n instance\n this.$i18n = createVueI18n({\n __i18n: options.__i18n,\n __injectWithOption: true,\n __extender: i18n.__vueI18nExtend,\n __root: composer\n });\n // extend VueI18n instance\n const _vueI18n = this.$i18n;\n if (_vueI18n.__extender) {\n _vueI18n.__disposer = _vueI18n.__extender(this.$i18n);\n }\n }\n }\n else {\n // attach global VueI18n instance\n this.$i18n = vuei18n;\n }\n if (options.__i18nGlobal) {\n adjustI18nResources(composer, options, options);\n }\n // defines vue-i18n legacy APIs\n this.$t = (...args) => this.$i18n.t(...args);\n this.$rt = (...args) => this.$i18n.rt(...args);\n this.$te = (key, locale) => this.$i18n.te(key, locale);\n this.$d = (...args) => this.$i18n.d(...args);\n this.$n = (...args) => this.$i18n.n(...args);\n this.$tm = (key) => this.$i18n.tm(key);\n i18n.__setInstance(instance, this.$i18n);\n },\n mounted() {\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$i18n) {\n const _vueI18n = this.$i18n;\n this.$el.__VUE_I18N__ = _vueI18n.__composer;\n const emitter = (this.__v_emitter =\n createEmitter());\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n emitter.on('*', addTimelineEvent);\n }\n },\n unmounted() {\n const instance = getCurrentInstance();\n /* istanbul ignore if */\n if (!instance) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const _vueI18n = this.$i18n;\n /* istanbul ignore if */\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n this.$el &&\n this.$el.__VUE_I18N__) {\n if (this.__v_emitter) {\n this.__v_emitter.off('*', addTimelineEvent);\n delete this.__v_emitter;\n }\n if (this.$i18n) {\n _vueI18n.__disableEmitter && _vueI18n.__disableEmitter();\n delete this.$el.__VUE_I18N__;\n }\n }\n delete this.$t;\n delete this.$rt;\n delete this.$te;\n delete this.$d;\n delete this.$n;\n delete this.$tm;\n if (_vueI18n.__disposer) {\n _vueI18n.__disposer();\n delete _vueI18n.__disposer;\n delete _vueI18n.__extender;\n }\n i18n.__deleteInstance(instance);\n delete this.$i18n;\n }\n };\n}\nfunction mergeToGlobal(g, options) {\n g.locale = options.locale || g.locale;\n g.fallbackLocale = options.fallbackLocale || g.fallbackLocale;\n g.missing = options.missing || g.missing;\n g.silentTranslationWarn =\n options.silentTranslationWarn || g.silentFallbackWarn;\n g.silentFallbackWarn = options.silentFallbackWarn || g.silentFallbackWarn;\n g.formatFallbackMessages =\n options.formatFallbackMessages || g.formatFallbackMessages;\n g.postTranslation = options.postTranslation || g.postTranslation;\n g.warnHtmlInMessage = options.warnHtmlInMessage || g.warnHtmlInMessage;\n g.escapeParameterHtml = options.escapeParameterHtml || g.escapeParameterHtml;\n g.sync = options.sync || g.sync;\n g.__composer[SetPluralRulesSymbol](options.pluralizationRules || g.pluralizationRules);\n const messages = getLocaleMessages(g.locale, {\n messages: options.messages,\n __i18n: options.__i18n\n });\n Object.keys(messages).forEach(locale => g.mergeLocaleMessage(locale, messages[locale]));\n if (options.datetimeFormats) {\n Object.keys(options.datetimeFormats).forEach(locale => g.mergeDateTimeFormat(locale, options.datetimeFormats[locale]));\n }\n if (options.numberFormats) {\n Object.keys(options.numberFormats).forEach(locale => g.mergeNumberFormat(locale, options.numberFormats[locale]));\n }\n return g;\n}\n\nconst baseFormatProps = {\n tag: {\n type: [String, Object]\n },\n locale: {\n type: String\n },\n scope: {\n type: String,\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n validator: (val /* ComponentI18nScope */) => val === 'parent' || val === 'global',\n default: 'parent' /* ComponentI18nScope */\n },\n i18n: {\n type: Object\n }\n};\n\nfunction getInterpolateArg(\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n{ slots }, // SetupContext,\nkeys) {\n if (keys.length === 1 && keys[0] === 'default') {\n // default slot with list\n const ret = slots.default ? slots.default() : [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return ret.reduce((slot, current) => {\n return [\n ...slot,\n // prettier-ignore\n ...(current.type === Fragment ? current.children : [current])\n ];\n }, []);\n }\n else {\n // named slots\n return keys.reduce((arg, key) => {\n const slot = slots[key];\n if (slot) {\n arg[key] = slot();\n }\n return arg;\n }, create());\n }\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getFragmentableTag() {\n return Fragment;\n}\n\nconst TranslationImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-t',\n props: assign({\n keypath: {\n type: String,\n required: true\n },\n plural: {\n type: [Number, String],\n validator: (val) => isNumber(val) || !isNaN(val)\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const { slots, attrs } = context;\n // NOTE: avoid https://github.com/microsoft/rushstack/issues/1050\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return () => {\n const keys = Object.keys(slots).filter(key => key[0] !== '_');\n const options = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (props.plural !== undefined) {\n options.plural = isString(props.plural) ? +props.plural : props.plural;\n }\n const arg = getInterpolateArg(context, keys);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const children = i18n[TranslateVNodeSymbol](props.keypath, arg, options);\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Translation Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [TranslationProps](component#translationprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Component Interpolation](../guide/advanced/component)\n *\n * @example\n * ```html\n *
\n * \n * \n * {{ $t('tos') }}\n * \n * \n *
\n * ```\n * ```js\n * import { createApp } from 'vue'\n * import { createI18n } from 'vue-i18n'\n *\n * const messages = {\n * en: {\n * tos: 'Term of Service',\n * term: 'I accept xxx {0}.'\n * },\n * ja: {\n * tos: '利用規約',\n * term: '私は xxx の{0}に同意します。'\n * }\n * }\n *\n * const i18n = createI18n({\n * locale: 'en',\n * messages\n * })\n *\n * const app = createApp({\n * data: {\n * url: '/term'\n * }\n * }).use(i18n).mount('#app')\n * ```\n *\n * @VueI18nComponent\n */\nconst Translation = TranslationImpl;\nconst I18nT = Translation;\n\nfunction isVNode(target) {\n return isArray(target) && !isString(target[0]);\n}\nfunction renderFormatter(props, context, slotKeys, partFormatter) {\n const { slots, attrs } = context;\n return () => {\n const options = { part: true };\n let overrides = create();\n if (props.locale) {\n options.locale = props.locale;\n }\n if (isString(props.format)) {\n options.key = props.format;\n }\n else if (isObject(props.format)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (isString(props.format.key)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options.key = props.format.key;\n }\n // Filter out number format options only\n overrides = Object.keys(props.format).reduce((options, prop) => {\n return slotKeys.includes(prop)\n ? assign(create(), options, { [prop]: props.format[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any\n : options;\n }, create());\n }\n const parts = partFormatter(...[props.value, options, overrides]);\n let children = [options.key];\n if (isArray(parts)) {\n children = parts.map((part, index) => {\n const slot = slots[part.type];\n const node = slot\n ? slot({ [part.type]: part.value, index, parts })\n : [part.value];\n if (isVNode(node)) {\n node[0].key = `${part.type}-${index}`;\n }\n return node;\n });\n }\n else if (isString(parts)) {\n children = [parts];\n }\n const assignedAttrs = assign(create(), attrs);\n const tag = isString(props.tag) || isObject(props.tag)\n ? props.tag\n : getFragmentableTag();\n return h(tag, assignedAttrs, children);\n };\n}\n\nconst NumberFormatImpl = /*#__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-n',\n props: assign({\n value: {\n type: Number,\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, NUMBER_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[NumberPartsSymbol](...args));\n }\n});\n/**\n * export the public type for h/tsx inference\n * also to avoid inline import() in generated d.ts files\n */\n/**\n * Number Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/number#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.NumberFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-numberformat)\n *\n * @VueI18nComponent\n */\nconst NumberFormat = NumberFormatImpl;\nconst I18nN = NumberFormat;\n\nfunction getComposer$1(i18n, instance) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n return (i18nInternal.__getInstance(instance) || i18n.global);\n }\n else {\n const vueI18n = i18nInternal.__getInstance(instance);\n return vueI18n != null\n ? vueI18n.__composer\n : i18n.global.__composer;\n }\n}\n/**\n * @deprecated will be removed at vue-i18n v12\n */\nfunction vTDirective(i18n) {\n const _process = (binding) => {\n if ((process.env.NODE_ENV !== 'production')) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE));\n }\n const { instance, value } = binding;\n /* istanbul ignore if */\n if (!instance || !instance.$) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const composer = getComposer$1(i18n, instance.$);\n const parsedValue = parseValue(value);\n return [\n Reflect.apply(composer.t, composer, [...makeParams(parsedValue)]),\n composer\n ];\n };\n const register = (el, binding) => {\n const [textContent, composer] = _process(binding);\n if (inBrowser && i18n.global === composer) {\n // global scope only\n el.__i18nWatcher = watch(composer.locale, () => {\n binding.instance && binding.instance.$forceUpdate();\n });\n }\n el.__composer = composer;\n el.textContent = textContent;\n };\n const unregister = (el) => {\n if (inBrowser && el.__i18nWatcher) {\n el.__i18nWatcher();\n el.__i18nWatcher = undefined;\n delete el.__i18nWatcher;\n }\n if (el.__composer) {\n el.__composer = undefined;\n delete el.__composer;\n }\n };\n const update = (el, { value }) => {\n if (el.__composer) {\n const composer = el.__composer;\n const parsedValue = parseValue(value);\n el.textContent = Reflect.apply(composer.t, composer, [\n ...makeParams(parsedValue)\n ]);\n }\n };\n const getSSRProps = (binding) => {\n const [textContent] = _process(binding);\n return { textContent };\n };\n return {\n created: register,\n unmounted: unregister,\n beforeUpdate: update,\n getSSRProps\n };\n}\nfunction parseValue(value) {\n if (isString(value)) {\n return { path: value };\n }\n else if (isPlainObject(value)) {\n if (!('path' in value)) {\n throw createI18nError(I18nErrorCodes.REQUIRED_VALUE, 'path');\n }\n return value;\n }\n else {\n throw createI18nError(I18nErrorCodes.INVALID_VALUE);\n }\n}\nfunction makeParams(value) {\n const { path, locale, args, choice, plural } = value;\n const options = {};\n const named = args || {};\n if (isString(locale)) {\n options.locale = locale;\n }\n if (isNumber(choice)) {\n options.plural = choice;\n }\n if (isNumber(plural)) {\n options.plural = plural;\n }\n return [path, named, options];\n}\n\nfunction apply(app, i18n, ...options) {\n const pluginOptions = isPlainObject(options[0])\n ? options[0]\n : {};\n const globalInstall = isBoolean(pluginOptions.globalInstall)\n ? pluginOptions.globalInstall\n : true;\n if (globalInstall) {\n [Translation.name, 'I18nT'].forEach(name => app.component(name, Translation));\n [NumberFormat.name, 'I18nN'].forEach(name => app.component(name, NumberFormat));\n [DatetimeFormat.name, 'I18nD'].forEach(name => app.component(name, DatetimeFormat));\n }\n // install directive\n {\n app.directive('t', vTDirective(i18n));\n }\n}\n\n/**\n * Injection key for {@link useI18n}\n *\n * @remarks\n * The global injection key for I18n instances with `useI18n`. this injection key is used in Web Components.\n * Specify the i18n instance created by {@link createI18n} together with `provide` function.\n *\n * @VueI18nGeneral\n */\nconst I18nInjectionKey = \n/* #__PURE__*/ makeSymbol('global-vue-i18n');\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction createI18n(options = {}) {\n // prettier-ignore\n const __legacyMode = __VUE_I18N_LEGACY_API__ && isBoolean(options.legacy)\n ? options.legacy\n : __VUE_I18N_LEGACY_API__;\n if ((process.env.NODE_ENV !== 'production') && __legacyMode) {\n warnOnce(getWarnMessage(I18nWarnCodes.DEPRECATE_LEGACY_MODE));\n }\n // prettier-ignore\n const __globalInjection = isBoolean(options.globalInjection)\n ? options.globalInjection\n : true;\n const __instances = new Map();\n const [globalScope, __global] = createGlobal(options, __legacyMode);\n const symbol = /* #__PURE__*/ makeSymbol((process.env.NODE_ENV !== 'production') ? 'vue-i18n' : '');\n function __getInstance(component) {\n return __instances.get(component) || null;\n }\n function __setInstance(component, instance) {\n __instances.set(component, instance);\n }\n function __deleteInstance(component) {\n __instances.delete(component);\n }\n const i18n = {\n // mode\n get mode() {\n return __VUE_I18N_LEGACY_API__ && __legacyMode\n ? 'legacy'\n : 'composition';\n },\n // install plugin\n async install(app, ...options) {\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n app.__VUE_I18N__ = i18n;\n }\n // setup global provider\n app.__VUE_I18N_SYMBOL__ = symbol;\n app.provide(app.__VUE_I18N_SYMBOL__, i18n);\n // set composer & vuei18n extend hook options from plugin options\n if (isPlainObject(options[0])) {\n const opts = options[0];\n i18n.__composerExtend =\n opts.__composerExtend;\n i18n.__vueI18nExtend =\n opts.__vueI18nExtend;\n }\n // global method and properties injection for Composition API\n let globalReleaseHandler = null;\n if (!__legacyMode && __globalInjection) {\n globalReleaseHandler = injectGlobalFields(app, i18n.global);\n }\n // install built-in components and directive\n if (__VUE_I18N_FULL_INSTALL__) {\n apply(app, i18n, ...options);\n }\n // setup mixin for Legacy API\n if (__VUE_I18N_LEGACY_API__ && __legacyMode) {\n app.mixin(defineMixin(__global, __global.__composer, i18n));\n }\n // release global scope\n const unmountApp = app.unmount;\n app.unmount = () => {\n globalReleaseHandler && globalReleaseHandler();\n i18n.dispose();\n unmountApp();\n };\n // setup vue-devtools plugin\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) && !false) {\n const ret = await enableDevTools(app, i18n);\n if (!ret) {\n throw createI18nError(I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN);\n }\n const emitter = createEmitter();\n if (__legacyMode) {\n const _vueI18n = __global;\n _vueI18n.__enableEmitter && _vueI18n.__enableEmitter(emitter);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = __global;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n }\n emitter.on('*', addTimelineEvent);\n }\n },\n // global accessor\n get global() {\n return __global;\n },\n dispose() {\n globalScope.stop();\n },\n // @internal\n __instances,\n // @internal\n __getInstance,\n // @internal\n __setInstance,\n // @internal\n __deleteInstance\n };\n return i18n;\n}\nfunction useI18n(options = {}) {\n const instance = getCurrentInstance();\n if (instance == null) {\n throw createI18nError(I18nErrorCodes.MUST_BE_CALL_SETUP_TOP);\n }\n if (!instance.isCE &&\n instance.appContext.app != null &&\n !instance.appContext.app.__VUE_I18N_SYMBOL__) {\n throw createI18nError(I18nErrorCodes.NOT_INSTALLED);\n }\n const i18n = getI18nInstance(instance);\n const gl = getGlobalComposer(i18n);\n const componentOptions = getComponentOptions(instance);\n const scope = getScope(options, componentOptions);\n if (scope === 'global') {\n adjustI18nResources(gl, options, componentOptions);\n return gl;\n }\n if (scope === 'parent') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let composer = getComposer(i18n, instance, options.__useComponent);\n if (composer == null) {\n if ((process.env.NODE_ENV !== 'production')) {\n warn(getWarnMessage(I18nWarnCodes.NOT_FOUND_PARENT_SCOPE));\n }\n composer = gl;\n }\n return composer;\n }\n const i18nInternal = i18n;\n let composer = i18nInternal.__getInstance(instance);\n if (composer == null) {\n const composerOptions = assign({}, options);\n if ('__i18n' in componentOptions) {\n composerOptions.__i18n = componentOptions.__i18n;\n }\n if (gl) {\n composerOptions.__root = gl;\n }\n composer = createComposer(composerOptions);\n if (i18nInternal.__composerExtend) {\n composer[DisposeSymbol] =\n i18nInternal.__composerExtend(composer);\n }\n setupLifeCycle(i18nInternal, instance, composer);\n i18nInternal.__setInstance(instance, composer);\n }\n else {\n if ((process.env.NODE_ENV !== 'production') && scope === 'local') {\n warn(getWarnMessage(I18nWarnCodes.DUPLICATE_USE_I18N_CALLING));\n }\n }\n return composer;\n}\nfunction createGlobal(options, legacyMode) {\n const scope = effectScope();\n const obj = __VUE_I18N_LEGACY_API__ && legacyMode\n ? scope.run(() => createVueI18n(options))\n : scope.run(() => createComposer(options));\n if (obj == null) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n return [scope, obj];\n}\nfunction getI18nInstance(instance) {\n const i18n = inject(!instance.isCE\n ? instance.appContext.app.__VUE_I18N_SYMBOL__\n : I18nInjectionKey);\n /* istanbul ignore if */\n if (!i18n) {\n throw createI18nError(!instance.isCE\n ? I18nErrorCodes.UNEXPECTED_ERROR\n : I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE);\n }\n return i18n;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getScope(options, componentOptions) {\n // prettier-ignore\n return isEmptyObject(options)\n ? ('__i18n' in componentOptions)\n ? 'local'\n : 'global'\n : !options.useScope\n ? 'local'\n : options.useScope;\n}\nfunction getGlobalComposer(i18n) {\n // prettier-ignore\n return i18n.mode === 'composition'\n ? i18n.global\n : i18n.global.__composer;\n}\nfunction getComposer(i18n, target, useComponent = false) {\n let composer = null;\n const root = target.root;\n let current = getParentComponentInstance(target, useComponent);\n while (current != null) {\n const i18nInternal = i18n;\n if (i18n.mode === 'composition') {\n composer = i18nInternal.__getInstance(current);\n }\n else {\n if (__VUE_I18N_LEGACY_API__) {\n const vueI18n = i18nInternal.__getInstance(current);\n if (vueI18n != null) {\n composer = vueI18n\n .__composer;\n if (useComponent &&\n composer &&\n !composer[InejctWithOptionSymbol] // eslint-disable-line @typescript-eslint/no-explicit-any\n ) {\n composer = null;\n }\n }\n }\n }\n if (composer != null) {\n break;\n }\n if (root === current) {\n break;\n }\n current = current.parent;\n }\n return composer;\n}\nfunction getParentComponentInstance(target, useComponent = false) {\n if (target == null) {\n return null;\n }\n // if `useComponent: true` will be specified, we get lexical scope owner instance for use-case slots\n return !useComponent\n ? target.parent\n : target.vnode.ctx || target.parent; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\nfunction setupLifeCycle(i18n, target, composer) {\n let emitter = null;\n onMounted(() => {\n // inject composer instance to DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el) {\n target.vnode.el.__VUE_I18N__ = composer;\n emitter = createEmitter();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n _composer[EnableEmitter] && _composer[EnableEmitter](emitter);\n emitter.on('*', addTimelineEvent);\n }\n }, target);\n onUnmounted(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _composer = composer;\n // remove composer instance from DOM for intlify-devtools\n if (((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) &&\n !false &&\n target.vnode.el &&\n target.vnode.el.__VUE_I18N__) {\n emitter && emitter.off('*', addTimelineEvent);\n _composer[DisableEmitter] && _composer[DisableEmitter]();\n delete target.vnode.el.__VUE_I18N__;\n }\n i18n.__deleteInstance(target);\n // dispose extended resources\n const dispose = _composer[DisposeSymbol];\n if (dispose) {\n dispose();\n delete _composer[DisposeSymbol];\n }\n }, target);\n}\nconst globalExportProps = [\n 'locale',\n 'fallbackLocale',\n 'availableLocales'\n];\nconst globalExportMethods = ['t', 'rt', 'd', 'n', 'tm', 'te']\n ;\nfunction injectGlobalFields(app, composer) {\n const i18n = Object.create(null);\n globalExportProps.forEach(prop => {\n const desc = Object.getOwnPropertyDescriptor(composer, prop);\n if (!desc) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n const wrap = isRef(desc.value) // check computed props\n ? {\n get() {\n return desc.value.value;\n },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n set(val) {\n desc.value.value = val;\n }\n }\n : {\n get() {\n return desc.get && desc.get();\n }\n };\n Object.defineProperty(i18n, prop, wrap);\n });\n app.config.globalProperties.$i18n = i18n;\n globalExportMethods.forEach(method => {\n const desc = Object.getOwnPropertyDescriptor(composer, method);\n if (!desc || !desc.value) {\n throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR);\n }\n Object.defineProperty(app.config.globalProperties, `$${method}`, desc);\n });\n const dispose = () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties.$i18n;\n globalExportMethods.forEach(method => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete app.config.globalProperties[`$${method}`];\n });\n };\n return dispose;\n}\n\nconst DatetimeFormatImpl = /* #__PURE__*/ defineComponent({\n /* eslint-disable */\n name: 'i18n-d',\n props: assign({\n value: {\n type: [Number, Date],\n required: true\n },\n format: {\n type: [String, Object]\n }\n }, baseFormatProps),\n /* eslint-enable */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setup(props, context) {\n const i18n = props.i18n ||\n useI18n({\n useScope: props.scope,\n __useComponent: true\n });\n return renderFormatter(props, context, DATETIME_FORMAT_OPTIONS_KEYS, (...args) => \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n i18n[DatetimePartsSymbol](...args));\n }\n});\n/**\n * Datetime Format Component\n *\n * @remarks\n * See the following items for property about details\n *\n * @VueI18nSee [FormattableProps](component#formattableprops)\n * @VueI18nSee [BaseFormatProps](component#baseformatprops)\n * @VueI18nSee [Custom Formatting](../guide/essentials/datetime#custom-formatting)\n *\n * @VueI18nDanger\n * Not supported IE, due to no support `Intl.DateTimeFormat#formatToParts` in [IE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts)\n *\n * If you want to use it, you need to use [polyfill](https://github.com/formatjs/formatjs/tree/main/packages/intl-datetimeformat)\n *\n * @VueI18nComponent\n */\nconst DatetimeFormat = DatetimeFormatImpl;\nconst I18nD = DatetimeFormat;\n\n{\n initFeatureFlags();\n}\n// register message compiler at vue-i18n\nregisterMessageCompiler(compile);\n// register message resolver at vue-i18n\nregisterMessageResolver(resolveValue);\n// register fallback locale at vue-i18n\nregisterLocaleFallbacker(fallbackWithLocaleChain);\n// NOTE: experimental !!\nif ((process.env.NODE_ENV !== 'production') || __INTLIFY_PROD_DEVTOOLS__) {\n const target = getGlobalThis();\n target.__INTLIFY__ = true;\n setDevToolsHook(target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__);\n}\nif ((process.env.NODE_ENV !== 'production')) ;\n\nexport { DatetimeFormat, I18nD, I18nInjectionKey, I18nN, I18nT, NumberFormat, Translation, VERSION, createI18n, useI18n, vTDirective };\n","import { commonMessages } from '@/i18n/messages/common';\nimport { panelMessages } from '@/i18n/messages/panel';\nimport { runtimeMessages } from '@/i18n/messages/runtime';\nimport type { AppLocale, MessageModule } from '@/i18n/messages/types';\nimport { createI18n, type LocaleMessageValue, useI18n } from 'vue-i18n';\n\nconst allMessages = {\n ...commonMessages,\n ...panelMessages,\n ...runtimeMessages,\n};\n\nexport type MessageKey = keyof typeof allMessages;\nexport type TranslationParams = Record;\n\nexport function resolveMvuLocale(locale: string): AppLocale {\n return locale.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';\n}\n\nfunction getHostLocale(): string {\n try {\n return SillyTavern.getCurrentLocale();\n } catch {\n return document.documentElement.lang || navigator.language || 'en';\n }\n}\n\ntype LocaleMessages = Record;\n\nfunction buildLocaleMessages(locale: AppLocale): LocaleMessages {\n // 消息源使用便于类型检查的扁平键,这里再展开成 vue-i18n 所需的嵌套对象。\n const result: LocaleMessages = {};\n for (const [key, translations] of Object.entries(allMessages) as [\n MessageKey,\n MessageModule[string],\n ][]) {\n const path = key.split('.');\n const leaf = path.pop()!;\n let parent = result;\n for (const segment of path) {\n const existing = parent[segment];\n if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {\n parent[segment] = {};\n }\n parent = parent[segment] as LocaleMessages;\n }\n parent[leaf] = translations[locale];\n }\n return result;\n}\n\nexport const i18n = createI18n({\n legacy: false,\n globalInjection: false,\n locale: resolveMvuLocale(getHostLocale()),\n fallbackLocale: 'zh-CN',\n messages: {\n 'zh-CN': buildLocaleMessages('zh-CN'),\n en: buildLocaleMessages('en'),\n },\n missingWarn: false,\n fallbackWarn: false,\n warnHtmlMessage: false,\n});\n\nfunction translate(key: MessageKey, params?: TranslationParams): string {\n return params ? i18n.global.t(key, params) : i18n.global.t(key);\n}\n\nexport function tr(key: MessageKey, params?: TranslationParams): string {\n return translate(key, params);\n}\n\nexport function useMvuI18n() {\n const composer = useI18n({ useScope: 'global' });\n return {\n locale: composer.locale,\n t: (key: MessageKey, params?: TranslationParams) =>\n params ? composer.t(key, params) : composer.t(key),\n };\n}\n","declare const jest: any;\ndeclare const process: any;\n\nexport const is_jest_environment =\n typeof jest !== 'undefined' ||\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test');\n","import {\n migrateExtraModelApiProfiles,\n normalizeExtraModelPiProfileContextWindow,\n} from '@/function/update/extra_model_api_profiles';\nimport {\n CharacterSettingsOverride,\n CharacterSettingsOverridePath,\n getCharacterSettingsOverrideValue,\n hasActiveCharacterSettingsOverride,\n hasOwnCharacterSettingsOverride,\n} from '@/function/character_override/schema';\nimport { PI_THINKING_LEVELS } from '@/function/update/pi/thinking_setting';\nimport { is_jest_environment } from '@/jest';\nimport { registerAsUniqueScript } from '@util/script';\nimport { klona } from 'klona';\nimport { defineStore } from 'pinia';\nimport { computed, ref, toRaw, watch } from 'vue';\nimport * as z from 'zod';\n\n/**\n * 保留导入或界面中的无效窗口值,使其与合法的“0 = 使用目录值”明确区分。\n * 请求预检只接受 0 或正整数,避免错误配置静默回退到目录元数据。\n */\nconst ExtraModelPiContextWindow = z\n .union([z.number(), z.string()])\n .catch('__invalid_context_window__')\n .default(0)\n .transform((value): number | string => {\n if (typeof value === 'number') {\n return value;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return 0;\n }\n const parsed = Number(trimmed);\n return Number.isInteger(parsed) && parsed >= 0 ? parsed : value;\n });\n\n// 方案快照使用规范化整数;无效值使整个连接快照失效,而不污染其余设置。\nconst ExtraModelPiProfileContextWindow = z\n .union([z.number(), z.string()])\n .transform(value => normalizeExtraModelPiProfileContextWindow(value))\n .pipe(z.number());\n\n// 方案只保存连接与请求覆盖,OAuth 凭证和按目标缓存的密钥由活动设置独立持有。\nconst ExtraModelPiProfileSnapshot = z\n .object({\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n provider: z.string().trim(),\n api: z.string().trim(),\n authType: z\n .string()\n .trim()\n .pipe(z.enum(['api_key', 'oauth'])),\n endpoint: z.string().trim(),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim(),\n contextWindow: ExtraModelPiProfileContextWindow,\n customHeaders: z.string(),\n customIncludeBody: z.string(),\n customExcludeBody: z.string(),\n })\n .loose()\n .transform(({ credentials: _credentials, apiKeys: _apiKeys, ...connection }) => connection);\n\n// 导入的残缺 Pi 快照不能靠默认服务商或协议补成可发送连接。\n// 保留外层方案,只丢弃无效连接,应用时再要求用户补全。\nconst ExtraModelPiProfile = z.union([ExtraModelPiProfileSnapshot, z.undefined()]).catch(undefined);\n\n// 活动连接容许保留未知字段,实际可发送性由统一预检决定。\nconst ExtraModelPiSettings = z\n .object({\n provider: z.string().trim().default('openai'),\n api: z.string().trim().default('openai-responses'),\n // 保留未知认证标识供用户查看;请求和方案保存会拒绝它,迁移会清除无归属的活动密钥。\n authType: z.string().trim().default('api_key'),\n endpoint: z.string().trim().default(''),\n useProxy: z.boolean().catch(false).default(false),\n thinkingLevel: z.enum(PI_THINKING_LEVELS).catch('default').default('default'),\n model: z.string().trim().default(''),\n contextWindow: ExtraModelPiContextWindow,\n credentialIds: z.record(z.string(), z.string()).optional().catch({}),\n credentials: z.record(z.string(), z.unknown()).catch({}).default({}),\n apiKeys: z.record(z.string(), z.string()).catch({}).default({}),\n customHeaders: z.string().default(''),\n customIncludeBody: z.string().default(''),\n customExcludeBody: z.string().default(''),\n })\n .loose()\n .prefault({});\n\nexport const EXTRA_MODEL_RESPONSE_FORMATS = [\n '聊天消息',\n '工具调用',\n '格式化输出',\n '格式化输出(v4兼容)',\n] as const;\n\nconst ExtraModelResponseFormat = z.enum(EXTRA_MODEL_RESPONSE_FORMATS);\nconst ExtraModelJailbreakStrategy = z.enum(['使用内置破限', '使用当前预设', '使用其他预设']);\n\nconst ExtraModelApiProfile = z\n .object({\n 名称: z.string().trim().min(1),\n backend: z.enum(['custom', 'pi']).default('custom'),\n api地址: z.string().default(''),\n 密钥: z.string().default(''),\n 模型名称: z.string().default(''),\n pi: ExtraModelPiProfile.optional(),\n // 缺失选项保持未设置,使旧自定义方案可以一次性继承当前请求选项。\n 破限方案: ExtraModelJailbreakStrategy.optional().catch(undefined),\n 其他预设名称: z.string().optional().catch(undefined),\n 随机头部: z.boolean().optional().catch(undefined),\n 应答格式: ExtraModelResponseFormat.optional().catch(undefined),\n 关闭thinking: z.boolean().optional().catch(undefined),\n 兼容假流式: z.boolean().optional().catch(undefined),\n })\n .loose()\n .transform(({ customApiKey: _customApiKey, ...profile }) => profile);\n\n// 逐项容错,单个损坏方案只被过滤,不使整份用户设置重置。\nconst ExtraModelApiProfileList = z\n .array(ExtraModelApiProfile.nullable().catch(null))\n .catch([])\n .transform(profiles =>\n profiles.filter(\n (profile): profile is z.infer => profile !== null\n )\n );\n\nconst OldSettings = z\n .object({\n 通知: z\n .object({\n 变量更新出错: z.boolean(),\n 额外模型解析中: z.boolean(),\n })\n .loose(),\n 更新方式: z.enum(['随AI输出', '额外模型解析']),\n 自动触发额外模型解析: z.boolean(),\n 额外模型解析配置: z\n .object({\n 发送预设: z.boolean(),\n 使用函数调用: z.boolean(),\n 模型来源: z.enum(['与插头相同', '自定义']),\n api地址: z.string(),\n 密钥: z.string(),\n 模型名称: z.string(),\n 温度: z.coerce.number(),\n 频率惩罚: z.coerce.number(),\n 存在惩罚: z.coerce.number(),\n top_p: z.coerce.number(),\n 最大回复token数: z.coerce.number(),\n })\n .loose(),\n 快照保留间隔: z.number(),\n 更新到聊天变量: z.boolean(),\n legacy: z\n .object({\n 显示老旧功能: z.boolean(),\n })\n .loose(),\n auto_cleanup: z\n .object({\n 启用: z.boolean(),\n 要保留变量的最近楼层数: z.number(),\n 触发恢复变量的最近楼层数: z.number(),\n })\n .loose(),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean(),\n 已提醒自动清理旧变量功能: z.boolean(),\n 已提醒更新了API温度等配置: z.boolean(),\n 已默认开启自动清理旧变量功能: z.boolean(),\n })\n .loose(),\n })\n .loose()\n .transform(data => {\n const {\n 自动触发额外模型解析,\n 额外模型解析配置: { 发送预设, 使用函数调用, ...extra_model_settings },\n 快照保留间隔,\n 更新到聊天变量,\n legacy,\n auto_cleanup,\n 自动清理变量: existing_auto_cleanup,\n 兼容性: existing_compatibility,\n ...settings\n } = data;\n const existing_auto_cleanup_settings = _.isPlainObject(existing_auto_cleanup)\n ? (existing_auto_cleanup as Record)\n : {};\n const existing_compatibility_settings = _.isPlainObject(existing_compatibility)\n ? (existing_compatibility as Record)\n : {};\n\n return NewSettings.decode({\n ...settings,\n 额外模型解析配置: {\n 破限方案: 发送预设 ? '使用当前预设' : '使用内置破限',\n 启用自动请求: 自动触发额外模型解析,\n 应答格式: 使用函数调用 ? '工具调用' : '聊天消息',\n ...extra_model_settings,\n },\n 自动清理变量: {\n ...auto_cleanup,\n 快照保留间隔,\n ...existing_auto_cleanup_settings,\n },\n 兼容性: {\n ...legacy,\n 更新到聊天变量,\n ...existing_compatibility_settings,\n },\n });\n });\n\nconst NewSettings = z\n .object({\n 通知: z\n .object({\n MVU框架加载成功: z.boolean().default(true),\n 变量初始化成功: z.boolean().default(true),\n 变量更新出错: z.boolean().default(false),\n 额外模型解析中: z.boolean().default(true),\n })\n .loose()\n .prefault({}),\n 更新方式: z.enum(['随AI输出', '额外模型解析']).default('随AI输出'),\n 额外模型解析配置: z\n .object({\n 破限方案: ExtraModelJailbreakStrategy.default('使用内置破限'),\n 其他预设名称: z.string().default(''),\n 使用函数调用: z.boolean().optional(),\n 应答格式: ExtraModelResponseFormat.optional(),\n 关闭thinking: z.boolean().default(false),\n 兼容假流式: z.boolean().default(false),\n 随机头部: z.boolean().default(true),\n\n 启用自动请求: z.boolean().default(true),\n 请求方式: z\n .enum([\n '依次请求,失败后重试',\n '同时请求多次',\n '先请求一次, 失败后再同时请求多次',\n ])\n .default('依次请求,失败后重试'),\n 请求次数: z.number().default(3),\n 世界书条目白名单正则: z.string().default(''),\n 世界书条目黑名单正则: z.string().default(''),\n\n 模型来源: z.enum(['与插头相同', '自定义', '更多']).default('与插头相同'),\n api地址: z.string().default('http://localhost:1234/v1'),\n 密钥: z.string().default(''),\n customApiKey: z.string().catch('').default(''),\n 模型名称: z.string().default('gemini-2.5-flash-nothinking'),\n pi: ExtraModelPiSettings,\n 温度: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 2)),\n 频率惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n 存在惩罚: z.coerce\n .number()\n .default(0.0)\n .transform(value => _.clamp(value, -2, 2)),\n top_p: z.coerce\n .number()\n .default(1)\n .transform(value => _.clamp(value, 0, 1)),\n top_k: z.coerce\n .number()\n .default(0)\n .transform(value => _.clamp(value, 0, 500)),\n max_chat_history: z.coerce\n .number()\n .default(2)\n .transform(value => _.clamp(Math.round(value), 2, 100)),\n 最大回复token数: z.coerce\n .number()\n // 回复 token 的 0 没有默认值语义,可保留为阻止请求的无效导入值。\n .catch(0)\n .default(4096)\n .transform(value => Math.max(0, value)),\n api方案列表: ExtraModelApiProfileList.default([]),\n 当前api方案: z.string().catch('').default(''),\n })\n .loose()\n .transform(({ 使用函数调用, 应答格式, ...data }) =>\n migrateExtraModelApiProfiles({\n ...data,\n 应答格式: 应答格式 ?? (使用函数调用 ? '工具调用' : '聊天消息'),\n })\n )\n .prefault({}),\n 自动清理变量: z\n .object({\n 启用: z.boolean().default(true),\n 快照保留间隔: z.number().default(50),\n 要保留变量的最近楼层数: z.number().default(20),\n 触发恢复变量的最近楼层数: z.number().default(10),\n })\n .loose()\n .prefault({}),\n 兼容性: z\n .object({\n 更新到聊天变量: z.boolean().default(false),\n 显示老旧功能: z.boolean().default(false),\n 额外模型解析非阻塞: z.boolean().default(false),\n sendas不视为user消息: z.boolean().optional(),\n sandas不视为user消息: z.boolean().optional(),\n })\n .loose()\n .transform(({ sendas不视为user消息, sandas不视为user消息, ...data }) => ({\n ...data,\n 更新到聊天变量: data.更新到聊天变量,\n 显示老旧功能: data.显示老旧功能,\n sendas不视为user消息: sendas不视为user消息 ?? sandas不视为user消息 ?? false,\n }))\n .prefault({}),\n internal: z\n .object({\n 已提醒更新了配置界面: z.boolean().default(false),\n 已提醒自动清理旧变量功能: z.boolean().default(false),\n 已提醒更新了API温度等配置: z.boolean().default(false),\n 已默认开启自动清理旧变量功能: z.boolean().default(false),\n 已提醒内置破限: z.boolean().default(false),\n 已提醒额外模型同时请求: z.boolean().default(false),\n 已开启默认不兼容假流式: z.boolean().default(false),\n })\n .loose()\n .prefault({}),\n })\n .loose()\n .transform(data => {\n // 新安装由 Schema 提供 false;迁移时保留已保存的流式选项及方案继承值。\n data.internal.已开启默认不兼容假流式 = true;\n return data;\n })\n .prefault({});\n\nconst Settings = z.union([OldSettings, NewSettings]).catch(() => NewSettings.parse({}));\n\nexport type MvuSettings = z.infer;\n\nexport type CharacterSettingsState = {\n status: 'loading' | 'unbound' | 'ready' | 'error';\n character_name: string;\n worldbook_name: string | null;\n entry_uid: number | null;\n expected_content: string | null;\n draft: CharacterSettingsOverride;\n is_valid: boolean;\n revision: number;\n has_pending_save: boolean;\n is_saving: boolean;\n};\n\nfunction createCharacterSettingsState(): CharacterSettingsState {\n return {\n status: 'loading',\n character_name: '',\n worldbook_name: null,\n entry_uid: null,\n expected_content: null,\n draft: {},\n is_valid: true,\n revision: 0,\n has_pending_save: false,\n is_saving: false,\n };\n}\n\nconst Runtimes = z\n .object({\n unsupported_warnings: z.string().default(''),\n is_during_extra_analysis: z.boolean().default(false),\n is_function_call_enabled: z.boolean().default(false),\n 上次世界书条目过滤结果: z\n .array(\n z.object({\n lore: z.enum(['globalLore', 'characterLore', 'chatLore', 'personaLore']),\n world: z.string(),\n comment: z.string(),\n reason: z.enum(['白名单', '黑名单']),\n sources: z.array(z.enum(['用户全局配置', '角色卡配置'])).default([]),\n })\n )\n .default([]),\n debug: z\n .object({\n 首次额外请求必失败: z.boolean().default(false),\n })\n .prefault({}),\n })\n .prefault({});\n\nexport const useDataStore = defineStore('MVU变量框架', () => {\n const settings = ref(Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {})));\n watch(\n settings,\n new_settings => {\n _.set(SillyTavern.extensionSettings, 'mvu_settings', toRaw(new_settings));\n if (!is_jest_environment) SillyTavern.saveSettingsDebounced();\n },\n { deep: true }\n );\n const _reload_settings = () => {\n settings.value = Settings.parse(_.get(SillyTavern.extensionSettings, 'mvu_settings', {}));\n };\n\n const character_settings = ref(createCharacterSettingsState());\n const effective_settings = computed(() => {\n // 运行时读取合并后的配置,但不修改用户持久化的全局设置。\n const result = klona(settings.value);\n if (!character_settings.value.is_valid) {\n return result;\n }\n\n const draft = character_settings.value.draft;\n // 正则采用“全局 + 角色卡”的叠加语义,由 filterEntries 单独合并,不在此处覆盖。\n if (_.has(draft, '更新方式')) {\n result.更新方式 = draft.更新方式!;\n }\n if (_.has(draft, '额外模型解析配置.启用自动请求')) {\n result.额外模型解析配置.启用自动请求 = draft.额外模型解析配置!.启用自动请求!;\n }\n if (_.has(draft, '兼容性.更新到聊天变量')) {\n result.兼容性.更新到聊天变量 = draft.兼容性!.更新到聊天变量!;\n }\n if (_.has(draft, '兼容性.sendas不视为user消息')) {\n result.兼容性.sendas不视为user消息 = draft.兼容性!.sendas不视为user消息!;\n }\n return result;\n });\n const has_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid &&\n hasOwnCharacterSettingsOverride(character_settings.value.draft, path);\n const get_character_settings_override = (path: CharacterSettingsOverridePath) =>\n character_settings.value.is_valid\n ? getCharacterSettingsOverrideValue(character_settings.value.draft, path)\n : undefined;\n const is_character_settings_override_active = computed(\n () =>\n character_settings.value.is_valid &&\n hasActiveCharacterSettingsOverride(character_settings.value.draft)\n );\n const resetCharacterSettings = () => {\n character_settings.value = createCharacterSettingsState();\n };\n\n const runtimes = ref(Runtimes.parse({}));\n watch(\n () => runtimes.value.is_during_extra_analysis,\n new_value => insertOrAssignVariables({ extra_analysis: new_value }, { type: 'global' }),\n { immediate: true }\n );\n const resetRuntimes = () => {\n runtimes.value = Runtimes.parse({});\n };\n\n const versions = ref<{ sillytavern: string; tavernhelper: string }>({\n sillytavern: '',\n tavernhelper: '',\n });\n const _wait_init = async () => {\n versions.value.sillytavern = await fetch('/version')\n .then(res => res.json())\n .then(data => data.pkgVersion)\n .catch(() => '1.0.0');\n versions.value.tavernhelper = await getTavernHelperVersion();\n };\n\n const should_enable = ref(false);\n watch(should_enable, (new_enable, old_enable) => {\n if (new_enable && !old_enable) {\n _reload_settings();\n }\n });\n\n // 当存在多个 MVU 脚本实例时,仅优先实例应启用运行逻辑。\n registerAsUniqueScript('MVU变量框架').listenPreferenceState(preferred_script_id => {\n should_enable.value = preferred_script_id === getScriptId();\n });\n\n return {\n settings,\n _reload_settings,\n character_settings,\n effective_settings,\n has_character_settings_override,\n get_character_settings_override,\n is_character_settings_override_active,\n resetCharacterSettings,\n runtimes,\n resetRuntimes,\n versions,\n _wait_init,\n should_enable,\n };\n});\n","import { tr } from '@/i18n';\nimport { compare } from 'compare-versions';\nimport JSON5 from 'json5';\nimport { jsonrepair } from 'jsonrepair';\n\n// 修正 _.merge 对数组的合并逻辑, [1, 2, 3] 和 [4, 5] 合并后变成 [4, 5] 而不是 [4, 5, 3]\nexport function correctlyMerge(lhs: TObject, rhs: TSource): TObject & TSource {\n return _.mergeWith(lhs, rhs, (_lhs, rhs) => (_.isArray(rhs) ? rhs : undefined));\n}\n\nexport function uuidv4(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nexport async function checkMinimumVersion(\n expected: string,\n notice: { message: string; title: string }\n) {\n if (compare(await getTavernHelperVersion(), expected, '<')) {\n toastr.error(notice.message, notice.title);\n }\n}\nexport function literalYamlify(value: any) {\n return YAML.stringify(value, { blockQuote: 'literal' });\n}\n\nexport function parseString(content: string): any {\n const json_first = /^[[{]/s.test(content.trimStart());\n try {\n if (json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error1) {\n try {\n // eslint-disable-next-line import-x/no-named-as-default-member\n return JSON5.parse(content);\n } catch (json5_error) {\n try {\n return JSON.parse(jsonrepair(content));\n } catch (json_error) {\n try {\n if (!json_first) {\n throw Error(`expected error`);\n }\n return YAML.parseDocument(content, { merge: true }).toJS();\n } catch (yaml_error2) {\n const toError = (error: unknown) =>\n error instanceof Error\n ? `${error.stack ? error.stack : error.message}`\n : String(error);\n\n throw new Error(\n literalYamlify({\n [tr('runtime.parseString.invalidFormat')]: {\n [tr('runtime.parseString.content')]: content,\n [tr('runtime.parseString.yamlError')]: toError(\n json_first ? yaml_error2 : yaml_error1\n ),\n [tr('runtime.parseString.json5Error')]: toError(json5_error),\n [tr('runtime.parseString.jsonError')]: toError(json_error),\n },\n })\n );\n }\n }\n }\n }\n}\n","export function teleportStyle(\n append_to:\n | JQuery.Selector\n | JQuery.htmlString\n | JQuery.TypeOrArray\n | JQuery = 'head'\n): { destroy: () => void } {\n const $div = $(`
`)\n .attr('script_id', getScriptId())\n .append($(`head > style`, document).clone())\n .appendTo(append_to);\n\n return {\n destroy: () => $div.remove(),\n };\n}\n\nexport function createScriptIdDiv(): JQuery {\n return $('
').attr('script_id', getScriptId()) as JQuery;\n}\n\n/**\n * 将当前脚本实例注册到共享的“唯一脚本”命名空间。\n *\n * 当同一脚本被重复加载时,优先实例为:仍存在于 TavernHelper 脚本列表中、\n * 且在注册顺序上最后出现的 script id。\n * 调用方可通过 `listenPreferenceState` 订阅优先实例变化,并仅在\n * `getScriptId()` 与优先实例一致时启用功能。\n */\nexport function registerAsUniqueScript(id: string): {\n unregister: () => void;\n getPreferredScriptId: () => string | undefined;\n listenPreferenceState: (callback: (perferred_script_id: string) => void) => EventOnReturn;\n} {\n // 当前实例在 TavernHelper 中的唯一脚本 ID。\n const script_id = getScriptId();\n // 以业务 id 作为命名空间,避免不同功能之间冲突。\n const path = `th_unique_check.${id}`;\n\n const getPreferredScriptId = () => {\n // 从共享状态中取出已注册实例集合(跨脚本实例共享在 window.parent)。\n const registered_scripts = _.get(window.parent, path, new Set());\n // 以页面上实际存在的脚本顺序为准,选出“最后一个仍有效”的实例作为优先实例。\n return _($('#tavern_helper').find('div[data-script-id]').toArray())\n .map(element => String($(element).attr('data-script-id')))\n .filter(element => registered_scripts.has(element))\n .last();\n };\n\n // 将当前实例加入注册集合。不存在的场合创建。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value === undefined) {\n return new Set([script_id]);\n }\n //避免重复添加\n if (value.has(script_id)) return value;\n value.add(script_id);\n return value;\n });\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n\n return {\n unregister: () => {\n // 卸载时从注册集合移除,并重新广播优先实例。\n _.update(window.parent, path, (value: Set | undefined) => {\n if (value !== undefined) {\n value.delete(script_id);\n }\n return value;\n });\n eventEmit(path, getPreferredScriptId());\n },\n getPreferredScriptId,\n // 监听优先实例变化(回调入参是当前优先实例 script id)。\n listenPreferenceState: (callback: (enabled_script_id: string) => void) => {\n const ret = eventOn(path, callback);\n // 广播一次当前优先实例,通知监听方更新启用状态。\n eventEmit(path, getPreferredScriptId());\n return ret;\n },\n };\n}\n","/* unplugin-vue-components disabled */// style-loader: Adds some css to the DOM by adding a