archicore 0.2.0 → 0.2.1
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/dist/cli/commands/interactive.js +51 -19
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +66 -1
- package/dist/server/routes/admin.js +149 -1
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/github.js +17 -4
- package/dist/server/services/audit-service.d.ts +88 -0
- package/dist/server/services/audit-service.js +380 -0
- package/dist/server/services/auth-service.d.ts +11 -5
- package/dist/server/services/auth-service.js +299 -52
- package/dist/server/services/cache.d.ts +77 -0
- package/dist/server/services/cache.js +245 -0
- package/dist/server/services/database.d.ts +43 -0
- package/dist/server/services/database.js +221 -0
- package/package.json +17 -2
|
@@ -444,7 +444,7 @@ async function handleIndexCommand() {
|
|
|
444
444
|
async function handleAnalyzeCommand(args) {
|
|
445
445
|
if (!state.projectId) {
|
|
446
446
|
printError('No project selected');
|
|
447
|
-
printInfo('Use /
|
|
447
|
+
printInfo('Use /index first');
|
|
448
448
|
return;
|
|
449
449
|
}
|
|
450
450
|
const description = args.join(' ') || 'General analysis';
|
|
@@ -465,15 +465,20 @@ async function handleAnalyzeCommand(args) {
|
|
|
465
465
|
const data = await response.json();
|
|
466
466
|
spinner.succeed('Analysis complete');
|
|
467
467
|
printSection('Impact Analysis');
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const affected = impact.affectedNodes || [];
|
|
468
|
+
// Handle various response formats
|
|
469
|
+
const impact = data.impact || data.result || data || {};
|
|
470
|
+
const affected = impact.affectedNodes || impact.affected || impact.nodes || [];
|
|
471
471
|
console.log(` ${colors.highlight('Affected Components:')} ${affected.length}`);
|
|
472
|
+
if (affected.length === 0) {
|
|
473
|
+
printInfo('No components affected by this change');
|
|
474
|
+
showTokenUsage();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
472
477
|
const byLevel = {
|
|
473
|
-
critical: affected.filter((n) => n.impactLevel === 'critical'),
|
|
474
|
-
high: affected.filter((n) => n.impactLevel === 'high'),
|
|
475
|
-
medium: affected.filter((n) => n.impactLevel === 'medium'),
|
|
476
|
-
low: affected.filter((n) => n.impactLevel === 'low'),
|
|
478
|
+
critical: affected.filter((n) => n.impactLevel === 'critical' || n.level === 'critical'),
|
|
479
|
+
high: affected.filter((n) => n.impactLevel === 'high' || n.level === 'high'),
|
|
480
|
+
medium: affected.filter((n) => n.impactLevel === 'medium' || n.level === 'medium'),
|
|
481
|
+
low: affected.filter((n) => n.impactLevel === 'low' || n.level === 'low'),
|
|
477
482
|
};
|
|
478
483
|
if (byLevel.critical.length > 0) {
|
|
479
484
|
console.log(` ${colors.critical(`${icons.severityCritical} Critical: ${byLevel.critical.length}`)}`);
|
|
@@ -487,27 +492,47 @@ async function handleAnalyzeCommand(args) {
|
|
|
487
492
|
if (byLevel.low.length > 0) {
|
|
488
493
|
console.log(` ${colors.low(`${icons.severityLow} Low: ${byLevel.low.length}`)}`);
|
|
489
494
|
}
|
|
495
|
+
// Show top affected files
|
|
496
|
+
if (affected.length > 0) {
|
|
497
|
+
console.log();
|
|
498
|
+
console.log(` ${colors.highlight('Top affected:')}`);
|
|
499
|
+
for (const node of affected.slice(0, 5)) {
|
|
500
|
+
const name = node.name || node.symbol || 'unknown';
|
|
501
|
+
const file = node.filePath || node.file || '';
|
|
502
|
+
const level = node.impactLevel || node.level || 'unknown';
|
|
503
|
+
const levelColor = level === 'critical' ? colors.critical :
|
|
504
|
+
level === 'high' ? colors.high :
|
|
505
|
+
level === 'medium' ? colors.medium : colors.muted;
|
|
506
|
+
console.log(` ${levelColor(`[${level}]`)} ${name}`);
|
|
507
|
+
if (file)
|
|
508
|
+
console.log(` ${colors.dim(file)}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
490
511
|
// Risks
|
|
491
|
-
|
|
512
|
+
const risks = impact.risks || [];
|
|
513
|
+
if (risks.length > 0) {
|
|
492
514
|
console.log();
|
|
493
515
|
console.log(` ${colors.highlight('Risks:')}`);
|
|
494
|
-
for (const risk of
|
|
495
|
-
|
|
516
|
+
for (const risk of risks.slice(0, 5)) {
|
|
517
|
+
const desc = risk.description || risk.message || risk;
|
|
518
|
+
console.log(` ${colors.warning(icons.warning)} ${desc}`);
|
|
496
519
|
}
|
|
497
520
|
}
|
|
498
521
|
// Recommendations
|
|
499
|
-
|
|
522
|
+
const recommendations = impact.recommendations || [];
|
|
523
|
+
if (recommendations.length > 0) {
|
|
500
524
|
console.log();
|
|
501
525
|
console.log(` ${colors.highlight('Recommendations:')}`);
|
|
502
|
-
for (const rec of
|
|
503
|
-
|
|
526
|
+
for (const rec of recommendations.slice(0, 3)) {
|
|
527
|
+
const desc = rec.description || rec.message || rec;
|
|
528
|
+
console.log(` ${colors.info(icons.info)} ${desc}`);
|
|
504
529
|
}
|
|
505
530
|
}
|
|
506
531
|
showTokenUsage();
|
|
507
532
|
}
|
|
508
533
|
catch (error) {
|
|
509
534
|
spinner.fail('Analysis failed');
|
|
510
|
-
|
|
535
|
+
printFormattedError(error, { operation: 'Impact analysis' });
|
|
511
536
|
}
|
|
512
537
|
}
|
|
513
538
|
async function handleSearchCommand(query) {
|
|
@@ -923,13 +948,20 @@ async function handleRulesCommand() {
|
|
|
923
948
|
console.log();
|
|
924
949
|
for (const v of violations.slice(0, 10)) {
|
|
925
950
|
const severity = v.severity || 'warning';
|
|
926
|
-
const rule = v.rule || v.name || '
|
|
951
|
+
const rule = v.rule || v.type || v.name || '';
|
|
927
952
|
const message = v.message || v.description || '';
|
|
953
|
+
const file = v.file || v.filePath || '';
|
|
928
954
|
const severityColor = severity === 'error' ? colors.error :
|
|
929
955
|
severity === 'warning' ? colors.warning : colors.muted;
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
console.log(`
|
|
956
|
+
// Format: show rule type if present, otherwise just message
|
|
957
|
+
if (rule && message) {
|
|
958
|
+
console.log(` ${severityColor(icons.error)} [${rule}] ${message}`);
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
console.log(` ${severityColor(icons.error)} ${message || rule}`);
|
|
962
|
+
}
|
|
963
|
+
if (file) {
|
|
964
|
+
console.log(` ${colors.dim(file)}`);
|
|
933
965
|
}
|
|
934
966
|
}
|
|
935
967
|
}
|
|
@@ -96,9 +96,13 @@ export declare class GitHubService {
|
|
|
96
96
|
*/
|
|
97
97
|
private deleteWebhook;
|
|
98
98
|
/**
|
|
99
|
-
* Verify webhook signature
|
|
99
|
+
* Verify webhook signature (HMAC-SHA256)
|
|
100
100
|
*/
|
|
101
101
|
verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Get decrypted webhook secret
|
|
104
|
+
*/
|
|
105
|
+
getWebhookSecret(encryptedSecret: string): string;
|
|
102
106
|
/**
|
|
103
107
|
* Find repository by webhook payload
|
|
104
108
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles OAuth, API calls, webhooks, and repository management
|
|
5
5
|
*/
|
|
6
|
-
import { randomBytes, createHash, createHmac, createCipheriv, createDecipheriv } from 'crypto';
|
|
6
|
+
import { randomBytes, createHash, createHmac, createCipheriv, createDecipheriv, timingSafeEqual } from 'crypto';
|
|
7
7
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { Logger } from '../utils/logger.js';
|
|
@@ -483,12 +483,30 @@ export class GitHubService {
|
|
|
483
483
|
});
|
|
484
484
|
}
|
|
485
485
|
/**
|
|
486
|
-
* Verify webhook signature
|
|
486
|
+
* Verify webhook signature (HMAC-SHA256)
|
|
487
487
|
*/
|
|
488
488
|
verifyWebhookSignature(payload, signature, secret) {
|
|
489
489
|
const hmac = createHmac('sha256', secret);
|
|
490
490
|
const digest = 'sha256=' + hmac.update(payload).digest('hex');
|
|
491
|
-
|
|
491
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
492
|
+
if (signature.length !== digest.length) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
// Convert to buffers for timing-safe comparison
|
|
496
|
+
const signatureBuffer = Buffer.from(signature);
|
|
497
|
+
const digestBuffer = Buffer.from(digest);
|
|
498
|
+
try {
|
|
499
|
+
return timingSafeEqual(signatureBuffer, digestBuffer);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get decrypted webhook secret
|
|
507
|
+
*/
|
|
508
|
+
getWebhookSecret(encryptedSecret) {
|
|
509
|
+
return this.decrypt(encryptedSecret);
|
|
492
510
|
}
|
|
493
511
|
/**
|
|
494
512
|
* Find repository by webhook payload
|
|
@@ -7,11 +7,17 @@ export declare class EmbeddingService {
|
|
|
7
7
|
private config;
|
|
8
8
|
private initialized;
|
|
9
9
|
private _isAvailable;
|
|
10
|
-
private
|
|
10
|
+
private jinaApiKeys;
|
|
11
|
+
private currentKeyIndex;
|
|
12
|
+
private keyFailures;
|
|
11
13
|
private embeddingDimension;
|
|
12
14
|
constructor(config: EmbeddingConfig);
|
|
13
15
|
getEmbeddingDimension(): number;
|
|
14
16
|
private ensureInitialized;
|
|
17
|
+
private getCurrentJinaKey;
|
|
18
|
+
private rotateToNextKey;
|
|
19
|
+
private shouldSkipKey;
|
|
20
|
+
private findWorkingKeyIndex;
|
|
15
21
|
isAvailable(): boolean;
|
|
16
22
|
generateEmbedding(text: string): Promise<number[]>;
|
|
17
23
|
private generateJinaEmbedding;
|
|
@@ -19,6 +25,7 @@ export declare class EmbeddingService {
|
|
|
19
25
|
* Generate embeddings for multiple texts - uses true batch API when available
|
|
20
26
|
*/
|
|
21
27
|
generateBatchEmbeddings(texts: string[], progressCallback?: (current: number, total: number) => void): Promise<number[][]>;
|
|
28
|
+
private generateJinaBatchWithRetry;
|
|
22
29
|
private generateOpenAIEmbedding;
|
|
23
30
|
prepareCodeForEmbedding(code: string, context?: string): string;
|
|
24
31
|
generateCodeEmbedding(code: string, metadata: {
|
|
@@ -5,7 +5,9 @@ export class EmbeddingService {
|
|
|
5
5
|
config;
|
|
6
6
|
initialized = false;
|
|
7
7
|
_isAvailable = false;
|
|
8
|
-
|
|
8
|
+
jinaApiKeys = [];
|
|
9
|
+
currentKeyIndex = 0;
|
|
10
|
+
keyFailures = new Map();
|
|
9
11
|
embeddingDimension = 1024; // Jina default, OpenAI small = 1536
|
|
10
12
|
constructor(config) {
|
|
11
13
|
this.config = config;
|
|
@@ -23,16 +25,21 @@ export class EmbeddingService {
|
|
|
23
25
|
}
|
|
24
26
|
try {
|
|
25
27
|
if (this.config.provider === 'jina') {
|
|
26
|
-
// Jina AI - free embeddings
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Jina AI - free embeddings with key rotation support
|
|
29
|
+
// Support multiple keys: JINA_API_KEYS (comma-separated) or JINA_API_KEY (single)
|
|
30
|
+
const keysEnv = process.env.JINA_API_KEYS || process.env.JINA_API_KEY || '';
|
|
31
|
+
this.jinaApiKeys = keysEnv
|
|
32
|
+
.split(',')
|
|
33
|
+
.map(k => k.trim())
|
|
34
|
+
.filter(k => k.length > 0);
|
|
35
|
+
if (this.jinaApiKeys.length === 0) {
|
|
36
|
+
Logger.warn('JINA_API_KEY(S) not set - semantic search disabled');
|
|
30
37
|
Logger.info('Get free API key at: https://jina.ai/embeddings/');
|
|
31
38
|
return;
|
|
32
39
|
}
|
|
33
40
|
this.embeddingDimension = 1024;
|
|
34
41
|
this._isAvailable = true;
|
|
35
|
-
Logger.success(
|
|
42
|
+
Logger.success(`Jina AI embeddings enabled (${this.jinaApiKeys.length} key${this.jinaApiKeys.length > 1 ? 's' : ''} configured)`);
|
|
36
43
|
}
|
|
37
44
|
else {
|
|
38
45
|
// OpenAI embeddings
|
|
@@ -51,6 +58,45 @@ export class EmbeddingService {
|
|
|
51
58
|
Logger.warn('Embedding init failed: ' + error);
|
|
52
59
|
}
|
|
53
60
|
}
|
|
61
|
+
getCurrentJinaKey() {
|
|
62
|
+
if (this.jinaApiKeys.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
return this.jinaApiKeys[this.currentKeyIndex];
|
|
65
|
+
}
|
|
66
|
+
rotateToNextKey() {
|
|
67
|
+
if (this.jinaApiKeys.length <= 1)
|
|
68
|
+
return false;
|
|
69
|
+
const oldIndex = this.currentKeyIndex;
|
|
70
|
+
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.jinaApiKeys.length;
|
|
71
|
+
// Track failure for old key
|
|
72
|
+
const failure = this.keyFailures.get(oldIndex) || { count: 0, lastFail: new Date() };
|
|
73
|
+
failure.count++;
|
|
74
|
+
failure.lastFail = new Date();
|
|
75
|
+
this.keyFailures.set(oldIndex, failure);
|
|
76
|
+
Logger.warn(`Jina API key ${oldIndex + 1} exhausted/failed, rotating to key ${this.currentKeyIndex + 1}`);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
shouldSkipKey(index) {
|
|
80
|
+
const failure = this.keyFailures.get(index);
|
|
81
|
+
if (!failure)
|
|
82
|
+
return false;
|
|
83
|
+
// Skip key if it failed recently (within 5 minutes) and has many failures
|
|
84
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
85
|
+
return failure.count >= 3 && failure.lastFail > fiveMinutesAgo;
|
|
86
|
+
}
|
|
87
|
+
findWorkingKeyIndex() {
|
|
88
|
+
const startIndex = this.currentKeyIndex;
|
|
89
|
+
for (let i = 0; i < this.jinaApiKeys.length; i++) {
|
|
90
|
+
const index = (startIndex + i) % this.jinaApiKeys.length;
|
|
91
|
+
if (!this.shouldSkipKey(index)) {
|
|
92
|
+
return index;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// All keys are marked as failed, reset failures and try first key
|
|
96
|
+
Logger.warn('All Jina API keys marked as failed, resetting failure counters');
|
|
97
|
+
this.keyFailures.clear();
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
54
100
|
isAvailable() {
|
|
55
101
|
this.ensureInitialized();
|
|
56
102
|
return this._isAvailable;
|
|
@@ -60,7 +106,7 @@ export class EmbeddingService {
|
|
|
60
106
|
if (!this._isAvailable)
|
|
61
107
|
return new Array(this.embeddingDimension).fill(0);
|
|
62
108
|
try {
|
|
63
|
-
if (this.config.provider === 'jina' && this.
|
|
109
|
+
if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
|
|
64
110
|
return await this.generateJinaEmbedding(text);
|
|
65
111
|
}
|
|
66
112
|
else if (this.openai) {
|
|
@@ -73,28 +119,51 @@ export class EmbeddingService {
|
|
|
73
119
|
return new Array(this.embeddingDimension).fill(0);
|
|
74
120
|
}
|
|
75
121
|
}
|
|
76
|
-
async generateJinaEmbedding(text) {
|
|
77
|
-
|
|
122
|
+
async generateJinaEmbedding(text, retryCount = 0) {
|
|
123
|
+
const maxRetries = this.jinaApiKeys.length;
|
|
124
|
+
// Find a working key index before first attempt
|
|
125
|
+
if (retryCount === 0) {
|
|
126
|
+
this.currentKeyIndex = this.findWorkingKeyIndex();
|
|
127
|
+
}
|
|
128
|
+
const apiKey = this.getCurrentJinaKey();
|
|
129
|
+
if (!apiKey)
|
|
78
130
|
throw new Error('Jina API key not set');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch('https://api.jina.ai/v1/embeddings', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'Authorization': `Bearer ${apiKey}`
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
model: this.config.model || 'jina-embeddings-v3',
|
|
140
|
+
task: 'text-matching',
|
|
141
|
+
dimensions: 1024,
|
|
142
|
+
input: [text.substring(0, 8000)]
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const errorText = await response.text();
|
|
147
|
+
// Check for rate limit or auth errors - try next key
|
|
148
|
+
if (response.status === 429 || response.status === 401 || response.status === 403) {
|
|
149
|
+
if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
|
|
150
|
+
Logger.warn(`Jina API key error (${response.status}), trying next key...`);
|
|
151
|
+
return this.generateJinaEmbedding(text, retryCount + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Jina API error: ${response.status} ${errorText}`);
|
|
155
|
+
}
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
return data.data[0].embedding;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
// Network error or other issue - try next key
|
|
161
|
+
if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
|
|
162
|
+
Logger.warn(`Jina API request failed, trying next key...`);
|
|
163
|
+
return this.generateJinaEmbedding(text, retryCount + 1);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
95
166
|
}
|
|
96
|
-
const data = await response.json();
|
|
97
|
-
return data.data[0].embedding;
|
|
98
167
|
}
|
|
99
168
|
/**
|
|
100
169
|
* Generate embeddings for multiple texts - uses true batch API when available
|
|
@@ -110,30 +179,13 @@ export class EmbeddingService {
|
|
|
110
179
|
Logger.progress(`Generating embeddings for ${texts.length} texts...`);
|
|
111
180
|
try {
|
|
112
181
|
// Use true batch API for Jina (much faster!)
|
|
113
|
-
if (this.config.provider === 'jina' && this.
|
|
182
|
+
if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
|
|
114
183
|
const BATCH_SIZE = 500;
|
|
115
184
|
const allEmbeddings = [];
|
|
116
185
|
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
117
186
|
const batch = texts.slice(i, i + BATCH_SIZE).map(t => t.substring(0, 8000));
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
headers: {
|
|
121
|
-
'Content-Type': 'application/json',
|
|
122
|
-
'Authorization': `Bearer ${this.jinaApiKey}`
|
|
123
|
-
},
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
model: this.config.model || 'jina-embeddings-v3',
|
|
126
|
-
task: 'text-matching',
|
|
127
|
-
dimensions: 1024,
|
|
128
|
-
input: batch
|
|
129
|
-
})
|
|
130
|
-
});
|
|
131
|
-
if (!response.ok) {
|
|
132
|
-
const error = await response.text();
|
|
133
|
-
throw new Error(`Jina API error: ${response.status} ${error}`);
|
|
134
|
-
}
|
|
135
|
-
const data = await response.json();
|
|
136
|
-
allEmbeddings.push(...data.data.map(d => d.embedding));
|
|
187
|
+
const batchEmbeddings = await this.generateJinaBatchWithRetry(batch);
|
|
188
|
+
allEmbeddings.push(...batchEmbeddings);
|
|
137
189
|
if (progressCallback) {
|
|
138
190
|
progressCallback(Math.min(i + BATCH_SIZE, texts.length), texts.length);
|
|
139
191
|
}
|
|
@@ -161,6 +213,48 @@ export class EmbeddingService {
|
|
|
161
213
|
return texts.map(() => new Array(this.embeddingDimension).fill(0));
|
|
162
214
|
}
|
|
163
215
|
}
|
|
216
|
+
async generateJinaBatchWithRetry(batch, retryCount = 0) {
|
|
217
|
+
const maxRetries = this.jinaApiKeys.length;
|
|
218
|
+
const apiKey = this.getCurrentJinaKey();
|
|
219
|
+
if (!apiKey)
|
|
220
|
+
throw new Error('Jina API key not set');
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch('https://api.jina.ai/v1/embeddings', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
'Authorization': `Bearer ${apiKey}`
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
model: this.config.model || 'jina-embeddings-v3',
|
|
230
|
+
task: 'text-matching',
|
|
231
|
+
dimensions: 1024,
|
|
232
|
+
input: batch
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
const errorText = await response.text();
|
|
237
|
+
// Check for rate limit or auth errors - try next key
|
|
238
|
+
if (response.status === 429 || response.status === 401 || response.status === 403) {
|
|
239
|
+
if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
|
|
240
|
+
Logger.warn(`Jina API batch error (${response.status}), trying next key...`);
|
|
241
|
+
return this.generateJinaBatchWithRetry(batch, retryCount + 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Jina API error: ${response.status} ${errorText}`);
|
|
245
|
+
}
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
return data.data.map(d => d.embedding);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
// Network error or other issue - try next key
|
|
251
|
+
if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
|
|
252
|
+
Logger.warn(`Jina API batch request failed, trying next key...`);
|
|
253
|
+
return this.generateJinaBatchWithRetry(batch, retryCount + 1);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
164
258
|
async generateOpenAIEmbedding(text) {
|
|
165
259
|
if (!this.openai)
|
|
166
260
|
throw new Error('OpenAI client not initialized');
|
package/dist/server/index.js
CHANGED
|
@@ -25,6 +25,9 @@ import { adminRouter } from './routes/admin.js';
|
|
|
25
25
|
import { developerRouter } from './routes/developer.js';
|
|
26
26
|
import { githubRouter } from './routes/github.js';
|
|
27
27
|
import deviceAuthRouter from './routes/device-auth.js';
|
|
28
|
+
import { cache } from './services/cache.js';
|
|
29
|
+
import { db } from './services/database.js';
|
|
30
|
+
import { AuthService } from './services/auth-service.js';
|
|
28
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
32
|
const __dirname = path.dirname(__filename);
|
|
30
33
|
// CORS whitelist - настройте под свои домены
|
|
@@ -156,6 +159,7 @@ export class ArchiCoreServer {
|
|
|
156
159
|
this.app.use(express.static(publicPath, {
|
|
157
160
|
maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0,
|
|
158
161
|
etag: true,
|
|
162
|
+
index: false, // Отключаем автоматический index.html - используем явные маршруты
|
|
159
163
|
}));
|
|
160
164
|
}
|
|
161
165
|
setupRoutes() {
|
|
@@ -187,7 +191,54 @@ export class ArchiCoreServer {
|
|
|
187
191
|
const deviceAuthPath = path.join(__dirname, '../../public/device-auth.html');
|
|
188
192
|
res.sendFile(deviceAuthPath);
|
|
189
193
|
});
|
|
190
|
-
//
|
|
194
|
+
// Clean URL routing
|
|
195
|
+
// Landing page (главная)
|
|
196
|
+
this.app.get('/', (_req, res) => {
|
|
197
|
+
res.sendFile(path.join(__dirname, '../../public/landing.html'));
|
|
198
|
+
});
|
|
199
|
+
// Dashboard (после авторизации)
|
|
200
|
+
this.app.get('/dashboard', (_req, res) => {
|
|
201
|
+
res.sendFile(path.join(__dirname, '../../public/index.html'));
|
|
202
|
+
});
|
|
203
|
+
// Add new project page
|
|
204
|
+
this.app.get('/new', (_req, res) => {
|
|
205
|
+
res.sendFile(path.join(__dirname, '../../public/index.html'));
|
|
206
|
+
});
|
|
207
|
+
// Pricing page
|
|
208
|
+
this.app.get('/pricing', (_req, res) => {
|
|
209
|
+
res.sendFile(path.join(__dirname, '../../public/pricing.html'));
|
|
210
|
+
});
|
|
211
|
+
// API Dashboard page
|
|
212
|
+
this.app.get('/api-dashboard', (_req, res) => {
|
|
213
|
+
res.sendFile(path.join(__dirname, '../../public/api-dashboard.html'));
|
|
214
|
+
});
|
|
215
|
+
// Auth page
|
|
216
|
+
this.app.get('/auth', (_req, res) => {
|
|
217
|
+
res.sendFile(path.join(__dirname, '../../public/auth.html'));
|
|
218
|
+
});
|
|
219
|
+
// Login alias
|
|
220
|
+
this.app.get('/login', (_req, res) => {
|
|
221
|
+
res.sendFile(path.join(__dirname, '../../public/auth.html'));
|
|
222
|
+
});
|
|
223
|
+
// Register alias
|
|
224
|
+
this.app.get('/register', (_req, res) => {
|
|
225
|
+
res.redirect('/auth?mode=register');
|
|
226
|
+
});
|
|
227
|
+
// Admin page
|
|
228
|
+
this.app.get('/admin', (_req, res) => {
|
|
229
|
+
res.sendFile(path.join(__dirname, '../../public/admin.html'));
|
|
230
|
+
});
|
|
231
|
+
// Legal pages
|
|
232
|
+
this.app.get('/privacy', (_req, res) => {
|
|
233
|
+
res.sendFile(path.join(__dirname, '../../public/privacy.html'));
|
|
234
|
+
});
|
|
235
|
+
this.app.get('/terms', (_req, res) => {
|
|
236
|
+
res.sendFile(path.join(__dirname, '../../public/terms.html'));
|
|
237
|
+
});
|
|
238
|
+
this.app.get('/security', (_req, res) => {
|
|
239
|
+
res.sendFile(path.join(__dirname, '../../public/security.html'));
|
|
240
|
+
});
|
|
241
|
+
// SPA fallback - все остальные маршруты отдают index.html (dashboard)
|
|
191
242
|
this.app.get('/*splat', (_req, res) => {
|
|
192
243
|
const indexPath = path.join(__dirname, '../../public/index.html');
|
|
193
244
|
res.sendFile(indexPath);
|
|
@@ -208,6 +259,17 @@ export class ArchiCoreServer {
|
|
|
208
259
|
});
|
|
209
260
|
}
|
|
210
261
|
async start() {
|
|
262
|
+
// Initialize database
|
|
263
|
+
try {
|
|
264
|
+
await db.init();
|
|
265
|
+
const authService = AuthService.getInstance();
|
|
266
|
+
await authService.initDatabase();
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
Logger.warn('Database initialization failed, using JSON fallback:', error);
|
|
270
|
+
}
|
|
271
|
+
// Initialize cache
|
|
272
|
+
await cache.connect();
|
|
211
273
|
return new Promise((resolve) => {
|
|
212
274
|
this.server = createServer(this.app);
|
|
213
275
|
const host = this.config.host || '0.0.0.0';
|
|
@@ -221,6 +283,9 @@ export class ArchiCoreServer {
|
|
|
221
283
|
});
|
|
222
284
|
}
|
|
223
285
|
async stop() {
|
|
286
|
+
// Disconnect cache and database
|
|
287
|
+
await cache.disconnect();
|
|
288
|
+
await db.close();
|
|
224
289
|
return new Promise((resolve) => {
|
|
225
290
|
if (this.server) {
|
|
226
291
|
this.server.close(() => {
|