feat: ship native boss android console

This commit is contained in:
kris
2026-03-26 23:16:56 +08:00
parent 90e904814d
commit 90cb6b7ff1
261 changed files with 40051 additions and 135 deletions

101
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

70
android/app/build.gradle Normal file
View File

@@ -0,0 +1,70 @@
apply plugin: 'com.android.application'
def releaseSigningProps = new Properties()
def releaseSigningPropsFile = rootProject.file('signing/release-signing.properties')
def hasReleaseSigning = releaseSigningPropsFile.exists()
if (hasReleaseSigning) {
releaseSigningProps.load(new FileInputStream(releaseSigningPropsFile))
}
android {
namespace = "com.hyzq.boss"
compileSdk = rootProject.ext.compileSdkVersion
buildFeatures {
buildConfig true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
signingConfigs {
if (hasReleaseSigning) {
release {
storeFile file(releaseSigningProps['storeFile'])
storePassword releaseSigningProps['storePassword']
keyAlias releaseSigningProps['keyAlias']
keyPassword releaseSigningProps['keyPassword']
enableV1Signing true
enableV2Signing true
}
}
}
defaultConfig {
applicationId "com.hyzq.boss"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 7
versionName "2.1.0"
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (hasReleaseSigning) {
signingConfig signingConfigs.release
}
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
}
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ProjectDetailActivity" android:exported="false" />
<activity android:name=".ProjectGoalsActivity" android:exported="false" />
<activity android:name=".ProjectVersionsActivity" android:exported="false" />
<activity android:name=".ProjectForwardActivity" android:exported="false" />
<activity android:name=".ThreadDetailActivity" android:exported="false" />
<activity android:name=".DeviceDetailActivity" android:exported="false" />
<activity android:name=".DeviceEnrollmentActivity" android:exported="false" />
<activity android:name=".SkillInventoryActivity" android:exported="false" />
<activity android:name=".SecurityActivity" android:exported="false" />
<activity android:name=".SettingsActivity" android:exported="false" />
<activity android:name=".AiAccountsActivity" android:exported="false" />
<activity android:name=".OpsCenterActivity" android:exported="false" />
<activity android:name=".AboutActivity" android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,122 @@
package com.hyzq.boss;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class AboutActivity extends BossScreenActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("关于 / OTA", "原生版本中心");
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse settings = apiClient.getSettings();
BossApiClient.ApiResponse ota = apiClient.getOtaStatus();
BossApiClient.ApiResponse session = apiClient.getSession();
if (!settings.ok() || !ota.ok() || !session.ok()) {
throw new IllegalStateException("PROFILE_OR_OTA_LOAD_FAILED");
}
runOnUiThread(() -> renderAbout(
settings.json.optJSONObject("user"),
ota.json,
session.json.optJSONObject("session")
));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "关于页加载失败:" + error.getMessage()));
});
}
});
}
private void renderAbout(@Nullable JSONObject user, JSONObject ota, @Nullable JSONObject session) {
replaceContent();
if (user != null) {
appendContent(BossUi.buildCard(
this,
"当前版本",
user.optString("version", "-")
+ "\n当前账号" + user.optString("account", "-")
+ "\n绑定 Codex" + user.optString("boundCodexNodeLabel", "未绑定"),
session == null ? "-" : "会话到期 " + session.optString("expiresAt", "-")
));
}
JSONObject availableRelease = ota.optJSONObject("availableRelease");
String otaBody = availableRelease == null
? "当前已经是最新版本。"
: availableRelease.optString("version", "未知版本")
+ "\n" + availableRelease.optString("summary", "暂无摘要")
+ "\n文件" + availableRelease.optString("packageFileName", "-");
appendContent(BossUi.buildCard(
this,
"OTA 状态",
otaBody,
"当前版本 " + ota.optString("currentVersion", "-")
));
LinearLayout actionCard = BossUi.buildCard(this, "OTA 操作", "可在原生页直接检查更新、登记 OTA 并下载 APK。", "当前接口:/api/v1/user/ota");
Button check = BossUi.buildPrimaryButton(this, "检查更新");
check.setOnClickListener(v -> performOtaAction("check"));
actionCard.addView(check);
Button apply = BossUi.buildSecondaryButton(this, "登记应用 OTA");
apply.setOnClickListener(v -> performOtaAction("apply"));
actionCard.addView(apply);
Button download = BossUi.buildSecondaryButton(this, "下载最新 APK");
download.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apiClient.getProtectedOtaPackageUrl()));
startActivity(intent);
});
actionCard.addView(download);
appendContent(actionCard);
JSONArray logs = ota.optJSONArray("logs");
if (logs != null) {
for (int i = 0; i < logs.length(); i++) {
JSONObject log = logs.optJSONObject(i);
if (log == null) continue;
appendContent(BossUi.buildCard(
this,
log.optString("version", "OTA"),
log.optString("summary", ""),
log.optString("status", "-") + " · " + log.optString("createdAt", "-")
));
}
}
setRefreshing(false);
}
private void performOtaAction(String action) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = "check".equals(action) ? apiClient.checkOta() : apiClient.applyOta();
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("check".equals(action) ? "已完成版本检查" : "已登记 OTA 应用");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("OTA 操作失败:" + error.getMessage());
});
}
});
}
}

View File

@@ -0,0 +1,382 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
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"};
private LinearLayout accountList;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("AI 账号", "主 GPT / 备用 GPT / API 容灾");
setHeaderAction("新增", v -> openAccountEditor(null, null));
replaceContent(buildIntroCard(), buildAccountListShell());
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 LinearLayout buildIntroCard() {
return BossUi.buildCard(
this,
"账号说明",
"当前页面管理 Boss 的主控 AI 账号。主链路优先使用已绑定电脑上的 Master Codex NodeAPI 容灾在同页可补充配置。",
"支持新增、编辑、激活、校验和删除"
);
}
private LinearLayout buildAccountListShell() {
LinearLayout wrapper = new LinearLayout(this);
wrapper.setOrientation(LinearLayout.VERTICAL);
accountList = new LinearLayout(this);
accountList.setOrientation(LinearLayout.VERTICAL);
wrapper.addView(accountList);
return wrapper;
}
private void renderAccounts(JSONObject payload) {
JSONArray accounts = payload.optJSONArray("accounts");
JSONObject activeIdentity = payload.optJSONObject("activeIdentity");
JSONArray switchHistory = payload.optJSONArray("switchHistory");
accountList.removeAllViews();
replaceContent(buildIntroCard(), buildActiveIdentityCard(activeIdentity), buildAccountsSection(accounts), buildSwitchHistoryCard(switchHistory));
setRefreshing(false);
}
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
String body = activeIdentity == null
? "当前没有可用的主控身份。"
: activeIdentity.optString("label", "AI 账号")
+ "\n" + activeIdentity.optString("displayName", "-")
+ "\n" + activeIdentity.optString("providerLabel", "-")
+ (activeIdentity.optString("nodeLabel").isEmpty() ? "" : "\n节点" + activeIdentity.optString("nodeLabel"));
String meta = activeIdentity == null
? "请先配置一个可用账号"
: activeIdentity.optString("roleLabel", "-") + " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildCard(this, "当前主控身份", body, meta);
}
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildCard(
this,
"账号列表",
accounts == null || accounts.length() == 0 ? "当前还没有 AI 账号。" : "点击卡片可编辑,按钮可激活 / 校验 / 删除。",
"当前 API/api/v1/accounts"
));
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 buildAccountCard(JSONObject account) {
String statusLabel = account.optString("statusLabel", account.optString("status", "-"));
String meta = account.optString("roleLabel", "-")
+ " · " + account.optString("providerLabel", "-")
+ " · " + statusLabel
+ (account.optBoolean("isActive") ? " · 当前主控" : "")
+ (account.optBoolean("apiKeyConfigured") ? " · 已配置 Key" : "");
String body = account.optString("displayName", "-")
+ "\n账号" + account.optString("accountIdentifier", "-")
+ (account.optString("nodeLabel").isEmpty() ? "" : "\n节点" + account.optString("nodeLabel"))
+ (account.optString("loginStatusNote").isEmpty() ? "" : "\n" + account.optString("loginStatusNote"));
LinearLayout card = BossUi.buildCard(
this,
account.optString("label", "未命名账号"),
body,
meta,
v -> openAccountEditor(account, null)
);
Button activate = BossUi.buildPrimaryButton(this, account.optBoolean("isActive") ? "已激活" : "设为当前主控");
activate.setEnabled(!account.optBoolean("isActive"));
activate.setOnClickListener(v -> activateAccount(account));
card.addView(activate);
Button validate = BossUi.buildSecondaryButton(this, "校验连接");
validate.setOnClickListener(v -> validateAccount(account));
card.addView(validate);
Button edit = BossUi.buildSecondaryButton(this, "编辑账号");
edit.setOnClickListener(v -> openAccountEditor(account, null));
card.addView(edit);
Button delete = BossUi.buildSecondaryButton(this, "删除账号");
delete.setOnClickListener(v -> confirmDeleteAccount(account));
card.addView(delete);
return card;
}
private LinearLayout buildSwitchHistoryCard(@Nullable JSONArray switchHistory) {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildCard(
this,
"切换历史",
switchHistory == null || switchHistory.length() == 0 ? "当前没有切换记录。" : "最近切换记录会保留 40 条。",
"用于追踪主控身份变化"
));
if (switchHistory == null || switchHistory.length() == 0) {
section.addView(BossUi.buildEmptyCard(this, "当前没有 AI 账号切换历史。"));
return section;
}
for (int i = 0; i < switchHistory.length(); i++) {
JSONObject record = switchHistory.optJSONObject(i);
if (record == null) continue;
String body = "" + record.optString("fromLabel", "")
+ "\n到 " + record.optString("toLabel", "-")
+ "\n原因" + record.optString("reason", "-");
String meta = record.optString("role", "-") + " · " + record.optString("switchedAt", "-");
section.addView(BossUi.buildCard(this, "切换记录", body, meta));
}
return section;
}
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(labelInput);
form.addView(displayNameInput);
form.addView(accountIdentifierInput);
form.addView(nodeIdInput);
form.addView(nodeLabelInput);
form.addView(modelInput);
form.addView(apiKeyInput);
form.addView(loginStatusInput);
form.addView(roleSpinner);
form.addView(providerSpinner);
form.addView(enabledSwitch);
form.addView(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("账号校验成功");
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());
});
}
});
}
}

View File

