Skip to content

Commit 4e075ac

Browse files
committed
feat(qoderProxyApi): 全量迁移 TypeScript(Node 24 原生 type stripping,零构建)
- 58 个 .js/.mjs 统一迁移为 .ts(git mv 保历史),import 后缀同步更新 - tsconfig strict + nodenext + noEmit + allowImportingTsExtensions;tsc --noEmit 0 错误 - core 的 28 个 JSDoc typedef 升级为原生 TS 类型(CanonicalRequest/ProviderEvent 联合/ProviderCapabilities/QoderProvider) - CanonicalMessage.role 收录 system(对齐 gateway 透传行为,wire shape 测试锁定) - 消融清理:删除测试侧 8 个冗余 ProviderErrorFields 接口及 cast(src 已声明字段) - devDeps 新增 typescript 5.9 + @types/node;运行时依赖保持为零,无构建步骤 - CI 新增 typecheck 步骤;package.json 0.2.1,engines >=24.21.0
1 parent 030be7e commit 4e075ac

72 files changed

Lines changed: 2417 additions & 1077 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
node-version: 24
1717
cache: pnpm
1818
- run: pnpm install --frozen-lockfile
19+
- name: Typecheck(tsc --noEmit,strict)
20+
run: pnpm typecheck
1921
- name: Lint(oxlint,0 警告基线)
2022
run: pnpm lint
2123
- name: Format check(oxfmt)

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ npm run start:cli # qodercli 子进程
4141
npm run start:cloud-agents # Cloud Agents(v0.1 已验证路径)
4242
4343
# 等价 CLI:
44-
node src/index.js --provider=gateway
45-
node src/index.js --provider=cli
46-
node src/index.js --provider=cloud-agents # 兼容 cloudAgents 写法
44+
node src/index.ts --provider=gateway
45+
node src/index.ts --provider=cli
46+
node src/index.ts --provider=cloud-agents # 兼容 cloudAgents 写法
4747
```
4848

4949
默认监听 `http://127.0.0.1:8320`(只绑定 loopback,可用 `HOST`/`PORT` 覆盖)。
@@ -155,7 +155,7 @@ npm test
155155

