feat: cron agent binding + doctor UI refresh
This commit is contained in:
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "./auth-profiles.js";
|
||||
@@ -70,9 +71,10 @@ async function readJson(pathname: string): Promise<unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildMinimaxApiProvider(): ProviderConfig {
|
||||
function buildMinimaxApiProvider(apiKey?: string): ProviderConfig {
|
||||
return {
|
||||
baseUrl: MINIMAX_API_BASE_URL,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
@@ -88,6 +90,35 @@ function buildMinimaxApiProvider(): ProviderConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMinimaxApiKeyFromStore(
|
||||
store: AuthProfileStore,
|
||||
): string | undefined {
|
||||
const profileIds = listProfilesForProvider(store, "minimax");
|
||||
for (const profileId of profileIds) {
|
||||
const cred = store.profiles[profileId];
|
||||
if (!cred) continue;
|
||||
if (cred.type === "api_key") {
|
||||
const key = cred.key?.trim();
|
||||
if (key) return key;
|
||||
continue;
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
const token = cred.token?.trim();
|
||||
if (!token) continue;
|
||||
if (
|
||||
typeof cred.expires === "number" &&
|
||||
Number.isFinite(cred.expires) &&
|
||||
cred.expires > 0 &&
|
||||
Date.now() >= cred.expires
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveImplicitProviders(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
agentDir: string;
|
||||
@@ -95,10 +126,10 @@ function resolveImplicitProviders(params: {
|
||||
const providers: Record<string, ProviderConfig> = {};
|
||||
const minimaxEnv = resolveEnvApiKey("minimax");
|
||||
const authStore = ensureAuthProfileStore(params.agentDir);
|
||||
const hasMinimaxProfile =
|
||||
listProfilesForProvider(authStore, "minimax").length > 0;
|
||||
if (minimaxEnv || hasMinimaxProfile) {
|
||||
providers.minimax = buildMinimaxApiProvider();
|
||||
const minimaxKey =
|
||||
minimaxEnv?.apiKey ?? resolveMinimaxApiKeyFromStore(authStore);
|
||||
if (minimaxKey) {
|
||||
providers.minimax = buildMinimaxApiProvider(minimaxKey);
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripHeartbeatToken } from "./heartbeat.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
stripHeartbeatToken,
|
||||
} from "./heartbeat.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
describe("stripHeartbeatToken", () => {
|
||||
@@ -52,7 +55,7 @@ describe("stripHeartbeatToken", () => {
|
||||
});
|
||||
|
||||
it("keeps heartbeat replies when remaining content exceeds threshold", () => {
|
||||
const long = "A".repeat(400);
|
||||
const long = "A".repeat(DEFAULT_HEARTBEAT_ACK_MAX_CHARS + 1);
|
||||
expect(
|
||||
stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
{ timeoutMs: 5000 },
|
||||
).catch(() => null);
|
||||
|
||||
if (gitLog?.stdout.trim()) {
|
||||
if (gitLog && gitLog.code === 0 && gitLog.stdout.trim()) {
|
||||
note(
|
||||
`UI assets are older than the protocol schema.\nFunctional changes since last build:\n${gitLog.stdout
|
||||
.trim()
|
||||
@@ -64,7 +64,7 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
// Use scripts/ui.js to build, assuming node is available as we are running in it.
|
||||
// We use the same node executable to run the script.
|
||||
const uiScriptPath = path.join(root, "scripts/ui.js");
|
||||
await runCommandWithTimeout(
|
||||
const buildResult = await runCommandWithTimeout(
|
||||
[process.execPath, uiScriptPath, "build"],
|
||||
{
|
||||
cwd: root,
|
||||
@@ -72,11 +72,21 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
env: { ...process.env, FORCE_COLOR: "1" },
|
||||
},
|
||||
);
|
||||
note("UI rebuild complete.", "UI");
|
||||
if (buildResult.code === 0) {
|
||||
note("UI rebuild complete.", "UI");
|
||||
} else {
|
||||
const details = [
|
||||
`UI rebuild failed (exit ${buildResult.code ?? "unknown"}).`,
|
||||
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
note(details, "UI");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
// If files don't exist, we can't check.
|
||||
// If git fails, we silently skip.
|
||||
// runtime.debug(`UI freshness check failed: ${String(err)}`);
|
||||
|
||||
@@ -66,7 +66,6 @@ import {
|
||||
maybeMigrateLegacyConfigFile,
|
||||
normalizeLegacyConfigValues,
|
||||
} from "./doctor-legacy-config.js";
|
||||
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||
import {
|
||||
maybeRepairSandboxImages,
|
||||
@@ -81,6 +80,7 @@ import {
|
||||
detectLegacyStateMigrations,
|
||||
runLegacyStateMigrations,
|
||||
} from "./doctor-state-migrations.js";
|
||||
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
||||
import {
|
||||
detectLegacyWorkspaceDirs,
|
||||
formatLegacyWorkspaceWarning,
|
||||
@@ -249,6 +249,8 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
await maybeRepairUiProtocolFreshness(runtime, prompter);
|
||||
|
||||
await maybeMigrateLegacyConfigFile(runtime);
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
@@ -272,9 +274,9 @@ export async function doctorCommand(
|
||||
options.nonInteractive === true
|
||||
? true
|
||||
: await prompter.confirm({
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
});
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
||||
const { config: migrated, changes } = migrateLegacyConfig(
|
||||
@@ -327,9 +329,9 @@ export async function doctorCommand(
|
||||
: options.nonInteractive === true
|
||||
? false
|
||||
: await prompter.confirmRepair({
|
||||
message: "Generate and configure a gateway token now?",
|
||||
initialValue: true,
|
||||
});
|
||||
message: "Generate and configure a gateway token now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (shouldSetToken) {
|
||||
const nextToken = randomToken();
|
||||
cfg = {
|
||||
@@ -355,9 +357,9 @@ export async function doctorCommand(
|
||||
options.nonInteractive === true
|
||||
? true
|
||||
: await prompter.confirm({
|
||||
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
||||
initialValue: true,
|
||||
});
|
||||
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
const migrated = await runLegacyStateMigrations({
|
||||
detected: legacyState,
|
||||
@@ -479,11 +481,13 @@ export async function doctorCommand(
|
||||
note(
|
||||
[
|
||||
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
||||
`Missing requirements: ${skillsReport.skills.filter(
|
||||
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
||||
).length
|
||||
`Missing requirements: ${
|
||||
skillsReport.skills.filter(
|
||||
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
||||
).length
|
||||
}`,
|
||||
`Blocked by allowlist: ${skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
||||
`Blocked by allowlist: ${
|
||||
skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
||||
}`,
|
||||
].join("\n"),
|
||||
"Skills status",
|
||||
@@ -493,10 +497,10 @@ export async function doctorCommand(
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
logger: {
|
||||
info: () => { },
|
||||
warn: () => { },
|
||||
error: () => { },
|
||||
debug: () => { },
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
},
|
||||
});
|
||||
if (pluginRegistry.plugins.length > 0) {
|
||||
@@ -512,9 +516,9 @@ export async function doctorCommand(
|
||||
`Errors: ${errored.length}`,
|
||||
errored.length > 0
|
||||
? `- ${errored
|
||||
.slice(0, 10)
|
||||
.map((p) => p.id)
|
||||
.join("\n- ")}${errored.length > 10 ? "\n- ..." : ""}`
|
||||
.slice(0, 10)
|
||||
.map((p) => p.id)
|
||||
.join("\n- ")}${errored.length > 10 ? "\n- ..." : ""}`
|
||||
: null,
|
||||
].filter((line): line is string => Boolean(line));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user