0.0.7:CalDAV 地址自动发现 + 人话错误提示 + 只读公开日历

背景:用户反馈(Nextcloud/内网)——DAVx5 用 CalDAV 地址能完整读取,换本应用
读不出日历;用"分享链接"绑定时提示服务器返回 405。

定位:不是协议问题,而是"填入的 URL 不是 CalDAV 端点"。
  ① 我们的发现=PROPFIND+Depth:1 只认 resourcetype 含 calendar 的集合;填 DAV 根
     /remote.php/dav 只能看到 principals/calendars/addressbooks → "未发现日历本"。
  ② 分享链接(/apps/calendar/p/<token>、/s/<token>)是网页路由 → PROPFIND 405。
  ③ 全工程此前无 .well-known 自动发现(grep 无命中)→ 必须手填完整路径,
     而 DAVx5 内置自动发现,故"DAVx5 能用、我们不能"。

实现(新增 common/DavDiscovery.ets):
1. .well-known/caldav 自动发现:207/200 直接用;301/302/307/308/401 读 Location
2. 智能补全:按 Nextcloud/ownCloud/Radicale/Baikal 等常见路径生成候选并逐个探测,
   保留子目录前缀(填 https://h/nextcloud → /nextcloud/remote.php/dav/calendars/<u>/)
3. 分享链接识别:/apps/calendar/p/、/s/、/index.php/s/ 直接拦下并给改法
4. OPTIONS 能力探测:读 DAV/Allow 头,区分"不是 DAV 端点"与"服务端挡了方法"
5. 状态码人话化:405/401/403/404/跳转/网络失败各有可操作指引;
   "未发现日历本"区分"地址差一层"(给出补全后的正确地址)与"确实为空"
6. 只读公开日历:识别 public-calendars 链接;只读由既有 calendarWritable 探测自动标记

接入三处:
- CalendarListPage.fetchCalendars / EditAccountPage:先 resolve() 定位端点,
  命中则自动修正 serverUrl(并落库),失败则直接给人话原因
- AddAccountPage:连接前拦分享链接;probeServer 遇 405 用 OPTIONS 判定并给指引;
  地址输入框提示同步更新

设计要点:发现顺序=原地址优先(保持既有行为、不破坏已能用的用户)→ .well-known
→ 常见候选;单个候选失败不抛错;ArkTS 静态方法内禁用 this → 统一用类名调用。

验证:编译通过;离线用例覆盖分类/完整性判定/候选路径(4 种输入形态)全部通过。

版本:AppScope/app.json5 → 0.0.7 / 700
文档:新增《上架更新说明-0.0.6到0.0.7.md》;官网 sf/uh 补功能说明与使用指引、
  版本号与日期更新、404 重建,已部署并线上核验。
打包:assembleApp release → verify-app success、profile type=release、
  app-distribution-type=app_gallery、包内版本 700/0.0.7。
This commit is contained in:
2026-10-13 12:43:26 +08:00
parent 68329c57a0
commit ed26da8dc4
5 changed files with 555 additions and 13 deletions
+28 -4
View File
@@ -10,6 +10,7 @@ import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { DavAccount, AccountStore } from '../common/AccountStore';
import { DavDiscovery, DiscoverResult } from '../common/DavDiscovery';
import { DavClient, DavCalendarDiscovery, DavCalendarEntry } from '../common/DavClient';
import { EventDb } from '../common/EventDb';
import { BackupTargetCheck, SystemCalendarMirror } from '../common/SystemCalendarMirror';
@@ -141,16 +142,39 @@ struct EditAccountPage {
return;
}
// PROPFIND 与解析统一走 DavClient.listCalendars(与「添加账号」页共用同一实现)
const auth: string = 'Basic ' + token;
// ⭐ 0.0.7:地址自动发现/智能补全(老账号可能当初填的是 DAV 根或已失效的路径)
try {
const disc: DavCalendarDiscovery = await DavClient.listCalendars(this.acc.serverUrl, 'Basic ' + token);
const resolved: DiscoverResult = await DavDiscovery.resolve(
this.acc.serverUrl,
this.acc.username,
auth,
async (u: string): Promise<boolean> => {
const d: DavCalendarDiscovery = await DavClient.listCalendars(u, auth);
return d.entries !== undefined && d.entries.length > 0;
});
if (resolved.ok && resolved.url !== '' && resolved.url !== this.acc.serverUrl) {
LogUtil.write(`编辑账号:服务地址自动修正 ${this.acc.serverUrl} → ${resolved.url}(${resolved.via})`);
this.acc.serverUrl = resolved.url;
} else if (!resolved.ok && resolved.hint !== '') {
this.isLoading = false;
this.statusMsg = resolved.hint;
this.statusOk = false;
return;
}
} catch (err) {
LogUtil.write(`编辑账号:地址发现异常,回退原地址 ${(err as BusinessError).message}`);
}
try {
const disc: DavCalendarDiscovery = await DavClient.listCalendars(this.acc.serverUrl, auth);
this.isLoading = false;
if (disc.statusCode === 401) {
this.statusMsg = '登录已失效,请检查账号密码';
this.statusMsg = '服务器拒绝凭据(401):请检查用户名与密码。若服务器开启了二次验证,请使用「应用密码」';
this.statusOk = false;
return;
}
if (disc.statusCode !== 207 && disc.statusCode !== 200) {
this.statusMsg = `获取日历列表失败,服务器返回:${disc.statusCode}`;
this.statusMsg = DavDiscovery.statusHint(disc.statusCode);
this.statusOk = false;
return;
}
@@ -158,7 +182,7 @@ struct EditAccountPage {
new EditCalendarItem(e.href, e.displayName, e.color));
if (list.length === 0) {
LogUtil.write('编辑账号:未发现任何日历本');
this.statusMsg = '该路径下未发现日历本';
this.statusMsg = DavDiscovery.emptyHint(this.acc.serverUrl, this.acc.username);
this.statusOk = false;
} else {
// 已勾选的日历本按账号当前配置预选