@@ -0,0 +1,375 @@
package com.hyzq.boss;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
public class BossApiClient {
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";
private static final String KEY_ACCOUNT = "account";
private static final String KEY_DISPLAY_NAME = "display_name";
private final SharedPreferences prefs;
private final String baseUrl;
public BossApiClient(Context context) {
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.baseUrl = BuildConfig.BOSS_API_BASE_URL;
}
public boolean hasSessionHints() {
return !getSessionCookie().isEmpty() || !getRestoreToken().isEmpty();
}
public ApiResponse autoLogin() throws IOException, JSONException {
ApiResponse response = request("POST", "/api/auth/login", new JSONObject(), false);
if (response.ok()) {
rememberIdentity(response.json);
}
return response;
}
public ApiResponse restoreSession() throws IOException, JSONException {
if (getRestoreToken().isEmpty()) {
return ApiResponse.error(401, new JSONObject().put("ok", false).put("message", "NO_RESTORE_TOKEN"));
}
JSONObject body = new JSONObject();
body.put("restoreToken", getRestoreToken());
ApiResponse response = request("POST", "/api/auth/restore", body, false);
if (response.ok()) {
rememberIdentity(response.json);
}
return response;
}
public ApiResponse getSession() throws IOException, JSONException {
return request("GET", "/api/auth/session", null, true);
}
public ApiResponse getConversations() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/conversations", null);
}
public ApiResponse getProjectDetail(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId), null);
}
public ApiResponse sendProjectMessage(String projectId, String body, String kind) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("body", body);
payload.put("kind", kind);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/messages", payload);
}
public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, String note) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("targetProjectId", targetProjectId);
payload.put("note", note);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", payload);
}
public ApiResponse getThreadDetail(String threadId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/threads/" + encode(threadId) + "/context-budget", null);
}
public ApiResponse toggleGoal(String projectId, String goalId) throws IOException, JSONException {
return requestWithRestore("POST", "/api/projects/" + encode(projectId) + "/goals/" + encode(goalId) + "/toggle", new JSONObject());
}
public ApiResponse createGoal(String projectId, String text) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("action", "create");
payload.put("text", text);
return requestWithRestore("POST", "/api/projects/" + encode(projectId) + "/goals/update", payload);
}
public ApiResponse updateGoal(String projectId, String goalId, String text) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("action", "update");
payload.put("goalId", goalId);
payload.put("text", text);
return requestWithRestore("POST", "/api/projects/" + encode(projectId) + "/goals/update", payload);
}
public ApiResponse getDevices() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/devices", null);
}
public ApiResponse getDeviceDetail(String deviceId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/devices?device=" + encode(deviceId), null);
}
public ApiResponse updateDevice(String deviceId, JSONObject payload) throws IOException, JSONException {
return requestWithRestore("PATCH", "/api/v1/devices/" + encode(deviceId), payload);
}
public ApiResponse getDeviceSkills(String deviceId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/devices/" + encode(deviceId) + "/skills", null);
}
public ApiResponse getDeviceEnrollments() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/devices/enrollments", null);
}
public ApiResponse createDeviceEnrollment(JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/devices/enrollments", payload);
}
public ApiResponse getAccounts() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/accounts", null);
}
public ApiResponse createAccount(JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/accounts", payload);
}
public ApiResponse updateAccount(String accountId, JSONObject payload) throws IOException, JSONException {
return requestWithRestore("PATCH", "/api/v1/accounts/" + encode(accountId), payload);
}
public ApiResponse deleteAccount(String accountId) throws IOException, JSONException {
return requestWithRestore("DELETE", "/api/v1/accounts/" + encode(accountId), null);
}
public ApiResponse activateAccount(String accountId, String reason) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("reason", reason);
return requestWithRestore("POST", "/api/v1/accounts/" + encode(accountId) + "/activate", payload);
}
public ApiResponse validateAccount(String accountId) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/accounts/" + encode(accountId) + "/validate", new JSONObject());
}
public ApiResponse getOpsSummary() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/ops/summary", null);
}
public ApiResponse approveRepairTicket(String ticketId) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/ops/repair-tickets/" + encode(ticketId) + "/approve", new JSONObject());
}
public ApiResponse verifyRepairTicket(String ticketId) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/ops/repair-tickets/" + encode(ticketId) + "/verify", new JSONObject());
}
public ApiResponse getAuditSummary() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/audits/summary", null);
}
public ApiResponse getSettings() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/settings", null);
}
public ApiResponse updateSettings(JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/settings", payload);
}
public ApiResponse getOtaStatus() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/user/ota", null);
}
public ApiResponse checkOta() throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("action", "check");
return requestWithRestore("POST", "/api/v1/user/ota", payload);
}
public ApiResponse applyOta() throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("action", "apply");
return requestWithRestore("POST", "/api/v1/user/ota", payload);
}
public ApiResponse logout() throws IOException, JSONException {
ApiResponse response = request("POST", "/api/auth/logout", new JSONObject(), false);
clearSession();
return response;
}
public String getAccountLabel() {
return prefs.getString(KEY_ACCOUNT, "17600003315");
}
public String getDisplayName() {
return prefs.getString(KEY_DISPLAY_NAME, "Boss 超级管理员");
}
public String getRestoreToken() {
return prefs.getString(KEY_RESTORE_TOKEN, "");
}
public String getSessionCookie() {
return prefs.getString(KEY_SESSION_COOKIE, "");
}
public String getBaseUrl() {
return baseUrl;
}
public String getProtectedOtaPackageUrl() {
return baseUrl + "/api/v1/user/ota/package";
}
private ApiResponse requestWithRestore(String method, String path, JSONObject body) throws IOException, JSONException {
ApiResponse response = request(method, path, body, true);
if (response.statusCode == 401 && !getRestoreToken().isEmpty()) {
ApiResponse restored = restoreSession();
if (restored.ok()) {
return request(method, path, body, true);
}
}
return response;
}
private ApiResponse request(String method, String path, JSONObject body, boolean expectProtected) throws IOException, JSONException {
HttpURLConnection connection = (HttpURLConnection) new URL(baseUrl + path).openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(12000);
connection.setReadTimeout(12000);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("x-boss-native-app", "1");
String cookie = getSessionCookie();
if (!cookie.isEmpty()) {
connection.setRequestProperty("Cookie", cookie);
}
if (body != null) {
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
try (OutputStream outputStream = connection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
writer.write(body.toString());
}
}
int statusCode = connection.getResponseCode();
captureSessionCookie(connection.getHeaderFields());
JSONObject json = readJson(statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream());
if (statusCode == 401 && !expectProtected) {
clearSession();
}
if (json != null) {
rememberIdentity(json);
}
return new ApiResponse(statusCode, json == null ? new JSONObject() : json);
}
private JSONObject readJson(InputStream stream) throws IOException, JSONException {
if (stream == null) {
return new JSONObject();
}
StringBuilder builder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
}
String raw = builder.toString().trim();
if (raw.isEmpty()) {
return new JSONObject();
}
return new JSONObject(raw);
}
private void captureSessionCookie(Map<String, List<String>> headers) {
if (headers == null) return;
List<String> setCookieHeaders = headers.get("Set-Cookie");
if (setCookieHeaders == null) {
setCookieHeaders = headers.get("set-cookie");
}
if (setCookieHeaders == null) return;
for (String header : setCookieHeaders) {
if (header == null || !header.startsWith("boss_session=")) continue;
String cookiePair = header.split(";", 2)[0];
if (header.contains("Max-Age=0")) {
clearSession();
return;
}
prefs.edit().putString(KEY_SESSION_COOKIE, cookiePair).apply();
return;
}
}
private void rememberIdentity(JSONObject json) {
if (json == null) return;
JSONObject session = json.optJSONObject("session");
JSONObject source = session != null ? session : json;
SharedPreferences.Editor editor = prefs.edit();
String restoreToken = source.optString("restoreToken", "");
if (!restoreToken.isEmpty()) {
editor.putString(KEY_RESTORE_TOKEN, restoreToken);
}
String account = source.optString("account", "");
if (!account.isEmpty()) {
editor.putString(KEY_ACCOUNT, account);
}
String displayName = source.optString("displayName", "");
if (!displayName.isEmpty()) {
editor.putString(KEY_DISPLAY_NAME, displayName);
}
editor.apply();
}
private void clearSession() {
prefs.edit()
.remove(KEY_SESSION_COOKIE)
.remove(KEY_RESTORE_TOKEN)
.apply();
}
private String encode(String value) {
return Uri.encode(value);
}
public static class ApiResponse {
public final int statusCode;
public final JSONObject json;
public ApiResponse(int statusCode, JSONObject json) {
this.statusCode = statusCode;
this.json = json;
}
public boolean ok() {
return statusCode >= 200 && statusCode < 300 && json.optBoolean("ok", false);
}
public String message() {
return json.optString("message", "UNKNOWN");
}
public static ApiResponse error(int statusCode, JSONObject json) {
return new ApiResponse(statusCode, json);
}
}
}

View File

@@ -0,0 +1,91 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public abstract class BossScreenActivity extends AppCompatActivity {
protected final ExecutorService executor = Executors.newSingleThreadExecutor();
protected BossApiClient apiClient;
protected Button backButton;
protected Button refreshButton;
protected Button headerActionButton;
protected TextView titleView;
protected TextView subtitleView;
protected SwipeRefreshLayout refreshLayout;
protected LinearLayout contentLayout;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screen);
apiClient = new BossApiClient(this);
backButton = findViewById(R.id.screen_back_button);
refreshButton = findViewById(R.id.screen_refresh_button);
headerActionButton = findViewById(R.id.screen_header_action);
titleView = findViewById(R.id.screen_title);
subtitleView = findViewById(R.id.screen_subtitle);
refreshLayout = findViewById(R.id.screen_refresh_layout);
contentLayout = findViewById(R.id.screen_content);
backButton.setOnClickListener(v -> finish());
refreshButton.setOnClickListener(v -> reload());
refreshLayout.setOnRefreshListener(this::reload);
}
@Override
protected void onDestroy() {
executor.shutdownNow();
super.onDestroy();
}
protected void configureScreen(String title, String subtitle) {
titleView.setText(title);
subtitleView.setText(subtitle == null || subtitle.isEmpty() ? "原生页面" : subtitle);
}
protected void setHeaderAction(String label, android.view.View.OnClickListener listener) {
headerActionButton.setVisibility(android.view.View.VISIBLE);
headerActionButton.setText(label);
headerActionButton.setOnClickListener(listener);
}
protected void hideHeaderAction() {
headerActionButton.setVisibility(android.view.View.GONE);
headerActionButton.setOnClickListener(null);
}
protected void setRefreshing(boolean refreshing) {
refreshLayout.setRefreshing(refreshing);
refreshButton.setEnabled(!refreshing);
refreshButton.setText(refreshing ? "同步中" : "刷新");
}
protected void replaceContent(android.view.View... views) {
contentLayout.removeAllViews();
for (android.view.View view : views) {
contentLayout.addView(view);
}
}
protected void appendContent(android.view.View view) {
contentLayout.addView(view);
}
protected void showMessage(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
protected abstract void reload();
}

