init
This commit is contained in:
commit
90525cdfd8
22 files changed
+9590
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import { Context } from "hono"
|
||||
import { contentJson, OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
import { ChannelConfigRow } from "../db/db_model";
|
||||
|
||||
// 获取所有 Channel 配置
|
||||
export class ChannelGetEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get all channel configurations',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.array(z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const result = await c.env.DB.prepare(
|
||||
"SELECT * FROM channel_config"
|
||||
).all<ChannelConfigRow>();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.results
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建或更新 Channel 配置
|
||||
export class ChannelUpsertEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Create or update channel configuration',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Channel key'),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
name: z.string().describe('Channel name'),
|
||||
type: z.string().describe('Channel type'),
|
||||
endpoint: z.string().describe('API endpoint'),
|
||||
api_key: z.string().describe('API key'),
|
||||
api_version: z.string().describe('API version'),
|
||||
deployment_mapper: z.record(z.string()).describe('Model deployment mapping'),
|
||||
model_pricing: z.record(z.object({
|
||||
input: z.number().describe('Input token price per 1000 tokens'),
|
||||
output: z.number().describe('Output token price per 1000 tokens'),
|
||||
})).optional().describe('Custom model pricing for this channel'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
const config = await c.req.json<ChannelConfig>();
|
||||
|
||||
// Upsert channel config directly using SQL
|
||||
// excluded.value 指的是 INSERT 语句中要插入的新值
|
||||
// 当发生冲突时,用新值更新现有记录
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO channel_config (key, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = datetime('now')`
|
||||
).bind(key, JSON.stringify(config)).run();
|
||||
|
||||
if (!result.success) {
|
||||
return c.text('Failed to upsert channel config', 500);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: true
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 Channel 配置
|
||||
export class ChannelDeleteEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Delete channel configuration',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Channel key'),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Delete channel config directly using SQL
|
||||
const result = await c.env.DB.prepare(
|
||||
`DELETE FROM channel_config WHERE key = ?`
|
||||
).bind(key).run();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.success
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Context } from "hono"
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import db from "../db"
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
export class DBInitializeEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.string()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
await db.initialize(c);
|
||||
await db.migrate(c);
|
||||
return {
|
||||
success: true,
|
||||
data: await db.getVersion(c)
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Context, Hono, Next } from "hono"
|
||||
import { fromHono } from 'chanfana';
|
||||
import { DBInitializeEndpoint } from "./db_api"
|
||||
import {
|
||||
ChannelGetEndpoint, ChannelUpsertEndpoint, ChannelDeleteEndpoint
|
||||
} from "./channel_api"
|
||||
import {
|
||||
TokenListEndpoint, TokenUpsertEndpoint, TokenDeleteEndpoint
|
||||
} from "./token_api"
|
||||
import {
|
||||
PricingGetEndpoint, PricingUpdateEndpoint
|
||||
} from "./pricing_api"
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
export const api = fromHono(app)
|
||||
|
||||
// Authentication Middleware - using environment variable
|
||||
app.use('/api/admin/*', async (c, next) => {
|
||||
const token = c.req.header('x-admin-token');
|
||||
const adminToken = c.env.ADMIN_TOKEN;
|
||||
|
||||
if (!token || !adminToken || token !== adminToken) {
|
||||
return c.text("Unauthorized", 401);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
api.post("/api/admin/db_initialize", DBInitializeEndpoint)
|
||||
|
||||
api.get("/api/admin/channel", ChannelGetEndpoint)
|
||||
api.post("/api/admin/channel/:key", ChannelUpsertEndpoint)
|
||||
api.delete("/api/admin/channel/:key", ChannelDeleteEndpoint)
|
||||
|
||||
// Token management routes
|
||||
api.get("/api/admin/token", TokenListEndpoint)
|
||||
api.post("/api/admin/token/:key", TokenUpsertEndpoint)
|
||||
api.delete("/api/admin/token/:key", TokenDeleteEndpoint)
|
||||
|
||||
// Pricing management routes
|
||||
api.get("/api/admin/pricing", PricingGetEndpoint)
|
||||
api.post("/api/admin/pricing", PricingUpdateEndpoint)
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Context } from "hono";
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
import { getJsonSetting, saveSetting } from "../utils";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
// Pricing 获取配置 API
|
||||
export class PricingGetEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get global pricing configuration',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.record(z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const pricingConfig = await getJsonSetting<Record<string, ModelPricing>>(
|
||||
c,
|
||||
CONSTANTS.MODEL_PRICING_KEY
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: pricingConfig || {}
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Pricing 更新配置 API
|
||||
export class PricingUpdateEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Update global pricing configuration',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.record(z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const body = await c.req.json<Record<string, ModelPricing>>();
|
||||
|
||||
await saveSetting(
|
||||
c,
|
||||
CONSTANTS.MODEL_PRICING_KEY,
|
||||
JSON.stringify(body)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Pricing config updated successfully"
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Context } from "hono"
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
// Token 列表 API
|
||||
export class TokenListEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get all tokens',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.array(z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
usage: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM api_token ORDER BY created_at DESC`
|
||||
).all<ApiTokenRow>();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.results
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Token 创建/更新 API (Upsert)
|
||||
export class TokenUpsertEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Create or update a token',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Token key'),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
name: z.string().describe('Token name'),
|
||||
channel_keys: z.array(z.string()).describe('Channel keys to bind (empty array means access to all channels)'),
|
||||
total_quota: z.number().describe('Total quota amount'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const body = await c.req.json<ApiTokenData>();
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Validate channels exist using batch query (if channel_keys is not empty)
|
||||
if (body.channel_keys && body.channel_keys.length > 0) {
|
||||
const channelQuery = body.channel_keys.map(() => '?').join(',');
|
||||
const existingChannels = await c.env.DB.prepare(
|
||||
`SELECT key FROM channel_config WHERE key IN (${channelQuery})`
|
||||
).bind(...body.channel_keys).all();
|
||||
|
||||
if (!existingChannels.results || existingChannels.results.length !== body.channel_keys.length) {
|
||||
const existingKeys = existingChannels.results?.map((row: any) => row.key) || [];
|
||||
const missingKeys = body.channel_keys.filter(key => !existingKeys.includes(key));
|
||||
return c.text(`Channels not found: ${missingKeys.join(', ')}`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert token directly using SQL with ON CONFLICT
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO api_token (key, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = datetime('now')`
|
||||
).bind(key, JSON.stringify(body)).run();
|
||||
|
||||
if (!result.success) {
|
||||
return c.text('Failed to upsert token', 500);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: true
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Token 删除 API
|
||||
export class TokenDeleteEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Delete token',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Token key'),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Delete token directly using SQL
|
||||
const result = await c.env.DB.prepare(
|
||||
`DELETE FROM api_token WHERE key = ?`
|
||||
).bind(key).run();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.success
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
// Token 工具类
|
||||
export class TokenUtils {
|
||||
static async updateUsage(c: Context<HonoCustomType>, key: string, usageAmount: number): Promise<boolean> {
|
||||
try {
|
||||
const result = await c.env.DB.prepare(
|
||||
`UPDATE api_token SET usage = usage + ?, updated_at = datetime('now') WHERE key = ?`
|
||||
).bind(usageAmount, key).run();
|
||||
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('Error updating token usage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user