fix(agent): Agent 对话 - 修复无效 URL 与上游取消日志
This commit is contained in:
@@ -3054,12 +3054,11 @@ func recordProviderRequest(req *http.Request, startedAt time.Time, statusCode in
|
||||
return
|
||||
}
|
||||
status := model.ApiCallStatusSucceeded
|
||||
errorCode := ""
|
||||
errorText := ""
|
||||
if requestErr != nil || statusCode < 200 || statusCode >= 300 {
|
||||
status = model.ApiCallStatusFailed
|
||||
if requestErr != nil {
|
||||
errorText = safeProviderLogError(requestErr)
|
||||
}
|
||||
errorCode, errorText = providerRequestErrorDetails(requestErr)
|
||||
}
|
||||
requestKind := providerRequestKind(req.Method, req.URL.Path)
|
||||
if metadata.RequestKind != "" {
|
||||
@@ -3075,7 +3074,7 @@ func recordProviderRequest(req *http.Request, startedAt time.Time, statusCode in
|
||||
RequestKind: requestKind, Billable: req.Method == http.MethodPost && requestKind != "cancel",
|
||||
APIFormat: apiFormat, Method: req.Method, Path: req.URL.Path, Model: metadata.Model,
|
||||
Status: status, StatusCode: statusCode, DurationMs: time.Since(startedAt).Milliseconds(),
|
||||
Error: errorText, ConcurrencyLimit: metadata.ConcurrencyLimit, UpstreamURL: req.URL.Scheme + "://" + req.URL.Host + req.URL.Path,
|
||||
ErrorCode: errorCode, Error: errorText, ConcurrencyLimit: metadata.ConcurrencyLimit, UpstreamURL: req.URL.Scheme + "://" + req.URL.Host + req.URL.Path,
|
||||
ProviderRequestID: metadata.ProviderRequestID, RequestContentType: req.Header.Get("Content-Type"), RequestBody: requestPayloadForLog(req), ResponseBody: SanitizeAPICallPayload(responseBody, ""),
|
||||
}
|
||||
channelSlotFailure := false
|
||||
@@ -3102,6 +3101,19 @@ func recordProviderRequest(req *http.Request, startedAt time.Time, statusCode in
|
||||
}
|
||||
}
|
||||
|
||||
func providerRequestErrorDetails(err error) (string, string) {
|
||||
if err == nil {
|
||||
return "", ""
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return "request_cancelled", "任务取消,中断上游请求"
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return "upstream_timeout", "等待上游响应超时"
|
||||
}
|
||||
return "", safeProviderLogError(err)
|
||||
}
|
||||
|
||||
func safeProviderLogError(err error) string {
|
||||
var httpErr providerHTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
|
||||
@@ -21,6 +21,27 @@ import (
|
||||
const testReferenceImageDataURL = "data:image/png;base64,aGVsbG8="
|
||||
const testGeminiReferenceImageDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
|
||||
func TestProviderRequestErrorDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
code string
|
||||
text string
|
||||
}{
|
||||
{name: "cancelled", err: context.Canceled, code: "request_cancelled", text: "任务取消,中断上游请求"},
|
||||
{name: "timeout", err: context.DeadlineExceeded, code: "upstream_timeout", text: "等待上游响应超时"},
|
||||
{name: "network error", err: errors.New("dial tcp: connection refused"), text: "dial tcp: connection refused"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
code, text := providerRequestErrorDetails(tt.err)
|
||||
if code != tt.code || text != tt.text {
|
||||
t.Fatalf("providerRequestErrorDetails() = (%q, %q), want (%q, %q)", code, text, tt.code, tt.text)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMediaPartSanitizesFilenameAndSetsMimeType(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
@@ -17,7 +17,8 @@ export function channelRequest(config: RelayConfig, upstreamUrl: string, headers
|
||||
return { url: upstreamUrl, headers: Object.fromEntries(normalizedHeaders.entries()), credentials: "include" };
|
||||
}
|
||||
|
||||
const normalizedUpstreamUrl = new URL(upstreamUrl).toString();
|
||||
const normalizedBaseUrl = requireHttpUrl(runtimeConfig.baseUrl, "当前模型渠道 Base URL");
|
||||
const normalizedUpstreamUrl = requireHttpUrl(upstreamUrl, "当前模型请求地址");
|
||||
normalizedHeaders.delete("X-Canvas-Upstream-Headers");
|
||||
normalizedHeaders.delete("x-goog-api-key");
|
||||
normalizedHeaders.set("Authorization", `Bearer ${runtimeConfig.apiKey}`);
|
||||
@@ -25,7 +26,7 @@ export function channelRequest(config: RelayConfig, upstreamUrl: string, headers
|
||||
normalizedHeaders.set("X-Canvas-Upstream-Format", runtimeConfig.apiFormat === "gemini" ? "gemini" : "openai");
|
||||
if (runtimeConfig.allowLocalChannel === true) {
|
||||
normalizedHeaders.set("X-Canvas-Allow-Local-Channel", "1");
|
||||
normalizedHeaders.set("X-Canvas-Upstream-Base-URL", new URL(runtimeConfig.baseUrl).toString());
|
||||
normalizedHeaders.set("X-Canvas-Upstream-Base-URL", normalizedBaseUrl);
|
||||
} else {
|
||||
normalizedHeaders.delete("X-Canvas-Allow-Local-Channel");
|
||||
normalizedHeaders.delete("X-Canvas-Upstream-Base-URL");
|
||||
@@ -38,6 +39,20 @@ export function channelRequest(config: RelayConfig, upstreamUrl: string, headers
|
||||
};
|
||||
}
|
||||
|
||||
function requireHttpUrl(value: string, label: string) {
|
||||
const normalized = value.trim();
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(normalized);
|
||||
} catch {
|
||||
throw new Error(`${label} 无效,请填写完整地址,例如:https://api.example.com/v1`);
|
||||
}
|
||||
if (!parsed.hostname || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
||||
throw new Error(`${label} 无效,请填写完整地址,例如:https://api.example.com/v1`);
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function encodeChannelHeaders(headers: ChannelHeader[]) {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(headers));
|
||||
let binary = "";
|
||||
|
||||
@@ -120,4 +120,12 @@ describe("允许本机渠道前端请求合同", () => {
|
||||
setLocalChannelRuntime(true, "localhost");
|
||||
expect(channelRequest(config, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBe("1");
|
||||
});
|
||||
|
||||
test("非法自定义渠道 Base URL 返回可读错误,而不是原生 URL 异常", () => {
|
||||
expect(() => channelRequest({ baseUrl: "api.example.com/v1", apiKey: "key", apiFormat: "openai" }, "api.example.com/v1/images/generations")).toThrow("当前模型渠道 Base URL 无效");
|
||||
});
|
||||
|
||||
test("非法上游请求地址返回可读错误", () => {
|
||||
expect(() => channelRequest({ baseUrl: "https://api.example.com/v1", apiKey: "key", apiFormat: "openai" }, "api.example.com/v1/images/generations")).toThrow("当前模型请求地址 无效");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user