ai-zero-token 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/api.js +6 -0
- package/dist/api.js.map +1 -0
- package/dist/cli/commands/ask.js +15 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/clear.js +11 -0
- package/dist/cli/commands/clear.js.map +1 -0
- package/dist/cli/commands/help.js +26 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/login.js +21 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/models.js +14 -0
- package/dist/cli/commands/models.js.map +1 -0
- package/dist/cli/commands/serve.js +18 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/status.js +32 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/shared.js +50 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/context.js +24 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/models/openai-codex-models.js +34 -0
- package/dist/core/models/openai-codex-models.js.map +1 -0
- package/dist/core/providers/http-client.js +84 -0
- package/dist/core/providers/http-client.js.map +1 -0
- package/dist/core/providers/openai-codex/chat.js +102 -0
- package/dist/core/providers/openai-codex/chat.js.map +1 -0
- package/dist/core/providers/openai-codex/oauth.js +284 -0
- package/dist/core/providers/openai-codex/oauth.js.map +1 -0
- package/dist/core/providers/openai-codex/pkce.js +21 -0
- package/dist/core/providers/openai-codex/pkce.js.map +1 -0
- package/dist/core/services/auth-service.js +73 -0
- package/dist/core/services/auth-service.js.map +1 -0
- package/dist/core/services/chat-service.js +28 -0
- package/dist/core/services/chat-service.js.map +1 -0
- package/dist/core/services/config-service.js +69 -0
- package/dist/core/services/config-service.js.map +1 -0
- package/dist/core/services/model-service.js +42 -0
- package/dist/core/services/model-service.js.map +1 -0
- package/dist/core/store/profile-store.js +74 -0
- package/dist/core/store/profile-store.js.map +1 -0
- package/dist/core/store/settings-store.js +51 -0
- package/dist/core/store/settings-store.js.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/http.js +6 -0
- package/dist/http.js.map +1 -0
- package/dist/models.js +14 -0
- package/dist/models.js.map +1 -0
- package/dist/oauth.js +10 -0
- package/dist/oauth.js.map +1 -0
- package/dist/pkce.js +6 -0
- package/dist/pkce.js.map +1 -0
- package/dist/server/app.js +115 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/index.js +23 -0
- package/dist/server/index.js.map +1 -0
- package/dist/store.js +20 -0
- package/dist/store.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/models/openai-codex-models.ts"],"sourcesContent":["import type { ModelInfo } from \"../types.js\";\n\nexport const DEFAULT_CODEX_MODEL = \"gpt-5.4\";\n\nexport const CODEX_MODEL_INFOS: ModelInfo[] = [\n { provider: \"openai-codex\", id: \"gpt-5.4\", name: \"GPT-5.4\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.2\", name: \"GPT-5.2\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.2-codex\", name: \"GPT-5.2 Codex\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.3-codex\", name: \"GPT-5.3 Codex\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.3-codex-spark\", name: \"GPT-5.3 Codex Spark\", input: [\"text\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.1\", name: \"GPT-5.1\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.1-codex\", name: \"GPT-5.1 Codex\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.1-codex-mini\", name: \"GPT-5.1 Codex Mini\", input: [\"text\", \"image\"], source: \"static\" },\n { provider: \"openai-codex\", id: \"gpt-5.1-codex-max\", name: \"GPT-5.1 Codex Max\", input: [\"text\", \"image\"], source: \"static\" },\n];\n\nexport const SUPPORTED_CODEX_MODELS = [\n \"gpt-5.4\",\n \"gpt-5.2\",\n \"gpt-5.2-codex\",\n \"gpt-5.3-codex\",\n \"gpt-5.3-codex-spark\",\n \"gpt-5.1\",\n \"gpt-5.1-codex\",\n \"gpt-5.1-codex-mini\",\n \"gpt-5.1-codex-max\",\n] as const;\n\nexport type SupportedCodexModel = (typeof SUPPORTED_CODEX_MODELS)[number];\n\nexport function isSupportedCodexModel(model: string): model is SupportedCodexModel {\n return SUPPORTED_CODEX_MODELS.includes(model as SupportedCodexModel);\n}\n"],"mappings":";AAEO,MAAM,sBAAsB;AAE5B,MAAM,oBAAiC;AAAA,EAC5C,EAAE,UAAU,gBAAgB,IAAI,WAAW,MAAM,WAAW,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACvG,EAAE,UAAU,gBAAgB,IAAI,WAAW,MAAM,WAAW,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACvG,EAAE,UAAU,gBAAgB,IAAI,iBAAiB,MAAM,iBAAiB,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACnH,EAAE,UAAU,gBAAgB,IAAI,iBAAiB,MAAM,iBAAiB,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACnH,EAAE,UAAU,gBAAgB,IAAI,uBAAuB,MAAM,uBAAuB,OAAO,CAAC,MAAM,GAAG,QAAQ,SAAS;AAAA,EACtH,EAAE,UAAU,gBAAgB,IAAI,WAAW,MAAM,WAAW,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACvG,EAAE,UAAU,gBAAgB,IAAI,iBAAiB,MAAM,iBAAiB,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EACnH,EAAE,UAAU,gBAAgB,IAAI,sBAAsB,MAAM,sBAAsB,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAAA,EAC7H,EAAE,UAAU,gBAAgB,IAAI,qBAAqB,MAAM,qBAAqB,OAAO,CAAC,QAAQ,OAAO,GAAG,QAAQ,SAAS;AAC7H;AAEO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,sBAAsB,OAA6C;AACjF,SAAO,uBAAuB,SAAS,KAA4B;AACrE;","names":[]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
const CURL_STATUS_MARKER = "\n__CURL_STATUS__:";
|
|
4
|
+
async function runCurlRequest(init) {
|
|
5
|
+
const args = [
|
|
6
|
+
"--silent",
|
|
7
|
+
"--show-error",
|
|
8
|
+
"--location",
|
|
9
|
+
"--request",
|
|
10
|
+
init.method,
|
|
11
|
+
init.url,
|
|
12
|
+
"--write-out",
|
|
13
|
+
`${CURL_STATUS_MARKER}%{http_code}`
|
|
14
|
+
];
|
|
15
|
+
for (const [key, value] of Object.entries(init.headers ?? {})) {
|
|
16
|
+
args.push("--header", `${key}: ${value}`);
|
|
17
|
+
}
|
|
18
|
+
if (typeof init.body === "string") {
|
|
19
|
+
args.push("--data-raw", init.body);
|
|
20
|
+
}
|
|
21
|
+
const child = spawn("curl", args, {
|
|
22
|
+
env: process.env,
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
24
|
+
});
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
child.stdout.setEncoding("utf8");
|
|
28
|
+
child.stdout.on("data", (chunk) => {
|
|
29
|
+
stdout += chunk;
|
|
30
|
+
});
|
|
31
|
+
child.stderr.setEncoding("utf8");
|
|
32
|
+
child.stderr.on("data", (chunk) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
36
|
+
child.on("error", reject);
|
|
37
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
38
|
+
});
|
|
39
|
+
if (exitCode !== 0) {
|
|
40
|
+
throw new Error(stderr.trim() || `curl \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}`);
|
|
41
|
+
}
|
|
42
|
+
const markerIndex = stdout.lastIndexOf(CURL_STATUS_MARKER);
|
|
43
|
+
if (markerIndex === -1) {
|
|
44
|
+
throw new Error("curl \u54CD\u5E94\u7F3A\u5C11\u72B6\u6001\u7801\u6807\u8BB0\u3002");
|
|
45
|
+
}
|
|
46
|
+
const body = stdout.slice(0, markerIndex);
|
|
47
|
+
const statusText = stdout.slice(markerIndex + CURL_STATUS_MARKER.length).trim();
|
|
48
|
+
const status = Number.parseInt(statusText, 10);
|
|
49
|
+
if (!Number.isFinite(status)) {
|
|
50
|
+
throw new Error(`\u65E0\u6CD5\u89E3\u6790 curl \u72B6\u6001\u7801: ${statusText}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
body,
|
|
54
|
+
status,
|
|
55
|
+
transport: "curl"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function requestText(init) {
|
|
59
|
+
const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === "1";
|
|
60
|
+
const timeoutMs = init.timeoutMs ?? 2e4;
|
|
61
|
+
if (!useCurlOnly) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(init.url, {
|
|
64
|
+
method: init.method,
|
|
65
|
+
headers: init.headers,
|
|
66
|
+
body: init.body,
|
|
67
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
body: await response.text(),
|
|
71
|
+
status: response.status,
|
|
72
|
+
transport: "fetch"
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
console.log(`fetch \u8BF7\u6C42\u5931\u8D25\uFF0C\u51C6\u5907\u56DE\u9000\u5230 curl: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return runCurlRequest(init);
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
requestText
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=http-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/providers/http-client.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\n\ntype HttpTextResponse = {\n body: string;\n status: number;\n transport: \"fetch\" | \"curl\";\n};\n\ntype TextRequestInit = {\n body?: string;\n headers?: Record<string, string>;\n method: \"GET\" | \"POST\";\n timeoutMs?: number;\n url: string;\n};\n\nconst CURL_STATUS_MARKER = \"\\n__CURL_STATUS__:\";\n\nasync function runCurlRequest(init: TextRequestInit): Promise<HttpTextResponse> {\n const args = [\n \"--silent\",\n \"--show-error\",\n \"--location\",\n \"--request\",\n init.method,\n init.url,\n \"--write-out\",\n `${CURL_STATUS_MARKER}%{http_code}`,\n ];\n\n for (const [key, value] of Object.entries(init.headers ?? {})) {\n args.push(\"--header\", `${key}: ${value}`);\n }\n\n if (typeof init.body === \"string\") {\n args.push(\"--data-raw\", init.body);\n }\n\n const child = spawn(\"curl\", args, {\n env: process.env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk) => {\n stdout += chunk;\n });\n\n child.stderr.setEncoding(\"utf8\");\n child.stderr.on(\"data\", (chunk) => {\n stderr += chunk;\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.on(\"error\", reject);\n child.on(\"close\", (code) => resolve(code ?? 1));\n });\n\n if (exitCode !== 0) {\n throw new Error(stderr.trim() || `curl 请求失败,退出码 ${exitCode}`);\n }\n\n const markerIndex = stdout.lastIndexOf(CURL_STATUS_MARKER);\n if (markerIndex === -1) {\n throw new Error(\"curl 响应缺少状态码标记。\");\n }\n\n const body = stdout.slice(0, markerIndex);\n const statusText = stdout.slice(markerIndex + CURL_STATUS_MARKER.length).trim();\n const status = Number.parseInt(statusText, 10);\n if (!Number.isFinite(status)) {\n throw new Error(`无法解析 curl 状态码: ${statusText}`);\n }\n\n return {\n body,\n status,\n transport: \"curl\",\n };\n}\n\nexport async function requestText(init: TextRequestInit): Promise<HttpTextResponse> {\n const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === \"1\";\n const timeoutMs = init.timeoutMs ?? 20000;\n\n if (!useCurlOnly) {\n try {\n const response = await fetch(init.url, {\n method: init.method,\n headers: init.headers,\n body: init.body,\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n return {\n body: await response.text(),\n status: response.status,\n transport: \"fetch\",\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.log(`fetch 请求失败,准备回退到 curl: ${message}`);\n }\n }\n\n return runCurlRequest(init);\n}\n"],"mappings":";AAAA,SAAS,aAAa;AAgBtB,MAAM,qBAAqB;AAE3B,eAAe,eAAe,MAAkD;AAC9E,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AAAA,IACA,GAAG,kBAAkB;AAAA,EACvB;AAEA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,GAAG;AAC7D,SAAK,KAAK,YAAY,GAAG,GAAG,KAAK,KAAK,EAAE;AAAA,EAC1C;AAEA,MAAI,OAAO,KAAK,SAAS,UAAU;AACjC,SAAK,KAAK,cAAc,KAAK,IAAI;AAAA,EACnC;AAEA,QAAM,QAAQ,MAAM,QAAQ,MAAM;AAAA,IAChC,KAAK,QAAQ;AAAA,IACb,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,EAClC,CAAC;AAED,MAAI,SAAS;AACb,MAAI,SAAS;AAEb,QAAM,OAAO,YAAY,MAAM;AAC/B,QAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AACjC,cAAU;AAAA,EACZ,CAAC;AAED,QAAM,OAAO,YAAY,MAAM;AAC/B,QAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AACjC,cAAU;AAAA,EACZ,CAAC;AAED,QAAM,WAAW,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC9D,UAAM,GAAG,SAAS,MAAM;AACxB,UAAM,GAAG,SAAS,CAAC,SAAS,QAAQ,QAAQ,CAAC,CAAC;AAAA,EAChD,CAAC;AAED,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI,MAAM,OAAO,KAAK,KAAK,yDAAiB,QAAQ,EAAE;AAAA,EAC9D;AAEA,QAAM,cAAc,OAAO,YAAY,kBAAkB;AACzD,MAAI,gBAAgB,IAAI;AACtB,UAAM,IAAI,MAAM,mEAAiB;AAAA,EACnC;AAEA,QAAM,OAAO,OAAO,MAAM,GAAG,WAAW;AACxC,QAAM,aAAa,OAAO,MAAM,cAAc,mBAAmB,MAAM,EAAE,KAAK;AAC9E,QAAM,SAAS,OAAO,SAAS,YAAY,EAAE;AAC7C,MAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,UAAM,IAAI,MAAM,qDAAkB,UAAU,EAAE;AAAA,EAChD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AACF;AAEA,eAAsB,YAAY,MAAkD;AAClF,QAAM,cAAc,QAAQ,IAAI,wBAAwB;AACxD,QAAM,YAAY,KAAK,aAAa;AAEpC,MAAI,CAAC,aAAa;AAChB,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK,KAAK;AAAA,QACrC,QAAQ,KAAK;AAAA,QACb,SAAS,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,QACX,QAAQ,YAAY,QAAQ,SAAS;AAAA,MACvC,CAAC;AAED,aAAO;AAAA,QACL,MAAM,MAAM,SAAS,KAAK;AAAA,QAC1B,QAAQ,SAAS;AAAA,QACjB,WAAW;AAAA,MACb;AAAA,IACF,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,cAAQ,IAAI,4EAA0B,OAAO,EAAE;AAAA,IACjD;AAAA,EACF;AAEA,SAAO,eAAe,IAAI;AAC5B;","names":[]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { DEFAULT_CODEX_MODEL } from "../../models/openai-codex-models.js";
|
|
3
|
+
import { requestText } from "../http-client.js";
|
|
4
|
+
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
5
|
+
function extractOutputText(payload) {
|
|
6
|
+
if (!payload || typeof payload !== "object") {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
const data = payload;
|
|
10
|
+
if (typeof data.output_text === "string" && data.output_text.trim()) {
|
|
11
|
+
return data.output_text.trim();
|
|
12
|
+
}
|
|
13
|
+
for (const item of data.output ?? []) {
|
|
14
|
+
if (typeof item?.text === "string" && item.text.trim()) {
|
|
15
|
+
return item.text.trim();
|
|
16
|
+
}
|
|
17
|
+
for (const part of item?.content ?? []) {
|
|
18
|
+
if ((part?.type === "output_text" || part?.type === "text") && typeof part.text === "string") {
|
|
19
|
+
const trimmed = part.text.trim();
|
|
20
|
+
if (trimmed) {
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
function parseSseEvents(body) {
|
|
29
|
+
const events = [];
|
|
30
|
+
for (const chunk of body.split("\n\n")) {
|
|
31
|
+
const lines = chunk.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean);
|
|
32
|
+
if (lines.length === 0) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const data = lines.join("\n").trim();
|
|
36
|
+
if (!data || data === "[DONE]") {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
events.push(JSON.parse(data));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return events;
|
|
45
|
+
}
|
|
46
|
+
function extractCodexText(body) {
|
|
47
|
+
const events = parseSseEvents(body);
|
|
48
|
+
let responsePayload;
|
|
49
|
+
let accumulated = "";
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
if (event.type === "response.completed" || event.type === "response.done" || event.type === "response.incomplete") {
|
|
52
|
+
responsePayload = event.response;
|
|
53
|
+
}
|
|
54
|
+
if (typeof event.delta === "string" && event.delta) {
|
|
55
|
+
accumulated += event.delta;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const completedText = extractOutputText(responsePayload);
|
|
59
|
+
if (completedText) {
|
|
60
|
+
return { text: completedText, raw: responsePayload ?? events };
|
|
61
|
+
}
|
|
62
|
+
return { text: accumulated.trim(), raw: responsePayload ?? events };
|
|
63
|
+
}
|
|
64
|
+
async function askOpenAICodex(params) {
|
|
65
|
+
const response = await requestText({
|
|
66
|
+
method: "POST",
|
|
67
|
+
url: CODEX_RESPONSES_URL,
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: "text/event-stream",
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
Authorization: `Bearer ${params.profile.access}`,
|
|
72
|
+
"ChatGPT-Account-Id": params.profile.accountId,
|
|
73
|
+
"OpenAI-Beta": "responses=experimental",
|
|
74
|
+
Originator: "pi",
|
|
75
|
+
"User-Agent": "pi (bun demo)"
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model: params.model ?? DEFAULT_CODEX_MODEL,
|
|
79
|
+
store: false,
|
|
80
|
+
stream: true,
|
|
81
|
+
instructions: params.system ?? "",
|
|
82
|
+
input: [
|
|
83
|
+
{
|
|
84
|
+
role: "user",
|
|
85
|
+
content: [{ type: "input_text", text: params.prompt }]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
text: { verbosity: "medium" },
|
|
89
|
+
include: ["reasoning.encrypted_content"],
|
|
90
|
+
tool_choice: "auto",
|
|
91
|
+
parallel_tool_calls: true
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
if (response.status < 200 || response.status >= 300) {
|
|
95
|
+
throw new Error(`\u8C03\u7528 Responses API \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
|
|
96
|
+
}
|
|
97
|
+
return extractCodexText(response.body);
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
askOpenAICodex
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/core/providers/openai-codex/chat.ts"],"sourcesContent":["import type { OAuthProfile } from \"../../types.js\";\nimport { DEFAULT_CODEX_MODEL } from \"../../models/openai-codex-models.js\";\nimport { requestText } from \"../http-client.js\";\n\nconst CODEX_RESPONSES_URL = \"https://chatgpt.com/backend-api/codex/responses\";\n\ntype CodexSseEvent = {\n type?: string;\n response?: unknown;\n delta?: string;\n};\n\nfunction extractOutputText(payload: unknown): string {\n if (!payload || typeof payload !== \"object\") {\n return \"\";\n }\n\n const data = payload as {\n output_text?: unknown;\n output?: Array<{\n type?: string;\n content?: Array<{ type?: string; text?: string }>;\n text?: string;\n }>;\n };\n\n if (typeof data.output_text === \"string\" && data.output_text.trim()) {\n return data.output_text.trim();\n }\n\n for (const item of data.output ?? []) {\n if (typeof item?.text === \"string\" && item.text.trim()) {\n return item.text.trim();\n }\n\n for (const part of item?.content ?? []) {\n if ((part?.type === \"output_text\" || part?.type === \"text\") && typeof part.text === \"string\") {\n const trimmed = part.text.trim();\n if (trimmed) {\n return trimmed;\n }\n }\n }\n }\n\n return \"\";\n}\n\nfunction parseSseEvents(body: string): CodexSseEvent[] {\n const events: CodexSseEvent[] = [];\n for (const chunk of body.split(\"\\n\\n\")) {\n const lines = chunk\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).trim())\n .filter(Boolean);\n\n if (lines.length === 0) {\n continue;\n }\n\n const data = lines.join(\"\\n\").trim();\n if (!data || data === \"[DONE]\") {\n continue;\n }\n\n try {\n events.push(JSON.parse(data) as CodexSseEvent);\n } catch {\n // ignore malformed SSE chunks\n }\n }\n return events;\n}\n\nfunction extractCodexText(body: string): { text: string; raw: unknown } {\n const events = parseSseEvents(body);\n let responsePayload: unknown;\n let accumulated = \"\";\n\n for (const event of events) {\n if (\n event.type === \"response.completed\" ||\n event.type === \"response.done\" ||\n event.type === \"response.incomplete\"\n ) {\n responsePayload = event.response;\n }\n\n if (typeof event.delta === \"string\" && event.delta) {\n accumulated += event.delta;\n }\n }\n\n const completedText = extractOutputText(responsePayload);\n if (completedText) {\n return { text: completedText, raw: responsePayload ?? events };\n }\n\n return { text: accumulated.trim(), raw: responsePayload ?? events };\n}\n\nexport async function askOpenAICodex(params: {\n profile: OAuthProfile;\n prompt: string;\n model?: string;\n system?: string;\n}): Promise<{ text: string; raw: unknown }> {\n const response = await requestText({\n method: \"POST\",\n url: CODEX_RESPONSES_URL,\n headers: {\n Accept: \"text/event-stream\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${params.profile.access}`,\n \"ChatGPT-Account-Id\": params.profile.accountId,\n \"OpenAI-Beta\": \"responses=experimental\",\n Originator: \"pi\",\n \"User-Agent\": \"pi (bun demo)\",\n },\n body: JSON.stringify({\n model: params.model ?? DEFAULT_CODEX_MODEL,\n store: false,\n stream: true,\n instructions: params.system ?? \"\",\n input: [\n {\n role: \"user\",\n content: [{ type: \"input_text\", text: params.prompt }],\n },\n ],\n text: { verbosity: \"medium\" },\n include: [\"reasoning.encrypted_content\"],\n tool_choice: \"auto\",\n parallel_tool_calls: true,\n }),\n });\n\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`调用 Responses API 失败: HTTP ${response.status} via ${response.transport} ${response.body}`);\n }\n\n return extractCodexText(response.body);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
import { requestText } from "../http-client.js";
|
|
8
|
+
import { generatePKCE } from "./pkce.js";
|
|
9
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
10
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
11
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
12
|
+
const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
13
|
+
const SCOPE = "openid profile email offline_access";
|
|
14
|
+
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
15
|
+
const SUCCESS_HTML = `<!doctype html>
|
|
16
|
+
<html lang="zh-CN">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>OAuth Success</title>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<p>\u767B\u5F55\u6210\u529F\uFF0C\u8BF7\u56DE\u5230\u7EC8\u7AEF\u7EE7\u7EED\u3002</p>
|
|
24
|
+
</body>
|
|
25
|
+
</html>`;
|
|
26
|
+
function createState() {
|
|
27
|
+
return randomBytes(16).toString("hex");
|
|
28
|
+
}
|
|
29
|
+
function decodeJwtPayload(token) {
|
|
30
|
+
try {
|
|
31
|
+
const parts = token.split(".");
|
|
32
|
+
if (parts.length !== 3) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const payload = parts[1] ?? "";
|
|
36
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
37
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
38
|
+
const decoded = Buffer.from(normalized + padding, "base64").toString("utf8");
|
|
39
|
+
return JSON.parse(decoded);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseAuthorizationInput(value) {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(trimmed);
|
|
51
|
+
return {
|
|
52
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
53
|
+
state: url.searchParams.get("state") ?? void 0
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
if (trimmed.includes("#")) {
|
|
58
|
+
const [code, state] = trimmed.split("#", 2);
|
|
59
|
+
return { code, state };
|
|
60
|
+
}
|
|
61
|
+
if (trimmed.includes("code=")) {
|
|
62
|
+
const params = new URLSearchParams(trimmed);
|
|
63
|
+
return {
|
|
64
|
+
code: params.get("code") ?? void 0,
|
|
65
|
+
state: params.get("state") ?? void 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { code: trimmed };
|
|
69
|
+
}
|
|
70
|
+
function extractProfile(accessToken, refreshToken, expires) {
|
|
71
|
+
const payload = decodeJwtPayload(accessToken);
|
|
72
|
+
const authClaim = payload?.[JWT_CLAIM_PATH];
|
|
73
|
+
const accountId = authClaim?.chatgpt_account_id;
|
|
74
|
+
if (typeof accountId !== "string" || !accountId.trim()) {
|
|
75
|
+
throw new Error("\u65E0\u6CD5\u4ECE access token \u4E2D\u63D0\u53D6 accountId\u3002");
|
|
76
|
+
}
|
|
77
|
+
const email = typeof payload?.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
|
|
78
|
+
const profileKey = email ?? accountId;
|
|
79
|
+
return {
|
|
80
|
+
provider: "openai-codex",
|
|
81
|
+
profileId: `openai-codex:${profileKey}`,
|
|
82
|
+
mode: "oauth_account",
|
|
83
|
+
access: accessToken,
|
|
84
|
+
refresh: refreshToken,
|
|
85
|
+
expires,
|
|
86
|
+
accountId,
|
|
87
|
+
email
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function exchangeAuthorizationCode(code, verifier) {
|
|
91
|
+
const response = await requestText({
|
|
92
|
+
method: "POST",
|
|
93
|
+
url: TOKEN_URL,
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
96
|
+
},
|
|
97
|
+
body: new URLSearchParams({
|
|
98
|
+
grant_type: "authorization_code",
|
|
99
|
+
client_id: CLIENT_ID,
|
|
100
|
+
code,
|
|
101
|
+
code_verifier: verifier,
|
|
102
|
+
redirect_uri: REDIRECT_URI
|
|
103
|
+
}).toString()
|
|
104
|
+
});
|
|
105
|
+
if (response.status < 200 || response.status >= 300) {
|
|
106
|
+
throw new Error(`\u6388\u6743\u7801\u6362 token \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
|
|
107
|
+
}
|
|
108
|
+
const json = JSON.parse(response.body);
|
|
109
|
+
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
110
|
+
throw new Error("token \u54CD\u5E94\u7F3A\u5C11 access_token / refresh_token / expires_in\u3002");
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
access: json.access_token,
|
|
114
|
+
refresh: json.refresh_token,
|
|
115
|
+
expires: Date.now() + json.expires_in * 1e3
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function refreshOpenAICodexToken(profile) {
|
|
119
|
+
const response = await requestText({
|
|
120
|
+
method: "POST",
|
|
121
|
+
url: TOKEN_URL,
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
124
|
+
},
|
|
125
|
+
body: new URLSearchParams({
|
|
126
|
+
grant_type: "refresh_token",
|
|
127
|
+
refresh_token: profile.refresh,
|
|
128
|
+
client_id: CLIENT_ID
|
|
129
|
+
}).toString()
|
|
130
|
+
});
|
|
131
|
+
if (response.status < 200 || response.status >= 300) {
|
|
132
|
+
throw new Error(`\u5237\u65B0 token \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
|
|
133
|
+
}
|
|
134
|
+
const json = JSON.parse(response.body);
|
|
135
|
+
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
136
|
+
throw new Error("\u5237\u65B0\u54CD\u5E94\u7F3A\u5C11 access_token / refresh_token / expires_in\u3002");
|
|
137
|
+
}
|
|
138
|
+
return extractProfile(
|
|
139
|
+
json.access_token,
|
|
140
|
+
json.refresh_token,
|
|
141
|
+
Date.now() + json.expires_in * 1e3
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function tryOpenBrowser(url) {
|
|
145
|
+
try {
|
|
146
|
+
if (process.platform === "darwin") {
|
|
147
|
+
const child2 = spawn("open", [url], { stdio: "ignore", detached: true });
|
|
148
|
+
child2.unref();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
const child2 = spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true });
|
|
153
|
+
child2.unref();
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const child = spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
157
|
+
child.unref();
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function promptLine(message) {
|
|
164
|
+
const rl = readline.createInterface({ input, output });
|
|
165
|
+
try {
|
|
166
|
+
return (await rl.question(message)).trim();
|
|
167
|
+
} finally {
|
|
168
|
+
rl.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function startLocalCallbackServer(expectedState) {
|
|
172
|
+
let lastCode = null;
|
|
173
|
+
let closed = false;
|
|
174
|
+
const server = http.createServer((req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const url = new URL(req.url || "", "http://127.0.0.1");
|
|
177
|
+
if (url.pathname !== "/auth/callback") {
|
|
178
|
+
res.statusCode = 404;
|
|
179
|
+
res.end("Not found");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const state = url.searchParams.get("state");
|
|
183
|
+
const code = url.searchParams.get("code");
|
|
184
|
+
if (state !== expectedState) {
|
|
185
|
+
res.statusCode = 400;
|
|
186
|
+
res.end("State mismatch");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!code) {
|
|
190
|
+
res.statusCode = 400;
|
|
191
|
+
res.end("Missing authorization code");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
lastCode = code;
|
|
195
|
+
res.statusCode = 200;
|
|
196
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
197
|
+
res.end(SUCCESS_HTML);
|
|
198
|
+
} catch {
|
|
199
|
+
res.statusCode = 500;
|
|
200
|
+
res.end("Internal error");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
await new Promise((resolve) => {
|
|
204
|
+
server.listen(1455, "127.0.0.1", () => resolve());
|
|
205
|
+
server.on("error", () => resolve());
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
close: () => {
|
|
209
|
+
if (closed) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
closed = true;
|
|
213
|
+
server.close();
|
|
214
|
+
},
|
|
215
|
+
waitForCode: async () => {
|
|
216
|
+
for (let index = 0; index < 600; index += 1) {
|
|
217
|
+
if (lastCode) {
|
|
218
|
+
return lastCode;
|
|
219
|
+
}
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function requestManualCode(expectedState) {
|
|
227
|
+
const manual = await promptLine("\u6CA1\u6709\u81EA\u52A8\u56DE\u8C03\uFF0C\u8BF7\u7C98\u8D34\u5B8C\u6574\u56DE\u8C03 URL \u6216 code: ");
|
|
228
|
+
const parsed = parseAuthorizationInput(manual);
|
|
229
|
+
if (parsed.state && parsed.state !== expectedState) {
|
|
230
|
+
throw new Error("state \u4E0D\u5339\u914D\uFF0C\u5DF2\u62D2\u7EDD\u672C\u6B21\u6388\u6743\u7ED3\u679C\u3002");
|
|
231
|
+
}
|
|
232
|
+
if (!parsed.code) {
|
|
233
|
+
throw new Error("\u6CA1\u6709\u89E3\u6790\u51FA authorization code\u3002");
|
|
234
|
+
}
|
|
235
|
+
return parsed.code;
|
|
236
|
+
}
|
|
237
|
+
async function loginOpenAICodex() {
|
|
238
|
+
const { verifier, challenge } = await generatePKCE();
|
|
239
|
+
const state = createState();
|
|
240
|
+
const authorizeUrl = new URL(AUTHORIZE_URL);
|
|
241
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
242
|
+
authorizeUrl.searchParams.set("client_id", CLIENT_ID);
|
|
243
|
+
authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
244
|
+
authorizeUrl.searchParams.set("scope", SCOPE);
|
|
245
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
246
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
247
|
+
authorizeUrl.searchParams.set("state", state);
|
|
248
|
+
authorizeUrl.searchParams.set("id_token_add_organizations", "true");
|
|
249
|
+
authorizeUrl.searchParams.set("codex_cli_simplified_flow", "true");
|
|
250
|
+
authorizeUrl.searchParams.set("originator", "pi");
|
|
251
|
+
const callbackServer = await startLocalCallbackServer(state);
|
|
252
|
+
const url = authorizeUrl.toString();
|
|
253
|
+
console.log("\u5F00\u59CB OpenAI Codex OAuth \u767B\u5F55\u3002");
|
|
254
|
+
console.log(`\u56DE\u8C03\u5730\u5740: ${REDIRECT_URI}`);
|
|
255
|
+
console.log(`\u6388\u6743\u5730\u5740: ${url}`);
|
|
256
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
|
|
257
|
+
console.log("\u68C0\u6D4B\u5230\u4EE3\u7406\u73AF\u5883\u53D8\u91CF\uFF0Ctoken \u4EA4\u6362\u4F1A\u590D\u7528\u5F53\u524D\u7EC8\u7AEF\u4EE3\u7406\u3002");
|
|
258
|
+
} else {
|
|
259
|
+
console.log("\u5F53\u524D\u672A\u68C0\u6D4B\u5230 HTTP_PROXY / HTTPS_PROXY\u3002");
|
|
260
|
+
}
|
|
261
|
+
if (process.env.OAUTH_DEMO_USE_CURL === "1") {
|
|
262
|
+
console.log("\u5DF2\u542F\u7528 curl-only \u6A21\u5F0F\u8FDB\u884C token \u8BF7\u6C42\u3002");
|
|
263
|
+
}
|
|
264
|
+
const opened = tryOpenBrowser(url);
|
|
265
|
+
if (opened) {
|
|
266
|
+
console.log("\u5DF2\u5C1D\u8BD5\u6253\u5F00\u6D4F\u89C8\u5668\u3002");
|
|
267
|
+
} else {
|
|
268
|
+
console.log("\u672A\u80FD\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00\u4E0A\u9762\u7684\u6388\u6743\u5730\u5740\u3002");
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const code = await callbackServer.waitForCode() ?? await requestManualCode(state);
|
|
272
|
+
console.log("\u5DF2\u6536\u5230\u6388\u6743\u56DE\u8C03\uFF0C\u6B63\u5728\u4EA4\u6362 access token...");
|
|
273
|
+
const token = await exchangeAuthorizationCode(code, verifier);
|
|
274
|
+
console.log("token \u4EA4\u6362\u6210\u529F\uFF0C\u6B63\u5728\u89E3\u6790\u8D26\u53F7\u4FE1\u606F...");
|
|
275
|
+
return extractProfile(token.access, token.refresh, token.expires);
|
|
276
|
+
} finally {
|
|
277
|
+
callbackServer.close();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
loginOpenAICodex,
|
|
282
|
+
refreshOpenAICodexToken
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/core/providers/openai-codex/oauth.ts"],"sourcesContent":["import http from \"node:http\";\nimport { randomBytes } from \"node:crypto\";\nimport { spawn } from \"node:child_process\";\nimport readline from \"node:readline/promises\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport type { OAuthProfile } from \"../../types.js\";\nimport { requestText } from \"../http-client.js\";\nimport { generatePKCE } from \"./pkce.js\";\n\nconst CLIENT_ID = \"app_EMoamEEZ73f0CkXaXp7hrann\";\nconst AUTHORIZE_URL = \"https://auth.openai.com/oauth/authorize\";\nconst TOKEN_URL = \"https://auth.openai.com/oauth/token\";\nconst REDIRECT_URI = \"http://localhost:1455/auth/callback\";\nconst SCOPE = \"openid profile email offline_access\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\";\nconst SUCCESS_HTML = `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>OAuth Success</title>\n</head>\n<body>\n <p>登录成功,请回到终端继续。</p>\n</body>\n</html>`;\n\ntype AuthorizationResult = {\n code?: string;\n state?: string;\n};\n\ntype TokenResult = {\n access: string;\n refresh: string;\n expires: number;\n};\n\nfunction createState(): string {\n return randomBytes(16).toString(\"hex\");\n}\n\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n const payload = parts[1] ?? \"\";\n const normalized = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = normalized.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (normalized.length % 4));\n const decoded = Buffer.from(normalized + padding, \"base64\").toString(\"utf8\");\n return JSON.parse(decoded) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction parseAuthorizationInput(value: string): AuthorizationResult {\n const trimmed = value.trim();\n if (!trimmed) {\n return {};\n }\n\n try {\n const url = new URL(trimmed);\n return {\n code: url.searchParams.get(\"code\") ?? undefined,\n state: url.searchParams.get(\"state\") ?? undefined,\n };\n } catch {\n // ignore\n }\n\n if (trimmed.includes(\"#\")) {\n const [code, state] = trimmed.split(\"#\", 2);\n return { code, state };\n }\n\n if (trimmed.includes(\"code=\")) {\n const params = new URLSearchParams(trimmed);\n return {\n code: params.get(\"code\") ?? undefined,\n state: params.get(\"state\") ?? undefined,\n };\n }\n\n return { code: trimmed };\n}\n\nfunction extractProfile(accessToken: string, refreshToken: string, expires: number): OAuthProfile {\n const payload = decodeJwtPayload(accessToken);\n const authClaim = payload?.[JWT_CLAIM_PATH] as Record<string, unknown> | undefined;\n const accountId = authClaim?.chatgpt_account_id;\n if (typeof accountId !== \"string\" || !accountId.trim()) {\n throw new Error(\"无法从 access token 中提取 accountId。\");\n }\n\n const email = typeof payload?.email === \"string\" && payload.email.trim() ? payload.email.trim() : undefined;\n const profileKey = email ?? accountId;\n\n return {\n provider: \"openai-codex\",\n profileId: `openai-codex:${profileKey}`,\n mode: \"oauth_account\",\n access: accessToken,\n refresh: refreshToken,\n expires,\n accountId,\n email,\n };\n}\n\nasync function exchangeAuthorizationCode(code: string, verifier: string): Promise<TokenResult> {\n const response = await requestText({\n method: \"POST\",\n url: TOKEN_URL,\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: REDIRECT_URI,\n }).toString(),\n });\n\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`授权码换 token 失败: HTTP ${response.status} via ${response.transport} ${response.body}`);\n }\n\n const json = JSON.parse(response.body) as {\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n };\n\n if (!json.access_token || !json.refresh_token || typeof json.expires_in !== \"number\") {\n throw new Error(\"token 响应缺少 access_token / refresh_token / expires_in。\");\n }\n\n return {\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + json.expires_in * 1000,\n };\n}\n\nexport async function refreshOpenAICodexToken(profile: OAuthProfile): Promise<OAuthProfile> {\n const response = await requestText({\n method: \"POST\",\n url: TOKEN_URL,\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: profile.refresh,\n client_id: CLIENT_ID,\n }).toString(),\n });\n\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`刷新 token 失败: HTTP ${response.status} via ${response.transport} ${response.body}`);\n }\n\n const json = JSON.parse(response.body) as {\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n };\n\n if (!json.access_token || !json.refresh_token || typeof json.expires_in !== \"number\") {\n throw new Error(\"刷新响应缺少 access_token / refresh_token / expires_in。\");\n }\n\n return extractProfile(\n json.access_token,\n json.refresh_token,\n Date.now() + json.expires_in * 1000,\n );\n}\n\nfunction tryOpenBrowser(url: string): boolean {\n try {\n if (process.platform === \"darwin\") {\n const child = spawn(\"open\", [url], { stdio: \"ignore\", detached: true });\n child.unref();\n return true;\n }\n\n if (process.platform === \"win32\") {\n const child = spawn(\"cmd\", [\"/c\", \"start\", \"\", url], { stdio: \"ignore\", detached: true });\n child.unref();\n return true;\n }\n\n const child = spawn(\"xdg-open\", [url], { stdio: \"ignore\", detached: true });\n child.unref();\n return true;\n } catch {\n return false;\n }\n}\n\nasync function promptLine(message: string): Promise<string> {\n const rl = readline.createInterface({ input, output });\n try {\n return (await rl.question(message)).trim();\n } finally {\n rl.close();\n }\n}\n\nasync function startLocalCallbackServer(expectedState: string): Promise<{\n close: () => void;\n waitForCode: () => Promise<string | null>;\n}> {\n let lastCode: string | null = null;\n let closed = false;\n\n const server = http.createServer((req, res) => {\n try {\n const url = new URL(req.url || \"\", \"http://127.0.0.1\");\n if (url.pathname !== \"/auth/callback\") {\n res.statusCode = 404;\n res.end(\"Not found\");\n return;\n }\n\n const state = url.searchParams.get(\"state\");\n const code = url.searchParams.get(\"code\");\n if (state !== expectedState) {\n res.statusCode = 400;\n res.end(\"State mismatch\");\n return;\n }\n\n if (!code) {\n res.statusCode = 400;\n res.end(\"Missing authorization code\");\n return;\n }\n\n lastCode = code;\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n res.end(SUCCESS_HTML);\n } catch {\n res.statusCode = 500;\n res.end(\"Internal error\");\n }\n });\n\n await new Promise<void>((resolve) => {\n server.listen(1455, \"127.0.0.1\", () => resolve());\n server.on(\"error\", () => resolve());\n });\n\n return {\n close: () => {\n if (closed) {\n return;\n }\n closed = true;\n server.close();\n },\n waitForCode: async () => {\n for (let index = 0; index < 600; index += 1) {\n if (lastCode) {\n return lastCode;\n }\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n return null;\n },\n };\n}\n\nasync function requestManualCode(expectedState: string): Promise<string> {\n const manual = await promptLine(\"没有自动回调,请粘贴完整回调 URL 或 code: \");\n const parsed = parseAuthorizationInput(manual);\n if (parsed.state && parsed.state !== expectedState) {\n throw new Error(\"state 不匹配,已拒绝本次授权结果。\");\n }\n if (!parsed.code) {\n throw new Error(\"没有解析出 authorization code。\");\n }\n return parsed.code;\n}\n\nexport async function loginOpenAICodex(): Promise<OAuthProfile> {\n const { verifier, challenge } = await generatePKCE();\n const state = createState();\n const authorizeUrl = new URL(AUTHORIZE_URL);\n\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", CLIENT_ID);\n authorizeUrl.searchParams.set(\"redirect_uri\", REDIRECT_URI);\n authorizeUrl.searchParams.set(\"scope\", SCOPE);\n authorizeUrl.searchParams.set(\"code_challenge\", challenge);\n authorizeUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authorizeUrl.searchParams.set(\"state\", state);\n authorizeUrl.searchParams.set(\"id_token_add_organizations\", \"true\");\n authorizeUrl.searchParams.set(\"codex_cli_simplified_flow\", \"true\");\n authorizeUrl.searchParams.set(\"originator\", \"pi\");\n\n const callbackServer = await startLocalCallbackServer(state);\n const url = authorizeUrl.toString();\n\n console.log(\"开始 OpenAI Codex OAuth 登录。\");\n console.log(`回调地址: ${REDIRECT_URI}`);\n console.log(`授权地址: ${url}`);\n if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {\n console.log(\"检测到代理环境变量,token 交换会复用当前终端代理。\");\n } else {\n console.log(\"当前未检测到 HTTP_PROXY / HTTPS_PROXY。\");\n }\n if (process.env.OAUTH_DEMO_USE_CURL === \"1\") {\n console.log(\"已启用 curl-only 模式进行 token 请求。\");\n }\n\n const opened = tryOpenBrowser(url);\n if (opened) {\n console.log(\"已尝试打开浏览器。\");\n } else {\n console.log(\"未能自动打开浏览器,请手动打开上面的授权地址。\");\n }\n\n try {\n const code = (await callbackServer.waitForCode()) ?? (await requestManualCode(state));\n console.log(\"已收到授权回调,正在交换 access token...\");\n const token = await exchangeAuthorizationCode(code, verifier);\n console.log(\"token 交换成功,正在解析账号信息...\");\n return extractProfile(token.access, token.refresh, token.expires);\n } finally {\n callbackServer.close();\n }\n}\n"],"mappings":"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","names":["child"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
function base64urlEncode(bytes) {
|
|
3
|
+
let binary = "";
|
|
4
|
+
for (const byte of bytes) {
|
|
5
|
+
binary += String.fromCharCode(byte);
|
|
6
|
+
}
|
|
7
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
8
|
+
}
|
|
9
|
+
async function generatePKCE() {
|
|
10
|
+
const verifierBytes = new Uint8Array(32);
|
|
11
|
+
crypto.getRandomValues(verifierBytes);
|
|
12
|
+
const verifier = base64urlEncode(verifierBytes);
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
|
|
15
|
+
const challenge = base64urlEncode(new Uint8Array(hashBuffer));
|
|
16
|
+
return { verifier, challenge };
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
generatePKCE
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/core/providers/openai-codex/pkce.ts"],"sourcesContent":["function base64urlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\nexport async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {\n const verifierBytes = new Uint8Array(32);\n crypto.getRandomValues(verifierBytes);\n const verifier = base64urlEncode(verifierBytes);\n\n const encoder = new TextEncoder();\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoder.encode(verifier));\n const challenge = base64urlEncode(new Uint8Array(hashBuffer));\n\n return { verifier, challenge };\n}\n"],"mappings":";AAAA,SAAS,gBAAgB,OAA2B;AAClD,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,cAAU,OAAO,aAAa,IAAI;AAAA,EACpC;AACA,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,MAAM,EAAE;AAC9E;AAEA,eAAsB,eAAiE;AACrF,QAAM,gBAAgB,IAAI,WAAW,EAAE;AACvC,SAAO,gBAAgB,aAAa;AACpC,QAAM,WAAW,gBAAgB,aAAa;AAE9C,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,QAAQ,OAAO,QAAQ,CAAC;AACjF,QAAM,YAAY,gBAAgB,IAAI,WAAW,UAAU,CAAC;AAE5D,SAAO,EAAE,UAAU,UAAU;AAC/B;","names":[]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
clearStore,
|
|
4
|
+
getActiveProfile,
|
|
5
|
+
saveProfile
|
|
6
|
+
} from "../store/profile-store.js";
|
|
7
|
+
import {
|
|
8
|
+
loginOpenAICodex,
|
|
9
|
+
refreshOpenAICodexToken
|
|
10
|
+
} from "../providers/openai-codex/oauth.js";
|
|
11
|
+
class AuthService {
|
|
12
|
+
constructor(configService) {
|
|
13
|
+
this.configService = configService;
|
|
14
|
+
}
|
|
15
|
+
async login(provider) {
|
|
16
|
+
if (provider !== "openai-codex") {
|
|
17
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
18
|
+
}
|
|
19
|
+
const profile = await loginOpenAICodex();
|
|
20
|
+
await saveProfile(profile);
|
|
21
|
+
return {
|
|
22
|
+
...profile,
|
|
23
|
+
mode: "oauth_account"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async getActiveProfile(provider = "openai-codex") {
|
|
27
|
+
const profile = await getActiveProfile();
|
|
28
|
+
if (!profile || profile.provider !== provider) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...profile,
|
|
33
|
+
mode: "oauth_account"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async requireUsableProfile(provider = "openai-codex") {
|
|
37
|
+
const profile = await this.getActiveProfile(provider);
|
|
38
|
+
if (!profile) {
|
|
39
|
+
throw new Error(`\u8FD8\u6CA1\u6709\u767B\u5F55 ${provider}\u3002\u5148\u8FD0\u884C bun src/cli.js login`);
|
|
40
|
+
}
|
|
41
|
+
if (Date.now() < profile.expires) {
|
|
42
|
+
return profile;
|
|
43
|
+
}
|
|
44
|
+
const refreshed = await refreshOpenAICodexToken(profile);
|
|
45
|
+
await saveProfile(refreshed);
|
|
46
|
+
return {
|
|
47
|
+
...refreshed,
|
|
48
|
+
mode: "oauth_account"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async logoutAll() {
|
|
52
|
+
await clearStore();
|
|
53
|
+
}
|
|
54
|
+
async getStatus() {
|
|
55
|
+
const profile = await this.getActiveProfile();
|
|
56
|
+
const defaultModel = await this.configService.getDefaultModel();
|
|
57
|
+
const server = await this.configService.getServerConfig();
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
activeProvider: profile?.provider,
|
|
61
|
+
activeProfileId: profile?.profileId,
|
|
62
|
+
defaultModel,
|
|
63
|
+
loggedIn: Boolean(profile),
|
|
64
|
+
expiresAt: profile?.expires,
|
|
65
|
+
serverHost: server.host,
|
|
66
|
+
serverPort: server.port
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
AuthService
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=auth-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/services/auth-service.ts"],"sourcesContent":["import {\n clearStore,\n getActiveProfile,\n saveProfile,\n} from \"../store/profile-store.js\";\nimport type { GatewayStatus, OAuthProfile, ProviderId } from \"../types.js\";\nimport {\n loginOpenAICodex,\n refreshOpenAICodexToken,\n} from \"../providers/openai-codex/oauth.js\";\nimport { ConfigService } from \"./config-service.js\";\n\nexport class AuthService {\n constructor(private readonly configService: ConfigService) {}\n\n async login(provider: ProviderId): Promise<OAuthProfile> {\n if (provider !== \"openai-codex\") {\n throw new Error(`暂不支持 provider: ${provider}`);\n }\n\n const profile = await loginOpenAICodex();\n await saveProfile(profile);\n return {\n ...profile,\n mode: \"oauth_account\",\n };\n }\n\n async getActiveProfile(provider: ProviderId = \"openai-codex\"): Promise<OAuthProfile | null> {\n const profile = await getActiveProfile();\n if (!profile || profile.provider !== provider) {\n return null;\n }\n\n return {\n ...profile,\n mode: \"oauth_account\",\n };\n }\n\n async requireUsableProfile(provider: ProviderId = \"openai-codex\"): Promise<OAuthProfile> {\n const profile = await this.getActiveProfile(provider);\n if (!profile) {\n throw new Error(`还没有登录 ${provider}。先运行 bun src/cli.js login`);\n }\n\n if (Date.now() < profile.expires) {\n return profile;\n }\n\n const refreshed = await refreshOpenAICodexToken(profile);\n await saveProfile(refreshed);\n return {\n ...refreshed,\n mode: \"oauth_account\",\n };\n }\n\n async logoutAll(): Promise<void> {\n await clearStore();\n }\n\n async getStatus(): Promise<GatewayStatus> {\n const profile = await this.getActiveProfile();\n const defaultModel = await this.configService.getDefaultModel();\n const server = await this.configService.getServerConfig();\n return {\n ok: true,\n activeProvider: profile?.provider,\n activeProfileId: profile?.profileId,\n defaultModel,\n loggedIn: Boolean(profile),\n expiresAt: profile?.expires,\n serverHost: server.host,\n serverPort: server.port,\n };\n }\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGA,MAAM,YAAY;AAAA,EACvB,YAA6B,eAA8B;AAA9B;AAAA,EAA+B;AAAA,EAE5D,MAAM,MAAM,UAA6C;AACvD,QAAI,aAAa,gBAAgB;AAC/B,YAAM,IAAI,MAAM,sCAAkB,QAAQ,EAAE;AAAA,IAC9C;AAEA,UAAM,UAAU,MAAM,iBAAiB;AACvC,UAAM,YAAY,OAAO;AACzB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,WAAuB,gBAA8C;AAC1F,UAAM,UAAU,MAAM,iBAAiB;AACvC,QAAI,CAAC,WAAW,QAAQ,aAAa,UAAU;AAC7C,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,WAAuB,gBAAuC;AACvF,UAAM,UAAU,MAAM,KAAK,iBAAiB,QAAQ;AACpD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,kCAAS,QAAQ,+CAA2B;AAAA,IAC9D;AAEA,QAAI,KAAK,IAAI,IAAI,QAAQ,SAAS;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,MAAM,wBAAwB,OAAO;AACvD,UAAM,YAAY,SAAS;AAC3B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,YAA2B;AAC/B,UAAM,WAAW;AAAA,EACnB;AAAA,EAEA,MAAM,YAAoC;AACxC,UAAM,UAAU,MAAM,KAAK,iBAAiB;AAC5C,UAAM,eAAe,MAAM,KAAK,cAAc,gBAAgB;AAC9D,UAAM,SAAS,MAAM,KAAK,cAAc,gBAAgB;AACxD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,gBAAgB,SAAS;AAAA,MACzB,iBAAiB,SAAS;AAAA,MAC1B;AAAA,MACA,UAAU,QAAQ,OAAO;AAAA,MACzB,WAAW,SAAS;AAAA,MACpB,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
|