156156
```
157157
src/
158-
├── index.js / bootstrap.js 启动与 Provider 组装
158+
├── index.ts / bootstrap.ts 启动与 Provider 组装(TypeScript,Node 24 原生 type stripping)
159159
├── core/ Canonical Request/Event、Provider Contract、聚合、能力协商、错误码
160160
├── protocols/openai/ Chat / Responses / 请求转换 / 流式 wire
161161
├── protocols/anthropic/ Messages / 请求转换 / 流式 wire

‎package.json‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
11
{
22
"name": "qoder-proxy-api",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"private": true,
55
"description": "Local protocol gateway exposing OpenAI/Anthropic-compatible APIs over three Qoder providers (gateway / cli / cloud-agents).",
66
"license": "MIT",
77
"bin": {
8-
"qoder-proxy-api": "./src/index.js"
8+
"qoder-proxy-api": "./src/index.ts"
99
},
1010
"type": "module",
1111
"scripts": {
12-
"start": "node ./src/index.js --provider=gateway",
13-
"start:cli": "node ./src/index.js --provider=cli",
14-
"start:cloud-agents": "node ./src/index.js --provider=cloud-agents",
12+
"start": "node ./src/index.ts --provider=gateway",
13+
"start:cli": "node ./src/index.ts --provider=cli",
14+
"start:cloud-agents": "node ./src/index.ts --provider=cloud-agents",
1515
"test": "node --test",
16+
"typecheck": "tsc --noEmit",
1617
"lint": "oxlint .",
1718
"format": "oxfmt --write .",
1819
"format:check": "oxfmt --check .",
19-
"setup:cloudAgents": "node ./scripts/setupCloudAgents.mjs",
20-
"doctor": "node ./scripts/doctor.mjs"
20+
"setup:cloudAgents": "node ./scripts/setupCloudAgents.ts",
21+
"doctor": "node ./scripts/doctor.ts"
2122
},
2223
"devDependencies": {
24+
"@types/node": "^24.19.0",
2325
"oxfmt": "^0.70.0",
24-
"oxlint": "^1.85.0"
26+
"oxlint": "^1.85.0",
27+
"typescript": "^5.9.3"
2528
},
2629
"engines": {
27-
"node": ">=24.21.0 <25"
30+
"node": ">=24.21.0"
2831
},
2932
"packageManager": "pnpm@12.6.0"
3033
}

‎pnpm-lock.yaml‎

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { fileURLToPath } from "node:url";
66

77
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "src");
88

9-
function walk(dir, files = []) {
9+
function walk(dir: string, files: string[] = []): string[] {
1010
for (const entry of readdirSync(dir)) {
1111
const full = path.join(dir, entry);
1212
if (statSync(full).isDirectory()) walk(full, files);
13-
else if (full.endsWith(".js")) files.push(full);
13+
else if (full.endsWith(".ts")) files.push(full);
1414
}
1515
return files;
1616
}
1717

18-
function zoneOf(absPath) {
18+
function zoneOf(absPath: string): string {
1919
const norm = path.relative(root, absPath).split(path.sep).join("/");
2020
if (norm.startsWith("core")) return "core";
2121
if (norm.startsWith("protocols")) return "protocols";
@@ -47,7 +47,7 @@ console.log(violations.length ? violations.join("\n") : "ALL DEPENDENCY RULES PA
4747

4848
// server 层协议细节泄漏检查
4949
const leaks = [];
50-
for (const rel of ["server/server.js", "server/http.js", "server/sse.js"]) {
50+
for (const rel of ["server/server.ts", "server/http.ts", "server/sse.ts"]) {
5151
const text = readFileSync(path.join(root, rel), "utf8");
5252
// session_id/qoder_session_id 是下游请求的会话 key 字段(客户端 API 表面),
5353
// 不属于上游协议细节,故不在检查列表中。
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ if (existsSync(".env") && typeof process.loadEnvFile === "function") {
99
process.loadEnvFile(".env");
1010
}
1111

12-
const mark = (ok) => (ok ? "OK " : "MISSING");
12+
const mark = (ok: boolean): string => (ok ? "OK " : "MISSING");
1313

1414
console.log("qoder-proxy-api 配置体检(npm start 默认 gateway 链路)\n");
1515

@@ -24,7 +24,7 @@ const bin = process.env.QODER_CLI_BIN || "qodercli";
2424
const nodePath = process.env.QODER_CLI_NODE;
2525
let cliOk = false;
2626
let cliDetail = "";
27-
if (nodePath || bin.endsWith(".mjs") || bin.endsWith(".js")) {
27+
if (nodePath || bin.endsWith(".mjs") || bin.endsWith(".ts")) {
2828
const probe = spawnSync(nodePath || process.execPath, [bin, "--version"], { encoding: "utf8" });
2929
cliOk = probe.status === 0;
3030
cliDetail = cliOk
Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,24 @@ function maybeLoadEnv() {
1818
async function readToken() {
1919
if (process.env.QODER_ACCESS_TOKEN) return process.env.QODER_ACCESS_TOKEN.trim();
2020
if (!process.argv.includes("--stdin-token")) return "";
21-
const chunks = [];
21+
const chunks: Buffer[] = [];
2222
for await (const chunk of process.stdin) chunks.push(chunk);
2323
return Buffer.concat(chunks).toString("utf8").trim().split(/\r?\n/)[0]?.trim() || "";
2424
}
2525

26-
async function request(token, path, options = {}) {
26+
interface RequestOptions {
27+
method?: string;
28+
body?: unknown;
29+
}
30+
31+
// Qoder cloud API 返回体是动态 JSON:顶层或 data 字段里装实体数组。
32+
interface QoderEntity {
33+
id?: string;
34+
name?: string;
35+
model?: string;
36+
}
37+
38+
async function request(token: string, path: string, options: RequestOptions = {}) {
2739
const res = await fetch(`${BASE_URL}${path}`, {
2840
method: options.method || "GET",
2941
headers: {
@@ -41,13 +53,19 @@ async function request(token, path, options = {}) {
4153
return json;
4254
}
4355

44-
function dataArray(value) {
45-
if (Array.isArray(value)) return value;
46-
if (Array.isArray(value?.data)) return value.data;
56+
function dataArray(value: unknown): QoderEntity[] {
57+
if (Array.isArray(value)) return value as QoderEntity[];
58+
if (
59+
value !== null &&
60+
typeof value === "object" &&
61+
Array.isArray((value as Record<string, unknown>).data)
62+
) {
63+
return (value as { data: QoderEntity[] }).data;
64+
}
4765
return [];
4866
}
4967

50-
async function getOrCreateEnvironment(token) {
68+
async function getOrCreateEnvironment(token: string) {
5169
const existing = dataArray(await request(token, "/environments"));
5270
const env = existing.find((item) => item?.name === ENV_NAME) || existing[0];
5371
if (env?.id) return { id: env.id, name: env.name || ENV_NAME, created: false };
@@ -58,7 +76,7 @@ async function getOrCreateEnvironment(token) {
5876
return { id: created.id || created?.data?.id, name: created.name || ENV_NAME, created: true };
5977
}
6078

61-
async function getOrCreateAgent(token) {
79+
async function getOrCreateAgent(token: string) {
6280
const existing = dataArray(await request(token, "/agents"));
6381
const agent = existing.find((item) => item?.name === AGENT_NAME) || existing[0];
6482
if (agent?.id)
@@ -86,7 +104,7 @@ async function getOrCreateAgent(token) {
86104
};
87105
}
88106

89-
function requireId(label, value) {
107+
function requireId(label: string, value: unknown) {
90108
if (!value) throw new Error(`missing ${label} in Qoder response`);
91109
return value;
92110
}

src/bootstrap.js renamed to src/bootstrap.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,89 @@
11
// 启动组装:--provider 参数 → Provider 实例 + 注册表 + 路由表。
22
// 默认 provider 是 gateway(任务要求);显式路由之外的模型名直通默认
33
// provider 并由其模型解析器裁决(这是路由策略,不是故障 fallback)。
4-
import { GatewayProvider } from "./providers/gateway/index.js";
5-
import { CliProvider } from "./providers/cli/index.js";
6-
import { CloudAgentsClient, SessionStore } from "./providers/cloudAgents/client.js";
7-
import { CloudAgentsProvider } from "./providers/cloudAgents/index.js";
8-
import { loadCliConfig } from "./providers/cli/config.js";
9-
import { GATEWAY_REGIONS } from "./providers/gateway/auth.js";
10-
import { createProviderRegistry } from "./routing/providerRegistry.js";
11-
import { parseModelRoutes } from "./routing/modelRouter.js";
12-
import { loadConfig } from "./config.js";
4+
import { GatewayProvider } from "./providers/gateway/index.ts";
5+
import { CliProvider } from "./providers/cli/index.ts";
6+
import { CloudAgentsClient, SessionStore } from "./providers/cloudAgents/client.ts";
7+
import { CloudAgentsProvider } from "./providers/cloudAgents/index.ts";
8+
import { loadCliConfig } from "./providers/cli/config.ts";
9+
import { GATEWAY_REGIONS } from "./providers/gateway/auth.ts";
10+
import { createProviderRegistry } from "./routing/providerRegistry.ts";
11+
import { parseModelRoutes } from "./routing/modelRouter.ts";
12+
import { loadConfig } from "./config.ts";
13+
import type { AppConfig } from "./config.ts";
14+
import type { QoderProvider } from "./core/provider.ts";
15+
import type { ProviderRegistry } from "./routing/providerRegistry.ts";
16+
import type { ModelRoutes } from "./routing/modelRouter.ts";
1317

14-
export const PROVIDER_IDS = ["gateway", "cli", "cloudAgents"];
18+
export const PROVIDER_IDS = ["gateway", "cli", "cloudAgents"] as const;
1519

16-
export function resolveProviderId(argv = process.argv, env = process.env) {
17-
const normalize = (value) => (value === "cloud-agents" ? "cloudAgents" : value);
20+
export type ProviderId = (typeof PROVIDER_IDS)[number];
21+
22+
export function resolveProviderId(
23+
argv: readonly string[] = process.argv,
24+
env: NodeJS.ProcessEnv = process.env,
25+
): ProviderId {
26+
const normalize = (value: string): string => (value === "cloud-agents" ? "cloudAgents" : value);
1827
for (const arg of argv) {
1928
const match = String(arg).match(/^--provider=(.+)$/);
2029
if (match) {
2130
const id = normalize(match[1].trim());
22-
if (!PROVIDER_IDS.includes(id)) {
31+
if (!(PROVIDER_IDS as readonly string[]).includes(id)) {
2332
throw new Error(`unknown provider "${match[1]}"; valid: gateway, cli, cloud-agents`);
2433
}
25-
return id;
34+
return id as ProviderId;
2635
}
2736
}
2837
const fromEnv = String(env.QODER_DEFAULT_PROVIDER || "").trim();
2938
if (fromEnv) {
3039
const id = normalize(fromEnv);
31-
if (!PROVIDER_IDS.includes(id)) {
40+
if (!(PROVIDER_IDS as readonly string[]).includes(id)) {
3241
throw new Error(
3342
`unknown QODER_DEFAULT_PROVIDER "${fromEnv}"; valid: gateway, cli, cloud-agents`,
3443
);
3544
}
36-
return id;
45+
return id as ProviderId;
3746
}
3847
return "gateway";
3948
}
4049

41-
export function createApp({ providerId, env = process.env, config = loadConfig(env) } = {}) {
50+
export interface CreateAppOptions {
51+
providerId?: ProviderId;
52+
env?: NodeJS.ProcessEnv;
53+
config?: AppConfig;
54+
}
55+
56+
export interface CreateAppResult {
57+
config: AppConfig;
58+
registry: ProviderRegistry;
59+
modelRoutes: ModelRoutes;
60+
defaultProviderId: ProviderId | undefined;
61+
}
62+
63+
export function createApp({
64+
providerId,
65+
env = process.env,
66+
config = loadConfig(env),
67+
}: CreateAppOptions = {}): CreateAppResult {
4268
const registry = createProviderRegistry();
4369

70+
// Provider 类的 id 字段被声明为拓宽的 string;运行时即注册 id 本身,
71+
// 在组装点按 QoderProvider 契约收窄(纯编译期,无行为变化)。
4472
if (providerId === "gateway") {
73+
const regionKey = (env.QODER_GATEWAY_REGION || "cn") as keyof typeof GATEWAY_REGIONS;
4574
registry.register(
4675
new GatewayProvider({
4776
pat: env.QODER_GATEWAY_PAT,
48-
region: GATEWAY_REGIONS[env.QODER_GATEWAY_REGION || "cn"] || GATEWAY_REGIONS.cn,
77+
region: GATEWAY_REGIONS[regionKey] || GATEWAY_REGIONS.cn,
4978
requestTimeoutMs: config.requestTimeoutMs,
5079
modelCacheTtlMs: Number(env.QODER_GATEWAY_MODEL_CACHE_TTL_MS) || 600000,
5180
defaultModel: env.QODER_GATEWAY_DEFAULT_MODEL || "",
52-
}),
81+
}) as unknown as QoderProvider,
5382
);
5483
} else if (providerId === "cli") {
55-
registry.register(new CliProvider({ cliConfig: loadCliConfig(env) }));
84+
registry.register(
85+
new CliProvider({ cliConfig: loadCliConfig(env) }) as unknown as QoderProvider,
86+
);
5687
} else if (providerId === "cloudAgents") {
5788
const client = new CloudAgentsClient({
5889
baseUrl: config.qoderApiBaseUrl,
@@ -65,7 +96,7 @@ export function createApp({ providerId, env = process.env, config = loadConfig(e
6596
sessionStore: new SessionStore({ ttlMs: config.sessionTtlMs }),
6697
modelMap: config.modelMap,
6798
archiveEphemeralSessions: config.archiveEphemeralSessions,
68-
}),
99+
}) as unknown as QoderProvider,
69100
);
70101
}
71102

0 commit comments

Comments
 (0)