fix: restore master agent relay guidance

This commit is contained in:
kris
2026-03-30 17:42:21 +08:00
parent 5eb1246f02
commit 7c6101f22b
9 changed files with 312 additions and 22 deletions

View File

@@ -57,6 +57,22 @@ function emptyDraft(): AccountDraft {
};
}
function buildMasterNodeLoginGuide(account: {
nodeLabel?: string;
nodeId?: string;
statusLabel?: string;
}) {
const nodeLabel = account.nodeLabel?.trim() || account.nodeId?.trim() || "绑定设备";
return [
`主 GPT 不在手机里直接登录。`,
`请到绑定设备 ${nodeLabel} 上打开 Codex / ChatGPT Plus 会话完成登录。`,
`登录完成后,回到这里点“测试连接”确认 relay 和主 Agent 已接通。`,
account.statusLabel ? `当前状态:${account.statusLabel}` : null,
]
.filter(Boolean)
.join("\n");
}
function draftFromAccount(account: AiAccountSummary): AccountDraft {
return {
label: account.label,
@@ -142,6 +158,7 @@ export function AiAccountsClient({
const [newDraft, setNewDraft] = useState<AccountDraft>(emptyDraft());
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
const [guideAccountId, setGuideAccountId] = useState<string | null>(null);
const accountDrafts = useMemo(
() =>
@@ -437,6 +454,19 @@ export function AiAccountsClient({
</div>
<div className="mt-4 flex flex-wrap gap-2">
{account.provider === "master_codex_node" ? (
<button
type="button"
onClick={() =>
setGuideAccountId((current) =>
current === account.accountId ? null : account.accountId,
)
}
className="rounded-full border border-[#D9D9D9] px-3 py-2 text-[12px] font-semibold text-[#57606A]"
>
{guideAccountId === account.accountId ? "收起登录指引" : "登录指引"}
</button>
) : null}
<button
type="button"
onClick={() => void validateAccount(account.accountId)}
@@ -472,6 +502,12 @@ export function AiAccountsClient({
</button>
) : null}
</div>
{account.provider === "master_codex_node" && guideAccountId === account.accountId ? (
<div className="mt-3 rounded-2xl bg-[#F7F8FA] px-3 py-3 text-[12px] leading-6 text-[#57606A] whitespace-pre-line">
{buildMasterNodeLoginGuide(account)}
</div>
) : null}
</div>
);
})}

View File

@@ -753,13 +753,55 @@ export async function validateAiAccountConnection(accountId: string) {
}
if (account.provider === "master_codex_node") {
const state = await readState();
const nodeId = account.nodeId?.trim() || state.user.boundDeviceId || "";
const boundDevice = state.devices.find((device) => device.id === nodeId);
const boundNodeLabel =
account.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
state.user.boundDeviceId ||
"绑定设备";
if (!nodeId) {
await updateAiAccountHealth({
accountId: account.accountId,
status: "needs_login",
lastError: "MASTER_CODEX_NODE_NOT_CONFIGURED",
lastValidatedAt: new Date().toISOString(),
});
return {
ok: false as const,
status: "needs_login" as const,
message: `主 GPT 不在手机里直接登录。请先在绑定设备(例如 ${boundNodeLabel})上的 Codex / ChatGPT Plus 会话里登录,并填写正确的节点 ID再回来校验连接。`,
};
}
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: account.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
return {
ok: false as const,
status: "degraded" as const,
message: `主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过该节点对话。请先在这台设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线。`,
};
}
await updateAiAccountHealth({
accountId: account.accountId,
status: "ready",
lastError: undefined,
lastValidatedAt: new Date().toISOString(),
lastUsedAt: boundDevice.lastSeenAt || new Date().toISOString(),
});
return {
ok: Boolean(account.nodeId?.trim()) as boolean,
status: account.nodeId?.trim() ? "ready" : "needs_login",
message:
account.nodeId?.trim()
? "Master Codex Node 已配置。主 Agent 会通过 local-agent relay 把任务转交给该节点上的 Codex。"
: "请先填写 Master Codex Node 的节点 ID再让 local-agent 认领主 Agent 任务。",
ok: true as const,
status: "ready" as const,
message: `主 GPT 不在手机里直接登录。当前已通过绑定设备 ${boundNodeLabel} 接好 Master Codex Node主 Agent 会把任务转交给这台设备上的 Codex / ChatGPT Plus 会话。`,
};
}
@@ -811,6 +853,27 @@ export async function replyToMasterAgentUserMessage(params: {
if (runtime.account.provider === "master_codex_node") {
const state = await readState();
const deviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const boundDevice = state.devices.find((device) => device.id === deviceId);
const boundNodeLabel =
runtime.account.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
deviceId;
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
@@ -845,7 +908,7 @@ export async function replyToMasterAgentUserMessage(params: {
await appendMasterAgentSystemReply(
[
`当前主控身份是 ${runtime.summary.roleLabel},任务已经转交到 ${runtime.account.nodeLabel ?? deviceId} 的 Master Codex Node。`,
`当前主控身份是 ${runtime.summary.roleLabel},任务已经转交到 ${boundNodeLabel} 的 Master Codex Node。`,
"如果本机 Codex 节点在线,回复会在稍后自动回写到当前会话。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,