feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
This commit is contained in:
@@ -23,15 +23,22 @@
|
||||
*/
|
||||
|
||||
import { BaseAdapter } from './base-adapter';
|
||||
import log from 'electron-log';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
|
||||
import { MetonaFinishReason, MetonaStreamEventType } from '../types';
|
||||
import type { MetonaModelInfo } from '../types/metona-adapter';
|
||||
|
||||
export class OllamaAdapter extends BaseAdapter {
|
||||
override readonly provider: string = 'ollama';
|
||||
// H-2 修复: provider → providerId(规范要求)
|
||||
override readonly providerId: string = 'ollama';
|
||||
readonly supportedModels = ['qwen3:latest', 'gemma3:latest', 'deepseek-r1:latest'];
|
||||
readonly supportsToolCalling = true;
|
||||
readonly supportsThinking = true;
|
||||
|
||||
// H-2 修复: Ollama 本地模型默认上下文窗口(可由 options.num_ctx 覆盖)
|
||||
private static readonly DEFAULT_CONTEXT_WINDOW = 4096;
|
||||
|
||||
private baseURL: string;
|
||||
|
||||
constructor(config: ConstructorParameters<typeof BaseAdapter>[0]) {
|
||||
@@ -41,14 +48,16 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// ===== POST /api/chat =====
|
||||
|
||||
async chat(request: MetonaRequest): Promise<MetonaResponse> {
|
||||
// H-2 修复: chat → send(规范要求)
|
||||
async send(request: MetonaRequest): Promise<MetonaResponse> {
|
||||
const nativeRequest = this.toNativeRequest(request);
|
||||
|
||||
const response = await fetch(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: false }),
|
||||
signal: AbortSignal.timeout(this.config.timeoutMs ?? 300_000),
|
||||
// C-2 修复: 使用合并后的 signal(外部 abort + timeout)
|
||||
signal: this.getFetchSignal(this.config.timeoutMs ?? 300_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -59,14 +68,16 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
return this.toMetonaResponse(data, request.meta.requestId, request.meta.iteration);
|
||||
}
|
||||
|
||||
async *chatStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
|
||||
// H-2 修复: chatStream → sendStream(规范要求)
|
||||
async *sendStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
|
||||
const nativeRequest = this.toNativeRequest(request);
|
||||
|
||||
const response = await fetch(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: true }),
|
||||
signal: AbortSignal.timeout(this.config.timeoutMs ?? 300_000),
|
||||
// C-2 修复: 使用合并后的 signal(外部 abort + timeout)
|
||||
signal: this.getFetchSignal(this.config.timeoutMs ?? 300_000),
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
@@ -133,7 +144,8 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
seq: seq++,
|
||||
timestamp: Date.now(),
|
||||
toolCall: {
|
||||
id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
// L-9 修复: 统一使用 nanoid 生成工具调用 ID(与 sse-stream.ts 一致)
|
||||
id: `tc_${nanoid(8)}`,
|
||||
name: tc.function?.name ?? '',
|
||||
args: parsedArgs,
|
||||
iteration: request.meta.iteration,
|
||||
@@ -250,17 +262,55 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// ===== GET /api/tags =====
|
||||
|
||||
async listModels(): Promise<string[]> {
|
||||
/**
|
||||
* H-2 修复: 返回 MetonaModelInfo[](规范要求)
|
||||
*
|
||||
* Ollama /api/tags 返回模型列表含详细信息(name, size, details),
|
||||
* 转换为 MetonaModelInfo 并补充默认元数据。
|
||||
*/
|
||||
async listModels(): Promise<MetonaModelInfo[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/api/tags`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) return this.supportedModels;
|
||||
const data = await response.json() as { models?: Array<{ name: string }> };
|
||||
return data.models?.map((m) => m.name) ?? this.supportedModels;
|
||||
if (response.ok) {
|
||||
const data = await response.json() as {
|
||||
models?: Array<{
|
||||
name: string;
|
||||
size?: number;
|
||||
details?: { parameter_size?: string; quantization_level?: string; family?: string };
|
||||
}>;
|
||||
};
|
||||
if (data.models?.length) {
|
||||
return data.models.map((m) => ({
|
||||
id: m.name,
|
||||
name: m.name,
|
||||
// Ollama 模型上下文窗口由 options.num_ctx 决定,此处给保守值
|
||||
contextWindow: OllamaAdapter.DEFAULT_CONTEXT_WINDOW,
|
||||
supportsToolCalling: true, // Ollama 多数模型支持,具体能力需通过 /api/show 查询
|
||||
supportsThinking: true,
|
||||
description: m.details
|
||||
? `${m.details.family ?? 'unknown'} / ${m.details.parameter_size ?? '?'} / ${m.details.quantization_level ?? '?'}`
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return this.supportedModels;
|
||||
// API 不可用时降级
|
||||
}
|
||||
// 回退到 supportedModels
|
||||
return this.supportedModels.map((id) => ({ id }));
|
||||
}
|
||||
|
||||
/**
|
||||
* H-2 修复: 获取上下文窗口大小(规范要求)
|
||||
*
|
||||
* Ollama 上下文窗口由 options.num_ctx 决定(默认 4096),
|
||||
* Engine 应通过 MetonaRequest.params.contextLength 显式设置。
|
||||
* 此处返回默认值,供 Engine 在未指定时参考。
|
||||
*/
|
||||
override getContextWindow(): number {
|
||||
return OllamaAdapter.DEFAULT_CONTEXT_WINDOW;
|
||||
}
|
||||
|
||||
// ===== POST /api/show =====
|
||||
@@ -312,7 +362,10 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
try {
|
||||
const chunk = JSON.parse(line);
|
||||
onProgress?.({ status: chunk.status, completed: chunk.completed, total: chunk.total });
|
||||
} catch {}
|
||||
} catch {
|
||||
// L-3 修复: 添加日志便于诊断非标准行(如进度通知、空行等)
|
||||
log.debug('[Ollama] skipped non-JSON line during pull:', line.slice(0, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,7 +419,8 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
].filter(Boolean).join('\n\n'),
|
||||
},
|
||||
...request.messages.filter((m) => m.role !== 'system').map((m) => {
|
||||
const msg: Record<string, unknown> = { role: m.role, content: m.content };
|
||||
// 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) => {
|
||||
@@ -439,7 +493,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
return {
|
||||
meta: {
|
||||
requestId,
|
||||
provider: this.provider,
|
||||
provider: this.providerId,
|
||||
model: (data.model as string) ?? this.config.defaultModel,
|
||||
latencyMs: 0,
|
||||
timestamp: Date.now(),
|
||||
@@ -464,7 +518,8 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
args = {};
|
||||
}
|
||||
return {
|
||||
id: `tc_${Date.now()}_${i}`,
|
||||
// L-9 修复(审计补充): 非流式路径统一使用 nanoid,与流式路径(sendStream)保持一致
|
||||
id: `tc_${nanoid(8)}`,
|
||||
name: (fn?.name as string) ?? '',
|
||||
args,
|
||||
iteration,
|
||||
|
||||
Reference in New Issue
Block a user