chore: initialize clean metapi repository

This commit is contained in:
cita
2026-02-28 00:36:28 +08:00
commit 2ecc23730d
199 changed files with 46078 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema.js';
import { config } from '../config.js';
import { mkdirSync } from 'fs';
import { dirname } from 'path';
const dbPath = `${config.dataDir}/hub.db`;
mkdirSync(dirname(dbPath), { recursive: true });
const sqlite = new Database(dbPath);
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
function tableColumnExists(table: string, column: string): boolean {
const rows = sqlite.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
return rows.some((row) => row.name === column);
}
function ensureTokenManagementSchema() {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS account_tokens (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
account_id integer NOT NULL,
name text NOT NULL,
token text NOT NULL,
source text DEFAULT 'manual',
enabled integer DEFAULT true,
is_default integer DEFAULT false,
created_at text DEFAULT (datetime('now')),
updated_at text DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE cascade
);
`);
if (!tableColumnExists('route_channels', 'token_id')) {
sqlite.exec('ALTER TABLE route_channels ADD COLUMN token_id integer;');
}
sqlite.exec(`
INSERT INTO account_tokens (account_id, name, token, source, enabled, is_default, created_at, updated_at)
SELECT
a.id,
'default',
a.api_token,
'legacy',
true,
true,
datetime('now'),
datetime('now')
FROM accounts AS a
WHERE
a.api_token IS NOT NULL
AND trim(a.api_token) <> ''
AND NOT EXISTS (
SELECT 1 FROM account_tokens AS t
WHERE t.account_id = a.id
AND t.token = a.api_token
);
`);
sqlite.exec(`
CREATE TABLE IF NOT EXISTS token_model_availability (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
token_id integer NOT NULL,
model_name text NOT NULL,
available integer,
latency_ms integer,
checked_at text DEFAULT (datetime('now')),
FOREIGN KEY (token_id) REFERENCES account_tokens(id) ON DELETE cascade
);
`);
sqlite.exec(`
CREATE UNIQUE INDEX IF NOT EXISTS token_model_availability_token_model_unique
ON token_model_availability(token_id, model_name);
`);
}
function ensureSiteStatusSchema() {
if (!tableColumnExists('sites', 'status')) {
sqlite.exec(`ALTER TABLE sites ADD COLUMN status text DEFAULT 'active';`);
}
sqlite.exec(`
UPDATE sites
SET status = lower(trim(status))
WHERE status IS NOT NULL
AND lower(trim(status)) IN ('active', 'disabled')
AND status != lower(trim(status));
`);
sqlite.exec(`
UPDATE sites
SET status = 'active'
WHERE status IS NULL
OR trim(status) = ''
OR lower(trim(status)) NOT IN ('active', 'disabled');
`);
}
ensureTokenManagementSchema();
ensureSiteStatusSchema();
export const db = drizzle(sqlite, { schema });
export { schema };
+15
View File
@@ -0,0 +1,15 @@
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
import { config } from '../config.js';
import { mkdirSync } from 'fs';
import { dirname } from 'path';
const dbPath = `${config.dataDir}/hub.db`;
mkdirSync(dirname(dbPath), { recursive: true });
const sqlite = new Database(dbPath);
const db = drizzle(sqlite);
migrate(db, { migrationsFolder: './drizzle' });
sqlite.close();
console.log('Migration complete.');
+176
View File
@@ -0,0 +1,176 @@
import { sqliteTable, text, integer, real, uniqueIndex, index } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const sites = sqliteTable('sites', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
url: text('url').notNull(),
platform: text('platform').notNull(), // 'new-api' | 'one-api' | 'veloera' | 'one-hub' | 'done-hub' | 'sub2api'
status: text('status').notNull().default('active'), // 'active' | 'disabled'
apiKey: text('api_key'),
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
}, (table) => ({
statusIdx: index('sites_status_idx').on(table.status),
}));
export const accounts = sqliteTable('accounts', {
id: integer('id').primaryKey({ autoIncrement: true }),
siteId: integer('site_id').notNull().references(() => sites.id, { onDelete: 'cascade' }),
username: text('username'),
accessToken: text('access_token').notNull(),
apiToken: text('api_token'),
balance: real('balance').default(0),
balanceUsed: real('balance_used').default(0),
quota: real('quota').default(0),
unitCost: real('unit_cost'),
valueScore: real('value_score').default(0),
status: text('status').default('active'), // 'active' | 'disabled' | 'expired'
checkinEnabled: integer('checkin_enabled', { mode: 'boolean' }).default(true),
lastCheckinAt: text('last_checkin_at'),
lastBalanceRefresh: text('last_balance_refresh'),
extraConfig: text('extra_config'), // JSON string
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
}, (table) => ({
siteIdIdx: index('accounts_site_id_idx').on(table.siteId),
statusIdx: index('accounts_status_idx').on(table.status),
siteStatusIdx: index('accounts_site_status_idx').on(table.siteId, table.status),
}));
export const accountTokens = sqliteTable('account_tokens', {
id: integer('id').primaryKey({ autoIncrement: true }),
accountId: integer('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
token: text('token').notNull(),
source: text('source').default('manual'), // 'manual' | 'sync' | 'legacy'
enabled: integer('enabled', { mode: 'boolean' }).default(true),
isDefault: integer('is_default', { mode: 'boolean' }).default(false),
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
}, (table) => ({
accountIdIdx: index('account_tokens_account_id_idx').on(table.accountId),
accountEnabledIdx: index('account_tokens_account_enabled_idx').on(table.accountId, table.enabled),
enabledIdx: index('account_tokens_enabled_idx').on(table.enabled),
}));
export const checkinLogs = sqliteTable('checkin_logs', {
id: integer('id').primaryKey({ autoIncrement: true }),
accountId: integer('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
status: text('status').notNull(), // 'success' | 'failed' | 'skipped'
message: text('message'),
reward: text('reward'),
createdAt: text('created_at').default(sql`(datetime('now'))`),
}, (table) => ({
accountCreatedIdx: index('checkin_logs_account_created_at_idx').on(table.accountId, table.createdAt),
createdAtIdx: index('checkin_logs_created_at_idx').on(table.createdAt),
statusIdx: index('checkin_logs_status_idx').on(table.status),
}));
export const modelAvailability = sqliteTable('model_availability', {
id: integer('id').primaryKey({ autoIncrement: true }),
accountId: integer('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
modelName: text('model_name').notNull(),
available: integer('available', { mode: 'boolean' }),
latencyMs: integer('latency_ms'),
checkedAt: text('checked_at').default(sql`(datetime('now'))`),
}, (table) => ({
accountModelUnique: uniqueIndex('model_availability_account_model_unique').on(table.accountId, table.modelName),
accountAvailableIdx: index('model_availability_account_available_idx').on(table.accountId, table.available),
modelNameIdx: index('model_availability_model_name_idx').on(table.modelName),
}));
export const tokenModelAvailability = sqliteTable('token_model_availability', {
id: integer('id').primaryKey({ autoIncrement: true }),
tokenId: integer('token_id').notNull().references(() => accountTokens.id, { onDelete: 'cascade' }),
modelName: text('model_name').notNull(),
available: integer('available', { mode: 'boolean' }),
latencyMs: integer('latency_ms'),
checkedAt: text('checked_at').default(sql`(datetime('now'))`),
}, (table) => ({
tokenModelUnique: uniqueIndex('token_model_availability_token_model_unique').on(table.tokenId, table.modelName),
tokenAvailableIdx: index('token_model_availability_token_available_idx').on(table.tokenId, table.available),
modelNameIdx: index('token_model_availability_model_name_idx').on(table.modelName),
availableIdx: index('token_model_availability_available_idx').on(table.available),
}));
export const tokenRoutes = sqliteTable('token_routes', {
id: integer('id').primaryKey({ autoIncrement: true }),
modelPattern: text('model_pattern').notNull(),
modelMapping: text('model_mapping'), // JSON
enabled: integer('enabled', { mode: 'boolean' }).default(true),
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
}, (table) => ({
modelPatternIdx: index('token_routes_model_pattern_idx').on(table.modelPattern),
enabledIdx: index('token_routes_enabled_idx').on(table.enabled),
}));
export const routeChannels = sqliteTable('route_channels', {
id: integer('id').primaryKey({ autoIncrement: true }),
routeId: integer('route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
accountId: integer('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
tokenId: integer('token_id').references(() => accountTokens.id, { onDelete: 'set null' }),
priority: integer('priority').default(0),
weight: integer('weight').default(10),
enabled: integer('enabled', { mode: 'boolean' }).default(true),
manualOverride: integer('manual_override', { mode: 'boolean' }).default(false),
successCount: integer('success_count').default(0),
failCount: integer('fail_count').default(0),
totalLatencyMs: integer('total_latency_ms').default(0),
totalCost: real('total_cost').default(0),
lastUsedAt: text('last_used_at'),
lastFailAt: text('last_fail_at'),
cooldownUntil: text('cooldown_until'),
}, (table) => ({
routeIdIdx: index('route_channels_route_id_idx').on(table.routeId),
accountIdIdx: index('route_channels_account_id_idx').on(table.accountId),
tokenIdIdx: index('route_channels_token_id_idx').on(table.tokenId),
routeEnabledIdx: index('route_channels_route_enabled_idx').on(table.routeId, table.enabled),
routeTokenIdx: index('route_channels_route_token_idx').on(table.routeId, table.tokenId),
}));
export const proxyLogs = sqliteTable('proxy_logs', {
id: integer('id').primaryKey({ autoIncrement: true }),
routeId: integer('route_id'),
channelId: integer('channel_id'),
accountId: integer('account_id'),
modelRequested: text('model_requested'),
modelActual: text('model_actual'),
status: text('status'), // 'success' | 'failed' | 'retried'
httpStatus: integer('http_status'),
latencyMs: integer('latency_ms'),
promptTokens: integer('prompt_tokens'),
completionTokens: integer('completion_tokens'),
totalTokens: integer('total_tokens'),
estimatedCost: real('estimated_cost'),
errorMessage: text('error_message'),
retryCount: integer('retry_count').default(0),
createdAt: text('created_at').default(sql`(datetime('now'))`),
}, (table) => ({
createdAtIdx: index('proxy_logs_created_at_idx').on(table.createdAt),
accountCreatedIdx: index('proxy_logs_account_created_at_idx').on(table.accountId, table.createdAt),
statusCreatedIdx: index('proxy_logs_status_created_at_idx').on(table.status, table.createdAt),
modelActualCreatedIdx: index('proxy_logs_model_actual_created_at_idx').on(table.modelActual, table.createdAt),
}));
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value'), // JSON
});
export const events = sqliteTable('events', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type').notNull(), // 'checkin' | 'balance' | 'token' | 'proxy' | 'status'
title: text('title').notNull(),
message: text('message'),
level: text('level').notNull().default('info'), // 'info' | 'warning' | 'error'
read: integer('read', { mode: 'boolean' }).default(false),
relatedId: integer('related_id'),
relatedType: text('related_type'), // 'account' | 'site' | 'route'
createdAt: text('created_at').default(sql`(datetime('now'))`),
}, (table) => ({
readCreatedIdx: index('events_read_created_at_idx').on(table.read, table.createdAt),
typeCreatedIdx: index('events_type_created_at_idx').on(table.type, table.createdAt),
createdAtIdx: index('events_created_at_idx').on(table.createdAt),
}));