View File

@@ -0,0 +1,177 @@
package com.hyzq.boss;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Typeface;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
public final class BossUi {
private BossUi() {}
public static LinearLayout buildCard(Context context, String title, String body, String meta) {
return buildCard(context, title, body, meta, null);
}
public static LinearLayout buildCard(
Context context,
String title,
String body,
String meta,
@Nullable View.OnClickListener listener
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 12);
card.setLayoutParams(params);
card.setPadding(dp(context, 18), dp(context, 18), dp(context, 18), dp(context, 18));
card.setBackgroundResource(R.drawable.bg_card);
if (listener != null) {
card.setClickable(true);
card.setFocusable(true);
card.setOnClickListener(listener);
}
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(18);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
TextView bodyView = new TextView(context);
bodyView.setText(body);
bodyView.setTextSize(14);
bodyView.setTextColor(context.getColor(R.color.boss_text_primary));
bodyView.setPadding(0, dp(context, 8), 0, 0);
TextView metaView = new TextView(context);
metaView.setText(meta);
metaView.setTextSize(12);
metaView.setTextColor(context.getColor(R.color.boss_text_muted));
metaView.setPadding(0, dp(context, 10), 0, 0);
card.addView(titleView);
card.addView(bodyView);
card.addView(metaView);
return card;
}
public static LinearLayout buildMenuRow(
Context context,
String title,
String description,
@Nullable String badge,
View.OnClickListener listener
) {
LinearLayout row = buildCard(context, title, description, badge == null ? "点击进入" : badge, listener);
row.setOrientation(LinearLayout.HORIZONTAL);
row.removeAllViews();
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
TextView descView = new TextView(context);
descView.setText(description);
descView.setTextSize(13);
descView.setTextColor(context.getColor(R.color.boss_text_muted));
descView.setPadding(0, dp(context, 6), 0, 0);
textWrap.addView(titleView);
textWrap.addView(descView);
TextView accessory = new TextView(context);
accessory.setText(badge == null ? "" : badge + " ");
accessory.setTextSize(13);
accessory.setTextColor(context.getColor(R.color.boss_green));
row.addView(textWrap);
row.addView(accessory);
return row;
}
public static LinearLayout buildEmptyCard(Context context, String text) {
return buildCard(context, "暂无内容", text, "下拉或点击顶部刷新按钮重试。");
}
public static Button buildPrimaryButton(Context context, String label) {
Button button = new Button(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 12);
button.setLayoutParams(params);
button.setText(label);
button.setTextColor(context.getColor(R.color.boss_surface));
button.setBackgroundResource(R.drawable.bg_primary_button);
button.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
button.setAllCaps(false);
return button;
}
public static Button buildSecondaryButton(Context context, String label) {
Button button = new Button(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 10);
button.setLayoutParams(params);
button.setText(label);
button.setTextColor(context.getColor(R.color.boss_green));
button.setBackgroundResource(R.drawable.bg_secondary_button);
button.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
button.setAllCaps(false);
return button;
}
public static EditText buildInput(Context context, String hint, boolean multiline) {
EditText input = new EditText(context);
input.setHint(hint);
input.setTextColor(context.getColor(R.color.boss_text_primary));
input.setHintTextColor(context.getColor(R.color.boss_text_muted));
input.setBackgroundResource(R.drawable.bg_secondary_button);
input.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
input.setSingleLine(!multiline);
if (multiline) {
input.setMinLines(3);
input.setMaxLines(6);
}
return input;
}
public static void copyText(Context context, String label, String text) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
clipboard.setPrimaryClip(ClipData.newPlainText(label, text));
Toast.makeText(context, label + " 已复制", Toast.LENGTH_SHORT).show();
}
}
public static int dp(Context context, int value) {
return Math.round(value * context.getResources().getDisplayMetrics().density);
}
}

View File

@@ -0,0 +1,211 @@
package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONArray;
import org.json.JSONObject;
public class DeviceDetailActivity extends BossScreenActivity {
public static final String EXTRA_DEVICE_ID = "device_id";
public static final String EXTRA_DEVICE_NAME = "device_name";
private String deviceId;
private String deviceName;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
deviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
configureScreen(deviceName == null ? "设备详情" : deviceName, "原生设备详情");
setHeaderAction("编辑", v -> openEditDialog());
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getDeviceDetail(deviceId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderDevice(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "设备详情加载失败:" + error.getMessage()));
});
}
});
}
private void renderDevice(JSONObject payload) {
JSONObject workspace = payload.optJSONObject("workspace");
JSONObject device = workspace == null ? null : workspace.optJSONObject("selectedDevice");
JSONArray relatedThreads = workspace == null ? null : workspace.optJSONArray("relatedThreads");
JSONObject enrollment = workspace == null ? null : workspace.optJSONObject("activeEnrollment");
replaceContent();
if (device == null) {
appendContent(BossUi.buildEmptyCard(this, "设备不存在。"));
setRefreshing(false);
return;
}
deviceName = device.optString("name", deviceId);
configureScreen(deviceName, device.optString("endpoint", "设备详情"));
appendContent(BossUi.buildCard(
this,
device.optString("name", "设备"),
device.optString("note", "暂无备注"),
"状态 " + device.optString("status", "unknown")
+ " · 账号 " + device.optString("account", "-")
+ " · 5h " + device.optInt("quota5h", 0)
+ " · 7d " + device.optInt("quota7d", 0)
));
Button skillsButton = BossUi.buildPrimaryButton(this, "查看技能");
skillsButton.setOnClickListener(v -> openSkills());
appendContent(skillsButton);
if (relatedThreads != null && relatedThreads.length() > 0) {
for (int i = 0; i < relatedThreads.length(); i++) {
JSONObject thread = relatedThreads.optJSONObject(i);
if (thread == null) continue;
appendContent(BossUi.buildCard(
this,
thread.optString("title", "线程"),
thread.optString("summary", ""),
thread.optString("workerId", "-")
+ " · " + thread.optInt("contextBudgetRemainingPct", 0) + "%"
+ " · " + thread.optString("contextBudgetLevel", "safe"),
v -> openThread(thread.optString("threadId"))
));
}
}
if (enrollment != null) {
appendContent(BossUi.buildCard(
this,
"当前绑定草稿",
"pairingCode " + enrollment.optString("pairingCode", "-")
+ "\ntoken " + enrollment.optString("token", "-"),
enrollment.optString("status", "ready")
+ " · 到期 " + enrollment.optString("expiresAt", "-")
));
}
setRefreshing(false);
}
private void openThread(String threadId) {
Intent intent = new Intent(this, ThreadDetailActivity.class);
intent.putExtra(ThreadDetailActivity.EXTRA_THREAD_ID, threadId);
startActivity(intent);
}
private void openSkills() {
Intent intent = new Intent(this, SkillInventoryActivity.class);
intent.putExtra(SkillInventoryActivity.EXTRA_DEVICE_ID, deviceId);
intent.putExtra(SkillInventoryActivity.EXTRA_DEVICE_NAME, deviceName);
startActivity(intent);
}
private void openEditDialog() {
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getDeviceDetail(deviceId);
if (!response.ok()) throw new IllegalStateException(response.message());
JSONObject workspace = response.json.optJSONObject("workspace");
JSONObject device = workspace == null ? null : workspace.optJSONObject("selectedDevice");
if (device == null) throw new IllegalStateException("DEVICE_NOT_FOUND");
runOnUiThread(() -> showEditForm(device));
} catch (Exception error) {
runOnUiThread(() -> showMessage("读取设备失败:" + error.getMessage()));
}
});
}
private void showEditForm(JSONObject device) {
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.setPadding(0, 0, 0, 0);
final android.widget.EditText nameInput = BossUi.buildInput(this, "设备名", false);
nameInput.setText(device.optString("name", ""));
final android.widget.EditText avatarInput = BossUi.buildInput(this, "头像字符", false);
avatarInput.setText(device.optString("avatar", ""));
final android.widget.EditText endpointInput = BossUi.buildInput(this, "endpoint", false);
endpointInput.setText(device.optString("endpoint", ""));
final android.widget.EditText noteInput = BossUi.buildInput(this, "备注", true);
noteInput.setText(device.optString("note", ""));
final android.widget.EditText projectsInput = BossUi.buildInput(this, "项目列表,逗号分隔", true);
projectsInput.setText(joinArray(device.optJSONArray("projects")));
form.addView(nameInput);
form.addView(avatarInput);
form.addView(endpointInput);
form.addView(noteInput);
form.addView(projectsInput);
new AlertDialog.Builder(this)
.setTitle("编辑设备")
.setView(form)
.setNegativeButton("取消", null)
.setPositiveButton("保存", (dialog, which) -> saveDevice(
nameInput.getText().toString().trim(),
avatarInput.getText().toString().trim(),
endpointInput.getText().toString().trim(),
noteInput.getText().toString().trim(),
projectsInput.getText().toString().trim()
))
.show();
}
private void saveDevice(String name, String avatar, String endpoint, String note, String projectsText) {
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("name", name);
payload.put("avatar", avatar);
payload.put("endpoint", endpoint);
payload.put("note", note);
JSONArray projects = new JSONArray();
for (String item : projectsText.split(",")) {
String trimmed = item.trim();
if (!trimmed.isEmpty()) projects.put(trimmed);
}
payload.put("projects", projects);
BossApiClient.ApiResponse response = apiClient.updateDevice(deviceId, payload);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("设备已更新");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
private String joinArray(@Nullable JSONArray values) {
if (values == null || values.length() == 0) return "";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < values.length(); i++) {
String value = values.optString(i);
if (value == null || value.isEmpty()) continue;
if (builder.length() > 0) builder.append(", ");
builder.append(value);
}
return builder.toString();
}
}

View File

