diff --git a/README.md b/README.md index ecc702e..70cfb51 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Android APK: - 当前原生聊天页已把待审批推荐前移到主消息流:`ProjectDetailActivity` 会直接显示 `确认下发 / 拒绝`,刷新后也能恢复最近一条待确认推荐 - 当前三条聊天主链都已接入真实等待链路:`主 Agent 单聊 / 普通线程单聊 / 群聊确认下发` 当前都会返回任务信息,原生 Android 会保持等待直到收到真实回写或明确超时提示 - 当前 `我的 > AI 账号` 已补 `登录 OpenAI 平台账号` 与 `绑定 Master Codex Node` 两条显式入口;OpenAI API 登录成功后会立即设为当前主控 +- 当前 `登录 OpenAI 平台账号` 已升级成浏览器辅助登录流:会先进入原生引导页,再自动打开 `OpenAI Platform` 登录页;用户登录后可直接跳到 `API Keys` 页面,回 APP 粘贴 key 完成接入 - 当前主控若还是 `Master Codex Node`,但节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API` 备用账号,避免聊天直接掉成失败日志 - 当前群资料页已经支持“修复群成员”:如果历史脏群里混入了 `master-agent` 或失效线程引用,前台会明确提示并允许重新选择真实线程成员,修复后会正式写回群成员账本 - 当前原生聊天页也会直接提示“修复群成员”:当群里存在失效线程或不可下发成员时,`ProjectDetailActivity` 会在消息流上方直接给出 `去修复` 入口,并跳到群资料页完成修复 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 00e0e46..e3b1a28 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + diff --git a/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java b/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java index 79cb730..3ae4060 100644 --- a/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java @@ -2,6 +2,7 @@ package com.hyzq.boss; import android.os.Bundle; import android.text.InputType; +import android.content.Intent; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; @@ -20,6 +21,7 @@ public class AiAccountsActivity extends BossScreenActivity { 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"}; + private boolean refreshOnResume; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -30,6 +32,15 @@ public class AiAccountsActivity extends BossScreenActivity { reload(); } + @Override + protected void onResume() { + super.onResume(); + if (refreshOnResume) { + refreshOnResume = false; + reload(); + } + } + @Override protected void reload() { setRefreshing(true); @@ -124,10 +135,10 @@ public class AiAccountsActivity extends BossScreenActivity { section.addView(BossUi.buildWechatMenuRow( this, "登录 OpenAI 平台账号", - "填写 API Key 后直接设为当前主控。", - "适合手机端直连主 Agent。", + "先打开 OpenAI 登录页,再回 APP 完成接入。", + "成功后会立即设为当前主控。", "推荐", - v -> openOpenAiOnboardingDialog() + v -> openOpenAiOnboardingScreen() )); section.addView(BossUi.buildWechatMenuRow( @@ -142,6 +153,13 @@ public class AiAccountsActivity extends BossScreenActivity { return section; } + private void openOpenAiOnboardingScreen() { + refreshOnResume = true; + Intent intent = new Intent(this, OpenAiOnboardingActivity.class); + intent.putExtra(OpenAiOnboardingActivity.EXTRA_AUTO_OPEN_LOGIN, true); + startActivity(intent); + } + private LinearLayout buildAccountCard(JSONObject account) { String statusLabel = account.optString("statusLabel", account.optString("status", "-")); String meta = account.optString("roleLabel", "-") diff --git a/android/app/src/main/java/com/hyzq/boss/OpenAiOnboardingActivity.java b/android/app/src/main/java/com/hyzq/boss/OpenAiOnboardingActivity.java new file mode 100644 index 0000000..f795941 --- /dev/null +++ b/android/app/src/main/java/com/hyzq/boss/OpenAiOnboardingActivity.java @@ -0,0 +1,197 @@ +package com.hyzq.boss; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.InputType; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import org.json.JSONObject; + +public class OpenAiOnboardingActivity extends BossScreenActivity { + public static final String EXTRA_AUTO_OPEN_LOGIN = "extra_auto_open_login"; + private static final String STATE_AUTO_OPENED = "state_auto_opened"; + private static final String OPENAI_LOGIN_URL = "https://platform.openai.com/login"; + private static final String OPENAI_KEYS_URL = "https://platform.openai.com/api-keys"; + + private EditText labelInput; + private EditText displayNameInput; + private EditText accountIdentifierInput; + private EditText modelInput; + private EditText apiKeyInput; + private boolean autoOpened; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + configureScreen("登录 OpenAI 平台账号", "先登录 OpenAI,再回这里接入 API Key"); + hideHeaderAction(); + refreshButton.setVisibility(View.GONE); + refreshLayout.setEnabled(false); + if (savedInstanceState != null) { + autoOpened = savedInstanceState.getBoolean(STATE_AUTO_OPENED, false); + } + buildForm(); + reload(); + if (getIntent().getBooleanExtra(EXTRA_AUTO_OPEN_LOGIN, false) && !autoOpened) { + autoOpened = true; + openExternalUrl(OPENAI_LOGIN_URL, "已打开 OpenAI 登录页,登录后回到这里继续。"); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(STATE_AUTO_OPENED, autoOpened); + } + + @Override + protected void reload() { + replaceContent(); + appendContent(BossUi.buildSimpleProfileHeader( + this, + "OpenAI 平台账号", + "像 Codex 一样,先去浏览器登录,再回 APP 完成接入。", + "OpenAI 目前不会把可直接调用 API 的凭据通过第三方 OAuth 直接交给 APP,所以最后一步仍然需要 API Key。" + )); + appendContent(BossUi.buildWechatMenuRow( + this, + "第一步:打开 OpenAI 登录页", + "先在浏览器完成 OpenAI Platform 登录。", + "返回 APP 后继续下一步。", + null, + v -> openExternalUrl(OPENAI_LOGIN_URL, "已打开 OpenAI 登录页。") + )); + appendContent(BossUi.buildWechatMenuRow( + this, + "第二步:打开 API Keys 页面", + "登录后创建或复制新的 API Key。", + "建议创建专门给 Boss 使用的 Key。", + null, + v -> openExternalUrl(OPENAI_KEYS_URL, "已打开 API Keys 页面。") + )); + + appendContent(BossUi.buildFormCell(this, "标签", "建议使用 主 GPT", labelInput)); + appendContent(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表和当前主控里", displayNameInput)); + appendContent(BossUi.buildFormCell(this, "账号标识", "可填邮箱、账号名或自定义备注", accountIdentifierInput)); + appendContent(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput)); + appendContent(BossUi.buildFormCell(this, "API Key", "从 OpenAI Platform 复制后粘贴到这里", apiKeyInput)); + + appendContent(BossUi.buildWechatMenuRow( + this, + "从剪贴板粘贴 API Key", + "如果你刚从浏览器复制了 key,可以直接粘贴到输入框。", + null, + null, + v -> pasteApiKeyFromClipboard() + )); + + android.widget.Button guideButton = BossUi.buildSecondaryButton(this, "主 GPT 登录说明"); + guideButton.setOnClickListener(v -> + new AlertDialog.Builder(this) + .setTitle("为什么还要 API Key?") + .setMessage("浏览器登录解决的是账号身份校验,但主 Agent 真正调用 OpenAI 模型仍然需要 API Key。当前这条链会先帮你打开 OpenAI 登录和 API Keys 页面,再回 APP 完成接入。") + .setPositiveButton("知道了", null) + .show() + ); + appendContent(guideButton); + + android.widget.Button submitButton = BossUi.buildPrimaryButton(this, "验证并设为当前主控"); + submitButton.setOnClickListener(v -> submit()); + appendContent(submitButton); + setRefreshing(false); + } + + private void buildForm() { + labelInput = BossUi.buildInput(this, "标签,例如 主 GPT", false); + labelInput.setText("主 GPT"); + displayNameInput = BossUi.buildInput(this, "显示名称", false); + displayNameInput.setText("OpenAI 平台账号"); + accountIdentifierInput = BossUi.buildInput(this, "账号标识 / 备注", false); + modelInput = BossUi.buildInput(this, "模型,例如 gpt-5.4", false); + modelInput.setText("gpt-5.4"); + apiKeyInput = BossUi.buildInput(this, "OpenAI API Key", false); + apiKeyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + + private void pasteApiKeyFromClipboard() { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard == null || !clipboard.hasPrimaryClip()) { + showMessage("剪贴板里还没有可用的 API Key。"); + return; + } + ClipData clip = clipboard.getPrimaryClip(); + if (clip == null || clip.getItemCount() == 0) { + showMessage("剪贴板里还没有可用的 API Key。"); + return; + } + CharSequence text = clip.getItemAt(0).coerceToText(this); + if (text == null || text.toString().trim().isEmpty()) { + showMessage("剪贴板里还没有可用的 API Key。"); + return; + } + apiKeyInput.setText(text.toString().trim()); + showMessage("已从剪贴板粘贴 API Key。"); + } + + private void openExternalUrl(String url, String successMessage) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(intent); + showMessage(successMessage); + } catch (Exception error) { + showMessage("打开浏览器失败:" + error.getMessage()); + } + } + + private void submit() { + String label = labelInput.getText().toString().trim(); + String displayName = displayNameInput.getText().toString().trim(); + String accountIdentifier = accountIdentifierInput.getText().toString().trim(); + String model = modelInput.getText().toString().trim(); + String apiKey = apiKeyInput.getText().toString().trim(); + 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()); + runOnUiThread(() -> { + showMessage("OpenAI 平台账号已登录,并设为当前主控。"); + setResult(RESULT_OK); + finish(); + }); + } catch (Exception error) { + runOnUiThread(() -> { + setRefreshing(false); + String detail = error.getMessage(); + showMessage(detail == null || detail.trim().isEmpty() + ? "OpenAI 平台账号登录失败,请稍后重试。" + : "OpenAI 平台账号登录失败:" + detail); + }); + } + }); + } +} diff --git a/android/app/src/test/java/com/hyzq/boss/OpenAiOnboardingActivityTest.java b/android/app/src/test/java/com/hyzq/boss/OpenAiOnboardingActivityTest.java new file mode 100644 index 0000000..faa2bd0 --- /dev/null +++ b/android/app/src/test/java/com/hyzq/boss/OpenAiOnboardingActivityTest.java @@ -0,0 +1,97 @@ +package com.hyzq.boss; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 34) +public class OpenAiOnboardingActivityTest { + @Test + public void autoOpenLoginLaunchesOpenAiPlatformLoginPage() { + Intent intent = new Intent() + .putExtra(OpenAiOnboardingActivity.EXTRA_AUTO_OPEN_LOGIN, true); + OpenAiOnboardingActivity activity = Robolectric + .buildActivity(OpenAiOnboardingActivity.class, intent) + .setup() + .get(); + + ShadowActivity shadowActivity = org.robolectric.Shadows.shadowOf(activity); + Intent nextIntent = shadowActivity.getNextStartedActivity(); + assertNotNull(nextIntent); + assertEquals(Intent.ACTION_VIEW, nextIntent.getAction()); + assertEquals(Uri.parse("https://platform.openai.com/login"), nextIntent.getData()); + } + + @Test + public void tappingApiKeysRowLaunchesApiKeysPage() { + OpenAiOnboardingActivity activity = Robolectric + .buildActivity(OpenAiOnboardingActivity.class) + .setup() + .get(); + + View row = findClickableViewContainingText( + activity.findViewById(R.id.screen_content), + "第二步:打开 API Keys 页面" + ); + assertNotNull(row); + row.performClick(); + + ShadowActivity shadowActivity = org.robolectric.Shadows.shadowOf(activity); + Intent nextIntent = shadowActivity.getNextStartedActivity(); + assertNotNull(nextIntent); + assertEquals(Intent.ACTION_VIEW, nextIntent.getAction()); + assertEquals(Uri.parse("https://platform.openai.com/api-keys"), nextIntent.getData()); + } + + private static View findClickableViewContainingText(View root, String expectedText) { + if (root == null) { + return null; + } + if (root.isClickable() && viewTreeContainsText(root, expectedText)) { + return root; + } + if (!(root instanceof ViewGroup)) { + return null; + } + ViewGroup group = (ViewGroup) root; + for (int index = 0; index < group.getChildCount(); index += 1) { + View match = findClickableViewContainingText(group.getChildAt(index), expectedText); + if (match != null) { + return match; + } + } + return null; + } + + private static boolean viewTreeContainsText(View root, String expectedText) { + if (root instanceof TextView) { + CharSequence text = ((TextView) root).getText(); + if (text != null && text.toString().contains(expectedText)) { + return true; + } + } + if (!(root instanceof ViewGroup)) { + return false; + } + ViewGroup group = (ViewGroup) root; + for (int index = 0; index < group.getChildCount(); index += 1) { + if (viewTreeContainsText(group.getChildAt(index), expectedText)) { + return true; + } + } + return false; + } +} diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index ec92a20..a75b87a 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -39,6 +39,7 @@ - `SecurityActivity` - `SettingsActivity` - `AiAccountsActivity` + - `OpenAiOnboardingActivity` - `OpsCenterActivity` - `AboutActivity` - 当前项目聊天页: @@ -72,6 +73,10 @@ - 当前 `我的` 根页: - 保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` - `运维与修复` 直接进入 `OpsCenterActivity` +- 当前 `OpenAiOnboardingActivity`: + - 会先自动打开 `OpenAI Platform` 登录页 + - 支持继续打开 `API Keys` 页面 + - 回 APP 后可直接粘贴 key,并设为当前主控 - 当前登录:临时免验证,点击登录直接创建最高管理员会话 - 当前会话恢复:`SharedPreferences` 中保存 `boss_session / restore_token / account` diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index aa03588..d052d6b 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -107,6 +107,7 @@ cd /Users/kris/code/boss - 当前普通单线程聊天也已补上真实执行链:`POST /api/v1/projects/[projectId]/messages` 不再只写用户消息,而是会追加 `conversation_reply` 任务;绑定设备上的 `local-agent` 认领后会继续恢复到真实 Codex 线程,再把线程原始回复回写到该聊天窗口 - 当前 Web 群聊详情页也已补上待确认推荐的刷新恢复:服务端会在页面渲染时读取最近一条 `pending_user_confirmation` 的 dispatch plan,聊天输入区会继续显示“等待你确认主 Agent 推荐”,不再因刷新丢失确认入口 - 当前 `AI 账号` 页面已分成两条显式接入链:`登录 OpenAI 平台账号(API Key)` 和 `绑定 Master Codex Node`;OpenAI API 登录成功后会立即切成当前主控 +- 当前 `登录 OpenAI 平台账号` 已升级成浏览器辅助登录流:原生 Android 会先进入 `OpenAiOnboardingActivity`,自动打开 `OpenAI Platform` 登录页;用户登录后可直接跳到 `API Keys` 页面,回 APP 粘贴 key 完成接入 - 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API` 备用账号,不再把失败日志直接原样回给用户 - 当前设备导入主链也已补上第一轮后端闭环:`heartbeat` 可上报真实项目候选,服务端会生成 `deviceImportDraft`;用户可提交勾选结果、生成导入决议,再把选中的线程真正落成聊天窗口 - Web 与原生 Android 当前都已补上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台流程;已绑定生产设备继续保留 heartbeat 自动导入主链 diff --git a/public/downloads/boss-android-latest.apk b/public/downloads/boss-android-latest.apk index 574d352..4fcb713 100644 Binary files a/public/downloads/boss-android-latest.apk and b/public/downloads/boss-android-latest.apk differ diff --git a/public/downloads/boss-android-latest.json b/public/downloads/boss-android-latest.json index f83bf80..788a8d4 100644 --- a/public/downloads/boss-android-latest.json +++ b/public/downloads/boss-android-latest.json @@ -1,9 +1,9 @@ { "fileName": "boss-android-v2.5.5-release.apk", "urlPath": "/api/v1/user/ota/package", - "sizeBytes": 3105013, - "updatedAt": "2026-03-30T20:15:32Z", - "sha256": "f2da722d8ea57e7bd6e16687ae161c45332cc0ba00304725f9e57ddb5b20293e", + "sizeBytes": 3107985, + "updatedAt": "2026-03-30T20:31:07Z", + "sha256": "61127ae868995d367616192200667b48b980dd39623c37ab2f776a5edc2700e6", "versionName": "2.5.5", "versionCode": 18, "buildFlavor": "release" diff --git a/public/downloads/boss-android-v2.5.5-release.apk b/public/downloads/boss-android-v2.5.5-release.apk index 574d352..4fcb713 100644 Binary files a/public/downloads/boss-android-v2.5.5-release.apk and b/public/downloads/boss-android-v2.5.5-release.apk differ