feat: implement macOS notarization and ad-hoc signing in afterSign script

feat: add run-server script to manage server execution and compatibility checks

feat: create main application logic for desktop with window management, server handling, and auto-updater

test: add unit tests for desktop runtime helpers

feat: implement desktop server environment configuration and health check

test: add tests for server configuration and desktop routes

feat: add native module guard for better-sqlite3 compatibility

test: add tests for native module guard functionality

test: add comprehensive tests for account verification feedback handling

feat: implement account verification feedback helpers

chore: add TypeScript configuration for desktop builds
This commit is contained in:
cita
2026-03-06 16:24:41 +08:00
parent acdc846b13
commit 2a810facb8
45 changed files with 6712 additions and 405 deletions
@@ -20,6 +20,21 @@ describe('accountHealthService', () => {
expect(health.state).toBe('unhealthy');
});
it('does not reuse expired session-token health for proxy-only accounts', () => {
const health = buildRuntimeHealthForAccount({
accountStatus: 'expired',
siteStatus: 'active',
extraConfig: null,
sessionCapable: false,
});
expect(health).toMatchObject({
state: 'unknown',
source: 'none',
});
expect(health.reason).not.toContain('访问令牌已过期');
});
it('returns stored runtime health from extra config when available', () => {
const health = buildRuntimeHealthForAccount({
accountStatus: 'active',
+2 -1
View File
@@ -86,6 +86,7 @@ export function buildRuntimeHealthForAccount(input: {
accountStatus?: string | null;
siteStatus?: string | null;
extraConfig?: string | null;
sessionCapable?: boolean;
}): RuntimeHealthInfo {
const accountStatus = (input.accountStatus || 'active').toLowerCase();
const siteStatus = (input.siteStatus || 'active').toLowerCase();
@@ -99,7 +100,7 @@ export function buildRuntimeHealthForAccount(input: {
};
}
if (accountStatus === 'expired') {
if (accountStatus === 'expired' && input.sessionCapable !== false) {
return {
state: 'unhealthy',
reason: '访问令牌已过期',
@@ -13,6 +13,7 @@ describe('databaseMigrationService', () => {
dialect: 'postgres',
connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
overwrite: true,
ssl: false,
});
});
@@ -24,6 +25,7 @@ describe('databaseMigrationService', () => {
expect(normalized.dialect).toBe('mysql');
expect(normalized.overwrite).toBe(true);
expect(normalized.ssl).toBe(false);
});
it('accepts sqlite file migration target path', () => {
@@ -37,6 +39,7 @@ describe('databaseMigrationService', () => {
dialect: 'sqlite',
connectionString: './data/target.db',
overwrite: false,
ssl: false,
});
});
@@ -58,5 +61,40 @@ describe('databaseMigrationService', () => {
const masked = maskConnectionString('postgres://admin:super-secret@db.example.com:5432/metapi');
expect(masked).toBe('postgres://admin:***@db.example.com:5432/metapi');
});
it('normalizes ssl boolean from input', () => {
const normalized = normalizeMigrationInput({
dialect: 'mysql',
connectionString: 'mysql://user:pass@tidb.example.com:4000/db',
ssl: true,
});
expect(normalized.ssl).toBe(true);
});
it('defaults ssl to false when not provided', () => {
const normalized = normalizeMigrationInput({
dialect: 'postgres',
connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
});
expect(normalized.ssl).toBe(false);
});
it('parses ssl from string values', () => {
const normalized = normalizeMigrationInput({
dialect: 'mysql',
connectionString: 'mysql://user:pass@host:3306/db',
ssl: '1',
});
expect(normalized.ssl).toBe(true);
});
it('parses ssl false from string "0"', () => {
const normalized = normalizeMigrationInput({
dialect: 'mysql',
connectionString: 'mysql://user:pass@host:3306/db',
ssl: '0',
});
expect(normalized.ssl).toBe(false);
});
});
@@ -11,12 +11,14 @@ export interface DatabaseMigrationInput {
dialect?: unknown;
connectionString?: unknown;
overwrite?: unknown;
ssl?: unknown;
}
export interface NormalizedDatabaseMigrationInput {
dialect: MigrationDialect;
connectionString: string;
overwrite: boolean;
ssl: boolean;
}
type BackupSnapshot = {
@@ -173,6 +175,7 @@ export function normalizeMigrationInput(input: DatabaseMigrationInput): Normaliz
dialect,
connectionString,
overwrite: input.overwrite === undefined ? true : asBoolean(input.overwrite, true),
ssl: asBoolean(input.ssl, false),
};
}
@@ -224,8 +227,12 @@ async function toBackupSnapshot(): Promise<BackupSnapshot> {
};
}
async function createPostgresClient(connectionString: string): Promise<SqlClient> {
const client = new pg.Client({ connectionString });
async function createPostgresClient(connectionString: string, ssl: boolean): Promise<SqlClient> {
const clientOptions: pg.ClientConfig = { connectionString };
if (ssl) {
clientOptions.ssl = { rejectUnauthorized: false };
}
const client = new pg.Client(clientOptions);
await client.connect();
return {
@@ -244,8 +251,12 @@ async function createPostgresClient(connectionString: string): Promise<SqlClient
};
}
async function createMySqlClient(connectionString: string): Promise<SqlClient> {
const connection = await mysql.createConnection(connectionString);
async function createMySqlClient(connectionString: string, ssl: boolean): Promise<SqlClient> {
const connectionOptions: mysql.ConnectionOptions = { uri: connectionString };
if (ssl) {
connectionOptions.ssl = { rejectUnauthorized: false };
}
const connection = await mysql.createConnection(connectionOptions);
return {
dialect: 'mysql',
@@ -291,8 +302,8 @@ async function createSqliteClient(connectionString: string): Promise<SqlClient>
}
async function createClient(input: NormalizedDatabaseMigrationInput): Promise<SqlClient> {
if (input.dialect === 'postgres') return createPostgresClient(input.connectionString);
if (input.dialect === 'mysql') return createMySqlClient(input.connectionString);
if (input.dialect === 'postgres') return createPostgresClient(input.connectionString, input.ssl);
if (input.dialect === 'mysql') return createMySqlClient(input.connectionString, input.ssl);
return createSqliteClient(input.connectionString);
}