feat: sync project understanding for imported devices
This commit is contained in:
@@ -399,6 +399,10 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/devices/" + encode(deviceId) + "/import-draft/apply", new JSONObject());
|
||||
}
|
||||
|
||||
public ApiResponse syncDeviceProjectUnderstanding(String deviceId) throws IOException, JSONException {
|
||||
return requestWithRestore("POST", "/api/v1/devices/" + encode(deviceId) + "/project-understanding-sync", new JSONObject());
|
||||
}
|
||||
|
||||
public ApiResponse getAccounts() throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/accounts", null);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
));
|
||||
}
|
||||
appendContent(BossUi.buildMenuRow(this, "导入项目", "勾选这台设备上要暴露到会话首页的项目和线程", null, v -> openImportDraft()));
|
||||
appendContent(BossUi.buildMenuRow(this, "同步项目理解", "让主 Agent 主动询问这台设备上的活跃项目目标、进度和架构", null, v -> syncProjectUnderstanding()));
|
||||
appendContent(BossUi.buildMenuRow(this, "查看技能", "查看当前设备同步的 Skill 清单", null, v -> openSkills()));
|
||||
setRefreshing(false);
|
||||
}
|
||||
@@ -93,6 +94,33 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void syncProjectUnderstanding() {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.syncDeviceProjectUnderstanding(deviceId);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
JSONObject payload = response.json;
|
||||
JSONArray queuedTasks = payload.optJSONArray("queuedTasks");
|
||||
int queuedCount = queuedTasks == null ? 0 : queuedTasks.length();
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
if (queuedCount <= 0) {
|
||||
showMessage("当前设备没有可同步的活跃线程。");
|
||||
} else {
|
||||
showMessage("主 Agent 已开始同步 " + queuedCount + " 个项目理解。");
|
||||
}
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("同步失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openEditDialog() {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowToast;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class DeviceDetailActivityTest {
|
||||
@Test
|
||||
public void renderDeviceShowsSyncProjectUnderstandingEntry() {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestDeviceDetailActivity.class,
|
||||
new Intent()
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "同步项目理解"));
|
||||
assertTrue(viewTreeContainsText(content, "让主 Agent 主动询问这台设备上的活跃项目目标、进度和架构"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tappingSyncProjectUnderstandingCallsApiAndShowsQueuedCount() {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestDeviceDetailActivity.class,
|
||||
new Intent()
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
View syncLabel = findViewWithText(activity.findViewById(R.id.screen_content), "同步项目理解");
|
||||
syncLabel.getParent().getParent();
|
||||
View clickable = findClickableAncestor(syncLabel);
|
||||
clickable.performClick();
|
||||
org.robolectric.Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.fakeClient.syncCalls);
|
||||
assertEquals("主 Agent 已开始同步 2 个项目理解。", ShadowToast.getTextOfLatestToast());
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (text != null && text.toString().contains(expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static View findViewWithText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (text != null && text.toString().contains(expectedText)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return null;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
View match = findViewWithText(group.getChildAt(index), expectedText);
|
||||
if (match != null) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static View findClickableAncestor(View view) {
|
||||
View current = view;
|
||||
while (current != null && !current.isClickable()) {
|
||||
if (!(current.getParent() instanceof View)) {
|
||||
break;
|
||||
}
|
||||
current = (View) current.getParent();
|
||||
}
|
||||
return current == null ? view : current;
|
||||
}
|
||||
|
||||
public static class TestDeviceDetailActivity extends DeviceDetailActivity {
|
||||
FakeBossApiClient fakeClient;
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (fakeClient == null) {
|
||||
fakeClient = new FakeBossApiClient(this);
|
||||
}
|
||||
this.apiClient = fakeClient;
|
||||
try {
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
this,
|
||||
"renderDevice",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDevicePayload())
|
||||
);
|
||||
} catch (Exception error) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject buildDevicePayload() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("workspace", new JSONObject()
|
||||
.put("selectedDevice", new JSONObject()
|
||||
.put("id", "device-1")
|
||||
.put("name", "Mac Studio")
|
||||
.put("avatar", "M")
|
||||
.put("account", "17600003315")
|
||||
.put("status", "online")
|
||||
.put("quota5h", 75)
|
||||
.put("quota7d", 88)
|
||||
.put("projects", new JSONArray().put("Boss"))
|
||||
.put("endpoint", "mac://studio.local")
|
||||
.put("note", "测试设备")));
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeBossApiClient extends BossApiClient {
|
||||
int syncCalls = 0;
|
||||
|
||||
FakeBossApiClient(DeviceDetailActivity activity) {
|
||||
super(activity.getSharedPreferences("test-boss-api", Context.MODE_PRIVATE), "https://boss.hyzq.net");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse syncDeviceProjectUnderstanding(String deviceId) {
|
||||
syncCalls += 1;
|
||||
try {
|
||||
return new ApiResponse(
|
||||
200,
|
||||
new JSONObject()
|
||||
.put("ok", true)
|
||||
.put("queuedTasks", new JSONArray()
|
||||
.put(new JSONObject().put("projectId", "project-1"))
|
||||
.put(new JSONObject().put("projectId", "project-2")))
|
||||
);
|
||||
} catch (Exception error) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user