跨端: 出厂默认地址改成 example.com(原来是开发者自己的生产域名)

用户 2026-09-21:「为什么现在登陆页面默认填写我们的服务器地址?不应该是 example 地址吗」

★ 用户的直觉是对的:**这确实是错的**,而且比"预填"更彻底。

## 现状:三处互相打脸

  ① 输入框的 placeholder:
       TextInput({ placeholder: 'https://example.com/api/v1', text: this.serverAddr })
                            ↑ 「请填你自己的」
  ② 同一个输入框的 text 预填:
       @State serverAddr: string = DEFAULT_API_BASE
       export const DEFAULT_API_BASE = 'https://mail.jianfgit.xyz/api/v1'
                            ↑ 「就是这个」—— 与 ① 直接矛盾
  ③ 校验失败的 5 条提示文案全在教用户填同一个生产域名:
       '请填写服务器地址,例如 https://mail.jianfgit.xyz/api/v1'
                            ↑ 用户是照着它填的

通用客户端把**某一个人的后端**写成出厂默认,等于宣称"本产品只有一个后端";
更坏的是用户会以为"直接登录就行",而连的其实是别人的机器。

## WebUI 的做法(对照)

它**一个域名都不写死**(`api/config.ts:30`):
    const base = (runtime || build || '/api/v1').trim();
因为 WebUI 跑在服务端自己发出来的页面上,"同源"天然正确。
**鸿蒙是独立 App,没有"同源"可依** —— 所以只能要求用户填,而默认值只能是格式示例。

## 改法

· `DEFAULT_API_BASE` → `https://example.com/api/v1`(与 placeholder 同一句话);
· `ApiBase.ts` 里 5 条面向用户的 error 文案 → 同样换成 `example.com`。

★ 注释里的 `mail.jianfgit.xyz` **保留不动** —— 那是"当时线上发生了什么"的取证
  (那段讲的是 2026-09-15 用户报"连不上服务器"的两个坑),删掉就销毁了依据。
  判据因此是**剥注释后再扫**代码。

★ 为什么不留空串:这个字段必须填对,留空用户不知道格式;
  而预填一个**看起来能用的真域名**更坏。

## 判据:原先只钉"格式",抓不到"值是谁的"

原来的断言是「https + 带 /api/v1」—— 而 `https://mail.jianfgit.xyz/api/v1`
**两条都满足** ⇒ 全绿。**格式判据抓不到语义事故**:那个值可以完全合法却仍然是错的。

⇒ 新增两条**语义**判据:
  ① 出厂默认必须落在 **RFC 2606 保留域名**(`example.com/net/org`、`.test`、`.invalid`);
  ② 面向用户的**提示文案**里不许出现真实域名(剥注释后扫)。

变异逐条验过会红:
    默认值改回生产域名                        → 红 ✓
    把某条 error 文案换成 my-real-server.com  → 红 ✓
    两者都在                                  → 13/13 全绿 ✓

★ 期间修了判据自己的一个 bug:我用了 `code('model/ApiBase.ts')`,
  而 `code()` 是相对 **electron 包**解析的 ⇒ `ENOENT .../client/electron/model/ApiBase.ts`
  —— **判据自己崩了**(整条不跑、退出码 1)。已改用本文件既有的 `read(rel)`。

## 设备验证

清掉 preferences 里的旧地址后冷启,`uitest dumpLayout`:
    text  'https://example.com/api/v1'
    hint  'https://example.com/api/v1'
⇒ 预填与提示**同话**,不再互相矛盾。

(老装机 preferences 里已有的地址**保持不动** —— 那是用户自己配的服务器,不该被清。)
This commit is contained in:
2026-09-21 17:26:58 +08:00
parent f4d5a75976
commit 1571eb2ec4
2 changed files with 36 additions and 14 deletions

View File

