feat: TypeScript + Electron v2 重构 - 纯桌面版
- 全面迁移到 TypeScript,严格类型定义 - 放弃 Web 版,专注 Electron 桌面应用 - 主进程模块化:main.ts, preload.ts, menu.ts, tray.ts, ipc.ts, utils.ts - 渲染进程完整迁移所有功能组件 - 删除 PWA 相关文件 (sw.js, manifest.json) - 删除 Web 版降级逻辑 - 保留所有核心功能:流式对话、多模型、Think推理、多模态、RAG知识库、Agent预设、历史管理 - 保留 Windows 11 Fluent Design 暗色主题样式
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Ollama API 客户端封装
|
||||
*/
|
||||
|
||||
import type {
|
||||
OllamaChatParams, OllamaStreamChunk, OllamaModelsResponse,
|
||||
OllamaPsResponse, OllamaVersionResponse, OllamaModelDetail,
|
||||
OllamaEmbedResponse
|
||||
} from '../types.js';
|
||||
|
||||
export class OllamaAPI {
|
||||
baseUrl: string;
|
||||
|
||||
constructor(baseUrl = 'http://127.0.0.1:11434') {
|
||||
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
private async _request(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const config: RequestInit = {
|
||||
...options,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers as Record<string, string> || {}) }
|
||||
};
|
||||
const response = await fetch(url, config);
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text().catch(() => '');
|
||||
throw new Error(`Ollama API 错误 ${response.status}: ${errorBody || response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private async _json<T = unknown>(path: string, body: unknown = null): Promise<T> {
|
||||
const options: RequestInit = body ? { method: 'POST', body: JSON.stringify(body) } : {};
|
||||
const response = await this._request(path, options);
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async listModels(): Promise<OllamaModelsResponse> {
|
||||
return this._json('/api/tags');
|
||||
}
|
||||
|
||||
async psModels(): Promise<OllamaPsResponse> {
|
||||
return this._json('/api/ps');
|
||||
}
|
||||
|
||||
async getVersion(): Promise<OllamaVersionResponse> {
|
||||
return this._json('/api/version');
|
||||
}
|
||||
|
||||
async showModel(model: string): Promise<OllamaModelDetail> {
|
||||
return this._json('/api/show', { model });
|
||||
}
|
||||
|
||||
async chat(params: Omit<OllamaChatParams, 'stream'>): Promise<unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
model: params.model,
|
||||
messages: params.messages,
|
||||
stream: false,
|
||||
};
|
||||
if (params.think !== undefined) body.think = params.think;
|
||||
if (params.system) body.system = params.system;
|
||||
if (params.keep_alive !== undefined) body.keep_alive = params.keep_alive;
|
||||
if (params.options) body.options = params.options;
|
||||
return this._json('/api/chat', body);
|
||||
}
|
||||
|
||||
async chatStream(
|
||||
params: OllamaChatParams,
|
||||
onChunk: (chunk: OllamaStreamChunk) => void,
|
||||
abortController?: AbortController
|
||||
): Promise<void> {
|
||||
const body: Record<string, unknown> = {
|
||||
model: params.model,
|
||||
messages: params.messages,
|
||||
stream: true,
|
||||
};
|
||||
if (params.think !== undefined) body.think = params.think;
|
||||
if (params.system) body.system = params.system;
|
||||
if (params.keep_alive !== undefined) body.keep_alive = params.keep_alive;
|
||||
if (params.options) body.options = params.options;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
};
|
||||
|
||||
if (abortController) {
|
||||
fetchOptions.signal = abortController.signal;
|
||||
}
|
||||
|
||||
const response = await this._request('/api/chat', fetchOptions);
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
if (abortController) {
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
reader.cancel();
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
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.trim()) continue;
|
||||
try {
|
||||
const chunk: OllamaStreamChunk = JSON.parse(line);
|
||||
if (onChunk) onChunk(chunk);
|
||||
if (chunk.done) {
|
||||
reader.cancel();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[OllamaAPI] 流式数据解析警告:', (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const chunk: OllamaStreamChunk = JSON.parse(buffer);
|
||||
if (onChunk) onChunk(chunk);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
async embed(model: string, input: string): Promise<OllamaEmbedResponse> {
|
||||
return this._json('/api/embed', { model, input });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user