feat: 升级至 v0.3.16 — 工单修复 51 项 + 审查修复 30 项(安全加固/适配器/工具系统/数据层/前端)
This commit is contained in:
@@ -50,15 +50,15 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// H-2 修复: chat → send(规范要求)
|
||||
async send(request: MetonaRequest): Promise<MetonaResponse> {
|
||||
const nativeRequest = this.toNativeRequest(request);
|
||||
// #2 修复: toNativeRequest 改为 async(需下载 URL 图片转 base64)
|
||||
const nativeRequest = await this.toNativeRequest(request);
|
||||
|
||||
const response = await fetch(`${this.baseURL}/api/chat`, {
|
||||
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
|
||||
const response = await this.fetchWithTimeout(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: false }),
|
||||
// C-2 修复: 使用合并后的 signal(外部 abort + timeout)
|
||||
signal: this.getFetchSignal(this.config.timeoutMs ?? 300_000),
|
||||
});
|
||||
}, this.config.timeoutMs ?? 300_000);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.throwHttpError(response, 'Ollama API error');
|
||||
@@ -70,15 +70,15 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// H-2 修复: chatStream → sendStream(规范要求)
|
||||
async *sendStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
|
||||
const nativeRequest = this.toNativeRequest(request);
|
||||
// #2 修复: toNativeRequest 改为 async(需下载 URL 图片转 base64)
|
||||
const nativeRequest = await this.toNativeRequest(request);
|
||||
|
||||
const response = await fetch(`${this.baseURL}/api/chat`, {
|
||||
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
|
||||
const response = await this.fetchWithTimeout(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: true }),
|
||||
// C-2 修复: 使用合并后的 signal(外部 abort + timeout)
|
||||
signal: this.getFetchSignal(this.config.timeoutMs ?? 300_000),
|
||||
});
|
||||
}, this.config.timeoutMs ?? 300_000);
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
await this.throwHttpError(response, 'Ollama stream error');
|
||||
@@ -409,8 +409,33 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// ========== 私有转换方法 ==========
|
||||
|
||||
private toNativeRequest(request: MetonaRequest): Record<string, unknown> {
|
||||
const messages = [
|
||||
/**
|
||||
* #2 修复: 下载 http(s) URL 图片并转为纯 base64 字符串(不含 data: 前缀)
|
||||
*
|
||||
* Ollama API 的 images 字段要求纯 base64 字符串数组。
|
||||
* 当 MetonaMessage.images 中存储的是 URL 时,需先下载转为 base64。
|
||||
* 下载失败时返回空字符串(Ollama 会忽略空图片),不阻断整个请求。
|
||||
*/
|
||||
private async resolveImageToBase64(url: string): Promise<string> {
|
||||
try {
|
||||
// 审查修复: 使用基类 fetchWithTimeout 合并 externalAbortSignal 和 30s 超时,
|
||||
// 避免用户中断时图片下载最多阻塞 30s×N(externalAbortSignal 是 BaseAdapter 的
|
||||
// private 属性,子类无法直接访问,故复用已合并 signal 的 fetchWithTimeout,
|
||||
// 该方法同时处理了 listener 泄漏问题)
|
||||
const res = await this.fetchWithTimeout(url, {}, 30_000);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
return buf.toString('base64');
|
||||
} catch (error) {
|
||||
log.warn(`[Ollama] Failed to download image ${url.slice(0, 100)}: ${(error as Error).message}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async toNativeRequest(request: MetonaRequest): Promise<Record<string, unknown>> {
|
||||
const messages: Record<string, unknown>[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: [
|
||||
@@ -420,20 +445,34 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
request.systemPrompt.dynamicReminders,
|
||||
].filter(Boolean).join('\n\n'),
|
||||
},
|
||||
...request.messages.filter((m) => m.role !== 'system').map((m) => {
|
||||
];
|
||||
|
||||
// #2 修复: 改为 for 循环以支持 async 图片下载(map 回调无法 await)
|
||||
for (const m of request.messages) {
|
||||
if (m.role === 'system') continue;
|
||||
// C-6 修复: Ollama API 不支持 null content,assistant 仅有 tool_calls 时转为空字符串
|
||||
const msg: Record<string, unknown> = { role: m.role, content: m.content ?? '' };
|
||||
// Ollama 图片使用 images 字段(纯 base64 数组,不含 data: 前缀)
|
||||
if (m.images?.length) {
|
||||
msg.images = m.images.map((img) => {
|
||||
// #2 修复: 支持公网 URL 图片,下载后转为纯 base64
|
||||
// 之前直接将 URL 字符串传给 Ollama,导致 base64 解码错误
|
||||
const resolvedImages: string[] = [];
|
||||
for (const img of m.images) {
|
||||
const url = img.url;
|
||||
// data:image/png;base64,iVBOR... → iVBOR...
|
||||
if (url.startsWith('data:')) {
|
||||
// data:image/png;base64,iVBOR... → iVBOR...
|
||||
const base64Part = url.split(',')[1];
|
||||
return base64Part ?? url;
|
||||
resolvedImages.push(base64Part ?? url);
|
||||
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
// #2 修复: 公网 URL → 下载 → 纯 base64
|
||||
const base64 = await this.resolveImageToBase64(url);
|
||||
if (base64) resolvedImages.push(base64);
|
||||
} else {
|
||||
// 已是纯 base64 字符串(无 data: 前缀)
|
||||
resolvedImages.push(url);
|
||||
}
|
||||
return url;
|
||||
});
|
||||
}
|
||||
msg.images = resolvedImages;
|
||||
}
|
||||
// 工具结果
|
||||
if (m.role === 'tool' && m.toolResult) {
|
||||
@@ -455,9 +494,8 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
if (m.role === 'assistant' && m.reasoningContent) {
|
||||
(msg as Record<string, unknown>).reasoning_content = m.reasoningContent;
|
||||
}
|
||||
return msg;
|
||||
}),
|
||||
];
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.config.defaultModel,
|
||||
|
||||
Reference in New Issue
Block a user