@ -4,22 +4,44 @@
*/
/**
* 默认地址:**HTTPS + 公网域名**(2026-09-15 改)。
* 默认地址 —— **出厂占位值,不是"我们的服务器"**(2026-09-21 改)。
*
* 原来是 `http://192.168.2.60:8180/api/v1` —— 明文 + 写死内网 IP,两个问题:
* 1. 它只在"手机与这台机在同一网段"时可用,出门/换网就永远连不上,
* 而错误还只说"无法连接",用户无从判断;
* 2. 明文 http 会把登录凭据暴露在链路上。
* ── 改前是什么、为什么错 ──
*
* 域名解析到的就是同一台机(`mail.jianfgit.xyz` → 192.168.2.60),
* 所以本机/内网调试**不需要**为了速度牺牲 TLS:走 https 也能到。
* 真要直连内网明文,用设置页改成 `http://192.168.2.60:8180/api/v1`(见 network_config.json)。
* 原来这里是 `https://mail.jianfgit.xyz/api/v1` —— **开发者自己的生产域名**。
* 用户的直觉是对的(他问:「为什么现在登陆页面默认填写我们的服务器地址?
* 不应该是 example 地址吗」):一个通用客户端把**某一个人的服务器**作为出厂默认,
* 等于宣称"本产品只有一个后端"。
*
* 三处互相打脸,一眼就能看出这是疏漏而不是决定:
* ① 输入框的 `placeholder` 写的是 `https://example.com/api/v1`(= "请填你自己的");
* ② 同一个输入框的 `text` 却预填了生产域名(= "就是这个");
* ③ 校验失败的提示文案也全在教用户填 `mail.jianfgit.xyz`。
*
* WebUI 那边**一个域名都不写死**:`resolveBase()` 取「运行时全局 > 构建期变量 >
* 同源 `/api/v1`」(`api/config.ts:30`)—— 它跑在服务端自己发出来的页面上,
* 所以"同源"天然正确。**鸿蒙是独立 App,没有"同源"可依**,只能要求用户填。
*
* ── 现在的口径 ──
*
* 默认值是 **`https://example.com/api/v1`** —— 与 placeholder 同一句话:
* 它是**格式示例**,不是可用地址。用户必须改成自己部署的那台。
*
* 为什么保留"预填一个值"而不是留空:
* · 留空后用户完全不知道要填什么格式,而这是个必须填对的字段;
* · 预填一个明显是示例的域名,用户一眼就知道"这里要换成我的",
* 而预填一个**看起来能用的真域名**才会让人以为"直接登录就行"。
*
* ★ 末尾的 `/api/v1` **不能省**:`ApiClient` 里的路径都是相对它的
* (`'/auth/login'`、`'/me/sessions'`…),省掉就变成 `https://域名/auth/login` ⇒ 404。
* 归一化与校验在 `model/ApiBase.ts`(用户手填的地址走那里,判据跑那一份)。
*
* ★ 内网联调怎么办:那台机就是本机,用 `EMULATOR_HOST_BASE`
* (模拟器 NAT)或直接在设置页填内网明文地址 —— `network_config.json`
* 已经为 `192.168.2.60` / `10.0.2.2` / `localhost` 放行了明文。
* **但那属于开发场景,不该写进出厂默认值。**
*/
export const DEFAULT_API_BASE: string = 'https://mail.jianfgit.xyz/api/v1';
export const DEFAULT_API_BASE: string = 'https://example.com/api/v1';
/**
* 模拟器 NAT 访问宿主机地址(备用,LAN 直连不通时用)。

View File

@ -144,7 +144,7 @@ export function validateApiBase(raw: string): ApiBaseCheck {
return {
ok: false,
base: '',
error: '请填写服务器地址,例如 https://mail.jianfgit.xyz/api/v1',
error: '请填写服务器地址,例如 https://example.com/api/v1',
warning: ''
};
}
@ -154,7 +154,7 @@ export function validateApiBase(raw: string): ApiBaseCheck {
return {
ok: false,
base: base,
error: '地址要以 http:// 或 https:// 开头,例如 https://mail.jianfgit.xyz/api/v1'
error: '地址要以 http:// 或 https:// 开头,例如 https://example.com/api/v1'
+ '(不自动替你补 https —— 协议选错会把凭据送到明文通道上)',
warning: ''
};
@ -165,7 +165,7 @@ export function validateApiBase(raw: string): ApiBaseCheck {
return {
ok: false,
base: base,
error: '地址里不能有空格,例如 https://mail.jianfgit.xyz/api/v1',
error: '地址里不能有空格,例如 https://example.com/api/v1',
warning: ''
};
}
@ -174,7 +174,7 @@ export function validateApiBase(raw: string): ApiBaseCheck {
return {
ok: false,
base: base,
error: '地址里缺少域名,例如 https://mail.jianfgit.xyz/api/v1',
error: '地址里缺少域名,例如 https://example.com/api/v1',
warning: ''
};
}
@ -254,7 +254,7 @@ export function kindOfNativeCode(nativeCode: number): string {
export function describeFailure(status: number, nativeCode: number, serverMessage: string, url: string): FailureText {
if (status === 404) {
let message: string = 'HTTP 404:服务端没有 ' + url + ' 这个接口。'
+ '地址应当是完整的 API 前缀、以 /api/v1 结尾(例如 https://mail.jianfgit.xyz/api/v1)。';
+ '地址应当是完整的 API 前缀、以 /api/v1 结尾(例如 https://example.com/api/v1)。';
if (serverMessage.length > 0) {
message = message + '服务端回复:' + serverMessage;
}