feat: v0.3.17 修复多项问题(sandbox/全局配置层/继承选项/配置刷新/附件提示/content_filter 错误处理)
- fix: sandbox: true → false(ESM preload 不兼容 sandbox 导致 window.metona 不可用,选择文件夹按钮无反应) - feat: 全局配置层(GlobalConfigService)— LLM/Agent 等配置跨工作空间共享,切换工作空间后不再丢失 - 读取时工作空间 DB 空值或默认值回退到全局 JSON - 写入时双写(DB + 全局 JSON) - 首次启动迁移旧配置到全局层 - fix: 工作空间继承选项显示条件放宽 + push 前校验避免覆盖目标已有文件(SOUL.md/agent.db) - fix: config:changed IPC 广播 + 前端监听,修改 Agent 配置后详情栏 maxIterations 实时刷新 - feat: 上传图片/文件时在 System Prompt 注入附件提示,避免 AI 在工作空间查找用户上传的文件 - feat: content_filter 错误识别(ContentFilterError)+ 友好提示,替代原始 JSON 错误体透传 - fix: JSDoc 注释中 llm.* / agent.* / onboarding.completed 中的 */ 被解析为注释结束符
This commit is contained in:
@@ -18,6 +18,24 @@ import type {
|
||||
import { MetonaErrorCode } from '../types';
|
||||
import type { MetonaModelInfo } from '../types/metona-adapter';
|
||||
|
||||
/**
|
||||
* 内容审核错误 — Provider 的安全过滤策略触发的错误
|
||||
*
|
||||
* 当 Provider(如 MiMo)的 API 返回 content_filter 错误码时抛出。
|
||||
* Engine 识别此错误后映射为 MetonaErrorCode.CONTENT_FILTERED,
|
||||
* 前端显示友好提示而非原始 JSON 错误体。
|
||||
*/
|
||||
export class ContentFilterError extends Error {
|
||||
/** Provider 返回的原始错误消息(如 "The request was rejected because it was considered high risk") */
|
||||
readonly providerMessage: string;
|
||||
|
||||
constructor(providerMessage: string, context: string) {
|
||||
super(`${context}: 内容被 Provider 安全审核拦截 - ${providerMessage}`);
|
||||
this.name = 'ContentFilterError';
|
||||
this.providerMessage = providerMessage;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
// H-2 修复: provider → providerId(规范要求)
|
||||
abstract readonly providerId: string;
|
||||
@@ -157,10 +175,35 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
/**
|
||||
* P1-4 修复: 构造带 HTTP status 属性的 Error 并抛出
|
||||
* engine.isRetryableError 依赖 error.status 判断是否可重试(429/5xx)
|
||||
*
|
||||
* v0.3.17: 解析 Provider 返回的 JSON 错误体,识别 content_filter 错误码,
|
||||
* 抛出 ContentFilterError 让 Engine 映射为 CONTENT_FILTERED 错误码。
|
||||
*/
|
||||
protected async throwHttpError(response: Response, context: string): Promise<never> {
|
||||
let errorBody = '';
|
||||
try { errorBody = await response.text(); } catch { /* body 可能已消费或为 null */ }
|
||||
|
||||
// v0.3.17: 解析 JSON 错误体,识别 content_filter
|
||||
if (errorBody) {
|
||||
try {
|
||||
const parsed = JSON.parse(errorBody);
|
||||
// 兼容 OpenAI 格式 {error: {code, message}} 和 MiMo/其他格式
|
||||
const errObj = parsed?.error ?? parsed;
|
||||
const errorCode = errObj?.code ?? errObj?.type ?? '';
|
||||
const errorMessage = errObj?.message ?? '';
|
||||
|
||||
if (typeof errorCode === 'string' && errorCode.toLowerCase().includes('content_filter')) {
|
||||
const providerMsg = errorMessage || '内容触发安全过滤策略';
|
||||
const cfError = new ContentFilterError(providerMsg, context);
|
||||
(cfError as ContentFilterError & { status: number }).status = response.status;
|
||||
throw cfError;
|
||||
}
|
||||
} catch (parseErr) {
|
||||
// JSON 解析失败(非 JSON 响应体),走原逻辑
|
||||
if (parseErr instanceof ContentFilterError) throw parseErr;
|
||||
}
|
||||
}
|
||||
|
||||
const error = new Error(`${context}: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ''}`);
|
||||
(error as Error & { status: number }).status = response.status;
|
||||
throw error;
|
||||
@@ -171,6 +214,16 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
|
||||
*/
|
||||
protected mapError(error: unknown): MetonaError {
|
||||
if (error instanceof Error) {
|
||||
// v0.3.17: 优先识别 ContentFilterError
|
||||
if (error instanceof ContentFilterError) {
|
||||
return {
|
||||
code: MetonaErrorCode.CONTENT_FILTERED,
|
||||
message: '内容被 Provider 安全审核拦截,请修改图片或文本后重试',
|
||||
provider: this.providerId,
|
||||
retryable: false,
|
||||
};
|
||||
}
|
||||
|
||||
const msg = error.message.toLowerCase();
|
||||
|
||||
if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) {
|
||||
|
||||
Reference in New Issue
Block a user