@templmf/temp-solf-lmf 0.0.43 → 0.0.45
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/ai-gateway/.env +42 -0
- package/ai-gateway/README.md +295 -0
- package/ai-gateway/package-lock.json +1370 -0
- package/ai-gateway/package.json +18 -0
- package/ai-gateway/src/index.js +132 -0
- package/ai-gateway/src/middleware/auth.js +45 -0
- package/ai-gateway/src/middleware/rateLimit.js +87 -0
- package/ai-gateway/src/routes/chat.js +657 -0
- package/ai-gateway/src/skills/detector.js +145 -0
- package/ai-gateway/src/skills/html.md +18 -0
- package/ai-gateway/src/skills/markdown.md +18 -0
- package/ai-gateway/src/skills/react.md +27 -0
- package/ai-gateway/src/skills/registry.js +441 -0
- package/ai-gateway/src/skills/skill-creator/LICENSE.txt +202 -0
- package/ai-gateway/src/skills/skill-creator/SKILL.md +485 -0
- package/ai-gateway/src/skills/skill-creator/agents/analyzer.md +274 -0
- package/ai-gateway/src/skills/skill-creator/agents/comparator.md +202 -0
- package/ai-gateway/src/skills/skill-creator/agents/grader.md +223 -0
- package/ai-gateway/src/skills/skill-creator/assets/eval_review.html +146 -0
- package/ai-gateway/src/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/ai-gateway/src/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/ai-gateway/src/skills/skill-creator/references/schemas.md +430 -0
- package/ai-gateway/src/skills/skill-creator/scripts/__init__.py +0 -0
- package/ai-gateway/src/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/ai-gateway/src/skills/skill-creator/scripts/generate_report.py +326 -0
- package/ai-gateway/src/skills/skill-creator/scripts/improve_description.py +247 -0
- package/ai-gateway/src/skills/skill-creator/scripts/package_skill.py +136 -0
- package/ai-gateway/src/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/ai-gateway/src/skills/skill-creator/scripts/run_eval.py +310 -0
- package/ai-gateway/src/skills/skill-creator/scripts/run_loop.py +328 -0
- package/ai-gateway/src/skills/skill-creator/scripts/utils.py +47 -0
- package/ai-gateway/src/skills/skill-creator/skill-creator.skill +0 -0
- package/ai-gateway/src/skills/ticket.md +36 -0
- package/ai-gateway/src/skills/vue.md +31 -0
- package/ai-gateway/src/utils/logger.js +21 -0
- package/ai-gateway/src/utils/retry.js +90 -0
- package/ai-gateway/src/utils/sessionManager.js +159 -0
- package/ai-gateway/src/utils/structuredResponse.js +144 -0
- package/ai-gateway/src/utils/toolAdapter.js +151 -0
- package/package.json +1 -1
- package//345/216/213/347/274/251/345/220/216/347/232/204/346/226/207/344/273/266.7z +0 -0
- package/skill-mcp/README.md +0 -74
- package/skill-mcp/index.ts +0 -336
- package/skill-mcp/package (1).json +0 -19
- package/skill-mcp/tsconfig.json +0 -16
- package//347/247/273/345/212/250/345/272/224/347/224/250/345/217/260/350/264/246/347/247/273/344/272/244/346/270/205/345/215/225.xlsx +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-gateway",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI API Gateway - 兼容 OpenAI 格式,支持内网 Qwen 模型",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/index.js",
|
|
9
|
+
"dev": "node --watch src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"express": "^4.18.2",
|
|
13
|
+
"openai": "^4.28.0",
|
|
14
|
+
"dotenv": "^16.4.1",
|
|
15
|
+
"uuid": "^9.0.0",
|
|
16
|
+
"winston": "^3.11.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Gateway - 入口
|
|
3
|
+
*
|
|
4
|
+
* 对外接口(全部兼容 OpenAI 格式):
|
|
5
|
+
*
|
|
6
|
+
* POST /v1/chat/completions 核心接口
|
|
7
|
+
* GET /v1/models 模型列表(VSCode Roo 等插件需要)
|
|
8
|
+
* GET /health 健康检查 + Skill / Session 状态
|
|
9
|
+
* DELETE /v1/sessions/:id 清除指定会话
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import "dotenv/config";
|
|
13
|
+
import express from "express";
|
|
14
|
+
import { authMiddleware } from "./middleware/auth.js";
|
|
15
|
+
import { rateLimitMiddleware } from "./middleware/rateLimit.js";
|
|
16
|
+
import { chatRouter } from "./routes/chat.js";
|
|
17
|
+
import { loadAllSkills, getSkillRegistry } from "./skills/registry.js";
|
|
18
|
+
import { getSessionStats, deleteSession } from "./utils/sessionManager.js";
|
|
19
|
+
import { logger } from "./utils/logger.js";
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
const PORT = process.env.PORT || 3000;
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────
|
|
25
|
+
// 全局中间件
|
|
26
|
+
// ─────────────────────────────────────────────────────
|
|
27
|
+
app.use(express.json({ limit: "10mb" }));
|
|
28
|
+
|
|
29
|
+
// 请求日志(跳过健康检查,避免刷屏)
|
|
30
|
+
app.use((req, res, next) => {
|
|
31
|
+
if (req.path !== "/health") {
|
|
32
|
+
logger.debug(`${req.method} ${req.path}`, { ip: req.ip });
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────
|
|
38
|
+
// 健康检查(无需鉴权)
|
|
39
|
+
// ─────────────────────────────────────────────────────
|
|
40
|
+
app.get("/health", (req, res) => {
|
|
41
|
+
res.json({
|
|
42
|
+
status: "ok",
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
upstream: {
|
|
45
|
+
baseUrl: process.env.UPSTREAM_BASE_URL,
|
|
46
|
+
defaultModel: process.env.DEFAULT_MODEL
|
|
47
|
+
},
|
|
48
|
+
skills: getSkillRegistry(),
|
|
49
|
+
sessions: getSessionStats()
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────
|
|
54
|
+
// 模型列表(VSCode Roo / Open WebUI 等需要)
|
|
55
|
+
// ─────────────────────────────────────────────────────
|
|
56
|
+
app.get("/v1/models", authMiddleware, (req, res) => {
|
|
57
|
+
const models = (process.env.AVAILABLE_MODELS || process.env.DEFAULT_MODEL || "qwen2.5-72b-instruct")
|
|
58
|
+
.split(",")
|
|
59
|
+
.map(m => m.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
|
|
62
|
+
res.json({
|
|
63
|
+
object: "list",
|
|
64
|
+
data: models.map(id => ({
|
|
65
|
+
id,
|
|
66
|
+
object: "model",
|
|
67
|
+
created: Math.floor(Date.now() / 1000),
|
|
68
|
+
owned_by: "local",
|
|
69
|
+
permission: [],
|
|
70
|
+
root: id,
|
|
71
|
+
parent: null
|
|
72
|
+
}))
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────
|
|
77
|
+
// 会话管理接口
|
|
78
|
+
// ─────────────────────────────────────────────────────
|
|
79
|
+
app.delete("/v1/sessions/:sessionId", authMiddleware, (req, res) => {
|
|
80
|
+
const { sessionId } = req.params;
|
|
81
|
+
const deleted = deleteSession(sessionId);
|
|
82
|
+
if (deleted) {
|
|
83
|
+
res.json({ deleted: true, session_id: sessionId });
|
|
84
|
+
} else {
|
|
85
|
+
res.status(404).json({
|
|
86
|
+
error: { message: "Session not found", type: "not_found_error" }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────────────────
|
|
92
|
+
// 核心接口:鉴权 → 限流 → 处理
|
|
93
|
+
// ─────────────────────────────────────────────────────
|
|
94
|
+
app.use("/v1/chat/completions", authMiddleware, rateLimitMiddleware, chatRouter);
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────
|
|
97
|
+
// 全局错误兜底
|
|
98
|
+
// ─────────────────────────────────────────────────────
|
|
99
|
+
app.use((err, req, res, _next) => {
|
|
100
|
+
logger.error("Unhandled error", { error: err.message, stack: err.stack });
|
|
101
|
+
res.status(500).json({
|
|
102
|
+
error: { message: "Internal server error", type: "server_error" }
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 404
|
|
107
|
+
app.use((req, res) => {
|
|
108
|
+
res.status(404).json({
|
|
109
|
+
error: { message: `Route ${req.method} ${req.path} not found`, type: "not_found_error" }
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────
|
|
114
|
+
// 启动
|
|
115
|
+
// ─────────────────────────────────────────────────────
|
|
116
|
+
const skillsDir = process.env.SKILLS_DIR || "./src/skills";
|
|
117
|
+
const { loaded, missing, total } = loadAllSkills(skillsDir);
|
|
118
|
+
|
|
119
|
+
app.listen(PORT, () => {
|
|
120
|
+
logger.info("═══════════════════════════════════════");
|
|
121
|
+
logger.info(" AI Gateway started");
|
|
122
|
+
logger.info("═══════════════════════════════════════");
|
|
123
|
+
logger.info(` Port : ${PORT}`);
|
|
124
|
+
logger.info(` Upstream : ${process.env.UPSTREAM_BASE_URL}`);
|
|
125
|
+
logger.info(` Default model: ${process.env.DEFAULT_MODEL}`);
|
|
126
|
+
logger.info(` Skills : ${loaded}/${total} loaded${missing > 0 ? ` (${missing} missing)` : ""}`);
|
|
127
|
+
logger.info("───────────────────────────────────────");
|
|
128
|
+
logger.info(` Health : http://localhost:${PORT}/health`);
|
|
129
|
+
logger.info(` API : http://localhost:${PORT}/v1/chat/completions`);
|
|
130
|
+
logger.info(` Models : http://localhost:${PORT}/v1/models`);
|
|
131
|
+
logger.info("═══════════════════════════════════════");
|
|
132
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 鉴权中间件
|
|
3
|
+
*
|
|
4
|
+
* 客户端(VSCode Roo、前端应用等)调用网关时需要携带 Gateway API Key。
|
|
5
|
+
* 支持两种传入方式(与 OpenAI 兼容):
|
|
6
|
+
* 1. Header: Authorization: Bearer gw-key-xxx
|
|
7
|
+
* 2. Header: x-api-key: gw-key-xxx
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function authMiddleware(req, res, next) {
|
|
11
|
+
// 从环境变量读取允许的 key 列表
|
|
12
|
+
const allowedKeys = (process.env.GATEWAY_API_KEYS || "")
|
|
13
|
+
.split(",")
|
|
14
|
+
.map(k => k.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
|
|
17
|
+
// 开发模式:未配置 key 则跳过鉴权
|
|
18
|
+
if (allowedKeys.length === 0) {
|
|
19
|
+
req.clientId = "anonymous";
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 从 Header 提取 key
|
|
24
|
+
const authHeader = req.headers["authorization"] || "";
|
|
25
|
+
const xApiKey = req.headers["x-api-key"] || "";
|
|
26
|
+
|
|
27
|
+
let clientKey = xApiKey;
|
|
28
|
+
if (!clientKey && authHeader.startsWith("Bearer ")) {
|
|
29
|
+
clientKey = authHeader.slice(7).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!clientKey || !allowedKeys.includes(clientKey)) {
|
|
33
|
+
return res.status(401).json({
|
|
34
|
+
error: {
|
|
35
|
+
message: "Invalid API key. Please check your GATEWAY_API_KEYS configuration.",
|
|
36
|
+
type: "authentication_error",
|
|
37
|
+
code: "invalid_api_key"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 把 key 的索引作为 clientId(用于日志追踪,不暴露原始 key)
|
|
43
|
+
req.clientId = `client-${allowedKeys.indexOf(clientKey)}`;
|
|
44
|
+
next();
|
|
45
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 限流中间件
|
|
3
|
+
*
|
|
4
|
+
* 基于内存的滑动窗口限流,无需 Redis 依赖。
|
|
5
|
+
* 生产环境建议换成 Redis 实现以支持多实例部署。
|
|
6
|
+
*
|
|
7
|
+
* 限流维度(按优先级):
|
|
8
|
+
* 1. clientId(API Key 级别)
|
|
9
|
+
* 2. IP 地址(兜底,防止未鉴权滥用)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// 滑动窗口记录:{ key -> [timestamp, ...] }
|
|
13
|
+
const windows = new Map();
|
|
14
|
+
|
|
15
|
+
// 定期清理过期记录,防止内存泄漏
|
|
16
|
+
setInterval(() => {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
for (const [key, timestamps] of windows.entries()) {
|
|
19
|
+
const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000");
|
|
20
|
+
const valid = timestamps.filter(t => now - t < windowMs);
|
|
21
|
+
if (valid.length === 0) {
|
|
22
|
+
windows.delete(key);
|
|
23
|
+
} else {
|
|
24
|
+
windows.set(key, valid);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, 30_000);
|
|
28
|
+
|
|
29
|
+
function isRateLimited(key, maxRequests, windowMs) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const timestamps = windows.get(key) || [];
|
|
32
|
+
|
|
33
|
+
// 过滤掉窗口外的记录
|
|
34
|
+
const valid = timestamps.filter(t => now - t < windowMs);
|
|
35
|
+
|
|
36
|
+
if (valid.length >= maxRequests) {
|
|
37
|
+
windows.set(key, valid);
|
|
38
|
+
return {
|
|
39
|
+
limited: true,
|
|
40
|
+
remaining: 0,
|
|
41
|
+
resetMs: windowMs - (now - valid[0])
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
valid.push(now);
|
|
46
|
+
windows.set(key, valid);
|
|
47
|
+
return {
|
|
48
|
+
limited: false,
|
|
49
|
+
remaining: maxRequests - valid.length,
|
|
50
|
+
resetMs: 0
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rateLimitMiddleware(req, res, next) {
|
|
55
|
+
const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000");
|
|
56
|
+
const maxPerClient = parseInt(process.env.RATE_LIMIT_MAX_PER_CLIENT || "100");
|
|
57
|
+
const maxPerIp = parseInt(process.env.RATE_LIMIT_MAX_PER_IP || "200");
|
|
58
|
+
|
|
59
|
+
// 优先按 clientId 限流(鉴权后才有 clientId)
|
|
60
|
+
const limitKey = req.clientId && req.clientId !== "anonymous"
|
|
61
|
+
? `client:${req.clientId}`
|
|
62
|
+
: `ip:${req.ip}`;
|
|
63
|
+
|
|
64
|
+
const max = req.clientId && req.clientId !== "anonymous"
|
|
65
|
+
? maxPerClient
|
|
66
|
+
: maxPerIp;
|
|
67
|
+
|
|
68
|
+
const result = isRateLimited(limitKey, max, windowMs);
|
|
69
|
+
|
|
70
|
+
// 标准限流响应头
|
|
71
|
+
res.setHeader("X-RateLimit-Limit", max);
|
|
72
|
+
res.setHeader("X-RateLimit-Remaining", result.remaining);
|
|
73
|
+
res.setHeader("X-RateLimit-Window-Ms", windowMs);
|
|
74
|
+
|
|
75
|
+
if (result.limited) {
|
|
76
|
+
res.setHeader("Retry-After", Math.ceil(result.resetMs / 1000));
|
|
77
|
+
return res.status(429).json({
|
|
78
|
+
error: {
|
|
79
|
+
message: `Rate limit exceeded. Try again in ${Math.ceil(result.resetMs / 1000)}s.`,
|
|
80
|
+
type: "rate_limit_error",
|
|
81
|
+
code: "rate_limit_exceeded"
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
next();
|
|
87
|
+
}
|