@@ -0,0 +1,103 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.EditText;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class DeviceEnrollmentActivity extends BossScreenActivity {
private EditText nameInput;
private EditText avatarInput;
private EditText accountInput;
private EditText endpointInput;
private EditText noteInput;
private EditText projectsInput;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("添加设备", "通过 pairing code 或 token 把新设备接入");
hideHeaderAction();
buildForm();
}
@Override
protected void reload() {
// static form
}
private void buildForm() {
nameInput = BossUi.buildInput(this, "设备名,例如 Mac Studio", false);
avatarInput = BossUi.buildInput(this, "头像字符,例如 M", false);
accountInput = BossUi.buildInput(this, "账号", false);
accountInput.setText(apiClient.getAccountLabel());
endpointInput = BossUi.buildInput(this, "endpoint例如 mac://kris.local", false);
noteInput = BossUi.buildInput(this, "备注", true);
projectsInput = BossUi.buildInput(this, "项目列表,逗号分隔", true);
replaceContent(
BossUi.buildCard(
this,
"绑定新设备",
"支持通过 pairing code、临时 token 或登录引导把 Mac、Windows、云端节点接入。",
"当前原生页会直接调用 /api/v1/devices/enrollments"
),
nameInput,
avatarInput,
accountInput,
endpointInput,
noteInput,
projectsInput,
BossUi.buildPrimaryButton(this, "生成绑定草稿")
);
((android.widget.Button) contentLayout.getChildAt(contentLayout.getChildCount() - 1))
.setOnClickListener(v -> submitEnrollment());
}
private void submitEnrollment() {
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("name", nameInput.getText().toString().trim());
payload.put("avatar", avatarInput.getText().toString().trim());
payload.put("account", accountInput.getText().toString().trim());
payload.put("endpoint", endpointInput.getText().toString().trim());
payload.put("note", noteInput.getText().toString().trim());
JSONArray projects = new JSONArray();
for (String item : projectsInput.getText().toString().split(",")) {
String trimmed = item.trim();
if (!trimmed.isEmpty()) projects.put(trimmed);
}
payload.put("projects", projects);
BossApiClient.ApiResponse response = apiClient.createDeviceEnrollment(payload);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
JSONObject enrollment = response.json.optJSONObject("enrollment");
JSONObject device = response.json.optJSONObject("device");
replaceContent(
BossUi.buildCard(
this,
"绑定草稿已生成",
"设备 " + (device == null ? "-" : device.optString("name", "-"))
+ "\npairingCode " + (enrollment == null ? "-" : enrollment.optString("pairingCode", "-"))
+ "\ntoken " + (enrollment == null ? "-" : enrollment.optString("token", "-")),
enrollment == null ? "ready" : enrollment.optString("status", "ready")
+ " · 到期 " + enrollment.optString("expiresAt", "-")
)
);
setRefreshing(false);
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("创建失败:" + error.getMessage());
});
}
});
}
}

View File

