@southwind-ai/config 0.1.1 → 0.1.3
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/index.ts +1 -0
- package/package.json +3 -2
- package/src/ai-openai-compatible.ts +131 -0
- package/src/platform.ts +28 -1
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@southwind-ai/config",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.ts",
|
|
11
|
+
"src/ai-openai-compatible.ts",
|
|
11
12
|
"src/**/*.ts",
|
|
12
13
|
"!src/**/*.test.ts"
|
|
13
14
|
],
|
|
14
15
|
"dependencies": {
|
|
15
|
-
"@southwind-ai/shared": "0.1.
|
|
16
|
+
"@southwind-ai/shared": "0.1.2",
|
|
16
17
|
"zod": "^4.4.3"
|
|
17
18
|
},
|
|
18
19
|
"exports": {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineModuleConfig } from "./definition.js";
|
|
4
|
+
import {
|
|
5
|
+
loadModuleConfigSync,
|
|
6
|
+
strictInteger,
|
|
7
|
+
type ConfigEnvironment,
|
|
8
|
+
} from "./environment-loader.js";
|
|
9
|
+
import { EnvironmentSecretResolver } from "./secrets.js";
|
|
10
|
+
|
|
11
|
+
export interface OpenAiCompatibleDeploymentConfig {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
model: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
connectTimeoutMs: number;
|
|
16
|
+
requestTimeoutMs: number;
|
|
17
|
+
maxResponseBytes: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const deploymentSchema: z.ZodType<OpenAiCompatibleDeploymentConfig> =
|
|
21
|
+
z.strictObject({
|
|
22
|
+
baseUrl: z.string().transform(normalizeOpenAiCompatibleBaseUrl),
|
|
23
|
+
model: z.string().trim().min(1).max(200),
|
|
24
|
+
apiKey: z.string().trim().min(1).optional(),
|
|
25
|
+
connectTimeoutMs: z.number().int().min(100).max(30_000),
|
|
26
|
+
requestTimeoutMs: z.number().int().min(1_000).max(300_000),
|
|
27
|
+
maxResponseBytes: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.min(1_024)
|
|
31
|
+
.max(16 * 1_024 * 1_024),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const openAiCompatibleDeploymentConfigDefinition =
|
|
35
|
+
defineModuleConfig<OpenAiCompatibleDeploymentConfig>({
|
|
36
|
+
key: "ai.openai-compatible",
|
|
37
|
+
label: "OpenAI-compatible 模型部署",
|
|
38
|
+
ownedEnvironmentPrefixes: ["QWEN_", "AI_PROVIDER_"],
|
|
39
|
+
schema: deploymentSchema,
|
|
40
|
+
environment: [
|
|
41
|
+
{ key: "QWEN_API_URL", path: "baseUrl" },
|
|
42
|
+
{ key: "QWEN_MODEL", path: "model" },
|
|
43
|
+
{
|
|
44
|
+
key: "QWEN_API_KEY",
|
|
45
|
+
path: "apiKey",
|
|
46
|
+
secret: { provider: "env" },
|
|
47
|
+
},
|
|
48
|
+
integer("AI_PROVIDER_CONNECT_TIMEOUT_MS", "connectTimeoutMs", 5_000),
|
|
49
|
+
integer("AI_PROVIDER_REQUEST_TIMEOUT_MS", "requestTimeoutMs", 60_000),
|
|
50
|
+
integer(
|
|
51
|
+
"AI_PROVIDER_MAX_RESPONSE_BYTES",
|
|
52
|
+
"maxResponseBytes",
|
|
53
|
+
2 * 1_024 * 1_024,
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export function loadOpenAiCompatibleDeploymentConfig(
|
|
59
|
+
environment: ConfigEnvironment = process.env,
|
|
60
|
+
): OpenAiCompatibleDeploymentConfig {
|
|
61
|
+
return loadModuleConfigSync(
|
|
62
|
+
openAiCompatibleDeploymentConfigDefinition,
|
|
63
|
+
environment,
|
|
64
|
+
new EnvironmentSecretResolver(environment),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeOpenAiCompatibleBaseUrl(raw: string): string {
|
|
69
|
+
let url: URL;
|
|
70
|
+
try {
|
|
71
|
+
url = new URL(raw.trim());
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error("OpenAI-compatible Endpoint 无效");
|
|
74
|
+
}
|
|
75
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
76
|
+
throw new Error("OpenAI-compatible Endpoint 只允许 HTTP 或 HTTPS");
|
|
77
|
+
}
|
|
78
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
79
|
+
throw new Error("OpenAI-compatible Endpoint 不允许凭据、查询串或片段");
|
|
80
|
+
}
|
|
81
|
+
const hostname = url.hostname.toLowerCase();
|
|
82
|
+
if (isCloudMetadataHost(hostname)) {
|
|
83
|
+
throw new Error("OpenAI-compatible Endpoint 禁止访问云 metadata");
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol === "http:" && !isPrivateHost(hostname)) {
|
|
86
|
+
throw new Error("公网 OpenAI-compatible Endpoint 必须使用 HTTPS");
|
|
87
|
+
}
|
|
88
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
89
|
+
return url.toString().replace(/\/$/, "");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function integer(key: string, path: string, defaultValue: number) {
|
|
93
|
+
return { key, path, defaultValue, decode: strictInteger };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isPrivateHost(hostname: string): boolean {
|
|
97
|
+
if (
|
|
98
|
+
hostname === "localhost" ||
|
|
99
|
+
hostname.endsWith(".localhost") ||
|
|
100
|
+
hostname.endsWith(".internal") ||
|
|
101
|
+
hostname.endsWith(".local") ||
|
|
102
|
+
(!hostname.includes(".") && isIP(hostname) === 0)
|
|
103
|
+
) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (isIP(hostname) === 4) {
|
|
107
|
+
const [first = 0, second = 0] = hostname.split(".").map(Number);
|
|
108
|
+
return (
|
|
109
|
+
first === 10 ||
|
|
110
|
+
first === 127 ||
|
|
111
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
112
|
+
(first === 192 && second === 168)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (isIP(hostname) === 6) {
|
|
116
|
+
return (
|
|
117
|
+
hostname === "::1" ||
|
|
118
|
+
hostname.startsWith("fc") ||
|
|
119
|
+
hostname.startsWith("fd")
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isCloudMetadataHost(hostname: string): boolean {
|
|
126
|
+
return (
|
|
127
|
+
hostname === "169.254.169.254" ||
|
|
128
|
+
hostname === "metadata.google.internal" ||
|
|
129
|
+
hostname === "metadata.azure.internal"
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/platform.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DatabaseDialect,
|
|
3
|
+
LicenseCatalogDefinition,
|
|
4
|
+
} from "@southwind-ai/shared";
|
|
5
|
+
import {
|
|
6
|
+
parseLicenseCatalog,
|
|
7
|
+
parseLicenseCatalogJson,
|
|
8
|
+
} from "@southwind-ai/shared";
|
|
2
9
|
import { z } from "zod";
|
|
3
10
|
import { defineModuleConfig, ModuleConfigRegistry } from "./definition.js";
|
|
4
11
|
import {
|
|
@@ -33,6 +40,8 @@ export interface PlatformConfig {
|
|
|
33
40
|
publicKeyId?: string;
|
|
34
41
|
publicKey?: string;
|
|
35
42
|
machineCode?: string;
|
|
43
|
+
/** 宿主自定义 License 版本目录;未配置时回退 shared 默认三级目录。 */
|
|
44
|
+
versions?: LicenseCatalogDefinition[];
|
|
36
45
|
};
|
|
37
46
|
audit: { enabled: boolean; redactFields: string[] };
|
|
38
47
|
compatibility: {
|
|
@@ -75,6 +84,20 @@ const platformSchema: z.ZodType<PlatformConfig> = z.strictObject({
|
|
|
75
84
|
publicKeyId: z.string().trim().min(1).optional(),
|
|
76
85
|
publicKey: z.string().trim().min(1).optional(),
|
|
77
86
|
machineCode: z.string().trim().min(1).optional(),
|
|
87
|
+
versions: z
|
|
88
|
+
.unknown()
|
|
89
|
+
.transform((value, context) => {
|
|
90
|
+
try {
|
|
91
|
+
return parseLicenseCatalog(value);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
context.addIssue({
|
|
94
|
+
code: "custom",
|
|
95
|
+
message: error instanceof Error ? error.message : "版本目录无效",
|
|
96
|
+
});
|
|
97
|
+
return z.NEVER;
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
78
101
|
}),
|
|
79
102
|
audit: z.strictObject({
|
|
80
103
|
enabled: z.boolean(),
|
|
@@ -105,6 +128,7 @@ export const platformConfigDefinition = defineModuleConfig<PlatformConfig>({
|
|
|
105
128
|
"LICENSE_PUBLIC_KEY_ID",
|
|
106
129
|
"LICENSE_PUBLIC_KEY",
|
|
107
130
|
"LICENSE_MACHINE_CODE",
|
|
131
|
+
"WINDY_LICENSE_VERSIONS",
|
|
108
132
|
],
|
|
109
133
|
environment: [
|
|
110
134
|
value("PLATFORM_NAME", "name", "Windy Platform"),
|
|
@@ -133,6 +157,9 @@ export const platformConfigDefinition = defineModuleConfig<PlatformConfig>({
|
|
|
133
157
|
value("LICENSE_PUBLIC_KEY_ID", "license.publicKeyId"),
|
|
134
158
|
value("LICENSE_PUBLIC_KEY", "license.publicKey"),
|
|
135
159
|
secret("LICENSE_MACHINE_CODE", "license.machineCode"),
|
|
160
|
+
value("WINDY_LICENSE_VERSIONS", "license.versions", undefined, (raw) =>
|
|
161
|
+
parseLicenseCatalogJson(raw, "WINDY_LICENSE_VERSIONS"),
|
|
162
|
+
),
|
|
136
163
|
value("AUDIT_ENABLED", "audit.enabled", true, strictBoolean),
|
|
137
164
|
value(
|
|
138
165
|
"AUDIT_REDACT_FIELDS",
|