第58篇|AI 失败态:网络失败、Key 缺失、模型失败如何提示
第58篇|AI 失败态:网络失败、Key 缺失、模型失败如何提示
第 58 篇收束第 12 天内容:AI 能力必须把失败态讲清楚。Key 缺失、登录未完成、网络失败、响应为空、模型未开通,它们都不应该只变成一句“失败了”。
项目把前置检查放在页面层,请求异常放在服务层,模型不可用时再尝试候选模型。这样用户看到的是明确提示,开发者排查时也能顺着层级定位。
这一篇继续围绕 21 天「智能相机开发实战」训练营展开。阅读时可以先看界面效果,再顺着函数名回到 DevEco Studio 定位实现,最后把成功态、取消态和失败态串成一个可复现闭环。
本篇目标
- 区分 Key 缺失、选择为空、登录未完成和网络失败。
- 理解
requestJson如何包装请求错误。 - 掌握视频模型候选重试的判断。
- 保证失败后 busy 状态能恢复。
对应源码位置
entry/src/main/ets/pages/Index.etsentry/src/main/ets/services/VolcengineArkService.ets
一、失败态先从前置检查开始
很多失败不需要真的发请求。没有选择照片、没有配置 Key、入口关闭、华为账号未准备好,这些都可以在页面层提前拦截。
提前拦截能减少无意义网络请求,也能给用户更具体的下一步,比如“请先选择照片”或“请先配置火山方舟 API Key”。

AI 能力区需要清楚展示可恢复失败态
二、页面层负责用户可见提示
generateSmartDescriptionsForMovieSelection 先检查是否可生成、是否有选中照片、是否配置 Key、是否完成身份准备。每个失败点都更新不同状态文案。
这种写法比统一 catch 后显示“生成失败”更友好,用户知道该补照片、填 Key,还是等待登录。

