@@ -902,8 +902,17 @@ portalRouter.get('/portal/password/reset-link', (c) => {
// POST /portal/password/forgot — body {email}。**公開端點**(忘記密碼的人當然沒登入)。
//
// 不洩漏帳號存在性:帳號在不在,回的都是同一句話、同一個 200。
// 唯一會回錯的是「這台實例根本沒設代寄服務 」——那與「有沒有這個帳號」無關,講出來不洩漏任何事,
// 唯一會回錯的是「寄信功能根本沒接上 」——那與「有沒有這個帳號」無關,講出來不洩漏任何事,
// 而不講就會讓人對著一封永遠不會到的信等下去(#49「把故障講成用戶的問題」的反面)。
//
// 🔴 arcrun-rag#38/#69/#25( 2026-08-11 leo 親口點出兩個問題,改字前先讀):
// ① 「實例」是行話——目標用戶是「只會叫 AI 幫忙的人」,不懂什麼叫一台實例。
// ② 「請管理員直接幫你改密碼」對單人使用者是死路——他自己就是管理員,等於叫他聯絡自己
// (`#25` 同一種病:「我忘記 portal 密碼,畫面叫我去找管理員,但管理員就是我」)。
// ⇒ 訊息改成白話(不提「實例」),而且給一條他自己走得完的路:**重新跑一次安裝/更新**
// 現在**真的有用**(不是安慰話)——arcrun-rag#38/#69/#25 同批修正讓安裝器學會「版本號一樣
// 不代表這個功能已經接上,沒接上就當作舊的重推」,所以照原本收到的安裝網址走一次,
// 選同一個 Cloudflare 帳號,這個功能就會自動接上,不需要自己改任何設定。
portalRouter . post ( '/portal/password/forgot' , ( c ) = >
run ( c , async ( ) = > {
const body = await c . req . json ( ) . catch ( ( ) = > null ) ;
@@ -914,8 +923,9 @@ portalRouter.post('/portal/password/forgot', (c) =>
return c . json (
{
error :
'這台實例還沒有設定寄信服務, 「忘記密碼」的連結 寄不出去。' +
'請重新執行安裝/更新讓它就緒,或請管理員直接幫你改密碼。' ,
'寄信功能還沒接上,所以 「忘記密碼」的信 寄不出去。' +
'請照當初收到的安裝網址,重新執行一次安裝(選同一個 Cloudflare 帳號)——' +
'完成後這個功能就會自動接上,不需要自己設定任何東西,也不用找任何人幫忙。' ,
code : 'mail_relay_not_configured' ,
} ,
503 ,
@@ -924,7 +934,7 @@ portalRouter.post('/portal/password/forgot', (c) =>
const generic = {
success : true ,
message : '如果這個 email 在這台實例上 有帳號,我們已經把「修改密碼」的連結寄過去了(連結 30 分鐘內有效、只能用一次)。' ,
message : '如果這個 email 有帳號,我們已經把「修改密碼」的連結寄過去了(連結 30 分鐘內有效、只能用一次)。' ,
} ;
// 節流:同一個 email 兩分鐘內只寄一次(擋灌信,也擋拿這支當帳號存在性探針的節奏)