feat: v0.7.3 成本收口 · 状态一致 · 死账清理 — Prompt Cache 根治 + SSRF DNS Pinning + 87 用例扩充全量回归
P1 修复面收口: Prompt Cache 根治(日期/记忆/附件三类易变内容出 system 入用户消息 前置块 user-context.ts, system 跨 run 字节级稳定; Anthropic system 块数组化 + cache_control ephemeral 断言, DeepSeek 自动缓存前缀命中 — 多轮对话输入 token 成本降数量级); 编辑重发/重新生成幽灵 Trace 双侧根治(DB truncateMessagesAfter 同步过滤 metadata.traceSteps + 前端 trimTraceStepsByAnchor 镜像, 严格小于锚点 时间戳, 同毫秒等值判废); sessions:deleteMessage 死通道全链路删除(渲染层零调用 + message_count 漂移面); Ollama vision 能力门控全链路(MetonaModelInfo .supportsVision 贯穿 adapter/IPC/store/UI, model-capabilities.ts 三道判定纯函数, 未知保守放行); 记忆固化节流(consolidation-policy 纯函数: 总开关 + 内容门控 [回答>=200字符或存在成功工具调用] + 会话级 10 分钟频率窗口, 三 memory.* 配置键) P2 安全纵深: SSRF DNS Pinning 关闭 rebinding 窗口(ssrf-guard 重构 resolvePublicAddresses 单源; ssrf-dispatcher 以 undici Agent.connect.lookup 钉死校验 IP, TLS SNI 保持原域名, 一次性 dispatcher 用后即毁; 代理激活显式 退化为仅入口校验); web_fetch 重写手动逐跳重定向循环(每跳先校验后连接, 替代 redirect:follow 内核跟跳的中间跳裸奔, 上限 5 跳); http_request 换用 pinned fetch; web_search 可达性预检加固(私有 URL 零请求 + 不跟跳, 3xx 视为 可达); Agent 浏览器 CORS 通配收紧为 Origin 回显 + Vary: Origin; ConfirmationHook.forgetSession 会话终态清理(会话删除/abort 联动/SubAgent 终结三处接线, 根治 rememberedDecisions 泄漏) P3 架构还债: agent.enableReflection 死配置全链路接线(main→shared→引擎→ Orchestrator→设置开关, REFLECTING 状态真实可达); AgentLoopConfig.timeoutMs 死字段删除; MemoryManager.cleanupExpired 挂入健康检查周期(expires_at 回收 管道真实化); buildSafeEnv 收敛 utils/safe-env.ts 单源(run_command 与 MCP stdio 共用, 终结双实现漂移); Trace 生命周期治理(metadata 只保留最近 20 个 run — keepRecentRuns 纯函数; JSONL 录制启动自动清理保留 200 个 + 设置页 手动清理); SLO/健康快照可视化(app:healthSnapshot IPC + 设置页只读卡片 + 审计链一键校验) P4 能力演进: 会话标题 LLM 自动生成(TitleGenerator — 每会话幂等/并发重入复用 同一 Promise/自定义标题不覆盖/失败静默回退, Sidebar 经 config:changed 实时 刷新); MCP 自动重连(5s/15s/60s 退避最多 3 次, reconnecting 状态机, teardownConnection 内部拆除保留簿记 — 用户断开/开关关闭即时取消, 设置页 显示第 N/3 次); 死循环检测 ABAB 乒乓模式(最近4轮 A→B→A→B 交替判定, 补齐 docs 第五章"两状态反复切换"检测契约); i18n 第三阶段(ChatInput/LLMSettings/ OnboardingWizard/MemoryViewer 主链路文案出层, zh-CN + en-US 双字典补齐) 测试: 737 → 824 用例(+87, 新增 8 个测试文件 + 扩展 3 个)。新覆盖: user-context 分组/空值收缩/拼接契约、context-builder 字节级稳定性、Anthropic cache_control 四态、consolidation-policy 九路判定矩阵、ssrf-dispatcher(pinned lookup/重定向 解析/IP 校验)、forget-session 会话隔离、trace-lifecycle run 淘汰、 trace-trim 严格小于边界、safe-env 净化矩阵、mcp-reconnect 退避状态机 (fake timers)、title-generator 并发重入、SQLite 侧 truncate×TRACE 联动 (Electron ABI)。测试驱动修复: GIT_*/ 注释终止块注释、重连计数被自身重试 前置断开重置(拆 teardownConnection 保留簿记)、TitleGenerator 幂等占位与 并发去重的检查顺序竞态(去重先于幂等) 版本: 0.7.3; README 同步(配置表新增 agent.enableReflection/memory.*/mcp.autoReconnect) 回归: typecheck 双端 0 错误; ESLint 0/0; 系统 Node 771 通过 53 跳过 (better-sqlite3 ABI); Electron ABI 全量 824/824 零跳过
This commit is contained in:
@@ -59,17 +59,21 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
const nativeRequest = await this.toNativeRequest(request);
|
||||
|
||||
// #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 }),
|
||||
}, this.config.timeoutMs ?? 300_000);
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseURL}/api/chat`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: false }),
|
||||
},
|
||||
this.config.timeoutMs ?? 300_000,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.throwHttpError(response, 'Ollama API error');
|
||||
}
|
||||
|
||||
const data = await response.json() as Record<string, unknown>;
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
return this.toMetonaResponse(data, request.meta.requestId, request.meta.iteration);
|
||||
}
|
||||
|
||||
@@ -79,11 +83,15 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
const nativeRequest = await this.toNativeRequest(request);
|
||||
|
||||
// #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 }),
|
||||
}, this.config.timeoutMs ?? 300_000);
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseURL}/api/chat`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...nativeRequest, stream: true }),
|
||||
},
|
||||
this.config.timeoutMs ?? 300_000,
|
||||
);
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
await this.throwHttpError(response, 'Ollama stream error');
|
||||
@@ -209,7 +217,10 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
}
|
||||
} catch (parseErr) {
|
||||
// P2-8 修复: 与 sse-stream.ts 一致,记录解析失败行便于诊断
|
||||
log.warn(`[Ollama] Failed to parse NDJSON line: ${(parseErr as Error).message}`, trimmed.slice(0, 200));
|
||||
log.warn(
|
||||
`[Ollama] Failed to parse NDJSON line: ${(parseErr as Error).message}`,
|
||||
trimmed.slice(0, 200),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +250,13 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
format?: string | object;
|
||||
images?: string[];
|
||||
options?: Record<string, unknown>;
|
||||
}): Promise<{ response: string; thinking?: string; done: boolean; totalDuration: number; evalCount: number }> {
|
||||
}): Promise<{
|
||||
response: string;
|
||||
thinking?: string;
|
||||
done: boolean;
|
||||
totalDuration: number;
|
||||
evalCount: number;
|
||||
}> {
|
||||
const response = await fetch(`${this.baseURL}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -248,9 +265,12 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Ollama generate error: ${response.status}`);
|
||||
const data = await response.json() as {
|
||||
response?: string; thinking?: string; done?: boolean;
|
||||
total_duration?: number; eval_count?: number;
|
||||
const data = (await response.json()) as {
|
||||
response?: string;
|
||||
thinking?: string;
|
||||
done?: boolean;
|
||||
total_duration?: number;
|
||||
eval_count?: number;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -277,7 +297,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Ollama embed error: ${response.status}`);
|
||||
const data = await response.json() as { embeddings?: number[][]; total_duration?: number };
|
||||
const data = (await response.json()) as { embeddings?: number[][]; total_duration?: number };
|
||||
|
||||
return {
|
||||
embeddings: data.embeddings ?? [],
|
||||
@@ -299,7 +319,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json() as {
|
||||
const data = (await response.json()) as {
|
||||
models?: Array<{
|
||||
name: string;
|
||||
size?: number;
|
||||
@@ -309,6 +329,8 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
if (data.models?.length) {
|
||||
// v0.6.4 P4-1: 能力标志改为逐模型 /api/show 实测探测;单个探测失败
|
||||
// 该模型回退保守 true(不可用时行为与旧实现一致,fail-open 保可用性)
|
||||
// v0.7.3 P1-4: supportsVision 随探测结果透出(undefined = 未知 → 前端保守放行),
|
||||
// 供上传入口拒绝不支持图片的本地语言模型
|
||||
const enriched = await Promise.all(
|
||||
data.models.map(async (m) => {
|
||||
const caps = await this.probeCapabilities(m.name);
|
||||
@@ -319,6 +341,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
contextWindow: OllamaAdapter.DEFAULT_CONTEXT_WINDOW,
|
||||
supportsToolCalling: caps ? caps.supportsTools : true,
|
||||
supportsThinking: caps ? caps.supportsThinking : true,
|
||||
supportsVision: caps ? caps.supportsVision : undefined,
|
||||
description: m.details
|
||||
? `${m.details.family ?? 'unknown'} / ${m.details.parameter_size ?? '?'} / ${m.details.quantization_level ?? '?'}`
|
||||
: undefined,
|
||||
@@ -403,7 +426,9 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// ===== POST /api/show =====
|
||||
|
||||
async showModel(model: string): Promise<{ parameters: string; template: string; capabilities: string[] } | null> {
|
||||
async showModel(
|
||||
model: string,
|
||||
): Promise<{ parameters: string; template: string; capabilities: string[] } | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/api/show`, {
|
||||
method: 'POST',
|
||||
@@ -412,7 +437,11 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json() as { parameters?: string; template?: string; capabilities?: string[] };
|
||||
const data = (await response.json()) as {
|
||||
parameters?: string;
|
||||
template?: string;
|
||||
capabilities?: string[];
|
||||
};
|
||||
return {
|
||||
parameters: data.parameters ?? '',
|
||||
template: data.template ?? '',
|
||||
@@ -470,13 +499,22 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// ===== GET /api/ps =====
|
||||
|
||||
async listRunning(): Promise<Array<{ name: string; size: number; sizeVram: number; contextLength: number }>> {
|
||||
async listRunning(): Promise<
|
||||
Array<{ name: string; size: number; sizeVram: number; contextLength: number }>
|
||||
> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/api/ps`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json() as { models?: Array<{ name: string; size?: number; size_vram?: number; context_length?: number }> };
|
||||
const data = (await response.json()) as {
|
||||
models?: Array<{
|
||||
name: string;
|
||||
size?: number;
|
||||
size_vram?: number;
|
||||
context_length?: number;
|
||||
}>;
|
||||
};
|
||||
return (data.models ?? []).map((m) => ({
|
||||
name: m.name ?? '',
|
||||
size: m.size ?? 0,
|
||||
@@ -496,7 +534,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
if (!response.ok) return 'unknown';
|
||||
const data = await response.json() as { version?: string };
|
||||
const data = (await response.json()) as { version?: string };
|
||||
return data.version ?? 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
@@ -525,7 +563,9 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
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}`);
|
||||
log.warn(
|
||||
`[Ollama] Failed to download image ${url.slice(0, 100)}: ${(error as Error).message}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -539,58 +579,60 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
request.systemPrompt.outputConstraints,
|
||||
request.systemPrompt.safetyGuidelines,
|
||||
request.systemPrompt.dynamicReminders,
|
||||
].filter(Boolean).join('\n\n'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n'),
|
||||
},
|
||||
];
|
||||
|
||||
// #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) {
|
||||
// #2 修复: 支持公网 URL 图片,下载后转为纯 base64
|
||||
// 之前直接将 URL 字符串传给 Ollama,导致 base64 解码错误
|
||||
const resolvedImages: string[] = [];
|
||||
for (const img of m.images) {
|
||||
const url = img.url;
|
||||
if (url.startsWith('data:')) {
|
||||
// data:image/png;base64,iVBOR... → iVBOR...
|
||||
const base64Part = url.split(',')[1];
|
||||
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);
|
||||
}
|
||||
// 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) {
|
||||
// #2 修复: 支持公网 URL 图片,下载后转为纯 base64
|
||||
// 之前直接将 URL 字符串传给 Ollama,导致 base64 解码错误
|
||||
const resolvedImages: string[] = [];
|
||||
for (const img of m.images) {
|
||||
const url = img.url;
|
||||
if (url.startsWith('data:')) {
|
||||
// data:image/png;base64,iVBOR... → iVBOR...
|
||||
const base64Part = url.split(',')[1];
|
||||
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);
|
||||
}
|
||||
msg.images = resolvedImages;
|
||||
}
|
||||
// 工具结果
|
||||
if (m.role === 'tool' && m.toolResult) {
|
||||
msg.tool_call_id = m.toolResult.toolCallId;
|
||||
// CE-2 修复: 工具失败时 result 为 null,优先用 error 字段作为 content
|
||||
msg.content = m.toolResult.error
|
||||
? m.toolResult.error
|
||||
: (typeof m.toolResult.result === 'string'
|
||||
? m.toolResult.result
|
||||
: JSON.stringify(m.toolResult.result));
|
||||
}
|
||||
// assistant 工具调用(Ollama REST API 要求 arguments 为 JSON 字符串)
|
||||
if (m.role === 'assistant' && m.toolCalls?.length) {
|
||||
msg.tool_calls = m.toolCalls.map((tc) => ({
|
||||
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
||||
}));
|
||||
}
|
||||
// 推理内容回传(保持多轮推理链完整)
|
||||
if (m.role === 'assistant' && m.reasoningContent) {
|
||||
(msg as Record<string, unknown>).reasoning_content = m.reasoningContent;
|
||||
}
|
||||
messages.push(msg);
|
||||
msg.images = resolvedImages;
|
||||
}
|
||||
// 工具结果
|
||||
if (m.role === 'tool' && m.toolResult) {
|
||||
msg.tool_call_id = m.toolResult.toolCallId;
|
||||
// CE-2 修复: 工具失败时 result 为 null,优先用 error 字段作为 content
|
||||
msg.content = m.toolResult.error
|
||||
? m.toolResult.error
|
||||
: typeof m.toolResult.result === 'string'
|
||||
? m.toolResult.result
|
||||
: JSON.stringify(m.toolResult.result);
|
||||
}
|
||||
// assistant 工具调用(Ollama REST API 要求 arguments 为 JSON 字符串)
|
||||
if (m.role === 'assistant' && m.toolCalls?.length) {
|
||||
msg.tool_calls = m.toolCalls.map((tc) => ({
|
||||
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
||||
}));
|
||||
}
|
||||
// 推理内容回传(保持多轮推理链完整)
|
||||
if (m.role === 'assistant' && m.reasoningContent) {
|
||||
(msg as Record<string, unknown>).reasoning_content = m.reasoningContent;
|
||||
}
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
@@ -619,14 +661,23 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
|
||||
// Thinking 模式
|
||||
if (request.params.thinkingEnabled) {
|
||||
const effortMap: Record<string, string | boolean> = { low: 'low', medium: 'medium', high: 'high', max: true };
|
||||
const effortMap: Record<string, string | boolean> = {
|
||||
low: 'low',
|
||||
medium: 'medium',
|
||||
high: 'high',
|
||||
max: true,
|
||||
};
|
||||
body.think = effortMap[request.params.thinkingEffort ?? 'high'] ?? true;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private toMetonaResponse(data: Record<string, unknown>, requestId: string, iteration: number = 0): MetonaResponse {
|
||||
private toMetonaResponse(
|
||||
data: Record<string, unknown>,
|
||||
requestId: string,
|
||||
iteration: number = 0,
|
||||
): MetonaResponse {
|
||||
const message = data.message as Record<string, unknown> | undefined;
|
||||
const toolCalls = message?.tool_calls as Array<Record<string, unknown>> | undefined;
|
||||
return {
|
||||
@@ -638,11 +689,14 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
timestamp: Date.now(),
|
||||
perfStats: {
|
||||
loadDurationMs: data.load_duration ? (data.load_duration as number) / 1e6 : undefined,
|
||||
promptEvalDurationMs: data.prompt_eval_duration ? (data.prompt_eval_duration as number) / 1e6 : undefined,
|
||||
evalDurationMs: data.eval_duration ? (data.eval_duration as number) / 1e6 : undefined,
|
||||
tokensPerSecond: data.eval_count && data.eval_duration
|
||||
? ((data.eval_count as number) / ((data.eval_duration as number) / 1e9))
|
||||
promptEvalDurationMs: data.prompt_eval_duration
|
||||
? (data.prompt_eval_duration as number) / 1e6
|
||||
: undefined,
|
||||
evalDurationMs: data.eval_duration ? (data.eval_duration as number) / 1e6 : undefined,
|
||||
tokensPerSecond:
|
||||
data.eval_count && data.eval_duration
|
||||
? (data.eval_count as number) / ((data.eval_duration as number) / 1e9)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
content: (message?.content as string) ?? '',
|
||||
@@ -652,7 +706,10 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
const rawArgs = fn?.arguments;
|
||||
let args: Record<string, unknown> = {};
|
||||
try {
|
||||
args = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : (rawArgs as Record<string, unknown>) ?? {};
|
||||
args =
|
||||
typeof rawArgs === 'string'
|
||||
? JSON.parse(rawArgs)
|
||||
: ((rawArgs as Record<string, unknown>) ?? {});
|
||||
} catch (parseErr) {
|
||||
// v0.6.4: 非流式路径截断自愈对齐 —— 原 catch 静默降级 {},与流式修复后的
|
||||
// 行为不一致。统一转为 _truncatedArguments 错误参数。
|
||||
@@ -677,7 +734,10 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
outputTokens: (data.eval_count as number) ?? 0,
|
||||
totalTokens: ((data.prompt_eval_count as number) ?? 0) + ((data.eval_count as number) ?? 0),
|
||||
},
|
||||
finishReason: mapOllamaDoneReason(data.done_reason as string | undefined, !!message?.tool_calls),
|
||||
finishReason: mapOllamaDoneReason(
|
||||
data.done_reason as string | undefined,
|
||||
!!message?.tool_calls,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -693,10 +753,15 @@ function mapOllamaDoneReason(
|
||||
): MetonaFinishReason {
|
||||
if (hasToolCalls) return MetonaFinishReason.TOOL_CALLS;
|
||||
switch (reason) {
|
||||
case 'stop': return MetonaFinishReason.STOP;
|
||||
case 'length': return MetonaFinishReason.LENGTH;
|
||||
case 'load': return MetonaFinishReason.STOP; // 冷启动加载完成,非错误
|
||||
case 'unload': return MetonaFinishReason.STOP;
|
||||
default: return MetonaFinishReason.STOP;
|
||||
case 'stop':
|
||||
return MetonaFinishReason.STOP;
|
||||
case 'length':
|
||||
return MetonaFinishReason.LENGTH;
|
||||
case 'load':
|
||||
return MetonaFinishReason.STOP; // 冷启动加载完成,非错误
|
||||
case 'unload':
|
||||
return MetonaFinishReason.STOP;
|
||||
default:
|
||||
return MetonaFinishReason.STOP;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user