fix: restore master agent relay guidance

This commit is contained in:
kris
2026-03-30 17:42:21 +08:00
parent 5eb1246f02
commit 7c6101f22b
9 changed files with 312 additions and 22 deletions

View File

@@ -53,7 +53,7 @@ public class AiAccountsActivity extends BossScreenActivity {
this,
"AI 账号",
"这里统一管理主 GPT、备用 GPT 与 API 容灾账号。",
"轻点条目可编辑,按钮可切换、校验或删除",
"主 GPT 的登录发生在绑定设备上的 Codex / ChatGPT Plus会在这里给登录指引",
null,
null
));
@@ -143,16 +143,44 @@ public class AiAccountsActivity extends BossScreenActivity {
activate.setEnabled(!account.optBoolean("isActive"));
activate.setOnClickListener(v -> activateAccount(account));
Button loginGuide = null;
if ("master_codex_node".equals(account.optString("provider"))) {
loginGuide = BossUi.buildMiniActionButton(this, "登录指引", false);
loginGuide.setOnClickListener(v -> showMasterNodeLoginGuide(account));
}
Button validate = BossUi.buildMiniActionButton(this, "校验连接", false);
validate.setOnClickListener(v -> validateAccount(account));
Button delete = BossUi.buildMiniActionButton(this, "删除账号", false);
delete.setOnClickListener(v -> confirmDeleteAccount(account));
card.addView(BossUi.buildInlineActionRow(this, activate, validate, delete));
card.addView(loginGuide == null
? BossUi.buildInlineActionRow(this, activate, validate, delete)
: BossUi.buildInlineActionRow(this, activate, loginGuide, validate, delete));
return card;
}
private void showMasterNodeLoginGuide(JSONObject account) {
String nodeLabel = account.optString("nodeLabel");
if (nodeLabel == null || nodeLabel.trim().isEmpty()) {
nodeLabel = account.optString("nodeId");
}
if (nodeLabel == null || nodeLabel.trim().isEmpty()) {
nodeLabel = "绑定设备";
}
String message = "主 GPT 不在手机里直接登录。\n\n"
+ "请到绑定设备 " + nodeLabel + " 上打开 Codex / ChatGPT Plus 会话完成登录。\n"
+ "登录完成后,回到这里点“校验连接”,确认主 Agent relay 已经接通。";
new AlertDialog.Builder(this)
.setTitle("主 GPT 登录指引")
.setMessage(message)
.setPositiveButton("知道了", null)
.show();
}
private void openAccountEditor(@Nullable JSONObject existing, @Nullable String apiKeyHint) {
final android.widget.EditText labelInput = BossUi.buildInput(this, "标签,例如 主 GPT", false);
final android.widget.EditText displayNameInput = BossUi.buildInput(this, "显示名称", false);
@@ -313,7 +341,7 @@ public class AiAccountsActivity extends BossScreenActivity {
BossApiClient.ApiResponse response = apiClient.validateAccount(account.optString("accountId"));
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("账号校验成功");
showMessage(response.message());
reload();
});
} catch (Exception error) {

View File

@@ -27,6 +27,9 @@ import java.util.List;
import java.util.Map;
public class BossApiClient {
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 12000;
private static final int DEFAULT_READ_TIMEOUT_MS = 12000;
private static final int MASTER_AGENT_READ_TIMEOUT_MS = 65000;
private static final String PREFS_NAME = "boss_native_client";
private static final String KEY_SESSION_COOKIE = "session_cookie";
private static final String KEY_RESTORE_TOKEN = "restore_token";
@@ -130,7 +133,14 @@ public class BossApiClient {
JSONObject payload = new JSONObject();
payload.put("body", body);
payload.put("kind", kind);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/messages", payload);
int readTimeoutMs = "master-agent".equals(projectId) ? MASTER_AGENT_READ_TIMEOUT_MS : DEFAULT_READ_TIMEOUT_MS;
return requestWithRestoreRaw(
"POST",
"/api/v1/projects/" + encode(projectId) + "/messages",
payload.toString(),
DEFAULT_CONNECT_TIMEOUT_MS,
readTimeoutMs
);
}
public ApiResponse uploadAttachment(
@@ -157,7 +167,7 @@ public class BossApiClient {
String sourceType
) throws IOException, JSONException {
HttpURLConnection connection = openConnection("/api/v1/projects/" + encode(projectId) + "/attachments");
prepareConnection(connection, "POST");
prepareConnection(connection, "POST", DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
connection.setDoOutput(true);
String boundary = "BossBoundary" + System.currentTimeMillis();
@@ -373,27 +383,61 @@ public class BossApiClient {
}
private ApiResponse requestWithRestore(String method, String path, JSONObject body) throws IOException, JSONException {
return requestWithRestoreRaw(method, path, body == null ? null : body.toString());
return requestWithRestoreRaw(
method,
path,
body == null ? null : body.toString(),
DEFAULT_CONNECT_TIMEOUT_MS,
DEFAULT_READ_TIMEOUT_MS
);
}
private ApiResponse requestWithRestoreRaw(String method, String path, @Nullable String body) throws IOException, JSONException {
ApiResponse response = requestRaw(method, path, body, true);
return requestWithRestoreRaw(method, path, body, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
}
private ApiResponse requestWithRestoreRaw(
String method,
String path,
@Nullable String body,
int connectTimeoutMs,
int readTimeoutMs
) throws IOException, JSONException {
ApiResponse response = requestRaw(method, path, body, true, connectTimeoutMs, readTimeoutMs);
if (response.statusCode == 401 && !getRestoreToken().isEmpty()) {
ApiResponse restored = restoreSession();
if (restored.ok()) {
return requestRaw(method, path, body, true);
return requestRaw(method, path, body, true, connectTimeoutMs, readTimeoutMs);
}
}
return response;
}
private ApiResponse request(String method, String path, JSONObject body, boolean expectProtected) throws IOException, JSONException {
return requestRaw(method, path, body == null ? null : body.toString(), expectProtected);
return requestRaw(
method,
path,
body == null ? null : body.toString(),
expectProtected,
DEFAULT_CONNECT_TIMEOUT_MS,
DEFAULT_READ_TIMEOUT_MS
);
}
private ApiResponse requestRaw(String method, String path, @Nullable String body, boolean expectProtected) throws IOException, JSONException {
return requestRaw(method, path, body, expectProtected, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
}
private ApiResponse requestRaw(
String method,
String path,
@Nullable String body,
boolean expectProtected,
int connectTimeoutMs,
int readTimeoutMs
) throws IOException, JSONException {
HttpURLConnection connection = openConnection(path);
prepareConnection(connection, method);
prepareConnection(connection, method, connectTimeoutMs, readTimeoutMs);
if (body != null) {
connection.setDoOutput(true);
@@ -411,10 +455,15 @@ public class BossApiClient {
return (HttpURLConnection) new URL(baseUrl + path).openConnection();
}
private void prepareConnection(HttpURLConnection connection, String method) throws IOException {
private void prepareConnection(
HttpURLConnection connection,
String method,
int connectTimeoutMs,
int readTimeoutMs
) throws IOException {
connection.setRequestMethod(method);
connection.setConnectTimeout(12000);
connection.setReadTimeout(12000);
connection.setConnectTimeout(connectTimeoutMs);
connection.setReadTimeout(readTimeoutMs);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setRequestProperty("Accept", "application/json");
@@ -500,7 +549,7 @@ public class BossApiClient {
boolean expectProtected
) throws IOException {
HttpURLConnection connection = openConnection("/api/v1/attachments/" + encode(attachmentId) + "/download");
prepareConnection(connection, "GET");
prepareConnection(connection, "GET", DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
int statusCode = connection.getResponseCode();
captureSessionCookie(connection.getHeaderFields());

View File

@@ -52,6 +52,20 @@ public class BossApiClientDispatchPlansTest {
assertEquals("{\"approvedTargetProjectIds\":[\"target-1\",\"target-2\"]}", connection.requestBody());
}
@Test
public void sendProjectMessageUsesExtendedReadTimeoutForMasterAgent() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/messages"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.sendProjectMessage("master-agent", "你好", "text");
assertEquals(200, response.statusCode);
assertEquals("/api/v1/projects/master-agent/messages", apiClient.lastPath);
assertEquals("POST", connection.requestMethodValue);
assertEquals(12000, connection.connectTimeoutValue);
assertEquals(65000, connection.readTimeoutValue);
}
private static final class RecordingBossApiClient extends BossApiClient {
private final RecordingConnection connection;
private String lastPath = "";
@@ -82,6 +96,8 @@ public class BossApiClientDispatchPlansTest {
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
private final Map<String, String> requestHeaders = new HashMap<>();
private String requestMethodValue = "GET";
private int connectTimeoutValue = 0;
private int readTimeoutValue = 0;
RecordingConnection(URL url) {
super(url);
@@ -108,6 +124,16 @@ public class BossApiClientDispatchPlansTest {
requestHeaders.put(key, value);
}
@Override
public void setConnectTimeout(int timeout) {
connectTimeoutValue = timeout;
}
@Override
public void setReadTimeout(int timeout) {
readTimeoutValue = timeout;
}
@Override
public String getRequestProperty(String key) {
return requestHeaders.get(key);