第96篇 | HarmonyOS 异常合集:权限拒绝、网络失败、模型失败、相机失败
发布前最容易漏掉的是异常路径。双镜记忆相机串了相机、定位、相册、ARK、视频下载、系统分享和保险箱,只要其中一环失败,页面都应该给用户明确反馈。异常处理不是堆 catch,而是把错误变成可以继续操作的状态。
这一篇按四类异常整理:权限拒绝、网络/模型失败、文件下载和导出失败、系统分享失败。我们重点看服务层如何抛出可读错误,页面层如何把错误写回状态文案。
本篇目标
- 梳理服务层和页面层的错误边界。
- 确认 ARK 请求失败、空响应、网络异常都有明确错误。
- 检查图生图、图生视频、导出系统相册、系统分享的失败文案。
- 建立发布前异常测试矩阵。
对应源码位置
superImage/entry/src/main/ets/services/VolcengineArkService.etssuperImage/entry/src/main/ets/pages/Index.ets
服务层错误要保留上下文
requestJson 统一处理在线请求:非 2xx 状态抛出带状态码的错误,空响应也单独处理,传输失败再包装成 transport failed。这样页面拿到的不是模糊失败,而是能判断方向的错误。
服务层不直接弹 UI,这是边界。它负责把失败说清楚,页面负责决定给用户展示什么文案、保留哪些按钮。

异常处理的目标是让用户知道发生了什么,以及下一步能做什么
ts
private static async requestJson<T>(
url: string,
method: http.RequestMethod,
apiKey: string,
bodyText?: string
): Promise<ArkHttpJsonResponse<T>> {
const request = http.createHttp();
const header: ArkRequestHeaders = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
};
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 {
上传图片前先检查文件边界
在线图解会把本地图片转成 data URL。toDataUrl 里先读取文件大小,空文件直接报错,超过 10MB 也报错。这些检查能在请求发出前拦住明显失败,减少无意义网络调用。
这段代码也提醒我们:异常路径要尽量靠近问题发生的位置。文件为空就是文件问题,不应该等远端模型返回失败才提示。

图片上传前先检查空文件和 10MB 限制,减少无效请求
ts
private static toDataUrl(filePath: string): string {
const bytes = VolcengineArkService.readBinaryFile(filePath);
const base64Text = new util.Base64Helper().encodeToStringSync(bytes, util.Type.BASIC);
return `data:${VolcengineArkService.inferMimeType(filePath)};base64,${base64Text}`;
}
private static readBinaryFile(filePath: string): Uint8Array {
let file: fs.File | undefined = undefined;
try {
const stat = fs.statSync(filePath);
if (stat.size <= 0) {
throw new Error(`Image file is empty: ${filePath}`);
}
if (stat.size > VolcengineArkService.DATA_URL_IMAGE_LIMIT_BYTES) {
throw new Error(`Image exceeds 10 MB data-url limit: ${filePath}`);
}
file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const buffer = new ArrayBuffer(stat.size);
const bytesRead = fs.readSync(file.fd, buffer);
if (bytesRead <= 0) {
throw new Error(`Failed to read image bytes: ${filePath}`);
}
return new Uint8Array(buffer, 0, bytesRead);
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
throw new Error(`Failed to prepare image upload payload: ${message}`);
} finally {
模型失败要写成用户能读懂的文案
页面层会把部分模型权限问题转成更具体的文案,例如未开通 Seedream、没有图像生成模型权限、Seedance 参数不可用等。这比统一显示"调用失败"更利于用户排查。
对于训练营文章,异常文案也能帮助读者理解功能边界:不是所有失败都来自代码错误,账号权限、模型开通和网络状态都可能影响结果。

模型相关失败要区分 Key、选择记录、模型权限和参数错误
ts
if (this.arkApiKey.trim().length === 0) {
this.videoTaskStatusText = '请先配置火山方舟 API Key';
return;
}
const sourceRecords = this.getSelectedVideoRecords();
if (sourceRecords.length < 1) {
this.videoTaskStatusText = '请先选择照片';
return;
}
this.videoTaskBusy = true;
this.videoTaskStatusText = `正在提交 ${sourceRecords.length} 张照片的图生视频任务...`;
try {
const task = await VolcengineArkService.createVideoTask(
this.getAbilityContext(),
sourceRecords,
this.arkApiKey.trim()
);
this.videoTaskId = task.id;
this.videoUrl = task.videoUrl;
this.videoTaskStatusText = this.getVolcengineVideoRecordStatusText(task);
let syncedRecord = await this.syncVolcengineVideoManagerRecord(task, sourceRecords);
this.galleryMediaTab = 'video';
this.galleryViewMode = 'album';
const latestTask = await this.pollVolcengineVideoTask(task, sourceRecords);
this.videoUrl = latestTask.videoUrl;
this.videoTaskStatusText = this.getVolcengineVideoRecordStatusText(latestTask);
syncedRecord = await this.syncVolcengineVideoManagerRecord(latestTask, sourceRecords);
if (latestTask.status === 'succeeded' && latestTask.videoUrl.length > 0) {
this.galleryNoticeText = '';
this.regeneratingVideoManagerRecordId = '';
this.openVideoManagerRecordPreview(syncedRecord);
}
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
if (message.includes('ModelNotOpen')) {
this.videoTaskStatusText = '图生视频提交未成功,请确认方舟 API Key 与 Seedance 1.0 Pro Fast 权限后重试。';
} else if (message.includes('InvalidEndpointOrModel')) {
this.videoTaskStatusText = '图生视频模型参数不可用,已切换为 Seedance 1.0 Pro Fast 首帧模式,请重试。';
} else {
this.videoTaskStatusText = `智能介绍片提交失败:${message}`;
}
} finally {
导出和分享失败不能吞掉
导出系统相册和系统分享都是跨应用能力,失败原因可能来自文件不存在、系统取消、权限、目标应用异常。项目里会把错误写回 videoTaskStatusText 或对应状态,用户至少知道操作没有完成。
发布前异常测试可以故意清空视频路径、断网、取消系统弹窗、连续点击按钮。目标不是让所有异常都成功,而是每个异常都能被解释和恢复。

导出失败要回到页面状态,不能只在日志里出现
异常矩阵建议至少包含:拒绝相机权限、拒绝定位权限、ARK Key 为空、模型未开通、视频 URL 为空、系统分享取消、导出系统相册取消。
工程验收表
| 检查项 | 通过标准 |
|---|---|
| 服务层错误 | HTTP 状态码、空响应、传输失败都有明确错误。 |
| 文件边界 | 空文件、超大图片、空视频 URL 会提前失败。 |
| 用户文案 | 模型权限、Key 缺失、网络失败能被用户理解。 |
| 可恢复 | 失败后按钮状态恢复,用户可以修改条件后重试。 |
真机复测口令
先断网,再清空 ARK API Key,最后传入一个空视频 URL,分别触发图解、视频任务和导出流程。每一次失败都应该回到可读状态:用户知道哪里失败、能不能重试、下一步要改什么条件。
异常合集的重点是分层。服务层负责抛出带上下文的错误,页面层负责把错误翻译成用户文案,按钮状态负责恢复可操作。三层缺一层,用户看到的都会是模糊的"失败了"。
今日练习
- 模拟 HTTP 非 2xx 响应,确认
requestJson抛出的错误包含状态码。 - 清空 API Key 后点击图生视频,确认不会发起无意义请求。
- 取消系统分享面板,确认页面不会误提示分享成功。