mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
fix(frontend): cap envelope mismatch
This commit is contained in:
@@ -18,6 +18,8 @@ sidebar: false
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 修复登录 Cap 人机验证:前端 `cap-solver` 与 Cap 路由测试对齐 `b3a55d4` 之后的统一 API 信封 `{ error_msg, data }`,避免 `challenge` 解构失败。
|
||||||
|
|
||||||
- 修复 Agent CI/Docker 构建:将 `GeoLite2-Country.mmdb` 提交至仓库作为兜底,构建前优先尝试 `scripts/fetch-agent-geoip-mmdb.sh` 拉取最新库,远程失败时回退使用已提交文件。
|
- 修复 Agent CI/Docker 构建:将 `GeoLite2-Country.mmdb` 提交至仓库作为兜底,构建前优先尝试 `scripts/fetch-agent-geoip-mmdb.sh` 拉取最新库,远程失败时回退使用已提交文件。
|
||||||
|
|
||||||
## [v2.3.4] - 2026-06-17
|
## [v2.3.4] - 2026-06-17
|
||||||
|
|||||||
@@ -74,16 +74,19 @@ sequenceDiagram
|
|||||||
#### 1. 获取难题 (GET/POST /api/cap/challenge)
|
#### 1. 获取难题 (GET/POST /api/cap/challenge)
|
||||||
* **请求方式**:`POST`
|
* **请求方式**:`POST`
|
||||||
* **接口权限**:公开
|
* **接口权限**:公开
|
||||||
* **响应负载**:
|
* **响应负载**(统一 API 信封,`data` 为业务载荷):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"challenge": {
|
"error_msg": "",
|
||||||
"c": 50,
|
"data": {
|
||||||
"s": 32,
|
"challenge": {
|
||||||
"d": 4
|
"c": 50,
|
||||||
},
|
"s": 32,
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjIjo1MCwicyI6MzIsImQiOjQsImV4cCI6MTcxNzY2MDgwMCwiaWF0IjoxNzE3NjYwMjAwLCJuIjoiMGExYjJjM2Q0ZTVmNiJ9.signature",
|
"d": 4
|
||||||
"expires": 1717660800000
|
},
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"expires": 1717660800000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* OpenFlare 统一 API 响应信封解析。
|
||||||
|
* 与后端 internal/common/response.Response 及 axios api-client 约定一致。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ApiEnvelope<T = unknown> {
|
||||||
|
error_msg: string;
|
||||||
|
data: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiEnvelopeError extends Error {
|
||||||
|
readonly status: number;
|
||||||
|
|
||||||
|
constructor(message: string, status: number) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiEnvelopeError';
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEnvelopeShape(value: unknown): value is ApiEnvelope<unknown> {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return 'error_msg' in value && 'data' in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 fetch 响应体中的 { error_msg, data } 信封。
|
||||||
|
* - HTTP 非 2xx:优先使用 error_msg
|
||||||
|
* - HTTP 200 但 error_msg 非空:视为业务失败
|
||||||
|
*/
|
||||||
|
export async function readApiEnvelope<T>(
|
||||||
|
res: Response,
|
||||||
|
fallbackMessage: string,
|
||||||
|
): Promise<ApiEnvelope<T>> {
|
||||||
|
let body: unknown;
|
||||||
|
try {
|
||||||
|
body = await res.json();
|
||||||
|
} catch {
|
||||||
|
throw new ApiEnvelopeError(fallbackMessage, res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasEnvelopeShape(body)) {
|
||||||
|
throw new ApiEnvelopeError(fallbackMessage, res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envelope = body as ApiEnvelope<T>;
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new ApiEnvelopeError(envelope.error_msg || fallbackMessage, res.status);
|
||||||
|
}
|
||||||
|
if (envelope.error_msg) {
|
||||||
|
throw new ApiEnvelopeError(envelope.error_msg, res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readApiData<T>(
|
||||||
|
res: Response,
|
||||||
|
fallbackMessage: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const envelope = await readApiEnvelope<T>(res, fallbackMessage);
|
||||||
|
if (envelope.data == null) {
|
||||||
|
throw new ApiEnvelopeError(fallbackMessage, res.status);
|
||||||
|
}
|
||||||
|
return envelope.data;
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
// ——— Challenge / Redeem API types ———
|
// ——— Challenge / Redeem API types ———
|
||||||
|
|
||||||
|
import {readApiData} from '@/lib/api-envelope';
|
||||||
|
|
||||||
export interface ChallengeResponse {
|
export interface ChallengeResponse {
|
||||||
challenge: { c: number; s: number; d: number };
|
challenge: { c: number; s: number; d: number };
|
||||||
token: string;
|
token: string;
|
||||||
@@ -138,10 +140,10 @@ export async function getCapToken(scope = 'login'): Promise<string> {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ scope }),
|
body: JSON.stringify({ scope }),
|
||||||
});
|
});
|
||||||
if (!challengeRes.ok) {
|
const challenge = await readApiData<ChallengeResponse>(
|
||||||
throw new Error('获取人机验证难题失败');
|
challengeRes,
|
||||||
}
|
'获取人机验证难题失败',
|
||||||
const challenge: ChallengeResponse = await challengeRes.json();
|
);
|
||||||
const { c: count, s: size, d: difficulty } = challenge.challenge;
|
const { c: count, s: size, d: difficulty } = challenge.challenge;
|
||||||
|
|
||||||
console.groupCollapsed('[Cap] 人机验证 PoW 求解开始');
|
console.groupCollapsed('[Cap] 人机验证 PoW 求解开始');
|
||||||
@@ -174,7 +176,10 @@ export async function getCapToken(scope = 'login'): Promise<string> {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ token: challenge.token, solutions, scope }),
|
body: JSON.stringify({ token: challenge.token, solutions, scope }),
|
||||||
});
|
});
|
||||||
const redeemData: RedeemResponse = await redeemRes.json();
|
const redeemData = await readApiData<RedeemResponse>(
|
||||||
|
redeemRes,
|
||||||
|
'人机验证失败',
|
||||||
|
);
|
||||||
if (!redeemData.success || !redeemData.token) {
|
if (!redeemData.success || !redeemData.token) {
|
||||||
throw new Error(redeemData.error || '人机验证失败');
|
throw new Error(redeemData.error || '人机验证失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ import (
|
|||||||
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func decodeAPIResponse[T any](t *testing.T, body []byte) T {
|
||||||
|
t.Helper()
|
||||||
|
var envelope struct {
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
Data T `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal API envelope: %v", err)
|
||||||
|
}
|
||||||
|
if envelope.ErrorMsg != "" {
|
||||||
|
t.Fatalf("unexpected API error_msg: %s", envelope.ErrorMsg)
|
||||||
|
}
|
||||||
|
return envelope.Data
|
||||||
|
}
|
||||||
|
|
||||||
func TestCapEndpointsAndMiddleware(t *testing.T) {
|
func TestCapEndpointsAndMiddleware(t *testing.T) {
|
||||||
sqliteDB, _, cleanup := testhelper.SetupTestEnvironment(t)
|
sqliteDB, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -55,10 +70,7 @@ func TestCapEndpointsAndMiddleware(t *testing.T) {
|
|||||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var challengeResp pkgcap.ChallengeResponse
|
challengeResp := decodeAPIResponse[pkgcap.ChallengeResponse](t, w.Body.Bytes())
|
||||||
if err := json.Unmarshal(w.Body.Bytes(), &challengeResp); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal challenge response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if challengeResp.Token == "" {
|
if challengeResp.Token == "" {
|
||||||
t.Fatalf("expected token in challenge response")
|
t.Fatalf("expected token in challenge response")
|
||||||
@@ -108,10 +120,7 @@ func TestCapEndpointsAndMiddleware(t *testing.T) {
|
|||||||
t.Fatalf("expected 200 OK for redeem, got %d. Body: %s", w.Code, w.Body.String())
|
t.Fatalf("expected 200 OK for redeem, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var redeemResp RedeemResponse
|
redeemResp := decodeAPIResponse[RedeemResponse](t, w.Body.Bytes())
|
||||||
if err := json.Unmarshal(w.Body.Bytes(), &redeemResp); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal redeem response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !redeemResp.Success || redeemResp.Token == "" {
|
if !redeemResp.Success || redeemResp.Token == "" {
|
||||||
t.Fatalf("redeem failed or returned empty token: %+v", redeemResp)
|
t.Fatalf("redeem failed or returned empty token: %+v", redeemResp)
|
||||||
|
|||||||
Reference in New Issue
Block a user