页面层在请求前处理 Key 缺失和选择为空
return this.aiSynthesisEntryVisible &&
this.hasMovieSelection() &&
!this.aiInsightBusy &&
!this.videoTaskBusy &&
!this.videoExportBusy &&
!this.systemShareBusy;
}
private async generateSmartDescriptionsForMovieSelection(): Promise<void> {
if (!this.canGenerateSmartDescriptionsFromSelection()) {
return;
}
const sourceRecords = this.getSelectedVideoRecords();
if (sourceRecords.length === 0) {
this.videoTaskStatusText = '请先选择照片';
return;
}
if (this.arkApiKey.trim().length === 0) {
this.videoTaskStatusText = '请先配置火山方舟 API Key';
return;
}
if (!await this.ensureHuaweiIdentityForAiSynthesis('movie')) {
return;
}
this.aiInsightBusy = true;
this.videoTaskStatusText = `正在生成 ${sourceRecords.length} 张照片的智能描述...`;
let nextRecords = this.galleryRecords.slice();
try {
for (let index = 0; index < sourceRecords.length; index += 1) {
const sourceRecord = sourceRecords[index];
const latestRecord = nextRecords.find((record: GalleryMoment) => record.id === sourceRecord.id) ?? sourceRecord;
this.videoTaskStatusText = `正在生成智能描述 ${index + 1}/${sourceRecords.length}`;
const insight = await VolcengineArkService.analyzeMoment(
this.getAbilityContext(),
latestRecord,
this.arkApiKey.trim()
);
nextRecords = nextRecords.map((record: GalleryMoment) => {
if (record.id !== sourceRecord.id) {
return record;
}
const nextRecord = this.buildAiReadyRecord(record, insight.aiCaption, insight.videoPrompt);
if (this.getRecordUserNote(record).length === 0) {
nextRecord.userNote = insight.aiCaption;
}
return nextRecord;
前置检查越清楚,真正进入服务层的请求就越干净。
三、请求层包装状态码、空响应和传输错误
requestJson 检查 HTTP 状态码、空响应和 JSON 解析前的响应文本。任何异常都会被包装成带上下文的错误。
页面层不需要知道底层是 500、空响应还是连接超时,它只展示服务层抛出的可读错误。开发者看日志时仍然能看到具体状态码和响应体。

requestJson 统一包装请求错误
};
const requestOptions: http.HttpRequestOptions = {
method: method,
header: header as Object,
expectDataType: http.HttpDataType.STRING,
readTimeout: 60000,
connectTimeout: 60000
};
if (bodyText) {
requestOptions.extraData = bodyText;
}
try {
const response = await request.request(url, requestOptions);
const responseText = typeof response.result === 'string'
? response.result
: JSON.stringify(response.result);
if ((response.responseCode as number) < 200 || (response.responseCode as number) >= 300) {
throw new Error(`Ark request failed (${response.responseCode}): ${responseText}`);
}
if (!responseText || responseText.length === 0) {
throw new Error('Ark response body is empty.');
}
const parsedData = JSON.parse(responseText) as T;
return {
data: parsedData,
rawText: responseText
};
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
throw new Error(`Ark request transport failed: ${message}`);
} finally {
request.destroy();
}
请求层的错误文案要对开发者有用,页面层的状态文案要对用户有用,两者不要混成一团。
四、模型不可用时可以尝试候选模型
视频生成模型可能因为未开通、端点不支持或模型名变化而失败。shouldRetryVideoModel 用关键词识别这类错误,允许项目尝试候选模型。
这不是为了掩盖问题,而是为了在可恢复的模型失败下继续给用户机会。真正不可恢复的错误则停止重试,把原因反馈给页面。

shouldRetryVideoModel 识别可重试的模型错误
private static buildVideoModelCandidates(videoModel: string): Array<string> {
const candidates: Array<string> = [];
VolcengineArkService.appendUniqueModel(candidates, VolcengineArkService.normalizeVideoModel(videoModel));
VolcengineArkService.appendUniqueModel(candidates, VolcengineArkService.DEFAULT_VIDEO_MODEL);
VolcengineArkService.appendUniqueModel(candidates, 'doubao-seedance-1-0-pro-fast-250610');
VolcengineArkService.appendUniqueModel(candidates, 'doubao-seedance-1-0-pro-250528');
VolcengineArkService.appendUniqueModel(candidates, 'doubao-seedance-1-0-lite-i2v-250428');
return candidates;
}
private static shouldRetryVideoModel(message: string): boolean {
const lowerMessage = message.toLowerCase();
return message.includes('ModelNotOpen') ||
message.includes('InvalidEndpointOrModel') ||
message.includes('模型') ||
message.includes('未开通') ||
message.includes('不存在') ||
message.includes('不支持') ||
第 58 篇的重点是分层:页面层拦截使用条件,服务层处理传输错误,模型层做候选重试。层级清楚,失败态就不会乱。
五、失败恢复后要回到稳定状态
失败态处理不能只看错误文案,还要看状态是否恢复。页面层在请求前会设置 busy 状态,请求结束后必须在 finally 中恢复。否则用户看到的不是一次失败,而是按钮一直不可用、页面一直“生成中”。
服务层兜底也要避免把技术响应写给用户。第 56 篇的 sanitizeProductText 可以作为失败态的下游保护:即使上游返回了模型元数据、JSON 片段或空响应,最终进入产品字段的也应该是用户能理解的短文案。

sanitizeProductText 过滤技术响应,避免异常内容进入产品文案
try {
for (let index = 0; index < sourceRecords.length; index += 1) {
const sourceRecord = sourceRecords[index];
const latestRecord = nextRecords.find((record: GalleryMoment) => record.id === sourceRecord.id) ?? sourceRecord;
this.videoTaskStatusText = `正在生成智能描述 ${index + 1}/${sourceRecords.length}`;
const insight = await VolcengineArkService.analyzeMoment(
this.getAbilityContext(),
latestRecord,
this.arkApiKey.trim()
);
nextRecords = nextRecords.map((record: GalleryMoment) => {
if (record.id !== sourceRecord.id) {
return record;
}
return this.buildAiReadyRecord(record, insight.aiCaption, insight.videoPrompt);
});
}
} finally {
this.aiInsightBusy = false;
}
这段流程说明了失败态的另一半:无论成功还是失败,忙碌状态都要释放。否则下一次点击、下一次重试和下一篇训练营里的验证步骤都会被旧状态挡住。
工程检查清单
- Key 缺失和照片未选择在页面层提前提示。
- 请求层统一包装状态码、空响应和传输错误。
- 模型未开通或不支持时允许候选重试。
- 失败后所有 busy 状态都能恢复。
- 用户文案和开发者日志各自服务不同对象。
今日练习
- 删除本地 Key 后触发智能描述,记录页面提示。
- 模拟空响应,推演
requestJson会抛出什么错误。 - 为
shouldRetryVideoModel补充一个你遇到过的模型错误关键词。
训练营后面的文章会继续按“真实页面效果 -> 源码定位 -> 状态闭环 -> 可验证结果”的节奏推进。每一篇都尽量让你能拿着代码直接回到项目里复现,而不是只停留在概念说明。
更多推荐



所有评论(0)