fix(agent): Agent 对话 - 修复无效 URL 与上游取消日志
This commit is contained in:
1 parent
c7452b701d
commit
de00e211b3
4 files changed
+62
-6
No files matched your search
@@ -3054,12 +3054,11 @@ func recordProviderRequest(req *http.Request, startedAt time.Time, statusCode in
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
status := model.ApiCallStatusSucceeded
|
status := model.ApiCallStatusSucceeded
|
||||||
|
errorCode := ""
|
||||||
errorText := ""
|
errorText := ""
|
||||||
if requestErr != nil || statusCode < 200 || statusCode >= 300 {
|
if requestErr != nil || statusCode < 200 || statusCode >= 300 {
|
||||||
status = model.ApiCallStatusFailed
|
status = model.ApiCallStatusFailed
|
||||||
if requestErr != nil {
|
errorCode, errorText = providerRequestErrorDetails(requestErr)
|
||||||
errorText = safeProviderLogError(requestErr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
requestKind := providerRequestKind(req.Method, req.URL.Path)
|
requestKind := providerRequestKind(req.Method, req.URL.Path)
|
||||||
if metadata.RequestKind != "" {
|
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",
|
RequestKind: requestKind, Billable: req.Method == http.MethodPost && requestKind != "cancel",
|
||||||
APIFormat: apiFormat, Method: req.Method, Path: req.URL.Path, Model: metadata.Model,
|
APIFormat: apiFormat, Method: req.Method, Path: req.URL.Path, Model: metadata.Model,
|
||||||
Status: status, StatusCode: statusCode, DurationMs: time.Since(startedAt).Milliseconds(),
|
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, ""),
|
ProviderRequestID: metadata.ProviderRequestID, RequestContentType: req.Header.Get("Content-Type"), RequestBody: requestPayloadForLog(req), ResponseBody: SanitizeAPICallPayload(responseBody, ""),
|
||||||
}
|
}
|
||||||
channelSlotFailure := false
|
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 {
|
func safeProviderLogError(err error) string {
|
||||||
var httpErr providerHTTPError
|
var httpErr providerHTTPError
|
||||||
if errors.As(err, &httpErr) {
|
if errors.As(err, &httpErr) {
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ import (
|
|||||||
const testReferenceImageDataURL = "data:image/png;base64,aGVsbG8="
|
const testReferenceImageDataURL = "data:image/png;base64,aGVsbG8="
|
||||||
const testGeminiReferenceImageDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
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) {
|
func TestWriteMediaPartSanitizesFilenameAndSetsMimeType(t *testing.T) {
|
||||||
var body bytes.Buffer
|
var body bytes.Buffer
|
||||||
writer := multipart.NewWriter(&body)
|
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" };
|
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-Canvas-Upstream-Headers");
|
||||||
normalizedHeaders.delete("x-goog-api-key");
|
normalizedHeaders.delete("x-goog-api-key");
|
||||||
normalizedHeaders.set("Authorization", `Bearer ${runtimeConfig.apiKey}`);
|
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");
|
normalizedHeaders.set("X-Canvas-Upstream-Format", runtimeConfig.apiFormat === "gemini" ? "gemini" : "openai");
|
||||||
if (runtimeConfig.allowLocalChannel === true) {
|
if (runtimeConfig.allowLocalChannel === true) {
|
||||||
normalizedHeaders.set("X-Canvas-Allow-Local-Channel", "1");
|
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 {
|
} else {
|
||||||
normalizedHeaders.delete("X-Canvas-Allow-Local-Channel");
|
normalizedHeaders.delete("X-Canvas-Allow-Local-Channel");
|
||||||
normalizedHeaders.delete("X-Canvas-Upstream-Base-URL");
|
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[]) {
|
function encodeChannelHeaders(headers: ChannelHeader[]) {
|
||||||
const bytes = new TextEncoder().encode(JSON.stringify(headers));
|
const bytes = new TextEncoder().encode(JSON.stringify(headers));
|
||||||
let binary = "";
|
let binary = "";
|
||||||
|
|||||||
@@ -120,4 +120,12 @@ describe("允许本机渠道前端请求合同", () => {
|
|||||||
setLocalChannelRuntime(true, "localhost");
|
setLocalChannelRuntime(true, "localhost");
|
||||||
expect(channelRequest(config, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBe("1");
|
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