chore: initialize clean metapi repository
This commit is contained in:
@@ -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 };
|
||||
@@ -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.');
|
||||
@@ -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),
|
||||
}));
|
||||
Reference in New Issue
Block a user