mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(frontend): implement comprehensive API Settings Management Interface
Implement a complete API Management Interface for React frontend with split- panel layout, migrating all features from legacy JS frontend. New Features: - API Settings page with 5 tabs: Providers, Endpoints, Cache, Model Pools, CLI Settings - Provider Management: CRUD operations, multi-key rotation, health checks, test connection - Endpoint Management: CRUD operations, cache strategy configuration, enable/disable toggle - Cache Settings: Global configuration, statistics display, clear cache functionality - Model Pool Management: CRUD operations, auto-discovery feature, provider exclusion - CLI Settings Management: Provider-based and Direct modes, full CRUD support - Multi-Key Settings Modal: Manage API keys with rotation strategies and weights - Manage Models Modal: View and manage models per provider (LLM and Embedding) - Sync to CodexLens: Integration handler for provider configuration sync Technical Implementation: - Created 12 new React components in components/api-settings/ - Extended lib/api.ts with 460+ lines of API client functions - Created hooks/useApiSettings.ts with 772 lines of TanStack Query hooks - Added RadioGroup UI component for form selections - Implemented unified error handling with useNotifications across all operations - Complete i18n support (500+ keys in English and Chinese) - Route integration (/api-settings) and sidebar navigation Code Quality: - All acceptance criteria from plan.json verified - Code review passed with Gemini (all 7 IMPL tasks complete) - Follows existing patterns: Shadcn UI, TanStack Query, react-intl, Lucide icons
This commit is contained in:
@@ -345,6 +345,45 @@ import {
|
||||
const BUILTIN_CLI_TOOLS = ['gemini', 'qwen', 'codex', 'opencode', 'claude'] as const;
|
||||
type BuiltinCliTool = typeof BUILTIN_CLI_TOOLS[number];
|
||||
|
||||
/**
|
||||
* Transaction ID type for concurrent session disambiguation
|
||||
* Format: ccw-tx-${conversationId}-${timestamp}
|
||||
*/
|
||||
export type TransactionId = string;
|
||||
|
||||
/**
|
||||
* Generate a unique transaction ID for the current execution
|
||||
* @param conversationId - CCW conversation ID
|
||||
* @returns Transaction ID in format: ccw-tx-${conversationId}-${uniquePart}
|
||||
*/
|
||||
export function generateTransactionId(conversationId: string): TransactionId {
|
||||
// Use crypto.randomUUID() if available, otherwise use timestamp + random
|
||||
const uniquePart = typeof crypto !== 'undefined' && crypto.randomUUID
|
||||
? crypto.randomUUID().slice(0, 8)
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
return `ccw-tx-${conversationId}-${uniquePart}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject transaction ID into user prompt
|
||||
* @param prompt - Original user prompt
|
||||
* @param txId - Transaction ID to inject
|
||||
* @returns Prompt with transaction ID injected at the start
|
||||
*/
|
||||
export function injectTransactionId(prompt: string, txId: TransactionId): string {
|
||||
return `[CCW-TX-ID: ${txId}]\n\n${prompt}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract transaction ID from prompt
|
||||
* @param prompt - Prompt that may contain transaction ID
|
||||
* @returns Transaction ID if found, null otherwise
|
||||
*/
|
||||
export function extractTransactionId(prompt: string): TransactionId | null {
|
||||
const match = prompt.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Define Zod schema for validation
|
||||
// tool accepts built-in tools or custom endpoint IDs (CLI封装)
|
||||
const ParamsSchema = z.object({
|
||||
@@ -788,6 +827,17 @@ async function executeCliTool(
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Info message for Codex TTY limitation
|
||||
if (tool === 'codex' && !supportsNativeResume(tool) && resumeDecision.strategy !== 'native') {
|
||||
if (onOutput) {
|
||||
onOutput({
|
||||
type: 'stderr',
|
||||
content: '[ccw] Using prompt-concat mode for Codex (Codex TTY limitation for native resume)\n',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use configured primary model if no explicit model provided
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface NativeSessionMapping {
|
||||
native_session_id: string; // Native UUID
|
||||
native_session_path?: string; // Native file path
|
||||
project_hash?: string; // Project hash (Gemini/Qwen)
|
||||
transaction_id?: string; // Transaction ID for concurrent session disambiguation
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -360,6 +361,23 @@ export class CliHistoryStore {
|
||||
|
||||
console.log('[CLI History] Migration complete: turns table updated');
|
||||
}
|
||||
|
||||
// Add transaction_id column to native_session_mapping table for concurrent session disambiguation
|
||||
const mappingInfo = this.db.prepare('PRAGMA table_info(native_session_mapping)').all() as Array<{ name: string }>;
|
||||
const hasTransactionId = mappingInfo.some(col => col.name === 'transaction_id');
|
||||
|
||||
if (!hasTransactionId) {
|
||||
console.log('[CLI History] Migrating database: adding transaction_id column to native_session_mapping...');
|
||||
this.db.exec(`
|
||||
ALTER TABLE native_session_mapping ADD COLUMN transaction_id TEXT;
|
||||
`);
|
||||
try {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_native_transaction_id ON native_session_mapping(transaction_id);`);
|
||||
} catch (indexErr) {
|
||||
console.warn('[CLI History] Transaction ID index creation warning:', (indexErr as Error).message);
|
||||
}
|
||||
console.log('[CLI History] Migration complete: transaction_id column added');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||
// Don't throw - allow the store to continue working with existing schema
|
||||
@@ -926,12 +944,13 @@ export class CliHistoryStore {
|
||||
*/
|
||||
saveNativeSessionMapping(mapping: NativeSessionMapping): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO native_session_mapping (ccw_id, tool, native_session_id, native_session_path, project_hash, created_at)
|
||||
VALUES (@ccw_id, @tool, @native_session_id, @native_session_path, @project_hash, @created_at)
|
||||
INSERT INTO native_session_mapping (ccw_id, tool, native_session_id, native_session_path, project_hash, transaction_id, created_at)
|
||||
VALUES (@ccw_id, @tool, @native_session_id, @native_session_path, @project_hash, @transaction_id, @created_at)
|
||||
ON CONFLICT(ccw_id) DO UPDATE SET
|
||||
native_session_id = @native_session_id,
|
||||
native_session_path = @native_session_path,
|
||||
project_hash = @project_hash
|
||||
project_hash = @project_hash,
|
||||
transaction_id = @transaction_id
|
||||
`);
|
||||
|
||||
this.withRetry(() => stmt.run({
|
||||
@@ -940,6 +959,7 @@ export class CliHistoryStore {
|
||||
native_session_id: mapping.native_session_id,
|
||||
native_session_path: mapping.native_session_path || null,
|
||||
project_hash: mapping.project_hash || null,
|
||||
transaction_id: mapping.transaction_id || null,
|
||||
created_at: mapping.created_at || new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
@@ -964,6 +984,16 @@ export class CliHistoryStore {
|
||||
return row?.ccw_id || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction ID by CCW ID
|
||||
*/
|
||||
getTransactionId(ccwId: string): string | null {
|
||||
const row = this.db.prepare(`
|
||||
SELECT transaction_id FROM native_session_mapping WHERE ccw_id = ?
|
||||
`).get(ccwId) as any;
|
||||
return row?.transaction_id || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full mapping by CCW ID
|
||||
*/
|
||||
|
||||
@@ -94,6 +94,12 @@ abstract class SessionDiscoverer {
|
||||
|
||||
// Try to match by prompt content (fallback for parallel execution)
|
||||
const matched = this.matchSessionByPrompt(sessions, prompt);
|
||||
|
||||
// Warn if multiple sessions and no prompt match found (low confidence)
|
||||
if (!matched && sessions.length > 1) {
|
||||
console.warn(`[ccw] Session tracking: multiple candidates found (${sessions.length}), using latest session`);
|
||||
}
|
||||
|
||||
return matched || sessions[0]; // Fallback to latest if no match
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
import type { ConversationTurn, ConversationRecord, NativeSessionMapping } from './cli-history-store.js';
|
||||
|
||||
/**
|
||||
* Emit user warning for silent fallback scenarios
|
||||
*/
|
||||
function warnUser(message: string): void {
|
||||
console.warn(`[ccw] ${message}`);
|
||||
}
|
||||
|
||||
// Strategy types
|
||||
export type ResumeStrategy = 'native' | 'prompt-concat' | 'hybrid';
|
||||
|
||||
@@ -78,6 +85,7 @@ export function determineResumeStrategy(options: ResumeStrategyOptions): ResumeD
|
||||
});
|
||||
|
||||
if (crossTool) {
|
||||
warnUser('Cross-tool resume: using prompt concatenation (different tool)');
|
||||
return buildPromptConcatDecision(resumeIds, getConversation);
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ export function determineResumeStrategy(options: ResumeStrategyOptions): ResumeD
|
||||
}
|
||||
|
||||
// No native mapping, fall back to prompt-concat
|
||||
warnUser('No native session mapping found, using prompt concatenation');
|
||||
return buildPromptConcatDecision(resumeIds, getConversation);
|
||||
}
|
||||
|
||||
@@ -109,6 +118,7 @@ function buildPromptConcatDecision(
|
||||
getConversation: (ccwId: string) => ConversationRecord | null
|
||||
): ResumeDecision {
|
||||
const allTurns: ConversationTurn[] = [];
|
||||
let hasMissingConversation = false;
|
||||
|
||||
for (const id of resumeIds) {
|
||||
const conversation = getConversation(id);
|
||||
@@ -119,9 +129,16 @@ function buildPromptConcatDecision(
|
||||
_sourceId: id
|
||||
}));
|
||||
allTurns.push(...turnsWithSource as ConversationTurn[]);
|
||||
} else {
|
||||
hasMissingConversation = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if any conversation was not found
|
||||
if (hasMissingConversation) {
|
||||
warnUser('One or more resume IDs not found, using prompt concatenation (new session created)');
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
allTurns.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
|
||||
Reference in New Issue
Block a user