Refresh stale device capability timestamps

This commit is contained in:
kris
2026-04-06 14:38:18 +08:00
parent 5789707072
commit 5782804df3
2 changed files with 106 additions and 15 deletions

View File

@@ -1742,6 +1742,36 @@ function normalizeDeviceCapabilities(
};
}
function refreshDeviceCapabilityLastSeenAt(
raw: DeviceCapabilitiesInput | undefined,
fallbackLastSeenAt: string,
preserveExistingLastSeenAt = true,
): DeviceCapabilitiesInput | undefined {
if (!raw) {
return raw;
}
return {
gui: raw.gui
? {
...raw.gui,
lastSeenAt:
preserveExistingLastSeenAt
? trimToDefined(raw.gui.lastSeenAt) ?? fallbackLastSeenAt
: fallbackLastSeenAt,
}
: raw.gui,
cli: raw.cli
? {
...raw.cli,
lastSeenAt:
preserveExistingLastSeenAt
? trimToDefined(raw.cli.lastSeenAt) ?? fallbackLastSeenAt
: fallbackLastSeenAt,
}
: raw.cli,
};
}
function normalizePreferredExecutionMode(value: unknown): DeviceExecutionMode {
return value === "gui" ? "gui" : "cli";
}
@@ -7273,6 +7303,7 @@ export async function updateDevice(deviceId: string, payload: DeviceUpdatePayloa
const device = await mutateState((state) => {
const nextDevice = state.devices.find((item) => item.id === deviceId);
if (!nextDevice) throw new Error("DEVICE_NOT_FOUND");
const nextLastSeenAt = nowIso();
if (payload.name) nextDevice.name = payload.name.trim();
if (payload.avatar) nextDevice.avatar = payload.avatar.trim().slice(0, 2) || nextDevice.avatar;
@@ -7283,21 +7314,23 @@ export async function updateDevice(deviceId: string, payload: DeviceUpdatePayloa
if (payload.projects) {
nextDevice.projects = payload.projects.filter(Boolean);
}
if (payload.capabilities) {
nextDevice.capabilities = normalizeDeviceCapabilities(
{
gui: payload.capabilities.gui ?? nextDevice.capabilities?.gui,
cli: payload.capabilities.cli ?? nextDevice.capabilities?.cli,
},
trimToDefined(payload.capabilities.gui?.lastSeenAt) ??
trimToDefined(payload.capabilities.cli?.lastSeenAt) ??
nextDevice.lastSeenAt,
);
}
nextDevice.capabilities = normalizeDeviceCapabilities(
refreshDeviceCapabilityLastSeenAt(
payload.capabilities
? {
gui: payload.capabilities.gui ?? nextDevice.capabilities?.gui,
cli: payload.capabilities.cli ?? nextDevice.capabilities?.cli,
}
: nextDevice.capabilities,
nextLastSeenAt,
Boolean(payload.capabilities),
),
nextLastSeenAt,
);
if (payload.preferredExecutionMode !== undefined) {
nextDevice.preferredExecutionMode = normalizePreferredExecutionMode(payload.preferredExecutionMode);
}
nextDevice.lastSeenAt = nowIso();
nextDevice.lastSeenAt = nextLastSeenAt;
return nextDevice;
});
publishBossEvent("devices.updated", { deviceId });
@@ -7635,6 +7668,7 @@ export async function upsertDeviceHeartbeat(payload: {
if (device.token && payload.token && device.token !== payload.token && !claimedEnrollment) {
throw new Error("DEVICE_TOKEN_MISMATCH");
}
const nextLastSeenAt = nowIso();
device.name = payload.name;
device.avatar = payload.avatar;
device.account = payload.account;
@@ -7643,12 +7677,16 @@ export async function upsertDeviceHeartbeat(payload: {
device.projects = payload.projects;
device.quota5h = payload.quota5h;
device.quota7d = payload.quota7d;
device.lastSeenAt = nowIso();
device.lastSeenAt = nextLastSeenAt;
device.endpoint = payload.endpoint ?? device.endpoint;
device.token = claimedEnrollment?.token ?? payload.token ?? device.token;
device.capabilities = normalizeDeviceCapabilities(
payload.capabilities ?? device.capabilities,
device.lastSeenAt,
refreshDeviceCapabilityLastSeenAt(
payload.capabilities ?? device.capabilities,
nextLastSeenAt,
Boolean(payload.capabilities),
),
nextLastSeenAt,
);
if (device.preferredExecutionMode === undefined && payload.preferredExecutionMode !== undefined) {
device.preferredExecutionMode = normalizePreferredExecutionMode(payload.preferredExecutionMode);

View File

@@ -240,3 +240,56 @@ test("device heartbeat does not overwrite the preferred execution mode chosen in
assert.ok(device);
assert.equal(device.preferredExecutionMode, "gui");
});
test("device heartbeat without capability payload refreshes stale gui cli lastSeenAt", async () => {
await setup();
const staleCapabilityTime = "2026-03-25T11:52:00+08:00";
const staleState = await readState();
await writeState({
...staleState,
devices: staleState.devices.map((device) =>
device.id === "mac-studio"
? {
...device,
lastSeenAt: staleCapabilityTime,
capabilities: {
gui: {
...device.capabilities?.gui,
connected: true,
lastSeenAt: staleCapabilityTime,
lastActiveProjectId: "master-agent",
},
cli: {
...device.capabilities?.cli,
connected: true,
lastSeenAt: staleCapabilityTime,
lastActiveProjectId: "master-agent",
},
},
}
: device,
),
});
await upsertDeviceHeartbeat({
deviceId: "mac-studio",
name: "Mac Studio",
avatar: "M",
account: "17600003315",
status: "online",
quota5h: 72,
quota7d: 86,
preferredExecutionMode: "cli",
projects: ["硬件审计协作"],
endpoint: "mac://kris.local",
});
const state = await readState();
const device = state.devices.find((item) => item.id === "mac-studio");
assert.ok(device);
assert.notEqual(device.lastSeenAt, staleCapabilityTime);
assert.equal(device.capabilities?.gui.lastSeenAt, device.lastSeenAt);
assert.equal(device.capabilities?.cli.lastSeenAt, device.lastSeenAt);
});