@@ -0,0 +1,468 @@
package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
public static final String EXTRA_INITIAL_TAB = "initial_tab";
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private BossApiClient apiClient;
private View loginPanel;
private View contentPanel;
private TextView loginHint;
private Button loginButton;
private ProgressBar loginProgress;
private Button backButton;
private TextView topTitle;
private TextView topSubtitle;
private Button refreshButton;
private Button tabConversations;
private Button tabDevices;
private Button tabMe;
private SwipeRefreshLayout screenRefresh;
private LinearLayout screenContent;
private String activeTab = "conversations";
private @Nullable JSONObject sessionData;
private @Nullable JSONObject otaData;
private @Nullable JSONArray conversationsData;
private @Nullable JSONArray devicesData;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
apiClient = new BossApiClient(this);
bindViews();
bindActions();
applyInitialTab(getIntent());
bootstrapSession();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
applyInitialTab(intent);
if (contentPanel.getVisibility() == View.VISIBLE) {
renderCurrentTab();
}
}
@Override
protected void onDestroy() {
executor.shutdownNow();
super.onDestroy();
}
private void bindViews() {
loginPanel = findViewById(R.id.login_panel);
contentPanel = findViewById(R.id.content_panel);
loginHint = findViewById(R.id.login_hint);
loginButton = findViewById(R.id.login_button);
loginProgress = findViewById(R.id.login_progress);
backButton = findViewById(R.id.back_button);
topTitle = findViewById(R.id.top_title);
topSubtitle = findViewById(R.id.top_subtitle);
refreshButton = findViewById(R.id.refresh_button);
tabConversations = findViewById(R.id.tab_conversations);
tabDevices = findViewById(R.id.tab_devices);
tabMe = findViewById(R.id.tab_me);
screenRefresh = findViewById(R.id.screen_refresh);
screenContent = findViewById(R.id.screen_content);
}
private void bindActions() {
loginButton.setOnClickListener(v -> performAutoLogin());
backButton.setVisibility(View.GONE);
refreshButton.setOnClickListener(v -> refreshCurrentTab());
tabConversations.setOnClickListener(v -> switchTab("conversations"));
tabDevices.setOnClickListener(v -> switchTab("devices"));
tabMe.setOnClickListener(v -> switchTab("me"));
screenRefresh.setOnRefreshListener(this::refreshCurrentTab);
}
private void applyInitialTab(@Nullable Intent intent) {
String requested = intent == null ? null : intent.getStringExtra(EXTRA_INITIAL_TAB);
if ("devices".equals(requested) || "me".equals(requested) || "conversations".equals(requested)) {
activeTab = requested;
}
}
private void bootstrapSession() {
showLogin("原生 Android 客户端已启用。点击下方按钮直接进入系统。");
if (!apiClient.hasSessionHints()) {
return;
}
setLoginLoading(true, "正在恢复上次登录状态...");
executor.execute(() -> {
try {
BossApiClient.ApiResponse sessionResponse = apiClient.getSession();
if (!sessionResponse.ok()) {
sessionResponse = apiClient.restoreSession();
}
if (sessionResponse.ok()) {
JSONObject session = sessionResponse.json.optJSONObject("session");
runOnUiThread(() -> {
showContent();
refreshAllData(session);
});
return;
}
} catch (Exception ignored) {
// Fall back to login panel.
}
runOnUiThread(() -> setLoginLoading(false, "点击登录后会直接进入系统。"));
});
}
private void performAutoLogin() {
setLoginLoading(true, "正在创建会话...");
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.autoLogin();
if (response.ok()) {
JSONObject session = response.json.optJSONObject("session");
runOnUiThread(() -> {
showContent();
refreshAllData(session);
});
return;
}
runOnUiThread(() -> setLoginLoading(false, "登录失败:" + response.message()));
} catch (Exception error) {
runOnUiThread(() -> setLoginLoading(false, "登录链路异常:" + error.getMessage()));
}
});
}
private void refreshCurrentTab() {
refreshAllData(sessionData);
}
private void refreshAllData(@Nullable JSONObject initialSession) {
startRefreshing(true);
topSubtitle.setText("正在同步最新数据...");
executor.execute(() -> {
try {
JSONObject session = initialSession;
if (session == null) {
BossApiClient.ApiResponse sessionResponse = apiClient.getSession();
if (!sessionResponse.ok()) {
sessionResponse = apiClient.restoreSession();
}
if (!sessionResponse.ok()) {
throw new IOException("SESSION_UNAVAILABLE");
}
session = sessionResponse.json.optJSONObject("session");
}
BossApiClient.ApiResponse conversations = apiClient.getConversations();
BossApiClient.ApiResponse devices = apiClient.getDevices();
BossApiClient.ApiResponse ota = apiClient.getOtaStatus();
if (!conversations.ok() || !devices.ok() || !ota.ok()) {
throw new IOException("API_REFRESH_FAILED");
}
JSONObject finalSession = session;
runOnUiThread(() -> {
sessionData = finalSession;
conversationsData = conversations.json.optJSONArray("conversations");
devicesData = devices.json.optJSONArray("devices");
otaData = ota.json;
renderCurrentTab();
startRefreshing(false);
});
} catch (Exception error) {
runOnUiThread(() -> {
startRefreshing(false);
sessionData = null;
conversationsData = null;
devicesData = null;
otaData = null;
showLogin("当前登录已失效或同步失败,请重新点击登录。");
});
}
});
}
private void showLogin(String hint) {
loginPanel.setVisibility(View.VISIBLE);
contentPanel.setVisibility(View.GONE);
setLoginLoading(false, hint);
}
private void showContent() {
loginPanel.setVisibility(View.GONE);
contentPanel.setVisibility(View.VISIBLE);
switchTab(activeTab);
}
private void setLoginLoading(boolean loading, String hint) {
loginProgress.setVisibility(loading ? View.VISIBLE : View.GONE);
loginButton.setEnabled(!loading);
loginButton.setText(loading ? "处理中..." : "登录");
loginHint.setText(hint);
}
private void switchTab(String tab) {
activeTab = tab;
updateTabStyles();
renderCurrentTab();
}
private void renderCurrentTab() {
if (contentPanel.getVisibility() != View.VISIBLE) {
return;
}
switch (activeTab) {
case "devices":
updateHeader("设备", "只展示当前正式接入生产链路的设备。");
renderDevicesRoot();
break;
case "me":
updateHeader("我的", "账号、安全、技能、运维、OTA 都从这里进入。");
renderMeRoot();
break;
case "conversations":
default:
updateHeader("会话", "原生会话列表直接消费 /api/v1/conversations。");
renderConversationsRoot();
break;
}
}
private void updateHeader(String title, String subtitle) {
topTitle.setText(title);
topSubtitle.setText(subtitle);
}
private void updateTabStyles() {
styleTab(tabConversations, "conversations".equals(activeTab));
styleTab(tabDevices, "devices".equals(activeTab));
styleTab(tabMe, "me".equals(activeTab));
}
private void styleTab(Button button, boolean active) {
button.setBackgroundResource(active ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
button.setTextColor(getColor(active ? R.color.boss_surface : R.color.boss_green));
}
private void renderConversationsRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildCard(
this,
"会话首页",
"当前原生首页会直接进入项目详情、目标、版本、转发与线程预算详情。",
conversationsData == null ? "正在等待数据" : "会话数 " + conversationsData.length()
));
if (conversationsData == null || conversationsData.length() == 0) {
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有会话数据。"));
return;
}
for (int i = 0; i < conversationsData.length(); i++) {
JSONObject item = conversationsData.optJSONObject(i);
if (item == null) continue;
String projectId = item.optString("projectId", "");
String title = item.optString("projectTitle", "未命名会话");
StringBuilder body = new StringBuilder(item.optString("preview", "暂无预览"));
if (item.optInt("activeDeviceCount", 0) > 0) {
body.append("\n设备 ").append(item.optString("deviceNamesPreview", "未标注"));
}
JSONObject budget = item.optJSONObject("contextBudgetIndicator");
String meta = "风险 " + item.optString("riskLevel", "unknown")
+ " · 未读 " + item.optInt("unreadCount", 0)
+ " · " + item.optString("latestReplyLabel", "-");
if (budget != null && budget.optBoolean("visible", false)) {
meta = meta + " · 预算 " + budget.optInt("percent", 0) + "%";
}
screenContent.addView(BossUi.buildCard(this, title, body.toString(), meta, v -> {
if (projectId.isEmpty()) {
showMessage("缺少 projectId");
return;
}
openProject(projectId, title);
}));
}
}
private void renderDevicesRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildCard(
this,
"设备首页",
"设备详情、技能清单和配对草稿都改为原生页。",
devicesData == null ? "正在等待数据" : "设备数 " + devicesData.length()
));
Button addDeviceButton = BossUi.buildPrimaryButton(this, "添加设备");
addDeviceButton.setOnClickListener(v -> startActivity(new Intent(this, DeviceEnrollmentActivity.class)));
screenContent.addView(addDeviceButton);
if (devicesData == null || devicesData.length() == 0) {
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有接入设备。"));
return;
}
for (int i = 0; i < devicesData.length(); i++) {
JSONObject item = devicesData.optJSONObject(i);
if (item == null) continue;
String deviceId = item.optString("id", "");
String title = item.optString("name", "未命名设备");
String body = item.optString("note", item.optString("endpoint", "暂无设备说明"));
String meta = "状态 " + item.optString("status", "unknown")
+ " · 账号 " + item.optString("account", "-")
+ " · 5h " + item.optInt("quota5h", 0)
+ " · 7d " + item.optInt("quota7d", 0);
screenContent.addView(BossUi.buildCard(this, title, body, meta, v -> {
if (deviceId.isEmpty()) {
showMessage("缺少 deviceId");
return;
}
openDevice(deviceId, title);
}));
}
}
private void renderMeRoot() {
screenContent.removeAllViews();
String displayName = sessionData == null
? apiClient.getDisplayName()
: sessionData.optString("displayName", apiClient.getDisplayName());
String account = sessionData == null
? apiClient.getAccountLabel()
: sessionData.optString("account", apiClient.getAccountLabel());
String expiresAt = sessionData == null ? "-" : sessionData.optString("expiresAt", "-");
screenContent.addView(BossUi.buildCard(
this,
displayName,
"账号 " + account + "\n当前原生客户端已覆盖会话 / 设备 / 我的一级导航。",
"会话到期 " + expiresAt
));
screenContent.addView(BossUi.buildMenuRow(
this,
"账号与安全",
"查看当前会话、登录模式和退出登录。",
null,
v -> startActivity(new Intent(this, SecurityActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"设置",
"实时刷新、风险徽标和默认首页。",
null,
v -> startActivity(new Intent(this, SettingsActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"运维与修复",
"查看故障、repair ticket、审计请求和能力注册表。",
null,
v -> startActivity(new Intent(this, OpsCenterActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"AI 账号",
"管理主 GPT、备用 GPT、Master Codex Node 与 API 容灾。",
null,
v -> startActivity(new Intent(this, AiAccountsActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"技能",
"按绑定设备查看 Skill并一键复制调用语句。",
null,
v -> startActivity(new Intent(this, SkillInventoryActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"关于",
"查看版本、OTA 状态和当前绑定节点。",
otaData == null ? null : otaData.optBoolean("hasOta", false) ? "OTA" : null,
v -> startActivity(new Intent(this, AboutActivity.class))
));
if (otaData != null) {
JSONObject availableRelease = otaData.optJSONObject("availableRelease");
String body = "当前版本 " + otaData.optString("currentVersion", "-");
String meta = availableRelease == null
? "当前没有待安装版本"
: "可用版本 " + availableRelease.optString("version", "-")
+ " · 文件 " + availableRelease.optString("packageFileName", "-");
screenContent.addView(BossUi.buildCard(this, "OTA 状态", body, meta));
}
Button logoutButton = BossUi.buildSecondaryButton(this, "退出登录");
logoutButton.setOnClickListener(v -> logout());
screenContent.addView(logoutButton);
}
private void logout() {
startRefreshing(true);
executor.execute(() -> {
try {
apiClient.logout();
} catch (Exception ignored) {
// Ignore transport errors and still clear UI.
}
runOnUiThread(() -> {
startRefreshing(false);
sessionData = null;
conversationsData = null;
devicesData = null;
otaData = null;
showLogin("已退出登录。点击登录可重新进入系统。");
});
});
}
private void openProject(String projectId, String projectName) {
Intent intent = new Intent(this, ProjectDetailActivity.class);
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, projectId);
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, projectName);
startActivity(intent);
}
private void openDevice(String deviceId, String deviceName) {
Intent intent = new Intent(this, DeviceDetailActivity.class);
intent.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, deviceId);
intent.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, deviceName);
startActivity(intent);
}
private void startRefreshing(boolean refreshing) {
screenRefresh.setRefreshing(refreshing);
refreshButton.setEnabled(!refreshing);
refreshButton.setText(refreshing ? "同步中" : "刷新");
}
private void showMessage(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,326 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class OpsCenterActivity extends BossScreenActivity {
private enum Tab {
OPS,
AUDIT
}
private Tab activeTab = Tab.OPS;
private LinearLayout contentRoot;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("运维中心", "运维对话 / 审计对话");
setHeaderAction("刷新", v -> reload());
contentRoot = new LinearLayout(this);
contentRoot.setOrientation(LinearLayout.VERTICAL);
replaceContent(contentRoot);
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse ops = apiClient.getOpsSummary();
BossApiClient.ApiResponse audit = apiClient.getAuditSummary();
if (!ops.ok() || !audit.ok()) {
throw new IllegalStateException("OPS_OR_AUDIT_LOAD_FAILED");
}
runOnUiThread(() -> render(ops.json, audit.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "运维中心加载失败:" + error.getMessage()));
});
}
});
}
private void render(JSONObject ops, JSONObject audit) {
contentRoot.removeAllViews();
contentRoot.addView(buildTabBar());
if (activeTab == Tab.OPS) {
renderOpsTab(ops);
} else {
renderAuditTab(audit);
}
setRefreshing(false);
}
private LinearLayout buildTabBar() {
LinearLayout bar = new LinearLayout(this);
bar.setOrientation(LinearLayout.HORIZONTAL);
bar.addView(buildTabButton("运维对话", activeTab == Tab.OPS, v -> {
activeTab = Tab.OPS;
reload();
}));
bar.addView(buildTabButton("审计对话", activeTab == Tab.AUDIT, v -> {
activeTab = Tab.AUDIT;
reload();
}));
return bar;
}
private Button buildTabButton(String label, boolean active, android.view.View.OnClickListener listener) {
Button button = BossUi.buildPrimaryButton(this, label);
button.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
button.setBackgroundResource(active ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
button.setTextColor(getColor(active ? R.color.boss_surface : R.color.boss_green));
button.setOnClickListener(listener);
return button;
}
private void renderOpsTab(JSONObject ops) {
contentRoot.addView(BossUi.buildCard(
this,
"当前巡检模式",
ops.optString("mode", "idle").equals("active")
? "active当前存在风险线程或未关闭运维工单。"
: "idle当前没有高风险工单保持低频巡检。",
"来源:/api/v1/ops/summary"
));
JSONArray faults = ops.optJSONArray("faults");
if (faults == null || faults.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有运维故障。"));
} else {
for (int i = 0; i < faults.length(); i++) {
JSONObject fault = faults.optJSONObject(i);
if (fault == null) continue;
contentRoot.addView(buildFaultCard(fault, ops.optJSONArray("tickets")));
}
}
}
private LinearLayout buildFaultCard(JSONObject fault, @Nullable JSONArray tickets) {
LinearLayout card = BossUi.buildCard(
this,
fault.optString("faultKey", "故障"),
fault.optString("summary", "暂无摘要"),
fault.optString("severity", "-")
+ " · " + fault.optString("status", "-")
+ " · " + fault.optString("nodeId", "-")
+ " · " + fault.optString("serviceName", "-")
);
card.addView(BossUi.buildCard(
this,
"建议动作",
fault.optString("suggestedNextAction", "暂无"),
"trace " + fault.optString("traceId", "-")
));
if (tickets != null) {
for (int i = 0; i < tickets.length(); i++) {
JSONObject ticket = tickets.optJSONObject(i);
if (ticket == null) continue;
if (!fault.optString("faultId").equals(ticket.optString("faultId"))) continue;
card.addView(buildTicketCard(ticket));
}
}
return card;
}
private LinearLayout buildTicketCard(JSONObject ticket) {
LinearLayout card = BossUi.buildCard(
this,
ticket.optString("title", "修复工单"),
ticket.optString("actionSummary", "暂无动作摘要"),
ticket.optString("approvalStatus", "-")
+ " · " + ticket.optString("executionStatus", "-")
+ " · " + ticket.optString("targetNodeId", "-")
);
if (ticket.optJSONObject("verification") != null) {
JSONObject verification = ticket.optJSONObject("verification");
card.addView(BossUi.buildCard(
this,
"验证结果",
verification.optString("summary", "暂无"),
verification.optString("status", "-")
+ " · " + verification.optString("verifiedAt", "-")
));
}
Button approve = BossUi.buildPrimaryButton(this, "批准修复");
approve.setOnClickListener(v -> approveTicket(ticket.optString("ticketId")));
card.addView(approve);
Button verify = BossUi.buildSecondaryButton(this, "验证修复");
verify.setOnClickListener(v -> verifyTicket(ticket.optString("ticketId")));
card.addView(verify);
return card;
}
private void renderAuditTab(JSONObject audit) {
contentRoot.addView(BossUi.buildCard(
this,
"审计概要",
"待处理请求 " + (audit.optJSONArray("pendingRequests") == null ? 0 : audit.optJSONArray("pendingRequests").length())
+ "\n最新结果 " + (audit.optJSONArray("latestResults") == null ? 0 : audit.optJSONArray("latestResults").length()),
"来源:/api/v1/audits/summary"
));
JSONArray pendingRequests = audit.optJSONArray("pendingRequests");
if (pendingRequests == null || pendingRequests.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待处理的审计请求。"));
} else {
for (int i = 0; i < pendingRequests.length(); i++) {
JSONObject request = pendingRequests.optJSONObject(i);
if (request == null) continue;
contentRoot.addView(buildAuditRequestCard(request));
}
}
JSONArray latestResults = audit.optJSONArray("latestResults");
if (latestResults != null && latestResults.length() > 0) {
contentRoot.addView(BossUi.buildCard(this, "审计结果", "最近完成的审计会展示在这里。", "可回看 decision / findings"));
for (int i = 0; i < latestResults.length(); i++) {
JSONObject result = latestResults.optJSONObject(i);
if (result == null) continue;
contentRoot.addView(buildAuditResultCard(result));
}
}
JSONArray capabilities = audit.optJSONArray("capabilities");
if (capabilities != null && capabilities.length() > 0) {
contentRoot.addView(BossUi.buildCard(this, "能力注册表", "展示当前设备上的可用能力。", "与审计请求的 capabilityRequirements 对应"));
for (int i = 0; i < capabilities.length(); i++) {
JSONObject capability = capabilities.optJSONObject(i);
if (capability == null) continue;
contentRoot.addView(BossUi.buildCard(
this,
capability.optString("displayName", "能力"),
capability.optString("capabilityType", "-")
+ "\n提供者" + capability.optString("providerId", "-")
+ "\n模式" + capability.optString("leaseMode", "-")
+ "\n动作" + joinArray(capability.optJSONArray("supportedActions")),
capability.optString("status", "-")
+ " · " + capability.optString("healthStatus", "-")
+ " · " + capability.optString("nodeId", "-")
));
}
}
}
private LinearLayout buildAuditRequestCard(JSONObject request) {
LinearLayout card = BossUi.buildCard(
this,
request.optString("projectName", "审计请求"),
request.optString("objective", "暂无目标"),
request.optString("auditType", "-")
+ " · priority " + request.optInt("priority", 0)
+ " · " + request.optString("trigger", "-")
);
card.addView(BossUi.buildCard(
this,
"审计条件",
"要求:" + joinStringArray(request.optJSONArray("acceptanceCriteria"))
+ "\n风险" + joinStringArray(request.optJSONArray("riskFocus"))
+ "\n证据" + joinStringArray(request.optJSONArray("evidenceRefs")),
"时限 " + request.optInt("timeBudgetSeconds", 0) + ""
));
return card;
}
private LinearLayout buildAuditResultCard(JSONObject result) {
LinearLayout card = BossUi.buildCard(
this,
result.optString("decision", "result"),
result.optString("summary", "暂无摘要"),
result.optString("status", "-")
+ " · confidence " + result.optDouble("confidence", 0.0)
+ " · " + result.optString("completedAt", "-")
);
card.addView(BossUi.buildCard(
this,
"审计发现",
joinStringArray(result.optJSONArray("findings")),
"需要动作:" + joinStringArray(result.optJSONArray("requiredActions"))
));
return card;
}
private void approveTicket(String ticketId) {
if (ticketId == null || ticketId.isEmpty()) {
showMessage("缺少 ticketId");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.approveRepairTicket(ticketId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("修复工单已批准");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("批准失败:" + error.getMessage());
});
}
});
}
private void verifyTicket(String ticketId) {
if (ticketId == null || ticketId.isEmpty()) {
showMessage("缺少 ticketId");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.verifyRepairTicket(ticketId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("修复结果已验证");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("验证失败:" + error.getMessage());
});
}
});
}
private String joinArray(@Nullable JSONArray values) {
if (values == null || values.length() == 0) return "-";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < values.length(); i++) {
String value = values.optString(i);
if (value == null || value.isEmpty()) continue;
if (builder.length() > 0) builder.append(" · ");
builder.append(value);
}
return builder.length() == 0 ? "-" : builder.toString();
}
private String joinStringArray(@Nullable JSONArray values) {
if (values == null || values.length() == 0) return "-";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < values.length(); i++) {
String value = values.optString(i);
if (value == null || value.isEmpty()) continue;
if (builder.length() > 0) builder.append("");
builder.append(value);
}
return builder.length() == 0 ? "-" : builder.toString();
}
}

