From 7b5ea6b540003a07d09d9a1d3b465be260f55e41 Mon Sep 17 00:00:00 2001 From: Erencv Date: Sun, 1 Nov 2026 00:00:38 +0300 Subject: [PATCH] feat(runtime): support z.ai Start Plan providers in standalone mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled provider catalog ships account:zai-start-plan / account:bigmodel-start-plan (e.g. the ZCODE WEEKEND BUILD token promos), and Desktop's host registers them whenever the account is entitled, but the standalone account paths only enumerate Individual Coding Plan providers, so the CLI fails with 'Provider Registry 中不存在 Model'. Five minimal edits, AST-anchored on the existing bundler labels: - readStandaloneCodingPlanCatalog: enumerate Start Plan providers too - resolveStandaloneCodingPlanProvider: keep login's one-provider-per-family contract by excluding Start Plan ids - standaloneAccountIdentityCredentialKey: map Start Plan providers to the family's Coding Plan identity (the identity is account-scoped) - createStandaloneProviderRuntimeHeadersPort: accept the Start Plan access mode, and authenticate Start Plan requests with the zcode-scoped JWT credential, since zcode.z.ai/api/v1/zcode-plan rejects the Coding Plan api-key and the exchanged api.z.ai project JWT with 401 No protocol changes: requests are still built and signed by the genuine runtime. Registered as an optional patch with a behavioral test (test/runtime/start-plan.test.ts) covering the fixture patch, identity mapping and idempotency. --- scripts/runtime-start-plan-patches.ts | 109 ++++++++++++++++++++++++++ scripts/sync-runtime.ts | 6 ++ test/runtime/start-plan.test.ts | 55 +++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 scripts/runtime-start-plan-patches.ts create mode 100644 test/runtime/start-plan.test.ts diff --git a/scripts/runtime-start-plan-patches.ts b/scripts/runtime-start-plan-patches.ts new file mode 100644 index 0000000..9d0871e --- /dev/null +++ b/scripts/runtime-start-plan-patches.ts @@ -0,0 +1,109 @@ +import { parseExpressionAt, type FunctionExpression } from "acorn"; + +const identifier = "[A-Za-z_$][\\w$]*"; + +function incompatible(detail: string): never { + throw new Error(`ZCode runtime is incompatible with standalone Start Plan support (${detail}).`); +} + +function unique(items: Item[], name: string): Item { + if (items.length !== 1) incompatible(`${name} missing or ambiguous`); + return items[0]!; +} + +function escapePattern(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function namedFunction(runtime: string, label: string): FunctionExpression { + const name = unique([...runtime.matchAll(new RegExp(`${identifier}\\((${identifier}),"${label}"\\)`, "gu"))], label)[1]!; + const start = unique([...runtime.matchAll(new RegExp(`(?:async )?function ${escapePattern(name)}\\(`, "gu"))], label).index!; + const fn = parseExpressionAt(runtime, start, { ecmaVersion: "latest" }); + if (fn.type !== "FunctionExpression") incompatible(`${label} declaration changed`); + return fn; +} + +/** + * Admit z.ai/bigmodel Start Plan providers (for example the ZCODE WEEKEND BUILD + * token promos) in standalone mode. The bundled provider catalog already ships + * `account:zai-start-plan` / `account:bigmodel-start-plan`, and Desktop's host + * registers them whenever the account is entitled, but the standalone account + * paths only enumerate Individual Coding Plan providers. Five minimal edits: + * + * 1. the standalone catalog reader enumerates Start Plan providers as well, so + * they reach the provider registry and get an entitlement computed; + * 2. the login provider resolution keeps its "exactly one per family" contract + * by excluding Start Plan providers (login must keep targeting the Coding + * Plan api-key flow); + * 3. the standalone identity credential key maps Start Plan providers to the + * family's Coding Plan identity — the identity is account-scoped, and the + * server reports Start Plan grants for the same account; + * 4. the request credential refresher accepts the Start Plan access mode; + * 5. Start Plan requests authenticate with the zcode-scoped JWT credential + * (`zcodejwttoken`): Start Plan traffic is served from + * `zcode.z.ai/api/v1/zcode-plan/...`, which rejects the Coding Plan api-key + * and the exchanged api.z.ai project JWT with 401. + * + * No protocol changes: requests are still built and signed by the genuine + * runtime; only provider enumeration and credential selection are widened to + * cover an entitlement the account already has. + */ + +/** True when the runtime already admits Start Plan providers standalone (native or patched). */ +export function hasRuntimeStartPlan(runtime: string): boolean { + try { + const catalog = namedFunction(runtime, "readStandaloneCodingPlanCatalog"); + const headers = namedFunction(runtime, "createStandaloneProviderRuntimeHeadersPort"); + return runtime.slice(catalog.start, catalog.end).includes('"start-plan"') + && runtime.slice(headers.start, headers.end).includes('mode==="start-plan"'); + } catch { + return false; + } +} + +export function patchRuntimeStartPlan(runtime: string): string { + if (hasRuntimeStartPlan(runtime)) return runtime; + const edits: { start: number; end: number; value: string }[] = []; + const plan = (label: string, pattern: RegExp, value: string, name: string) => { + const fn = namedFunction(runtime, label); + const source = runtime.slice(fn.start, fn.end); + const match = unique([...source.matchAll(pattern)], name); + edits.push({ start: fn.start + match.index!, end: fn.start + match.index! + match[0].length, value }); + }; + + // 1) standalone catalog: enumerate Start Plan providers beside Coding Plan providers + plan("readStandaloneCodingPlanCatalog", + new RegExp('c\\?\\.type==="zhipu-account"&&c\\.mode==="individual-coding-plan"&&c\\.accountType&&u', "gu"), + 'c?.type==="zhipu-account"&&(c.mode==="individual-coding-plan"||c.mode==="start-plan")&&c.accountType&&u', + "catalog provider predicate"); + + // 2) login: keep resolving the Coding Plan provider uniquely (Start Plan must not break it) + plan("resolveStandaloneCodingPlanProvider", + new RegExp('\\.filter\\(o=>o\\.family===e\\);if\\(n\\.length!==1\\)', "gu"), + '.filter(o=>o.family===e&&!/-start-plan$/.test(o.providerId));if(n.length!==1)', + "login provider uniqueness filter"); + + // 3) identity: Start Plan providers share the family's account-scoped identity credential + plan("standaloneAccountIdentityCredentialKey", + new RegExp('return`account-provider:\\$\\{t\\}:identity`', "gu"), + 'return`account-provider:${/-start-plan$/.test(t)?t.replace(/-start-plan$/u,"-individual-coding-plan"):t}:identity`', + "identity credential key mapping"); + + // 4) request refresher: accept the Start Plan access mode + plan("createStandaloneProviderRuntimeHeadersPort", + new RegExp('if\\(!f\\|\\|f\\.mode!=="individual-coding-plan"\\)throw', "gu"), + 'if(!f||f.mode!=="individual-coding-plan"&&f.mode!=="start-plan")throw', + "request access mode check"); + + // 5) request auth: Start Plan requests use the zcode-scoped JWT credential + const credentialCheck = 'if(c.abortSignal?.throwIfAborted(),!R)throw new Error(`Standalone Account Provider \\u7F3A\\u5C11\\u8BF7\\u6C42\\u51ED\\u636E: ${u}`)'; + plan("createStandaloneProviderRuntimeHeadersPort", + new RegExp(escapePattern(credentialCheck), "gu"), + `if(f?.mode==="start-plan"){let $zSpToken=(await e.load("zcodejwttoken"))?.trim();if($zSpToken)R=$zSpToken}${credentialCheck}`, + "Start Plan request auth override"); + + edits.sort((first, second) => second.start - first.start); + let output = runtime; + for (const edit of edits) output = output.slice(0, edit.start) + edit.value + output.slice(edit.end); + return output; +} diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index 23bbdb0..ca89dc9 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -19,6 +19,8 @@ export { hasRuntimeSessionLifecycle, patchRuntimeSessionLifecycle } from "./runt import { hasRuntimeBigmodelOAuth, patchRuntimeBigmodelOAuth } from "./runtime-bigmodel-oauth-patches.ts"; import { hasRuntimeSharedOAuth, patchRuntimeSharedOAuth } from "./runtime-shared-oauth-patches.ts"; export { hasRuntimeSharedOAuth, patchRuntimeSharedOAuth } from "./runtime-shared-oauth-patches.ts"; +import { hasRuntimeStartPlan, patchRuntimeStartPlan } from "./runtime-start-plan-patches.ts"; +export { hasRuntimeStartPlan, patchRuntimeStartPlan } from "./runtime-start-plan-patches.ts"; export { hasRuntimeBigmodelOAuth, patchRuntimeBigmodelOAuth } from "./runtime-bigmodel-oauth-patches.ts"; import { hasRuntimeBrowserUrlOpening, patchRuntimeBrowserUrlOpening } from "./runtime-browser-patches.ts"; export { hasRuntimeBrowserUrlOpening, patchRuntimeBrowserUrlOpening } from "./runtime-browser-patches.ts"; @@ -1967,6 +1969,10 @@ export const runtimePatchPlan: readonly RuntimePatchDefinition[] = [ id: "shared-oauth", requirement: "required", apply: patchRuntimeSharedOAuth, verify: hasRuntimeSharedOAuth }, + { + id: "start-plan-standalone", requirement: "optional", apply: patchRuntimeStartPlan, + verify: hasRuntimeStartPlan + }, { id: "login-model-defaults", requirement: "required", diff --git a/test/runtime/start-plan.test.ts b/test/runtime/start-plan.test.ts new file mode 100644 index 0000000..3fd9c21 --- /dev/null +++ b/test/runtime/start-plan.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { hasRuntimeStartPlan, patchRuntimeStartPlan } from "../../scripts/runtime-start-plan-patches.ts"; + +const root = join(import.meta.dir, "../.."); + +// Minimal stand-in for the vendored runtime: the four account functions the +// Start Plan patches touch, in their real (pristine, minified) statement +// shapes. Escapes are doubled where the outer template would consume them, so +// the fixture string carries the same literal \uXXXX / ${...} text as bundle. +const fixture = ` +var K3n=require("node:crypto"); +async function Y3n(e,t){if(t)return{zcodeBuiltinRevision:t.revision,providers:t.providers.entries().flatMap(([i,a])=>{let c=a.access,u=a.builtinModelIds?.find(f=>f.trim())?.trim();return c?.type==="zhipu-account"&&c.mode==="individual-coding-plan"&&c.accountType&&u?[{family:c.accountType,modelId:u,providerId:i}]:[]})};throw new Error(\`\${VG} is required for login\`)} +r(Y3n,"readStandaloneCodingPlanCatalog"); +async function Kgi(e,t){let n=(await Z3n(t)).filter(o=>o.family===e);if(n.length!==1)throw new Error(\`ZCode Built-in Config \\u5FC5\\u987B\\u4E3A \${e} \\u58F0\\u660E\\u552F\\u4E00 Individual Coding Plan Provider\`);return n[0]} +r(Kgi,"resolveStandaloneCodingPlanProvider"); +function fW(e){let t=e.trim();if(!t)throw new Error("Standalone Account Provider ID \\u4E0D\\u80FD\\u4E3A\\u7A7A");return\`account-provider:\${t}:identity\`} +r(fW,"standaloneAccountIdentityCredentialKey"); +function Ygi(e){return{async refreshBeforeModelRequest(c){let u=c.providerId.trim(),f=c.accountAccess,_=f.accountType;if(!f||f.mode!=="individual-coding-plan")throw new Error(\`Standalone Account Provider \\u8BF7\\u6C42\\u8EAB\\u4EFD\\u65E0\\u6548: \${u}\`);let h=(await e.load(fW(u)))?.trim();if(!h)throw new Error("Standalone Account Provider \\u51ED\\u8BC1\\u5DF2\\u7ECF\\u5931\\u6548: "+u);let v=bX(u),S=await e.load(v),k=J3n(_),I=S==="manual"?void 0:(await e.load(k))?.trim(),C=S==="oauth"||S!=="manual"&&!!I,R,D,B;if(C){R=await exchange()}else if(S==="manual"){R=(await e.load($5e({providerId:u,accountIdentity:h})))?.trim()}if(!C&&S!=="manual")throw new Error("project_token_login_required");if(c.abortSignal?.throwIfAborted(),!R)throw new Error(\`Standalone Account Provider \\u7F3A\\u5C11\\u8BF7\\u6C42\\u51ED\\u636E: \${u}\`);return{headersApplied:!0,requestAuth:{apiKey:R,...D?{apiKeyId:D}:{},...B?{accountScope:B}:{}}}}}} +r(Ygi,"createStandaloneProviderRuntimeHeadersPort"); +`; + +test("patched fixture admits Start Plan providers and is idempotent", () => { + expect(hasRuntimeStartPlan(fixture)).toBe(false); + const patched = patchRuntimeStartPlan(fixture); + expect(hasRuntimeStartPlan(patched)).toBe(true); + expect(patchRuntimeStartPlan(patched)).toBe(patched); + expect(patched).toContain('c.mode==="individual-coding-plan"||c.mode==="start-plan"'); + expect(patched).toContain('!/-start-plan$/.test(o.providerId)'); + expect(patched).toContain('t.replace(/-start-plan$/u,"-individual-coding-plan")'); + expect(patched).toContain('f.mode!=="individual-coding-plan"&&f.mode!=="start-plan"'); + expect(patched).toContain('(await e.load("zcodejwttoken"))?.trim()'); +}); + +test("Start Plan identity resolves to the family Coding Plan identity credential", () => { + const patched = patchRuntimeStartPlan(fixture); + // Label/env stubs only; the function under test touches none of the others. + const fW = new Function("require", "r", "VG", "jM", "Z3n", "bX", "J3n", "$5e", `${patched}\nreturn fW;`)( + (id: string) => (id === "node:crypto" ? {} : undefined), + () => {}, + "VG", class {}, async () => [], + () => "auth-source", () => "oauth", () => "api-key", + ) as (id: string) => string; + expect(fW("account:zai-individual-coding-plan")).toBe("account-provider:account:zai-individual-coding-plan:identity"); + expect(fW("account:zai-start-plan")).toBe("account-provider:account:zai-individual-coding-plan:identity"); + expect(fW("account:bigmodel-start-plan")).toBe("account-provider:account:bigmodel-individual-coding-plan:identity"); + expect(fW("account:zai-team-coding-plan")).toBe("account-provider:account:zai-team-coding-plan:identity"); +}); + +test("synced runtime ships standalone Start Plan support", async () => { + const runtime = await readFile(join(root, "vendor/zcode.cjs"), "utf8"); + expect(hasRuntimeStartPlan(runtime)).toBe(true); + expect(patchRuntimeStartPlan(runtime)).toBe(runtime); +});