feat: Anthropic provider, Hanzo Node discovery, org switcher, settings sync
- Add AnthropicProvider: full Messages API support (chat, streaming, system msg) - Add HanzoNodeProvider: discovers standalone Rust node on :3690/:8080 - Fix Hanzo Cloud URL to api.hanzo.ai (not llm.hanzo.ai) - Add local.discover support for Hanzo Node alongside Ollama/LM Studio/Desktop - Add local.chat Anthropic routing (x-api-key + anthropic-version headers) - Add settings.get/set/export/import with CDP bridge sync to ~/.hanzo - Add org.list/org.switch: fetch orgs from IAM, persist active org - Add apikey.save/get/list: per-provider API key storage in chrome.storage - All settings persist in chrome.storage.local (survives restarts)
This commit is contained in:
@@ -48,14 +48,14 @@ export interface AIProvider {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hanzo Cloud Provider (api.hanzo.ai / llm.hanzo.ai)
|
||||
// Hanzo Cloud Provider (api.hanzo.ai / api.hanzo.ai)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class HanzoCloudProvider implements AIProvider {
|
||||
id = 'hanzo-cloud';
|
||||
name = 'Hanzo Cloud';
|
||||
type = 'cloud' as const;
|
||||
private baseUrl = 'https://llm.hanzo.ai/v1';
|
||||
private baseUrl = 'https://api.hanzo.ai/v1';
|
||||
private token: string | null = null;
|
||||
private models: AIModel[] = [];
|
||||
private available = false;
|
||||
@@ -453,6 +453,268 @@ export class AIProviderRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anthropic-Compatible Provider (messages API)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class AnthropicProvider implements AIProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'cloud' | 'local';
|
||||
private baseUrl: string;
|
||||
private apiKey: string;
|
||||
private models: AIModel[] = [];
|
||||
private available = false;
|
||||
|
||||
constructor(id: string, name: string, baseUrl: string, apiKey: string, type: 'cloud' | 'local' = 'cloud') {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
this.apiKey = apiKey;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
async discover(): Promise<boolean> {
|
||||
try {
|
||||
// Anthropic doesn't have a /models endpoint — test with a minimal request
|
||||
const resp = await fetch(`${this.baseUrl}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
// Any response (even 400) means the API is reachable
|
||||
this.available = resp.status !== 0 && resp.status < 500;
|
||||
if (this.available && !this.models.length) {
|
||||
this.models = [
|
||||
{ id: 'claude-opus-4-20250514', name: 'Claude Opus 4', provider: this.id },
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: this.id },
|
||||
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: this.id },
|
||||
];
|
||||
}
|
||||
return this.available;
|
||||
} catch {
|
||||
this.available = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async listModels(): Promise<AIModel[]> {
|
||||
if (!this.models.length) await this.discover();
|
||||
return this.models;
|
||||
}
|
||||
|
||||
async chat(model: string, messages: AIMessage[], options?: AICompletionOptions): Promise<string> {
|
||||
// Anthropic messages API: system is separate from messages
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
const chatMsgs = messages.filter(m => m.role !== 'system').map(m => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
const body: any = {
|
||||
model,
|
||||
max_tokens: options?.max_tokens || 4096,
|
||||
messages: chatMsgs,
|
||||
};
|
||||
if (systemMsg) body.system = systemMsg.content;
|
||||
if (options?.temperature !== undefined) body.temperature = options.temperature;
|
||||
if (options?.top_p !== undefined) body.top_p = options.top_p;
|
||||
if (options?.stop) body.stop_sequences = options.stop;
|
||||
|
||||
const resp = await fetch(`${this.baseUrl}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`Anthropic: ${resp.status} ${text}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
return data.content?.[0]?.text || '';
|
||||
}
|
||||
|
||||
async chatStream(model: string, messages: AIMessage[], onToken: (token: string) => void, options?: AICompletionOptions): Promise<string> {
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
const chatMsgs = messages.filter(m => m.role !== 'system').map(m => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
const body: any = {
|
||||
model,
|
||||
max_tokens: options?.max_tokens || 4096,
|
||||
messages: chatMsgs,
|
||||
stream: true,
|
||||
};
|
||||
if (systemMsg) body.system = systemMsg.content;
|
||||
if (options?.temperature !== undefined) body.temperature = options.temperature;
|
||||
|
||||
const resp = await fetch(`${this.baseUrl}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Anthropic stream: ${resp.status}`);
|
||||
|
||||
const reader = resp.body?.getReader();
|
||||
if (!reader) throw new Error('No body');
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '', full = '';
|
||||
|
||||
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) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const payload = line.slice(6);
|
||||
if (payload === '[DONE]') continue;
|
||||
try {
|
||||
const json = JSON.parse(payload);
|
||||
if (json.type === 'content_block_delta' && json.delta?.text) {
|
||||
onToken(json.delta.text);
|
||||
full += json.delta.text;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
getStatus(): AIProviderStatus {
|
||||
return {
|
||||
id: this.id, name: this.name, type: this.type,
|
||||
available: this.available, url: this.baseUrl,
|
||||
models: this.models, requiresAuth: true, authenticated: !!this.apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hanzo Node Provider (gRPC/REST node with OpenAI-compatible inference)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class HanzoNodeProvider implements AIProvider {
|
||||
id = 'hanzo-node';
|
||||
name = 'Hanzo Node';
|
||||
type = 'local' as const;
|
||||
private baseUrl = 'http://localhost:3690';
|
||||
private models: AIModel[] = [];
|
||||
private available = false;
|
||||
|
||||
async discover(): Promise<boolean> {
|
||||
// Hanzo Node: REST on 3690, gRPC on 9090, HTTP health on 8080
|
||||
const ports = [3690, 8080, 9090];
|
||||
for (const port of ports) {
|
||||
try {
|
||||
const url = `http://localhost:${port}`;
|
||||
const endpoint = port === 3690 ? `${url}/v2/health` : `${url}/health`;
|
||||
const resp = await fetch(endpoint, { signal: AbortSignal.timeout(2000) });
|
||||
if (resp.ok) {
|
||||
this.baseUrl = url;
|
||||
this.available = true;
|
||||
return true;
|
||||
}
|
||||
} catch { /* next */ }
|
||||
}
|
||||
this.available = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
async listModels(): Promise<AIModel[]> {
|
||||
try {
|
||||
// Hanzo Node v2 API
|
||||
const resp = await fetch(`${this.baseUrl}/v2/models`, { signal: AbortSignal.timeout(5000) });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
this.models = (data.data || data.models || data || []).map((m: any) => ({
|
||||
id: m.id || m.name || m.model,
|
||||
name: m.id || m.name || m.model,
|
||||
provider: this.id,
|
||||
}));
|
||||
return this.models;
|
||||
}
|
||||
// Fallback: OpenAI-compat /v1/models
|
||||
const resp2 = await fetch(`${this.baseUrl}/v1/models`, { signal: AbortSignal.timeout(5000) });
|
||||
if (resp2.ok) {
|
||||
const data = await resp2.json();
|
||||
this.models = (data.data || []).map((m: any) => ({
|
||||
id: m.id, name: m.id, provider: this.id,
|
||||
}));
|
||||
}
|
||||
return this.models;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async chat(model: string, messages: AIMessage[], options?: AICompletionOptions): Promise<string> {
|
||||
// Try OpenAI-compat first, then Hanzo Node v2 inference
|
||||
const body: any = { model, messages, stream: false };
|
||||
if (options?.temperature !== undefined) body.temperature = options.temperature;
|
||||
if (options?.max_tokens !== undefined) body.max_tokens = options.max_tokens;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
return data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
} catch { /* try fallback */ }
|
||||
|
||||
// Fallback: Hanzo Node v2 inference endpoint
|
||||
const inferBody: any = {
|
||||
prompt: messages.map(m => `${m.role}: ${m.content}`).join('\n'),
|
||||
model,
|
||||
max_tokens: options?.max_tokens || 2048,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
};
|
||||
const resp = await fetch(`${this.baseUrl}/v1/inference`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(inferBody),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Hanzo Node: ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
return data.choices?.[0]?.text || data.response || '';
|
||||
}
|
||||
|
||||
getStatus(): AIProviderStatus {
|
||||
return {
|
||||
id: this.id, name: this.name, type: this.type,
|
||||
available: this.available, url: this.baseUrl,
|
||||
models: this.models, requiresAuth: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default registry with built-in providers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -467,6 +729,7 @@ export function createDefaultRegistry(): AIProviderRegistry {
|
||||
registry.register(new OllamaNativeProvider());
|
||||
registry.register(new OpenAICompatibleProvider('lmstudio', 'LM Studio', 'http://localhost:1234/v1'));
|
||||
registry.register(new OpenAICompatibleProvider('hanzo-desktop', 'Hanzo Desktop', 'http://localhost:11435/v1'));
|
||||
registry.register(new HanzoNodeProvider());
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -1538,32 +1538,57 @@ async function handleMessage(request: any, sender: chrome.runtime.MessageSender,
|
||||
if (provider === 'ollama' && !endpoint) {
|
||||
result = await ollamaClient.chat(model, messages, options);
|
||||
} else {
|
||||
// LM Studio, Hanzo Desktop, or custom endpoint — all use OpenAI-compatible API
|
||||
const baseUrl = endpoint || (
|
||||
provider === 'lmstudio' ? 'http://localhost:1234/v1' :
|
||||
provider === 'hanzo-desktop' ? 'http://localhost:11435/v1' :
|
||||
'http://localhost:8080/v1'
|
||||
);
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (request.apiKey) headers['Authorization'] = `Bearer ${request.apiKey}`;
|
||||
if (provider === 'anthropic') {
|
||||
// Anthropic Messages API
|
||||
const baseUrl = endpoint || 'https://api.anthropic.com';
|
||||
const systemMsg = messages.find((m: any) => m.role === 'system');
|
||||
const chatMsgs = messages.filter((m: any) => m.role !== 'system');
|
||||
const body: any = { model, max_tokens: options.num_predict || 4096, messages: chatMsgs, stream: false };
|
||||
if (systemMsg) body.system = systemMsg.content;
|
||||
if (options.temperature !== undefined) body.temperature = options.temperature;
|
||||
|
||||
const body: any = { model, messages, stream: false };
|
||||
if (options.temperature !== undefined) body.temperature = options.temperature;
|
||||
if (options.top_p !== undefined) body.top_p = options.top_p;
|
||||
if (options.num_predict !== undefined) body.max_tokens = options.num_predict;
|
||||
const resp = await fetch(`${baseUrl}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': request.apiKey || '',
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (!resp.ok) { const text = await resp.text(); throw new Error(`Anthropic: ${resp.status} ${text}`); }
|
||||
const data = await resp.json();
|
||||
result = data.content?.[0]?.text || '';
|
||||
} else {
|
||||
// OpenAI-compatible: LM Studio, Hanzo Desktop, Hanzo Node, custom
|
||||
const baseUrl = endpoint || (
|
||||
provider === 'lmstudio' ? 'http://localhost:1234/v1' :
|
||||
provider === 'hanzo-desktop' ? 'http://localhost:11435/v1' :
|
||||
provider === 'hanzo-node' ? 'http://localhost:3690/v1' :
|
||||
'http://localhost:8080/v1'
|
||||
);
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (request.apiKey) headers['Authorization'] = `Bearer ${request.apiKey}`;
|
||||
|
||||
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`${provider} error: ${resp.status} ${text}`);
|
||||
const body: any = { model, messages, stream: false };
|
||||
if (options.temperature !== undefined) body.temperature = options.temperature;
|
||||
if (options.top_p !== undefined) body.top_p = options.top_p;
|
||||
if (options.num_predict !== undefined) body.max_tokens = options.num_predict;
|
||||
|
||||
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`${provider} error: ${resp.status} ${text}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
result = data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
const data = await resp.json();
|
||||
result = data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
|
||||
sendResponse({ success: true, content: result });
|
||||
@@ -1647,6 +1672,23 @@ async function handleMessage(request: any, sender: chrome.runtime.MessageSender,
|
||||
providers.push({ name: 'hanzo-desktop', url: 'http://localhost:11435', available: false });
|
||||
}
|
||||
|
||||
// Hanzo Node (standalone Rust node, REST API on :3690 or :8080)
|
||||
let hanzoNodeFound = false;
|
||||
for (const port of [3690, 8080]) {
|
||||
try {
|
||||
const endpoint = port === 3690 ? `http://localhost:${port}/v2/health` : `http://localhost:${port}/health`;
|
||||
const resp = await fetch(endpoint, { signal: AbortSignal.timeout(2000) });
|
||||
if (resp.ok) {
|
||||
providers.push({ name: 'hanzo-node', url: `http://localhost:${port}`, available: true });
|
||||
hanzoNodeFound = true;
|
||||
break;
|
||||
}
|
||||
} catch { /* next */ }
|
||||
}
|
||||
if (!hanzoNodeFound) {
|
||||
providers.push({ name: 'hanzo-node', url: 'http://localhost:3690', available: false });
|
||||
}
|
||||
|
||||
// Check for custom endpoints saved in storage
|
||||
const stored = await chrome.storage.local.get(['customLLMEndpoints']);
|
||||
const customs: { name: string; url: string; apiKey?: string }[] = stored.customLLMEndpoints || [];
|
||||
@@ -1744,6 +1786,178 @@ async function handleMessage(request: any, sender: chrome.runtime.MessageSender,
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Settings Sync (unified chrome.storage + ~/.hanzo via CDP bridge) ---
|
||||
case 'settings.get': {
|
||||
try {
|
||||
const keys = request.keys || [
|
||||
'selectedProvider', 'selectedModel', 'customLLMEndpoints', 'ollamaUrl',
|
||||
'anthropicApiKey', 'openaiApiKey', 'hanzoApiKey',
|
||||
'mcpPort', 'cdpPort', 'zapPorts',
|
||||
'source-maps', 'webgpu-ai', 'browser-control', 'tab-filesystem',
|
||||
'activeOrgId', 'browserBackend',
|
||||
];
|
||||
const result = await chrome.storage.local.get(keys);
|
||||
sendResponse({ success: true, settings: result });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'settings.set': {
|
||||
try {
|
||||
await chrome.storage.local.set(request.settings);
|
||||
// Also persist to ~/.hanzo/extension/config.json via CDP bridge if connected
|
||||
try {
|
||||
const bridge = getCDPBridge();
|
||||
if (bridge.isConnected()) {
|
||||
for (const [key, value] of Object.entries(request.settings)) {
|
||||
bridge.sendConfig(key, value);
|
||||
}
|
||||
}
|
||||
} catch { /* bridge not connected, local-only save is fine */ }
|
||||
sendResponse({ success: true });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'settings.export': {
|
||||
// Export all settings as JSON
|
||||
try {
|
||||
const all = await chrome.storage.local.get(null);
|
||||
// Filter out tokens and sensitive data
|
||||
const safe: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(all)) {
|
||||
if (k.startsWith('hanzo_iam_')) continue; // skip tokens
|
||||
safe[k] = v;
|
||||
}
|
||||
sendResponse({ success: true, settings: safe });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'settings.import': {
|
||||
try {
|
||||
const settings = request.settings || {};
|
||||
// Don't import token data
|
||||
delete settings.hanzo_iam_access_token;
|
||||
delete settings.hanzo_iam_refresh_token;
|
||||
delete settings.hanzo_iam_id_token;
|
||||
await chrome.storage.local.set(settings);
|
||||
sendResponse({ success: true });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Organization Switcher ---
|
||||
case 'org.list': {
|
||||
// Fetch user's organizations from Hanzo IAM
|
||||
try {
|
||||
const token = await auth.getValidAccessToken();
|
||||
if (!token) { sendResponse({ success: false, error: 'Not authenticated' }); break; }
|
||||
|
||||
const resp = await fetch('https://iam.hanzo.ai/api/get-account', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`IAM: ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
const user = data.data || data;
|
||||
|
||||
// Casdoor stores org memberships in user.groups or via org API
|
||||
const orgs: { id: string; name: string; displayName?: string }[] = [];
|
||||
|
||||
// Primary org from user's owner field
|
||||
if (user.owner) {
|
||||
orgs.push({ id: user.owner, name: user.owner, displayName: user.owner });
|
||||
}
|
||||
|
||||
// Fetch orgs the user belongs to
|
||||
try {
|
||||
const orgsResp = await fetch('https://iam.hanzo.ai/api/get-organizations', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (orgsResp.ok) {
|
||||
const orgsData = await orgsResp.json();
|
||||
for (const org of (orgsData.data || orgsData || [])) {
|
||||
if (!orgs.find(o => o.id === org.name)) {
|
||||
orgs.push({ id: org.name, name: org.name, displayName: org.displayName || org.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* user may not have permission to list all orgs */ }
|
||||
|
||||
// Get active org from storage
|
||||
const stored = await chrome.storage.local.get(['activeOrgId']);
|
||||
sendResponse({
|
||||
success: true,
|
||||
orgs,
|
||||
activeOrgId: stored.activeOrgId || orgs[0]?.id || null,
|
||||
user: { name: user.displayName || user.name, email: user.email, avatar: user.avatar },
|
||||
});
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'org.switch': {
|
||||
try {
|
||||
await chrome.storage.local.set({ activeOrgId: request.orgId });
|
||||
sendResponse({ success: true, activeOrgId: request.orgId });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// --- API Key Management ---
|
||||
case 'apikey.save': {
|
||||
try {
|
||||
const keyMap: Record<string, string> = {};
|
||||
keyMap[`apikey_${request.provider}`] = request.apiKey;
|
||||
await chrome.storage.local.set(keyMap);
|
||||
sendResponse({ success: true });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'apikey.get': {
|
||||
try {
|
||||
const key = `apikey_${request.provider}`;
|
||||
const stored = await chrome.storage.local.get([key]);
|
||||
sendResponse({ success: true, apiKey: stored[key] || null });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'apikey.list': {
|
||||
try {
|
||||
const all = await chrome.storage.local.get(null);
|
||||
const keys: { provider: string; hasKey: boolean }[] = [];
|
||||
for (const [k] of Object.entries(all)) {
|
||||
if (k.startsWith('apikey_')) {
|
||||
keys.push({ provider: k.replace('apikey_', ''), hasKey: true });
|
||||
}
|
||||
}
|
||||
sendResponse({ success: true, keys });
|
||||
} catch (e: any) {
|
||||
sendResponse({ success: false, error: e.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user