View File

@@ -0,0 +1,285 @@
package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class ProjectDetailActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private String projectId;
private String initialProjectName;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
initialProjectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
configureScreen(initialProjectName == null ? "项目详情" : initialProjectName, "正在同步项目详情...");
setHeaderAction("发消息", v -> chooseMessageKindAndSend());
reload();
}
@Override
protected void reload() {
if (projectId == null || projectId.isEmpty()) {
showMessage("缺少 projectId");
finish();
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getProjectDetail(projectId);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> renderProject(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "项目详情加载失败:" + error.getMessage()));
});
}
});
}
private void renderProject(JSONObject payload) {
JSONObject project = payload.optJSONObject("project");
JSONArray devices = payload.optJSONArray("devices");
JSONArray threadContexts = payload.optJSONArray("activeThreadContexts");
JSONArray recentLogs = payload.optJSONArray("recentAppLogs");
String title = project != null ? project.optString("name", "项目详情") : "项目详情";
String subtitle = "设备:" + joinDeviceNames(devices);
configureScreen(title, subtitle);
replaceContent();
appendContent(buildActionGrid());
JSONObject masterIdentity = payload.optJSONObject("masterIdentity");
if (masterIdentity != null) {
String body = masterIdentity.optString("roleLabel", "主控")
+ " · " + masterIdentity.optString("displayName", "-")
+ (masterIdentity.optString("nodeLabel").isEmpty() ? "" : " · " + masterIdentity.optString("nodeLabel"))
+ (masterIdentity.optString("model").isEmpty() ? "" : "\n模型 " + masterIdentity.optString("model"));
String meta = masterIdentity.optString("statusLabel", "")
+ (masterIdentity.optString("lastSwitchedAt").isEmpty() ? "" : " · 最近切换 " + masterIdentity.optString("lastSwitchedAt"));
appendContent(BossUi.buildCard(this, "当前主控身份", body, meta));
}
appendContent(BossUi.buildCard(
this,
"主 Agent 调度结论",
payload.optString("masterContextStrategySummary", "暂无调度摘要。"),
"原生项目详情已接入 /api/v1/projects/{projectId}"
));
if (threadContexts != null && threadContexts.length() > 0) {
for (int i = 0; i < threadContexts.length(); i++) {
JSONObject thread = threadContexts.optJSONObject(i);
if (thread == null) continue;
JSONObject snapshot = thread.optJSONObject("snapshot");
if (snapshot == null) continue;
String threadId = snapshot.optString("threadId");
String body = snapshot.optString("summary", "暂无摘要");
String meta = snapshot.optString("workerId", "-")
+ " · " + snapshot.optString("nodeId", "-")
+ " · " + snapshot.optInt("contextBudgetRemainingPct", 0) + "%"
+ " · " + snapshot.optString("contextBudgetLevel", "safe");
appendContent(BossUi.buildCard(
this,
snapshot.optString("title", "线程详情"),
body,
meta,
v -> openThread(threadId)
));
}
} else {
appendContent(BossUi.buildEmptyCard(this, "当前项目还没有线程预算数据。"));
}
if (recentLogs != null && recentLogs.length() > 0) {
for (int i = 0; i < recentLogs.length(); i++) {
JSONObject log = recentLogs.optJSONObject(i);
if (log == null) continue;
String body = log.optString("message", "无消息体");
if (!log.optString("detail").isEmpty()) {
body = body + "\n" + log.optString("detail");
}
String meta = log.optString("deviceId", "-")
+ " · " + log.optString("category", "-")
+ " · " + log.optString("createdAt", "-");
appendContent(BossUi.buildCard(this, "实时 APP 日志", body, meta));
}
}
JSONArray messages = project == null ? null : project.optJSONArray("messages");
if (messages != null && messages.length() > 0) {
for (int i = 0; i < messages.length(); i++) {
JSONObject message = messages.optJSONObject(i);
if (message == null) continue;
String meta = message.optString("sentAt", "-")
+ (message.optString("kind").isEmpty() ? "" : " · " + message.optString("kind"));
appendContent(BossUi.buildCard(
this,
message.optString("senderLabel", "消息"),
message.optString("body", ""),
meta
));
}
}
appendContent(BossUi.buildCard(
this,
"媒体与转发说明",
"语音、图片、视频与转发现在都通过原生入口触发,并写回现有 Boss 消息账本。",
"对象存储与真实媒体文件仍保持 MVP 占位。"
));
setRefreshing(false);
}
private LinearLayout buildActionGrid() {
LinearLayout wrapper = new LinearLayout(this);
wrapper.setOrientation(LinearLayout.VERTICAL);
wrapper.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
wrapper.addView(buildActionRow(
buildActionButton("发送消息", v -> chooseMessageKindAndSend()),
buildActionButton("项目目标", v -> openGoals())
));
wrapper.addView(buildActionRow(
buildActionButton("版本记录", v -> openVersions()),
buildActionButton("消息转发", v -> openForward())
));
return wrapper;
}
private LinearLayout buildActionRow(Button left, Button right) {
LinearLayout row = new LinearLayout(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = BossUi.dp(this, 12);
row.setLayoutParams(params);
row.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams childParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
childParams.rightMargin = BossUi.dp(this, 6);
left.setLayoutParams(childParams);
LinearLayout.LayoutParams rightParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
rightParams.leftMargin = BossUi.dp(this, 6);
right.setLayoutParams(rightParams);
row.addView(left);
row.addView(right);
return row;
}
private Button buildActionButton(String label, android.view.View.OnClickListener listener) {
Button button = BossUi.buildPrimaryButton(this, label);
button.setOnClickListener(listener);
return button;
}
private void chooseMessageKindAndSend() {
final String[] labels = {"文本消息", "语音意图", "图片意图", "视频意图"};
final String[] kinds = {"text", "voice_intent", "image_intent", "video_intent"};
new AlertDialog.Builder(this)
.setTitle("选择消息类型")
.setItems(labels, (dialog, which) -> showSendDialog(kinds[which], labels[which]))
.setNegativeButton("取消", null)
.show();
}
private void showSendDialog(String kind, String label) {
final android.widget.EditText input = BossUi.buildInput(this, "请输入要发送给项目的内容", true);
new AlertDialog.Builder(this)
.setTitle("发送" + label)
.setView(input)
.setNegativeButton("取消", null)
.setPositiveButton("发送", (dialog, which) -> sendProjectMessage(kind, input.getText().toString().trim()))
.show();
}
private void sendProjectMessage(String kind, String body) {
if (body.isEmpty()) {
showMessage("请输入消息内容");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.sendProjectMessage(projectId, body, kind);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
setRefreshing(false);
showMessage("消息已发送");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("发送失败:" + error.getMessage());
});
}
});
}
private void openGoals() {
Intent intent = new Intent(this, ProjectGoalsActivity.class);
intent.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_ID, projectId);
intent.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_NAME, initialProjectName);
startActivity(intent);
}
private void openVersions() {
Intent intent = new Intent(this, ProjectVersionsActivity.class);
intent.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_ID, projectId);
intent.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_NAME, initialProjectName);
startActivity(intent);
}
private void openForward() {
Intent intent = new Intent(this, ProjectForwardActivity.class);
intent.putExtra(ProjectForwardActivity.EXTRA_PROJECT_ID, projectId);
intent.putExtra(ProjectForwardActivity.EXTRA_PROJECT_NAME, initialProjectName);
startActivity(intent);
}
private void openThread(String threadId) {
Intent intent = new Intent(this, ThreadDetailActivity.class);
intent.putExtra(ThreadDetailActivity.EXTRA_THREAD_ID, threadId);
intent.putExtra(ThreadDetailActivity.EXTRA_PROJECT_ID, projectId);
startActivity(intent);
}
private String joinDeviceNames(@Nullable JSONArray devices) {
if (devices == null || devices.length() == 0) {
return "未绑定设备";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < devices.length(); i++) {
JSONObject device = devices.optJSONObject(i);
if (device == null) continue;
if (builder.length() > 0) builder.append(" / ");
builder.append(device.optString("name", device.optString("id", "设备")));
}
return builder.length() == 0 ? "未绑定设备" : builder.toString();
}
}

View File

