Merge pull request #306 from ProtectCookies/main

fix(network): 修复长耗时请求被误报「Network disconnected」中断的问题
This commit is contained in:
leokun
2026-08-19 11:36:02 +08:00
committed by GitHub
5 changed files with 77 additions and 33 deletions
+22 -22
View File
@@ -125,8 +125,8 @@
"refs": [ "refs": [
{ {
"file": "src/views/Config.vue", "file": "src/views/Config.vue",
"line": 51, "line": 50,
"column": 1 "column": 48
} }
] ]
}, },
@@ -149,8 +149,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/HomeMetricsCard.vue", "file": "src/components/HomeMetricsCard.vue",
"line": 338, "line": 337,
"column": 1 "column": 65
} }
] ]
}, },
@@ -366,8 +366,8 @@
"refs": [ "refs": [
{ {
"file": "src/views/Config.vue", "file": "src/views/Config.vue",
"line": 65, "line": 64,
"column": 1 "column": 48
} }
] ]
}, },
@@ -483,8 +483,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/ModelAdapterTestCard.vue", "file": "src/components/ModelAdapterTestCard.vue",
"line": 146, "line": 145,
"column": 1 "column": 7
} }
] ]
}, },
@@ -531,8 +531,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/ModelAdapterTestCard.vue", "file": "src/components/ModelAdapterTestCard.vue",
"line": 125, "line": 124,
"column": 1 "column": 60
} }
] ]
}, },
@@ -604,8 +604,8 @@
}, },
{ {
"file": "src/components/CursorAccountCard.vue", "file": "src/components/CursorAccountCard.vue",
"line": 188, "line": 187,
"column": 1 "column": 11
} }
] ]
}, },
@@ -1473,8 +1473,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/HomeMetricsCard.vue", "file": "src/components/HomeMetricsCard.vue",
"line": 390, "line": 389,
"column": 1 "column": 65
} }
] ]
}, },
@@ -1750,8 +1750,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/HomeMetricsCard.vue", "file": "src/components/HomeMetricsCard.vue",
"line": 342, "line": 341,
"column": 1 "column": 23
} }
] ]
}, },
@@ -2666,8 +2666,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/CursorAccountCard.vue", "file": "src/components/CursorAccountCard.vue",
"line": 170, "line": 169,
"column": 1 "column": 53
} }
] ]
}, },
@@ -2777,8 +2777,8 @@
"refs": [ "refs": [
{ {
"file": "src/views/Config.vue", "file": "src/views/Config.vue",
"line": 77, "line": 76,
"column": 1 "column": 48
} }
] ]
}, },
@@ -2854,8 +2854,8 @@
"refs": [ "refs": [
{ {
"file": "src/components/CursorAccountCard.vue", "file": "src/components/CursorAccountCard.vue",
"line": 173, "line": 172,
"column": 1 "column": 81
} }
] ]
}, },
+14
View File
@@ -691,6 +691,20 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
return nil return nil
}), }),
), ),
// The always-local extension probes NetworkService/IsConnected roughly 10s
// after any slow request starts. A 404 here is treated as "network
// disconnected" and aborts in-flight work (e.g. commit message
// generation) even while the model is still streaming. Always answer OK.
server.POST("/aiserver.v1.NetworkService/IsConnected",
server.Name("network_is_connected"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "network_is_connected",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.IsConnectedResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.Any("/aiserver.v1.NetworkService/*", server.Any("/aiserver.v1.NetworkService/*",
server.Name("network_service"), server.Name("network_service"),
server.HTTP(), server.HTTP(),
@@ -451,6 +451,8 @@ func newProtoMessage(typeName string) (proto.Message, error) {
return &aiserverv1.SubmitLogsResponse{}, nil return &aiserverv1.SubmitLogsResponse{}, nil
case "aiserver.v1.TrackEventsResponse": case "aiserver.v1.TrackEventsResponse":
return &aiserverv1.TrackEventsResponse{}, nil return &aiserverv1.TrackEventsResponse{}, nil
case "aiserver.v1.IsConnectedResponse":
return &aiserverv1.IsConnectedResponse{}, nil
default: default:
return nil, fmt.Errorf("unsupported proto message type %q", typeName) return nil, fmt.Errorf("unsupported proto message type %q", typeName)
} }
+26 -11
View File
@@ -33,6 +33,14 @@ var cursorStateDisabledStatsigGates = []string{
"disable_terminal_output_ui_streaming", "disable_terminal_output_ui_streaming",
} }
// cursorStateEnabledStatsigGates forces gates to true. Disabling the network
// change monitor stops the always-local extension from probing
// NetworkService/IsConnected and aborting slow in-flight requests (such as
// commit message generation) with "Network disconnected".
var cursorStateEnabledStatsigGates = []string{
"disable_network_change_monitor_local",
}
// InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the // InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
// Settings page. It does not modify the installed Cursor app bundle. // Settings page. It does not modify the installed Cursor app bundle.
func InjectCursorUserInfo(email, token string) error { func InjectCursorUserInfo(email, token string) error {
@@ -50,12 +58,13 @@ func InjectCursorUserInfo(email, token string) error {
} }
logger.Infof( logger.Infof(
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s", "injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s enabled_statsig_gates=%s",
stateDBPath, stateDBPath,
values["cursorAuth/cachedEmail"], values["cursorAuth/cachedEmail"],
values["cursorAuth/stripeMembershipType"], values["cursorAuth/stripeMembershipType"],
values["cursorAuth/stripeSubscriptionStatus"], values["cursorAuth/stripeSubscriptionStatus"],
strings.Join(cursorStateDisabledStatsigGates, ","), strings.Join(cursorStateDisabledStatsigGates, ","),
strings.Join(cursorStateEnabledStatsigGates, ","),
) )
return nil return nil
} }
@@ -120,7 +129,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error {
} }
} }
if err := disableCursorStatsigGates(ctx, tx); err != nil { if err := syncCursorStatsigGateOverrides(ctx, tx); err != nil {
return err return err
} }
@@ -131,7 +140,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error {
return nil return nil
} }
func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { func syncCursorStatsigGateOverrides(ctx context.Context, tx *sql.Tx) error {
var raw []byte var raw []byte
err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw) err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw)
if err != nil { if err != nil {
@@ -154,9 +163,15 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error {
hashUsed, _ := payload["hash_used"].(string) hashUsed, _ := payload["hash_used"].(string)
for _, gate := range cursorStateDisabledStatsigGates { for _, gate := range cursorStateDisabledStatsigGates {
disableCursorStatsigGate(featureGates, gate) setCursorStatsigGate(featureGates, gate, false, "local_disabled")
if strings.EqualFold(hashUsed, "djb2") { if strings.EqualFold(hashUsed, "djb2") {
disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate)) setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), false, "local_disabled")
}
}
for _, gate := range cursorStateEnabledStatsigGates {
setCursorStatsigGate(featureGates, gate, true, "local_enabled")
if strings.EqualFold(hashUsed, "djb2") {
setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), true, "local_enabled")
} }
} }
@@ -170,21 +185,21 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error {
return nil return nil
} }
func disableCursorStatsigGate(featureGates map[string]any, key string) { func setCursorStatsigGate(featureGates map[string]any, key string, value bool, rule string) {
gate, _ := featureGates[key].(map[string]any) gate, _ := featureGates[key].(map[string]any)
if gate == nil { if gate == nil {
gate = map[string]any{ gate = map[string]any{
"name": key, "name": key,
"rule_id": "local_disabled", "rule_id": rule,
"ruleID": "local_disabled", "ruleID": rule,
"group_name": "local_disabled", "group_name": rule,
"groupName": "local_disabled", "groupName": rule,
"id_type": "userID", "id_type": "userID",
"idType": "userID", "idType": "userID",
} }
featureGates[key] = gate featureGates[key] = gate
} }
gate["value"] = false gate["value"] = value
} }
func cursorStateDJB2Hash(value string) string { func cursorStateDJB2Hash(value string) string {
+13
View File
@@ -17785,6 +17785,14 @@ message IsAllowedFreeTrialUsageResponse {
bool is_allowed = 1; bool is_allowed = 1;
} }
// Copied from: local:aiserver.v1.IsConnectedRequest
message IsConnectedRequest {
}
// Copied from: local:aiserver.v1.IsConnectedResponse
message IsConnectedResponse {
}
// Copied from: local:aiserver.v1.IsCursorPredictionEnabledRequest (var: An) // Copied from: local:aiserver.v1.IsCursorPredictionEnabledRequest (var: An)
message IsCursorPredictionEnabledRequest { message IsCursorPredictionEnabledRequest {
} }
@@ -36919,6 +36927,11 @@ service MetricsService {
rpc ReportGauge(ReportMetricsRequest) returns (ReportMetricsResponse) {} rpc ReportGauge(ReportMetricsRequest) returns (ReportMetricsResponse) {}
} }
// Source: aiserver.v1.NetworkService
service NetworkService {
rpc IsConnected(IsConnectedRequest) returns (IsConnectedResponse) {}
}
// Source: aiserver.v1.PerformanceEventService (var: i) // Source: aiserver.v1.PerformanceEventService (var: i)
service PerformanceEventService { service PerformanceEventService {
rpc SubmitPerformanceEvents(SubmitPerformanceEventsRequest) returns (SubmitPerformanceEventsResponse) {} rpc SubmitPerformanceEvents(SubmitPerformanceEventsRequest) returns (SubmitPerformanceEventsResponse) {}