mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-19 08:54:05 +00:00
Unify config storage and gateway bootstrap
This commit is contained in:
parent
7c4a877c99
commit
6971be8e87
107 changed files with 2861 additions and 6615 deletions
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -13,8 +13,33 @@ concurrency:
|
|||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
model-catalog:
|
||||
name: Update model catalog
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Update models.json
|
||||
run: npm run models:update
|
||||
|
||||
- name: Upload model catalog
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-model-catalog
|
||||
path: packages/core/models.json
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
macos:
|
||||
name: macOS (${{ matrix.display_name }})
|
||||
needs: model-catalog
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -45,6 +70,12 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download updated model catalog
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-model-catalog
|
||||
path: packages/core
|
||||
|
||||
- name: Verify tag version
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -155,6 +186,12 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download updated model catalog
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-model-catalog
|
||||
path: packages/core
|
||||
|
||||
- name: Verify tag version
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -189,6 +226,12 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download updated model catalog
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-model-catalog
|
||||
path: packages/core
|
||||
|
||||
- name: Verify tag version
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -310,9 +310,7 @@ function buildScenario(strategy: StrategyId, corpus: Corpus, iteration: number):
|
|||
}
|
||||
|
||||
function benchmarkConfig(): AppConfig {
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: "/tmp/ccr-context-archive-benchmark-gateway.json"
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
return {
|
||||
...config,
|
||||
APIKEY: "benchmark-key",
|
||||
|
|
|
|||
|
|
@ -664,9 +664,7 @@ function normalizeMatchText(value: string): string {
|
|||
}
|
||||
|
||||
function benchmarkConfig(): AppConfig {
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: "/tmp/ccr-context-archive-real-agent-benchmark.json"
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
return {
|
||||
...config,
|
||||
APIKEY: "benchmark-key",
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ export function createMainBuildOptions({ mode = "production", plugins = [] } = {
|
|||
path.join(electronSourceRoot, "main", "main.ts"),
|
||||
path.join(electronSourceRoot, "main", "browser-preload.ts"),
|
||||
gatewayRuntimeInput,
|
||||
path.join(coreSourceRoot, "gateway", "core-runtime", "gateway-bootstrap.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "browser-web-search-proxy-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
|
||||
|
|
@ -258,6 +259,7 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {}
|
|||
entryNames: "[name]",
|
||||
entryPoints: [
|
||||
path.join(cliSourceRoot, "cli.ts"),
|
||||
path.join(coreSourceRoot, "gateway", "core-runtime", "gateway-bootstrap.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "media-tools-proxy-mcp.ts"),
|
||||
|
|
@ -286,6 +288,7 @@ export function createCoreServerBuildOptions({ mode = "production", plugins = []
|
|||
entryNames: "[name]",
|
||||
entryPoints: [
|
||||
path.join(coreSourceRoot, "entrypoints", "server.ts"),
|
||||
path.join(coreSourceRoot, "gateway", "core-runtime", "gateway-bootstrap.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "media-tools-proxy-mcp.ts"),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const testProjects = {
|
|||
core: {
|
||||
runtimeEntryPoints: {
|
||||
"runtime/fusion-vision-mcp": path.join(packageRoots.core, "mcp", "fusion-vision-mcp.ts"),
|
||||
"runtime/gateway-bootstrap": path.join(packageRoots.core, "gateway", "core-runtime", "gateway-bootstrap.ts"),
|
||||
"runtime/media-tools-proxy-mcp": path.join(packageRoots.core, "mcp", "media-tools-proxy-mcp.ts"),
|
||||
"runtime/request-log-worker": path.join(packageRoots.core, "observability", "request-log-worker.ts"),
|
||||
"runtime/route-script-worker": path.join(packageRoots.core, "routing", "route-script-worker.ts"),
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -36,6 +36,7 @@
|
|||
"electron": "^42.3.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "^0.27.7",
|
||||
"js-yaml": "^4.2.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"motion": "^12.40.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
|
|
@ -39,11 +39,13 @@
|
|||
"build:docker": "node build/docker-build.mjs",
|
||||
"build:app:mac": "npm run build:app:mac:local",
|
||||
"build:app:mac:local": "npm run build:assets && electron-builder --config build/electron-builder.local.cjs --mac --publish never",
|
||||
"build:app:mac:release": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --publish never",
|
||||
"build:app:mac:release": "node build/macos-release-preflight.mjs && npm run models:update && npm run build:assets && electron-builder --mac --publish never",
|
||||
"build:app:win": "npm run build:assets && electron-builder --win",
|
||||
"prepack": "npm run build:assets",
|
||||
"models:update": "node scripts/generate-models-json.mjs",
|
||||
"prepack": "npm run models:update && npm run build:assets",
|
||||
"prepublishOnly": "npm run typecheck",
|
||||
"preview": "npm run build:assets && electron .",
|
||||
"version": "npm run models:update",
|
||||
"bench:context-archive": "node benchmarks/run-context-archive-benchmark.mjs",
|
||||
"bench:context-archive-agent": "node benchmarks/run-context-archive-real-agent-benchmark.mjs",
|
||||
"bench:context-archive-real": "node benchmarks/run-context-archive-real-agent-benchmark.mjs",
|
||||
|
|
@ -92,6 +94,7 @@
|
|||
"electron": "^42.3.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "^0.27.7",
|
||||
"js-yaml": "^4.2.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"motion": "^12.40.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npm --prefix ../.. run build:assets",
|
||||
"prepack": "npm --prefix ../.. run models:update && npm --prefix ../.. run build:assets",
|
||||
"prepublishOnly": "npm --prefix ../.. run typecheck",
|
||||
"test": "node ../../build/test.mjs cli && node ../../build/run-tests.mjs cli",
|
||||
"test:integration": "node ../../build/test.mjs cli --scope integration && node ../../build/run-tests.mjs cli"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ type ZcodeGatewayConfigValues = {
|
|||
};
|
||||
|
||||
const legacyZcodeTomlConfigFile = "~/.zcode/config.toml";
|
||||
const defaultZcodeConfigFile = "~/.zcode/cli/config.json";
|
||||
const originalBackupSuffix = ".ccr-original";
|
||||
const originalMissingSuffix = ".ccr-original-missing";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,327 +0,0 @@
|
|||
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { API_KEYS_DB_FILE, LEGACY_API_KEYS_DB_FILES } from "@ccr/core/config/constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
import type { ApiKeyConfig, ApiKeyLimitConfig } from "@ccr/core/contracts/app";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
||||
type StoredApiKeyRow = {
|
||||
createdAt: string;
|
||||
encryption: string;
|
||||
expiresAt: string;
|
||||
id: string;
|
||||
limitsJson: string;
|
||||
name: string;
|
||||
storedKey: string;
|
||||
};
|
||||
|
||||
const plainStorage = "plain";
|
||||
const privateDirMode = 0o700;
|
||||
const privateFileMode = 0o600;
|
||||
|
||||
class ApiKeyStore {
|
||||
private database?: SqlDatabase;
|
||||
private initPromise?: Promise<SqlDatabase>;
|
||||
|
||||
constructor(private readonly dbFile: string) {}
|
||||
|
||||
async list(): Promise<ApiKeyConfig[]> {
|
||||
const database = await this.getDatabase();
|
||||
const rows = queryRows(
|
||||
database,
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
encrypted_key,
|
||||
encryption,
|
||||
created_at,
|
||||
expires_at,
|
||||
limits_json
|
||||
FROM api_keys
|
||||
ORDER BY rowid
|
||||
`
|
||||
);
|
||||
|
||||
return uniqueApiKeyConfigs(rows.map(toApiKeyConfig));
|
||||
}
|
||||
|
||||
async replace(apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
const normalized = uniqueApiKeyConfigs(apiKeys);
|
||||
const database = await this.getDatabase();
|
||||
const statement = database.prepare(`
|
||||
INSERT INTO api_keys (
|
||||
id,
|
||||
name,
|
||||
encrypted_key,
|
||||
encryption,
|
||||
created_at,
|
||||
expires_at,
|
||||
limits_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
try {
|
||||
database.exec("BEGIN TRANSACTION");
|
||||
database.exec("DELETE FROM api_keys");
|
||||
for (const apiKey of normalized) {
|
||||
const stored = storeApiKey(apiKey.key);
|
||||
statement.run(
|
||||
apiKey.id,
|
||||
apiKey.name ?? "",
|
||||
stored.value,
|
||||
stored.encryption,
|
||||
apiKey.createdAt,
|
||||
apiKey.expiresAt ?? "",
|
||||
apiKey.limits ? JSON.stringify(apiKey.limits) : ""
|
||||
);
|
||||
}
|
||||
database.exec("COMMIT");
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return normalized;
|
||||
} catch (error) {
|
||||
try {
|
||||
database.exec("ROLLBACK");
|
||||
} catch {
|
||||
// Ignore rollback errors; the original write error is more useful.
|
||||
}
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDatabase(): Promise<SqlDatabase> {
|
||||
if (this.database) {
|
||||
return this.database;
|
||||
}
|
||||
|
||||
this.initPromise ??= this.open();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private async open(): Promise<SqlDatabase> {
|
||||
const dbDir = dirname(this.dbFile);
|
||||
mkdirSync(dbDir, { mode: privateDirMode, recursive: true });
|
||||
securePathPermissions(dbDir, privateDirMode);
|
||||
const database = createBetterSqliteDatabase(this.dbFile);
|
||||
configureSqliteDatabase(database);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
encrypted_key TEXT NOT NULL,
|
||||
encryption TEXT NOT NULL DEFAULT '${plainStorage}',
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL DEFAULT '',
|
||||
limits_json TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS api_keys_created_at_idx ON api_keys(created_at);
|
||||
`);
|
||||
|
||||
this.database = database;
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return database;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiKeyStore = new ApiKeyStore(API_KEYS_DB_FILE);
|
||||
|
||||
export async function loadPersistedApiKeys(): Promise<ApiKeyConfig[]> {
|
||||
if (!existsSync(API_KEYS_DB_FILE)) {
|
||||
const legacyApiKeys = await readLegacyApiKeys();
|
||||
if (legacyApiKeys.length > 0) {
|
||||
return legacyApiKeys;
|
||||
}
|
||||
}
|
||||
return apiKeyStore.list();
|
||||
}
|
||||
|
||||
export async function replacePersistedApiKeys(apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
return apiKeyStore.replace(apiKeys);
|
||||
}
|
||||
|
||||
async function readLegacyApiKeys(): Promise<ApiKeyConfig[]> {
|
||||
for (const dbFile of LEGACY_API_KEYS_DB_FILES) {
|
||||
if (!existsSync(dbFile) || samePath(dbFile, API_KEYS_DB_FILE)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const store = new ApiKeyStore(dbFile);
|
||||
const apiKeys = await store.list();
|
||||
if (apiKeys.length > 0) {
|
||||
return apiKeys;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[config] Failed to read legacy API key database ${dbFile}: ${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function toApiKeyConfig(row: Record<string, SqlValue>): ApiKeyConfig | undefined {
|
||||
const stored = toStoredApiKeyRow(row);
|
||||
if (!stored) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = readStoredApiKey(stored.storedKey, stored.encryption);
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const limits = parseApiKeyLimits(stored.limitsJson);
|
||||
return {
|
||||
createdAt: stored.createdAt,
|
||||
...(stored.expiresAt ? { expiresAt: stored.expiresAt } : {}),
|
||||
id: stored.id,
|
||||
key,
|
||||
...(limits ? { limits } : {}),
|
||||
...(stored.name ? { name: stored.name } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function toStoredApiKeyRow(row: Record<string, SqlValue>): StoredApiKeyRow | undefined {
|
||||
const id = readString(row.id);
|
||||
const storedKey = readString(row.encrypted_key);
|
||||
const createdAt = readString(row.created_at) || new Date(0).toISOString();
|
||||
if (!id || !storedKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
createdAt,
|
||||
encryption: readString(row.encryption) || plainStorage,
|
||||
expiresAt: readString(row.expires_at) || "",
|
||||
id,
|
||||
limitsJson: readString(row.limits_json) || "",
|
||||
name: readString(row.name) || "",
|
||||
storedKey
|
||||
};
|
||||
}
|
||||
|
||||
function storeApiKey(key: string): { encryption: string; value: string } {
|
||||
return {
|
||||
encryption: plainStorage,
|
||||
value: key
|
||||
};
|
||||
}
|
||||
|
||||
function readStoredApiKey(value: string, encryption: string): string | undefined {
|
||||
if (encryption !== plainStorage) {
|
||||
console.warn(`[api-keys] Stored API key uses unsupported storage "${encryption}". Re-save the API key to migrate it.`);
|
||||
return undefined;
|
||||
}
|
||||
return value.trim() || undefined;
|
||||
}
|
||||
|
||||
function secureDatabaseFilePermissions(file: string): void {
|
||||
securePathPermissions(file, privateFileMode);
|
||||
securePathPermissions(`${file}-wal`, privateFileMode);
|
||||
securePathPermissions(`${file}-shm`, privateFileMode);
|
||||
}
|
||||
|
||||
function securePathPermissions(file: string, mode: number): void {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
if (!existsSync(file)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
chmodSync(file, mode);
|
||||
} catch {
|
||||
// Best effort for filesystems that do not support chmod.
|
||||
}
|
||||
}
|
||||
|
||||
function parseApiKeyLimits(value: string): ApiKeyLimitConfig | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (!isObject(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
const limits: ApiKeyLimitConfig = {};
|
||||
for (const key of ["ipd", "iph", "ipm", "maxRequests", "maxTokens", "quotaWindowMs", "rpd", "rph", "rpm", "tpd", "tph", "tpm", "windowMs"] as const) {
|
||||
const limit = readPositiveInteger(parsed[key]);
|
||||
if (limit) {
|
||||
limits[key] = limit;
|
||||
}
|
||||
}
|
||||
return Object.keys(limits).length ? limits : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function configureSqliteDatabase(database: SqlDatabase): void {
|
||||
database.pragma("journal_mode = WAL");
|
||||
database.pragma("synchronous = NORMAL");
|
||||
database.pragma("busy_timeout = 5000");
|
||||
}
|
||||
|
||||
function queryRows(database: SqlDatabase, sql: string, params: SqlValue[] = []): Array<Record<string, SqlValue>> {
|
||||
return database.prepare(sql).all(...params) as Array<Record<string, SqlValue>>;
|
||||
}
|
||||
|
||||
function uniqueApiKeyConfigs(values: Array<ApiKeyConfig | undefined>): ApiKeyConfig[] {
|
||||
const seenKeys = new Set<string>();
|
||||
const seenIds = new Set<string>();
|
||||
const result: ApiKeyConfig[] = [];
|
||||
for (const [index, value] of values.entries()) {
|
||||
const key = value?.key.trim();
|
||||
if (!value || !key || seenKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
const id = uniqueApiKeyId(value.id || `key-${index + 1}`, seenIds, index);
|
||||
result.push({
|
||||
createdAt: value.createdAt || new Date(0).toISOString(),
|
||||
...(value.expiresAt ? { expiresAt: value.expiresAt } : {}),
|
||||
id,
|
||||
key,
|
||||
...(value.limits ? { limits: value.limits } : {}),
|
||||
...(value.name ? { name: value.name } : {})
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function uniqueApiKeyId(id: string, seenIds: Set<string>, index: number): string {
|
||||
const base = id.trim() || `key-${index + 1}`;
|
||||
let candidate = base;
|
||||
let suffix = 2;
|
||||
while (seenIds.has(candidate)) {
|
||||
candidate = `${base}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
seenIds.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: unknown): number | undefined {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? Math.ceil(number) : undefined;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function samePath(left: string, right: string): boolean {
|
||||
return left.toLowerCase() === right.toLowerCase();
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { APP_CONFIG_DB_FILE, LEGACY_APP_CONFIG_DB_FILES } from "@ccr/core/config/constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
||||
const appConfigKey = "default";
|
||||
const privateDirMode = 0o700;
|
||||
const privateFileMode = 0o600;
|
||||
|
||||
class AppConfigStore {
|
||||
private database?: SqlDatabase;
|
||||
private initPromise?: Promise<SqlDatabase>;
|
||||
|
||||
constructor(private readonly dbFile: string) {}
|
||||
|
||||
async read(): Promise<unknown | undefined> {
|
||||
return this.readKey(appConfigKey);
|
||||
}
|
||||
|
||||
async readKey(key: string): Promise<unknown | undefined> {
|
||||
const database = await this.getDatabase();
|
||||
const row = queryRows(database, "SELECT value_json FROM app_config WHERE key = ? LIMIT 1", [key])[0];
|
||||
const valueJson = readString(row?.value_json);
|
||||
if (!valueJson) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(valueJson) as unknown;
|
||||
}
|
||||
|
||||
async replace(value: unknown): Promise<void> {
|
||||
await this.replaceKey(appConfigKey, value);
|
||||
}
|
||||
|
||||
async replaceKey(key: string, value: unknown): Promise<void> {
|
||||
const database = await this.getDatabase();
|
||||
database.prepare(`
|
||||
INSERT INTO app_config (key, value_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value_json = excluded.value_json,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(key, JSON.stringify(value), new Date().toISOString());
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
}
|
||||
|
||||
private async getDatabase(): Promise<SqlDatabase> {
|
||||
if (this.database) {
|
||||
return this.database;
|
||||
}
|
||||
|
||||
this.initPromise ??= this.open();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private async open(): Promise<SqlDatabase> {
|
||||
const dbDir = dirname(this.dbFile);
|
||||
mkdirSync(dbDir, { mode: privateDirMode, recursive: true });
|
||||
securePathPermissions(dbDir, privateDirMode);
|
||||
const database = createBetterSqliteDatabase(this.dbFile);
|
||||
configureSqliteDatabase(database);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
this.database = database;
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return database;
|
||||
}
|
||||
}
|
||||
|
||||
export const appConfigStore = new AppConfigStore(APP_CONFIG_DB_FILE);
|
||||
|
||||
export async function loadPersistedAppConfig(): Promise<unknown | undefined> {
|
||||
if (!existsSync(APP_CONFIG_DB_FILE)) {
|
||||
const legacyValue = await readLegacyAppConfigKey(appConfigKey);
|
||||
if (legacyValue !== undefined) {
|
||||
return legacyValue;
|
||||
}
|
||||
}
|
||||
return appConfigStore.read();
|
||||
}
|
||||
|
||||
export async function loadPersistedAppSetting(key: string): Promise<unknown | undefined> {
|
||||
if (!existsSync(APP_CONFIG_DB_FILE)) {
|
||||
const legacyValue = await readLegacyAppConfigKey(key);
|
||||
if (legacyValue !== undefined) {
|
||||
return legacyValue;
|
||||
}
|
||||
}
|
||||
return appConfigStore.readKey(key);
|
||||
}
|
||||
|
||||
export async function replacePersistedAppConfig(value: unknown): Promise<void> {
|
||||
await appConfigStore.replace(value);
|
||||
}
|
||||
|
||||
export async function replacePersistedAppSetting(key: string, value: unknown): Promise<void> {
|
||||
await appConfigStore.replaceKey(key, value);
|
||||
}
|
||||
|
||||
async function readLegacyAppConfigKey(key: string): Promise<unknown | undefined> {
|
||||
for (const dbFile of LEGACY_APP_CONFIG_DB_FILES) {
|
||||
if (!existsSync(dbFile) || samePath(dbFile, APP_CONFIG_DB_FILE)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const store = new AppConfigStore(dbFile);
|
||||
const value = await store.readKey(key);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[config] Failed to read legacy app config database ${dbFile}: ${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function configureSqliteDatabase(database: SqlDatabase): void {
|
||||
database.pragma("journal_mode = WAL");
|
||||
database.pragma("synchronous = NORMAL");
|
||||
database.pragma("busy_timeout = 5000");
|
||||
}
|
||||
|
||||
function queryRows(database: SqlDatabase, sql: string, params: SqlValue[] = []): Array<Record<string, SqlValue>> {
|
||||
return database.prepare(sql).all(...params) as Array<Record<string, SqlValue>>;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function secureDatabaseFilePermissions(file: string): void {
|
||||
securePathPermissions(file, privateFileMode);
|
||||
securePathPermissions(`${file}-wal`, privateFileMode);
|
||||
securePathPermissions(`${file}-shm`, privateFileMode);
|
||||
}
|
||||
|
||||
function securePathPermissions(file: string, mode: number): void {
|
||||
if (process.platform === "win32" || !existsSync(file)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
chmodSync(file, mode);
|
||||
} catch {
|
||||
// Best effort for filesystems that do not support chmod.
|
||||
}
|
||||
}
|
||||
|
||||
function samePath(left: string, right: string): boolean {
|
||||
return left.toLowerCase() === right.toLowerCase();
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
842
packages/core/src/config/config-repository.ts
Normal file
842
packages/core/src/config/config-repository.ts
Normal file
|
|
@ -0,0 +1,842 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_APP_CONFIG_DB_FILES,
|
||||
LEGACY_API_KEYS_DB_FILES,
|
||||
LEGACY_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
ONBOARDING_FINISHED_AT_SETTING_KEY,
|
||||
ONBOARDING_FINISHED_FILE
|
||||
} from "@ccr/core/config/constants";
|
||||
import type { ApiKeyConfig, ApiKeyLimitConfig } from "@ccr/core/contracts/app";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
||||
type StoredApiKeyRow = {
|
||||
createdAt: string;
|
||||
encryption: string;
|
||||
expiresAt: string;
|
||||
id: string;
|
||||
limitsJson: string;
|
||||
name: string;
|
||||
storedKey: string;
|
||||
};
|
||||
|
||||
const appConfigKey = "default";
|
||||
const plainStorage = "plain";
|
||||
const privateDirMode = 0o700;
|
||||
const privateFileMode = 0o600;
|
||||
const backupDedupMigrationId = "legacy-storage-backup-dedup-v1";
|
||||
const cleanupQueueBackfillMigrationId = "legacy-storage-cleanup-queue-v1";
|
||||
const unifiedConfigMigrationId = "unified-config-store-v1";
|
||||
const legacyJsonConfigFiles = [
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
LEGACY_CONFIG_FILE
|
||||
];
|
||||
|
||||
type ConfigRepositoryOptions = {
|
||||
removeFile?: (file: string) => void;
|
||||
};
|
||||
|
||||
export class ConfigRepository {
|
||||
private database?: SqlDatabase;
|
||||
private initPromise?: Promise<SqlDatabase>;
|
||||
private readonly removeFile: (file: string) => void;
|
||||
|
||||
constructor(
|
||||
private readonly dbFile: string,
|
||||
options: ConfigRepositoryOptions = {}
|
||||
) {
|
||||
this.removeFile = options.removeFile ?? ((file) => rmSync(file, { force: true }));
|
||||
}
|
||||
|
||||
async readAppConfig(): Promise<unknown | undefined> {
|
||||
return this.readSetting(appConfigKey);
|
||||
}
|
||||
|
||||
async readSetting(key: string): Promise<unknown | undefined> {
|
||||
const database = await this.getDatabase();
|
||||
const row = queryRows(database, "SELECT value_json FROM app_config WHERE key = ? LIMIT 1", [key])[0];
|
||||
const valueJson = readString(row?.value_json);
|
||||
return valueJson ? JSON.parse(valueJson) as unknown : undefined;
|
||||
}
|
||||
|
||||
async replaceAppConfig(value: unknown): Promise<void> {
|
||||
const database = await this.getDatabase();
|
||||
replaceAppConfigRow(database, appConfigKey, value);
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
}
|
||||
|
||||
async replaceSetting(key: string, value: unknown): Promise<void> {
|
||||
const database = await this.getDatabase();
|
||||
replaceAppConfigRow(database, key, value);
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
}
|
||||
|
||||
async readRuntimeState(key: string): Promise<unknown | undefined> {
|
||||
const database = await this.getDatabase();
|
||||
const row = queryRows(database, "SELECT value_json FROM runtime_state WHERE key = ? LIMIT 1", [key])[0];
|
||||
const valueJson = readString(row?.value_json);
|
||||
return valueJson ? JSON.parse(valueJson) as unknown : undefined;
|
||||
}
|
||||
|
||||
async replaceRuntimeState(key: string, value: unknown): Promise<void> {
|
||||
const database = await this.getDatabase();
|
||||
replaceJsonRow(database, "runtime_state", key, value);
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
}
|
||||
|
||||
async deleteRuntimeState(key: string): Promise<void> {
|
||||
const database = await this.getDatabase();
|
||||
database.prepare("DELETE FROM runtime_state WHERE key = ?").run(key);
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
}
|
||||
|
||||
async listApiKeys(): Promise<ApiKeyConfig[]> {
|
||||
const database = await this.getDatabase();
|
||||
return listApiKeys(database);
|
||||
}
|
||||
|
||||
async replaceApiKeys(apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
const normalized = uniqueApiKeyConfigs(apiKeys);
|
||||
const database = await this.getDatabase();
|
||||
runTransaction(database, () => replaceApiKeyRows(database, normalized));
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async replaceConfigSnapshot(value: unknown, apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
const normalized = uniqueApiKeyConfigs(apiKeys);
|
||||
const database = await this.getDatabase();
|
||||
runTransaction(database, () => {
|
||||
replaceAppConfigRow(database, appConfigKey, value);
|
||||
replaceApiKeyRows(database, normalized);
|
||||
});
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async archiveLegacyJsonConfigFiles(files: string[]): Promise<void> {
|
||||
const requestedFiles = uniquePaths(files);
|
||||
const unsupportedFiles = requestedFiles.filter(
|
||||
(file) => !legacyJsonConfigFiles.some((candidate) => samePath(candidate, file))
|
||||
);
|
||||
if (unsupportedFiles.length > 0) {
|
||||
throw new Error(`Refusing to archive unknown legacy JSON config file: ${unsupportedFiles.join(", ")}`);
|
||||
}
|
||||
if (requestedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
const database = await this.getDatabase();
|
||||
archiveAndRemoveFiles(database, requestedFiles, "legacy-json-config", this.removeFile);
|
||||
}
|
||||
|
||||
private async getDatabase(): Promise<SqlDatabase> {
|
||||
if (this.database) {
|
||||
return this.database;
|
||||
}
|
||||
this.initPromise ??= this.open();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private async open(): Promise<SqlDatabase> {
|
||||
const dbDir = dirname(this.dbFile);
|
||||
mkdirSync(dbDir, { mode: privateDirMode, recursive: true });
|
||||
securePathPermissions(dbDir, privateDirMode);
|
||||
const database = createBetterSqliteDatabase(this.dbFile);
|
||||
configureSqliteDatabase(database);
|
||||
createSchema(database);
|
||||
|
||||
try {
|
||||
migrateLegacySqliteStores(database, this.removeFile);
|
||||
migrateLegacyOnboardingMarker(database, this.removeFile);
|
||||
drainLegacyCleanup(database, this.removeFile, "legacy-json-config");
|
||||
} catch (error) {
|
||||
database.close();
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.database = database;
|
||||
secureDatabaseFilePermissions(this.dbFile);
|
||||
return database;
|
||||
}
|
||||
}
|
||||
|
||||
export const configRepository = new ConfigRepository(APP_CONFIG_DB_FILE);
|
||||
|
||||
export async function loadPersistedAppConfig(): Promise<unknown | undefined> {
|
||||
return configRepository.readAppConfig();
|
||||
}
|
||||
|
||||
export async function loadPersistedAppSetting(key: string): Promise<unknown | undefined> {
|
||||
return configRepository.readSetting(key);
|
||||
}
|
||||
|
||||
export async function replacePersistedAppConfig(value: unknown): Promise<void> {
|
||||
await configRepository.replaceAppConfig(value);
|
||||
}
|
||||
|
||||
export async function replacePersistedAppSetting(key: string, value: unknown): Promise<void> {
|
||||
await configRepository.replaceSetting(key, value);
|
||||
}
|
||||
|
||||
export async function loadPersistedRuntimeState(key: string): Promise<unknown | undefined> {
|
||||
return configRepository.readRuntimeState(key);
|
||||
}
|
||||
|
||||
export async function replacePersistedRuntimeState(key: string, value: unknown): Promise<void> {
|
||||
await configRepository.replaceRuntimeState(key, value);
|
||||
}
|
||||
|
||||
export async function deletePersistedRuntimeState(key: string): Promise<void> {
|
||||
await configRepository.deleteRuntimeState(key);
|
||||
}
|
||||
|
||||
export async function loadPersistedApiKeys(): Promise<ApiKeyConfig[]> {
|
||||
return configRepository.listApiKeys();
|
||||
}
|
||||
|
||||
export async function replacePersistedApiKeys(apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
return configRepository.replaceApiKeys(apiKeys);
|
||||
}
|
||||
|
||||
export async function replacePersistedConfigSnapshot(value: unknown, apiKeys: ApiKeyConfig[]): Promise<ApiKeyConfig[]> {
|
||||
return configRepository.replaceConfigSnapshot(value, apiKeys);
|
||||
}
|
||||
|
||||
export async function archiveLegacyJsonConfigFiles(files: string[]): Promise<void> {
|
||||
await configRepository.archiveLegacyJsonConfigFiles(files);
|
||||
}
|
||||
|
||||
function createSchema(database: SqlDatabase): void {
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
encrypted_key TEXT NOT NULL,
|
||||
encryption TEXT NOT NULL DEFAULT '${plainStorage}',
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL DEFAULT '',
|
||||
limits_json TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS api_keys_created_at_idx ON api_keys(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config_schema_migrations (
|
||||
id TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL,
|
||||
details_json TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS runtime_state (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS legacy_storage_backups (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_path TEXT NOT NULL,
|
||||
source_kind TEXT NOT NULL,
|
||||
sha256 TEXT NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
archived_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
DELETE FROM legacy_storage_backups
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM config_schema_migrations
|
||||
WHERE id = '${backupDedupMigrationId}'
|
||||
)
|
||||
AND rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM legacy_storage_backups
|
||||
GROUP BY source_path, source_kind, sha256
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS legacy_storage_backups_content_idx
|
||||
ON legacy_storage_backups(source_kind, source_path, sha256);
|
||||
|
||||
INSERT OR IGNORE INTO config_schema_migrations (id, applied_at, details_json)
|
||||
VALUES ('${backupDedupMigrationId}', '${new Date().toISOString()}', '');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS legacy_storage_cleanup (
|
||||
source_path TEXT PRIMARY KEY,
|
||||
source_kind TEXT NOT NULL,
|
||||
sha256 TEXT NOT NULL,
|
||||
queued_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO legacy_storage_cleanup (
|
||||
source_path,
|
||||
source_kind,
|
||||
sha256,
|
||||
queued_at
|
||||
)
|
||||
SELECT
|
||||
source_path,
|
||||
source_kind,
|
||||
sha256,
|
||||
archived_at
|
||||
FROM legacy_storage_backups
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM config_schema_migrations
|
||||
WHERE id = '${cleanupQueueBackfillMigrationId}'
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO config_schema_migrations (id, applied_at, details_json)
|
||||
VALUES ('${cleanupQueueBackfillMigrationId}', '${new Date().toISOString()}', '');
|
||||
`);
|
||||
}
|
||||
|
||||
function migrateLegacySqliteStores(database: SqlDatabase, removeFile: (file: string) => void): void {
|
||||
const legacyAppConfig = readLegacyAppConfigRows();
|
||||
const legacyApiKeys = readLegacyApiKeyRows();
|
||||
const appConfigRows = legacyAppConfig.rows;
|
||||
const apiKeyRows = legacyApiKeys.rows;
|
||||
const failedFiles = uniquePaths([...legacyAppConfig.failedFiles, ...legacyApiKeys.failedFiles]);
|
||||
const readableFiles = uniquePaths([...legacyAppConfig.readableFiles, ...legacyApiKeys.readableFiles])
|
||||
.filter((file) => !failedFiles.some((failedFile) => samePath(file, failedFile)));
|
||||
const backups = collectFileBackups(readableFiles.flatMap(sqliteDataFiles), "legacy-sqlite-config");
|
||||
|
||||
runTransaction(database, () => {
|
||||
for (const row of appConfigRows) {
|
||||
database.prepare(`
|
||||
INSERT INTO app_config (key, value_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO NOTHING
|
||||
`).run(row.key, row.valueJson, row.updatedAt);
|
||||
}
|
||||
insertRawApiKeyRows(database, apiKeyRows);
|
||||
insertLegacyBackups(database, backups);
|
||||
});
|
||||
|
||||
const cleanupFailures = drainLegacyCleanup(database, removeFile, "legacy-sqlite-config");
|
||||
const incompleteFiles = uniquePaths([...failedFiles, ...cleanupFailures]);
|
||||
runTransaction(database, () => {
|
||||
if (incompleteFiles.length > 0) {
|
||||
database.prepare("DELETE FROM config_schema_migrations WHERE id = ?").run(unifiedConfigMigrationId);
|
||||
return;
|
||||
}
|
||||
database.prepare(`
|
||||
INSERT INTO config_schema_migrations (id, applied_at, details_json)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
applied_at = excluded.applied_at,
|
||||
details_json = excluded.details_json
|
||||
`).run(
|
||||
unifiedConfigMigrationId,
|
||||
new Date().toISOString(),
|
||||
JSON.stringify({
|
||||
appConfigRows: appConfigRows.length,
|
||||
apiKeyRows: apiKeyRows.length,
|
||||
archivedFiles: backups.length
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (incompleteFiles.length > 0) {
|
||||
console.warn(
|
||||
`[config] Preserving legacy SQLite source${incompleteFiles.length === 1 ? "" : "s"} for a future migration retry: ${incompleteFiles.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLegacyOnboardingMarker(database: SqlDatabase, removeFile: (file: string) => void): void {
|
||||
if (!existsSync(ONBOARDING_FINISHED_FILE)) {
|
||||
drainLegacyCleanup(database, removeFile, "legacy-onboarding-state");
|
||||
return;
|
||||
}
|
||||
const backup = collectFileBackups([ONBOARDING_FINISHED_FILE], "legacy-onboarding-state")[0];
|
||||
if (!backup) {
|
||||
return;
|
||||
}
|
||||
const existing = queryRows(
|
||||
database,
|
||||
"SELECT key FROM app_config WHERE key = ? LIMIT 1",
|
||||
[ONBOARDING_FINISHED_AT_SETTING_KEY]
|
||||
).length > 0;
|
||||
|
||||
runTransaction(database, () => {
|
||||
if (!existing) {
|
||||
const finishedAt = statSync(ONBOARDING_FINISHED_FILE).mtime.toISOString();
|
||||
replaceAppConfigRow(database, ONBOARDING_FINISHED_AT_SETTING_KEY, finishedAt);
|
||||
}
|
||||
insertLegacyBackups(database, [backup]);
|
||||
});
|
||||
drainLegacyCleanup(database, removeFile, "legacy-onboarding-state");
|
||||
}
|
||||
|
||||
function archiveAndRemoveFiles(
|
||||
database: SqlDatabase,
|
||||
files: string[],
|
||||
sourceKind: string,
|
||||
removeFile: (file: string) => void
|
||||
): void {
|
||||
const backups = collectFileBackups(uniquePaths(files), sourceKind);
|
||||
if (backups.length > 0) {
|
||||
runTransaction(database, () => insertLegacyBackups(database, backups));
|
||||
}
|
||||
drainLegacyCleanup(database, removeFile, sourceKind);
|
||||
}
|
||||
|
||||
type LegacyBackup = {
|
||||
content: Buffer;
|
||||
id: string;
|
||||
sha256: string;
|
||||
sourceKind: string;
|
||||
sourcePath: string;
|
||||
};
|
||||
|
||||
function collectFileBackups(files: string[], sourceKind: string): LegacyBackup[] {
|
||||
return files.flatMap((file): LegacyBackup[] => {
|
||||
if (!existsSync(file) || samePath(file, APP_CONFIG_DB_FILE)) {
|
||||
return [];
|
||||
}
|
||||
const content = readFileSync(file);
|
||||
const sha256 = createHash("sha256").update(content).digest("hex");
|
||||
return [{
|
||||
content,
|
||||
id: createHash("sha256").update(`${sourceKind}\0${resolve(file)}\0${sha256}`).digest("hex"),
|
||||
sha256,
|
||||
sourceKind,
|
||||
sourcePath: resolve(file)
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function insertLegacyBackups(database: SqlDatabase, backups: LegacyBackup[]): void {
|
||||
const archivedAt = new Date().toISOString();
|
||||
const backupStatement = database.prepare(`
|
||||
INSERT OR IGNORE INTO legacy_storage_backups (
|
||||
id, source_path, source_kind, sha256, content, archived_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
const cleanupStatement = database.prepare(`
|
||||
INSERT INTO legacy_storage_cleanup (
|
||||
source_path, source_kind, sha256, queued_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(source_path) DO UPDATE SET
|
||||
source_kind = excluded.source_kind,
|
||||
sha256 = excluded.sha256,
|
||||
queued_at = excluded.queued_at
|
||||
`);
|
||||
for (const backup of backups) {
|
||||
backupStatement.run(
|
||||
backup.id,
|
||||
backup.sourcePath,
|
||||
backup.sourceKind,
|
||||
backup.sha256,
|
||||
backup.content,
|
||||
archivedAt
|
||||
);
|
||||
cleanupStatement.run(
|
||||
backup.sourcePath,
|
||||
backup.sourceKind,
|
||||
backup.sha256,
|
||||
archivedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function drainLegacyCleanup(
|
||||
database: SqlDatabase,
|
||||
removeFile: (file: string) => void,
|
||||
sourceKind: string
|
||||
): string[] {
|
||||
const rows = queryRows(
|
||||
database,
|
||||
`
|
||||
SELECT source_path, sha256
|
||||
FROM legacy_storage_cleanup
|
||||
WHERE source_kind = ?
|
||||
ORDER BY queued_at, source_path
|
||||
`,
|
||||
[sourceKind]
|
||||
);
|
||||
const failedFiles: string[] = [];
|
||||
for (const row of rows) {
|
||||
const file = readString(row.source_path);
|
||||
const archivedSha256 = readString(row.sha256);
|
||||
if (!file || !archivedSha256) {
|
||||
continue;
|
||||
}
|
||||
if (!existsSync(file)) {
|
||||
deleteLegacyCleanupRow(database, file);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const currentSha256 = createHash("sha256").update(readFileSync(file)).digest("hex");
|
||||
if (currentSha256 !== archivedSha256) {
|
||||
const matchingBackup = queryRows(
|
||||
database,
|
||||
`
|
||||
SELECT id
|
||||
FROM legacy_storage_backups
|
||||
WHERE source_path = ? AND sha256 = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[file, currentSha256]
|
||||
)[0];
|
||||
if (!matchingBackup) {
|
||||
failedFiles.push(file);
|
||||
console.warn(`[config] Preserving changed legacy config source for a future migration retry: ${file}`);
|
||||
continue;
|
||||
}
|
||||
database.prepare(`
|
||||
UPDATE legacy_storage_cleanup
|
||||
SET sha256 = ?, queued_at = ?
|
||||
WHERE source_path = ?
|
||||
`).run(currentSha256, new Date().toISOString(), file);
|
||||
}
|
||||
removeFile(file);
|
||||
if (existsSync(file)) {
|
||||
throw new Error("source still exists after cleanup");
|
||||
}
|
||||
deleteLegacyCleanupRow(database, file);
|
||||
} catch (error) {
|
||||
failedFiles.push(file);
|
||||
console.warn(`[config] Failed to remove migrated config source ${file}: ${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
return failedFiles;
|
||||
}
|
||||
|
||||
function deleteLegacyCleanupRow(database: SqlDatabase, file: string): void {
|
||||
database.prepare("DELETE FROM legacy_storage_cleanup WHERE source_path = ?").run(file);
|
||||
}
|
||||
|
||||
type LegacyAppConfigRow = {
|
||||
key: string;
|
||||
updatedAt: string;
|
||||
valueJson: string;
|
||||
};
|
||||
|
||||
type LegacySqliteReadResult<Row> = {
|
||||
failedFiles: string[];
|
||||
readableFiles: string[];
|
||||
rows: Row[];
|
||||
};
|
||||
|
||||
function readLegacyAppConfigRows(): LegacySqliteReadResult<LegacyAppConfigRow> {
|
||||
const result: LegacySqliteReadResult<LegacyAppConfigRow> = {
|
||||
failedFiles: [],
|
||||
readableFiles: [],
|
||||
rows: []
|
||||
};
|
||||
for (const dbFile of LEGACY_APP_CONFIG_DB_FILES) {
|
||||
if (!existsSync(dbFile) || samePath(dbFile, APP_CONFIG_DB_FILE)) {
|
||||
continue;
|
||||
}
|
||||
let database: SqlDatabase | undefined;
|
||||
try {
|
||||
database = createBetterSqliteDatabase(dbFile, { fileMustExist: true, readonly: true });
|
||||
const rows = queryRows(database, "SELECT key, value_json, updated_at FROM app_config");
|
||||
result.readableFiles.push(dbFile);
|
||||
result.rows.push(...rows.flatMap((row): LegacyAppConfigRow[] => {
|
||||
const key = readString(row.key);
|
||||
const valueJson = readString(row.value_json);
|
||||
if (!key || !valueJson) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
key,
|
||||
updatedAt: readString(row.updated_at) || new Date(0).toISOString(),
|
||||
valueJson
|
||||
}];
|
||||
}));
|
||||
} catch (error) {
|
||||
result.failedFiles.push(dbFile);
|
||||
console.warn(`[config] Failed to read legacy app config database ${dbFile}: ${formatError(error)}`);
|
||||
} finally {
|
||||
database?.close();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function readLegacyApiKeyRows(): LegacySqliteReadResult<Record<string, SqlValue>> {
|
||||
const result: LegacySqliteReadResult<Record<string, SqlValue>> = {
|
||||
failedFiles: [],
|
||||
readableFiles: [],
|
||||
rows: []
|
||||
};
|
||||
for (const dbFile of LEGACY_API_KEYS_DB_FILES) {
|
||||
if (!existsSync(dbFile) || samePath(dbFile, APP_CONFIG_DB_FILE)) {
|
||||
continue;
|
||||
}
|
||||
let database: SqlDatabase | undefined;
|
||||
try {
|
||||
database = createBetterSqliteDatabase(dbFile, { fileMustExist: true, readonly: true });
|
||||
const rows = queryRows(database, `
|
||||
SELECT id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
FROM api_keys
|
||||
ORDER BY rowid
|
||||
`);
|
||||
result.readableFiles.push(dbFile);
|
||||
result.rows.push(...rows);
|
||||
} catch (error) {
|
||||
result.failedFiles.push(dbFile);
|
||||
console.warn(`[config] Failed to read legacy API key database ${dbFile}: ${formatError(error)}`);
|
||||
} finally {
|
||||
database?.close();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function insertRawApiKeyRows(database: SqlDatabase, rows: Array<Record<string, SqlValue>>): void {
|
||||
const statement = database.prepare(`
|
||||
INSERT OR IGNORE INTO api_keys (
|
||||
id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const stored = toStoredApiKeyRow(row);
|
||||
if (!stored) {
|
||||
continue;
|
||||
}
|
||||
statement.run(
|
||||
stored.id,
|
||||
stored.name,
|
||||
stored.storedKey,
|
||||
stored.encryption,
|
||||
stored.createdAt,
|
||||
stored.expiresAt,
|
||||
stored.limitsJson
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAppConfigRow(database: SqlDatabase, key: string, value: unknown): void {
|
||||
replaceJsonRow(database, "app_config", key, value);
|
||||
}
|
||||
|
||||
function replaceJsonRow(database: SqlDatabase, table: "app_config" | "runtime_state", key: string, value: unknown): void {
|
||||
database.prepare(`
|
||||
INSERT INTO ${table} (key, value_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value_json = excluded.value_json,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(key, JSON.stringify(value), new Date().toISOString());
|
||||
}
|
||||
|
||||
function listApiKeys(database: SqlDatabase): ApiKeyConfig[] {
|
||||
const rows = queryRows(database, `
|
||||
SELECT id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
FROM api_keys
|
||||
ORDER BY rowid
|
||||
`);
|
||||
return uniqueApiKeyConfigs(rows.map(toApiKeyConfig));
|
||||
}
|
||||
|
||||
function replaceApiKeyRows(database: SqlDatabase, apiKeys: ApiKeyConfig[]): void {
|
||||
const statement = database.prepare(`
|
||||
INSERT INTO api_keys (
|
||||
id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
database.exec("DELETE FROM api_keys");
|
||||
for (const apiKey of apiKeys) {
|
||||
statement.run(
|
||||
apiKey.id,
|
||||
apiKey.name ?? "",
|
||||
apiKey.key,
|
||||
plainStorage,
|
||||
apiKey.createdAt,
|
||||
apiKey.expiresAt ?? "",
|
||||
apiKey.limits ? JSON.stringify(apiKey.limits) : ""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toApiKeyConfig(row: Record<string, SqlValue>): ApiKeyConfig | undefined {
|
||||
const stored = toStoredApiKeyRow(row);
|
||||
if (!stored) {
|
||||
return undefined;
|
||||
}
|
||||
if (stored.encryption !== plainStorage) {
|
||||
console.warn(`[api-keys] Stored API key uses unsupported storage "${stored.encryption}". Re-save the API key to migrate it.`);
|
||||
return undefined;
|
||||
}
|
||||
const key = stored.storedKey.trim();
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
const limits = parseApiKeyLimits(stored.limitsJson);
|
||||
return {
|
||||
createdAt: stored.createdAt,
|
||||
...(stored.expiresAt ? { expiresAt: stored.expiresAt } : {}),
|
||||
id: stored.id,
|
||||
key,
|
||||
...(limits ? { limits } : {}),
|
||||
...(stored.name ? { name: stored.name } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function toStoredApiKeyRow(row: Record<string, SqlValue>): StoredApiKeyRow | undefined {
|
||||
const id = readString(row.id);
|
||||
const storedKey = readString(row.encrypted_key);
|
||||
if (!id || !storedKey) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
createdAt: readString(row.created_at) || new Date(0).toISOString(),
|
||||
encryption: readString(row.encryption) || plainStorage,
|
||||
expiresAt: readString(row.expires_at) || "",
|
||||
id,
|
||||
limitsJson: readString(row.limits_json) || "",
|
||||
name: readString(row.name) || "",
|
||||
storedKey
|
||||
};
|
||||
}
|
||||
|
||||
function parseApiKeyLimits(value: string): ApiKeyLimitConfig | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (!isObject(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
const limits: ApiKeyLimitConfig = {};
|
||||
for (const key of ["ipd", "iph", "ipm", "maxRequests", "maxTokens", "quotaWindowMs", "rpd", "rph", "rpm", "tpd", "tph", "tpm", "windowMs"] as const) {
|
||||
const limit = readPositiveInteger(parsed[key]);
|
||||
if (limit) {
|
||||
limits[key] = limit;
|
||||
}
|
||||
}
|
||||
return Object.keys(limits).length ? limits : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueApiKeyConfigs(values: Array<ApiKeyConfig | undefined>): ApiKeyConfig[] {
|
||||
const seenKeys = new Set<string>();
|
||||
const seenIds = new Set<string>();
|
||||
const result: ApiKeyConfig[] = [];
|
||||
for (const [index, value] of values.entries()) {
|
||||
const key = value?.key.trim();
|
||||
if (!value || !key || seenKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
const id = uniqueApiKeyId(value.id || `key-${index + 1}`, seenIds, index);
|
||||
result.push({
|
||||
createdAt: value.createdAt || new Date(0).toISOString(),
|
||||
...(value.expiresAt ? { expiresAt: value.expiresAt } : {}),
|
||||
id,
|
||||
key,
|
||||
...(value.limits ? { limits: value.limits } : {}),
|
||||
...(value.name ? { name: value.name } : {})
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function uniqueApiKeyId(id: string, seenIds: Set<string>, index: number): string {
|
||||
const base = id.trim() || `key-${index + 1}`;
|
||||
let candidate = base;
|
||||
let suffix = 2;
|
||||
while (seenIds.has(candidate)) {
|
||||
candidate = `${base}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
seenIds.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function runTransaction(database: SqlDatabase, operation: () => void): void {
|
||||
try {
|
||||
database.exec("BEGIN IMMEDIATE");
|
||||
operation();
|
||||
database.exec("COMMIT");
|
||||
} catch (error) {
|
||||
try {
|
||||
database.exec("ROLLBACK");
|
||||
} catch {
|
||||
// Preserve the original transaction error.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function sqliteDataFiles(file: string): string[] {
|
||||
return [file, `${file}-wal`, `${file}-shm`];
|
||||
}
|
||||
|
||||
function configureSqliteDatabase(database: SqlDatabase): void {
|
||||
database.pragma("journal_mode = WAL");
|
||||
database.pragma("synchronous = NORMAL");
|
||||
database.pragma("busy_timeout = 5000");
|
||||
}
|
||||
|
||||
function queryRows(database: SqlDatabase, sql: string, params: SqlValue[] = []): Array<Record<string, SqlValue>> {
|
||||
return database.prepare(sql).all(...params) as Array<Record<string, SqlValue>>;
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: unknown): number | undefined {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? Math.ceil(number) : undefined;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function secureDatabaseFilePermissions(file: string): void {
|
||||
securePathPermissions(file, privateFileMode);
|
||||
securePathPermissions(`${file}-wal`, privateFileMode);
|
||||
securePathPermissions(`${file}-shm`, privateFileMode);
|
||||
}
|
||||
|
||||
function securePathPermissions(file: string, mode: number): void {
|
||||
if (process.platform === "win32" || !existsSync(file)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
chmodSync(file, mode);
|
||||
} catch {
|
||||
// Best effort for filesystems that do not support chmod.
|
||||
}
|
||||
}
|
||||
|
||||
function uniquePaths(files: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
return files.filter((file) => {
|
||||
const normalized = resolve(file).toLowerCase();
|
||||
if (seen.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(normalized);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function samePath(left: string, right: string): boolean {
|
||||
return resolve(left).toLowerCase() === resolve(right).toLowerCase();
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
@ -1,13 +1,19 @@
|
|||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/config/app-config-store";
|
||||
import { loadPersistedApiKeys, replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "@ccr/core/config/constants";
|
||||
import {
|
||||
archiveLegacyJsonConfigFiles,
|
||||
loadPersistedApiKeys,
|
||||
loadPersistedAppConfig,
|
||||
replacePersistedApiKeys,
|
||||
replacePersistedAppConfig,
|
||||
replacePersistedConfigSnapshot
|
||||
} from "@ccr/core/config/config-repository";
|
||||
import { LEGACY_ACTIVE_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "@ccr/core/config/constants";
|
||||
import { normalizeCodexProviderAccountConfig } from "@ccr/core/agents/local-providers/codex";
|
||||
import { normalizeGrokProviderAccountConfig, normalizeGrokProviderMediaCapabilities } from "@ccr/core/agents/local-providers/grok";
|
||||
import { removeOpenCodeProviderAccountConfig } from "@ccr/core/agents/local-providers/opencode";
|
||||
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_DESIGN_PLUGIN_ID, CLAUDE_SHIP_PLUGIN_ID, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, GATEWAY_PLUGIN_PERMISSION_IDS, GATEWAY_PLUGIN_SURFACE_IDS, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, ROUTER_SCRIPT_API_VERSION, ROUTER_SCRIPT_DEFAULT_TIMEOUT_MS, ROUTER_SCRIPT_MAX_TIMEOUT_MS, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent, knownGatewayPluginDefaultApps, knownGatewayPluginDefaultPermissions, knownGatewayPluginDefaultSurfaces } from "@ccr/core/contracts/app";
|
||||
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_DESIGN_PLUGIN_ID, CLAUDE_SHIP_PLUGIN_ID, DEFAULT_TRAY_COMPONENT_VARIANTS, GATEWAY_PLUGIN_PERMISSION_IDS, GATEWAY_PLUGIN_SURFACE_IDS, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, ROUTER_SCRIPT_API_VERSION, ROUTER_SCRIPT_DEFAULT_TIMEOUT_MS, ROUTER_SCRIPT_MAX_TIMEOUT_MS, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent, knownGatewayPluginDefaultApps, knownGatewayPluginDefaultPermissions, knownGatewayPluginDefaultSurfaces } from "@ccr/core/contracts/app";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config";
|
||||
import { maxRequestLogBodyBytes } from "@ccr/core/observability/request-log-limits";
|
||||
import { findProviderPresetByBaseUrl, primaryProviderPresetEndpoint, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
|
||||
|
|
@ -32,7 +38,6 @@ import type {
|
|||
GatewayProviderCapability,
|
||||
GatewayProviderCapabilityProtocol,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderProtocol,
|
||||
MediaToolsConfig,
|
||||
ObservabilityConfig,
|
||||
OverviewMetricKind,
|
||||
|
|
@ -99,10 +104,16 @@ type LoadedAppConfig = Partial<Omit<AppConfig, "Router" | "agent" | "botGateway"
|
|||
export type RawAppConfigSource = "default" | "legacy-json" | "sqlite";
|
||||
|
||||
type RawAppConfigLoadResult = {
|
||||
legacyJsonFile?: string;
|
||||
source: RawAppConfigSource;
|
||||
value: Partial<AppConfig>;
|
||||
};
|
||||
|
||||
type LegacyJsonConfigLoadResult = {
|
||||
file: string;
|
||||
value: Partial<AppConfig>;
|
||||
};
|
||||
|
||||
const REMOVED_LEGACY_ROUTER_RULE_IDS = new Set([
|
||||
"legacy-subagent",
|
||||
"legacy-background",
|
||||
|
|
@ -115,8 +126,7 @@ const GENERATED_GATEWAY_API_KEY_ID = "local-gateway";
|
|||
const GATEWAY_PLUGIN_PERMISSION_ID_SET = new Set<string>(GATEWAY_PLUGIN_PERMISSION_IDS);
|
||||
|
||||
const DEFAULT_CONFIG: AppConfig = createDefaultAppConfig({
|
||||
coreHost: INTERNAL_GATEWAY_CORE_HOST,
|
||||
generatedConfigFile: GATEWAY_CONFIG_FILE
|
||||
coreHost: INTERNAL_GATEWAY_CORE_HOST
|
||||
});
|
||||
|
||||
function completeBotGatewayConfig(config: LoadedBotGatewayConfig | undefined): BotGatewayRuntimeConfig {
|
||||
|
|
@ -280,7 +290,6 @@ export async function loadAppConfig(): Promise<AppConfig> {
|
|||
...gatewayConfig,
|
||||
coreHost: INTERNAL_GATEWAY_CORE_HOST,
|
||||
corePort,
|
||||
generatedConfigFile: GATEWAY_CONFIG_FILE,
|
||||
host: gatewayConfig.host ?? host,
|
||||
port: gatewayConfig.port ?? port
|
||||
},
|
||||
|
|
@ -329,11 +338,13 @@ export async function loadAppConfig(): Promise<AppConfig> {
|
|||
const shouldPersistApiKeys = loadedApiKeys.length === 0 || hasConfigFileApiKeys(rawValue) || configFileApiKeys.length > 0;
|
||||
const shouldRepairProviderCapabilities = hasUnsupportedNvidiaCapabilities(value.Providers);
|
||||
const shouldRepairKnownPlugins = pluginMigration.changed;
|
||||
if (shouldPersistApiKeys) {
|
||||
await replacePersistedApiKeys(apiKeys);
|
||||
}
|
||||
if (loadedRawConfig.source !== "sqlite" || shouldPersistApiKeys || shouldRepairProviderCapabilities || shouldRepairKnownPlugins) {
|
||||
await writeSanitizedConfig(config);
|
||||
await replacePersistedConfigSnapshot(sanitizeConfigForDisk(config), apiKeys);
|
||||
}
|
||||
if (loadedRawConfig.source === "legacy-json" && loadedRawConfig.legacyJsonFile) {
|
||||
await archiveLegacyJsonConfigFiles([loadedRawConfig.legacyJsonFile]).catch((archiveError) => {
|
||||
console.warn(`[config] Failed to archive legacy JSON config: ${formatError(archiveError)}`);
|
||||
});
|
||||
}
|
||||
return config;
|
||||
} catch (error) {
|
||||
|
|
@ -381,14 +392,13 @@ async function saveAppConfigNow(config: AppConfig): Promise<AppConfig> {
|
|||
assertProviderApiKeysAreSafe(normalizedConfig);
|
||||
const apiKeys = ensureGatewayApiKeys(normalizeApiKeys(normalizedConfig.APIKEYS, normalizedConfig.APIKEY).filter((apiKey) => !isDefaultSeedApiKey(apiKey)));
|
||||
const pluginMigration = migrateKnownGatewayPluginConfigs(normalizedConfig.plugins);
|
||||
await replacePersistedApiKeys(apiKeys);
|
||||
await writeSanitizedConfig({
|
||||
await replacePersistedConfigSnapshot(sanitizeConfigForDisk({
|
||||
...normalizedConfig,
|
||||
theme: appThemePreferenceOverride ?? normalizedConfig.theme,
|
||||
APIKEY: apiKeys[0]?.key ?? "",
|
||||
APIKEYS: apiKeys,
|
||||
plugins: pluginMigration.plugins
|
||||
});
|
||||
}), apiKeys);
|
||||
return loadAppConfig();
|
||||
}
|
||||
|
||||
|
|
@ -593,8 +603,9 @@ async function loadRawAppConfig(): Promise<RawAppConfigLoadResult> {
|
|||
const legacyConfig = readLegacyJsonConfig();
|
||||
if (legacyConfig) {
|
||||
return {
|
||||
legacyJsonFile: legacyConfig.file,
|
||||
source: "legacy-json",
|
||||
value: legacyConfig
|
||||
value: legacyConfig.value
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -604,8 +615,8 @@ async function loadRawAppConfig(): Promise<RawAppConfigLoadResult> {
|
|||
};
|
||||
}
|
||||
|
||||
function readLegacyJsonConfig(): Partial<AppConfig> | undefined {
|
||||
const files = uniqueStrings([CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
||||
function readLegacyJsonConfig(): LegacyJsonConfigLoadResult | undefined {
|
||||
const files = uniqueStrings([LEGACY_ACTIVE_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
||||
for (const file of files) {
|
||||
if (!existsSync(file)) {
|
||||
continue;
|
||||
|
|
@ -613,7 +624,10 @@ function readLegacyJsonConfig(): Partial<AppConfig> | undefined {
|
|||
try {
|
||||
const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
|
||||
if (isObject(parsed)) {
|
||||
return parsed as Partial<AppConfig>;
|
||||
return {
|
||||
file,
|
||||
value: parsed as Partial<AppConfig>
|
||||
};
|
||||
}
|
||||
console.warn(`[config] Ignoring legacy config with non-object root: ${file}`);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -10,19 +10,22 @@ export { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR };
|
|||
export const CONFIGDIR = resolveRuntimeConfigDir();
|
||||
export const LEGACY_WINDOWS_CONFIGDIR = path.join(resolveRuntimeAppPath("appData"), APP_NAME);
|
||||
export const LEGACY_WINDOWS_CONFIG_FILE = path.join(LEGACY_WINDOWS_CONFIGDIR, "config.json");
|
||||
export const CONFIG_FILE = path.join(CONFIGDIR, "config.json");
|
||||
export const LEGACY_ACTIVE_CONFIG_FILE = path.join(CONFIGDIR, "config.json");
|
||||
export const ONBOARDING_FINISHED_FILE = path.join(CONFIGDIR, ".onboard_finished");
|
||||
export const ONBOARDING_FINISHED_AT_SETTING_KEY = "onboardingFinishedAt";
|
||||
export const DATADIR = resolveRuntimeDataDir();
|
||||
export const APP_CONFIG_DB_FILE = path.join(CONFIGDIR, "config.sqlite");
|
||||
export const API_KEYS_DB_FILE = path.join(DATADIR, "api-keys.sqlite");
|
||||
export const LEGACY_API_KEYS_DB_FILE = path.join(DATADIR, "api-keys.sqlite");
|
||||
export const LEGACY_APP_CONFIG_DB_FILES = process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "config.sqlite")] : [];
|
||||
export const LEGACY_API_KEYS_DB_FILES = process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "api-keys.sqlite")] : [];
|
||||
export const LEGACY_API_KEYS_DB_FILES = [
|
||||
LEGACY_API_KEYS_DB_FILE,
|
||||
...(process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "api-keys.sqlite")] : [])
|
||||
];
|
||||
export const CERTDIR = path.join(DATADIR, "certs");
|
||||
export const PROVIDER_ICON_CACHE_DIR = path.join(DATADIR, "provider-icons");
|
||||
export const PROXY_CA_CERT_FILE = path.join(CERTDIR, "ca.pem");
|
||||
export const PROXY_CA_CERT_DER_FILE = path.join(CERTDIR, "ca.cer");
|
||||
export const PROXY_CA_KEY_FILE = path.join(CERTDIR, "key.pem");
|
||||
export const GATEWAY_CONFIG_FILE = path.join(CONFIGDIR, "gateway.config.json");
|
||||
export const REQUEST_LOGS_DB_FILE = path.join(DATADIR, "request-logs.sqlite");
|
||||
export const CONTEXT_ARCHIVE_DB_FILE = path.join(DATADIR, "context-archive.sqlite");
|
||||
export const RAW_TRACE_SPOOL_DIR = path.join(DATADIR, "raw-trace-spool");
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ export const DEFAULT_PROXY_TARGETS: ProxyRouteTarget[] = [
|
|||
|
||||
export type DefaultAppConfigOptions = {
|
||||
coreHost?: string;
|
||||
generatedConfigFile: string;
|
||||
};
|
||||
|
||||
export function createDefaultAppConfig(options: DefaultAppConfigOptions): AppConfig {
|
||||
export function createDefaultAppConfig(options: DefaultAppConfigOptions = {}): AppConfig {
|
||||
const coreHost = options.coreHost ?? "127.0.0.1";
|
||||
return {
|
||||
APIKEY: "",
|
||||
|
|
@ -106,7 +105,6 @@ export function createDefaultAppConfig(options: DefaultAppConfigOptions): AppCon
|
|||
coreHost,
|
||||
corePort: 3457,
|
||||
enabled: true,
|
||||
generatedConfigFile: options.generatedConfigFile,
|
||||
host: "127.0.0.1",
|
||||
port: 3456
|
||||
},
|
||||
|
|
|
|||
27
packages/core/src/config/onboarding-state.ts
Normal file
27
packages/core/src/config/onboarding-state.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import {
|
||||
loadPersistedAppSetting,
|
||||
replacePersistedAppSetting
|
||||
} from "@ccr/core/config/config-repository";
|
||||
import {
|
||||
ONBOARDING_FINISHED_AT_SETTING_KEY,
|
||||
ONBOARDING_FINISHED_FILE
|
||||
} from "@ccr/core/config/constants";
|
||||
|
||||
export async function loadOnboardingFinished(): Promise<boolean> {
|
||||
try {
|
||||
const persisted = await loadPersistedAppSetting(ONBOARDING_FINISHED_AT_SETTING_KEY);
|
||||
return typeof persisted === "string" && Boolean(persisted.trim());
|
||||
} catch (error) {
|
||||
console.warn(`[config] Failed to load onboarding state: ${formatError(error)}`);
|
||||
return existsSync(ONBOARDING_FINISHED_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markOnboardingFinished(): Promise<void> {
|
||||
await replacePersistedAppSetting(ONBOARDING_FINISHED_AT_SETTING_KEY, new Date().toISOString());
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
export type AppInfo = {
|
||||
appConfigDbFile: string;
|
||||
apiKeysDbFile: string;
|
||||
chatgptAppPath?: string;
|
||||
configDbFile: string;
|
||||
configDir: string;
|
||||
configFile: string;
|
||||
dataDir: string;
|
||||
desktop: boolean;
|
||||
gatewayConfigFile: string;
|
||||
launchAtLoginSupported: boolean;
|
||||
requestLogsDbFile: string;
|
||||
name: string;
|
||||
|
|
@ -729,7 +726,6 @@ export type GatewayRuntimeConfig = {
|
|||
coreHost: string;
|
||||
corePort: number;
|
||||
enabled: boolean;
|
||||
generatedConfigFile: string;
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
|
@ -1778,7 +1774,6 @@ export type GatewayStatus = {
|
|||
coreEndpoint: string;
|
||||
coreManagedExternally?: boolean;
|
||||
endpoint: string;
|
||||
generatedConfigFile: string;
|
||||
lastError?: string;
|
||||
lastStartedAt?: string;
|
||||
networkEndpoints: GatewayNetworkEndpoint[];
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import { getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch
|
|||
import { pluginService } from "@ccr/core/plugins/service";
|
||||
import { proxyService } from "@ccr/core/proxy/service";
|
||||
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
|
||||
import { writeCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-writer";
|
||||
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler";
|
||||
import { closeServer, formatError } from "@ccr/core/gateway/http/io";
|
||||
import { RawTraceSynchronizer } from "@ccr/core/observability/raw-trace-sync";
|
||||
import { GatewayBillingSynchronizer } from "@ccr/core/usage/billing-sync";
|
||||
import { assertLoopbackCoreHost, endpoint, gatewayNetworkEndpoints, generateCoreGatewayAuthToken, isCoreGatewayHealthy, loopbackCoreHostError, removeManagedCoreGatewayMarker, shouldRunGatewayRuntime, shouldRunUnifiedServer, spawnGatewayProcess, stopPreviousManagedCoreGateway, writeManagedCoreGatewayMarker } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { assertLoopbackCoreHost, endpoint, gatewayNetworkEndpoints, generateCoreGatewayAuthToken, isCoreGatewayHealthy, loopbackCoreHostError, removeManagedCoreGatewayMarker, shouldRunGatewayRuntime, shouldRunUnifiedServer, spawnGatewayProcess, stopPreviousManagedCoreGateway, waitForManagedCoreGatewayReady, writeManagedCoreGatewayMarker } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { coreGatewayAuthHeader } from "@ccr/core/gateway/internal/shared";
|
||||
import type { BrowserAutomationMcpIntegration, BrowserWebSearchMcpIntegration, GatewayStopOptions } from "@ccr/core/gateway/internal/shared";
|
||||
import { GatewayRequestPipeline } from "@ccr/core/gateway/request/pipeline";
|
||||
|
|
@ -71,7 +71,6 @@ class GatewayService {
|
|||
private status: GatewayStatus = {
|
||||
coreEndpoint: "",
|
||||
endpoint: "",
|
||||
generatedConfigFile: "",
|
||||
networkEndpoints: [],
|
||||
state: "stopped"
|
||||
};
|
||||
|
|
@ -105,7 +104,6 @@ class GatewayService {
|
|||
this.status = {
|
||||
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
|
||||
endpoint: endpoint(config.gateway.host, config.gateway.port),
|
||||
generatedConfigFile: config.gateway.generatedConfigFile,
|
||||
networkEndpoints: gatewayNetworkEndpoints(config.gateway.host, config.gateway.port),
|
||||
state: "starting"
|
||||
};
|
||||
|
|
@ -146,7 +144,7 @@ class GatewayService {
|
|||
if (shouldRunGateway) {
|
||||
await proxyService.refreshUpstreamProxyFromCurrentSystem();
|
||||
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https", config);
|
||||
await writeCoreGatewayConfig(
|
||||
const coreGatewayConfig = await compileCoreGatewayConfig(
|
||||
config,
|
||||
this.rawTraceSynchronizer.token,
|
||||
this.billingSynchronizer.token,
|
||||
|
|
@ -154,20 +152,32 @@ class GatewayService {
|
|||
this.browserWebSearchMcpIntegration,
|
||||
upstreamProxyUrl
|
||||
);
|
||||
await stopPreviousManagedCoreGateway(config, this.status.coreEndpoint);
|
||||
await stopPreviousManagedCoreGateway(this.status.coreEndpoint);
|
||||
if (await isCoreGatewayHealthy(this.status.coreEndpoint)) {
|
||||
throw new Error(`Core gateway endpoint is already in use: ${this.status.coreEndpoint}`);
|
||||
}
|
||||
const runtimeId = randomUUID();
|
||||
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, coreAuthToken);
|
||||
const spawnedGateway = spawnGatewayProcess(config, coreGatewayConfig, upstreamProxyUrl, runtimeId, coreAuthToken);
|
||||
this.child = spawnedGateway.child;
|
||||
this.coreAuthToken = coreAuthToken;
|
||||
const managedChild = this.child;
|
||||
writeManagedCoreGatewayMarker(config, this.child, runtimeId);
|
||||
let markerWritePromise: Promise<void> | undefined;
|
||||
let startupFailure: Error | undefined;
|
||||
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`));
|
||||
this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
|
||||
this.child.on("exit", (code, signal) => {
|
||||
void this.handleCoreGatewayExit(managedChild, code, signal);
|
||||
this.child.once("error", (error) => {
|
||||
startupFailure ??= new Error(`Core gateway failed to start: ${formatError(error)}`);
|
||||
void this.handleCoreGatewayTermination(managedChild, markerWritePromise, startupFailure.message);
|
||||
});
|
||||
this.child.once("exit", (code, signal) => {
|
||||
startupFailure ??= new Error(coreGatewayExitMessage(code, signal));
|
||||
void this.handleCoreGatewayTermination(managedChild, markerWritePromise, startupFailure.message);
|
||||
});
|
||||
markerWritePromise = writeManagedCoreGatewayMarker(managedChild, runtimeId);
|
||||
await Promise.all([markerWritePromise, spawnedGateway.configAccepted]);
|
||||
assertManagedGatewayStartupContinues(managedChild, startupFailure);
|
||||
await waitForManagedCoreGatewayReady(this.status.coreEndpoint, runtimeId, managedChild);
|
||||
assertManagedGatewayStartupContinues(managedChild, startupFailure);
|
||||
}
|
||||
|
||||
this.status = {
|
||||
|
|
@ -191,13 +201,14 @@ class GatewayService {
|
|||
|
||||
async stop(options: GatewayStopOptions = {}): Promise<GatewayStatus> {
|
||||
const child = this.child;
|
||||
const config = this.config;
|
||||
this.child = undefined;
|
||||
this.coreAuthToken = "";
|
||||
if (child && !child.killed) {
|
||||
child.kill();
|
||||
}
|
||||
removeManagedCoreGatewayMarker(config);
|
||||
if (child) {
|
||||
await removeManagedCoreGatewayMarkerBestEffort();
|
||||
}
|
||||
|
||||
const server = this.server;
|
||||
this.server = undefined;
|
||||
|
|
@ -255,7 +266,6 @@ class GatewayService {
|
|||
...this.status,
|
||||
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
|
||||
endpoint: endpoint(config.gateway.host, config.gateway.port),
|
||||
generatedConfigFile: config.gateway.generatedConfigFile,
|
||||
networkEndpoints: gatewayNetworkEndpoints(config.gateway.host, config.gateway.port)
|
||||
};
|
||||
}
|
||||
|
|
@ -344,15 +354,22 @@ class GatewayService {
|
|||
});
|
||||
}
|
||||
|
||||
private async handleCoreGatewayExit(child: ChildProcess, code: number | null, signal: NodeJS.Signals | null): Promise<void> {
|
||||
private async handleCoreGatewayTermination(
|
||||
child: ChildProcess,
|
||||
markerWritePromise: Promise<void> | undefined,
|
||||
message: string
|
||||
): Promise<void> {
|
||||
await markerWritePromise?.catch(() => undefined);
|
||||
if (this.child !== child || this.status.state === "stopped") {
|
||||
return;
|
||||
}
|
||||
removeManagedCoreGatewayMarker(this.config);
|
||||
this.child = undefined;
|
||||
this.coreAuthToken = "";
|
||||
await removeManagedCoreGatewayMarkerBestEffort();
|
||||
this.status = {
|
||||
...this.status,
|
||||
coreManagedExternally: undefined,
|
||||
lastError: `Core gateway exited with ${signal ?? code ?? "unknown status"}`,
|
||||
lastError: message,
|
||||
pid: undefined,
|
||||
state: "error"
|
||||
};
|
||||
|
|
@ -368,5 +385,21 @@ class GatewayService {
|
|||
|
||||
}
|
||||
|
||||
function coreGatewayExitMessage(code: number | null, signal: NodeJS.Signals | null): string {
|
||||
return `Core gateway exited with ${signal ?? code ?? "unknown status"}`;
|
||||
}
|
||||
|
||||
function assertManagedGatewayStartupContinues(child: ChildProcess, startupFailure: Error | undefined): void {
|
||||
if (startupFailure || child.exitCode !== null || child.signalCode !== null || child.killed) {
|
||||
throw startupFailure ?? new Error(coreGatewayExitMessage(child.exitCode, child.signalCode));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeManagedCoreGatewayMarkerBestEffort(): Promise<void> {
|
||||
await removeManagedCoreGatewayMarker().catch((error) => {
|
||||
console.warn(`[gateway] Failed to remove gateway runtime marker: ${formatError(error)}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const gatewayService = new GatewayService();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
|
||||
import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { loadPersistedApiKeys } from "@ccr/core/config/config-repository";
|
||||
import { formatError, readAuthToken, readRemoteControlQueryAuthToken, sendJson } from "@ccr/core/gateway/http/io";
|
||||
import { estimateLimitUsage, limitRules, readWindowCounter } from "@ccr/core/gateway/limits/window-limiter";
|
||||
import type { ApiKeyAuthorizationResult, ApiKeyLimitRule, ApiKeyLimitUsage } from "@ccr/core/gateway/internal/shared";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
|
||||
*/
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { isGatewayProviderEnabled } from "@ccr/core/contracts/app";
|
||||
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { codexDefaultBaseUrl, kimiAccessTokenExpired, kimiIdentityHeaders, readClaudeCodeOauth, readCodexAuth, readGrokAuth, readKimiAuth, resolveGrokAuth, resolveKimiAuth } from "@ccr/core/agents/local-providers/service";
|
||||
|
|
@ -14,7 +13,7 @@ import { mediaToolsMcpServer } from "@ccr/core/mcp/grok-media-config";
|
|||
import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery";
|
||||
import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCapabilityNameMatches, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology";
|
||||
import { buildRawTraceConfig } from "@ccr/core/observability/raw-trace-sync";
|
||||
import { endpoint, resolveUndiciProxyAgentModule, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { endpoint, resolveUndiciProxyAgentModule, resolveUpstreamHeaderSanitizerEntry, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { billingUsageSyncHeader, billingUsageSyncPath, claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta, coreGatewayAuthHeader, coreGatewayAuthTokenEnv } from "@ccr/core/gateway/internal/shared";
|
||||
import type { BrowserWebSearchMcpIntegration, CoreGatewayProvider } from "@ccr/core/gateway/internal/shared";
|
||||
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
|
||||
|
|
@ -54,7 +53,7 @@ export async function compileCoreGatewayConfig(
|
|||
const providerPluginsWithCapabilityAliases = withProviderCapabilityPluginAliases(providerPlugins, enabledProviders);
|
||||
const virtualModelProfiles = coreGatewayVirtualModelProfiles(config);
|
||||
const coreEndpoint = endpoint(config.gateway.coreHost, config.gateway.corePort);
|
||||
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile(config, upstreamProxyUrl) : undefined;
|
||||
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile() : undefined;
|
||||
const proxyEnv = upstreamProxyUrl
|
||||
? { CCR_UPSTREAM_PROXY_URL: upstreamProxyUrl, CCR_UNDICI_MODULE: resolveUndiciProxyAgentModule() }
|
||||
: undefined;
|
||||
|
|
@ -136,7 +135,7 @@ export async function compileCoreGatewayConfig(
|
|||
{
|
||||
enabled: true,
|
||||
key: upstreamHeaderSanitizerPluginKey,
|
||||
modulePath: pathJoin(__dirname, "upstream-header-sanitizer.js")
|
||||
modulePath: resolveUpstreamHeaderSanitizerEntry()
|
||||
}
|
||||
],
|
||||
upstreamTimeoutMs: Number(config.API_TIMEOUT_MS) || 0,
|
||||
|
|
@ -151,7 +150,6 @@ export async function compileCoreGatewayConfig(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
function withProviderCapabilityPluginAliases(
|
||||
providerPlugins: unknown[],
|
||||
providers: GatewayProviderConfig[]
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import type { AppConfig } from "@ccr/core/contracts/app";
|
||||
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler";
|
||||
import { assertLoopbackCoreHost } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import {
|
||||
privateDirMode,
|
||||
privateFileMode,
|
||||
type BrowserWebSearchMcpIntegration
|
||||
} from "@ccr/core/gateway/internal/shared";
|
||||
|
||||
export async function writeCoreGatewayConfig(
|
||||
config: AppConfig,
|
||||
rawTraceSyncToken: string,
|
||||
billingUsageSyncToken: string,
|
||||
coreAuthToken: string,
|
||||
browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration,
|
||||
upstreamProxyUrl?: string
|
||||
): Promise<void> {
|
||||
assertLoopbackCoreHost(config.gateway.coreHost);
|
||||
mkdirSync(dirname(config.gateway.generatedConfigFile), {
|
||||
mode: privateDirMode,
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const payload = await compileCoreGatewayConfig(
|
||||
config,
|
||||
rawTraceSyncToken,
|
||||
billingUsageSyncToken,
|
||||
coreAuthToken,
|
||||
browserWebSearchMcpIntegration,
|
||||
upstreamProxyUrl
|
||||
);
|
||||
writePrivateTextFile(
|
||||
config.gateway.generatedConfigFile,
|
||||
`${JSON.stringify(payload, null, 2)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function writePrivateTextFile(file: string, content: string): void {
|
||||
writeFileSync(file, content, { encoding: "utf8", mode: privateFileMode });
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
chmodSync(file, privateFileMode);
|
||||
} catch {
|
||||
// Best effort for filesystems that do not support chmod.
|
||||
}
|
||||
}
|
||||
}
|
||||
115
packages/core/src/gateway/core-runtime/gateway-bootstrap.ts
Normal file
115
packages/core/src/gateway/core-runtime/gateway-bootstrap.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
type GatewayStartMessage = {
|
||||
config: Record<string, unknown>;
|
||||
gatewayEntry: string;
|
||||
protocolVersion: 1;
|
||||
type: "gateway:start";
|
||||
};
|
||||
|
||||
type MutableFs = {
|
||||
existsSync: (file: unknown) => boolean;
|
||||
readFileSync: (file: unknown, options?: unknown) => unknown;
|
||||
renameSync: (oldPath: unknown, newPath: unknown) => void;
|
||||
writeFileSync: (file: unknown, data: unknown, options?: unknown) => void;
|
||||
};
|
||||
|
||||
const requireFromHere = createRequire(__filename);
|
||||
const virtualConfigPath = path.resolve(process.cwd(), `.ccr-gateway-config-${process.pid}`);
|
||||
let started = false;
|
||||
|
||||
process.once("disconnect", () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("message", (message: unknown) => {
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const start = parseStartMessage(message);
|
||||
started = true;
|
||||
installVirtualConfigFile(start.config);
|
||||
process.env.GATEWAY_CONFIG_PATH = virtualConfigPath;
|
||||
requireFromHere(start.gatewayEntry);
|
||||
process.send?.({ protocolVersion: 1, type: "gateway:config-accepted" });
|
||||
} catch (error) {
|
||||
console.error(`[gateway-bootstrap] ${formatError(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
function installVirtualConfigFile(config: Record<string, unknown>): void {
|
||||
const fs = requireFromHere("node:fs") as MutableFs;
|
||||
const content = `${JSON.stringify(config)}\n`;
|
||||
const originalExistsSync = fs.existsSync.bind(fs);
|
||||
const originalReadFileSync = fs.readFileSync.bind(fs);
|
||||
const originalRenameSync = fs.renameSync.bind(fs);
|
||||
const originalWriteFileSync = fs.writeFileSync.bind(fs);
|
||||
|
||||
fs.existsSync = (file) => isVirtualConfigPath(file) || originalExistsSync(file);
|
||||
fs.readFileSync = (file, options) => {
|
||||
if (!isVirtualConfigPath(file)) {
|
||||
return originalReadFileSync(file, options);
|
||||
}
|
||||
return readsText(options) ? content : Buffer.from(content, "utf8");
|
||||
};
|
||||
fs.writeFileSync = (file, data, options) => {
|
||||
if (isManagedConfigPath(file)) {
|
||||
throw new Error("Gateway configuration is managed by Claude Code Router.");
|
||||
}
|
||||
originalWriteFileSync(file, data, options);
|
||||
};
|
||||
fs.renameSync = (oldPath, newPath) => {
|
||||
if (isManagedConfigPath(oldPath) || isManagedConfigPath(newPath)) {
|
||||
throw new Error("Gateway configuration is managed by Claude Code Router.");
|
||||
}
|
||||
originalRenameSync(oldPath, newPath);
|
||||
};
|
||||
}
|
||||
|
||||
function parseStartMessage(value: unknown): GatewayStartMessage {
|
||||
if (!isObject(value) || value.type !== "gateway:start" || value.protocolVersion !== 1) {
|
||||
throw new Error("Invalid gateway start message.");
|
||||
}
|
||||
if (!isObject(value.config)) {
|
||||
throw new Error("Gateway start message does not include a configuration object.");
|
||||
}
|
||||
if (typeof value.gatewayEntry !== "string" || !value.gatewayEntry.trim()) {
|
||||
throw new Error("Gateway start message does not include a runtime entry.");
|
||||
}
|
||||
return {
|
||||
config: value.config,
|
||||
gatewayEntry: path.resolve(value.gatewayEntry),
|
||||
protocolVersion: 1,
|
||||
type: "gateway:start"
|
||||
};
|
||||
}
|
||||
|
||||
function readsText(options: unknown): boolean {
|
||||
if (typeof options === "string") {
|
||||
return Boolean(options);
|
||||
}
|
||||
return isObject(options) && typeof options.encoding === "string" && Boolean(options.encoding);
|
||||
}
|
||||
|
||||
function isVirtualConfigPath(value: unknown): boolean {
|
||||
return typeof value === "string" && path.resolve(value) === virtualConfigPath;
|
||||
}
|
||||
|
||||
function isManagedConfigPath(value: unknown): boolean {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const resolved = path.resolve(value);
|
||||
return resolved === virtualConfigPath || resolved.startsWith(`${virtualConfigPath}.`);
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.stack || error.message : String(error);
|
||||
}
|
||||
|
|
@ -6,7 +6,13 @@ import { randomBytes } from "node:crypto";
|
|||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { networkInterfaces } from "node:os";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join as pathJoin, resolve as pathResolve } from "node:path";
|
||||
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import {
|
||||
deletePersistedRuntimeState,
|
||||
loadPersistedRuntimeState,
|
||||
replacePersistedRuntimeState
|
||||
} from "@ccr/core/config/config-repository";
|
||||
import type { AppConfig, GatewayNetworkEndpoint } from "@ccr/core/contracts/app";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value";
|
||||
|
|
@ -15,19 +21,144 @@ import { coreGatewayAuthHeader, coreGatewayAuthTokenEnv, gatewayEntryOverrideEnv
|
|||
import type { CoreGatewayHealth, ManagedGatewayRuntimeMarker } from "@ccr/core/gateway/internal/shared";
|
||||
import { delay } from "@ccr/core/gateway/internal/clock";
|
||||
|
||||
const gatewayRuntimeStateKey = "gateway";
|
||||
const gatewayConfigAcceptanceTimeoutMs = 5_000;
|
||||
const gatewayStartupTimeoutMs = 15_000;
|
||||
|
||||
export function spawnGatewayProcess(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): ChildProcess {
|
||||
export type SpawnedGatewayProcess = {
|
||||
child: ChildProcess;
|
||||
configAccepted: Promise<void>;
|
||||
};
|
||||
|
||||
export function spawnGatewayProcess(
|
||||
config: AppConfig,
|
||||
gatewayConfig: Record<string, unknown>,
|
||||
upstreamProxyUrl: string | undefined,
|
||||
runtimeId: string,
|
||||
coreAuthToken: string
|
||||
): SpawnedGatewayProcess {
|
||||
const gatewayEntry = resolveGatewayEntry();
|
||||
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile(config, upstreamProxyUrl) : undefined;
|
||||
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile() : undefined;
|
||||
const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId, coreAuthToken);
|
||||
const args = proxyPreloadFile ? ["--require", proxyPreloadFile, gatewayEntry] : [gatewayEntry];
|
||||
return spawn(process.execPath, args, {
|
||||
cwd: dirname(config.gateway.generatedConfigFile),
|
||||
const gatewayBootstrapEntry = resolveGatewayBootstrapEntry();
|
||||
const args = proxyPreloadFile ? ["--require", proxyPreloadFile, gatewayBootstrapEntry] : [gatewayBootstrapEntry];
|
||||
const child = spawn(process.execPath, args, {
|
||||
cwd: CONFIGDIR,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
serialization: "advanced",
|
||||
stdio: ["ignore", "pipe", "pipe", "ipc"]
|
||||
});
|
||||
if (!child.send) {
|
||||
child.kill();
|
||||
throw new Error("Gateway runtime did not create an IPC channel.");
|
||||
}
|
||||
const acceptance = monitorGatewayConfigAcceptance(child);
|
||||
void acceptance.promise.catch(() => undefined);
|
||||
try {
|
||||
child.send({
|
||||
config: gatewayConfig,
|
||||
gatewayEntry,
|
||||
protocolVersion: 1,
|
||||
type: "gateway:start"
|
||||
}, (error) => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
acceptance.reject(new Error(`Failed to send runtime config over IPC: ${formatError(error)}`));
|
||||
if (!child.killed) {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
acceptance.reject(new Error(`Failed to send runtime config over IPC: ${formatError(error)}`));
|
||||
if (!child.killed) {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
return {
|
||||
child,
|
||||
configAccepted: acceptance.promise
|
||||
};
|
||||
}
|
||||
|
||||
function monitorGatewayConfigAcceptance(child: ChildProcess): {
|
||||
promise: Promise<void>;
|
||||
reject: (error: Error) => void;
|
||||
} {
|
||||
let rejectAcceptance = (_error: Error): void => undefined;
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const finish = (error?: Error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
child.off("message", onMessage);
|
||||
child.off("error", onError);
|
||||
child.off("exit", onExit);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
const onMessage = (message: unknown) => {
|
||||
if (isRecord(message) && message.type === "gateway:config-accepted" && message.protocolVersion === 1) {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
const onError = (error: Error) => {
|
||||
finish(new Error(`Core gateway failed before accepting runtime config: ${formatError(error)}`));
|
||||
};
|
||||
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
finish(new Error(`Core gateway exited with ${signal ?? code ?? "unknown status"} before accepting runtime config.`));
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
finish(new Error(`Core gateway did not accept runtime config within ${gatewayConfigAcceptanceTimeoutMs}ms.`));
|
||||
}, gatewayConfigAcceptanceTimeoutMs);
|
||||
rejectAcceptance = (error) => finish(error);
|
||||
child.on("message", onMessage);
|
||||
child.once("error", onError);
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
return {
|
||||
promise,
|
||||
reject: (error) => rejectAcceptance(error)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGatewayBootstrapEntry(): string {
|
||||
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
|
||||
const entry = [
|
||||
pathJoin(__dirname, "gateway-bootstrap.js"),
|
||||
pathJoin(process.cwd(), ".test-dist", "core", "runtime", "gateway-bootstrap.js"),
|
||||
...(resourcesPath
|
||||
? [
|
||||
pathJoin(resourcesPath, "app.asar", "dist", "main", "gateway-bootstrap.js"),
|
||||
pathJoin(resourcesPath, "app", "dist", "main", "gateway-bootstrap.js")
|
||||
]
|
||||
: [])
|
||||
].find((candidate) => existsSync(candidate));
|
||||
if (!entry) {
|
||||
throw new Error("CCR gateway IPC bootstrap was not found. Run npm run build:assets.");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function resolveUpstreamHeaderSanitizerEntry(): string {
|
||||
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
|
||||
return [
|
||||
pathJoin(__dirname, "upstream-header-sanitizer.js"),
|
||||
pathJoin(process.cwd(), ".test-dist", "core", "runtime", "upstream-header-sanitizer.js"),
|
||||
...(resourcesPath
|
||||
? [
|
||||
pathJoin(resourcesPath, "app.asar", "dist", "main", "upstream-header-sanitizer.js"),
|
||||
pathJoin(resourcesPath, "app", "dist", "main", "upstream-header-sanitizer.js")
|
||||
]
|
||||
: [])
|
||||
].find((candidate) => existsSync(candidate)) ?? pathJoin(__dirname, "upstream-header-sanitizer.js");
|
||||
}
|
||||
|
||||
function resolveGatewayEntry(): string {
|
||||
const override = process.env[gatewayEntryOverrideEnv]?.trim();
|
||||
|
|
@ -54,7 +185,6 @@ function resolveGatewayEntry(): string {
|
|||
return requireFromHere.resolve(gatewayPackageCandidates[0]);
|
||||
}
|
||||
|
||||
|
||||
function resolveBundledGatewayEntry(): string | undefined {
|
||||
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
|
||||
return [
|
||||
|
|
@ -68,7 +198,6 @@ function resolveBundledGatewayEntry(): string | undefined {
|
|||
].find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
|
||||
export function resolveUndiciProxyAgentModule(): string {
|
||||
const bundled = resolveBundledUndiciProxyAgentModule();
|
||||
if (bundled) {
|
||||
|
|
@ -82,7 +211,6 @@ export function resolveUndiciProxyAgentModule(): string {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function resolveBundledUndiciProxyAgentModule(): string | undefined {
|
||||
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
|
||||
return [
|
||||
|
|
@ -96,7 +224,6 @@ function resolveBundledUndiciProxyAgentModule(): string | undefined {
|
|||
].find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
|
||||
function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
|
|
@ -109,7 +236,6 @@ function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | u
|
|||
CCR_GATEWAY_RUNTIME_ID: runtimeId,
|
||||
[coreGatewayAuthTokenEnv]: coreAuthToken,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
GATEWAY_CONFIG_PATH: config.gateway.generatedConfigFile,
|
||||
HOST: config.gateway.coreHost,
|
||||
PORT: String(config.gateway.corePort)
|
||||
};
|
||||
|
|
@ -146,9 +272,8 @@ function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | u
|
|||
return env;
|
||||
}
|
||||
|
||||
|
||||
export function writeGatewayProxyPreloadFile(config: AppConfig, upstreamProxyUrl: string): string {
|
||||
const file = pathJoin(dirname(config.gateway.generatedConfigFile), "gateway-proxy-preload.cjs");
|
||||
export function writeGatewayProxyPreloadFile(): string {
|
||||
const file = pathJoin(CONFIGDIR, "gateway-proxy-preload.cjs");
|
||||
writeFileSync(
|
||||
file,
|
||||
[
|
||||
|
|
@ -199,7 +324,6 @@ export function writeGatewayProxyPreloadFile(config: AppConfig, upstreamProxyUrl
|
|||
return file;
|
||||
}
|
||||
|
||||
|
||||
function mergeNoProxy(current: string | undefined, values: string[]): string {
|
||||
const merged = new Set<string>();
|
||||
for (const value of [...(current || "").split(","), ...values]) {
|
||||
|
|
@ -211,13 +335,11 @@ function mergeNoProxy(current: string | undefined, values: string[]): string {
|
|||
return [...merged].join(",");
|
||||
}
|
||||
|
||||
|
||||
export function endpoint(host: string, port: number): string {
|
||||
const endpointHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
||||
return `http://${endpointHost}:${port}`;
|
||||
}
|
||||
|
||||
|
||||
export function gatewayNetworkEndpoints(host: string, port: number): GatewayNetworkEndpoint[] {
|
||||
const normalizedHost = normalizeBindHost(host);
|
||||
const lanAddresses = physicalLanAddresses();
|
||||
|
|
@ -232,7 +354,6 @@ export function gatewayNetworkEndpoints(host: string, port: number): GatewayNetw
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
function physicalLanAddresses(): Array<{ address: string; interfaceName: string }> {
|
||||
const seen = new Set<string>();
|
||||
const result: Array<{ address: string; interfaceName: string }> = [];
|
||||
|
|
@ -263,27 +384,14 @@ function physicalLanAddresses(): Array<{ address: string; interfaceName: string
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function normalizeBindHost(host: string): string {
|
||||
return host.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
function isLoopbackBindHost(host: string): boolean {
|
||||
const normalized = normalizeBindHost(host).replace(/\.$/, "");
|
||||
return normalized === "localhost" ||
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "0:0:0:0:0:0:0:1" ||
|
||||
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
|
||||
}
|
||||
|
||||
|
||||
function isWildcardBindHost(host: string): boolean {
|
||||
return host === "" || host === "0.0.0.0" || host === "::" || host === "::0";
|
||||
}
|
||||
|
||||
|
||||
function isPrivateIpv4(address: string): boolean {
|
||||
const parts = address.split(".").map((part) => Number(part));
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
||||
|
|
@ -295,7 +403,6 @@ function isPrivateIpv4(address: string): boolean {
|
|||
(parts[0] === 192 && parts[1] === 168);
|
||||
}
|
||||
|
||||
|
||||
function isVirtualNetworkInterface(interfaceName: string): boolean {
|
||||
const normalized = interfaceName.toLowerCase();
|
||||
return [
|
||||
|
|
@ -331,34 +438,35 @@ function isVirtualNetworkInterface(interfaceName: string): boolean {
|
|||
].some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
|
||||
export async function stopPreviousManagedCoreGateway(config: AppConfig, coreEndpoint: string): Promise<void> {
|
||||
const marker = readManagedCoreGatewayMarker(config);
|
||||
export async function stopPreviousManagedCoreGateway(coreEndpoint: string): Promise<void> {
|
||||
const marker = await readManagedCoreGatewayMarker();
|
||||
const markerRuntimeId = stringValue(marker?.runtimeId);
|
||||
const pid = numberValue(marker?.pid);
|
||||
if (!markerRuntimeId || !pid) {
|
||||
await removeManagedCoreGatewayMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isProcessAlive(pid)) {
|
||||
await removeManagedCoreGatewayMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
const health = await readCoreGatewayHealth(coreEndpoint);
|
||||
if (health?.runtimeId !== markerRuntimeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isProcessAlive(pid)) {
|
||||
removeManagedCoreGatewayMarker(config);
|
||||
await removeManagedCoreGatewayMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(pid, "SIGTERM");
|
||||
} catch {
|
||||
removeManagedCoreGatewayMarker(config);
|
||||
await removeManagedCoreGatewayMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
if (await waitForCoreGatewayStop(coreEndpoint)) {
|
||||
removeManagedCoreGatewayMarker(config);
|
||||
await removeManagedCoreGatewayMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -368,12 +476,16 @@ export async function stopPreviousManagedCoreGateway(config: AppConfig, coreEndp
|
|||
// Process may have exited between the health check and SIGKILL.
|
||||
}
|
||||
await waitForCoreGatewayStop(coreEndpoint);
|
||||
removeManagedCoreGatewayMarker(config);
|
||||
await removeManagedCoreGatewayMarker();
|
||||
}
|
||||
|
||||
async function readManagedCoreGatewayMarker(): Promise<ManagedGatewayRuntimeMarker | undefined> {
|
||||
const persisted = await loadPersistedRuntimeState(gatewayRuntimeStateKey);
|
||||
if (isRecord(persisted)) {
|
||||
return persisted;
|
||||
}
|
||||
|
||||
function readManagedCoreGatewayMarker(config: AppConfig): ManagedGatewayRuntimeMarker | undefined {
|
||||
const file = managedCoreGatewayMarkerPath(config);
|
||||
const file = managedCoreGatewayMarkerPath();
|
||||
if (!existsSync(file)) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -385,50 +497,31 @@ function readManagedCoreGatewayMarker(config: AppConfig): ManagedGatewayRuntimeM
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function writeManagedCoreGatewayMarker(config: AppConfig, child: ChildProcess, runtimeId: string): void {
|
||||
export async function writeManagedCoreGatewayMarker(child: ChildProcess, runtimeId: string): Promise<void> {
|
||||
if (!child.pid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
writeFileSync(
|
||||
managedCoreGatewayMarkerPath(config),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
generatedConfigFile: config.gateway.generatedConfigFile,
|
||||
gatewayEntry: resolveGatewayEntry(),
|
||||
pid: child.pid,
|
||||
runtimeId,
|
||||
startedAt: new Date().toISOString()
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`[gateway] Failed to write gateway runtime marker: ${formatError(error)}`);
|
||||
throw new Error("Managed gateway process did not expose a PID.");
|
||||
}
|
||||
await replacePersistedRuntimeState(gatewayRuntimeStateKey, {
|
||||
gatewayEntry: resolveGatewayEntry(),
|
||||
pid: child.pid,
|
||||
runtimeId,
|
||||
startedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function removeManagedCoreGatewayMarker(config: AppConfig | undefined): void {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
export async function removeManagedCoreGatewayMarker(): Promise<void> {
|
||||
await deletePersistedRuntimeState(gatewayRuntimeStateKey);
|
||||
try {
|
||||
rmSync(managedCoreGatewayMarkerPath(config), { force: true });
|
||||
rmSync(managedCoreGatewayMarkerPath(), { force: true });
|
||||
} catch (error) {
|
||||
console.warn(`[gateway] Failed to remove gateway runtime marker: ${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function managedCoreGatewayMarkerPath(config: AppConfig): string {
|
||||
return pathJoin(dirname(config.gateway.generatedConfigFile), gatewayRuntimeMarkerFile);
|
||||
function managedCoreGatewayMarkerPath(): string {
|
||||
return pathJoin(CONFIGDIR, gatewayRuntimeMarkerFile);
|
||||
}
|
||||
|
||||
|
||||
async function waitForCoreGatewayStop(coreEndpoint: string): Promise<boolean> {
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
if (!(await isCoreGatewayHealthy(coreEndpoint))) {
|
||||
|
|
@ -439,7 +532,6 @@ async function waitForCoreGatewayStop(coreEndpoint: string): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
|
|
@ -449,7 +541,6 @@ function isProcessAlive(pid: number): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function assertLoopbackCoreHost(host: string): void {
|
||||
const error = loopbackCoreHostError(host);
|
||||
if (error) {
|
||||
|
|
@ -457,7 +548,6 @@ export function assertLoopbackCoreHost(host: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function loopbackCoreHostError(host: string): string | undefined {
|
||||
const normalized = host.trim().toLowerCase();
|
||||
return normalized === "127.0.0.1" || normalized === "::1"
|
||||
|
|
@ -465,17 +555,51 @@ export function loopbackCoreHostError(host: string): string | undefined {
|
|||
: "Core gateway host must be 127.0.0.1 or ::1.";
|
||||
}
|
||||
|
||||
|
||||
export function generateCoreGatewayAuthToken(): string {
|
||||
return randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
|
||||
export async function isCoreGatewayHealthy(coreEndpoint: string): Promise<boolean> {
|
||||
const health = await readCoreGatewayHealth(coreEndpoint);
|
||||
return health?.status === "ok";
|
||||
}
|
||||
|
||||
export async function waitForManagedCoreGatewayReady(
|
||||
coreEndpoint: string,
|
||||
runtimeId: string,
|
||||
child: ChildProcess
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + gatewayStartupTimeoutMs;
|
||||
let lastHealth: CoreGatewayHealth | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
assertGatewayChildRunning(child);
|
||||
lastHealth = await readCoreGatewayHealth(coreEndpoint);
|
||||
assertGatewayChildRunning(child);
|
||||
if (lastHealth?.status === "ok") {
|
||||
if (lastHealth.runtimeId === runtimeId) {
|
||||
return;
|
||||
}
|
||||
if (lastHealth.runtimeId) {
|
||||
throw new Error(
|
||||
`Core gateway endpoint ${coreEndpoint} is owned by a different runtime (${lastHealth.runtimeId}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
const healthDetail = lastHealth
|
||||
? ` Last health response: ${JSON.stringify(lastHealth)}.`
|
||||
: "";
|
||||
throw new Error(
|
||||
`Core gateway runtime ${runtimeId} did not become healthy within ${gatewayStartupTimeoutMs}ms.${healthDetail}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertGatewayChildRunning(child: ChildProcess): void {
|
||||
if (child.exitCode !== null || child.signalCode !== null || child.killed) {
|
||||
throw new Error(`Core gateway exited during startup with ${child.signalCode ?? child.exitCode ?? "unknown status"}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readCoreGatewayHealth(coreEndpoint: string): Promise<CoreGatewayHealth | undefined> {
|
||||
if (!coreEndpoint) {
|
||||
|
|
@ -504,19 +628,16 @@ async function readCoreGatewayHealth(coreEndpoint: string): Promise<CoreGatewayH
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function shouldRunUnifiedServer(config: AppConfig): boolean {
|
||||
return config.gateway.enabled || config.proxy.enabled;
|
||||
}
|
||||
|
||||
|
||||
export function shouldRunGatewayRuntime(config: AppConfig): boolean {
|
||||
return config.gateway.enabled ||
|
||||
config.mediaTools.enabled ||
|
||||
(config.proxy.enabled && config.proxy.mode === "gateway");
|
||||
}
|
||||
|
||||
|
||||
export function shouldServeGatewayRequest(config: AppConfig, request: IncomingMessage): boolean {
|
||||
if (config.gateway.enabled) {
|
||||
return true;
|
||||
|
|
@ -524,7 +645,6 @@ export function shouldServeGatewayRequest(config: AppConfig, request: IncomingMe
|
|||
return config.proxy.enabled && config.proxy.mode === "gateway" && readHeader(request.headers["x-ccr-proxy-mode"]) === "gateway";
|
||||
}
|
||||
|
||||
|
||||
export function applyCors(response: ServerResponse, config?: AppConfig): void {
|
||||
const origin = config ? endpoint(config.gateway.host, config.gateway.port) : "*";
|
||||
response.setHeader("Access-Control-Allow-Origin", origin);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { AppConfig, GatewayProviderProtocol, VirtualModelFusionWebSearchPro
|
|||
import { isRecord, numberValue, stringListValue, stringValue } from "@ccr/core/gateway/internal/value";
|
||||
import { normalizeCoreGatewayVirtualModelProfiles } from "@ccr/core/gateway/core-runtime/config-compiler";
|
||||
import { browserWebSearchUnavailableMessage, fusionModelNameFromSelector, readFusionWebSearchConfig, withCodexCompatibleVirtualModelProfiles, withFusionVirtualModelAliases } from "@ccr/core/mcp/fusion-config";
|
||||
import type { AnthropicWebSearchProtocolContext, BrowserWebSearchMcpIntegration, BrowserWebSearchProtocolRecord, ClaudeCodeWebSearchContinuationContext, HostedWebSearchProtocolContext } from "@ccr/core/gateway/internal/shared";
|
||||
import type { BrowserWebSearchMcpIntegration, BrowserWebSearchProtocolRecord, ClaudeCodeWebSearchContinuationContext, HostedWebSearchProtocolContext } from "@ccr/core/gateway/internal/shared";
|
||||
import { clampNumber, uniqueStrings } from "@ccr/core/gateway/internal/collections";
|
||||
import { defaultFusionWebSearchProvider } from "@ccr/core/gateway/internal/shared";
|
||||
import { queryMatchScore } from "@ccr/core/gateway/features/hosted-web-search/evidence";
|
||||
|
|
@ -29,8 +29,6 @@ type WebSearchProviderInput = {
|
|||
|
||||
type WebSearchProviderResult = BrowserWebSearchProtocolRecord["results"][number];
|
||||
|
||||
|
||||
|
||||
function hasAnthropicHostedWebSearchTool(tools: unknown): boolean {
|
||||
if (!Array.isArray(tools)) {
|
||||
return false;
|
||||
|
|
@ -38,8 +36,6 @@ function hasAnthropicHostedWebSearchTool(tools: unknown): boolean {
|
|||
return tools.some(isAnthropicHostedWebSearchTool);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function hasHostedWebSearchDeclaration(body: Record<string, unknown>, protocol: GatewayProviderProtocol): boolean {
|
||||
if (protocol === "anthropic_messages") {
|
||||
return hasAnthropicHostedWebSearchTool(body.tools);
|
||||
|
|
@ -53,8 +49,6 @@ export function hasHostedWebSearchDeclaration(body: Record<string, unknown>, pro
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hasOpenAiHostedWebSearchDeclaration(body: Record<string, unknown>): boolean {
|
||||
if (body.web_search_options !== undefined || body.webSearchOptions !== undefined) {
|
||||
return true;
|
||||
|
|
@ -62,8 +56,6 @@ function hasOpenAiHostedWebSearchDeclaration(body: Record<string, unknown>): boo
|
|||
return Array.isArray(body.tools) && body.tools.some(isOpenAiHostedWebSearchTool);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hasGeminiHostedWebSearchTool(tools: unknown): boolean {
|
||||
if (!Array.isArray(tools)) {
|
||||
return false;
|
||||
|
|
@ -79,8 +71,6 @@ function hasGeminiHostedWebSearchTool(tools: unknown): boolean {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function isAnthropicHostedWebSearchTool(tool: unknown): boolean {
|
||||
if (!isRecord(tool)) {
|
||||
return false;
|
||||
|
|
@ -88,8 +78,6 @@ export function isAnthropicHostedWebSearchTool(tool: unknown): boolean {
|
|||
return anthropicHostedWebSearchType(stringValue(tool.type));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function isOpenAiHostedWebSearchTool(tool: unknown): boolean {
|
||||
if (!isRecord(tool)) {
|
||||
return false;
|
||||
|
|
@ -97,8 +85,6 @@ export function isOpenAiHostedWebSearchTool(tool: unknown): boolean {
|
|||
return openAiHostedWebSearchType(stringValue(tool.type));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function openAiToolChoiceNamesWebSearch(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
return openAiHostedWebSearchType(value);
|
||||
|
|
@ -109,15 +95,11 @@ export function openAiToolChoiceNamesWebSearch(value: unknown): boolean {
|
|||
return openAiHostedWebSearchType(stringValue(value.type));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function anthropicHostedWebSearchType(value: string | undefined): boolean {
|
||||
const normalized = normalizedToolProtocolName(value);
|
||||
return normalized === "web_search" || normalized === "web_search_20250305";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiHostedWebSearchType(value: string | undefined): boolean {
|
||||
const normalized = normalizedToolProtocolName(value);
|
||||
return normalized === "web_search" ||
|
||||
|
|
@ -125,14 +107,10 @@ function openAiHostedWebSearchType(value: string | undefined): boolean {
|
|||
normalized.startsWith("web_search_preview_");
|
||||
}
|
||||
|
||||
|
||||
|
||||
function normalizedToolProtocolName(value: string | undefined): string {
|
||||
return value?.trim().toLowerCase().replace(/[-.]/g, "_") ?? "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function readAnthropicWebSearchMaxUses(tools: unknown): number | undefined {
|
||||
if (!Array.isArray(tools)) {
|
||||
return undefined;
|
||||
|
|
@ -141,8 +119,6 @@ function readAnthropicWebSearchMaxUses(tools: unknown): number | undefined {
|
|||
return isRecord(tool) ? numberValue(tool.max_uses ?? tool.maxUses) : undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function readHostedWebSearchMaxUses(body: Record<string, unknown>, protocol: GatewayProviderProtocol): number | undefined {
|
||||
if (protocol === "anthropic_messages") {
|
||||
return readAnthropicWebSearchMaxUses(body.tools);
|
||||
|
|
@ -154,8 +130,6 @@ export function readHostedWebSearchMaxUses(body: Record<string, unknown>, protoc
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function extractHostedWebSearchQueryHint(body: Record<string, unknown>, protocol: GatewayProviderProtocol): string | undefined {
|
||||
if (protocol === "anthropic_messages") {
|
||||
return extractAnthropicWebSearchQueryHint(body);
|
||||
|
|
@ -172,8 +146,6 @@ export function extractHostedWebSearchQueryHint(body: Record<string, unknown>, p
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function extractAnthropicWebSearchQueryHint(body: Record<string, unknown>): string | undefined {
|
||||
const userTexts = Array.isArray(body.messages)
|
||||
? body.messages.flatMap((message) => {
|
||||
|
|
@ -186,8 +158,6 @@ export function extractAnthropicWebSearchQueryHint(body: Record<string, unknown>
|
|||
return normalizedWebSearchQueryHintFromParts(userTexts);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function extractClaudeCodeWebSearchToolResultQuery(body: Record<string, unknown>): string | undefined {
|
||||
for (const text of claudeCodeWebSearchToolResultTexts(body)) {
|
||||
const quoted = /Web search results for query:\s*"([^"]+)"/i.exec(text);
|
||||
|
|
@ -202,8 +172,6 @@ export function extractClaudeCodeWebSearchToolResultQuery(body: Record<string, u
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function normalizedWebSearchQueryHintFromParts(parts: string[]): string | undefined {
|
||||
const candidates = parts
|
||||
.map((part) => part.trim())
|
||||
|
|
@ -223,8 +191,6 @@ function normalizedWebSearchQueryHintFromParts(parts: string[]): string | undefi
|
|||
return normalizedWebSearchQueryHint(candidates.join("\n"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function normalizedWebSearchQueryHint(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
|
|
@ -236,23 +202,17 @@ function normalizedWebSearchQueryHint(value: string | undefined): string | undef
|
|||
return joined.trim().slice(0, 500);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function extractExplicitWebSearchQuery(value: string): string | undefined {
|
||||
const explicit = /perform\s+a\s+web\s+search\s+for\s+the\s+query:\s*([\s\S]+)$/i.exec(value.trim());
|
||||
return normalizedWebSearchQueryHint(explicit?.[1]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripSearchIntentPrefix(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const match = /^(?:请)?(?:帮我)?(?:搜索|查询|查一下|帮我查一下|搜一下)\s*[::]?\s*([\s\S]+)$/i.exec(trimmed);
|
||||
return (match?.[1] || trimmed).trim();
|
||||
}
|
||||
|
||||
|
||||
|
||||
function isRuntimeContextText(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -270,8 +230,6 @@ function isRuntimeContextText(value: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textPartsFromAnthropicContent(content: unknown): string[] {
|
||||
if (typeof content === "string") {
|
||||
return [content];
|
||||
|
|
@ -282,8 +240,6 @@ function textPartsFromAnthropicContent(content: unknown): string[] {
|
|||
return content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function claudeCodeWebSearchToolResultTexts(body: Record<string, unknown>): string[] {
|
||||
if (!Array.isArray(body.messages)) {
|
||||
return [];
|
||||
|
|
@ -333,8 +289,6 @@ export function claudeCodeWebSearchToolResultTexts(body: Record<string, unknown>
|
|||
return texts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function anthropicToolResultContentText(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
|
|
@ -355,8 +309,6 @@ function anthropicToolResultContentText(content: unknown): string {
|
|||
}).join("\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textPartsFromOpenAiChatMessages(messages: unknown): string[] {
|
||||
if (!Array.isArray(messages)) {
|
||||
return [];
|
||||
|
|
@ -369,8 +321,6 @@ function textPartsFromOpenAiChatMessages(messages: unknown): string[] {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textPartsFromOpenAiContent(content: unknown): string[] {
|
||||
if (typeof content === "string") {
|
||||
return [content];
|
||||
|
|
@ -390,8 +340,6 @@ function textPartsFromOpenAiContent(content: unknown): string[] {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textPartsFromOpenAiResponsesInput(input: unknown): string[] {
|
||||
if (typeof input === "string") {
|
||||
return [input];
|
||||
|
|
@ -411,8 +359,6 @@ function textPartsFromOpenAiResponsesInput(input: unknown): string[] {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textPartsFromGeminiContents(contents: unknown): string[] {
|
||||
if (!Array.isArray(contents)) {
|
||||
return [];
|
||||
|
|
@ -430,8 +376,6 @@ function textPartsFromGeminiContents(contents: unknown): string[] {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function fusionWebSearchToolNameForRequest(config: AppConfig, model: string | undefined): string | undefined {
|
||||
const normalizedModel = model ? fusionModelNameFromSelector(model) : "";
|
||||
for (const candidate of fusionWebSearchToolCandidates(config)) {
|
||||
|
|
@ -442,14 +386,10 @@ export function fusionWebSearchToolNameForRequest(config: AppConfig, model: stri
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function fusionWebSearchProviderForToolName(config: AppConfig, toolName: string): VirtualModelFusionWebSearchProvider | undefined {
|
||||
return fusionWebSearchToolCandidateForToolName(config, toolName)?.provider;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function hostedWebSearchUnavailableMessage(config: AppConfig, toolName: string): string {
|
||||
const provider = fusionWebSearchProviderForToolName(config, toolName);
|
||||
if (provider && provider !== "browser") {
|
||||
|
|
@ -461,14 +401,10 @@ export function hostedWebSearchUnavailableMessage(config: AppConfig, toolName: s
|
|||
return browserWebSearchUnavailableMessage(toolName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function fusionWebSearchToolCandidateForToolName(config: AppConfig, toolName: string): FusionWebSearchToolCandidate | undefined {
|
||||
return fusionWebSearchToolCandidates(config).find((candidate) => candidate.toolName === toolName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function fusionWebSearchToolCandidates(config: AppConfig): FusionWebSearchToolCandidate[] {
|
||||
const rawProfiles = Array.isArray(config.virtualModelProfiles) ? config.virtualModelProfiles : [];
|
||||
const profiles = normalizeCoreGatewayVirtualModelProfiles(
|
||||
|
|
@ -506,8 +442,6 @@ function fusionWebSearchToolCandidates(config: AppConfig): FusionWebSearchToolCa
|
|||
return candidates;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function selectHostedWebSearchProtocolRecords(
|
||||
context: HostedWebSearchProtocolContext,
|
||||
integration?: BrowserWebSearchMcpIntegration,
|
||||
|
|
@ -543,8 +477,6 @@ export async function selectHostedWebSearchProtocolRecords(
|
|||
return config ? selectConfiguredWebSearchProtocolRecords(context, config) : [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function selectConfiguredWebSearchProtocolRecords(
|
||||
context: HostedWebSearchProtocolContext,
|
||||
config: AppConfig
|
||||
|
|
@ -584,8 +516,6 @@ async function selectConfiguredWebSearchProtocolRecords(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function runConfiguredWebSearch(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
switch (input.provider) {
|
||||
case "brave":
|
||||
|
|
@ -605,8 +535,6 @@ async function runConfiguredWebSearch(input: WebSearchProviderInput): Promise<We
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchBrave(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "BRAVE_SEARCH_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -624,8 +552,6 @@ async function searchBrave(input: WebSearchProviderInput): Promise<WebSearchProv
|
|||
return items.map((item) => webSearchResult(item, "title", "url", "description")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchBing(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "BING_SEARCH_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -644,8 +570,6 @@ async function searchBing(input: WebSearchProviderInput): Promise<WebSearchProvi
|
|||
return items.map((item) => webSearchResult(item, "name", "url", "snippet")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchGoogleCse(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "GOOGLE_SEARCH_API_KEY");
|
||||
const cx = searchEnv(input, "GOOGLE_SEARCH_CX");
|
||||
|
|
@ -663,8 +587,6 @@ async function searchGoogleCse(input: WebSearchProviderInput): Promise<WebSearch
|
|||
return items.map((item) => webSearchResult(item, "title", "link", "snippet")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchSerper(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "SERPER_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -684,8 +606,6 @@ async function searchSerper(input: WebSearchProviderInput): Promise<WebSearchPro
|
|||
return items.map((item) => webSearchResult(item, "title", "link", "snippet")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchSerpApi(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "SERPAPI_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -702,8 +622,6 @@ async function searchSerpApi(input: WebSearchProviderInput): Promise<WebSearchPr
|
|||
return items.map((item) => webSearchResult(item, "title", "link", "snippet")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchTavily(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "TAVILY_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -725,8 +643,6 @@ async function searchTavily(input: WebSearchProviderInput): Promise<WebSearchPro
|
|||
return items.map((item) => webSearchResult(item, "title", "url", "content")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function searchExa(input: WebSearchProviderInput): Promise<WebSearchProviderResult[]> {
|
||||
const apiKey = searchEnv(input, "EXA_API_KEY");
|
||||
if (!apiKey) {
|
||||
|
|
@ -749,8 +665,6 @@ async function searchExa(input: WebSearchProviderInput): Promise<WebSearchProvid
|
|||
return items.map((item) => webSearchResult(item, "title", "url", "text")).filter(isWebSearchProviderResult);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fetchJson(input: string, init: RequestInit): Promise<unknown> {
|
||||
const response = await fetchWithSystemProxy(input, init);
|
||||
if (!response.ok) {
|
||||
|
|
@ -759,14 +673,10 @@ async function fetchJson(input: string, init: RequestInit): Promise<unknown> {
|
|||
return response.json() as Promise<unknown>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function searchEnv(input: WebSearchProviderInput, key: string): string | undefined {
|
||||
return input.env?.[key]?.trim() || process.env[key]?.trim() || undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function webSearchResult(item: unknown, titleKey: string, urlKey: string, snippetKey: string): WebSearchProviderResult | undefined {
|
||||
if (!isRecord(item)) {
|
||||
return undefined;
|
||||
|
|
@ -784,14 +694,10 @@ function webSearchResult(item: unknown, titleKey: string, urlKey: string, snippe
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function isWebSearchProviderResult(value: WebSearchProviderResult | undefined): value is WebSearchProviderResult {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function searchProviderUrl(provider: VirtualModelFusionWebSearchProvider, query: string): string {
|
||||
const encoded = encodeURIComponent(query);
|
||||
switch (provider) {
|
||||
|
|
@ -814,8 +720,6 @@ function searchProviderUrl(provider: VirtualModelFusionWebSearchProvider, query:
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function selectClaudeCodeWebSearchContinuationRecords(
|
||||
context: ClaudeCodeWebSearchContinuationContext,
|
||||
integration: BrowserWebSearchMcpIntegration
|
||||
|
|
@ -834,17 +738,6 @@ export function selectClaudeCodeWebSearchContinuationRecords(
|
|||
.slice(0, 3);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function selectAnthropicWebSearchProtocolRecords(
|
||||
context: AnthropicWebSearchProtocolContext,
|
||||
integration: BrowserWebSearchMcpIntegration
|
||||
): Promise<BrowserWebSearchProtocolRecord[]> {
|
||||
return selectHostedWebSearchProtocolRecords(context, integration);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function uniqueSearchRecordFilter(): (record: BrowserWebSearchProtocolRecord) => boolean {
|
||||
const seen = new Set<string>();
|
||||
return (record) => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
|
|||
import { claudeCodeWebSearchToolResultTexts, extractAnthropicWebSearchQueryHint, extractClaudeCodeWebSearchToolResultQuery, extractHostedWebSearchQueryHint, fusionWebSearchToolNameForRequest, hasHostedWebSearchDeclaration, isAnthropicHostedWebSearchTool, isOpenAiHostedWebSearchTool, openAiToolChoiceNamesWebSearch, readHostedWebSearchMaxUses } from "@ccr/core/gateway/features/hosted-web-search/discovery";
|
||||
import { normalizeSearchComparisonText } from "@ccr/core/gateway/features/hosted-web-search/evidence";
|
||||
|
||||
|
||||
|
||||
export function createHostedWebSearchProtocolContext(input: {
|
||||
body: Buffer | undefined;
|
||||
config: AppConfig;
|
||||
|
|
@ -40,22 +38,6 @@ export function createHostedWebSearchProtocolContext(input: {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createAnthropicWebSearchProtocolContext(input: {
|
||||
body: Buffer | undefined;
|
||||
config: AppConfig;
|
||||
method: string;
|
||||
path: string;
|
||||
requestId: string;
|
||||
sinceMs: number;
|
||||
}): AnthropicWebSearchProtocolContext | undefined {
|
||||
const context = createHostedWebSearchProtocolContext(input);
|
||||
return context?.protocol === "anthropic_messages" ? context : undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function createClaudeCodeWebSearchContinuationContext(input: {
|
||||
body: Buffer | undefined;
|
||||
config: AppConfig;
|
||||
|
|
@ -82,8 +64,6 @@ export function createClaudeCodeWebSearchContinuationContext(input: {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function prepareHostedWebSearchProtocolRequestBody(
|
||||
body: Buffer | undefined,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -111,8 +91,6 @@ export function prepareHostedWebSearchProtocolRequestBody(
|
|||
return next ? serializeJsonBody(next) : undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function prepareAnthropicWebSearchProtocolRequestBody(
|
||||
body: Buffer | undefined,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -133,8 +111,6 @@ export function prepareAnthropicWebSearchProtocolRequestBody(
|
|||
return serializeJsonBody(next);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function prepareClaudeCodeWebSearchContinuationRequestBody(
|
||||
body: Buffer | undefined,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -160,8 +136,6 @@ export function prepareClaudeCodeWebSearchContinuationRequestBody(
|
|||
return serializeJsonBody(next);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function applyAnthropicWebSearchSynthesisControls(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const next = { ...body };
|
||||
const outputConfig = isRecord(next.output_config) ? { ...next.output_config } : {};
|
||||
|
|
@ -172,8 +146,6 @@ function applyAnthropicWebSearchSynthesisControls(body: Record<string, unknown>)
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function prepareOpenAiChatHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
|
||||
const next = stripOpenAiHostedWebSearchTools({
|
||||
...body,
|
||||
|
|
@ -182,8 +154,6 @@ function prepareOpenAiChatHostedWebSearchRequestBody(body: Record<string, unknow
|
|||
return applyOpenAiHostedWebSearchSynthesisControls(next);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function prepareOpenAiResponsesHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
|
||||
const next = stripOpenAiHostedWebSearchTools({
|
||||
...body,
|
||||
|
|
@ -192,8 +162,6 @@ function prepareOpenAiResponsesHostedWebSearchRequestBody(body: Record<string, u
|
|||
return applyOpenAiHostedWebSearchSynthesisControls(next);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function prepareGeminiHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
|
||||
return stripGeminiHostedWebSearchTools({
|
||||
...body,
|
||||
|
|
@ -201,8 +169,6 @@ function prepareGeminiHostedWebSearchRequestBody(body: Record<string, unknown>,
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function applyOpenAiHostedWebSearchSynthesisControls(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const next = { ...body };
|
||||
if (typeof next.reasoning_effort === "string") {
|
||||
|
|
@ -214,8 +180,6 @@ function applyOpenAiHostedWebSearchSynthesisControls(body: Record<string, unknow
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripAnthropicHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Array.isArray(body.tools)) {
|
||||
return body;
|
||||
|
|
@ -238,8 +202,6 @@ function stripAnthropicHostedWebSearchTools(body: Record<string, unknown>): Reco
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripClaudeCodeWebSearchContinuationTools(body: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Array.isArray(body.tools)) {
|
||||
return body;
|
||||
|
|
@ -250,8 +212,6 @@ function stripClaudeCodeWebSearchContinuationTools(body: Record<string, unknown>
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripOpenAiHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const next = { ...body };
|
||||
let removedTools = false;
|
||||
|
|
@ -276,8 +236,6 @@ function stripOpenAiHostedWebSearchTools(body: Record<string, unknown>): Record<
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripGeminiHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Array.isArray(body.tools)) {
|
||||
return body;
|
||||
|
|
@ -300,8 +258,6 @@ function stripGeminiHostedWebSearchTools(body: Record<string, unknown>): Record<
|
|||
return next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripGeminiHostedWebSearchTool(tool: unknown): { changed: boolean; value?: unknown } {
|
||||
if (!isRecord(tool)) {
|
||||
return { changed: false, value: tool };
|
||||
|
|
@ -317,8 +273,6 @@ function stripGeminiHostedWebSearchTool(tool: unknown): { changed: boolean; valu
|
|||
return Object.keys(next).length === 0 ? { changed, value: undefined } : { changed, value: next };
|
||||
}
|
||||
|
||||
|
||||
|
||||
function appendAnthropicSystemText(system: unknown, text: string): unknown {
|
||||
if (typeof system === "string") {
|
||||
return `${system.trimEnd()}\n\n${text}`;
|
||||
|
|
@ -330,22 +284,16 @@ function appendAnthropicSystemText(system: unknown, text: string): unknown {
|
|||
return [block];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function appendOpenAiChatSystemText(messages: unknown, text: string): unknown[] {
|
||||
const message = { content: text, role: "system" };
|
||||
return Array.isArray(messages) ? [message, ...messages] : [message];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function appendStringInstruction(value: unknown, text: string): string {
|
||||
const existing = rawStringValue(value);
|
||||
return existing ? `${existing.trimEnd()}\n\n${text}` : text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function appendGeminiSystemInstruction(value: unknown, text: string): Record<string, unknown> {
|
||||
const part = { text };
|
||||
if (typeof value === "string") {
|
||||
|
|
@ -361,8 +309,6 @@ function appendGeminiSystemInstruction(value: unknown, text: string): Record<str
|
|||
return { parts: [part] };
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hostedWebSearchEvidenceText(records: BrowserWebSearchProtocolRecord[], queryHint: string | undefined): string {
|
||||
const sections = records.flatMap((record, recordIndex) => {
|
||||
const resultLines = record.results.slice(0, 8).map((result, resultIndex) => {
|
||||
|
|
@ -403,8 +349,6 @@ function hostedWebSearchEvidenceText(records: BrowserWebSearchProtocolRecord[],
|
|||
].filter(Boolean).join("\n\n").slice(0, 10_000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function claudeCodeWebSearchContinuationEvidenceText(
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
queryHint: string | undefined,
|
||||
|
|
@ -428,8 +372,6 @@ function claudeCodeWebSearchContinuationEvidenceText(
|
|||
].filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
function focusedWebSearchContent(content: string | undefined, queryHint: string | undefined): string | undefined {
|
||||
const text = content?.replace(/\s+/g, " ").trim();
|
||||
if (!text) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import { parseSseEventBlock, parseSseEvents, serializeSseEvent, shiftSseContentB
|
|||
import type { ParsedSseEvent } from "@ccr/core/gateway/features/hosted-web-search/sse";
|
||||
import { anthropicSseTextBlockStartIndex, anthropicWebSearchProtocolBlocks, anthropicWebSearchSseEventsForBlock, mergeAnthropicWebSearchUsage, responseValueContainsAnthropicClientToolUse, responseValueContainsAnthropicWebSearchBlocks, responseValueContainsVisibleText, sanitizeAnthropicToolUseId, shouldEndAnthropicHostedWebSearchTurn, sseEventContainsAnthropicClientToolUse, sseEventContainsAnthropicWebSearchBlock, sseEventContainsVisibleText, sseEventIsAnthropicMessageEnd, sseEventsContainAnthropicClientToolUse, sseEventsContainAnthropicWebSearchBlocks, sseEventsContainVisibleText, synthesizeWebSearchAnswer, updateAnthropicWebSearchSseUsage, webSearchProtocolInsertIndex } from "@ccr/core/gateway/features/hosted-web-search/evidence";
|
||||
|
||||
|
||||
|
||||
export function hostedWebSearchProtocolResponseStream(
|
||||
input: Readable,
|
||||
headers: Headers,
|
||||
|
|
@ -59,8 +57,6 @@ export function hostedWebSearchProtocolResponseStream(
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hostedWebSearchProtocolSseStream(
|
||||
input: Readable,
|
||||
context: HostedWebSearchProtocolContext,
|
||||
|
|
@ -127,16 +123,12 @@ function hostedWebSearchProtocolSseStream(
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
type HostedWebSearchSseState = {
|
||||
done: boolean;
|
||||
maxOutputIndex: number;
|
||||
visibleText: boolean;
|
||||
};
|
||||
|
||||
|
||||
|
||||
function drainHostedWebSearchSseBlocks(
|
||||
stream: Transform,
|
||||
text: string,
|
||||
|
|
@ -168,8 +160,6 @@ function drainHostedWebSearchSseBlocks(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function writeHostedWebSearchSseEvent(
|
||||
stream: Transform,
|
||||
event: ParsedSseEvent,
|
||||
|
|
@ -194,8 +184,6 @@ function writeHostedWebSearchSseEvent(
|
|||
stream.push(`${serializeSseEvent(nextEvent)}${delimiter}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateHostedWebSearchSseState(
|
||||
event: ParsedSseEvent,
|
||||
state: HostedWebSearchSseState,
|
||||
|
|
@ -220,8 +208,6 @@ function updateHostedWebSearchSseState(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function writeHostedWebSearchSseFallback(
|
||||
stream: Transform,
|
||||
state: HostedWebSearchSseState,
|
||||
|
|
@ -243,8 +229,6 @@ function writeHostedWebSearchSseFallback(
|
|||
state.done = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hostedWebSearchSseFallbackEvents(
|
||||
answer: string,
|
||||
state: HostedWebSearchSseState,
|
||||
|
|
@ -271,8 +255,6 @@ function hostedWebSearchSseFallbackEvents(
|
|||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function anthropicHostedWebSearchProtocolSseStream(
|
||||
input: Readable,
|
||||
context: HostedWebSearchProtocolContext,
|
||||
|
|
@ -343,8 +325,6 @@ function anthropicHostedWebSearchProtocolSseStream(
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
type AnthropicHostedWebSearchSseState = {
|
||||
answerInjected: boolean;
|
||||
hasClientToolUse: boolean;
|
||||
|
|
@ -356,8 +336,6 @@ type AnthropicHostedWebSearchSseState = {
|
|||
visibleText: boolean;
|
||||
};
|
||||
|
||||
|
||||
|
||||
function drainAnthropicHostedWebSearchSseBlocks(
|
||||
stream: Transform,
|
||||
text: string,
|
||||
|
|
@ -402,8 +380,6 @@ function drainAnthropicHostedWebSearchSseBlocks(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function sseTrailingPartialBlock(text: string): string {
|
||||
let cursor = 0;
|
||||
for (const match of text.matchAll(/\r?\n\r?\n/g)) {
|
||||
|
|
@ -412,8 +388,6 @@ function sseTrailingPartialBlock(text: string): string {
|
|||
return text.slice(cursor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function writeAnthropicHostedWebSearchSseEvent(
|
||||
stream: Transform,
|
||||
event: ParsedSseEvent,
|
||||
|
|
@ -447,8 +421,6 @@ function writeAnthropicHostedWebSearchSseEvent(
|
|||
stream.push(`${serializeSseEvent(nextEvent)}${delimiter}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateAnthropicHostedWebSearchSseState(event: ParsedSseEvent, state: AnthropicHostedWebSearchSseState): void {
|
||||
if (isRecord(event.data) && Number.isFinite(event.data.index)) {
|
||||
state.maxIndex = Math.max(state.maxIndex, Number(event.data.index));
|
||||
|
|
@ -458,8 +430,6 @@ function updateAnthropicHostedWebSearchSseState(event: ParsedSseEvent, state: An
|
|||
state.visibleText ||= sseEventContainsVisibleText(event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function insertAnthropicHostedWebSearchSseBlocks(
|
||||
stream: Transform,
|
||||
state: AnthropicHostedWebSearchSseState,
|
||||
|
|
@ -479,8 +449,6 @@ function insertAnthropicHostedWebSearchSseBlocks(
|
|||
state.injectedBlockCount += blocks.length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function insertAnthropicHostedWebSearchSseAnswer(
|
||||
stream: Transform,
|
||||
state: AnthropicHostedWebSearchSseState,
|
||||
|
|
@ -493,8 +461,6 @@ function insertAnthropicHostedWebSearchSseAnswer(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function shiftAnthropicHostedWebSearchSseEvent(
|
||||
event: ParsedSseEvent,
|
||||
state: AnthropicHostedWebSearchSseState
|
||||
|
|
@ -505,8 +471,6 @@ function shiftAnthropicHostedWebSearchSseEvent(
|
|||
return shiftSseContentBlockIndex(event, state.insertIndex, state.injectedBlockCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function transformHostedWebSearchProtocolResponseValue(
|
||||
value: unknown,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -527,30 +491,6 @@ function transformHostedWebSearchProtocolResponseValue(
|
|||
return { changed: false, value };
|
||||
}
|
||||
|
||||
|
||||
|
||||
function transformHostedWebSearchProtocolSseText(
|
||||
body: string,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
context: Pick<HostedWebSearchProtocolContext, "protocol" | "queryHint" | "requestId">
|
||||
): string {
|
||||
if (context.protocol === "anthropic_messages") {
|
||||
return transformAnthropicWebSearchProtocolSseText(body, records, context.requestId, context.queryHint);
|
||||
}
|
||||
if (context.protocol === "openai_chat_completions") {
|
||||
return transformOpenAiChatHostedWebSearchSseText(body, records, context.queryHint);
|
||||
}
|
||||
if (context.protocol === "openai_responses") {
|
||||
return transformOpenAiResponsesHostedWebSearchSseText(body, records, context.requestId, context.queryHint);
|
||||
}
|
||||
if (context.protocol === "gemini_generate_content") {
|
||||
return transformGeminiHostedWebSearchSseText(body, records, context.queryHint);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformAnthropicWebSearchProtocolResponseValue(
|
||||
value: unknown,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -593,8 +533,6 @@ export function transformAnthropicWebSearchProtocolResponseValue(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformAnthropicWebSearchProtocolSseText(
|
||||
body: string,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -653,8 +591,6 @@ export function transformAnthropicWebSearchProtocolSseText(
|
|||
return `${shiftedEvents.map(serializeSseEvent).join("\n\n")}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformOpenAiChatHostedWebSearchResponseValue(
|
||||
value: unknown,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -691,8 +627,6 @@ export function transformOpenAiChatHostedWebSearchResponseValue(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformOpenAiChatHostedWebSearchSseText(
|
||||
body: string,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -725,8 +659,6 @@ export function transformOpenAiChatHostedWebSearchSseText(
|
|||
return `${shifted.map(serializeSseEvent).join("\n\n")}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformOpenAiResponsesHostedWebSearchResponseValue(
|
||||
value: unknown,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -757,8 +689,6 @@ export function transformOpenAiResponsesHostedWebSearchResponseValue(
|
|||
return { changed: true, value: next };
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformOpenAiResponsesHostedWebSearchSseText(
|
||||
body: string,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -785,8 +715,6 @@ export function transformOpenAiResponsesHostedWebSearchSseText(
|
|||
return serializeOpenAiResponsesSseEvents(normalizeOpenAiResponsesSseEvents(shifted, answer));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformGeminiHostedWebSearchResponseValue(
|
||||
value: unknown,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -829,8 +757,6 @@ export function transformGeminiHostedWebSearchResponseValue(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function transformGeminiHostedWebSearchSseText(
|
||||
body: string,
|
||||
records: BrowserWebSearchProtocolRecord[],
|
||||
|
|
@ -850,8 +776,6 @@ export function transformGeminiHostedWebSearchSseText(
|
|||
return `${events.map(serializeSseEvent).join("\n\n")}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiChatResponseContainsVisibleText(value: Record<string, unknown>): boolean {
|
||||
if (!Array.isArray(value.choices)) {
|
||||
return false;
|
||||
|
|
@ -862,8 +786,6 @@ function openAiChatResponseContainsVisibleText(value: Record<string, unknown>):
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiChatSseContainsVisibleText(events: ParsedSseEvent[]): boolean {
|
||||
return events.some((event) => {
|
||||
const choices = isRecord(event.data) && Array.isArray(event.data.choices) ? event.data.choices : [];
|
||||
|
|
@ -874,8 +796,6 @@ function openAiChatSseContainsVisibleText(events: ParsedSseEvent[]): boolean {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateOpenAiChatSseFinishReason(event: ParsedSseEvent): ParsedSseEvent {
|
||||
if (!isRecord(event.data) || !Array.isArray(event.data.choices)) {
|
||||
return event;
|
||||
|
|
@ -891,14 +811,10 @@ function updateOpenAiChatSseFinishReason(event: ParsedSseEvent): ParsedSseEvent
|
|||
return changed ? { ...event, data: { ...event.data, choices } } : event;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesValueContainsVisibleText(value: Record<string, unknown>): boolean {
|
||||
return Array.isArray(value.output) && value.output.some(openAiResponsesItemContainsVisibleText);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesItemContainsVisibleText(item: unknown): boolean {
|
||||
if (!isRecord(item)) {
|
||||
return false;
|
||||
|
|
@ -909,14 +825,10 @@ function openAiResponsesItemContainsVisibleText(item: unknown): boolean {
|
|||
return Boolean(stringValue(item.text)?.trim());
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesSseContainsVisibleText(events: ParsedSseEvent[]): boolean {
|
||||
return Boolean(openAiResponsesSseVisibleText(events));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesSseVisibleText(events: ParsedSseEvent[]): string | undefined {
|
||||
let deltaText = "";
|
||||
let doneText = "";
|
||||
|
|
@ -943,8 +855,6 @@ function openAiResponsesSseVisibleText(events: ParsedSseEvent[]): string | undef
|
|||
return nonEmptyText(deltaText) ?? nonEmptyText(doneText) ?? nonEmptyText(itemText);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesItemText(item: unknown): string | undefined {
|
||||
if (!isRecord(item)) {
|
||||
return undefined;
|
||||
|
|
@ -958,14 +868,10 @@ function openAiResponsesItemText(item: unknown): string | undefined {
|
|||
return stringValue(item.text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function nonEmptyText(value: string | undefined): string | undefined {
|
||||
return value && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesWebSearchCallItems(records: BrowserWebSearchProtocolRecord[], requestId: string): Record<string, unknown>[] {
|
||||
return records.map((record, index) => ({
|
||||
action: {
|
||||
|
|
@ -978,8 +884,6 @@ function openAiResponsesWebSearchCallItems(records: BrowserWebSearchProtocolReco
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesMessageItem(answer: string, requestId: string): Record<string, unknown> {
|
||||
return {
|
||||
content: [{ annotations: [], text: answer, type: "output_text" }],
|
||||
|
|
@ -990,8 +894,6 @@ function openAiResponsesMessageItem(answer: string, requestId: string): Record<s
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function nextOpenAiResponsesOutputIndex(events: ParsedSseEvent[]): number {
|
||||
const indexes = events.flatMap((event) => {
|
||||
const data = isRecord(event.data) ? event.data : undefined;
|
||||
|
|
@ -1001,8 +903,6 @@ function nextOpenAiResponsesOutputIndex(events: ParsedSseEvent[]): number {
|
|||
return indexes.length === 0 ? 0 : Math.max(...indexes) + 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesSseAnswerEvents(answer: string, requestId: string, outputIndex: number): ParsedSseEvent[] {
|
||||
const itemId = `msg_${sanitizeAnthropicToolUseId(requestId)}_web_search_answer`;
|
||||
const contentIndex = 0;
|
||||
|
|
@ -1059,8 +959,6 @@ function openAiResponsesSseAnswerEvents(answer: string, requestId: string, outpu
|
|||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateOpenAiResponsesCompletedStatus(event: ParsedSseEvent): ParsedSseEvent {
|
||||
if (!isRecord(event.data) || stringValue(event.data.type) !== "response.completed") {
|
||||
return event;
|
||||
|
|
@ -1080,8 +978,6 @@ function updateOpenAiResponsesCompletedStatus(event: ParsedSseEvent): ParsedSseE
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function normalizeOpenAiResponsesSseEvents(events: ParsedSseEvent[], visibleText?: string): ParsedSseEvent[] {
|
||||
const outputIndexMap = openAiResponsesOutputIndexMap(events);
|
||||
return events.flatMap((event) => {
|
||||
|
|
@ -1095,14 +991,10 @@ function normalizeOpenAiResponsesSseEvents(events: ParsedSseEvent[], visibleText
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function serializeOpenAiResponsesSseEvents(events: ParsedSseEvent[]): string {
|
||||
return `${events.map(serializeSseEvent).join("\n\n")}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAiResponsesOutputIndexMap(events: ParsedSseEvent[]): Map<number, number> {
|
||||
const indexes: number[] = [];
|
||||
for (const event of events) {
|
||||
|
|
@ -1118,8 +1010,6 @@ function openAiResponsesOutputIndexMap(events: ParsedSseEvent[]): Map<number, nu
|
|||
return new Map(indexes.sort((left, right) => left - right).map((index, nextIndex) => [index, nextIndex]));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function remapOpenAiResponsesOutputIndex(event: ParsedSseEvent, outputIndexMap: Map<number, number>): ParsedSseEvent {
|
||||
if (!isRecord(event.data)) {
|
||||
return event;
|
||||
|
|
@ -1137,8 +1027,6 @@ function remapOpenAiResponsesOutputIndex(event: ParsedSseEvent, outputIndexMap:
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateOpenAiResponsesCompletedOutput(event: ParsedSseEvent, visibleText?: string): ParsedSseEvent {
|
||||
if (!isRecord(event.data) || stringValue(event.data.type) !== "response.completed") {
|
||||
return event;
|
||||
|
|
@ -1167,8 +1055,6 @@ function updateOpenAiResponsesCompletedOutput(event: ParsedSseEvent, visibleText
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function isOpenAiResponsesReasoningSseEvent(event: ParsedSseEvent): boolean {
|
||||
const data = isRecord(event.data) ? event.data : undefined;
|
||||
if (!data) {
|
||||
|
|
@ -1186,34 +1072,24 @@ function isOpenAiResponsesReasoningSseEvent(event: ParsedSseEvent): boolean {
|
|||
return stringValue(part?.type) === "reasoning_text";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiResponseValueContainsVisibleText(value: Record<string, unknown>): boolean {
|
||||
return Array.isArray(value.candidates) && value.candidates.some(geminiCandidateContainsVisibleText);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiResponseArrayContainsVisibleText(values: unknown[]): boolean {
|
||||
return values.some((value) => isRecord(value) && geminiResponseValueContainsVisibleText(value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiCandidateContainsVisibleText(candidate: unknown): boolean {
|
||||
const content = isRecord(candidate) && isRecord(candidate.content) ? candidate.content : undefined;
|
||||
const parts = Array.isArray(content?.parts) ? content.parts : [];
|
||||
return parts.some((part) => isRecord(part) && Boolean(stringValue(part.text)?.trim()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiSseContainsVisibleText(events: ParsedSseEvent[]): boolean {
|
||||
return events.some((event) => isRecord(event.data) && geminiResponseValueContainsVisibleText(event.data));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiAnswerCandidate(answer: string): Record<string, unknown> {
|
||||
return {
|
||||
content: {
|
||||
|
|
@ -1225,28 +1101,20 @@ function geminiAnswerCandidate(answer: string): Record<string, unknown> {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geminiAnswerCandidateChunk(answer: string): Record<string, unknown> {
|
||||
return {
|
||||
candidates: [geminiAnswerCandidate(answer)]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function firstSseDataRecord(events: ParsedSseEvent[]): Record<string, unknown> | undefined {
|
||||
return events.map((event) => isRecord(event.data) ? event.data : undefined).find(Boolean);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function doneSseEventIndex(events: ParsedSseEvent[]): number {
|
||||
return events.findIndex((event) => event.raw?.includes("[DONE]"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function sseEventIsDone(event: ParsedSseEvent): boolean {
|
||||
return Boolean(event.raw?.includes("[DONE]"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/g
|
|||
import { authorize, reserveApiKeyLimits } from "@ccr/core/gateway/auth/api-key-authorizer";
|
||||
import { parseJsonObject, readRequestBody, sendJson } from "@ccr/core/gateway/http/io";
|
||||
import { shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
|
||||
import { applyCors, endpoint, shouldServeGatewayRequest } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { applyCors, shouldServeGatewayRequest } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { billingUsageSyncPath, rawTraceSyncPath } from "@ccr/core/gateway/internal/shared";
|
||||
import type { BrowserAutomationMcpIntegration } from "@ccr/core/gateway/internal/shared";
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ export type CoreGatewayHealth = {
|
|||
|
||||
|
||||
export type ManagedGatewayRuntimeMarker = {
|
||||
generatedConfigFile?: unknown;
|
||||
gatewayEntry?: unknown;
|
||||
pid?: unknown;
|
||||
runtimeId?: unknown;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import { createGatewayModelsResponse, prepareClaudeAppDiscoveredModelRequest, pr
|
|||
import { resolveProviderLogName, resolveResponseProviderProtocol, sanitizeHeaderValue } from "@ccr/core/providers/runtime-topology";
|
||||
import { createBodySampler, requestLogSampled, shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
|
||||
import { RequestRouteTraceRecorder } from "@ccr/core/observability/route-trace";
|
||||
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { coreGatewayUsageAttributionConfig } from "@ccr/core/gateway/core-runtime/config-compiler";
|
||||
import { providerModelPricingForUsage } from "@ccr/core/models/pricing-service";
|
||||
import { clientClosedRequestStatusCode, clientDisconnectMessage, resolveStreamRequestLogOutcome, UpstreamRequestError } from "@ccr/core/gateway/internal/shared";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import OpenAI from "openai";
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export class MediaJobStore {
|
|||
export class MediaArtifactStore {
|
||||
readonly artifactsDir: string;
|
||||
|
||||
constructor(private readonly rootDir: string) {
|
||||
constructor(rootDir: string) {
|
||||
this.artifactsDir = path.join(rootDir, "artifacts");
|
||||
mkdirSync(this.artifactsDir, { mode: privateDirectoryMode, recursive: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,14 +195,12 @@ class GatewayPluginService {
|
|||
private proxyRoutes: RegisteredProxyRoute[] = [];
|
||||
private providerAccountConnectors = new Map<string, GatewayPluginProviderAccountConnector>();
|
||||
private resourceOwnerIds = new Set<string>();
|
||||
private running = false;
|
||||
private stopHooks: StopHook[] = [];
|
||||
private virtualModelProfiles: unknown[] = [];
|
||||
|
||||
async start(config: AppConfig): Promise<void> {
|
||||
await this.stop({ nextConfig: config });
|
||||
this.config = config;
|
||||
this.running = true;
|
||||
|
||||
for (const pluginConfig of config.plugins ?? []) {
|
||||
if (pluginConfig.enabled === false) {
|
||||
|
|
@ -248,7 +246,6 @@ class GatewayPluginService {
|
|||
this.gatewayRoutes = [];
|
||||
this.proxyRoutes = [];
|
||||
this.providerAccountConnectors.clear();
|
||||
this.running = false;
|
||||
this.virtualModelProfiles = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,19 +444,3 @@ function windowsCommandQuote(value: string): string {
|
|||
function isWindowsCommandScript(command: string): boolean {
|
||||
return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(path.basename(command));
|
||||
}
|
||||
|
||||
function windowsCommandScriptInvocation(command: string, args: string[]): string {
|
||||
return [
|
||||
"call",
|
||||
windowsCommandInvocationArg(command),
|
||||
...args.map(windowsCommandInvocationArg)
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function windowsCommandInvocationArg(value: string): string {
|
||||
const normalized = value.replace(/\r?\n/g, " ");
|
||||
if (!normalized) {
|
||||
return "\"\"";
|
||||
}
|
||||
return `"${normalized.replace(/[\^"%&|<>()]/g, "^$&")}"`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const CCR_CLI_COMPANION_RUNTIME_FILE_NAMES = [
|
|||
"browser-web-search-proxy-mcp.js",
|
||||
"fusion-tool-fallback-mcp.js",
|
||||
"fusion-vision-mcp.js",
|
||||
"gateway-bootstrap.js",
|
||||
"media-tools-proxy-mcp.js",
|
||||
"next-ai-gateway.js",
|
||||
"request-log-worker.js",
|
||||
|
|
@ -1116,20 +1117,6 @@ function sendProfileProcessSignal(pid: number | undefined, signal: NodeJS.Signal
|
|||
}
|
||||
}
|
||||
|
||||
async function waitForProcessExit(pid: number | undefined, timeoutMs: number): Promise<boolean> {
|
||||
if (!pid) {
|
||||
return true;
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (!isProcessAlive(pid)) {
|
||||
return true;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
return !isProcessAlive(pid);
|
||||
}
|
||||
|
||||
async function waitForProfileAppStart(entry: Pick<RunningProfileApp, "pid" | "pidIsLauncher" | "spawnError" | "userDataDir">, timeoutMs: number): Promise<boolean> {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, availableGatewayModelIds, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, isGatewayProviderEnabled, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { replacePersistedApiKeys } from "@ccr/core/config/config-repository";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import {
|
||||
CLAUDE_CODE_MCP_CONFIG_ENV,
|
||||
|
|
@ -32,7 +32,6 @@ import {
|
|||
import {
|
||||
piWrapperFilename,
|
||||
resolvePiAgentDir,
|
||||
resolvePiSessionDir,
|
||||
writePiGatewayConfig
|
||||
} from "@ccr/core/agents/pi/profile-config";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
|
|
@ -3233,31 +3232,6 @@ function unavailableModelStatus(profile: ProfileConfig, file: string): ProfileCl
|
|||
};
|
||||
}
|
||||
|
||||
function disabledProfileMessage(profile: ProfileConfig): string {
|
||||
if (profile.agent === "claude-code") {
|
||||
return "Claude Code profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "grok") {
|
||||
return "Grok CLI profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "kimi") {
|
||||
return "Kimi CLI profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "pi") {
|
||||
return "Pi profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "claude-design") {
|
||||
return "Claude Design profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "opencode") {
|
||||
return "OpenCode profile is disabled.";
|
||||
}
|
||||
if (profile.agent === "kilo") {
|
||||
return "Kilo CLI profile is disabled.";
|
||||
}
|
||||
return `${codexCompatibleClientName(profile.agent)} profile is disabled.`;
|
||||
}
|
||||
|
||||
function isGlobalProfile(profile: ProfileConfig): boolean {
|
||||
return normalizeProfileScope(profile.scope) === "global";
|
||||
}
|
||||
|
|
@ -3494,10 +3468,6 @@ function tomlInlineStringTable(values: Record<string, string>): string {
|
|||
.join(", ")} }`;
|
||||
}
|
||||
|
||||
function tomlStringContent(value: string): string {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { parseProviderManifestPayload } from "@ccr/core/contracts/deep-link";
|
|||
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey, providerIdentitySafetyIssue } from "@ccr/core/providers/presets/index";
|
||||
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import type {
|
||||
GatewayProviderConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
|
|
|
|||
|
|
@ -1217,7 +1217,7 @@ function protocolHints(value: string): GatewayProviderCapabilityProtocol[] {
|
|||
function isProtocolSupported(
|
||||
status: number | undefined,
|
||||
message: string,
|
||||
protocol?: GatewayProviderCapabilityProtocol
|
||||
_protocol?: GatewayProviderCapabilityProtocol
|
||||
): boolean {
|
||||
if (status === undefined) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ export function providerCredentialPriority(credential: ProviderCredentialConfig,
|
|||
return Number.isFinite(credential.priority) ? Number(credential.priority) : index + 1;
|
||||
}
|
||||
|
||||
|
||||
export function toCoreGatewayProviders(provider: GatewayProviderConfig): CoreGatewayProvider[] {
|
||||
if (!isGatewayProviderEnabled(provider)) {
|
||||
return [];
|
||||
|
|
@ -115,7 +114,6 @@ export function toCoreGatewayProviders(provider: GatewayProviderConfig): CoreGat
|
|||
.filter((item): item is CoreGatewayProvider => Boolean(item));
|
||||
}
|
||||
|
||||
|
||||
function toCoreGatewayProvidersForCapability(
|
||||
provider: GatewayProviderConfig,
|
||||
capability?: GatewayProviderCapability
|
||||
|
|
@ -131,7 +129,6 @@ function toCoreGatewayProvidersForCapability(
|
|||
.filter((item): item is CoreGatewayProvider => Boolean(item));
|
||||
}
|
||||
|
||||
|
||||
function toCoreGatewayProvider(
|
||||
provider: GatewayProviderConfig,
|
||||
capability?: GatewayProviderCapability,
|
||||
|
|
@ -173,7 +170,6 @@ function toCoreGatewayProvider(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
export function sortProviderCredentialsForConfig(credentials: ProviderCredentialConfig[]): ProviderCredentialConfig[] {
|
||||
return [...credentials].sort((left, right) =>
|
||||
providerCredentialPriority(left, 0) - providerCredentialPriority(right, 0) ||
|
||||
|
|
@ -181,7 +177,6 @@ export function sortProviderCredentialsForConfig(credentials: ProviderCredential
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export function normalizedProviderCapabilities(provider: GatewayProviderConfig): GatewayProviderCapability[] {
|
||||
const capabilities = Array.isArray(provider.capabilities) ? provider.capabilities : [];
|
||||
const normalized: GatewayProviderCapability[] = [];
|
||||
|
|
@ -212,7 +207,6 @@ export function normalizedProviderCapabilities(provider: GatewayProviderConfig):
|
|||
return applyPresetProtocolLock(provider, normalized);
|
||||
}
|
||||
|
||||
|
||||
function applyPresetProtocolLock(
|
||||
provider: GatewayProviderConfig,
|
||||
capabilities: GatewayProviderCapability[]
|
||||
|
|
@ -239,7 +233,6 @@ function applyPresetProtocolLock(
|
|||
: [];
|
||||
}
|
||||
|
||||
|
||||
function lockedProviderPresetProtocols(
|
||||
provider: GatewayProviderConfig,
|
||||
capabilities: GatewayProviderCapability[]
|
||||
|
|
@ -265,7 +258,6 @@ function lockedProviderPresetProtocols(
|
|||
return [];
|
||||
}
|
||||
|
||||
|
||||
function providerCapabilityPriority(capability: GatewayProviderCapability): number {
|
||||
if (capability.source === "preset") {
|
||||
return 0;
|
||||
|
|
@ -276,24 +268,20 @@ function providerCapabilityPriority(capability: GatewayProviderCapability): numb
|
|||
return 1;
|
||||
}
|
||||
|
||||
|
||||
export function providerCapabilityInternalName(provider: GatewayProviderConfig, protocol: GatewayProviderCapabilityProtocol): string {
|
||||
return `${providerRuntimeId(provider)}::${protocol}`;
|
||||
}
|
||||
|
||||
|
||||
function providerCapabilityLegacyInternalName(providerName: string, protocol: GatewayProviderCapabilityProtocol): string {
|
||||
return `${providerName}::${protocol}`;
|
||||
}
|
||||
|
||||
|
||||
export function providerCapabilityNameMatches(provider: GatewayProviderConfig, protocol: GatewayProviderCapabilityProtocol, value: string): boolean {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return providerCapabilityInternalName(provider, protocol).toLowerCase() === normalized ||
|
||||
providerCapabilityLegacyInternalName(provider.name, protocol).toLowerCase() === normalized;
|
||||
}
|
||||
|
||||
|
||||
export function sanitizeHeaderValue(value: unknown): string {
|
||||
// HTTP header values must be ByteString (code point <= 255). Values derived
|
||||
// from user-facing names — model selectors like "小米mimo/...", provider
|
||||
|
|
@ -308,7 +296,6 @@ export function sanitizeHeaderValue(value: unknown): string {
|
|||
return sanitized || "unknown";
|
||||
}
|
||||
|
||||
|
||||
export function providerCredentialInternalName(
|
||||
provider: GatewayProviderConfig,
|
||||
protocol: GatewayProviderCapabilityProtocol,
|
||||
|
|
@ -317,7 +304,6 @@ export function providerCredentialInternalName(
|
|||
return `${providerCapabilityInternalName(provider, protocol)}::cred:${providerCredentialSlug(providerCredentialRuntimeId(provider, credential))}`;
|
||||
}
|
||||
|
||||
|
||||
export function parseProviderCredentialInternalName(value: string | undefined): {
|
||||
credentialSlug: string;
|
||||
providerId: string;
|
||||
|
|
@ -339,7 +325,6 @@ export function parseProviderCredentialInternalName(value: string | undefined):
|
|||
return protocol && providerId ? { credentialSlug, providerId, protocol } : undefined;
|
||||
}
|
||||
|
||||
|
||||
export function providerCredentialSlug(value: string | undefined): string {
|
||||
return (value ?? "")
|
||||
.trim()
|
||||
|
|
@ -348,7 +333,6 @@ export function providerCredentialSlug(value: string | undefined): string {
|
|||
.replace(/^-+|-+$/g, "") || "key";
|
||||
}
|
||||
|
||||
|
||||
export function providerCredentialRuntimeId(
|
||||
provider: GatewayProviderConfig,
|
||||
credential: ProviderCredentialConfig,
|
||||
|
|
@ -363,17 +347,14 @@ export function providerCredentialRuntimeId(
|
|||
return label ? `${providerCredentialSlug(label)}-${oneBasedIndex}` : `key-${oneBasedIndex}`;
|
||||
}
|
||||
|
||||
|
||||
function providerCredentialSortKey(credential: ProviderCredentialConfig): string {
|
||||
return providerCredentialSlug(credential.id || credential.name || credential.label);
|
||||
}
|
||||
|
||||
|
||||
export function providerCredentialApiKey(credential: ProviderCredentialConfig): string {
|
||||
return credential.api_key || credential.apiKey || credential.apikey || "";
|
||||
}
|
||||
|
||||
|
||||
export function findProviderCredentialByRuntimeId(
|
||||
provider: GatewayProviderConfig,
|
||||
credentialId: string
|
||||
|
|
@ -386,7 +367,6 @@ export function findProviderCredentialByRuntimeId(
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
export function findProviderCredentialBySlug(
|
||||
provider: GatewayProviderConfig,
|
||||
credentialSlug: string
|
||||
|
|
@ -395,7 +375,6 @@ export function findProviderCredentialBySlug(
|
|||
return (provider.credentials ?? []).find((credential, index) => providerCredentialSlug(providerCredentialRuntimeId(provider, credential, index)) === normalizedSlug);
|
||||
}
|
||||
|
||||
|
||||
export function normalizeProviderProtocol(value: unknown): GatewayProviderProtocol | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
|
|
@ -447,7 +426,6 @@ export function normalizeProviderCapabilityProtocol(value: unknown): GatewayProv
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
export function inferProtocol(provider: GatewayProviderConfig): GatewayProviderProtocol {
|
||||
const url = readBaseUrl(provider)?.toLowerCase() ?? "";
|
||||
const transformerNames = JSON.stringify(provider.transformer ?? "").toLowerCase();
|
||||
|
|
@ -463,7 +441,6 @@ export function inferProtocol(provider: GatewayProviderConfig): GatewayProviderP
|
|||
return "openai_chat_completions";
|
||||
}
|
||||
|
||||
|
||||
export function resolveResponseProviderProtocol(headers: Headers, config: AppConfig | undefined): GatewayProviderProtocol | undefined {
|
||||
const ccrProtocol = normalizeProviderProtocol(headers.get("x-ccr-provider-protocol"));
|
||||
if (ccrProtocol) {
|
||||
|
|
@ -492,7 +469,6 @@ export function resolveResponseProviderProtocol(headers: Headers, config: AppCon
|
|||
return normalizeProviderProtocol(provider.type) ?? normalizeProviderProtocol(provider.provider) ?? inferProtocol(provider);
|
||||
}
|
||||
|
||||
|
||||
export function resolveProviderLogName(headers: Headers, config: AppConfig | undefined, fallbackModel?: string): string | undefined {
|
||||
const providerSelector =
|
||||
headers.get("x-gateway-target-provider-name")?.trim() ||
|
||||
|
|
@ -511,15 +487,6 @@ export function resolveProviderLogName(headers: Headers, config: AppConfig | und
|
|||
return modelProvider?.name;
|
||||
}
|
||||
|
||||
|
||||
function providerMatchesName(provider: GatewayProviderConfig, name: string): boolean {
|
||||
const normalizedName = name.trim().toLowerCase();
|
||||
return [provider.id, provider.name, provider.provider]
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
.some((value) => value.trim().toLowerCase() === normalizedName);
|
||||
}
|
||||
|
||||
|
||||
function normalizeProviderRuntimeBaseUrl(value: string | undefined, type: GatewayProviderCapabilityProtocol): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
|
|
@ -529,7 +496,6 @@ function normalizeProviderRuntimeBaseUrl(value: string | undefined, type: Gatewa
|
|||
: normalizeProviderBaseUrlInput(value) || undefined;
|
||||
}
|
||||
|
||||
|
||||
function readBaseUrl(provider: GatewayProviderConfig): string | undefined {
|
||||
return provider.baseurl || provider.baseUrl || provider.api_base_url;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import type {
|
|||
ProxyForwardMode,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyMode,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import path from "node:path";
|
|||
|
||||
export const APP_NAME = "Claude Code Router";
|
||||
export const APP_STORAGE_NAME = "claude-code-router";
|
||||
export const LEGACY_CONFIGDIR = path.join(os.homedir(), ".claude-code-router");
|
||||
|
||||
const homeDirEnv = "CCR_INTERNAL_HOME_DIR";
|
||||
const appDataDirEnv = "CCR_INTERNAL_APP_DATA_DIR";
|
||||
|
|
@ -33,6 +32,8 @@ export function resolveRuntimeAppPath(name: RuntimePathName): string {
|
|||
return fallbackUserDataDir();
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIGDIR = path.join(resolveRuntimeAppPath("home"), ".claude-code-router");
|
||||
|
||||
export function resolveRuntimeConfigDir(): string {
|
||||
if (process.platform === "win32") {
|
||||
return path.join(resolveRuntimeAppPath("appData"), APP_STORAGE_NAME);
|
||||
|
|
|
|||
|
|
@ -888,100 +888,6 @@ function buildBuckets(
|
|||
});
|
||||
}
|
||||
|
||||
function buildModelRows(events: StoredUsageEvent[]): UsageComparisonRow[] {
|
||||
const grouped = new Map<string, StoredUsageEvent[]>();
|
||||
for (const event of events) {
|
||||
const key = `${event.provider}::${event.model}`;
|
||||
const bucket = grouped.get(key) ?? [];
|
||||
bucket.push(event);
|
||||
grouped.set(key, bucket);
|
||||
}
|
||||
|
||||
const rows = Array.from(grouped.entries())
|
||||
.map(([key, groupedEvents]) => {
|
||||
const latest = groupedEvents.at(-1);
|
||||
return {
|
||||
...buildTotals(groupedEvents),
|
||||
caption: latest?.provider || "unknown",
|
||||
credentialId: latest?.credentialId || undefined,
|
||||
key,
|
||||
label: latest?.model || "unknown",
|
||||
maxShare: 0,
|
||||
model: latest?.model,
|
||||
provider: latest?.provider
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalTokens - a.totalTokens || b.requestCount - a.requestCount)
|
||||
.slice(0, 8);
|
||||
|
||||
return applyMaxShare(rows, (row) => row.totalTokens || row.requestCount);
|
||||
}
|
||||
|
||||
function buildClientModelRows(events: StoredUsageEvent[]): UsageComparisonRow[] {
|
||||
const grouped = new Map<string, StoredUsageEvent[]>();
|
||||
for (const event of events) {
|
||||
const key = `${event.client}::${event.provider}::${event.credentialId}::${event.model}`;
|
||||
const bucket = grouped.get(key) ?? [];
|
||||
bucket.push(event);
|
||||
grouped.set(key, bucket);
|
||||
}
|
||||
|
||||
const rows = Array.from(grouped.entries())
|
||||
.map(([key, groupedEvents]) => {
|
||||
const latest = groupedEvents.at(-1);
|
||||
const model = latest?.model || "unknown";
|
||||
const provider = latest?.provider || "unknown";
|
||||
const credentialId = latest?.credentialId || "";
|
||||
return {
|
||||
...buildTotals(groupedEvents),
|
||||
caption: credentialId ? `${provider} / ${credentialId} / ${model}` : `${provider} / ${model}`,
|
||||
client: latest?.client,
|
||||
credentialId: credentialId || undefined,
|
||||
key,
|
||||
label: latest?.client || "unknown",
|
||||
maxShare: 0,
|
||||
model,
|
||||
provider
|
||||
};
|
||||
})
|
||||
.sort(compareUsageRows)
|
||||
.slice(0, 25);
|
||||
|
||||
return applyMaxShare(rows, (row) => row.totalTokens || row.requestCount);
|
||||
}
|
||||
|
||||
function buildProviderModelRows(events: StoredUsageEvent[]): UsageComparisonRow[] {
|
||||
const grouped = new Map<string, StoredUsageEvent[]>();
|
||||
for (const event of events) {
|
||||
const key = `${event.provider}::${event.credentialId}::${event.model}`;
|
||||
const bucket = grouped.get(key) ?? [];
|
||||
bucket.push(event);
|
||||
grouped.set(key, bucket);
|
||||
}
|
||||
|
||||
const rows = Array.from(grouped.entries())
|
||||
.map(([key, groupedEvents]) => {
|
||||
const latest = groupedEvents.at(-1);
|
||||
const model = latest?.model || "unknown";
|
||||
const provider = latest?.provider || "unknown";
|
||||
const credentialId = latest?.credentialId || "";
|
||||
return {
|
||||
...buildTotals(groupedEvents),
|
||||
caption: credentialId ? `${credentialId} / ${model}` : model,
|
||||
credentialId: credentialId || undefined,
|
||||
key,
|
||||
label: provider,
|
||||
maxShare: 0,
|
||||
model,
|
||||
provider
|
||||
};
|
||||
})
|
||||
.sort(compareUsageRows)
|
||||
.slice(0, 25);
|
||||
|
||||
return applyMaxShare(rows, (row) => row.totalTokens || row.requestCount);
|
||||
}
|
||||
|
||||
function buildRecentRequestRows(events: StoredUsageEvent[]): UsageComparisonRow[] {
|
||||
const recent = events.slice(-10).reverse();
|
||||
const rows = recent.map((event) => ({
|
||||
|
|
@ -1000,10 +906,6 @@ function buildRecentRequestRows(events: StoredUsageEvent[]): UsageComparisonRow[
|
|||
return applyMaxShare(rows, (row) => row.totalTokens || row.avgDurationMs || 1);
|
||||
}
|
||||
|
||||
function compareUsageRows(a: UsageComparisonRow, b: UsageComparisonRow): number {
|
||||
return b.totalTokens - a.totalTokens || b.requestCount - a.requestCount || a.label.localeCompare(b.label);
|
||||
}
|
||||
|
||||
function applyMaxShare<T extends UsageComparisonRow>(
|
||||
rows: T[],
|
||||
readValue: (row: T) => number
|
||||
|
|
|
|||
|
|
@ -6,14 +6,27 @@ import net from "node:net";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import packageJson from "../../package.json";
|
||||
import { loadPersistedAppSetting, replacePersistedAppSetting } from "@ccr/core/config/app-config-store";
|
||||
import { loadOnboardingFinished, markOnboardingFinished } from "@ccr/core/config/onboarding-state";
|
||||
import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/core/agents/bot-gateway/handoff-scan-service";
|
||||
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
|
||||
import { syncClaudeAppGatewayConfig, restoreClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledOpenCodeAppExecutable } from "@ccr/core/agents/opencode/app-launch";
|
||||
import { loadAppConfig, saveApiKeysConfig, saveAppConfig } from "@ccr/core/config/config";
|
||||
import { API_KEYS_DB_FILE, APP_CONFIG_DB_FILE, APP_NAME, CONFIGDIR, CONFIG_FILE, DATADIR, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, ONBOARDING_FINISHED_FILE, PROXY_CA_CERT_FILE, REQUEST_LOGS_DB_FILE, USAGE_DB_FILE } from "@ccr/core/config/constants";
|
||||
import {
|
||||
APP_CONFIG_DB_FILE,
|
||||
APP_NAME,
|
||||
CONFIGDIR,
|
||||
DATADIR,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_APP_CONFIG_DB_FILES,
|
||||
LEGACY_API_KEYS_DB_FILES,
|
||||
LEGACY_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
PROXY_CA_CERT_FILE,
|
||||
REQUEST_LOGS_DB_FILE,
|
||||
USAGE_DB_FILE
|
||||
} from "@ccr/core/config/constants";
|
||||
import { detectProviderIcon } from "@ccr/core/providers/icons";
|
||||
import { fetchProviderManifest } from "@ccr/core/providers/manifest-service";
|
||||
import { getLocalAgentProviderCandidates, importLocalAgentProvider, probeLocalAgentProvider } from "@ccr/core/agents/local-providers/service";
|
||||
|
|
@ -105,7 +118,6 @@ type WebManagementSecurityContext = {
|
|||
|
||||
const defaultWebHost = "127.0.0.1";
|
||||
const defaultWebPort = 3458;
|
||||
const onboardingFinishedAtSettingKey = "onboardingFinishedAt";
|
||||
const maxRpcBodyBytes = 8 * 1024 * 1024;
|
||||
const webAuthHeader = "x-ccr-web-auth";
|
||||
const webAuthQueryParam = "ccr_web_token";
|
||||
|
|
@ -279,7 +291,7 @@ const rpcHandlers: Record<string, RpcHandler> = {
|
|||
serviceToken === process.env.CCR_SERVICE_INSTANCE_TOKEN
|
||||
}),
|
||||
getLocalAgentProviderCandidates: () => getLocalAgentProviderCandidates(),
|
||||
getOnboardingFinished: async () => Boolean(readString(await loadPersistedAppSetting(onboardingFinishedAtSettingKey)) || existsSync(ONBOARDING_FINISHED_FILE)),
|
||||
getOnboardingFinished: () => loadOnboardingFinished(),
|
||||
getPluginMarketplace,
|
||||
getProfileOpenCommand: async (request) => getProfileOpenCommand(await loadAppConfig(), request as ProfileOpenRequest),
|
||||
getProfileRuntimeStatus: () => getProfileRuntimeStatus(),
|
||||
|
|
@ -396,7 +408,7 @@ const rpcHandlers: Record<string, RpcHandler> = {
|
|||
scanBotHandoffWifiTargets: () => scanBotHandoffWifiTargets(),
|
||||
selectPluginDirectory: (directory) => inspectPluginDirectory(readRequiredString(directory, "Plugin directory path is required.")),
|
||||
setOnboardingFinished: async () => {
|
||||
await replacePersistedAppSetting(onboardingFinishedAtSettingKey, new Date().toISOString());
|
||||
await markOnboardingFinished();
|
||||
return true;
|
||||
},
|
||||
setProxyNetworkCaptureEnabled: (enabled) => proxyService.setNetworkCaptureEnabled(Boolean(enabled)),
|
||||
|
|
@ -479,14 +491,11 @@ function getCliAppInfo(): AppInfo {
|
|||
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
|
||||
const opencodeAppPath = findInstalledOpenCodeAppExecutable().executable;
|
||||
return {
|
||||
appConfigDbFile: APP_CONFIG_DB_FILE,
|
||||
apiKeysDbFile: API_KEYS_DB_FILE,
|
||||
...(chatgptAppPath ? { chatgptAppPath } : {}),
|
||||
configDbFile: APP_CONFIG_DB_FILE,
|
||||
configDir: CONFIGDIR,
|
||||
configFile: CONFIG_FILE,
|
||||
dataDir: DATADIR,
|
||||
desktop: false,
|
||||
gatewayConfigFile: GATEWAY_CONFIG_FILE,
|
||||
launchAtLoginSupported: false,
|
||||
name: APP_NAME,
|
||||
...(opencodeAppPath ? { opencodeAppPath } : {}),
|
||||
|
|
@ -729,7 +738,7 @@ async function exportAppData(): Promise<AppDataExportResult> {
|
|||
version: packageJson.version
|
||||
},
|
||||
appState: {
|
||||
onboardingFinished: Boolean(readString(await loadPersistedAppSetting(onboardingFinishedAtSettingKey)) || existsSync(ONBOARDING_FINISHED_FILE))
|
||||
onboardingFinished: await loadOnboardingFinished()
|
||||
},
|
||||
config: await loadAppConfig(),
|
||||
exportedAt,
|
||||
|
|
@ -775,7 +784,8 @@ function readDataExportFiles(): Array<{ base64: string; name: string; path: stri
|
|||
function dataExportCandidateFiles(): string[] {
|
||||
return uniqueStrings([
|
||||
...sqliteDataFiles(APP_CONFIG_DB_FILE),
|
||||
...sqliteDataFiles(API_KEYS_DB_FILE),
|
||||
...LEGACY_APP_CONFIG_DB_FILES.flatMap(sqliteDataFiles),
|
||||
...LEGACY_API_KEYS_DB_FILES.flatMap(sqliteDataFiles),
|
||||
...sqliteDataFiles(REQUEST_LOGS_DB_FILE),
|
||||
...sqliteDataFiles(USAGE_DB_FILE)
|
||||
]);
|
||||
|
|
@ -788,12 +798,9 @@ function sqliteDataFiles(file: string): string[] {
|
|||
function assertExportTargetIsNotInternalDataFile(file: string): void {
|
||||
const target = path.resolve(file);
|
||||
const reserved = new Set([
|
||||
CONFIG_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
LEGACY_CONFIG_FILE,
|
||||
APP_CONFIG_DB_FILE,
|
||||
API_KEYS_DB_FILE,
|
||||
REQUEST_LOGS_DB_FILE,
|
||||
USAGE_DB_FILE,
|
||||
...dataExportCandidateFiles()
|
||||
].map((item) => path.resolve(item)));
|
||||
if (reserved.has(target)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
|
|
@ -39,17 +39,12 @@ test("gateway implementation modules do not depend on the public facade", () =>
|
|||
assert.deepEqual(reverseDependencies, []);
|
||||
});
|
||||
|
||||
test("core config compilation is separated from filesystem persistence", () => {
|
||||
test("core config compilation has no filesystem persistence", () => {
|
||||
const compiler = readFileSync(
|
||||
path.join(gatewayRoot, "core-runtime", "config-compiler.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const writer = readFileSync(
|
||||
path.join(gatewayRoot, "core-runtime", "config-writer.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(compiler, /node:fs|writeFileSync|mkdirSync/);
|
||||
assert.match(writer, /compileCoreGatewayConfig/);
|
||||
assert.match(writer, /writeFileSync/);
|
||||
assert.equal(existsSync(path.join(gatewayRoot, "core-runtime", "config-writer.ts")), false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ test("gateway treats downstream client aborts as expected stream cleanup", async
|
|||
}
|
||||
await waitForTcpListener(upstream);
|
||||
const upstreamPort = serverPort(upstream);
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(dir, "gateway.config.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "test-api-key";
|
||||
config.gateway.coreHost = "127.0.0.1";
|
||||
config.gateway.corePort = upstreamPort;
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ test("OpenAI video bindings expose the exact cross-protocol parameter subset", (
|
|||
|
||||
test("an imported Grok Agent supplies OAuth-backed Grok API media models without an API key", async (t) => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-grok-agent-media-"));
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.mediaTools.enabled = true;
|
||||
config.Providers = [normalizeGrokProviderMediaCapabilities({
|
||||
apiKey: "ccr-local-agent-login",
|
||||
|
|
@ -363,7 +363,7 @@ test("provider video jobs return immediately and finish through asynchronous pol
|
|||
});
|
||||
|
||||
function mediaConfig(baseUrlValue) {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.mediaTools = {
|
||||
...config.mediaTools,
|
||||
artifactTtlHours: 1,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { grokCandidate, importGrokProvider } from "@ccr/core/agents/local-providers/grok.ts";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
|
||||
import { writeCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-writer.ts";
|
||||
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler.ts";
|
||||
import { isCoreGatewayHealthy, spawnGatewayProcess } from "@ccr/core/gateway/core-runtime/supervisor.ts";
|
||||
import { coreGatewayAuthHeader } from "@ccr/core/gateway/internal/shared.ts";
|
||||
import { MediaService } from "@ccr/core/media/service.ts";
|
||||
|
|
@ -38,7 +38,7 @@ test("Fusion generates image and video through the local ai-gateway", { skip: !l
|
|||
|
||||
const configRoot = mkdtempSync(path.join(os.tmpdir(), "ccr-fusion-live-config-"));
|
||||
const artifactRoot = path.join(os.tmpdir(), `ccr-fusion-live-artifacts-${Date.now()}`);
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(configRoot, "gateway.config.json") });
|
||||
const config = createDefaultAppConfig({});
|
||||
const corePort = await availablePort();
|
||||
const coreEndpoint = `http://127.0.0.1:${corePort}`;
|
||||
const coreAuthToken = randomBytes(32).toString("base64url");
|
||||
|
|
@ -120,7 +120,7 @@ test("Fusion generates image and video through the local ai-gateway", { skip: !l
|
|||
authToken: coreAuthToken,
|
||||
baseUrl: coreEndpoint
|
||||
});
|
||||
await writeCoreGatewayConfig(
|
||||
const coreGatewayConfig = await compileCoreGatewayConfig(
|
||||
config,
|
||||
randomBytes(24).toString("base64url"),
|
||||
randomBytes(24).toString("base64url"),
|
||||
|
|
@ -128,12 +128,20 @@ test("Fusion generates image and video through the local ai-gateway", { skip: !l
|
|||
undefined,
|
||||
upstreamProxyUrl
|
||||
);
|
||||
enableGatewayDiagnostics(config.gateway.generatedConfigFile);
|
||||
enableGatewayDiagnostics(coreGatewayConfig);
|
||||
|
||||
const previousEntry = process.env.CCR_GATEWAY_ENTRY;
|
||||
process.env.CCR_GATEWAY_ENTRY = localGatewayEntry;
|
||||
try {
|
||||
child = spawnGatewayProcess(config, upstreamProxyUrl, randomUUID(), coreAuthToken);
|
||||
const spawnedGateway = spawnGatewayProcess(
|
||||
config,
|
||||
coreGatewayConfig,
|
||||
upstreamProxyUrl,
|
||||
randomUUID(),
|
||||
coreAuthToken
|
||||
);
|
||||
child = spawnedGateway.child;
|
||||
await spawnedGateway.configAccepted;
|
||||
} finally {
|
||||
restoreEnv("CCR_GATEWAY_ENTRY", previousEntry);
|
||||
}
|
||||
|
|
@ -212,8 +220,7 @@ function materializeProviderPlugins(templates, providerName, providerId, protoco
|
|||
return templates.map((template) => replacePlaceholders(template, replacements));
|
||||
}
|
||||
|
||||
function enableGatewayDiagnostics(file) {
|
||||
const generated = JSON.parse(readFileSync(file, "utf8"));
|
||||
function enableGatewayDiagnostics(generated) {
|
||||
generated.logging = { accessLog: false, enabled: true, level: "info" };
|
||||
const runtimeRoot = path.join(process.cwd(), ".test-dist", "core", "runtime");
|
||||
for (const plugin of generated.plugins ?? []) {
|
||||
|
|
@ -226,7 +233,6 @@ function enableGatewayDiagnostics(file) {
|
|||
server.args = [path.join(runtimeRoot, "media-tools-proxy-mcp.js")];
|
||||
}
|
||||
}
|
||||
writeFileSync(file, `${JSON.stringify(generated, null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
function replacePlaceholders(value, replacements) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ module.exports = {
|
|||
};
|
||||
`);
|
||||
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(dir, "gateway.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [{
|
||||
apiKey: "good-key",
|
||||
baseUrl: "https://good-provider.example/v1",
|
||||
|
|
@ -154,7 +154,7 @@ module.exports = {
|
|||
};
|
||||
`);
|
||||
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(dir, "gateway.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = false;
|
||||
config.plugins = [{
|
||||
id: "broken",
|
||||
|
|
@ -200,7 +200,7 @@ module.exports = {
|
|||
};
|
||||
`);
|
||||
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(dir, "gateway.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = false;
|
||||
config.plugins = [{
|
||||
id: "route-plugin",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,7 @@ test("profile service preserves user statusLine when the active global Claude ta
|
|||
version: 1
|
||||
}, null, 2)}\n`);
|
||||
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "ccr-profile-statusline-test";
|
||||
config.APIKEYS = [{
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
|
|
@ -189,9 +187,7 @@ test("profile service does not overwrite invalid global Claude settings JSON", {
|
|||
smallFastModel: "",
|
||||
surface: "auto"
|
||||
};
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "ccr-profile-invalid-settings-test";
|
||||
config.APIKEYS = [{
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
|
|
@ -237,9 +233,7 @@ test("profile service does not rewrite Claude settings when only user-managed fi
|
|||
smallFastModel: "",
|
||||
surface: "auto"
|
||||
};
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "ccr-profile-user-fields-test";
|
||||
config.APIKEYS = [{
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
|
|
@ -287,9 +281,7 @@ test("profile service can exclude ZCode from automatic synchronization", async (
|
|||
mkdirSync(path.dirname(configFile), { recursive: true });
|
||||
writeFileSync(configFile, original);
|
||||
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.profile.profiles = [
|
||||
{
|
||||
agent: "zcode",
|
||||
|
|
@ -339,9 +331,7 @@ test("profile service overwrites generated bin files without creating backups",
|
|||
writeFileSync(`${file}.ccr-original`, "old original\n");
|
||||
}
|
||||
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -445,9 +435,7 @@ test("Codex profile launcher bypasses middleware for Browser and Computer Use he
|
|||
].join("\n"));
|
||||
chmodSync(fakeCodex, 0o700);
|
||||
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "ccr-browser-helper-test";
|
||||
config.APIKEYS = [{
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
|
|
@ -521,9 +509,7 @@ test("Codex profile launcher bypasses middleware for Browser and Computer Use he
|
|||
|
||||
test("profile service injects ToolHub MCP into Codex config", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "codex-toolhub-test";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -657,9 +643,7 @@ test("profile service injects ToolHub MCP into Codex config", { skip: !process.e
|
|||
|
||||
test("profile service injects Context Archive MCP for managed Claude Code profile", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "managed-compact-claude";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -711,9 +695,7 @@ test("profile service injects Context Archive MCP for managed Claude Code profil
|
|||
|
||||
test("profile service injects Context Archive MCP for managed Codex profile", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "managed-compact-codex";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -775,9 +757,7 @@ test("profile service injects Context Archive MCP for managed Codex profile", {
|
|||
|
||||
test("profile service injects Context Archive MCP for managed Claude Code without ToolHub", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "context-archive-mcp-only";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -839,9 +819,7 @@ test("profile service writes a Grok CLI wrapper that points model discovery and
|
|||
mkdirSync(profileGrokHome, { recursive: true });
|
||||
symlinkSync(path.join(sourceGrokHome, "config.toml"), profileGrokConfig);
|
||||
}
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -928,9 +906,7 @@ test("profile service writes a multi-model Kimi CLI home that points inference t
|
|||
"enabled = false",
|
||||
""
|
||||
].join("\n"));
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -1056,9 +1032,7 @@ test("profile service writes a multi-model Kimi CLI home that points inference t
|
|||
|
||||
test("profile service writes a Pi config and wrapper that points inference to CCR", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "pi-gateway-test";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -1135,9 +1109,7 @@ test("profile service writes a Pi config and wrapper that points inference to CC
|
|||
|
||||
test("profile service writes an OpenCode CLI wrapper and shared CLI/App config", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "opencode-gateway-test";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -1199,9 +1171,7 @@ test("profile service writes an OpenCode CLI wrapper and shared CLI/App config",
|
|||
|
||||
test("profile service removes disabled and deleted OpenCode wrappers and API keys", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
const profileId = "opencode-cleanup-test";
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [
|
||||
{
|
||||
api_base_url: "https://example.test/v1",
|
||||
|
|
@ -1300,9 +1270,7 @@ test("profile service clears stale Claude Code ToolHub artifacts when no gateway
|
|||
}, null, 2)}\n`);
|
||||
writeFileSync(staleToolHubMcpConfigFile, "{}\n");
|
||||
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(CONFIGDIR, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [];
|
||||
config.virtualModelProfiles = [];
|
||||
config.profile.profiles = [
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ import {
|
|||
} from "@ccr/core/gateway/context-archive.ts";
|
||||
|
||||
function testConfig(overrides = {}) {
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: "/tmp/ccr-context-archive-test-gateway.json"
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
return {
|
||||
...config,
|
||||
APIKEY: "local-test-key",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "@ccr/core/agents/kilo/profile-config.ts";
|
||||
|
||||
function testConfig(root) {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(root, "gateway.config.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [{ api_base_url: "https://example.test/v1", api_key: "provider-key", models: ["model-a", "model-b"], name: "Provider" }];
|
||||
config.preferredProvider = "Provider";
|
||||
config.gateway.host = "127.0.0.1";
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ test("Core gateway config replaces imported Claude Code OAuth token with live ma
|
|||
access_token: "keychain-runtime-token",
|
||||
refresh_token: "keychain-refresh-token"
|
||||
}, async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(home, "config.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.providerPlugins = [
|
||||
{
|
||||
auth: {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "@ccr/core/agents/opencode/app-launch.ts";
|
||||
|
||||
function testConfig(root) {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(root, "gateway.config.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [{ api_base_url: "https://example.test/v1", api_key: "provider-key", models: ["model-a", "model-b"], name: "Provider" }];
|
||||
config.preferredProvider = "Provider";
|
||||
config.gateway.host = "127.0.0.1";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
|
|||
test("Pi profile config writes a CCR OpenAI Responses provider", () => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-pi-profile-"));
|
||||
try {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: path.join(root, "gateway.config.json") });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.host = "0.0.0.0";
|
||||
config.gateway.port = 3459;
|
||||
config.Providers = [
|
||||
|
|
|
|||
|
|
@ -314,9 +314,7 @@ async function startWithWarnings(config) {
|
|||
}
|
||||
|
||||
function baseConfig(dir) {
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(dir, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = false;
|
||||
config.plugins = [];
|
||||
return config;
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ test("gateway restart reports disabled reason to removed plugin stop hooks", { s
|
|||
});
|
||||
|
||||
function configWithPlugin(dir, pluginFile, enabled) {
|
||||
const config = createDefaultAppConfig({
|
||||
generatedConfigFile: path.join(dir, "gateway.config.json")
|
||||
});
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = false;
|
||||
config.plugins = [{
|
||||
enabled,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { prepareGatewayUpstreamAttemptForTest } from "@ccr/core/gateway/upstream
|
|||
|
||||
test("provider plugins use compiled runtime and capability identities", async () => {
|
||||
const unchangedPlugin = { key: "unscoped-plugin" };
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-provider-plugin-runtime-identity.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.providerPlugins = [
|
||||
{
|
||||
key: "single-protocol-plugin",
|
||||
|
|
@ -64,7 +64,7 @@ test("provider plugins use compiled runtime and capability identities", async ()
|
|||
});
|
||||
|
||||
test("Codex OAuth plugins retain the default base URL after runtime identity normalization", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-codex-oauth-runtime-identity.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.providerPlugins = [
|
||||
{
|
||||
codexOauth: {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("repeated legacy JSON cleanup failures keep one content-addressed backup", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`config-repository-backup-deduplication-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
const {
|
||||
ConfigRepository
|
||||
} = await import("@ccr/core/config/config-repository.ts");
|
||||
const {
|
||||
createBetterSqliteDatabase
|
||||
} = await import("@ccr/core/storage/sqlite-native.ts");
|
||||
|
||||
mkdirSync(path.dirname(LEGACY_ACTIVE_CONFIG_FILE), { recursive: true });
|
||||
const legacyContent = `${JSON.stringify({ PORT: 3456, Providers: [] }, null, 2)}\n`;
|
||||
writeFileSync(LEGACY_ACTIVE_CONFIG_FILE, legacyContent, "utf8");
|
||||
seedHistoricalDuplicateBackups(
|
||||
createBetterSqliteDatabase,
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
legacyContent
|
||||
);
|
||||
|
||||
let removalAttempts = 0;
|
||||
const failingRepository = new ConfigRepository(APP_CONFIG_DB_FILE, {
|
||||
removeFile() {
|
||||
removalAttempts += 1;
|
||||
throw new Error("simulated cleanup failure");
|
||||
}
|
||||
});
|
||||
await failingRepository.archiveLegacyJsonConfigFiles([LEGACY_ACTIVE_CONFIG_FILE]);
|
||||
await failingRepository.archiveLegacyJsonConfigFiles([LEGACY_ACTIVE_CONFIG_FILE]);
|
||||
|
||||
assert.equal(removalAttempts, 3);
|
||||
assert.equal(existsSync(LEGACY_ACTIVE_CONFIG_FILE), true);
|
||||
assert.equal(readBackupCount(
|
||||
createBetterSqliteDatabase,
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE
|
||||
), 1);
|
||||
assert.equal(readCleanupCount(createBetterSqliteDatabase, APP_CONFIG_DB_FILE), 1);
|
||||
|
||||
const retryRepository = new ConfigRepository(APP_CONFIG_DB_FILE);
|
||||
await retryRepository.readAppConfig();
|
||||
|
||||
assert.equal(existsSync(LEGACY_ACTIVE_CONFIG_FILE), false);
|
||||
assert.equal(readBackupCount(
|
||||
createBetterSqliteDatabase,
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE
|
||||
), 1);
|
||||
assert.equal(readCleanupCount(createBetterSqliteDatabase, APP_CONFIG_DB_FILE), 0);
|
||||
});
|
||||
|
||||
function seedHistoricalDuplicateBackups(createDatabase, dbFile, sourceFile, content) {
|
||||
mkdirSync(path.dirname(dbFile), { recursive: true });
|
||||
const database = createDatabase(dbFile);
|
||||
const sha256 = createHash("sha256").update(content).digest("hex");
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE config_schema_migrations (
|
||||
id TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL,
|
||||
details_json TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE legacy_storage_backups (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_path TEXT NOT NULL,
|
||||
source_kind TEXT NOT NULL,
|
||||
sha256 TEXT NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
archived_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
const insert = database.prepare(`
|
||||
INSERT INTO legacy_storage_backups (
|
||||
id, source_path, source_kind, sha256, content, archived_at
|
||||
) VALUES (?, ?, 'legacy-json-config', ?, ?, ?)
|
||||
`);
|
||||
insert.run("historical-backup-1", path.resolve(sourceFile), sha256, Buffer.from(content), new Date(0).toISOString());
|
||||
insert.run("historical-backup-2", path.resolve(sourceFile), sha256, Buffer.from(content), new Date(1).toISOString());
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
function readBackupCount(createDatabase, file, sourcePath) {
|
||||
return withReadonlyDatabase(createDatabase, file, (database) => database.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM legacy_storage_backups
|
||||
WHERE source_kind = 'legacy-json-config' AND source_path = ?
|
||||
`).get(path.resolve(sourcePath)).count);
|
||||
}
|
||||
|
||||
function readCleanupCount(createDatabase, file) {
|
||||
return withReadonlyDatabase(createDatabase, file, (database) => database.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM legacy_storage_cleanup
|
||||
WHERE source_kind = 'legacy-json-config'
|
||||
`).get().count);
|
||||
}
|
||||
|
||||
function withReadonlyDatabase(createDatabase, file, operation) {
|
||||
const database = createDatabase(file, {
|
||||
fileMustExist: true,
|
||||
readonly: true
|
||||
});
|
||||
try {
|
||||
return operation(database);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("legacy SQLite cleanup retries even when an old completion marker exists", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`config-repository-cleanup-retry-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_API_KEYS_DB_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
const {
|
||||
ConfigRepository
|
||||
} = await import("@ccr/core/config/config-repository.ts");
|
||||
const {
|
||||
createBetterSqliteDatabase
|
||||
} = await import("@ccr/core/storage/sqlite-native.ts");
|
||||
|
||||
createTargetDatabaseWithOldCompletionMarker(createBetterSqliteDatabase, APP_CONFIG_DB_FILE);
|
||||
createApiKeyDatabase(createBetterSqliteDatabase, LEGACY_API_KEYS_DB_FILE);
|
||||
const orphanWalFile = `${LEGACY_API_KEYS_DB_FILE}-wal`;
|
||||
writeFileSync(orphanWalFile, "legacy sidecar content\n", "utf8");
|
||||
|
||||
const failedRemovals = [];
|
||||
const firstRepository = new ConfigRepository(APP_CONFIG_DB_FILE, {
|
||||
removeFile(file) {
|
||||
failedRemovals.push(file);
|
||||
if (file === path.resolve(LEGACY_API_KEYS_DB_FILE)) {
|
||||
rmSync(file, { force: true });
|
||||
return;
|
||||
}
|
||||
throw new Error("simulated cleanup failure");
|
||||
}
|
||||
});
|
||||
const firstRead = await firstRepository.listApiKeys();
|
||||
|
||||
assert.deepEqual(firstRead.map((apiKey) => apiKey.id), ["legacy-key"]);
|
||||
assert.equal(existsSync(LEGACY_API_KEYS_DB_FILE), false);
|
||||
assert.equal(failedRemovals.includes(path.resolve(orphanWalFile)), true);
|
||||
assert.equal(existsSync(orphanWalFile), true);
|
||||
assert.equal(readMigration(createBetterSqliteDatabase, APP_CONFIG_DB_FILE), undefined);
|
||||
assert.equal(readBackupCount(
|
||||
createBetterSqliteDatabase,
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_API_KEYS_DB_FILE
|
||||
), 1);
|
||||
|
||||
const retryRepository = new ConfigRepository(APP_CONFIG_DB_FILE);
|
||||
const retryRead = await retryRepository.listApiKeys();
|
||||
|
||||
assert.deepEqual(retryRead.map((apiKey) => apiKey.id), ["legacy-key"]);
|
||||
assert.equal(existsSync(LEGACY_API_KEYS_DB_FILE), false);
|
||||
assert.equal(existsSync(orphanWalFile), false);
|
||||
assert.equal(readMigration(createBetterSqliteDatabase, APP_CONFIG_DB_FILE), "unified-config-store-v1");
|
||||
assert.equal(readBackupCount(
|
||||
createBetterSqliteDatabase,
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_API_KEYS_DB_FILE
|
||||
), 1);
|
||||
assert.equal(readCleanupCount(createBetterSqliteDatabase, APP_CONFIG_DB_FILE), 0);
|
||||
});
|
||||
|
||||
function createTargetDatabaseWithOldCompletionMarker(createDatabase, file) {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const database = createDatabase(file);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE config_schema_migrations (
|
||||
id TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL,
|
||||
details_json TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
`);
|
||||
database.prepare(`
|
||||
INSERT INTO config_schema_migrations (id, applied_at, details_json)
|
||||
VALUES ('unified-config-store-v1', ?, '')
|
||||
`).run(new Date(0).toISOString());
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
function createApiKeyDatabase(createDatabase, file) {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const database = createDatabase(file);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
encrypted_key TEXT NOT NULL,
|
||||
encryption TEXT NOT NULL DEFAULT 'plain',
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL DEFAULT '',
|
||||
limits_json TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
`);
|
||||
database.prepare(`
|
||||
INSERT INTO api_keys (
|
||||
id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
) VALUES ('legacy-key', '', 'sk-legacy', 'plain', ?, '', '')
|
||||
`).run(new Date(0).toISOString());
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
function readMigration(createDatabase, file) {
|
||||
return withReadonlyDatabase(createDatabase, file, (database) => database.prepare(`
|
||||
SELECT id
|
||||
FROM config_schema_migrations
|
||||
WHERE id = 'unified-config-store-v1'
|
||||
`).get()?.id);
|
||||
}
|
||||
|
||||
function readBackupCount(createDatabase, file, sourcePath) {
|
||||
return withReadonlyDatabase(createDatabase, file, (database) => database.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM legacy_storage_backups
|
||||
WHERE source_kind = 'legacy-sqlite-config' AND source_path = ?
|
||||
`).get(path.resolve(sourcePath)).count);
|
||||
}
|
||||
|
||||
function readCleanupCount(createDatabase, file) {
|
||||
return withReadonlyDatabase(createDatabase, file, (database) => database.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM legacy_storage_cleanup
|
||||
WHERE source_kind = 'legacy-sqlite-config'
|
||||
`).get().count);
|
||||
}
|
||||
|
||||
function withReadonlyDatabase(createDatabase, file, operation) {
|
||||
const database = createDatabase(file, {
|
||||
fileMustExist: true,
|
||||
readonly: true
|
||||
});
|
||||
try {
|
||||
return operation(database);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("legacy API keys are merged without replacing existing target keys", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`config-repository-existing-data-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_API_KEYS_DB_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
const {
|
||||
createBetterSqliteDatabase
|
||||
} = await import("@ccr/core/storage/sqlite-native.ts");
|
||||
|
||||
createApiKeyDatabase(createBetterSqliteDatabase, APP_CONFIG_DB_FILE, {
|
||||
id: "existing-key",
|
||||
key: "sk-existing"
|
||||
});
|
||||
createApiKeyDatabase(createBetterSqliteDatabase, LEGACY_API_KEYS_DB_FILE, {
|
||||
id: "legacy-key",
|
||||
key: "sk-legacy"
|
||||
});
|
||||
|
||||
const {
|
||||
loadPersistedApiKeys
|
||||
} = await import("@ccr/core/config/config-repository.ts");
|
||||
const apiKeys = await loadPersistedApiKeys();
|
||||
assert.deepEqual(apiKeys.map((apiKey) => apiKey.id), [
|
||||
"existing-key",
|
||||
"legacy-key"
|
||||
]);
|
||||
assert.equal(existsSync(LEGACY_API_KEYS_DB_FILE), false);
|
||||
|
||||
const database = createBetterSqliteDatabase(APP_CONFIG_DB_FILE, {
|
||||
fileMustExist: true,
|
||||
readonly: true
|
||||
});
|
||||
try {
|
||||
const migration = database.prepare(`
|
||||
SELECT id
|
||||
FROM config_schema_migrations
|
||||
WHERE id = 'unified-config-store-v1'
|
||||
`).get();
|
||||
assert.equal(migration?.id, "unified-config-store-v1");
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
function createApiKeyDatabase(createDatabase, file, apiKey) {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const database = createDatabase(file);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
encrypted_key TEXT NOT NULL,
|
||||
encryption TEXT NOT NULL DEFAULT 'plain',
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL DEFAULT '',
|
||||
limits_json TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
`);
|
||||
database.prepare(`
|
||||
INSERT INTO api_keys (
|
||||
id, name, encrypted_key, encryption, created_at, expires_at, limits_json
|
||||
) VALUES (?, '', ?, 'plain', ?, '', '')
|
||||
`).run(apiKey.id, apiKey.key, new Date(0).toISOString());
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("unreadable legacy SQLite sources are preserved for a future migration retry", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`config-repository-migration-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_API_KEYS_DB_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
mkdirSync(path.dirname(LEGACY_API_KEYS_DB_FILE), { recursive: true });
|
||||
writeFileSync(LEGACY_API_KEYS_DB_FILE, "not a SQLite database");
|
||||
|
||||
const {
|
||||
loadPersistedApiKeys
|
||||
} = await import("@ccr/core/config/config-repository.ts");
|
||||
assert.deepEqual(await loadPersistedApiKeys(), []);
|
||||
assert.equal(existsSync(LEGACY_API_KEYS_DB_FILE), true);
|
||||
|
||||
const {
|
||||
createBetterSqliteDatabase
|
||||
} = await import("@ccr/core/storage/sqlite-native.ts");
|
||||
const database = createBetterSqliteDatabase(APP_CONFIG_DB_FILE, {
|
||||
fileMustExist: true,
|
||||
readonly: true
|
||||
});
|
||||
try {
|
||||
const migration = database.prepare(`
|
||||
SELECT id
|
||||
FROM config_schema_migrations
|
||||
WHERE id = 'unified-config-store-v1'
|
||||
`).get();
|
||||
const backup = database.prepare(`
|
||||
SELECT source_path
|
||||
FROM legacy_storage_backups
|
||||
WHERE source_path = ?
|
||||
`).get(path.resolve(LEGACY_API_KEYS_DB_FILE));
|
||||
assert.equal(migration, undefined);
|
||||
assert.equal(backup, undefined);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("malformed legacy JSON is preserved when default config is persisted", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`legacy-json-preservation-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
mkdirSync(path.dirname(LEGACY_ACTIVE_CONFIG_FILE), { recursive: true });
|
||||
const malformedConfig = "{\n \"PORT\": 9999,\n";
|
||||
writeFileSync(LEGACY_ACTIVE_CONFIG_FILE, malformedConfig, "utf8");
|
||||
|
||||
const { loadAppConfig } = await import("@ccr/core/config/config.ts");
|
||||
const config = await loadAppConfig();
|
||||
|
||||
assert.equal(config.PORT, 3456);
|
||||
assert.equal(existsSync(APP_CONFIG_DB_FILE), true);
|
||||
assert.equal(existsSync(LEGACY_ACTIVE_CONFIG_FILE), true);
|
||||
assert.equal(readFileSync(LEGACY_ACTIVE_CONFIG_FILE, "utf8"), malformedConfig);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("only the successfully parsed legacy JSON source is archived", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`legacy-json-selected-source-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
mkdirSync(path.dirname(LEGACY_ACTIVE_CONFIG_FILE), { recursive: true });
|
||||
mkdirSync(path.dirname(LEGACY_WINDOWS_CONFIG_FILE), { recursive: true });
|
||||
|
||||
const malformedConfig = "{\n \"PORT\": 9999,\n";
|
||||
writeFileSync(LEGACY_ACTIVE_CONFIG_FILE, malformedConfig, "utf8");
|
||||
writeFileSync(LEGACY_WINDOWS_CONFIG_FILE, JSON.stringify({ PORT: 4567 }), "utf8");
|
||||
|
||||
const { loadAppConfig } = await import("@ccr/core/config/config.ts");
|
||||
const config = await loadAppConfig();
|
||||
|
||||
assert.equal(config.PORT, 4567);
|
||||
assert.equal(existsSync(LEGACY_ACTIVE_CONFIG_FILE), true);
|
||||
assert.equal(readFileSync(LEGACY_ACTIVE_CONFIG_FILE, "utf8"), malformedConfig);
|
||||
assert.equal(existsSync(LEGACY_WINDOWS_CONFIG_FILE), false);
|
||||
});
|
||||
28
packages/core/test/unit/config/onboarding-state.test.mjs
Normal file
28
packages/core/test/unit/config/onboarding-state.test.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("onboarding state falls back to the legacy marker when the config database cannot be read", async () => {
|
||||
const testRoot = path.join(
|
||||
process.env.CCR_INTERNAL_HOME_DIR,
|
||||
`onboarding-state-fallback-${process.pid}`
|
||||
);
|
||||
process.env.CCR_INTERNAL_HOME_DIR = path.join(testRoot, "home");
|
||||
process.env.CCR_INTERNAL_APP_DATA_DIR = path.join(testRoot, "app-data");
|
||||
process.env.CCR_INTERNAL_USER_DATA_DIR = path.join(testRoot, "user-data");
|
||||
|
||||
const {
|
||||
APP_CONFIG_DB_FILE,
|
||||
ONBOARDING_FINISHED_FILE
|
||||
} = await import("@ccr/core/config/constants.ts");
|
||||
mkdirSync(path.dirname(APP_CONFIG_DB_FILE), { recursive: true });
|
||||
writeFileSync(APP_CONFIG_DB_FILE, "not a SQLite database", "utf8");
|
||||
writeFileSync(ONBOARDING_FINISHED_FILE, "", "utf8");
|
||||
|
||||
const { loadOnboardingFinished } = await import("@ccr/core/config/onboarding-state.ts");
|
||||
assert.equal(await loadOnboardingFinished(), true);
|
||||
|
||||
rmSync(ONBOARDING_FINISHED_FILE);
|
||||
assert.equal(await loadOnboardingFinished(), false);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/config/app-config-store.ts";
|
||||
import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/config/config-repository.ts";
|
||||
import { loadAppConfig, saveAppConfig, saveAppThemePreference } from "@ccr/core/config/config.ts";
|
||||
|
||||
test("theme preference persistence changes only the theme field", async () => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-
|
|||
import { rawTraceSyncHeader, rawTraceSyncPath } from "@ccr/core/gateway/internal/shared.ts";
|
||||
|
||||
test("core gateway disables the full-trace billing webhook without disabling raw-trace observability", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-generated-config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.host = "0.0.0.0";
|
||||
config.gateway.port = 4567;
|
||||
config.observability.requestLogs = true;
|
||||
|
|
@ -44,7 +44,7 @@ test("core gateway disables the full-trace billing webhook without disabling raw
|
|||
});
|
||||
|
||||
test("Codex OAuth providers remove unsupported Responses request fields", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-generated-config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.providerPlugins = [{
|
||||
codexOauth: {},
|
||||
enabled: true,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-
|
|||
import { shouldRunGatewayRuntime } from "@ccr/core/gateway/core-runtime/supervisor.ts";
|
||||
|
||||
test("media tools start their internal gateway runtime when the public gateway is disabled", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = false;
|
||||
config.proxy.enabled = false;
|
||||
config.mediaTools.enabled = true;
|
||||
|
|
@ -14,7 +14,7 @@ test("media tools start their internal gateway runtime when the public gateway i
|
|||
});
|
||||
|
||||
test("core gateway compiles media capabilities and provider plugin aliases for credentials", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.Providers = [{
|
||||
api_base_url: "https://chat.example/v1",
|
||||
capabilities: [
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { mediaToolsConfigFromRawForTest, virtualModelProfileFromRawForTest } fro
|
|||
import { shouldRestartGatewayForRuntimeConfigChange } from "@ccr/core/gateway/runtime-change.ts";
|
||||
|
||||
test("ToolHub config changes restart the gateway runtime", () => {
|
||||
const previous = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const next = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const previous = createDefaultAppConfig();
|
||||
const next = createDefaultAppConfig();
|
||||
next.toolHub = {
|
||||
...next.toolHub,
|
||||
enabled: true,
|
||||
|
|
@ -27,8 +27,8 @@ test("ToolHub config changes restart the gateway runtime", () => {
|
|||
});
|
||||
|
||||
test("media tool policy changes restart the gateway runtime", () => {
|
||||
const previous = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const next = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const previous = createDefaultAppConfig();
|
||||
const next = createDefaultAppConfig();
|
||||
next.mediaTools.enabled = true;
|
||||
|
||||
assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true);
|
||||
|
|
@ -78,8 +78,8 @@ test("legacy virtual model tool loop limits are removed from application config"
|
|||
});
|
||||
|
||||
test("upstream proxy config changes restart the gateway runtime", () => {
|
||||
const previous = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const next = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const previous = createDefaultAppConfig();
|
||||
const next = createDefaultAppConfig();
|
||||
next.proxy.upstream = {
|
||||
custom: {
|
||||
password: "secret",
|
||||
|
|
@ -102,16 +102,16 @@ test("raw trace observability config changes restart the gateway runtime", () =>
|
|||
];
|
||||
|
||||
for (const mutate of mutations) {
|
||||
const previous = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const next = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const previous = createDefaultAppConfig();
|
||||
const next = createDefaultAppConfig();
|
||||
mutate(next);
|
||||
assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("main-process-only observability changes do not restart the gateway runtime", () => {
|
||||
const previous = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const next = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const previous = createDefaultAppConfig();
|
||||
const next = createDefaultAppConfig();
|
||||
next.observability.requestLogSuccessSampleRate = 0.25;
|
||||
|
||||
assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), false);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
configRepository,
|
||||
deletePersistedRuntimeState,
|
||||
loadPersistedRuntimeState,
|
||||
replacePersistedRuntimeState
|
||||
} from "@ccr/core/config/config-repository.ts";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
|
||||
import { gatewayService } from "@ccr/core/gateway/service.ts";
|
||||
|
||||
test("gateway start persists preflight validation failures for status polling", async () => {
|
||||
await gatewayService.stop();
|
||||
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.coreHost = "0.0.0.0";
|
||||
|
||||
const startStatus = await gatewayService.start(config);
|
||||
|
|
@ -18,3 +28,262 @@ test("gateway start persists preflight validation failures for status polling",
|
|||
|
||||
await gatewayService.stop();
|
||||
});
|
||||
|
||||
test("gateway stop preserves a runtime marker that this service instance does not own", async () => {
|
||||
await gatewayService.stop();
|
||||
const marker = {
|
||||
pid: 12345,
|
||||
runtimeId: "previous-runtime",
|
||||
startedAt: new Date(0).toISOString()
|
||||
};
|
||||
await replacePersistedRuntimeState("gateway", marker);
|
||||
|
||||
try {
|
||||
await gatewayService.stop();
|
||||
assert.deepEqual(await loadPersistedRuntimeState("gateway"), marker);
|
||||
} finally {
|
||||
await deletePersistedRuntimeState("gateway");
|
||||
}
|
||||
});
|
||||
|
||||
test("gateway start observes an exit that occurs while the runtime marker is being written", async () => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-gateway-early-exit-test-"));
|
||||
const gatewayEntry = path.join(dir, "gateway-entry.cjs");
|
||||
const gatewayEntryLoaded = path.join(dir, "gateway-entry-loaded");
|
||||
const previousGatewayEntry = process.env.CCR_GATEWAY_ENTRY;
|
||||
const previousGatewayEntryLoaded = process.env.CCR_GATEWAY_EARLY_EXIT_SENTINEL;
|
||||
const replaceRuntimeState = configRepository.replaceRuntimeState;
|
||||
writeFileSync(
|
||||
gatewayEntry,
|
||||
[
|
||||
'const { writeFileSync } = require("node:fs");',
|
||||
'writeFileSync(process.env.CCR_GATEWAY_EARLY_EXIT_SENTINEL, "loaded\\n");',
|
||||
"process.exit(23);",
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
try {
|
||||
await gatewayService.stop();
|
||||
process.env.CCR_GATEWAY_ENTRY = gatewayEntry;
|
||||
process.env.CCR_GATEWAY_EARLY_EXIT_SENTINEL = gatewayEntryLoaded;
|
||||
configRepository.replaceRuntimeState = async function (...args) {
|
||||
await waitForFile(gatewayEntryLoaded);
|
||||
await delay(100);
|
||||
return replaceRuntimeState.apply(this, args);
|
||||
};
|
||||
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = true;
|
||||
config.gateway.corePort = 0;
|
||||
config.gateway.port = 0;
|
||||
config.Providers = [{
|
||||
api_base_url: "http://127.0.0.1:9/v1",
|
||||
api_key: "test-provider-key",
|
||||
id: "early-exit-provider",
|
||||
models: ["early-exit-model"],
|
||||
name: "Early Exit Provider",
|
||||
type: "openai_chat_completions"
|
||||
}];
|
||||
|
||||
const status = await gatewayService.start(config);
|
||||
|
||||
assert.equal(status.state, "error");
|
||||
assert.match(status.lastError ?? "", /Core gateway exited with 23/);
|
||||
assert.equal(await loadPersistedRuntimeState("gateway"), undefined);
|
||||
} finally {
|
||||
configRepository.replaceRuntimeState = replaceRuntimeState;
|
||||
if (previousGatewayEntry === undefined) {
|
||||
delete process.env.CCR_GATEWAY_ENTRY;
|
||||
} else {
|
||||
process.env.CCR_GATEWAY_ENTRY = previousGatewayEntry;
|
||||
}
|
||||
if (previousGatewayEntryLoaded === undefined) {
|
||||
delete process.env.CCR_GATEWAY_EARLY_EXIT_SENTINEL;
|
||||
} else {
|
||||
process.env.CCR_GATEWAY_EARLY_EXIT_SENTINEL = previousGatewayEntryLoaded;
|
||||
}
|
||||
await gatewayService.stop();
|
||||
await deletePersistedRuntimeState("gateway");
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("gateway start waits for matching runtime health before reporting running", async () => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-gateway-readiness-test-"));
|
||||
const gatewayEntry = path.join(dir, "gateway-entry.cjs");
|
||||
const readySentinel = path.join(dir, "gateway-ready");
|
||||
const previousGatewayEntry = process.env.CCR_GATEWAY_ENTRY;
|
||||
const previousReadyDelay = process.env.CCR_GATEWAY_READY_DELAY_MS;
|
||||
const previousReadySentinel = process.env.CCR_GATEWAY_READY_SENTINEL;
|
||||
writeFileSync(gatewayEntry, delayedHealthyGatewayEntry(), "utf8");
|
||||
|
||||
try {
|
||||
await gatewayService.stop();
|
||||
process.env.CCR_GATEWAY_ENTRY = gatewayEntry;
|
||||
process.env.CCR_GATEWAY_READY_DELAY_MS = "250";
|
||||
process.env.CCR_GATEWAY_READY_SENTINEL = readySentinel;
|
||||
|
||||
const config = gatewayTestConfig(await findAvailablePort());
|
||||
const startedAt = Date.now();
|
||||
const status = await gatewayService.start(config);
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
|
||||
assert.equal(status.state, "running");
|
||||
assert.equal(existsSync(readySentinel), true);
|
||||
assert.ok(elapsedMs >= 200, `gateway start returned before delayed health was ready (${elapsedMs}ms)`);
|
||||
assert.match(String((await loadPersistedRuntimeState("gateway"))?.runtimeId), /.+/);
|
||||
} finally {
|
||||
restoreEnv("CCR_GATEWAY_ENTRY", previousGatewayEntry);
|
||||
restoreEnv("CCR_GATEWAY_READY_DELAY_MS", previousReadyDelay);
|
||||
restoreEnv("CCR_GATEWAY_READY_SENTINEL", previousReadySentinel);
|
||||
await gatewayService.stop();
|
||||
await deletePersistedRuntimeState("gateway");
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("gateway start completes the IPC and health handshake with the bundled runtime", async () => {
|
||||
const previousGatewayEntry = process.env.CCR_GATEWAY_ENTRY;
|
||||
try {
|
||||
await gatewayService.stop();
|
||||
delete process.env.CCR_GATEWAY_ENTRY;
|
||||
const status = await gatewayService.start(gatewayTestConfig(await findAvailablePort()));
|
||||
assert.equal(status.state, "running", status.lastError);
|
||||
|
||||
const marker = await loadPersistedRuntimeState("gateway");
|
||||
const healthResponse = await fetch(new URL("/health", status.coreEndpoint));
|
||||
const health = await healthResponse.json();
|
||||
|
||||
assert.equal(healthResponse.status, 200);
|
||||
assert.equal(health.status, "ok");
|
||||
assert.equal(health.runtimeId, marker?.runtimeId);
|
||||
} finally {
|
||||
restoreEnv("CCR_GATEWAY_ENTRY", previousGatewayEntry);
|
||||
await gatewayService.stop();
|
||||
await deletePersistedRuntimeState("gateway");
|
||||
}
|
||||
});
|
||||
|
||||
test("gateway start rejects healthy endpoints owned by a different runtime", async () => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-gateway-runtime-identity-test-"));
|
||||
const gatewayEntry = path.join(dir, "gateway-entry.cjs");
|
||||
const previousGatewayEntry = process.env.CCR_GATEWAY_ENTRY;
|
||||
writeFileSync(gatewayEntry, foreignRuntimeGatewayEntry(), "utf8");
|
||||
|
||||
try {
|
||||
await gatewayService.stop();
|
||||
process.env.CCR_GATEWAY_ENTRY = gatewayEntry;
|
||||
|
||||
const status = await gatewayService.start(gatewayTestConfig(await findAvailablePort()));
|
||||
|
||||
assert.equal(status.state, "error");
|
||||
assert.match(status.lastError ?? "", /owned by a different runtime/);
|
||||
assert.equal(await loadPersistedRuntimeState("gateway"), undefined);
|
||||
} finally {
|
||||
restoreEnv("CCR_GATEWAY_ENTRY", previousGatewayEntry);
|
||||
await gatewayService.stop();
|
||||
await deletePersistedRuntimeState("gateway");
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function gatewayTestConfig(corePort) {
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.enabled = true;
|
||||
config.gateway.corePort = corePort;
|
||||
config.gateway.port = 0;
|
||||
config.Providers = [{
|
||||
api_base_url: "http://127.0.0.1:9/v1",
|
||||
api_key: "test-provider-key",
|
||||
id: "readiness-provider",
|
||||
models: ["readiness-model"],
|
||||
name: "Readiness Provider",
|
||||
type: "openai_chat_completions"
|
||||
}];
|
||||
return config;
|
||||
}
|
||||
|
||||
function delayedHealthyGatewayEntry() {
|
||||
return [
|
||||
'const { writeFileSync } = require("node:fs");',
|
||||
'const { createServer } = require("node:http");',
|
||||
"const delayMs = Number(process.env.CCR_GATEWAY_READY_DELAY_MS || 0);",
|
||||
"setTimeout(() => {",
|
||||
" const server = createServer((request, response) => {",
|
||||
" if (request.url === '/health') {",
|
||||
" response.writeHead(200, { 'content-type': 'application/json' });",
|
||||
" response.end(JSON.stringify({",
|
||||
" runtimeId: process.env.CCR_GATEWAY_RUNTIME_ID,",
|
||||
" status: 'ok'",
|
||||
" }));",
|
||||
" return;",
|
||||
" }",
|
||||
" response.writeHead(404);",
|
||||
" response.end();",
|
||||
" });",
|
||||
" server.listen(Number(process.env.PORT), process.env.HOST, () => {",
|
||||
" writeFileSync(process.env.CCR_GATEWAY_READY_SENTINEL, 'ready\\n');",
|
||||
" });",
|
||||
"}, delayMs);",
|
||||
""
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function foreignRuntimeGatewayEntry() {
|
||||
return [
|
||||
'const { createServer } = require("node:http");',
|
||||
"createServer((request, response) => {",
|
||||
" if (request.url === '/health') {",
|
||||
" response.writeHead(200, { 'content-type': 'application/json' });",
|
||||
" response.end(JSON.stringify({ runtimeId: 'foreign-runtime', status: 'ok' }));",
|
||||
" return;",
|
||||
" }",
|
||||
" response.writeHead(404);",
|
||||
" response.end();",
|
||||
"}).listen(Number(process.env.PORT), process.env.HOST);",
|
||||
""
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function findAvailablePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address ? address.port : undefined;
|
||||
server.close(() => {
|
||||
if (port) {
|
||||
resolve(port);
|
||||
} else {
|
||||
reject(new Error("Failed to allocate a gateway test port."));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restoreEnv(key, value) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForFile(file, timeoutMs = 5000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (existsSync(file)) {
|
||||
return;
|
||||
}
|
||||
await delay(10);
|
||||
}
|
||||
assert.fail(`Timed out waiting for gateway entry sentinel: ${file}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ test("ToolHub runtime candidates include the clean Core test build", () => {
|
|||
});
|
||||
|
||||
test("Media tools are a Fusion MCP backend independent from ToolHub", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.host = "0.0.0.0";
|
||||
config.mediaTools.enabled = true;
|
||||
config.mediaTools.jobTimeoutMs = 600000;
|
||||
|
|
@ -69,7 +69,7 @@ test("Media tools are a Fusion MCP backend independent from ToolHub", () => {
|
|||
});
|
||||
|
||||
test("ToolHub does not absorb Fusion media tools", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.toolHub.enabled = true;
|
||||
config.mediaTools.enabled = true;
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ test("ToolHub does not absorb Fusion media tools", () => {
|
|||
});
|
||||
|
||||
test("Core Gateway registers media tools directly for Fusion models", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.mediaTools.enabled = true;
|
||||
config.toolHub.enabled = true;
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ test("Core Gateway registers media tools directly for Fusion models", async () =
|
|||
});
|
||||
|
||||
test("Core Gateway compiles one profile for each configured Fusion media model", async () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.mediaTools.enabled = true;
|
||||
config.Providers = [
|
||||
{ models: ["base-model"], name: "Provider" },
|
||||
|
|
@ -127,7 +127,7 @@ test("Core Gateway compiles one profile for each configured Fusion media model",
|
|||
});
|
||||
|
||||
test("ToolHub runtime includes the built-in browser automation backend", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.toolHub = {
|
||||
...config.toolHub,
|
||||
browserAutomation: true,
|
||||
|
|
@ -154,7 +154,7 @@ test("ToolHub runtime includes the built-in browser automation backend", () => {
|
|||
});
|
||||
|
||||
test("ToolHub browser automation backend uses a connectable loopback host", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.gateway.host = "0.0.0.0";
|
||||
config.toolHub = {
|
||||
...config.toolHub,
|
||||
|
|
@ -177,7 +177,7 @@ test("ToolHub browser automation backend uses a connectable loopback host", () =
|
|||
});
|
||||
|
||||
test("ToolHub runtime skips built-in browser automation until enabled", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.toolHub = {
|
||||
...config.toolHub,
|
||||
browserAutomation: false,
|
||||
|
|
|
|||
|
|
@ -875,7 +875,7 @@ test("fallback raw bundles keep unique bundle ids while sharing the logical requ
|
|||
});
|
||||
|
||||
function createConfig() {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-generated-config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.observability.requestLogs = true;
|
||||
config.observability.requestLogBodyCapture = "all";
|
||||
config.observability.requestLogSuccessSampleRate = 1;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function claudeProfileConfig() {
|
|||
scope: "ccr",
|
||||
surface: "cli"
|
||||
};
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.APIKEY = "profile-token";
|
||||
config.APIKEYS = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from "@ccr/core/proxy/system-proxy.ts";
|
||||
|
||||
test("custom upstream proxy config creates authenticated proxy URLs", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.proxy.upstream = {
|
||||
custom: {
|
||||
password: "pa:ss",
|
||||
|
|
@ -33,7 +33,7 @@ test("custom upstream proxy config creates authenticated proxy URLs", () => {
|
|||
});
|
||||
|
||||
test("none and incomplete custom upstream proxy configs do not create proxy servers", () => {
|
||||
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
|
||||
const config = createDefaultAppConfig();
|
||||
config.proxy.upstream = {
|
||||
...config.proxy.upstream,
|
||||
mode: "none"
|
||||
|
|
|
|||
|
|
@ -1022,10 +1022,6 @@ function normalizeGatewayBrowserAppHost(host: string): string {
|
|||
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||
}
|
||||
|
||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function sanitizeBrowserAppId(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
|
|
@ -1034,10 +1030,6 @@ function sanitizeBrowserAppId(value: string): string {
|
|||
.slice(0, 80);
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function isBrowserHomeUrl(value: string): boolean {
|
||||
return value.trim().toLowerCase() === browserHomeUrl;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { gunzipSync } from "node:zlib";
|
||||
import type { Event as ElectronEvent, Session, WebContents } from "electron";
|
||||
import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { loadPersistedApiKeys } from "@ccr/core/config/config-repository";
|
||||
import {
|
||||
CLAUDE_DESIGN_PLUGIN_ID,
|
||||
CLAUDE_SHIP_PLUGIN_ID,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { deflateSync, inflateSync } from "node:zlib";
|
||||
import { loadPersistedAppSetting, replacePersistedAppSetting } from "@ccr/core/config/app-config-store";
|
||||
import { loadOnboardingFinished, markOnboardingFinished } from "@ccr/core/config/onboarding-state";
|
||||
import { builtInBrowserService } from "./built-in-browser";
|
||||
import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/core/agents/bot-gateway/handoff-scan-service";
|
||||
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
|
||||
|
|
@ -12,7 +12,21 @@ import { syncClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-
|
|||
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
|
||||
import { findInstalledOpenCodeAppExecutable } from "@ccr/core/agents/opencode/app-launch";
|
||||
import { loadAppConfig, saveApiKeysConfig, saveAppConfig, saveAppThemePreference, withClaudeDesignRuntimePluginConfig } from "@ccr/core/config/config";
|
||||
import { API_KEYS_DB_FILE, APP_CONFIG_DB_FILE, APP_NAME, CONFIGDIR, CONFIG_FILE, DATADIR, GATEWAY_CONFIG_FILE, IPC_CHANNELS, LEGACY_CONFIG_FILE, ONBOARDING_FINISHED_FILE, PROXY_CA_CERT_FILE, REQUEST_LOGS_DB_FILE, USAGE_DB_FILE } from "@ccr/core/config/constants";
|
||||
import {
|
||||
APP_CONFIG_DB_FILE,
|
||||
APP_NAME,
|
||||
CONFIGDIR,
|
||||
DATADIR,
|
||||
IPC_CHANNELS,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_APP_CONFIG_DB_FILES,
|
||||
LEGACY_API_KEYS_DB_FILES,
|
||||
LEGACY_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
PROXY_CA_CERT_FILE,
|
||||
REQUEST_LOGS_DB_FILE,
|
||||
USAGE_DB_FILE
|
||||
} from "@ccr/core/config/constants";
|
||||
import { deepLinkService } from "./deep-link";
|
||||
import { gatewayService } from "@ccr/core/gateway/service";
|
||||
import { shouldRestartGatewayForRuntimeConfigChange } from "@ccr/core/gateway/runtime-change";
|
||||
|
|
@ -38,7 +52,6 @@ import { getUsageStats } from "@ccr/core/usage/store";
|
|||
import { applyNativeThemePreference } from "./native-theme";
|
||||
import windowsManager from "./windows";
|
||||
import { CLAUDE_DESIGN_PLUGIN_ID, GATEWAY_PLUGIN_PERMISSION_IDS, GATEWAY_PLUGIN_SURFACE_IDS, type AgentAnalysisFilter, type AgentAnalysisTracePayloadRequest, type ApiKeyConfig, type AppCaptureElementPngRequest, type AppCaptureElementPngResult, type AppConfig, type AppDataExportResult, type AppImageExportTargetRequest, type AppImageExportTargetResult, type AppInfo, type AppRenderHtmlPngRequest, type AppRenderHtmlPngResult, type AppSaveConfigOptions, type BotGatewayQrLoginCancelRequest, type BotGatewayQrLoginStartRequest, type BotGatewayQrLoginWaitRequest, type BotGatewayQrWindowCloseRequest, type BotGatewayQrWindowOpenRequest, type GatewayPluginAppConfig, type GatewayPluginPermission, type GatewayPluginSurface, type GatewayProviderConnectivityCheckRequest, type GatewayProviderProbeCandidatesRequest, type GatewayProviderProbeRequest, type GatewayStatus, type LocalAgentProviderImportRequest, type PluginDependency, type PluginDirectorySelection, type ProfileApplyResult, type ProfileOpenRequest, type ProfileOpenResult, type ProviderAccountResetRequest, type ProviderAccountSnapshotRequestOptions, type ProviderAccountTestRequest, type ProviderCatalogModelsRequest, type ProviderIconDetectionRequest, type ProviderManifestFetchRequest, type RequestLogListFilter, type RouteScriptTestRequest, type RouteScriptValidationRequest, type UsageStatsFilter, type UsageStatsRange } from "@ccr/core/contracts/app";
|
||||
const onboardingFinishedAtSettingKey = "onboardingFinishedAt";
|
||||
const imageExportTargets = new Map<string, string>();
|
||||
const gatewayPluginPermissionIdSet = new Set<string>(GATEWAY_PLUGIN_PERMISSION_IDS);
|
||||
const gatewayPluginSurfaceIdSet = new Set<string>(GATEWAY_PLUGIN_SURFACE_IDS);
|
||||
|
|
@ -52,14 +65,11 @@ ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
|
|||
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
|
||||
const opencodeAppPath = findInstalledOpenCodeAppExecutable().executable;
|
||||
return {
|
||||
appConfigDbFile: APP_CONFIG_DB_FILE,
|
||||
apiKeysDbFile: API_KEYS_DB_FILE,
|
||||
...(chatgptAppPath ? { chatgptAppPath } : {}),
|
||||
configDbFile: APP_CONFIG_DB_FILE,
|
||||
configDir: CONFIGDIR,
|
||||
configFile: CONFIG_FILE,
|
||||
dataDir: DATADIR,
|
||||
desktop: true,
|
||||
gatewayConfigFile: GATEWAY_CONFIG_FILE,
|
||||
launchAtLoginSupported: isLaunchAtLoginSupported(),
|
||||
name: APP_NAME,
|
||||
...(opencodeAppPath ? { opencodeAppPath } : {}),
|
||||
|
|
@ -84,10 +94,7 @@ ipcMain.handle(IPC_CHANNELS.appRenderHtmlPng, async (event, request: AppRenderHt
|
|||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.appGetConfig, () => loadAppConfig());
|
||||
ipcMain.handle(IPC_CHANNELS.appGetOnboardingFinished, async () => {
|
||||
const persisted = await loadPersistedAppSetting(onboardingFinishedAtSettingKey);
|
||||
return Boolean(readString(persisted) || existsSync(ONBOARDING_FINISHED_FILE));
|
||||
});
|
||||
ipcMain.handle(IPC_CHANNELS.appGetOnboardingFinished, () => loadOnboardingFinished());
|
||||
ipcMain.handle(IPC_CHANNELS.appGetPendingProviderDeepLinks, () => deepLinkService.consumePendingProviderRequests());
|
||||
ipcMain.handle(IPC_CHANNELS.appGetLocalAgentProviderCandidates, () => getLocalAgentProviderCandidates());
|
||||
ipcMain.handle(IPC_CHANNELS.appGetProfileOpenCommand, async (_event, request: ProfileOpenRequest) => {
|
||||
|
|
@ -331,7 +338,8 @@ ipcMain.handle(IPC_CHANNELS.appSaveApiKeys, async (_event, apiKeys: ApiKeyConfig
|
|||
return nextConfig;
|
||||
});
|
||||
ipcMain.handle(IPC_CHANNELS.appSetOnboardingFinished, async () => {
|
||||
await replacePersistedAppSetting(onboardingFinishedAtSettingKey, new Date().toISOString());
|
||||
await markOnboardingFinished();
|
||||
windowsManager.setOnboardingFinished(true);
|
||||
windowsManager.resizeMainWindowToScreenSize();
|
||||
return true;
|
||||
});
|
||||
|
|
@ -402,10 +410,7 @@ async function exportAppData(window: BrowserWindow | null): Promise<AppDataExpor
|
|||
|
||||
assertExportTargetIsNotInternalDataFile(result.filePath);
|
||||
const config = await loadAppConfig();
|
||||
const onboardingFinished = Boolean(
|
||||
readString(await loadPersistedAppSetting(onboardingFinishedAtSettingKey)) ||
|
||||
existsSync(ONBOARDING_FINISHED_FILE)
|
||||
);
|
||||
const onboardingFinished = await loadOnboardingFinished();
|
||||
const payload = {
|
||||
app: {
|
||||
name: APP_NAME,
|
||||
|
|
@ -933,7 +938,8 @@ function readDataExportFiles(): Array<{ base64: string; name: string; path: stri
|
|||
function dataExportCandidateFiles(): string[] {
|
||||
return uniqueStrings([
|
||||
...sqliteDataFiles(APP_CONFIG_DB_FILE),
|
||||
...sqliteDataFiles(API_KEYS_DB_FILE),
|
||||
...LEGACY_APP_CONFIG_DB_FILES.flatMap(sqliteDataFiles),
|
||||
...LEGACY_API_KEYS_DB_FILES.flatMap(sqliteDataFiles),
|
||||
...sqliteDataFiles(REQUEST_LOGS_DB_FILE),
|
||||
...sqliteDataFiles(USAGE_DB_FILE)
|
||||
]);
|
||||
|
|
@ -946,12 +952,9 @@ function sqliteDataFiles(file: string): string[] {
|
|||
function assertExportTargetIsNotInternalDataFile(file: string): void {
|
||||
const target = path.resolve(file);
|
||||
const reserved = new Set([
|
||||
CONFIG_FILE,
|
||||
LEGACY_ACTIVE_CONFIG_FILE,
|
||||
LEGACY_WINDOWS_CONFIG_FILE,
|
||||
LEGACY_CONFIG_FILE,
|
||||
APP_CONFIG_DB_FILE,
|
||||
API_KEYS_DB_FILE,
|
||||
REQUEST_LOGS_DB_FILE,
|
||||
USAGE_DB_FILE,
|
||||
...dataExportCandidateFiles()
|
||||
].map((item) => path.resolve(item)));
|
||||
if (reserved.has(target)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { app, BrowserWindow, dialog, shell } from "electron";
|
||||
import { setupApplicationMenu } from "./app-menu";
|
||||
import { loadAppConfig } from "@ccr/core/config/config";
|
||||
import { loadOnboardingFinished } from "@ccr/core/config/onboarding-state";
|
||||
import { restoreClaudeAppGatewayConfig, syncClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { deepLinkService } from "./deep-link";
|
||||
import { gatewayService } from "@ccr/core/gateway/service";
|
||||
|
|
@ -44,7 +45,9 @@ function startPrimaryInstance(): void {
|
|||
});
|
||||
|
||||
void app.whenReady().then(async () => {
|
||||
applyNativeThemePreference((await loadAppConfig()).theme);
|
||||
const config = await loadAppConfig();
|
||||
applyNativeThemePreference(config.theme);
|
||||
windowsManager.setOnboardingFinished(await loadOnboardingFinished());
|
||||
configureProxyDesktopIntegration();
|
||||
let ccrLauncherPreparation: CcrCliLauncherPreparation | undefined;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import { copyMissingDirectoryContents, sameFilesystemPath } from "@ccr/core/stor
|
|||
installSocketTypeOfServiceCompat();
|
||||
markDesktopAppRuntime();
|
||||
|
||||
const appDataPath = app.getPath("appData");
|
||||
const homePath = app.getPath("home");
|
||||
const appDataPath = readConfiguredRuntimePath("CCR_INTERNAL_APP_DATA_DIR") ?? app.getPath("appData");
|
||||
const homePath = readConfiguredRuntimePath("CCR_INTERNAL_HOME_DIR") ?? app.getPath("home");
|
||||
setRuntimeAppPaths({
|
||||
appData: appDataPath,
|
||||
home: homePath
|
||||
home: homePath,
|
||||
userData: readConfiguredRuntimePath("CCR_INTERNAL_USER_DATA_DIR")
|
||||
});
|
||||
const userDataPath = configureRuntimeUserDataPath(app.getPath("userData"));
|
||||
setRuntimeAppPaths({
|
||||
|
|
@ -54,6 +55,11 @@ function reportFatalStartupError(error: unknown): void {
|
|||
app.exit(1);
|
||||
}
|
||||
|
||||
function readConfiguredRuntimePath(key: string): string | undefined {
|
||||
const value = process.env[key]?.trim();
|
||||
return value || undefined;
|
||||
}
|
||||
|
||||
function configureRuntimeUserDataPath(currentUserDataPath: string): string {
|
||||
const sharedUserDataPath = resolveRuntimeDataDir();
|
||||
mkdirSync(sharedUserDataPath, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ const trayMascotIconPaths: Record<TrayMascotIconId, string> = {
|
|||
};
|
||||
|
||||
class TrayController {
|
||||
private activeDetailProvider?: string;
|
||||
private detailCloseTimer?: NodeJS.Timeout;
|
||||
private detailOpen = false;
|
||||
private detailPopover?: BrowserWindow;
|
||||
|
|
@ -82,7 +81,6 @@ class TrayController {
|
|||
hidePopover(): void {
|
||||
this.clearDetailCloseTimer();
|
||||
this.detailOpen = false;
|
||||
this.activeDetailProvider = undefined;
|
||||
this.hideDetailPopover();
|
||||
if (this.popover && !this.popover.isDestroyed()) {
|
||||
this.popover.hide();
|
||||
|
|
@ -184,7 +182,6 @@ class TrayController {
|
|||
const popover = this.ensurePopover();
|
||||
this.clearDetailCloseTimer();
|
||||
this.detailOpen = false;
|
||||
this.activeDetailProvider = undefined;
|
||||
this.hideDetailPopover();
|
||||
const { menu } = resolvePopoverLayout(this.tray?.getBounds(), false);
|
||||
|
||||
|
|
@ -240,52 +237,6 @@ class TrayController {
|
|||
return this.popover;
|
||||
}
|
||||
|
||||
private ensureDetailPopover(provider?: string): BrowserWindow {
|
||||
if (this.detailPopover && !this.detailPopover.isDestroyed()) {
|
||||
return this.detailPopover;
|
||||
}
|
||||
|
||||
this.detailPopover = new BrowserWindow({
|
||||
acceptFirstMouse: true,
|
||||
alwaysOnTop: true,
|
||||
frame: false,
|
||||
fullscreenable: false,
|
||||
hasShadow: true,
|
||||
height: popoverPreferredHeight - popoverDetailTopOffset,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
movable: false,
|
||||
roundedCorners: true,
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
title: `${APP_NAME} Usage Detail`,
|
||||
...trayWindowMaterialOptions(),
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
zoomFactor: 1
|
||||
},
|
||||
width: popoverDetailWidth
|
||||
});
|
||||
|
||||
reinforceTrayWindowMaterial(this.detailPopover);
|
||||
prepareTrayWindowForSharpRendering(this.detailPopover);
|
||||
this.detailPopover.setAlwaysOnTop(true, "pop-up-menu");
|
||||
this.detailPopover.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
this.detailPopover.on("blur", () => this.handlePopoverBlur());
|
||||
this.detailPopover.on("closed", () => {
|
||||
this.detailPopover = undefined;
|
||||
});
|
||||
|
||||
this.activeDetailProvider = normalizeDetailProvider(provider);
|
||||
void this.detailPopover.loadURL(createTrayPageUrl("detail", this.activeDetailProvider));
|
||||
return this.detailPopover;
|
||||
}
|
||||
|
||||
private showContextMenu(): void {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
|
|
@ -316,35 +267,6 @@ class TrayController {
|
|||
windowsManager.showMainWindow();
|
||||
}
|
||||
|
||||
private showDetailPopover(provider?: string): void {
|
||||
if (!this.popover || this.popover.isDestroyed() || !this.popover.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearDetailCloseTimer();
|
||||
this.detailOpen = true;
|
||||
|
||||
const { detail, menu } = resolvePopoverLayout(this.tray?.getBounds(), true);
|
||||
if (detail.width < 320) {
|
||||
this.detailOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.popover.setBounds(menu, false);
|
||||
|
||||
const detailPopover = this.ensureDetailPopover(provider);
|
||||
const nextProvider = normalizeDetailProvider(provider);
|
||||
if (this.activeDetailProvider !== nextProvider) {
|
||||
this.activeDetailProvider = nextProvider;
|
||||
void detailPopover.loadURL(createTrayPageUrl("detail", nextProvider));
|
||||
}
|
||||
|
||||
detailPopover.setBounds(detail, false);
|
||||
this.ignorePopoverBlurUntil = Date.now() + 120;
|
||||
detailPopover.showInactive();
|
||||
detailPopover.moveTop();
|
||||
}
|
||||
|
||||
private scheduleDetailClose(): void {
|
||||
this.clearDetailCloseTimer();
|
||||
this.detailCloseTimer = setTimeout(() => {
|
||||
|
|
@ -571,11 +493,6 @@ function createTrayPageUrl(mode: "detail" | "menu", provider?: string): string {
|
|||
return url.toString();
|
||||
}
|
||||
|
||||
function normalizeDetailProvider(provider?: string): string | undefined {
|
||||
const trimmed = provider?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function reinforceTrayWindowMaterial(window: BrowserWindow): void {
|
||||
const applyMaterial = () => {
|
||||
applyTrayWindowMaterial(window);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { app, BrowserWindow, screen, shell } from "electron";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { APP_NAME, IPC_CHANNELS, ONBOARDING_FINISHED_FILE } from "@ccr/core/config/constants";
|
||||
import { APP_NAME, IPC_CHANNELS } from "@ccr/core/config/constants";
|
||||
import { configureClaudeDesignWindowCdp, type ClaudeDesignWindowCdpOptions } from "./claude-design-window";
|
||||
|
||||
type WindowName = "main" | string;
|
||||
|
|
@ -42,8 +42,13 @@ const pluginAppScreenshotCaptures = new WeakSet<object>();
|
|||
const pluginAppScriptRuns = new WeakSet<object>();
|
||||
|
||||
class WindowsManager {
|
||||
private onboardingFinished = false;
|
||||
private windows = new Map<WindowName, BrowserWindow>();
|
||||
|
||||
setOnboardingFinished(finished: boolean): void {
|
||||
this.onboardingFinished = finished;
|
||||
}
|
||||
|
||||
createMainWindow(): BrowserWindow {
|
||||
const existing = this.getWindow("main");
|
||||
if (existing) {
|
||||
|
|
@ -51,7 +56,7 @@ class WindowsManager {
|
|||
return existing;
|
||||
}
|
||||
|
||||
const bounds = getMainWindowInitialBounds();
|
||||
const bounds = getMainWindowInitialBounds(this.onboardingFinished);
|
||||
|
||||
const window = new BrowserWindow({
|
||||
...bounds,
|
||||
|
|
@ -296,10 +301,10 @@ function fitWindowSize(preferred: number, minimum: number, available: number): n
|
|||
return Math.max(minimum, Math.min(preferred, available > 0 ? available : preferred));
|
||||
}
|
||||
|
||||
function getMainWindowInitialBounds(): WindowBounds {
|
||||
function getMainWindowInitialBounds(onboardingFinished: boolean): WindowBounds {
|
||||
const { height: availableHeight, width: availableWidth } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
if (existsSync(ONBOARDING_FINISHED_FILE)) {
|
||||
if (onboardingFinished) {
|
||||
return getMainWindowScreenBounds();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
createApiKeyList, createClaudeDesignRoutingDraft, createClaudeDesignRoutingRuleDraft, createCursorProxyRoutingDraft, createCursorProxyRoutingRuleDraft, createEmptyAgentAnalysis,
|
||||
copyTextToClipboard, createEmptyRequestLogPage, createEmptyUsageStats, createExtensionInstallDraft, createGeneratedApiKey, createPluginSettingsDraft, createProfileDraft,
|
||||
createProfileDraftFromProfile, createProviderDraft, createProviderDraftFromDeepLinkPayload, createProviderDraftFromProvider, createRoutingRuleDraft, createRoutingRuleDraftFromRule,
|
||||
createVirtualModelDraft, createVirtualModelDraftFromProfile, customProviderPresetId, DEFAULT_TRAY_WIDGETS, detectSystemLanguage, detectSystemTheme,
|
||||
createVirtualModelDraft, createVirtualModelDraftFromProfile, DEFAULT_TRAY_WIDGETS, detectSystemLanguage, detectSystemTheme,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
ExtensionConfigTarget, ExtensionDeleteTarget, ExtensionInstallDraft, ExtensionSource, fallbackAgentAnalysis, fallbackConfig,
|
||||
fallbackGatewayStatus, fallbackInfo, fallbackProxyNetworkSnapshot, fallbackProxyStatus, fallbackRequestLogPage,
|
||||
|
|
@ -19,8 +19,8 @@ import {
|
|||
isTraySupportedPlatform,
|
||||
LayoutGroup, mergeModelDisplayNames, mergeModelMetadata, mergeProviderModelLists, modelDescriptionsForModels, modelDisplayNamesForModels, modelMetadataForModels,
|
||||
navigation, NavigationId, normalizeApiKeys, normalizeBotGatewaySavedConfigs, normalizeConfig, normalizeLanguagePreference, normalizeObservabilityConfig, normalizeOverviewWidgets, normalizeProxyConfig,
|
||||
normalizeProfileItem, normalizeProfileScope, normalizeProviderBaseUrl, normalizeRouterBuiltInRules, normalizeRouterFallbackConfig, normalizeThemePreference, normalizeToolHubConfig, normalizeTrayBalanceProgressConfig, normalizeTrayIconPreference,
|
||||
normalizeTrayWidgets, normalizeTrayWindowModules, normalizeVirtualModelDraftPatch, numberValue, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder,
|
||||
normalizeProfileItem, normalizeProviderBaseUrl, normalizeRouterBuiltInRules, normalizeRouterFallbackConfig, normalizeThemePreference, normalizeToolHubConfig, normalizeTrayBalanceProgressConfig, normalizeTrayIconPreference,
|
||||
normalizeTrayWidgets, normalizeTrayWindowModules, normalizeVirtualModelDraftPatch, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder,
|
||||
OverviewWidgetConfig, parseProviderAccountDraft, pluginConfigPatchFromSettingsDraft,
|
||||
providerCredentialsFromDraft,
|
||||
persistLanguagePreference, PluginInstallCandidate, PluginMarketplaceEntry, PluginRoutingConfigTarget, PluginSettingsDraft, presetCapabilitiesFromDraft,
|
||||
|
|
@ -206,8 +206,8 @@ function App() {
|
|||
const [updateDialogStatus, setUpdateDialogStatus] = useState<AppUpdateStatus>(fallbackUpdateStatus);
|
||||
const [gatewayActionBusy, setGatewayActionBusy] = useState(false);
|
||||
const [gatewayActionTargetActive, setGatewayActionTargetActive] = useState<boolean>();
|
||||
const [actionMessage, setActionMessage] = useState("");
|
||||
const [actionError, setActionError] = useState("");
|
||||
const [, setActionMessage] = useState("");
|
||||
const [, setActionError] = useState("");
|
||||
const [profileActionError, setProfileActionError] = useState("");
|
||||
const [profileAddOpen, setProfileAddOpen] = useState(false);
|
||||
const [profileAgentTab, setProfileAgentTab] = useState<ProfileConfig["agent"]>("claude-code");
|
||||
|
|
@ -2130,21 +2130,6 @@ function App() {
|
|||
setPluginSettingsError("");
|
||||
}
|
||||
|
||||
function openConfigurePluginRouting(index: number) {
|
||||
const item = draftConfig.plugins[index];
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (isClaudeDesignPluginConfig(item)) {
|
||||
setClaudeDesignRoutingDraft(createClaudeDesignRoutingDraft(item.config));
|
||||
} else if (isCursorProxyPluginConfig(item)) {
|
||||
setCursorProxyRoutingDraft(createCursorProxyRoutingDraft(item.config));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setPluginRoutingConfigTarget({ index });
|
||||
}
|
||||
|
||||
function updateClaudeDesignRoutingDraft(patch: Partial<ClaudeDesignRoutingDraft>) {
|
||||
setClaudeDesignRoutingDraft((current) => ({ ...current, ...patch }));
|
||||
}
|
||||
|
|
@ -2602,24 +2587,6 @@ function App() {
|
|||
setProfileActionError("");
|
||||
}
|
||||
|
||||
function openProfileDialog(index: number) {
|
||||
const profile = draftConfig.profile.profiles[index];
|
||||
if (!profile?.enabled) {
|
||||
return;
|
||||
}
|
||||
setProfileActionError("");
|
||||
const surfaces = profileOpenSurfaces(profile);
|
||||
if (surfaces.length > 1) {
|
||||
void showProfileCliCommand(profile, "choose");
|
||||
return;
|
||||
}
|
||||
if (surfaces[0] === "app") {
|
||||
void openProfileApp(profile);
|
||||
return;
|
||||
}
|
||||
void showProfileCliCommand(profile);
|
||||
}
|
||||
|
||||
async function copyProfileCliCommand(index: number) {
|
||||
const profile = draftConfig.profile.profiles[index];
|
||||
if (!profile?.enabled || !profileOpenSurfaces(profile).includes("cli") || profileActionBusy) {
|
||||
|
|
@ -2721,31 +2688,6 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
async function showProfileCliCommand(profile: ProfileConfig, mode: "choose" | "cli" = "cli") {
|
||||
const fallbackCommand = profileOpenCommandFallback(profile, "cli");
|
||||
setProfileOpenDialog({ busy: "cli", command: fallbackCommand, mode, profile });
|
||||
if (!(await persistConfig(draftConfig, setProfileActionError))) {
|
||||
setProfileOpenDialog((current) => current?.profile.id === profile.id
|
||||
? { ...current, busy: "", error: profileActionError || t("Failed to save profile before opening.") }
|
||||
: current);
|
||||
return;
|
||||
}
|
||||
if (!window.ccr?.getProfileOpenCommand) {
|
||||
setProfileOpenDialog((current) => current?.profile.id === profile.id ? { ...current, busy: "" } : current);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await window.ccr.getProfileOpenCommand({ profileId: profile.id, surface: "cli" });
|
||||
setProfileOpenDialog((current) => current?.profile.id === profile.id
|
||||
? { ...current, busy: "", command: result.command, error: "" }
|
||||
: current);
|
||||
} catch (error) {
|
||||
setProfileOpenDialog((current) => current?.profile.id === profile.id
|
||||
? { ...current, busy: "", error: formatError(error) }
|
||||
: current);
|
||||
}
|
||||
}
|
||||
|
||||
async function showProfileReadyDialog(profile: ProfileConfig) {
|
||||
if (!profile.enabled) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle,
|
||||
DragEndEvent, DragOverEvent, DragOverlay, DragStartEvent, Field, formatAxisNumber, formatBytes,
|
||||
formatCompactNumber, formatDuration, formatLogDateTime, formatPercent, formatProviderAccountDetailDate, formatProviderAccountMeterTitle, formatProviderAccountMeterValue,
|
||||
formatStatusBucketDate, formatStatusCodeCounts, formatSystemStatusRange, formatToolCounts, formatUsdCost, KeyboardSensor,
|
||||
formatStatusBucketDate, formatSystemStatusRange, formatUsdCost, KeyboardSensor,
|
||||
LabelList, LayoutGroup, Line, LoaderCircle, MeasuringStrategy, MetricTone,
|
||||
motion, normalizeAgentFilterValue, normalizeOverviewWidget, normalizeOverviewWidgets,
|
||||
OverviewMetricKind, overviewMetricOptions, overviewWidgetCollisionDetection, OverviewWidgetConfig, OverviewWidgetSize, overviewWidgetSizeOptions,
|
||||
|
|
@ -1172,7 +1172,6 @@ function overviewMetricShowsRatio(metric: OverviewMetricKind): boolean {
|
|||
|
||||
function UsageTrendWidget({
|
||||
dimensions,
|
||||
usageRange,
|
||||
usageStats,
|
||||
variant
|
||||
}: {
|
||||
|
|
@ -3472,194 +3471,6 @@ export function AgentAnalysisView({
|
|||
);
|
||||
}
|
||||
|
||||
function AgentEndpointsCard({ endpoints }: { endpoints: AgentAnalysisSnapshot["endpoints"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Endpoint Health")}</CardTitle>
|
||||
<Badge variant="outline">{endpoints.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{endpoints.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No endpoint activity")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[380px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[980px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Path")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Success rate")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("P95")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Max concurrent")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Cache")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Status codes")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{endpoints.map((endpoint) => (
|
||||
<tr className={agentListRowClassName()} key={endpoint.key}>
|
||||
<td className="max-w-[260px] px-3 py-2" title={`${endpoint.method} ${endpoint.path}`}>
|
||||
<span className="font-mono font-semibold">{endpoint.method}</span> {endpoint.path}
|
||||
</td>
|
||||
<td className="px-3 py-2">{t(agentKindLabel(endpoint.agent))}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(endpoint.requestCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatPercent(endpoint.successRate)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatDuration(endpoint.p95DurationMs)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(endpoint.maxConcurrentRequests)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatPercent(endpoint.cacheRatio)}</td>
|
||||
<td className="px-3 py-2">{formatStatusCodeCounts(endpoint.statusCodes)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentClientsCard({ clients }: { clients: AgentAnalysisSnapshot["clients"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Client Signals")}</CardTitle>
|
||||
<Badge variant="outline">{clients.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{clients.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No client signals")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[380px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[720px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Client")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Sessions")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Success rate")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("P95")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("UA")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{clients.map((client) => (
|
||||
<tr className={agentListRowClassName()} key={client.key}>
|
||||
<td className="max-w-[160px] px-3 py-2 font-semibold" title={client.label}>{client.label}</td>
|
||||
<td className="px-3 py-2">{t(agentKindLabel(client.agent))}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(client.sessionCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(client.requestCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatPercent(client.successRate)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatDuration(client.p95DurationMs)}</td>
|
||||
<td className="max-w-[260px] px-3 py-2 font-mono" title={client.userAgent}>{compactUserAgent(client.userAgent)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRoutesCard({ routes }: { routes: AgentAnalysisSnapshot["routes"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Route Observability")}</CardTitle>
|
||||
<Badge variant="outline">{routes.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{routes.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No route activity")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[360px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[700px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Route")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Model")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Success rate")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("P95")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{routes.map((route) => (
|
||||
<tr className={agentListRowClassName()} key={route.key}>
|
||||
<td className="max-w-[180px] px-3 py-2 font-semibold" title={formatRouteReason(route.routeReason)}>{formatRouteReason(route.routeReason)}</td>
|
||||
<td className="px-3 py-2">{t(agentKindLabel(route.agent))}</td>
|
||||
<td className="max-w-[220px] px-3 py-2" title={`${route.provider}/${route.model}`}>{route.provider}/{route.model}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(route.requestCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatPercent(route.successRate)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatDuration(route.p95DurationMs)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentErrorsCard({ errors }: { errors: AgentAnalysisSnapshot["errors"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Recent Errors")}</CardTitle>
|
||||
<Badge variant="outline">{errors.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{errors.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No errors")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[360px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[900px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Time")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Status")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Path")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Route")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Duration")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{errors.map((error) => (
|
||||
<tr className={agentListRowClassName({ danger: true })} key={error.id}>
|
||||
<td className="px-3 py-2 font-mono">{formatLogDateTime(error.createdAt)}</td>
|
||||
<td className="px-3 py-2 font-semibold" title={error.error}>{error.statusCode || "-"}</td>
|
||||
<td className="max-w-[260px] px-3 py-2" title={`${error.method} ${error.path}`}>{error.method} {error.path}</td>
|
||||
<td className="px-3 py-2">{t(agentKindLabel(error.agent))}</td>
|
||||
<td className="max-w-[140px] px-3 py-2" title={formatRouteReason(error.routeReason)}>{formatRouteReason(error.routeReason)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatDuration(error.durationMs)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSessionDetailCard({
|
||||
clearSession,
|
||||
detail,
|
||||
|
|
@ -4205,32 +4016,6 @@ function traceRunBarClass(run: AgentAnalysisTraceRun): string {
|
|||
return "bg-blue-500";
|
||||
}
|
||||
|
||||
function SessionMetricCell({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="min-w-0 border-b border-r border-border/60 px-3 py-2 last:border-r-0 xl:border-b-0">
|
||||
<div className="truncate text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 truncate font-mono text-[13px] font-semibold text-foreground" title={value}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionInlineList({ title, value }: { title: string; value: string }) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-md border border-border/60 px-3 py-2 text-[11px]">
|
||||
<div className="text-muted-foreground">{title}</div>
|
||||
<div className="mt-1 truncate font-medium" title={value}>{value || "-"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatToolRows(tools: AgentAnalysisSnapshot["tools"]): string {
|
||||
return tools.slice(0, 5).map((tool) => `${tool.name} (${formatCompactNumber(tool.count)})`).join(", ");
|
||||
}
|
||||
|
||||
function formatRouteRows(routes: AgentAnalysisSnapshot["routes"]): string {
|
||||
return routes.slice(0, 5).map((route) => `${formatRouteReason(route.routeReason)}: ${formatCompactNumber(route.requestCount)}`).join(", ");
|
||||
}
|
||||
|
||||
function formatRouteReason(value: string | undefined): string {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -4311,149 +4096,6 @@ function AgentSessionsCard({
|
|||
);
|
||||
}
|
||||
|
||||
function AgentToolsCard({ tools }: { tools: AgentAnalysisSnapshot["tools"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Tool Usage")}</CardTitle>
|
||||
<Badge variant="outline">{tools.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tools.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No tool calls")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[380px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[560px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Tool")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Tool calls")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Sessions")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{tools.map((tool) => (
|
||||
<tr className={agentListRowClassName()} key={tool.name}>
|
||||
<td className="max-w-[220px] px-3 py-2 font-semibold" title={tool.name}>{tool.name}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(tool.count)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(tool.requestCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(tool.sessions)}</td>
|
||||
<td className="px-3 py-2">{tool.agents.map(agentKindLabel).map(t).join(", ")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSubagentsCard({ subagents }: { subagents: AgentAnalysisSnapshot["subagents"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Subagent Routing")}</CardTitle>
|
||||
<Badge variant="outline">{subagents.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{subagents.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No subagent calls")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[360px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[620px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Session")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Model")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Tokens")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Cache")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{subagents.map((subagent) => (
|
||||
<tr className={agentListRowClassName()} key={`${subagent.agent}:${subagent.sessionId}:${subagent.provider}:${subagent.model}`}>
|
||||
<td className="max-w-[160px] px-3 py-2 font-mono font-semibold" title={subagent.sessionId}>{compactId(subagent.sessionId)}</td>
|
||||
<td className="max-w-[240px] px-3 py-2" title={`${subagent.provider}/${subagent.model}`}>{subagent.provider}/{subagent.model}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(subagent.count)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(subagent.totalTokens)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(subagent.cacheReadTokens + subagent.cacheWriteTokens)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRecentRequestsCard({ requests }: { requests: AgentAnalysisSnapshot["recentRequests"] }) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{t("Recent Requests")}</CardTitle>
|
||||
<Badge variant="outline">{requests.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{requests.length === 0 ? (
|
||||
<AnalysisEmptyState label={t("No recent agent requests")} />
|
||||
) : (
|
||||
<div className={cn("max-h-[360px]", agentListFrameClassName)}>
|
||||
<table className={cn("min-w-[1240px]", agentListTableClassName)}>
|
||||
<thead className={agentListHeadClassName}>
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-semibold">{t("Time")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Agent")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Client")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Status")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Session")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Route")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("Model")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Tools")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Subagents")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Cache")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Concurrency")}</th>
|
||||
<th className="px-3 py-2 text-right font-semibold">{t("Duration")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={agentListBodyClassName}>
|
||||
{requests.map((request) => (
|
||||
<tr className={agentListRowClassName()} key={request.id}>
|
||||
<td className="px-3 py-2 font-mono">{formatLogDateTime(request.createdAt)}</td>
|
||||
<td className="px-3 py-2">{t(agentKindLabel(request.agent))}</td>
|
||||
<td className="max-w-[160px] px-3 py-2" title={request.userAgent || request.client}>{request.client}</td>
|
||||
<td className="px-3 py-2 font-semibold">{request.statusCode || "-"}</td>
|
||||
<td className="max-w-[150px] px-3 py-2 font-mono font-semibold" title={request.sessionId}>{compactId(request.sessionId)}</td>
|
||||
<td className="max-w-[130px] px-3 py-2" title={formatRouteReason(request.routeReason)}>{formatRouteReason(request.routeReason)}</td>
|
||||
<td className="max-w-[240px] px-3 py-2" title={`${request.provider}/${request.model}`}>{request.provider}/{request.model}</td>
|
||||
<td className="px-3 py-2 text-right" title={request.tools.join(", ")}>{formatCompactNumber(request.toolCallCount)}</td>
|
||||
<td className="px-3 py-2 text-right">{request.subagentModel ? request.subagentModel : "-"}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(request.cacheReadTokens + request.cacheWriteTokens)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatCompactNumber(request.concurrentRequests)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatDuration(request.durationMs)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AnalysisEmptyState({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-3 py-8 text-center text-[12px] text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ export function MainLayout({
|
|||
updateActionBusy,
|
||||
updateStatus,
|
||||
viewProps,
|
||||
requestLogsEnabled,
|
||||
visibleNavigation
|
||||
}: {
|
||||
activeView: ViewId;
|
||||
|
|
|
|||
|
|
@ -37,27 +37,6 @@ const onboardingStepDetails: Record<OnboardingStepId, {
|
|||
}
|
||||
};
|
||||
|
||||
const onboardingMascotPalettes: Record<OnboardingMascotTone, { accent: string; glow: string; main: string; shadow: string }> = {
|
||||
cyan: {
|
||||
accent: "#8CF7FF",
|
||||
glow: "rgba(34, 211, 238, 0.22)",
|
||||
main: "#22D3EE",
|
||||
shadow: "rgba(8, 145, 178, 0.22)"
|
||||
},
|
||||
orange: {
|
||||
accent: "#FFD166",
|
||||
glow: "rgba(249, 115, 22, 0.2)",
|
||||
main: "#F97316",
|
||||
shadow: "rgba(194, 65, 12, 0.22)"
|
||||
},
|
||||
violet: {
|
||||
accent: "#C084FC",
|
||||
glow: "rgba(139, 92, 246, 0.2)",
|
||||
main: "#8B5CF6",
|
||||
shadow: "rgba(109, 40, 217, 0.22)"
|
||||
}
|
||||
};
|
||||
|
||||
export function OnboardingView({
|
||||
activeStep,
|
||||
agentOptions,
|
||||
|
|
|
|||
|
|
@ -514,56 +514,6 @@ function ProfileCliCommandBlock({
|
|||
);
|
||||
}
|
||||
|
||||
function ProfileAgentTabs({
|
||||
activeAgent,
|
||||
agentOptions,
|
||||
profiles,
|
||||
setActiveAgent
|
||||
}: {
|
||||
activeAgent: ProfileConfig["agent"];
|
||||
agentOptions: ProfileAgentOption[];
|
||||
profiles: ProfileConfig[];
|
||||
setActiveAgent: (agent: ProfileConfig["agent"]) => void;
|
||||
}) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={t("Agent profiles")}
|
||||
className="grid grid-cols-1 gap-1 rounded-md border border-border bg-muted/20 p-1 sm:grid-cols-7"
|
||||
role="tablist"
|
||||
>
|
||||
{agentOptions.map((option) => {
|
||||
const agent = option.value;
|
||||
const selected = activeAgent === agent;
|
||||
const count = profiles.filter((profile) => profile.agent === agent).length;
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-selected={selected}
|
||||
className={cn(
|
||||
"flex h-11 min-w-0 items-center gap-2 rounded-[5px] px-2 text-left text-[12px] font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/25",
|
||||
selected
|
||||
? "bg-background text-foreground shadow-card"
|
||||
: "text-muted-foreground hover:bg-background/70 hover:text-foreground"
|
||||
)}
|
||||
key={agent}
|
||||
onClick={() => setActiveAgent(agent)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<AgentLogo agent={agent} className="h-6 w-6 rounded-[5px]" />
|
||||
<span className="min-w-0 flex-1 truncate">{t(profileAgentLabel(agent))}</span>
|
||||
<Badge className="shrink-0" variant={selected ? "secondary" : "outline"}>
|
||||
{count}
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSelectControl({
|
||||
agentOptions,
|
||||
onChange,
|
||||
|
|
|
|||
|
|
@ -1275,19 +1275,6 @@ function openExternalUrl(url: string) {
|
|||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function providerBaseOrigin(value: string): string | undefined {
|
||||
const url = normalizedHttpUrl(value);
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(url).origin;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizedHttpUrl(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -1724,8 +1711,6 @@ function ProviderFormStepHeader({
|
|||
index: number;
|
||||
title: string;
|
||||
}) {
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<div className="sm:col-span-2 flex min-w-0 items-start justify-between gap-3 pb-1">
|
||||
<div className="min-w-0 space-y-1">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
Activity, AppConfig, AppCopy, AppInfo, AppLanguagePreference, Boxes, BotGatewayConfigDraft, botGatewayAuthSpecsForPlatform,
|
||||
botGatewayDefaultAuthType, botGatewayFieldsForAuth, botGatewayPickAuthFields, botGatewayPlatformLabel, botGatewayPlatformOptions,
|
||||
botGatewaySavedConfigFromDraft, botGatewaySavedConfigLabel, BotGatewayQrLoginStartResult, BotGatewayQrLoginWaitResult, BotGatewayQrWindowOpenResult, BotGatewaySavedConfig, Button,
|
||||
botGatewaySavedConfigFromDraft, BotGatewayQrLoginStartResult, BotGatewayQrLoginWaitResult, BotGatewayQrWindowOpenResult, BotGatewaySavedConfig, Button,
|
||||
CircleAlert, closestCenter, cn, CSS, Database, Dialog, DialogBody, DialogContent,
|
||||
DialogFooter, DialogHeader, DialogTitle, endpointFromHostPort, Field, formatAppError, formatProviderAccountMeterValue, formatSystemOption, Gauge,
|
||||
Globe,
|
||||
|
|
@ -1157,8 +1157,7 @@ function DataSettingsSection({
|
|||
</div>
|
||||
|
||||
<div className="grid gap-2 rounded-md border border-border bg-background p-3">
|
||||
<DataPathRow label={t("Config database")} value={appInfo.appConfigDbFile} />
|
||||
<DataPathRow label={t("API key database")} value={appInfo.apiKeysDbFile} />
|
||||
<DataPathRow label={t("Config database")} value={appInfo.configDbFile} />
|
||||
<DataPathRow label={t("Request log database")} value={appInfo.requestLogsDbFile} />
|
||||
<DataPathRow label={t("Usage database")} value={appInfo.usageDbFile} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,376 +1,8 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
ApiKeyLimitConfig
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { isPlainRecord, stringValue } from "./common";
|
||||
import type { AddApiKeyDraft, ApiKeyLimitDraftRow, ApiKeyLimitMetric, ApiKeyListItem, LimitWindowPreset } from "./types";
|
||||
|
|
|
|||
|
|
@ -1,376 +1,30 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
TrayWindowModuleId
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { positiveInteger } from "./api-keys";
|
||||
import type { MetricTone } from "./controls";
|
||||
|
|
|
|||
|
|
@ -1,376 +1,9 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
AppConfig
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
fallbackConfig
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { normalizeApiKeys } from "./api-keys";
|
||||
import { normalizeOverviewWidgets, normalizeThemePreference, normalizeTrayBalanceProgressConfig, normalizeTrayComponentVariants, normalizeTrayIconPreference, normalizeTrayProgressTargetTokens, normalizeTrayWidgets, normalizeTrayWindowModules } from "./common";
|
||||
|
|
|
|||
|
|
@ -1,96 +1,14 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MorphIcon } from "@musistudio/lucide-morph-react";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pencil,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
X
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
|
|
@ -99,278 +17,29 @@ import { Switch } from "@/components/ui/switch";
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { playPauseMorph } from "@/lib/morph-icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
UsageTotals
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
useAppText
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedIconSwap,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
AnimatedPopover,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
pageSpringTransition,
|
||||
ViewMotionShell
|
||||
AnimatedPopover
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
formatDuration
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
formatPercent
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { metricToneBar, normalizeProviderModelSelector } from "./common";
|
||||
import { profileAgentLabel, profileAgentLogoUrl } from "./profiles";
|
||||
|
|
|
|||
|
|
@ -1,381 +1,17 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
GATEWAY_PLUGIN_PERMISSION_IDS,
|
||||
GATEWAY_PLUGIN_SURFACE_IDS,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
GATEWAY_PLUGIN_SURFACE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayPluginConfig,
|
||||
GatewayPluginPermission,
|
||||
GatewayPluginSurfacesConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
PluginMarketplaceEntry
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { isPlainRecord, stringValue } from "./common";
|
||||
import { isClaudeDesignPluginConfig, isCursorProxyPluginConfig, readClaudeDesignRoutingConfig } from "./routing";
|
||||
|
|
|
|||
|
|
@ -128,13 +128,11 @@ import {
|
|||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
|
|
@ -169,10 +167,6 @@ import type {
|
|||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
|
|
@ -220,10 +214,6 @@ import type {
|
|||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
|
|
@ -347,9 +337,6 @@ import {
|
|||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ import type {
|
|||
import { createDefaultAppConfig } from "@ccr/core/config/default-config";
|
||||
|
||||
export const fallbackInfo: AppInfo = {
|
||||
appConfigDbFile: "Browser preview",
|
||||
apiKeysDbFile: "Browser preview",
|
||||
configDbFile: "Browser preview",
|
||||
configDir: "Browser preview",
|
||||
configFile: "Browser preview",
|
||||
dataDir: "Browser preview",
|
||||
desktop: false,
|
||||
gatewayConfigFile: "Browser preview",
|
||||
launchAtLoginSupported: /^Mac|^Win/i.test(navigator.platform),
|
||||
name: "Claude Code Router",
|
||||
platform: navigator.platform,
|
||||
|
|
@ -34,14 +31,11 @@ export const fallbackUpdateStatus: AppUpdateStatus = {
|
|||
supported: false
|
||||
};
|
||||
|
||||
export const fallbackConfig: AppConfig = createDefaultAppConfig({
|
||||
generatedConfigFile: "Browser preview"
|
||||
});
|
||||
export const fallbackConfig: AppConfig = createDefaultAppConfig({});
|
||||
|
||||
export const fallbackGatewayStatus: GatewayStatus = {
|
||||
coreEndpoint: "http://127.0.0.1:3457",
|
||||
endpoint: "http://127.0.0.1:3456",
|
||||
generatedConfigFile: "Browser preview",
|
||||
networkEndpoints: [],
|
||||
state: "stopped"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,376 +1,10 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
RequestLogEntry
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
formatCompactNumber
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { isPlainRecord, stringValue } from "./common";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,105 +1,3 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import grokLogoUrl from "@/assets/agent-logos/grok.ico";
|
||||
|
|
@ -107,275 +5,28 @@ import kiloLogoUrl from "@/assets/agent-logos/kilo.svg";
|
|||
import openCodeLogoUrl from "@/assets/agent-logos/opencode.ico";
|
||||
import piLogoUrl from "@/assets/agent-logos/pi.svg";
|
||||
import zcodeLogoUrl from "@/assets/agent-logos/zcode.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
normalizeProfileScopeValue
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
VirtualModelProfileConfig
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
fallbackConfig
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { isPlainRecord, normalizeProviderModelSelector, stringValue, uniqueStrings } from "./common";
|
||||
import { virtualModelProfileModelNames } from "./providers";
|
||||
|
|
|
|||
|
|
@ -1,376 +1,10 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
ProviderAccountSnapshot
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
formatCompactNumber
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
export function compareProviderAccountSnapshots(a: ProviderAccountSnapshot, b: ProviderAccountSnapshot): number {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,384 +1,53 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import grokLogoUrl from "@/assets/agent-logos/grok.ico";
|
||||
import openCodeLogoUrl from "@/assets/agent-logos/opencode.ico";
|
||||
import zcodeLogoUrl from "@/assets/agent-logos/zcode.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
ROUTER_SCRIPT_MAX_TIMEOUT_MS,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
ROUTER_SCRIPT_MAX_TIMEOUT_MS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayProviderCapabilityProtocol,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
LocalAgentProviderKind,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProviderModelMetadata,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
VirtualModelProfileConfig
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
type ProviderPreset
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { newApiUserSelfConnectorConfig } from "@ccr/core/providers/new-api";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
routerConditionSourceOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
import type { RouterConditionSource } from "./options";
|
||||
|
||||
import { normalizeApiKeyLimits, positiveInteger } from "./api-keys";
|
||||
import { isPlainRecord, normalizeProviderModelSelector, stringValue, uniqueStrings } from "./common";
|
||||
|
|
@ -530,7 +199,7 @@ export function formatRouterRuleCondition(rule: RouterRule): string {
|
|||
return "condition unset";
|
||||
}
|
||||
|
||||
export function routerRuleConditionFromRule(rule: RouterRule, config?: AppConfig): RouterRuleCondition | undefined {
|
||||
export function routerRuleConditionFromRule(rule: RouterRule, _config?: AppConfig): RouterRuleCondition | undefined {
|
||||
if (rule.condition) {
|
||||
return rule.condition;
|
||||
}
|
||||
|
|
@ -1929,7 +1598,7 @@ export function selectedProviderProtocolsFromCapabilities(
|
|||
export function selectedProviderProtocolsForProbe(
|
||||
selectedProtocols: GatewayProviderProtocol[],
|
||||
probe: GatewayProviderProbeResult,
|
||||
fallback: GatewayProviderProtocol,
|
||||
_fallback: GatewayProviderProtocol,
|
||||
presetId?: string
|
||||
): GatewayProviderProtocol[] {
|
||||
const selectable = providerSelectableProtocolsFromProbe(probe);
|
||||
|
|
|
|||
|
|
@ -1,213 +1,12 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
ROUTER_FALLBACK_MAX_RETRY_COUNT,
|
||||
ROUTER_SCRIPT_API_VERSION,
|
||||
ROUTER_SCRIPT_DEFAULT_TIMEOUT_MS,
|
||||
ROUTER_SCRIPT_MAX_TIMEOUT_MS,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
ROUTER_SCRIPT_MAX_TIMEOUT_MS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouteScriptSampleRequest,
|
||||
RouterBuiltInAgentRuleId,
|
||||
RouterBuiltInRulesConfig,
|
||||
|
|
@ -219,165 +18,20 @@ import type {
|
|||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
RouterRuleType
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
fallbackConfig
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
routerRuleTypeOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
import { positiveInteger } from "./api-keys";
|
||||
import { isPlainRecord, normalizeProviderModelSelector, stringValue, uniqueStrings } from "./common";
|
||||
|
|
|
|||
|
|
@ -1,376 +1,14 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
ProxyStatus
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
useAppText
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
|
||||
export function endpointFromHostPort(host: string, port: number): string {
|
||||
const trimmedHost = host.trim() || "127.0.0.1";
|
||||
|
|
|
|||
|
|
@ -1,380 +1,50 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
getFirstCollision,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
rectIntersection,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type CollisionDetection,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
type DragStartEvent
|
||||
type CollisionDetection
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Copy,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
LoaderCircle,
|
||||
MoveRight,
|
||||
Network,
|
||||
Palette,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pause,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ComposedChart,
|
||||
LabelList,
|
||||
Line,
|
||||
Pie,
|
||||
PieChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PopoverContent } from "@/components/ui/popover";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import appLogoUrl from "@/assets/logo.png";
|
||||
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
|
||||
import codexLogoUrl from "@/assets/agent-logos/codex.png";
|
||||
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
|
||||
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
|
||||
import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
|
||||
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
import zhipuCnGeneralProviderIconUrl from "@/assets/provider-icons/zhipu-cn-general.png";
|
||||
import trayCyanIconUrl from "@/assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "@/assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "@/assets/tray-violet.png";
|
||||
import {
|
||||
BUILTIN_FUSION_TOOL_SERVER_NAME,
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
CLAUDE_CODE_DEFAULT_ENV,
|
||||
DEFAULT_OVERVIEW_WIDGETS,
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
normalizeProfileScopeValue,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_TOP_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "@ccr/core/contracts/app";
|
||||
import type {
|
||||
AgentAnalysisFilter,
|
||||
AgentAnalysisSessionSelection,
|
||||
AgentAnalysisSnapshot,
|
||||
AgentKind,
|
||||
AppConfig,
|
||||
AppInfo,
|
||||
AppUpdateStatus,
|
||||
ApiKeyConfig,
|
||||
ApiKeyLimitConfig,
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
BotGatewayQrLoginStartRequest,
|
||||
BotGatewayQrLoginStartResult,
|
||||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayQrWindowOpenResult,
|
||||
BotGatewayRuntimeConfig,
|
||||
BotGatewaySavedConfig,
|
||||
BotHandoffScanTarget,
|
||||
GatewayProviderConfig,
|
||||
GatewayProviderCapability,
|
||||
GatewayPluginAppConfig,
|
||||
GatewayPluginPermission,
|
||||
GatewayPluginSurfacesConfig,
|
||||
GatewayProviderConnectivityCheckModelResult,
|
||||
GatewayProviderConnectivityCheckReport,
|
||||
GatewayProviderProbeCandidate,
|
||||
GatewayProviderProbeCandidateResult,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpServerTransport,
|
||||
GatewayMcpStdioMessageMode,
|
||||
GatewayMcpToolInfo,
|
||||
GatewayStatus,
|
||||
OverviewMetricKind,
|
||||
OverviewWidgetConfig,
|
||||
OverviewWidgetSize,
|
||||
OverviewWidgetType,
|
||||
OverviewWidgetVariant,
|
||||
PluginDependency,
|
||||
PluginDirectorySelection,
|
||||
PluginMarketplaceEntry,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
ProviderAccountHttpJsonConnectorConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderAccountSnapshot,
|
||||
ProviderAccountTestPath,
|
||||
ProviderAccountTestResult,
|
||||
ProviderCredentialConfig,
|
||||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProviderModelMetadata,
|
||||
ProfileConfig,
|
||||
ProfileOpenSurface,
|
||||
CodexProfileConfigFormat,
|
||||
ProfileScope,
|
||||
ProfileSurface,
|
||||
ProxyCertificateInstallResult,
|
||||
ProxyCertificateStatus,
|
||||
ProxyNetworkBody,
|
||||
ProxyNetworkExchange,
|
||||
ProxyNetworkSnapshot,
|
||||
ProxyStatus,
|
||||
RequestLogBody,
|
||||
RequestLogEntry,
|
||||
RequestLogListFilter,
|
||||
RequestLogPage,
|
||||
RequestLogStatusFilter,
|
||||
RouterBuiltInAgentRuleId,
|
||||
RouterConfig,
|
||||
RouterFallbackConfig,
|
||||
RouterFallbackMode,
|
||||
RouterRule,
|
||||
RouterRuleCondition,
|
||||
RouterRuleOperator,
|
||||
RouterRuleRewrite,
|
||||
RouterRuleRewriteOperation,
|
||||
RouterRuleType,
|
||||
TrayBalanceProgressConfig,
|
||||
TrayComponentVariants,
|
||||
TrayWidgetConfig,
|
||||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId,
|
||||
UsageComparisonRow,
|
||||
UsageSeriesPoint,
|
||||
UsageStatsFilter,
|
||||
UsageStatsRange,
|
||||
UsageStatsSnapshot,
|
||||
UsageTotals,
|
||||
VirtualModelBaseModelMode,
|
||||
VirtualModelExecutionMode,
|
||||
VirtualModelFusionCustomToolConfig,
|
||||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider,
|
||||
VirtualModelProfileConfig,
|
||||
VirtualModelToolVisibility
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
customProviderPresetId,
|
||||
defaultProviderAccountConfig,
|
||||
standardProviderAccountConfig,
|
||||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
primaryProviderPresetEndpoint as primaryProviderPresetEndpointFromPreset,
|
||||
providerApiKeySafetyIssueInList,
|
||||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import {
|
||||
fallbackConfig,
|
||||
fallbackGatewayStatus,
|
||||
fallbackInfo,
|
||||
fallbackProxyCertificateStatus,
|
||||
fallbackProxyNetworkSnapshot,
|
||||
fallbackProxyStatus,
|
||||
fallbackUpdateStatus
|
||||
} from "./fallbacks";
|
||||
import {
|
||||
AppI18nContext,
|
||||
appCopy,
|
||||
languagePreferenceStorageKey,
|
||||
translateOptions,
|
||||
translateText,
|
||||
useAppText,
|
||||
type AppCopy
|
||||
} from "./i18n";
|
||||
import {
|
||||
AnimatedDisclosure,
|
||||
AnimatedFieldSlot,
|
||||
AnimatedListItem,
|
||||
disclosureSpringTransition,
|
||||
listSpringTransition,
|
||||
motionEase,
|
||||
pageSpringTransition,
|
||||
reducedMotionTransition,
|
||||
ViewMotionShell
|
||||
} from "./motion";
|
||||
import {
|
||||
clientInitial,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatHeaderName,
|
||||
formatNetworkDateTime,
|
||||
formatNetworkHeaders,
|
||||
formatNetworkRequestRaw,
|
||||
formatNetworkResponseRaw,
|
||||
formatNetworkTime,
|
||||
networkCodeLabel,
|
||||
networkExchangeMatchesQuery,
|
||||
networkHeaderRows,
|
||||
networkLifecycleLabel,
|
||||
networkQueryRows,
|
||||
networkRowId,
|
||||
networkStatusLabel,
|
||||
networkStatusVariant,
|
||||
networkSummaryRows
|
||||
} from "./network";
|
||||
import {
|
||||
agentKindLabel,
|
||||
compactId,
|
||||
compactUserAgent,
|
||||
createEmptyAgentAnalysis,
|
||||
createEmptyAgentConcurrencySeries,
|
||||
createEmptyRequestLogPage,
|
||||
createEmptyUsageSeries,
|
||||
createEmptyUsageStats,
|
||||
emptyUsageTotals,
|
||||
formatAxisNumber,
|
||||
formatCompactNumber,
|
||||
formatPercent,
|
||||
formatStatusCodeCounts,
|
||||
formatToolCounts,
|
||||
formatUsdCost,
|
||||
logSelectOptions,
|
||||
normalizeAgentFilterValue
|
||||
createEmptyUsageStats
|
||||
} from "./usage";
|
||||
import {
|
||||
agentAnalysisRangeOptions,
|
||||
agentFilterOptions,
|
||||
apiKeyExpirationOptions,
|
||||
apiKeyLimitMetricOptions,
|
||||
claudeDesignRouteRuleTypeOptions,
|
||||
customFusionToolName,
|
||||
defaultFusionWebSearchProvider,
|
||||
fusionToolOptions,
|
||||
fusionWebSearchEnvKeysByProvider,
|
||||
fusionWebSearchProviderOptions,
|
||||
getDefaultOnboardingStep,
|
||||
getNextOnboardingStep,
|
||||
isOnboardingProfileReady,
|
||||
isOnboardingProviderReady,
|
||||
legacyRouterRuleTypes,
|
||||
legacyUnimcpPackageName,
|
||||
legacyUnimcpServerName,
|
||||
limitWindowOptions,
|
||||
mcpServerStartupTimeoutMs,
|
||||
mcpServerTransportOptions,
|
||||
mcpStdioMessageModeOptions,
|
||||
navigation,
|
||||
onboardingStepOrder,
|
||||
overviewMetricOptions,
|
||||
overviewWidgetSizeOptions,
|
||||
profileAgentOptions,
|
||||
profileScopeOptions,
|
||||
profileSurfaceOptions,
|
||||
providerAccountModeOptions,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerUsageMethodOptions,
|
||||
requestLogPageSizeOptions,
|
||||
requestLogStatusOptions,
|
||||
removedLegacyRouterRuleIds,
|
||||
routerConditionSourceOptions,
|
||||
routerFallbackModeOptions,
|
||||
routerRewriteOperationOptions,
|
||||
routerRuleOperatorOptions,
|
||||
routerRuleTypeOptions,
|
||||
trayMascotIconUrls,
|
||||
usageRangeOptions,
|
||||
virtualModelBaseModeOptions,
|
||||
virtualModelClientToolsPolicyOptions,
|
||||
virtualModelExecutionModeOptions,
|
||||
virtualModelMatchModeOptions,
|
||||
virtualModelToolVisibilityOptions
|
||||
} from "./options";
|
||||
import type { AgentFilterValue, RouterConditionSource } from "./options";
|
||||
import type { MotionSafeDivAttributes } from "./motion";
|
||||
|
||||
import type { RouterConditionSource } from "./options";
|
||||
|
||||
export type ViewId = "onboarding" | "overview" | "observability" | "api-keys" | "server" | "profile" | "networking" | "logs" | "providers" | "models" | "routing" | "virtual-models" | "extensions";
|
||||
export type NavigationId = ViewId;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue