Files
boss/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java

605 lines
28 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.hyzq.boss;
import android.os.Bundle;
import android.text.InputType;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONArray;
import org.json.JSONObject;
public class AiAccountsActivity extends BossScreenActivity {
private static final String[] ROLE_VALUES = {"primary", "backup", "api_fallback"};
private static final String[] ROLE_LABELS = {"主 GPT", "备用 GPT", "API 容灾"};
private static final String[] PROVIDER_VALUES = {"master_codex_node", "openai_api"};
private static final String[] PROVIDER_LABELS = {"Master Codex Node", "OpenAI API"};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("AI 账号", "OpenAI API / Master Codex Node");
setHeaderAction("新增", v -> openAccountEditor(null, null));
replaceContent();
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getAccounts();
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderAccounts(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "AI 账号加载失败:" + error.getMessage()));
});
}
});
}
private void renderAccounts(JSONObject payload) {
JSONArray accounts = payload.optJSONArray("accounts");
JSONObject activeIdentity = payload.optJSONObject("activeIdentity");
replaceContent();
appendContent(BossUi.buildWechatMenuRow(
this,
"AI 账号",
"这里统一管理主 GPT、备用 GPT 与 API 容灾账号。",
"OpenAI API 可以在手机直接登录Master Codex Node 仍然在绑定设备上完成登录。",
null,
null
));
appendContent(buildActiveIdentityCard(activeIdentity));
appendContent(buildOnboardingEntrySection());
appendContent(buildAccountsSection(accounts));
setRefreshing(false);
}
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
if (activeIdentity == null) {
return BossUi.buildWechatMenuRow(
this,
"当前主控身份",
"当前没有可用账号。",
"请先新增或启用一个账号。",
null,
null
);
}
String subtitle = activeIdentity.optString("label", "AI 账号")
+ " · " + activeIdentity.optString("displayName", "-");
String meta = activeIdentity.optString("roleLabel", "-")
+ " · " + activeIdentity.optString("providerLabel", "-")
+ " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildWechatMenuRow(
this,
"当前主控身份",
subtitle,
meta,
null,
null
);
}
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildWechatMenuRow(
this,
"账号列表",
accounts == null || accounts.length() == 0 ? "当前还没有 AI 账号。" : "点开可编辑,按钮可激活、校验或删除。",
null,
null,
null
));
if (accounts == null || accounts.length() == 0) {
section.addView(BossUi.buildEmptyCard(this, "尚未配置任何 AI 账号。"));
return section;
}
for (int i = 0; i < accounts.length(); i++) {
JSONObject account = accounts.optJSONObject(i);
if (account == null) continue;
section.addView(buildAccountCard(account));
}
return section;
}
private LinearLayout buildOnboardingEntrySection() {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildWechatMenuRow(
this,
"登录 OpenAI 平台账号",
"填写 API Key 后直接设为当前主控。",
"适合手机端直连主 Agent。",
"推荐",
v -> openOpenAiOnboardingDialog()
));
section.addView(BossUi.buildWechatMenuRow(
this,
"绑定电脑上的 Codex 节点",
"把这台 Mac 上的 Codex / ChatGPT Plus 节点接回主 Agent。",
"登录发生在绑定设备上。",
null,
v -> openMasterNodeOnboardingDialog()
));
return section;
}
private LinearLayout buildAccountCard(JSONObject account) {
String statusLabel = account.optString("statusLabel", account.optString("status", "-"));
String meta = account.optString("roleLabel", "-")
+ " · " + account.optString("providerLabel", "-")
+ " · " + statusLabel
+ (account.optBoolean("apiKeyConfigured") ? " · 已配置 Key" : "");
StringBuilder subtitle = new StringBuilder(account.optString("displayName", "-"));
if (!account.optString("accountIdentifier").isEmpty()) {
subtitle.append(" · ").append(account.optString("accountIdentifier", "-"));
}
if (!account.optString("nodeLabel").isEmpty()) {
subtitle.append(" · ").append(account.optString("nodeLabel", "-"));
}
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildWechatMenuRow(
this,
account.optString("label", "未命名账号"),
subtitle.toString(),
meta,
account.optBoolean("isActive") ? "当前" : null,
v -> openAccountEditor(account, null)
));
Button activate = BossUi.buildMiniActionButton(this, account.optBoolean("isActive") ? "当前主控" : "设为当前", !account.optBoolean("isActive"));
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(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 openOpenAiOnboardingDialog() {
final EditText labelInput = BossUi.buildInput(this, "标签,例如 主 GPT", false);
labelInput.setText("主 GPT");
final EditText displayNameInput = BossUi.buildInput(this, "显示名称", false);
displayNameInput.setText("OpenAI 平台账号");
final EditText accountIdentifierInput = BossUi.buildInput(this, "账号标识 / 备注", false);
final EditText modelInput = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
modelInput.setText("gpt-5.4");
final EditText apiKeyInput = BossUi.buildInput(this, "OpenAI API Key", false);
apiKeyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.addView(BossUi.buildFormCell(this, "标签", "建议使用 主 GPT", labelInput));
form.addView(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表中", displayNameInput));
form.addView(BossUi.buildFormCell(this, "账号标识", "可填邮箱、账号名或自定义备注", accountIdentifierInput));
form.addView(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput));
form.addView(BossUi.buildFormCell(this, "API Key", "填写后会直接登录并设为当前主控", apiKeyInput));
new AlertDialog.Builder(this)
.setTitle("登录 OpenAI 平台账号")
.setMessage("手机端直接输入 OpenAI API Key登录成功后立即设为当前主控。")
.setView(form)
.setNegativeButton("取消", null)
.setPositiveButton("登录", (dialog, which) -> submitOpenAiOnboarding(
labelInput.getText().toString().trim(),
displayNameInput.getText().toString().trim(),
accountIdentifierInput.getText().toString().trim(),
modelInput.getText().toString().trim(),
apiKeyInput.getText().toString().trim()
))
.show();
}
private void openMasterNodeOnboardingDialog() {
final EditText labelInput = BossUi.buildInput(this, "标签,例如 主 GPT", false);
labelInput.setText("主 GPT");
final EditText displayNameInput = BossUi.buildInput(this, "显示名称", false);
displayNameInput.setText("绑定电脑上的 Codex 节点");
final EditText accountIdentifierInput = BossUi.buildInput(this, "账号标识 / 备注", false);
final EditText nodeIdInput = BossUi.buildInput(this, "节点 ID", false);
final EditText nodeLabelInput = BossUi.buildInput(this, "节点名称", false);
final EditText modelInput = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
modelInput.setText("gpt-5.4");
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.addView(BossUi.buildFormCell(this, "标签", "建议使用 主 GPT", labelInput));
form.addView(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表中", displayNameInput));
form.addView(BossUi.buildFormCell(this, "账号标识", "可填账号名或自定义备注", accountIdentifierInput));
form.addView(BossUi.buildFormCell(this, "节点 ID", "本机 Codex 节点的唯一标识", nodeIdInput));
form.addView(BossUi.buildFormCell(this, "节点名称", "例如 Mac Studio", nodeLabelInput));
form.addView(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput));
new AlertDialog.Builder(this)
.setTitle("绑定电脑上的 Codex 节点")
.setMessage("主 GPT 不在手机里直接登录,请在绑定设备上的 Codex / ChatGPT Plus 会话里登录后再回来校验。")
.setView(form)
.setNegativeButton("取消", null)
.setPositiveButton("绑定", (dialog, which) -> submitMasterNodeOnboarding(
labelInput.getText().toString().trim(),
displayNameInput.getText().toString().trim(),
accountIdentifierInput.getText().toString().trim(),
nodeIdInput.getText().toString().trim(),
nodeLabelInput.getText().toString().trim(),
modelInput.getText().toString().trim()
))
.show();
}
private void submitOpenAiOnboarding(
String label,
String displayName,
String accountIdentifier,
String model,
String apiKey
) {
if (label.isEmpty() || displayName.isEmpty() || apiKey.isEmpty()) {
showMessage("标签、显示名称和 API Key 不能为空");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("label", label);
payload.put("displayName", displayName);
payload.put("accountIdentifier", accountIdentifier);
payload.put("model", model);
payload.put("apiKey", apiKey);
payload.put("enabled", true);
payload.put("setActive", true);
payload.put("provider", "openai_api");
payload.put("role", "primary");
BossApiClient.ApiResponse response = apiClient.onboardOpenAiApiAccount(payload);
if (!response.ok()) throw new IllegalStateException(response.message());
String accountId = extractAccountId(response.json);
if (accountId.isEmpty()) {
runOnUiThread(() -> {
showMessage("OpenAI 平台账号已登录,并设为当前主控。");
reload();
});
return;
}
BossApiClient.ApiResponse validation = apiClient.validateAccount(accountId);
runOnUiThread(() -> {
showMessage(validation.ok()
? validation.message()
: "登录完成,但校验失败:" + validation.message());
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("登录失败:" + error.getMessage());
});
}
});
}
private void submitMasterNodeOnboarding(
String label,
String displayName,
String accountIdentifier,
String nodeId,
String nodeLabel,
String model
) {
if (label.isEmpty() || displayName.isEmpty() || nodeId.isEmpty()) {
showMessage("标签、显示名称和节点 ID 不能为空");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("label", label);
payload.put("displayName", displayName);
payload.put("accountIdentifier", accountIdentifier);
payload.put("nodeId", nodeId);
payload.put("nodeLabel", nodeLabel);
payload.put("model", model);
payload.put("enabled", true);
payload.put("setActive", true);
payload.put("provider", "master_codex_node");
payload.put("role", "primary");
BossApiClient.ApiResponse response = apiClient.onboardMasterNodeAccount(payload);
if (!response.ok()) throw new IllegalStateException(response.message());
String accountId = extractAccountId(response.json);
if (accountId.isEmpty()) {
runOnUiThread(() -> {
showMessage("Master Codex Node 已绑定,并设为当前主控。");
reload();
});
return;
}
BossApiClient.ApiResponse validation = apiClient.validateAccount(accountId);
runOnUiThread(() -> {
showMessage(validation.ok()
? validation.message()
: "绑定完成,但校验失败:" + validation.message());
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("绑定失败:" + error.getMessage());
});
}
});
}
private String extractAccountId(JSONObject json) {
if (json == null) {
return "";
}
String accountId = json.optString("accountId", "");
if (!accountId.isEmpty()) {
return accountId;
}
JSONObject account = json.optJSONObject("account");
if (account != null) {
return account.optString("accountId", "");
}
return "";
}
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);
final android.widget.EditText accountIdentifierInput = BossUi.buildInput(this, "账号标识 / 邮箱 / 登录名", false);
final android.widget.EditText nodeIdInput = BossUi.buildInput(this, "节点 ID", false);
final android.widget.EditText nodeLabelInput = BossUi.buildInput(this, "节点名称", false);
final android.widget.EditText modelInput = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
final android.widget.EditText apiKeyInput = BossUi.buildInput(this, "API Key", false);
final android.widget.EditText loginStatusInput = BossUi.buildInput(this, "登录状态备注", true);
final Spinner roleSpinner = new Spinner(this);
roleSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ROLE_LABELS));
final Spinner providerSpinner = new Spinner(this);
providerSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, PROVIDER_LABELS));
final SwitchCompat enabledSwitch = new SwitchCompat(this);
enabledSwitch.setText("启用");
enabledSwitch.setChecked(existing == null || existing.optBoolean("enabled", true));
final SwitchCompat setActiveSwitch = new SwitchCompat(this);
setActiveSwitch.setText("保存后设为当前主控");
setActiveSwitch.setChecked(existing != null ? existing.optBoolean("isActive") : false);
if (existing != null) {
labelInput.setText(existing.optString("label", ""));
displayNameInput.setText(existing.optString("displayName", ""));
accountIdentifierInput.setText(existing.optString("accountIdentifier", ""));
nodeIdInput.setText(existing.optString("nodeId", ""));
nodeLabelInput.setText(existing.optString("nodeLabel", ""));
modelInput.setText(existing.optString("model", ""));
loginStatusInput.setText(existing.optString("loginStatusNote", ""));
roleSpinner.setSelection(indexOf(ROLE_VALUES, existing.optString("role", "primary")));
providerSpinner.setSelection(indexOf(PROVIDER_VALUES, existing.optString("provider", "master_codex_node")));
}
if (apiKeyHint != null && !apiKeyHint.isEmpty()) {
apiKeyInput.setText(apiKeyHint);
}
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.addView(BossUi.buildFormCell(this, "标签", "例如 主 GPT", labelInput));
form.addView(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表中", displayNameInput));
form.addView(BossUi.buildFormCell(this, "账号标识", "邮箱、登录名或备注信息", accountIdentifierInput));
form.addView(BossUi.buildFormCell(this, "节点 ID", "Master Codex Node 的唯一标识", nodeIdInput));
form.addView(BossUi.buildFormCell(this, "节点名称", "用于快速识别节点", nodeLabelInput));
form.addView(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput));
form.addView(BossUi.buildFormCell(this, "API Key", "仅 OpenAI API 模式需要", apiKeyInput));
form.addView(BossUi.buildFormCell(this, "登录状态备注", "可记录 Plus、有无风控等状态", loginStatusInput));
form.addView(BossUi.buildFormCell(this, "账号角色", null, roleSpinner));
form.addView(BossUi.buildFormCell(this, "提供方", null, providerSpinner));
form.addView(BossUi.buildFormCell(this, "启用状态", null, enabledSwitch));
form.addView(BossUi.buildFormCell(this, "保存后动作", null, setActiveSwitch));
new AlertDialog.Builder(this)
.setTitle(existing == null ? "新增 AI 账号" : "编辑 AI 账号")
.setView(form)
.setNegativeButton("取消", null)
.setPositiveButton("保存", (dialog, which) -> saveAccount(
existing,
labelInput.getText().toString().trim(),
displayNameInput.getText().toString().trim(),
accountIdentifierInput.getText().toString().trim(),
nodeIdInput.getText().toString().trim(),
nodeLabelInput.getText().toString().trim(),
modelInput.getText().toString().trim(),
apiKeyInput.getText().toString().trim(),
loginStatusInput.getText().toString().trim(),
enabledSwitch.isChecked(),
setActiveSwitch.isChecked(),
ROLE_VALUES[roleSpinner.getSelectedItemPosition()],
PROVIDER_VALUES[providerSpinner.getSelectedItemPosition()]
))
.show();
}
private void saveAccount(
@Nullable JSONObject existing,
String label,
String displayName,
String accountIdentifier,
String nodeId,
String nodeLabel,
String model,
String apiKey,
String loginStatusNote,
boolean enabled,
boolean setActive,
String role,
String provider
) {
if (label.isEmpty() || displayName.isEmpty()) {
showMessage("标签和显示名称不能为空");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("label", label);
payload.put("displayName", displayName);
payload.put("accountIdentifier", accountIdentifier);
payload.put("nodeId", nodeId);
payload.put("nodeLabel", nodeLabel);
payload.put("model", model);
payload.put("apiKey", apiKey);
payload.put("loginStatusNote", loginStatusNote);
payload.put("enabled", enabled);
payload.put("setActive", setActive);
payload.put("role", role);
payload.put("provider", provider);
BossApiClient.ApiResponse response = existing == null
? apiClient.createAccount(payload)
: apiClient.updateAccount(existing.optString("accountId"), payload);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage(existing == null ? "AI 账号已新增" : "AI 账号已更新");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
private int indexOf(String[] values, String target) {
for (int i = 0; i < values.length; i++) {
if (values[i].equals(target)) {
return i;
}
}
return 0;
}
private void activateAccount(JSONObject account) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.activateAccount(account.optString("accountId"), "原生页面手动切换");
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("已切换当前主控");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("切换失败:" + error.getMessage());
});
}
});
}
private void validateAccount(JSONObject account) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.validateAccount(account.optString("accountId"));
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage(response.message());
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("校验失败:" + error.getMessage());
});
}
});
}
private void confirmDeleteAccount(JSONObject account) {
new AlertDialog.Builder(this)
.setTitle("删除 AI 账号")
.setMessage("确认删除 " + account.optString("label", "该账号") + " 吗?")
.setNegativeButton("取消", null)
.setPositiveButton("删除", (dialog, which) -> deleteAccount(account))
.show();
}
private void deleteAccount(JSONObject account) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.deleteAccount(account.optString("accountId"));
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("AI 账号已删除");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("删除失败:" + error.getMessage());
});
}
});
}
}