feat: narrow thread sync context and dedupe realtime refresh
This commit is contained in:
@@ -3,6 +3,7 @@ package com.hyzq.boss;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -10,6 +11,9 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
final class BossRealtimeClient {
|
||||
@@ -19,6 +23,7 @@ final class BossRealtimeClient {
|
||||
|
||||
private final BossApiClient apiClient;
|
||||
private final Listener listener;
|
||||
private static final String HEARTBEAT_EVENT_NAME = "heartbeat";
|
||||
private volatile boolean running;
|
||||
private @Nullable Thread workerThread;
|
||||
private @Nullable HttpURLConnection activeConnection;
|
||||
@@ -144,6 +149,9 @@ final class BossRealtimeClient {
|
||||
if (eventName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (HEARTBEAT_EVENT_NAME.equals(eventName)) {
|
||||
return null;
|
||||
}
|
||||
JSONObject payload = new JSONObject();
|
||||
if (dataBuilder.length() > 0) {
|
||||
try {
|
||||
@@ -151,7 +159,65 @@ final class BossRealtimeClient {
|
||||
} catch (JSONException ignored) {
|
||||
payload = new JSONObject();
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return new BossRealtimeEvent(eventName, payload);
|
||||
}
|
||||
|
||||
static String buildEventFingerprint(@Nullable BossRealtimeEvent event) {
|
||||
if (event == null || event.eventName.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return event.eventName + "|" + canonicalizeJson(event.payload);
|
||||
}
|
||||
|
||||
private static String canonicalizeJson(@Nullable Object value) {
|
||||
if (value == null || value == JSONObject.NULL) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof JSONObject) {
|
||||
JSONObject object = (JSONObject) value;
|
||||
ArrayList<String> keys = new ArrayList<>();
|
||||
Iterator<String> iterator = object.keys();
|
||||
while (iterator.hasNext()) {
|
||||
String key = iterator.next();
|
||||
if (!"at".equals(key)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
Collections.sort(keys);
|
||||
StringBuilder builder = new StringBuilder("{");
|
||||
for (int index = 0; index < keys.size(); index += 1) {
|
||||
if (index > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
String key = keys.get(index);
|
||||
builder.append(JSONObject.quote(key));
|
||||
builder.append(':');
|
||||
builder.append(canonicalizeJson(object.opt(key)));
|
||||
}
|
||||
builder.append('}');
|
||||
return builder.toString();
|
||||
}
|
||||
if (value instanceof JSONArray) {
|
||||
JSONArray array = (JSONArray) value;
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int index = 0; index < array.length(); index += 1) {
|
||||
if (index > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(canonicalizeJson(array.opt(index)));
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
if (value instanceof String) {
|
||||
return JSONObject.quote((String) value);
|
||||
}
|
||||
if (value instanceof Number || value instanceof Boolean) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
return JSONObject.quote(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private boolean conversationQuickActionsVisible = false;
|
||||
private boolean conversationAutoRefreshArmed = false;
|
||||
private boolean conversationAutoRefreshEnabled = false;
|
||||
private long lastRealtimeRefreshAt = 0L;
|
||||
private final java.util.HashMap<String, Long> recentRealtimeEventTimestamps = new java.util.HashMap<>();
|
||||
private final Set<String> selectedConversationProjectIds = new LinkedHashSet<>();
|
||||
private @Nullable RootPagerAdapter rootPagerAdapter;
|
||||
private boolean syncingRootPagerSelection = false;
|
||||
@@ -372,30 +372,73 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
boolean shouldRefresh = false;
|
||||
if ("conversations".equals(activeTab)) {
|
||||
shouldRefresh =
|
||||
"conversation.updated".equals(event.eventName) ||
|
||||
"project.messages.updated".equals(event.eventName) ||
|
||||
"master_agent.task.updated".equals(event.eventName) ||
|
||||
"conversation.context_indicator.updated".equals(event.eventName);
|
||||
shouldRefresh = shouldRefreshConversationsTab(event);
|
||||
} else if ("devices".equals(activeTab)) {
|
||||
shouldRefresh =
|
||||
"devices.updated".equals(event.eventName) ||
|
||||
"devices.skills.updated".equals(event.eventName) ||
|
||||
"conversation.updated".equals(event.eventName);
|
||||
shouldRefresh = shouldRefreshDevicesTab(event);
|
||||
} else if ("me".equals(activeTab)) {
|
||||
shouldRefresh = "ota.updated".equals(event.eventName) || "app.logs.updated".equals(event.eventName);
|
||||
}
|
||||
if (!shouldRefresh) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastRealtimeRefreshAt < REALTIME_REFRESH_THROTTLE_MS) {
|
||||
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
|
||||
if (eventFingerprint.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||
return;
|
||||
}
|
||||
lastRealtimeRefreshAt = now;
|
||||
runOnUiThread(this::refreshCurrentTab);
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
pruneRecentRealtimeEvents(now);
|
||||
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
|
||||
if (previousEventAt != null && now - previousEventAt < REALTIME_REFRESH_THROTTLE_MS) {
|
||||
return true;
|
||||
}
|
||||
recentRealtimeEventTimestamps.put(eventFingerprint, now);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pruneRecentRealtimeEvents(long now) {
|
||||
java.util.Iterator<java.util.Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
java.util.Map.Entry<String, Long> entry = iterator.next();
|
||||
if (now - entry.getValue() >= REALTIME_REFRESH_THROTTLE_MS) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRefreshConversationsTab(BossRealtimeEvent event) {
|
||||
if (!hasProjectId(event)) {
|
||||
return false;
|
||||
}
|
||||
return "conversation.updated".equals(event.eventName)
|
||||
|| "project.messages.updated".equals(event.eventName)
|
||||
|| "master_agent.task.updated".equals(event.eventName)
|
||||
|| "conversation.context_indicator.updated".equals(event.eventName);
|
||||
}
|
||||
|
||||
private boolean shouldRefreshDevicesTab(BossRealtimeEvent event) {
|
||||
if (!hasDeviceId(event)) {
|
||||
return false;
|
||||
}
|
||||
return "devices.updated".equals(event.eventName)
|
||||
|| "devices.skills.updated".equals(event.eventName)
|
||||
|| "conversation.updated".equals(event.eventName);
|
||||
}
|
||||
|
||||
private boolean hasProjectId(BossRealtimeEvent event) {
|
||||
return event != null && !event.payload.optString("projectId", "").trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasDeviceId(BossRealtimeEvent event) {
|
||||
return event != null && !event.payload.optString("deviceId", "").trim().isEmpty();
|
||||
}
|
||||
|
||||
private void refreshAllData(@Nullable JSONObject initialSession) {
|
||||
startRefreshing(true);
|
||||
topSubtitle.setText("");
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private ActivityResultLauncher<String> filePickerLauncher;
|
||||
private final ExecutorService replyWaitExecutor = Executors.newSingleThreadExecutor();
|
||||
private @Nullable BossRealtimeClient realtimeClient;
|
||||
private long lastRealtimeReloadAt;
|
||||
private final java.util.HashMap<String, Long> recentRealtimeEventTimestamps = new java.util.HashMap<>();
|
||||
|
||||
static final class ChromeBindings {
|
||||
final boolean multiSelecting;
|
||||
@@ -276,24 +276,51 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (event == null || event.eventName.isEmpty() || projectId == null || projectId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean shouldReload = false;
|
||||
String payloadProjectId = event.payload.optString("projectId", "");
|
||||
if ("project.messages.updated".equals(event.eventName) || "conversation.updated".equals(event.eventName)) {
|
||||
shouldReload = projectId.equals(payloadProjectId);
|
||||
} else if ("master_agent.task.updated".equals(event.eventName)) {
|
||||
shouldReload = "master-agent".equals(projectId);
|
||||
}
|
||||
boolean shouldReload = shouldReloadForRealtimeEvent(event);
|
||||
if (!shouldReload) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastRealtimeReloadAt < REALTIME_RELOAD_THROTTLE_MS) {
|
||||
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
|
||||
if (eventFingerprint.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||
return;
|
||||
}
|
||||
lastRealtimeReloadAt = now;
|
||||
runOnUiThread(this::triggerRealtimeReload);
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
pruneRecentRealtimeEvents(now);
|
||||
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
|
||||
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
|
||||
return true;
|
||||
}
|
||||
recentRealtimeEventTimestamps.put(eventFingerprint, now);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pruneRecentRealtimeEvents(long now) {
|
||||
java.util.Iterator<java.util.Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
java.util.Map.Entry<String, Long> entry = iterator.next();
|
||||
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
|
||||
String payloadProjectId = event.payload.optString("projectId", "").trim();
|
||||
if (payloadProjectId.isEmpty() || !payloadProjectId.equals(projectId)) {
|
||||
return false;
|
||||
}
|
||||
return "project.messages.updated".equals(event.eventName)
|
||||
|| "conversation.updated".equals(event.eventName)
|
||||
|| "master_agent.task.updated".equals(event.eventName);
|
||||
}
|
||||
|
||||
void triggerRealtimeReload() {
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -27,4 +27,14 @@ public class BossRealtimeClientTest {
|
||||
public void parseEventBlockReturnsNullForKeepaliveComment() {
|
||||
assertNull(BossRealtimeClient.parseEventBlock(": keepalive\n\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseEventBlockIgnoresHeartbeatControlEvents() {
|
||||
assertNull(BossRealtimeClient.parseEventBlock("event: heartbeat\n\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseEventBlockReturnsNullForEmptyEventPayloads() {
|
||||
assertNull(BossRealtimeClient.parseEventBlock("event: conversation.updated\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@@ -25,6 +26,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.refreshCount);
|
||||
}
|
||||
@@ -41,10 +43,79 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.refreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankProjectIdConversationEventDoesNotRefreshVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject())
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.refreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextIndicatorEventRefreshesVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.context_indicator.updated",
|
||||
new JSONObject().put("projectId", "project-1")
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.refreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distinctConversationEventsBackToBackBothRefreshVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"project.messages.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(2, activity.refreshCount);
|
||||
}
|
||||
|
||||
public static class TestMainActivity extends MainActivity {
|
||||
int refreshCount;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@@ -34,6 +35,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
@@ -57,10 +59,111 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void masterAgentTaskEventDoesNotRefreshForDifferentProjectId() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
TestRealtimeProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-2"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distinctRealtimeEventsBackToBackStillReloadMatchingProject() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线");
|
||||
TestRealtimeProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"project.messages.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(2, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateRealtimeEventsWithDifferentAtAreDeduped() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线");
|
||||
TestRealtimeProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"project.messages.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent(
|
||||
"project.messages.updated",
|
||||
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
public static class TestRealtimeProjectDetailActivity extends ProjectDetailActivity {
|
||||
int reloadCount;
|
||||
|
||||
|
||||
@@ -7161,7 +7161,7 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
sourceTaskId: `heartbeat-${candidate.candidateId}`,
|
||||
});
|
||||
}
|
||||
if (shouldQueueProjectUnderstandingSync(matchingProject, candidate.lastActiveAt, state)) {
|
||||
if (shouldQueueProjectUnderstandingSync(matchingProject, candidate.lastActiveAt, state, "heartbeat_activity")) {
|
||||
projectUnderstandingSyncRequests.push({
|
||||
projectId: matchingProject.id,
|
||||
observedActivityAt: candidate.lastActiveAt,
|
||||
@@ -7601,7 +7601,12 @@ function buildProjectUnderstandingTakeoverNotice(projectName: string, snapshot:
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function shouldQueueProjectUnderstandingSync(project: Project, observedActivityAt: string, state: BossState) {
|
||||
function shouldQueueProjectUnderstandingSync(
|
||||
project: Project,
|
||||
observedActivityAt: string,
|
||||
state: BossState,
|
||||
reason: "heartbeat_activity" | "thread_reply" = "heartbeat_activity",
|
||||
) {
|
||||
if (!isDispatchableThreadProject(project)) {
|
||||
return false;
|
||||
}
|
||||
@@ -7621,6 +7626,9 @@ function shouldQueueProjectUnderstandingSync(project: Project, observedActivityA
|
||||
const hasThreadStatusDocument = state.threadStatusDocuments.some(
|
||||
(item) => item.projectId === project.id && item.threadId === project.threadMeta.threadId,
|
||||
);
|
||||
if (reason === "thread_reply" && hasThreadStatusDocument) {
|
||||
return false;
|
||||
}
|
||||
if (project.projectUnderstanding && hasThreadStatusDocument) {
|
||||
const lastSyncedTs = Date.parse(
|
||||
project.threadMeta.lastProjectUnderstandingSyncedAt ??
|
||||
@@ -7673,7 +7681,7 @@ async function queueProjectUnderstandingSyncTask(input: {
|
||||
}) {
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state)) {
|
||||
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state, input.reason)) {
|
||||
return null;
|
||||
}
|
||||
const requestedByAccount = state.user.account || project.deviceIds[0] || "17600003315";
|
||||
@@ -8625,7 +8633,8 @@ export async function appendProjectMessage(payload: {
|
||||
return {
|
||||
message,
|
||||
shouldQueueUnderstandingSync:
|
||||
shouldTrackThreadProgress && shouldQueueProjectUnderstandingSync(project, message.sentAt, state),
|
||||
shouldTrackThreadProgress &&
|
||||
shouldQueueProjectUnderstandingSync(project, message.sentAt, state, "thread_reply"),
|
||||
};
|
||||
});
|
||||
if (result.shouldQueueUnderstandingSync) {
|
||||
|
||||
@@ -261,35 +261,10 @@ function buildRuntimeDigest(
|
||||
.filter((update) => update.status === "available")
|
||||
.map((update) => `${update.version} -> ${update.targetScope}`)
|
||||
.join("\n");
|
||||
const threadStatusDocuments = [...state.threadStatusDocuments]
|
||||
.sort((left, right) => {
|
||||
const updatedDelta = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
||||
if (updatedDelta !== 0) {
|
||||
return updatedDelta;
|
||||
}
|
||||
return right.documentId.localeCompare(left.documentId);
|
||||
})
|
||||
.slice(0, 6)
|
||||
.map((document) => buildThreadStatusDocumentDigest(state, document));
|
||||
const recentProgressEvents = [...state.threadProgressEvents]
|
||||
.sort((left, right) => {
|
||||
const createdDelta = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
||||
if (createdDelta !== 0) {
|
||||
return createdDelta;
|
||||
}
|
||||
return right.eventId.localeCompare(left.eventId);
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map((event) => buildThreadProgressEventDigest(state, event));
|
||||
const deepPullThreadUnderstandings = state.projects
|
||||
.filter((project) => project.id !== "master-agent" && project.projectUnderstanding)
|
||||
.sort((left, right) =>
|
||||
String(right.projectUnderstanding?.updatedAt ?? right.lastMessageAt).localeCompare(
|
||||
String(left.projectUnderstanding?.updatedAt ?? left.lastMessageAt),
|
||||
),
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((project) => buildDeepPullThreadUnderstandingDigest(project));
|
||||
const threadRuntimeSelection = selectThreadRuntimeDigestSelection(state, requestText);
|
||||
const threadStatusDocuments = threadRuntimeSelection.threadStatusDocuments;
|
||||
const recentProgressEvents = threadRuntimeSelection.recentProgressEvents;
|
||||
const deepPullThreadUnderstandings = threadRuntimeSelection.deepPullThreadUnderstandings;
|
||||
|
||||
const authSummary = [
|
||||
`登录会话策略:成功登录后默认保持 ${Math.round(AUTH_SESSION_TTL_MS / 24 / 60 / 60_000)} 天。`,
|
||||
@@ -309,9 +284,13 @@ function buildRuntimeDigest(
|
||||
"最近进展事件:",
|
||||
recentProgressEvents.length > 0 ? recentProgressEvents.join("\n") : "无",
|
||||
"",
|
||||
"关键时刻深拉线程兜底:",
|
||||
deepPullThreadUnderstandings.length > 0 ? deepPullThreadUnderstandings.join("\n") : "无",
|
||||
"",
|
||||
...(deepPullThreadUnderstandings.length > 0
|
||||
? [
|
||||
"关键时刻深拉线程兜底:",
|
||||
deepPullThreadUnderstandings.join("\n"),
|
||||
"",
|
||||
]
|
||||
: []),
|
||||
"最近主 Agent 对话:",
|
||||
recentMessages || "无",
|
||||
"",
|
||||
@@ -332,6 +311,111 @@ function buildRuntimeDigest(
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function selectThreadRuntimeDigestSelection(
|
||||
state: Awaited<ReturnType<typeof readState>>,
|
||||
requestText: string,
|
||||
) {
|
||||
const projectsWithRuntimeEvidence = state.projects
|
||||
.filter((project) =>
|
||||
state.threadStatusDocuments.some((document) => document.projectId === project.id) ||
|
||||
state.threadProgressEvents.some((event) => event.projectId === project.id),
|
||||
)
|
||||
.sort((left, right) => compareProjectRuntimeDigestActivity(right, left));
|
||||
|
||||
const scoredProjects = state.projects
|
||||
.map((project) => ({
|
||||
project,
|
||||
score: scoreMasterAgentDispatchCandidate(project, requestText),
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (right.score !== left.score) {
|
||||
return right.score - left.score;
|
||||
}
|
||||
return compareProjectRuntimeDigestActivity(right.project, left.project);
|
||||
});
|
||||
|
||||
const matchedProjects = scoredProjects.filter((item) => item.score > 0).map((item) => item.project);
|
||||
const matchedNonMasterProjects = matchedProjects.filter((project) => project.id !== "master-agent");
|
||||
const selectedProjects =
|
||||
matchedNonMasterProjects.length > 0
|
||||
? matchedNonMasterProjects
|
||||
: matchedProjects.length > 0
|
||||
? matchedProjects
|
||||
: projectsWithRuntimeEvidence.slice(0, 3);
|
||||
|
||||
let selectedProjectIds = new Set(selectedProjects.map((project) => project.id));
|
||||
let threadStatusDocuments = [...state.threadStatusDocuments]
|
||||
.filter((document) => selectedProjectIds.has(document.projectId))
|
||||
.sort((left, right) => {
|
||||
const updatedDelta = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
||||
if (updatedDelta !== 0) {
|
||||
return updatedDelta;
|
||||
}
|
||||
return right.documentId.localeCompare(left.documentId);
|
||||
});
|
||||
let recentProgressEvents = [...state.threadProgressEvents]
|
||||
.filter((event) => selectedProjectIds.has(event.projectId))
|
||||
.sort((left, right) => {
|
||||
const createdDelta = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
||||
if (createdDelta !== 0) {
|
||||
return createdDelta;
|
||||
}
|
||||
return right.eventId.localeCompare(left.eventId);
|
||||
});
|
||||
|
||||
if (threadStatusDocuments.length === 0 && recentProgressEvents.length === 0 && projectsWithRuntimeEvidence.length > 0) {
|
||||
selectedProjectIds = new Set(projectsWithRuntimeEvidence.slice(0, 3).map((project) => project.id));
|
||||
threadStatusDocuments = [...state.threadStatusDocuments]
|
||||
.filter((document) => selectedProjectIds.has(document.projectId))
|
||||
.sort((left, right) => {
|
||||
const updatedDelta = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
||||
if (updatedDelta !== 0) {
|
||||
return updatedDelta;
|
||||
}
|
||||
return right.documentId.localeCompare(left.documentId);
|
||||
});
|
||||
recentProgressEvents = [...state.threadProgressEvents]
|
||||
.filter((event) => selectedProjectIds.has(event.projectId))
|
||||
.sort((left, right) => {
|
||||
const createdDelta = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
||||
if (createdDelta !== 0) {
|
||||
return createdDelta;
|
||||
}
|
||||
return right.eventId.localeCompare(left.eventId);
|
||||
});
|
||||
}
|
||||
|
||||
const deepPullThreadUnderstandings =
|
||||
threadStatusDocuments.length === 0 && recentProgressEvents.length === 0 && projectsWithRuntimeEvidence.length === 0
|
||||
? state.projects
|
||||
.filter((project) => project.id !== "master-agent" && project.projectUnderstanding)
|
||||
.sort((left, right) => compareProjectRuntimeDigestActivity(right, left))
|
||||
.slice(0, 3)
|
||||
.map((project) => buildDeepPullThreadUnderstandingDigest(project))
|
||||
.filter((entry): entry is string => Boolean(entry))
|
||||
: [];
|
||||
|
||||
return {
|
||||
threadStatusDocuments: threadStatusDocuments.slice(0, 6).map((document) => buildThreadStatusDocumentDigest(state, document)),
|
||||
recentProgressEvents: recentProgressEvents.slice(0, 8).map((event) => buildThreadProgressEventDigest(state, event)),
|
||||
deepPullThreadUnderstandings,
|
||||
};
|
||||
}
|
||||
|
||||
function compareProjectRuntimeDigestActivity(left: Project, right: Project) {
|
||||
return projectRuntimeDigestActivityValue(left) - projectRuntimeDigestActivityValue(right);
|
||||
}
|
||||
|
||||
function projectRuntimeDigestActivityValue(project: Project) {
|
||||
return Math.max(
|
||||
Date.parse(project.updatedAt || ""),
|
||||
Date.parse(project.lastMessageAt || ""),
|
||||
Date.parse(project.threadMeta.updatedAt || ""),
|
||||
Date.parse(project.threadMeta.lastObservedCodexActivityAt || ""),
|
||||
Date.parse(project.projectUnderstanding?.updatedAt || ""),
|
||||
);
|
||||
}
|
||||
|
||||
function buildThreadStatusDocumentDigest(
|
||||
state: Awaited<ReturnType<typeof readState>>,
|
||||
document: Awaited<ReturnType<typeof readState>>["threadStatusDocuments"][number],
|
||||
|
||||
@@ -44,7 +44,7 @@ test.after(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事件和项目记忆,并保留深拉兜底", async () => {
|
||||
test("主 Agent 执行 prompt 命中线程时只读取相关状态文档和最近进展事件,不再常态注入深拉兜底", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
@@ -90,6 +90,8 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
const state = await readState();
|
||||
const auditProject = state.projects.find((project) => project.id === "audit-collab");
|
||||
assert.ok(auditProject, "expected seeded audit-collab project");
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
assert.ok(masterProject, "expected seeded master-agent project");
|
||||
auditProject!.projectUnderstanding = {
|
||||
projectGoal: "深拉兜底目标",
|
||||
currentProgress: "深拉兜底进度",
|
||||
@@ -100,7 +102,36 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
updatedAt: "2026-04-04T18:00:00+08:00",
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
masterProject!.projectUnderstanding = {
|
||||
projectGoal: "主 Agent 旧目标",
|
||||
currentProgress: "主 Agent 旧进度",
|
||||
technicalArchitecture: "主 Agent 旧架构",
|
||||
currentBlockers: "主 Agent 旧阻塞",
|
||||
recommendedNextStep: "主 Agent 旧下一步",
|
||||
sourceTaskId: "task-master-legacy",
|
||||
updatedAt: "2026-04-04T17:50:00+08:00",
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
state.threadStatusDocuments = [
|
||||
{
|
||||
documentId: "thread-status-doc-master",
|
||||
projectId: "master-agent",
|
||||
threadId: "thread-master-main",
|
||||
threadDisplayName: "主 Agent 汇总",
|
||||
folderName: "主控线程",
|
||||
deviceId: "mac-studio",
|
||||
projectGoal: "主 Agent 额外状态目标",
|
||||
currentPhase: "主 Agent 额外阶段",
|
||||
currentProgress: "主 Agent 额外进度",
|
||||
technicalArchitecture: "主 Agent 额外架构",
|
||||
currentBlockers: "主 Agent 额外阻塞",
|
||||
recommendedNextStep: "主 Agent 额外下一步",
|
||||
keyFiles: ["src/lib/boss-master-agent.ts"],
|
||||
keyCommands: ["npm run lint"],
|
||||
updatedAt: "2026-04-04T18:03:00+08:00",
|
||||
sourceTaskId: "task-master-status",
|
||||
sourceKind: "incremental_sync",
|
||||
},
|
||||
{
|
||||
documentId: "thread-status-doc-1",
|
||||
projectId: "audit-collab",
|
||||
@@ -122,6 +153,20 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
},
|
||||
];
|
||||
state.threadProgressEvents = [
|
||||
{
|
||||
eventId: "thread-progress-event-master",
|
||||
projectId: "master-agent",
|
||||
threadId: "thread-master-main",
|
||||
threadDisplayName: "主 Agent 汇总",
|
||||
deviceId: "mac-studio",
|
||||
eventType: "progress_updated",
|
||||
summary: "主 Agent 额外进展摘要",
|
||||
phase: "主 Agent 额外阶段",
|
||||
blockerDelta: "主 Agent 额外阻塞",
|
||||
nextStepDelta: "主 Agent 额外下一步",
|
||||
createdAt: "2026-04-04T18:03:30+08:00",
|
||||
sourceTaskId: "task-master-progress",
|
||||
},
|
||||
{
|
||||
eventId: "thread-progress-event-1",
|
||||
projectId: "audit-collab",
|
||||
@@ -142,7 +187,7 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
const resolved = await resolveMasterAgentExecutionConfig(
|
||||
"master-agent",
|
||||
"17600003315",
|
||||
"继续推进线程状态同步",
|
||||
"审计对话,请继续推进线程状态同步",
|
||||
);
|
||||
assert.ok(resolved.projectMemories.length > 0);
|
||||
assert.equal(resolved.projectMemories[0]?.content, "项目记忆正文");
|
||||
@@ -157,7 +202,7 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
);
|
||||
|
||||
const reply = await replyToMasterAgentUserMessage({
|
||||
requestText: "继续推进线程状态同步",
|
||||
requestText: "审计对话,请继续推进线程状态同步",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: "17600003315",
|
||||
mode: "enqueue",
|
||||
@@ -166,32 +211,169 @@ test("主 Agent 执行 prompt 默认读取线程状态文档、最近进展事
|
||||
assert.equal(reply.masterReplyState, "queued");
|
||||
|
||||
const queuedTask = (await readState()).masterAgentTasks.find(
|
||||
(task) => task.projectId === "master-agent" && task.requestText === "继续推进线程状态同步",
|
||||
(task) => task.projectId === "master-agent" && task.requestText === "审计对话,请继续推进线程状态同步",
|
||||
);
|
||||
assert.ok(queuedTask, "expected master-agent task to be queued");
|
||||
assert.ok(queuedTask?.executionPrompt.includes("线程状态文档:"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("线程状态目标"));
|
||||
assert.ok(!queuedTask?.executionPrompt.includes("主 Agent 额外状态目标"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("最近进展事件:"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("最近进展事件摘要"));
|
||||
assert.ok(!queuedTask?.executionPrompt.includes("主 Agent 额外进展摘要"));
|
||||
assert.ok(!queuedTask?.executionPrompt.includes("深拉兜底目标"));
|
||||
assert.ok(!queuedTask?.executionPrompt.includes("关键时刻深拉线程兜底:"));
|
||||
});
|
||||
|
||||
test("主 Agent 执行 prompt 在未命中时退回最近活跃项目,且不会常态注入深拉兜底", async () => {
|
||||
await setup();
|
||||
|
||||
const state = await readState();
|
||||
const auditProject = state.projects.find((project) => project.id === "audit-collab");
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
assert.ok(auditProject, "expected seeded audit-collab project");
|
||||
assert.ok(masterProject, "expected seeded master-agent project");
|
||||
|
||||
auditProject!.projectUnderstanding = {
|
||||
projectGoal: "审计兜底目标",
|
||||
currentProgress: "审计兜底进度",
|
||||
technicalArchitecture: "审计兜底架构",
|
||||
currentBlockers: "审计兜底阻塞",
|
||||
recommendedNextStep: "审计兜底下一步",
|
||||
sourceTaskId: "task-audit-fallback",
|
||||
updatedAt: "2026-04-04T17:55:00+08:00",
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
masterProject!.projectUnderstanding = {
|
||||
projectGoal: "最近活跃目标",
|
||||
currentProgress: "最近活跃进度",
|
||||
technicalArchitecture: "最近活跃架构",
|
||||
currentBlockers: "最近活跃阻塞",
|
||||
recommendedNextStep: "最近活跃下一步",
|
||||
sourceTaskId: "task-master-active",
|
||||
updatedAt: "2026-04-04T18:05:00+08:00",
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
state.threadStatusDocuments = [
|
||||
{
|
||||
documentId: "thread-status-doc-audit-fallback",
|
||||
projectId: "audit-collab",
|
||||
threadId: "thread-audit-chief",
|
||||
threadDisplayName: "审计对话",
|
||||
folderName: "审计群聊",
|
||||
deviceId: "mac-studio",
|
||||
projectGoal: "审计兜底状态",
|
||||
currentPhase: "审计兜底阶段",
|
||||
currentProgress: "审计兜底进度",
|
||||
technicalArchitecture: "审计兜底架构",
|
||||
currentBlockers: "审计兜底阻塞",
|
||||
recommendedNextStep: "审计兜底下一步",
|
||||
keyFiles: ["src/lib/boss-master-agent.ts"],
|
||||
keyCommands: ["npm run build"],
|
||||
updatedAt: "2026-04-04T17:56:00+08:00",
|
||||
sourceTaskId: "task-audit-status",
|
||||
sourceKind: "incremental_sync",
|
||||
},
|
||||
{
|
||||
documentId: "thread-status-doc-master-fallback",
|
||||
projectId: "master-agent",
|
||||
threadId: "thread-master-main",
|
||||
threadDisplayName: "主 Agent 汇总",
|
||||
folderName: "主控线程",
|
||||
deviceId: "mac-studio",
|
||||
projectGoal: "最近活跃状态",
|
||||
currentPhase: "最近活跃阶段",
|
||||
currentProgress: "最近活跃进度",
|
||||
technicalArchitecture: "最近活跃架构",
|
||||
currentBlockers: "最近活跃阻塞",
|
||||
recommendedNextStep: "最近活跃下一步",
|
||||
keyFiles: ["src/lib/boss-data.ts"],
|
||||
keyCommands: ["npm run lint"],
|
||||
updatedAt: "2026-04-04T18:06:00+08:00",
|
||||
sourceTaskId: "task-master-status-fallback",
|
||||
sourceKind: "incremental_sync",
|
||||
},
|
||||
];
|
||||
state.threadProgressEvents = [
|
||||
{
|
||||
eventId: "thread-progress-event-audit-fallback",
|
||||
projectId: "audit-collab",
|
||||
threadId: "thread-audit-chief",
|
||||
threadDisplayName: "审计对话",
|
||||
deviceId: "mac-studio",
|
||||
eventType: "progress_updated",
|
||||
summary: "审计兜底进展摘要",
|
||||
phase: "审计兜底阶段",
|
||||
blockerDelta: "审计兜底阻塞",
|
||||
nextStepDelta: "审计兜底下一步",
|
||||
createdAt: "2026-04-04T17:56:30+08:00",
|
||||
sourceTaskId: "task-audit-progress",
|
||||
},
|
||||
{
|
||||
eventId: "thread-progress-event-master-fallback",
|
||||
projectId: "master-agent",
|
||||
threadId: "thread-master-main",
|
||||
threadDisplayName: "主 Agent 汇总",
|
||||
deviceId: "mac-studio",
|
||||
eventType: "progress_updated",
|
||||
summary: "最近活跃进展摘要",
|
||||
phase: "最近活跃阶段",
|
||||
blockerDelta: "最近活跃阻塞",
|
||||
nextStepDelta: "最近活跃下一步",
|
||||
createdAt: "2026-04-04T18:06:30+08:00",
|
||||
sourceTaskId: "task-master-progress-fallback",
|
||||
},
|
||||
];
|
||||
await writeState(state);
|
||||
|
||||
const reply = await replyToMasterAgentUserMessage({
|
||||
requestText: "请继续推进线程状态同步(仅深拉兜底)",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: "17600003315",
|
||||
mode: "enqueue",
|
||||
});
|
||||
assert.equal(reply.ok, true);
|
||||
|
||||
const queuedTask = (await readState()).masterAgentTasks.find(
|
||||
(task) => task.projectId === "master-agent" && task.requestText === "请继续推进线程状态同步(仅深拉兜底)",
|
||||
);
|
||||
assert.ok(queuedTask, "expected master-agent task to be queued");
|
||||
assert.ok(queuedTask?.executionPrompt.includes("最近活跃状态"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("最近活跃进展摘要"));
|
||||
assert.ok(!queuedTask?.executionPrompt.includes("关键时刻深拉线程兜底:"));
|
||||
});
|
||||
|
||||
test("主 Agent 执行 prompt 在没有线程状态文档和进展事件时才会注入深拉兜底", async () => {
|
||||
await setup();
|
||||
|
||||
const state = await readState();
|
||||
const auditProject = state.projects.find((project) => project.id === "audit-collab");
|
||||
assert.ok(auditProject, "expected seeded audit-collab project");
|
||||
auditProject!.projectUnderstanding = {
|
||||
projectGoal: "深拉兜底目标",
|
||||
currentProgress: "深拉兜底进度",
|
||||
technicalArchitecture: "深拉兜底架构",
|
||||
currentBlockers: "深拉兜底阻塞",
|
||||
recommendedNextStep: "深拉兜底下一步",
|
||||
sourceTaskId: "task-deep-pull",
|
||||
updatedAt: "2026-04-04T18:00:00+08:00",
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
state.threadStatusDocuments = [];
|
||||
state.threadProgressEvents = [];
|
||||
await writeState(state);
|
||||
|
||||
const reply = await replyToMasterAgentUserMessage({
|
||||
requestText: "请继续推进线程状态同步",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: "17600003315",
|
||||
mode: "enqueue",
|
||||
});
|
||||
assert.equal(reply.ok, true);
|
||||
|
||||
const queuedTask = (await readState()).masterAgentTasks.find(
|
||||
(task) => task.projectId === "master-agent" && task.requestText === "请继续推进线程状态同步",
|
||||
);
|
||||
assert.ok(queuedTask, "expected master-agent task to be queued");
|
||||
assert.ok(queuedTask?.executionPrompt.includes("关键时刻深拉线程兜底:"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("深拉兜底目标"));
|
||||
|
||||
assert.ok(
|
||||
queuedTask?.executionPrompt.indexOf("管理员全局主提示词:") <
|
||||
queuedTask.executionPrompt.indexOf("用户私有主提示词:") &&
|
||||
queuedTask.executionPrompt.indexOf("用户私有主提示词:") <
|
||||
queuedTask.executionPrompt.indexOf("当前对话附加提示词:") &&
|
||||
queuedTask.executionPrompt.indexOf("当前对话附加提示词:") <
|
||||
queuedTask.executionPrompt.indexOf("项目记忆:") &&
|
||||
queuedTask.executionPrompt.indexOf("项目记忆:") <
|
||||
queuedTask.executionPrompt.indexOf("当前消息:") &&
|
||||
queuedTask.executionPrompt.indexOf("当前消息:") <
|
||||
queuedTask.executionPrompt.indexOf("当前对话覆盖:") &&
|
||||
queuedTask.executionPrompt.indexOf("当前对话覆盖:") <
|
||||
queuedTask.executionPrompt.indexOf("线程状态文档:") &&
|
||||
queuedTask.executionPrompt.indexOf("线程状态文档:") <
|
||||
queuedTask.executionPrompt.indexOf("最近进展事件:") &&
|
||||
queuedTask.executionPrompt.indexOf("最近进展事件:") <
|
||||
queuedTask.executionPrompt.indexOf("关键时刻深拉线程兜底:"),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user