第58篇|AI 失败态:网络失败、Key 缺失、模型失败如何提示

第 58 篇收束第 12 天内容:AI 能力必须把失败态讲清楚。Key 缺失、登录未完成、网络失败、响应为空、模型未开通,它们都不应该只变成一句“失败了”。

项目把前置检查放在页面层,请求异常放在服务层,模型不可用时再尝试候选模型。这样用户看到的是明确提示,开发者排查时也能顺着层级定位。

这一篇继续围绕 21 天「智能相机开发实战」训练营展开。阅读时可以先看界面效果,再顺着函数名回到 DevEco Studio 定位实现,最后把成功态、取消态和失败态串成一个可复现闭环。

本篇目标

  • 区分 Key 缺失、选择为空、登录未完成和网络失败。
  • 理解 requestJson 如何包装请求错误。
  • 掌握视频模型候选重试的判断。
  • 保证失败后 busy 状态能恢复。

对应源码位置

  • entry/src/main/ets/pages/Index.ets
  • entry/src/main/ets/services/VolcengineArkService.ets

一、失败态先从前置检查开始

很多失败不需要真的发请求。没有选择照片、没有配置 Key、入口关闭、华为账号未准备好,这些都可以在页面层提前拦截。

提前拦截能减少无意义网络请求,也能给用户更具体的下一步,比如“请先选择照片”或“请先配置火山方舟 API Key”。

AI 能力区需要清楚展示可恢复失败态

AI 能力区需要清楚展示可恢复失败态

二、页面层负责用户可见提示

generateSmartDescriptionsForMovieSelection 先检查是否可生成、是否有选中照片、是否配置 Key、是否完成身份准备。每个失败点都更新不同状态文案。

这种写法比统一 catch 后显示“生成失败”更友好,用户知道该补照片、填 Key,还是等待登录。

页面层在请求前处理 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 统一包装请求错误

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 识别可重试的模型错误

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 过滤技术响应,避免异常内容进入产品文案

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 状态都能恢复。
  • 用户文案和开发者日志各自服务不同对象。

今日练习

  1. 删除本地 Key 后触发智能描述,记录页面提示。
  2. 模拟空响应,推演 requestJson 会抛出什么错误。
  3. shouldRetryVideoModel 补充一个你遇到过的模型错误关键词。

训练营后面的文章会继续按“真实页面效果 -> 源码定位 -> 状态闭环 -> 可验证结果”的节奏推进。每一篇都尽量让你能拿着代码直接回到项目里复现,而不是只停留在概念说明。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