409 lines
15 KiB
JavaScript
409 lines
15 KiB
JavaScript
/**
|
|
* API Wrapper for ComfyUI-Doctor
|
|
*/
|
|
export const DoctorAPI = {
|
|
/**
|
|
* Get the last analysis result
|
|
*/
|
|
async getLastAnalysis() {
|
|
try {
|
|
const response = await fetch('/debugger/last_analysis');
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch analysis:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get error history
|
|
*/
|
|
async getHistory() {
|
|
try {
|
|
const response = await fetch('/debugger/history');
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch history:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set display language
|
|
* @param {string} language - 'en', 'zh_TW', 'zh_CN', 'ja'
|
|
*/
|
|
async setLanguage(language) {
|
|
try {
|
|
const response = await fetch('/debugger/set_language', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ language })
|
|
});
|
|
// The original code had a check for response.ok, but the provided snippet removed it.
|
|
// Keeping the behavior from the provided snippet.
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to set language:', error);
|
|
// The original code returned null, but the provided snippet returns { success: false }.
|
|
// Keeping the behavior from the provided snippet.
|
|
return { success: false };
|
|
}
|
|
},
|
|
|
|
async analyzeError(payload) {
|
|
try {
|
|
const response = await fetch('/doctor/analyze', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json();
|
|
throw new Error(err.error || `HTTP ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Analysis failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Verify API key validity
|
|
* @param {string} baseUrl - LLM API base URL
|
|
* @param {string} apiKey - API key to verify
|
|
* @returns {Promise<{success: boolean, message: string, is_local: boolean}>}
|
|
*/
|
|
async verifyApiKey(baseUrl, apiKey) {
|
|
try {
|
|
const response = await fetch('/doctor/verify_key', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ base_url: baseUrl, api_key: apiKey })
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Key verification failed:', error);
|
|
return { success: false, message: error.message, is_local: false };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* List available LLM models
|
|
* @param {string} baseUrl - LLM API base URL
|
|
* @param {string} apiKey - API key
|
|
* @returns {Promise<{success: boolean, models: Array<{id: string, name: string}>, message: string}>}
|
|
*/
|
|
async listModels(baseUrl, apiKey) {
|
|
try {
|
|
const response = await fetch('/doctor/list_models', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ base_url: baseUrl, api_key: apiKey })
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to list models:', error);
|
|
return { success: false, models: [], message: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Stream chat with LLM (SSE)
|
|
* @param {Object} payload - Chat payload
|
|
* @param {Array<{role: string, content: string}>} payload.messages - Conversation history
|
|
* @param {Object} payload.error_context - Error context {error, node_context, workflow}
|
|
* @param {string} payload.api_key - LLM API key
|
|
* @param {string} payload.base_url - LLM API base URL
|
|
* @param {string} payload.model - Model name
|
|
* @param {string} payload.language - Response language
|
|
* @param {Function} onChunk - Callback for each chunk {delta: string, done: boolean}
|
|
* @param {Function} onError - Callback for errors
|
|
* @param {AbortSignal} signal - Optional AbortSignal for cancellation
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async streamChat(payload, onChunk, onError, signal = null) {
|
|
try {
|
|
const response = await fetch('/doctor/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ...payload, stream: true }),
|
|
signal
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
throw new Error(err.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
|
|
|
const jsonStr = trimmed.slice(5).trim();
|
|
if (!jsonStr) continue;
|
|
|
|
try {
|
|
const data = JSON.parse(jsonStr);
|
|
if (data.error) {
|
|
onError?.(new Error(data.error));
|
|
return;
|
|
}
|
|
onChunk?.(data);
|
|
if (data.done) return;
|
|
} catch (parseErr) {
|
|
console.warn('[ComfyUI-Doctor] Failed to parse SSE chunk:', jsonStr);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
console.log('[ComfyUI-Doctor] Chat stream aborted');
|
|
return;
|
|
}
|
|
console.error('[ComfyUI-Doctor] Stream chat failed:', error);
|
|
onError?.(error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Non-streaming chat with LLM (fallback)
|
|
* @param {Object} payload - Same as streamChat
|
|
* @returns {Promise<{content: string, done: boolean}>}
|
|
*/
|
|
async chat(payload) {
|
|
try {
|
|
const response = await fetch('/doctor/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ...payload, stream: false })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
throw new Error(err.error || `HTTP ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Chat failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* F4: Get error statistics for dashboard
|
|
* @param {number} timeRangeDays - Number of days to include (default: 30)
|
|
* @returns {Promise<{success: boolean, statistics: Object}>}
|
|
*/
|
|
async getStatistics(timeRangeDays = 30) {
|
|
try {
|
|
const response = await fetch(`/doctor/statistics?time_range_days=${timeRangeDays}`);
|
|
if (!response.ok) {
|
|
const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
throw new Error(err.error || `HTTP ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch statistics:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
statistics: {
|
|
total_errors: 0,
|
|
pattern_frequency: {},
|
|
category_breakdown: {},
|
|
top_patterns: [],
|
|
resolution_rate: { resolved: 0, unresolved: 0, ignored: 0 },
|
|
trend: { last_24h: 0, last_7d: 0, last_30d: 0 }
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get internal health metrics
|
|
* @returns {Promise<{success: boolean, health?: any, error?: string}>}
|
|
*/
|
|
async getHealth() {
|
|
try {
|
|
const response = await fetch('/doctor/health');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch health:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get plugin trust report (scan-only)
|
|
* @returns {Promise<{success: boolean, plugins?: any, error?: string}>}
|
|
*/
|
|
async getPluginsReport() {
|
|
try {
|
|
const response = await fetch('/doctor/plugins');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch plugin report:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* F4: Mark an error as resolved/unresolved/ignored
|
|
* @param {string} timestamp - Error timestamp
|
|
* @param {string} status - Status: 'resolved', 'unresolved', or 'ignored'
|
|
* @returns {Promise<{success: boolean, message: string}>}
|
|
*/
|
|
async markResolved(timestamp, status = 'resolved') {
|
|
try {
|
|
const response = await fetch('/doctor/mark_resolved', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ timestamp, status })
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to mark error:', error);
|
|
return { success: false, message: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* R16: Reset statistics (clears error history)
|
|
* @returns {Promise<{success: boolean, message?: string}>}
|
|
*/
|
|
async resetStatistics() {
|
|
try {
|
|
const response = await fetch('/doctor/statistics/reset', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to reset statistics:', error);
|
|
return { success: false, message: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* F16: Preview sanitized community feedback payload before GitHub PR submission.
|
|
* @param {Object} payload
|
|
* @returns {Promise<{success:boolean, preview?:Object, field_errors?:Object}>}
|
|
*/
|
|
async previewCommunityFeedback(payload) {
|
|
try {
|
|
const response = await fetch('/doctor/feedback/preview', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload || {})
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to preview community feedback:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* F16: Submit community feedback and create GitHub PR (server-side token).
|
|
* @param {Object} payload
|
|
* @param {string} [adminToken]
|
|
* @returns {Promise<{success:boolean, github?:Object, field_errors?:Object}>}
|
|
*/
|
|
async submitCommunityFeedback(payload, adminToken) {
|
|
try {
|
|
const body = { ...(payload || {}) };
|
|
if (adminToken) body.admin_token = adminToken;
|
|
const response = await fetch('/doctor/feedback/submit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to submit community feedback:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
// ---- S8: Secrets API ----
|
|
|
|
/**
|
|
* Get provider key configuration status (never returns actual keys).
|
|
* @param {string} [adminToken] - Optional admin token
|
|
* @returns {Promise<{success: boolean, providers: Object}>}
|
|
*/
|
|
async getSecretsStatus(adminToken) {
|
|
try {
|
|
const headers = {};
|
|
if (adminToken) headers['X-Doctor-Admin-Token'] = adminToken;
|
|
const response = await fetch('/doctor/secrets/status', { headers });
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to fetch secrets status:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save a provider secret to server-side store.
|
|
* @param {string} provider - Provider ID (openai, anthropic, etc.)
|
|
* @param {string} apiKey - The API key value
|
|
* @param {string} [adminToken] - Optional admin token
|
|
* @returns {Promise<{success: boolean, message?: string}>}
|
|
*/
|
|
async saveSecret(provider, apiKey, adminToken) {
|
|
try {
|
|
const body = { provider, api_key: apiKey };
|
|
if (adminToken) body.admin_token = adminToken;
|
|
const response = await fetch('/doctor/secrets', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to save secret:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete a provider secret from server-side store.
|
|
* @param {string} provider - Provider ID
|
|
* @param {string} [adminToken] - Optional admin token
|
|
* @returns {Promise<{success: boolean, message?: string}>}
|
|
*/
|
|
async clearSecret(provider, adminToken) {
|
|
try {
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (adminToken) headers['X-Doctor-Admin-Token'] = adminToken;
|
|
const body = adminToken ? JSON.stringify({ admin_token: adminToken }) : undefined;
|
|
const response = await fetch(`/doctor/secrets/${encodeURIComponent(provider)}`, {
|
|
method: 'DELETE',
|
|
headers,
|
|
body
|
|
});
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('[ComfyUI-Doctor] Failed to clear secret:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
};
|