@@ -0,0 +1,106 @@
package com.hyzq.boss;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONArray;
import org.json.JSONObject;
public class ProjectForwardActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private String projectId;
private String projectName;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
configureScreen("消息转发", projectName == null ? "选择目标项目并写备注" : "源项目:" + projectName);
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getConversations();
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderTargets(response.json.optJSONArray("conversations")));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "转发目标加载失败:" + error.getMessage()));
});
}
});
}
private void renderTargets(@Nullable JSONArray conversations) {
replaceContent(BossUi.buildCard(
this,
"原生转发入口",
"选择一个目标项目,填写备注后会走现有 `/api/v1/projects/{projectId}/forwards`。",
"源项目:" + (projectName == null ? projectId : projectName)
));
if (conversations == null || conversations.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前没有可转发的目标项目。"));
setRefreshing(false);
return;
}
for (int i = 0; i < conversations.length(); i++) {
JSONObject item = conversations.optJSONObject(i);
if (item == null) continue;
String targetProjectId = item.optString("projectId");
if (projectId.equals(targetProjectId)) continue;
appendContent(BossUi.buildCard(
this,
item.optString("projectTitle", "未命名项目"),
item.optString("preview", ""),
item.optString("latestReplyLabel", "最近更新"),
v -> openForwardDialog(targetProjectId, item.optString("projectTitle", targetProjectId))
));
}
setRefreshing(false);
}
private void openForwardDialog(String targetProjectId, String targetTitle) {
final android.widget.EditText input = BossUi.buildInput(this, "请输入要附带的转发说明", true);
input.setText("请同步关注 " + targetTitle + " 的当前进展。");
new AlertDialog.Builder(this)
.setTitle("转发到 " + targetTitle)
.setView(input)
.setNegativeButton("取消", null)
.setPositiveButton("转发", (dialog, which) -> forwardMessage(targetProjectId, input.getText().toString().trim()))
.show();
}
private void forwardMessage(String targetProjectId, String note) {
if (note.isEmpty()) {
showMessage("请先填写转发说明");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.forwardProjectMessage(projectId, targetProjectId, note);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
setRefreshing(false);
showMessage("转发成功");
finish();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("转发失败:" + error.getMessage());
});
}
});
}
}

View File

@@ -0,0 +1,167 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONArray;
import org.json.JSONObject;
public class ProjectGoalsActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private String projectId;
private String projectName;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
configureScreen("项目目标", projectName == null ? "原生目标清单" : projectName);
setHeaderAction("新增", v -> openGoalEditor(null, ""));
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getProjectDetail(projectId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderGoals(response.json.optJSONObject("project")));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "目标清单加载失败:" + error.getMessage()));
});
}
});
}
private void renderGoals(@Nullable JSONObject project) {
replaceContent();
if (project == null) {
appendContent(BossUi.buildEmptyCard(this, "项目不存在。"));
setRefreshing(false);
return;
}
JSONArray goals = project.optJSONArray("goals");
int completedCount = 0;
if (goals != null) {
for (int i = 0; i < goals.length(); i++) {
JSONObject goal = goals.optJSONObject(i);
if (goal != null && "completed".equals(goal.optString("state"))) {
completedCount++;
}
}
}
appendContent(BossUi.buildCard(
this,
"主 Agent 已整理项目目标",
"已完成 " + completedCount + "/" + (goals == null ? 0 : goals.length()),
"用户可编辑,点按钮即可标记完成或修改正文。"
));
if (goals == null || goals.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前项目还没有目标。点击右上角新增即可。"));
} else {
for (int i = 0; i < goals.length(); i++) {
JSONObject goal = goals.optJSONObject(i);
if (goal == null) continue;
appendContent(buildGoalCard(goal));
}
}
appendContent(BossUi.buildCard(
this,
"当前约束",
"• 只能使用已绑定设备\n• 审计证据必须可回放\n• 版本记录仅主 Agent 可发布",
"原生目标页已覆盖 Web 目标清单"
));
setRefreshing(false);
}
private LinearLayout buildGoalCard(JSONObject goal) {
LinearLayout card = BossUi.buildCard(
this,
goal.optString("text", "未命名目标"),
goal.optString("note", "暂无备注"),
"状态 " + goal.optString("state", "pending")
);
Button toggle = BossUi.buildPrimaryButton(
this,
"completed".equals(goal.optString("state")) ? "标记未完成" : "标记完成"
);
toggle.setOnClickListener(v -> toggleGoal(goal.optString("id")));
card.addView(toggle);
Button edit = BossUi.buildSecondaryButton(this, "编辑目标");
edit.setOnClickListener(v -> openGoalEditor(goal.optString("id"), goal.optString("text")));
card.addView(edit);
return card;
}
private void toggleGoal(String goalId) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.toggleGoal(projectId, goalId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("目标状态已更新");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("更新失败:" + error.getMessage());
});
}
});
}
private void openGoalEditor(@Nullable String goalId, String currentText) {
final android.widget.EditText input = BossUi.buildInput(this, "请输入目标正文", true);
input.setText(currentText);
new AlertDialog.Builder(this)
.setTitle(goalId == null ? "新增目标" : "编辑目标")
.setView(input)
.setNegativeButton("取消", null)
.setPositiveButton("保存", (dialog, which) -> saveGoal(goalId, input.getText().toString().trim()))
.show();
}
private void saveGoal(@Nullable String goalId, String text) {
if (text.isEmpty()) {
showMessage("目标正文不能为空");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = goalId == null
? apiClient.createGoal(projectId, text)
: apiClient.updateGoal(projectId, goalId, text);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage(goalId == null ? "目标已新增" : "目标已更新");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
}

View File

@@ -0,0 +1,71 @@
package com.hyzq.boss;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class ProjectVersionsActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private String projectId;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
configureScreen("版本迭代记录", getIntent().getStringExtra(EXTRA_PROJECT_NAME));
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getProjectDetail(projectId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderVersions(response.json.optJSONObject("project")));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "版本记录加载失败:" + error.getMessage()));
});
}
});
}
private void renderVersions(@Nullable JSONObject project) {
replaceContent(BossUi.buildCard(
this,
"版本记录只读",
"版本记录由主 Agent 监督各线程提交,并在复核后自动发布。",
"原生版本页仅展示,不允许手工篡改正文。"
));
if (project == null) {
appendContent(BossUi.buildEmptyCard(this, "项目不存在。"));
setRefreshing(false);
return;
}
JSONArray versions = project.optJSONArray("versions");
if (versions == null || versions.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前项目还没有版本记录。"));
setRefreshing(false);
return;
}
for (int i = 0; i < versions.length(); i++) {
JSONObject item = versions.optJSONObject(i);
if (item == null) continue;
appendContent(BossUi.buildCard(
this,
item.optString("version", "未命名版本"),
item.optString("summary", ""),
item.optString("createdAt", "-")
));
}
setRefreshing(false);
}
}

View File

@@ -0,0 +1,87 @@
package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.json.JSONObject;
public class SecurityActivity extends BossScreenActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("账号与安全", "原生会话与设备安全");
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getSession();
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderSecurity(response.json.optJSONObject("session")));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "安全信息加载失败:" + error.getMessage()));
});
}
});
}
private void renderSecurity(@Nullable JSONObject session) {
replaceContent(
BossUi.buildCard(
this,
"当前登录模式",
"当前登录页已临时切到免验证模式,点击登录会直接创建最高管理员会话。",
"后续如收口认证,再切回账号密码 / 验证码登录。"
)
);
if (session != null) {
appendContent(BossUi.buildCard(
this,
"当前会话",
"账号 " + session.optString("account", "-")
+ "\n角色 " + session.optString("role", "-")
+ "\n登录方式 " + session.optString("loginMethod", "-"),
"到期 " + session.optString("expiresAt", "-")
));
}
android.widget.Button devicesButton = BossUi.buildPrimaryButton(this, "打开设备页");
devicesButton.setOnClickListener(v -> {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_INITIAL_TAB, "devices");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
});
appendContent(devicesButton);
android.widget.Button logoutButton = BossUi.buildSecondaryButton(this, "退出登录");
logoutButton.setOnClickListener(v -> logout());
appendContent(logoutButton);
setRefreshing(false);
}
private void logout() {
setRefreshing(true);
executor.execute(() -> {
try {
apiClient.logout();
} catch (Exception ignored) {
// ignore
}
runOnUiThread(() -> {
setRefreshing(false);
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
});
});
}
}

View File

@@ -0,0 +1,120 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONObject;
public class SettingsActivity extends BossScreenActivity {
private SwitchCompat liveUpdatesSwitch;
private SwitchCompat riskBadgesSwitch;
private SwitchCompat confirmActionsSwitch;
private Spinner preferredEntrySpinner;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("设置", "原生偏好配置");
setHeaderAction("保存", v -> saveSettings());
buildForm();
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getSettings();
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> populate(response.json.optJSONObject("settings")));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "设置加载失败:" + error.getMessage()));
});
}
});
}
private void buildForm() {
replaceContent(
BossUi.buildCard(
this,
"设置说明",
"当前设置会持久化到 data/boss-state.json下一线程接手不会丢失。",
"原生设置页直接走 /api/v1/settings"
)
);
liveUpdatesSwitch = new SwitchCompat(this);
liveUpdatesSwitch.setText("启用实时刷新");
riskBadgesSwitch = new SwitchCompat(this);
riskBadgesSwitch.setText("显示风险徽标");
confirmActionsSwitch = new SwitchCompat(this);
confirmActionsSwitch.setText("危险操作前确认");
preferredEntrySpinner = new Spinner(this);
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_spinner_dropdown_item,
new String[]{"conversations", "devices", "me"}
);
preferredEntrySpinner.setAdapter(adapter);
LinearLayout card = BossUi.buildCard(this, "交互偏好", "可切换默认首页与提醒行为。", "保存后立即生效");
card.addView(liveUpdatesSwitch);
card.addView(riskBadgesSwitch);
card.addView(confirmActionsSwitch);
card.addView(preferredEntrySpinner);
appendContent(card);
}
private void populate(@Nullable JSONObject settings) {
if (settings != null) {
liveUpdatesSwitch.setChecked(settings.optBoolean("liveUpdates", true));
riskBadgesSwitch.setChecked(settings.optBoolean("showRiskBadges", true));
confirmActionsSwitch.setChecked(settings.optBoolean("confirmDangerousActions", true));
String preferredEntry = settings.optString("preferredEntryPoint", "conversations");
if ("devices".equals(preferredEntry)) {
preferredEntrySpinner.setSelection(1);
} else if ("me".equals(preferredEntry)) {
preferredEntrySpinner.setSelection(2);
} else {
preferredEntrySpinner.setSelection(0);
}
}
setRefreshing(false);
}
private void saveSettings() {
setRefreshing(true);
executor.execute(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("liveUpdates", liveUpdatesSwitch.isChecked());
payload.put("showRiskBadges", riskBadgesSwitch.isChecked());
payload.put("confirmDangerousActions", confirmActionsSwitch.isChecked());
payload.put("preferredEntryPoint", preferredEntrySpinner.getSelectedItem().toString());
BossApiClient.ApiResponse response = apiClient.updateSettings(payload);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
setRefreshing(false);
showMessage("设置已保存");
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("设置保存失败:" + error.getMessage());
});
}
});
}
}

View File

@@ -0,0 +1,101 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class SkillInventoryActivity extends BossScreenActivity {
public static final String EXTRA_DEVICE_ID = "device_id";
public static final String EXTRA_DEVICE_NAME = "device_name";
private String deviceId;
private String deviceName;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
deviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
configureScreen("技能", deviceName == null ? "当前设备 Skill 清单" : deviceName);
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
String targetDeviceId = resolveTargetDeviceId();
BossApiClient.ApiResponse response = apiClient.getDeviceSkills(targetDeviceId);
if (!response.ok()) throw new IllegalStateException(response.message());
deviceId = targetDeviceId;
runOnUiThread(() -> renderSkills(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "技能列表加载失败:" + error.getMessage()));
});
}
});
}
private String resolveTargetDeviceId() throws Exception {
if (deviceId != null && !deviceId.isEmpty()) {
return deviceId;
}
BossApiClient.ApiResponse response = apiClient.getDevices();
if (!response.ok()) throw new IllegalStateException(response.message());
JSONArray devices = response.json.optJSONArray("devices");
if (devices == null || devices.length() == 0) {
throw new IllegalStateException("NO_DEVICE");
}
return devices.optJSONObject(0).optString("id");
}
private void renderSkills(JSONObject payload) {
replaceContent();
JSONObject device = payload.optJSONObject("device");
JSONArray skills = payload.optJSONArray("skills");
if (device != null) {
deviceName = device.optString("name", deviceId);
configureScreen("技能", deviceName);
appendContent(BossUi.buildCard(
this,
deviceName,
"当前页按设备查看 Skill 清单。",
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。"
));
}
if (skills == null || skills.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前设备还没有同步 Skill。"));
setRefreshing(false);
return;
}
for (int i = 0; i < skills.length(); i++) {
JSONObject skill = skills.optJSONObject(i);
if (skill == null) continue;
LinearLayout card = BossUi.buildCard(
this,
skill.optString("name", "未命名 Skill"),
skill.optString("description", "未提供说明"),
skill.optString("category", "-")
+ " · " + skill.optString("updatedAt", "-")
);
Button copyInvocation = BossUi.buildPrimaryButton(this, "复制调用语句");
copyInvocation.setOnClickListener(v -> BossUi.copyText(this, "Skill 调用", skill.optString("invocation", "")));
card.addView(copyInvocation);
Button copyPath = BossUi.buildSecondaryButton(this, "复制路径");
copyPath.setOnClickListener(v -> BossUi.copyText(this, "Skill 路径", skill.optString("path", "")));
card.addView(copyPath);
appendContent(card);
}
setRefreshing(false);
}
}

View File

@@ -0,0 +1,114 @@
package com.hyzq.boss;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class ThreadDetailActivity extends BossScreenActivity {
public static final String EXTRA_THREAD_ID = "thread_id";
public static final String EXTRA_PROJECT_ID = "project_id";
private String threadId;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
threadId = getIntent().getStringExtra(EXTRA_THREAD_ID);
configureScreen("线程详情", threadId == null ? "原生线程预算视图" : threadId);
reload();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getThreadDetail(threadId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> renderThread(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "线程详情加载失败:" + error.getMessage()));
});
}
});
}
private void renderThread(JSONObject payload) {
replaceContent();
JSONObject snapshot = payload.optJSONObject("snapshot");
if (snapshot == null) {
appendContent(BossUi.buildEmptyCard(this, "线程不存在。"));
setRefreshing(false);
return;
}
configureScreen("线程详情", snapshot.optString("title", threadId));
appendContent(BossUi.buildCard(
this,
snapshot.optString("title", "线程"),
snapshot.optString("summary", "暂无摘要"),
snapshot.optString("workerId", "-")
+ " · " + snapshot.optString("nodeId", "-")
+ " · " + snapshot.optInt("contextBudgetRemainingPct", 0) + "%"
+ " · " + snapshot.optString("contextBudgetLevel", "safe")
));
appendContent(BossUi.buildCard(
this,
"压缩前收尾清单",
joinBulletLines(payload.optJSONArray("currentChecklist")),
"这些步骤需要在上下文压缩前固化。"
));
appendContent(BossUi.buildCard(
this,
"主 Agent 动作",
joinBulletLines(payload.optJSONArray("masterActions")),
"若为空,说明当前无需额外动作。"
));
JSONObject handoffPackage = payload.optJSONObject("handoffPackage");
if (handoffPackage != null) {
appendContent(BossUi.buildCard(
this,
"handoff package",
handoffPackage.optString("summaryText", "暂无摘要"),
handoffPackage.optString("packageStatus", "draft")
));
}
JSONArray alerts = payload.optJSONArray("alerts");
if (alerts != null) {
for (int i = 0; i < alerts.length(); i++) {
JSONObject alert = alerts.optJSONObject(i);
if (alert == null) continue;
appendContent(BossUi.buildCard(
this,
"上下文告警",
alert.optString("summary", "无摘要"),
alert.optString("status", "opened")
));
}
}
setRefreshing(false);
}
private String joinBulletLines(@Nullable JSONArray items) {
if (items == null || items.length() == 0) {
return "当前没有内容。";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < items.length(); i++) {
String value = items.optString(i);
if (value == null || value.isEmpty()) continue;
if (builder.length() > 0) builder.append('\n');
builder.append("").append(value);
}
return builder.length() == 0 ? "当前没有内容。" : builder.toString();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:endColor="@color/boss_bg_end"
android:startColor="@color/boss_bg_start" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/boss_surface" />
<corners android:radius="24dp" />
<stroke
android:width="1dp"
android:color="@color/boss_card_stroke" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/boss_green" />
<corners android:radius="18dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/boss_surface" />
<corners android:radius="18dp" />
<stroke
android:width="1dp"
android:color="@color/boss_card_stroke" />
</shape>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_app_gradient">
<ScrollView
android:id="@+id/login_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingTop="72dp"
android:paddingRight="24dp"
android:paddingBottom="32dp">
<TextView
android:layout_width="72dp"
android:layout_height="72dp"
android:background="@drawable/bg_primary_button"
android:gravity="center"
android:text="B"
android:textColor="@color/boss_surface"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Boss 原生控制台"
android:textColor="@color/boss_text_primary"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:id="@+id/login_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:text="原生 Android 客户端已启用。点击下方按钮直接进入系统。"
android:textColor="@color/boss_text_muted"
android:textSize="15sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前临时模式"
android:textColor="@color/boss_text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:lineSpacingExtra="4dp"
android:text="1. 这是原生 Android Activity不再打开 WebView。\n2. 登录暂时不做验证,点击按钮会直接进入最高管理员会话。\n3. 会话 / 设备 / 我的三栏都直接调用现有 Boss API。"
android:textColor="@color/boss_text_primary"
android:textSize="14sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/login_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:visibility="gone" />
<Button
android:id="@+id/login_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/bg_primary_button"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:text="登录"
android:textAllCaps="false"
android:textColor="@color/boss_surface"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/content_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="18dp"
android:paddingRight="20dp"
android:paddingBottom="16dp">
<Button
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="12dp"
android:background="@drawable/bg_secondary_button"
android:paddingLeft="14dp"
android:paddingTop="10dp"
android:paddingRight="14dp"
android:paddingBottom="10dp"
android:text="返回"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/top_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="会话"
android:textColor="@color/boss_text_primary"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/top_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="原生 Android 客户端,直接消费 Boss API。"
android:textColor="@color/boss_text_muted"
android:textSize="13sp" />
</LinearLayout>
<Button
android:id="@+id/refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_secondary_button"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
android:paddingBottom="10dp"
android:text="刷新"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/screen_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/screen_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:id="@+id/screen_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingTop="8dp"
android:paddingRight="20dp"
android:paddingBottom="88dp" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="76dp"
android:background="@color/boss_surface"
android:elevation="10dp"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="12dp"
android:paddingRight="12dp">
<Button
android:id="@+id/tab_conversations"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginRight="6dp"
android:layout_weight="1"
android:background="@drawable/bg_primary_button"
android:text="会话"
android:textAllCaps="false"
android:textColor="@color/boss_surface"
android:textStyle="bold" />
<Button
android:id="@+id/tab_devices"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_weight="1"
android:background="@drawable/bg_secondary_button"
android:text="设备"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold" />
<Button
android:id="@+id/tab_me"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginLeft="6dp"
android:layout_weight="1"
android:background="@drawable/bg_secondary_button"
android:text="我的"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_app_gradient"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="14dp">
<Button
android:id="@+id/screen_back_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/bg_secondary_button"
android:minWidth="0dp"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:text="返回"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/screen_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textColor="@color/boss_text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/screen_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="副标题"
android:textColor="@color/boss_text_muted"
android:textSize="12sp" />
</LinearLayout>
<Button
android:id="@+id/screen_header_action"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginRight="8dp"
android:background="@drawable/bg_secondary_button"
android:minWidth="0dp"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:text="操作"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textStyle="bold"
android:visibility="gone" />
<Button
android:id="@+id/screen_refresh_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/bg_primary_button"
android:minWidth="0dp"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:text="刷新"
android:textAllCaps="false"
android:textColor="@color/boss_surface"
android:textStyle="bold" />
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/screen_refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:id="@+id/screen_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="18dp"
android:paddingTop="6dp"
android:paddingRight="18dp"
android:paddingBottom="24dp" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="boss_green">#07C160</color>
<color name="boss_green_dark">#04984B</color>
<color name="boss_surface">#FFFFFFFF</color>
<color name="boss_bg_start">#FFF1F6EE</color>
<color name="boss_bg_end">#FFE3F0E3</color>
<color name="boss_card_stroke">#1A0F1B12</color>
<color name="boss_text_primary">#FF111111</color>
<color name="boss_text_muted">#FF5F6B63</color>
<color name="colorPrimary">@color/boss_green</color>
<color name="colorPrimaryDark">@color/boss_green_dark</color>
<color name="colorAccent">@color/boss_green</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Boss</string>
<string name="title_activity_main">Boss</string>
<string name="package_name">com.hyzq.boss</string>
<string name="custom_url_scheme">com.hyzq.boss</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

29
android/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

22
android/gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
android/settings.gradle Normal file
View File

@@ -0,0 +1 @@
include ':app'

View File

@@ -0,0 +1,4 @@
storeFile=../keystores/boss-release.keystore
storePassword=replace-with-store-password
keyAlias=bossrelease
keyPassword=replace-with-key-password

16
android/variables.gradle Normal file
View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}