mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
refactor(commands): centralize shared command formatting helpers
This commit is contained in:
47
src/commands/channel-account-context.test.ts
Normal file
47
src/commands/channel-account-context.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
|
||||
|
||||
describe("resolveDefaultChannelAccountContext", () => {
|
||||
it("uses enabled/configured defaults when hooks are missing", async () => {
|
||||
const account = { token: "x" };
|
||||
const plugin = {
|
||||
id: "demo",
|
||||
config: {
|
||||
listAccountIds: () => ["acc-1"],
|
||||
resolveAccount: () => account,
|
||||
},
|
||||
} as unknown as ChannelPlugin;
|
||||
|
||||
const result = await resolveDefaultChannelAccountContext(plugin, {} as OpenClawConfig);
|
||||
|
||||
expect(result.accountIds).toEqual(["acc-1"]);
|
||||
expect(result.defaultAccountId).toBe("acc-1");
|
||||
expect(result.account).toBe(account);
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(result.configured).toBe(true);
|
||||
});
|
||||
|
||||
it("uses plugin enable/configure hooks", async () => {
|
||||
const account = { enabled: false };
|
||||
const isEnabled = vi.fn(() => false);
|
||||
const isConfigured = vi.fn(async () => false);
|
||||
const plugin = {
|
||||
id: "demo",
|
||||
config: {
|
||||
listAccountIds: () => ["acc-2"],
|
||||
resolveAccount: () => account,
|
||||
isEnabled,
|
||||
isConfigured,
|
||||
},
|
||||
} as unknown as ChannelPlugin;
|
||||
|
||||
const result = await resolveDefaultChannelAccountContext(plugin, {} as OpenClawConfig);
|
||||
|
||||
expect(isEnabled).toHaveBeenCalledWith(account, {});
|
||||
expect(isConfigured).toHaveBeenCalledWith(account, {});
|
||||
expect(result.enabled).toBe(false);
|
||||
expect(result.configured).toBe(false);
|
||||
});
|
||||
});
|
||||
29
src/commands/channel-account-context.ts
Normal file
29
src/commands/channel-account-context.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export type ChannelDefaultAccountContext = {
|
||||
accountIds: string[];
|
||||
defaultAccountId?: string;
|
||||
account: unknown;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
export async function resolveDefaultChannelAccountContext(
|
||||
plugin: ChannelPlugin,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<ChannelDefaultAccountContext> {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
||||
const configured = plugin.config.isConfigured
|
||||
? await plugin.config.isConfigured(account, cfg)
|
||||
: true;
|
||||
return { accountIds, defaultAccountId, account, enabled, configured };
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, test } from "vitest";
|
||||
import { describe, expect, it, test, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { buildCleanupPlan } from "./cleanup-utils.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
buildCleanupPlan,
|
||||
removeStateAndLinkedPaths,
|
||||
removeWorkspaceDirs,
|
||||
} from "./cleanup-utils.js";
|
||||
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
|
||||
|
||||
describe("buildCleanupPlan", () => {
|
||||
@@ -50,3 +55,47 @@ describe("applyAgentDefaultPrimaryModel", () => {
|
||||
expect(result.next).toBe(cfg);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanup path removals", () => {
|
||||
function createRuntimeMock() {
|
||||
return {
|
||||
log: vi.fn<(message: string) => void>(),
|
||||
error: vi.fn<(message: string) => void>(),
|
||||
} as unknown as RuntimeEnv & {
|
||||
log: ReturnType<typeof vi.fn<(message: string) => void>>;
|
||||
error: ReturnType<typeof vi.fn<(message: string) => void>>;
|
||||
};
|
||||
}
|
||||
|
||||
it("removes state and only linked paths outside state", async () => {
|
||||
const runtime = createRuntimeMock();
|
||||
const tmpRoot = path.join(path.parse(process.cwd()).root, "tmp", "openclaw-cleanup");
|
||||
await removeStateAndLinkedPaths(
|
||||
{
|
||||
stateDir: path.join(tmpRoot, "state"),
|
||||
configPath: path.join(tmpRoot, "state", "openclaw.json"),
|
||||
oauthDir: path.join(tmpRoot, "oauth"),
|
||||
configInsideState: true,
|
||||
oauthInsideState: false,
|
||||
},
|
||||
runtime,
|
||||
{ dryRun: true },
|
||||
);
|
||||
|
||||
const joinedLogs = runtime.log.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(joinedLogs).toContain("[dry-run] remove /tmp/openclaw-cleanup/state");
|
||||
expect(joinedLogs).toContain("[dry-run] remove /tmp/openclaw-cleanup/oauth");
|
||||
expect(joinedLogs).not.toContain("openclaw.json");
|
||||
});
|
||||
|
||||
it("removes every workspace directory", async () => {
|
||||
const runtime = createRuntimeMock();
|
||||
const workspaces = ["/tmp/openclaw-workspace-1", "/tmp/openclaw-workspace-2"];
|
||||
|
||||
await removeWorkspaceDirs(workspaces, runtime, { dryRun: true });
|
||||
|
||||
const logs = runtime.log.mock.calls.map(([line]) => line);
|
||||
expect(logs).toContain("[dry-run] remove /tmp/openclaw-workspace-1");
|
||||
expect(logs).toContain("[dry-run] remove /tmp/openclaw-workspace-2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,14 @@ export type RemovalResult = {
|
||||
skipped?: boolean;
|
||||
};
|
||||
|
||||
export type CleanupResolvedPaths = {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
oauthDir: string;
|
||||
configInsideState: boolean;
|
||||
oauthInsideState: boolean;
|
||||
};
|
||||
|
||||
export function collectWorkspaceDirs(cfg: OpenClawConfig | undefined): string[] {
|
||||
const dirs = new Set<string>();
|
||||
const defaults = cfg?.agents?.defaults;
|
||||
@@ -96,6 +104,42 @@ export async function removePath(
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeStateAndLinkedPaths(
|
||||
cleanup: CleanupResolvedPaths,
|
||||
runtime: RuntimeEnv,
|
||||
opts?: { dryRun?: boolean },
|
||||
): Promise<void> {
|
||||
await removePath(cleanup.stateDir, runtime, {
|
||||
dryRun: opts?.dryRun,
|
||||
label: cleanup.stateDir,
|
||||
});
|
||||
if (!cleanup.configInsideState) {
|
||||
await removePath(cleanup.configPath, runtime, {
|
||||
dryRun: opts?.dryRun,
|
||||
label: cleanup.configPath,
|
||||
});
|
||||
}
|
||||
if (!cleanup.oauthInsideState) {
|
||||
await removePath(cleanup.oauthDir, runtime, {
|
||||
dryRun: opts?.dryRun,
|
||||
label: cleanup.oauthDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeWorkspaceDirs(
|
||||
workspaceDirs: readonly string[],
|
||||
runtime: RuntimeEnv,
|
||||
opts?: { dryRun?: boolean },
|
||||
): Promise<void> {
|
||||
for (const workspace of workspaceDirs) {
|
||||
await removePath(workspace, runtime, {
|
||||
dryRun: opts?.dryRun,
|
||||
label: workspace,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAgentSessionDirs(stateDir: string): Promise<string[]> {
|
||||
const root = path.join(stateDir, "agents");
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
@@ -7,6 +6,7 @@ import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { isLoopbackHost, resolveGatewayBindHost } from "../gateway/net.js";
|
||||
import { resolveDmAllowState } from "../security/dm-policy-shared.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
|
||||
|
||||
export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
||||
const warnings: string[] = [];
|
||||
@@ -133,20 +133,11 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
||||
if (!plugin.security) {
|
||||
continue;
|
||||
}
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
||||
const { defaultAccountId, account, enabled, configured } =
|
||||
await resolveDefaultChannelAccountContext(plugin, cfg);
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
const configured = plugin.config.isConfigured
|
||||
? await plugin.config.isConfigured(account, cfg)
|
||||
: true;
|
||||
if (!configured) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,7 @@ import type { MessageActionRunResult } from "../infra/outbound/message-action-ru
|
||||
import { formatTargetDisplay } from "../infra/outbound/target-resolver.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
|
||||
const shortenText = (value: string, maxLen: number) => {
|
||||
const chars = Array.from(value);
|
||||
if (chars.length <= maxLen) {
|
||||
return value;
|
||||
}
|
||||
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||
};
|
||||
import { shortenText } from "./text-format.js";
|
||||
|
||||
const resolveChannelLabel = (channel: ChannelId) =>
|
||||
getChannelPlugin(channel)?.meta.label ?? channel;
|
||||
|
||||
99
src/commands/onboard-auth.config-shared.test.ts
Normal file
99
src/commands/onboard-auth.config-shared.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentModelEntryConfig } from "../config/types.agent-defaults.js";
|
||||
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||
import {
|
||||
applyProviderConfigWithDefaultModel,
|
||||
applyProviderConfigWithDefaultModels,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
} from "./onboard-auth.config-shared.js";
|
||||
|
||||
function makeModel(id: string): ModelDefinitionConfig {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
contextWindow: 4096,
|
||||
maxTokens: 1024,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
reasoning: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("onboard auth provider config merges", () => {
|
||||
const agentModels: Record<string, AgentModelEntryConfig> = {
|
||||
"custom/model-a": {},
|
||||
};
|
||||
|
||||
it("appends missing default models to existing provider models", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://old.example.com/v1",
|
||||
apiKey: " test-key ",
|
||||
models: [makeModel("model-a")],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const next = applyProviderConfigWithDefaultModels(cfg, {
|
||||
agentModels,
|
||||
providerId: "custom",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://new.example.com/v1",
|
||||
defaultModels: [makeModel("model-b")],
|
||||
defaultModelId: "model-b",
|
||||
});
|
||||
|
||||
expect(next.models?.providers?.custom?.models?.map((m) => m.id)).toEqual([
|
||||
"model-a",
|
||||
"model-b",
|
||||
]);
|
||||
expect(next.models?.providers?.custom?.apiKey).toBe("test-key");
|
||||
expect(next.agents?.defaults?.models).toEqual(agentModels);
|
||||
});
|
||||
|
||||
it("merges model catalogs without duplicating existing model ids", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://example.com/v1",
|
||||
models: [makeModel("model-a")],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const next = applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels,
|
||||
providerId: "custom",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://example.com/v1",
|
||||
catalogModels: [makeModel("model-a"), makeModel("model-c")],
|
||||
});
|
||||
|
||||
expect(next.models?.providers?.custom?.models?.map((m) => m.id)).toEqual([
|
||||
"model-a",
|
||||
"model-c",
|
||||
]);
|
||||
});
|
||||
|
||||
it("supports single default model convenience wrapper", () => {
|
||||
const next = applyProviderConfigWithDefaultModel(
|
||||
{},
|
||||
{
|
||||
agentModels,
|
||||
providerId: "custom",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://example.com/v1",
|
||||
defaultModel: makeModel("model-z"),
|
||||
},
|
||||
);
|
||||
|
||||
expect(next.models?.providers?.custom?.models?.map((m) => m.id)).toEqual(["model-z"]);
|
||||
});
|
||||
});
|
||||
@@ -71,36 +71,28 @@ export function applyProviderConfigWithDefaultModels(
|
||||
defaultModelId?: string;
|
||||
},
|
||||
): OpenClawConfig {
|
||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
||||
const existingProvider = providers[params.providerId] as ModelProviderConfig | undefined;
|
||||
|
||||
const existingModels: ModelDefinitionConfig[] = Array.isArray(existingProvider?.models)
|
||||
? existingProvider.models
|
||||
: [];
|
||||
const providerState = resolveProviderModelMergeState(cfg, params.providerId);
|
||||
|
||||
const defaultModels = params.defaultModels;
|
||||
const defaultModelId = params.defaultModelId ?? defaultModels[0]?.id;
|
||||
const hasDefaultModel = defaultModelId
|
||||
? existingModels.some((model) => model.id === defaultModelId)
|
||||
? providerState.existingModels.some((model) => model.id === defaultModelId)
|
||||
: true;
|
||||
const mergedModels =
|
||||
existingModels.length > 0
|
||||
providerState.existingModels.length > 0
|
||||
? hasDefaultModel || defaultModels.length === 0
|
||||
? existingModels
|
||||
: [...existingModels, ...defaultModels]
|
||||
? providerState.existingModels
|
||||
: [...providerState.existingModels, ...defaultModels]
|
||||
: defaultModels;
|
||||
providers[params.providerId] = buildProviderConfig({
|
||||
existingProvider,
|
||||
return applyProviderConfigWithMergedModels(cfg, {
|
||||
agentModels: params.agentModels,
|
||||
providerId: params.providerId,
|
||||
providerState,
|
||||
api: params.api,
|
||||
baseUrl: params.baseUrl,
|
||||
mergedModels,
|
||||
fallbackModels: defaultModels,
|
||||
});
|
||||
|
||||
return applyOnboardAuthAgentModelsAndProviders(cfg, {
|
||||
agentModels: params.agentModels,
|
||||
providers,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyProviderConfigWithDefaultModel(
|
||||
@@ -134,33 +126,68 @@ export function applyProviderConfigWithModelCatalog(
|
||||
catalogModels: ModelDefinitionConfig[];
|
||||
},
|
||||
): OpenClawConfig {
|
||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
||||
const existingProvider = providers[params.providerId] as ModelProviderConfig | undefined;
|
||||
const existingModels: ModelDefinitionConfig[] = Array.isArray(existingProvider?.models)
|
||||
? existingProvider.models
|
||||
: [];
|
||||
|
||||
const providerState = resolveProviderModelMergeState(cfg, params.providerId);
|
||||
const catalogModels = params.catalogModels;
|
||||
const mergedModels =
|
||||
existingModels.length > 0
|
||||
providerState.existingModels.length > 0
|
||||
? [
|
||||
...existingModels,
|
||||
...providerState.existingModels,
|
||||
...catalogModels.filter(
|
||||
(model) => !existingModels.some((existing) => existing.id === model.id),
|
||||
(model) => !providerState.existingModels.some((existing) => existing.id === model.id),
|
||||
),
|
||||
]
|
||||
: catalogModels;
|
||||
providers[params.providerId] = buildProviderConfig({
|
||||
existingProvider,
|
||||
return applyProviderConfigWithMergedModels(cfg, {
|
||||
agentModels: params.agentModels,
|
||||
providerId: params.providerId,
|
||||
providerState,
|
||||
api: params.api,
|
||||
baseUrl: params.baseUrl,
|
||||
mergedModels,
|
||||
fallbackModels: catalogModels,
|
||||
});
|
||||
}
|
||||
|
||||
type ProviderModelMergeState = {
|
||||
providers: Record<string, ModelProviderConfig>;
|
||||
existingProvider?: ModelProviderConfig;
|
||||
existingModels: ModelDefinitionConfig[];
|
||||
};
|
||||
|
||||
function resolveProviderModelMergeState(
|
||||
cfg: OpenClawConfig,
|
||||
providerId: string,
|
||||
): ProviderModelMergeState {
|
||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
||||
const existingProvider = providers[providerId] as ModelProviderConfig | undefined;
|
||||
const existingModels: ModelDefinitionConfig[] = Array.isArray(existingProvider?.models)
|
||||
? existingProvider.models
|
||||
: [];
|
||||
return { providers, existingProvider, existingModels };
|
||||
}
|
||||
|
||||
function applyProviderConfigWithMergedModels(
|
||||
cfg: OpenClawConfig,
|
||||
params: {
|
||||
agentModels: Record<string, AgentModelEntryConfig>;
|
||||
providerId: string;
|
||||
providerState: ProviderModelMergeState;
|
||||
api: ModelApi;
|
||||
baseUrl: string;
|
||||
mergedModels: ModelDefinitionConfig[];
|
||||
fallbackModels: ModelDefinitionConfig[];
|
||||
},
|
||||
): OpenClawConfig {
|
||||
params.providerState.providers[params.providerId] = buildProviderConfig({
|
||||
existingProvider: params.providerState.existingProvider,
|
||||
api: params.api,
|
||||
baseUrl: params.baseUrl,
|
||||
mergedModels: params.mergedModels,
|
||||
fallbackModels: params.fallbackModels,
|
||||
});
|
||||
return applyOnboardAuthAgentModelsAndProviders(cfg, {
|
||||
agentModels: params.agentModels,
|
||||
providers,
|
||||
providers: params.providerState.providers,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -383,6 +383,26 @@ async function promptCustomApiModelId(prompter: WizardPrompter): Promise<string>
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function applyCustomApiRetryChoice(params: {
|
||||
prompter: WizardPrompter;
|
||||
retryChoice: CustomApiRetryChoice;
|
||||
current: { baseUrl: string; apiKey: string; modelId: string };
|
||||
}): Promise<{ baseUrl: string; apiKey: string; modelId: string }> {
|
||||
let { baseUrl, apiKey, modelId } = params.current;
|
||||
if (params.retryChoice === "baseUrl" || params.retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter: params.prompter,
|
||||
initialBaseUrl: baseUrl,
|
||||
});
|
||||
baseUrl = retryInput.baseUrl;
|
||||
apiKey = retryInput.apiKey;
|
||||
}
|
||||
if (params.retryChoice === "model" || params.retryChoice === "both") {
|
||||
modelId = await promptCustomApiModelId(params.prompter);
|
||||
}
|
||||
return { baseUrl, apiKey, modelId };
|
||||
}
|
||||
|
||||
function resolveProviderApi(
|
||||
compatibility: CustomApiCompatibility,
|
||||
): "openai-completions" | "anthropic-messages" {
|
||||
@@ -618,17 +638,11 @@ export async function promptCustomApiConfig(params: {
|
||||
"Endpoint detection",
|
||||
);
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter,
|
||||
initialBaseUrl: baseUrl,
|
||||
});
|
||||
baseUrl = retryInput.baseUrl;
|
||||
apiKey = retryInput.apiKey;
|
||||
}
|
||||
if (retryChoice === "model" || retryChoice === "both") {
|
||||
modelId = await promptCustomApiModelId(prompter);
|
||||
}
|
||||
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
prompter,
|
||||
retryChoice,
|
||||
current: { baseUrl, apiKey, modelId },
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -653,17 +667,11 @@ export async function promptCustomApiConfig(params: {
|
||||
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
||||
}
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter,
|
||||
initialBaseUrl: baseUrl,
|
||||
});
|
||||
baseUrl = retryInput.baseUrl;
|
||||
apiKey = retryInput.apiKey;
|
||||
}
|
||||
if (retryChoice === "model" || retryChoice === "both") {
|
||||
modelId = await promptCustomApiModelId(prompter);
|
||||
}
|
||||
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
prompter,
|
||||
retryChoice,
|
||||
current: { baseUrl, apiKey, modelId },
|
||||
}));
|
||||
if (compatibilityChoice === "unknown") {
|
||||
compatibility = null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { selectStyled } from "../terminal/prompt-select-styled.js";
|
||||
import { stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import { resolveCleanupPlanFromDisk } from "./cleanup-plan.js";
|
||||
import { listAgentSessionDirs, removePath } from "./cleanup-utils.js";
|
||||
import {
|
||||
listAgentSessionDirs,
|
||||
removePath,
|
||||
removeStateAndLinkedPaths,
|
||||
removeWorkspaceDirs,
|
||||
} from "./cleanup-utils.js";
|
||||
|
||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||
|
||||
@@ -129,16 +134,12 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
|
||||
}
|
||||
|
||||
if (scope === "full") {
|
||||
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
||||
if (!configInsideState) {
|
||||
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||
}
|
||||
if (!oauthInsideState) {
|
||||
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
||||
}
|
||||
for (const workspace of workspaceDirs) {
|
||||
await removePath(workspace, runtime, { dryRun, label: workspace });
|
||||
}
|
||||
await removeStateAndLinkedPaths(
|
||||
{ stateDir, configPath, oauthDir, configInsideState, oauthInsideState },
|
||||
runtime,
|
||||
{ dryRun },
|
||||
);
|
||||
await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
|
||||
runtime.log(`Next: ${formatCliCommand("openclaw onboard --install-daemon")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
|
||||
import { formatRuntimeStatusWithDetails } from "../infra/runtime-status.ts";
|
||||
import type { SessionStatus } from "./status.types.js";
|
||||
export { shortenText } from "./text-format.js";
|
||||
|
||||
export const formatKTokens = (value: number) =>
|
||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
||||
@@ -12,14 +13,6 @@ export const formatDuration = (ms: number | null | undefined) => {
|
||||
return formatDurationPrecise(ms, { decimals: 1 });
|
||||
};
|
||||
|
||||
export const shortenText = (value: string, maxLen: number) => {
|
||||
const chars = Array.from(value);
|
||||
if (chars.length <= maxLen) {
|
||||
return value;
|
||||
}
|
||||
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||
};
|
||||
|
||||
export const formatTokensCompact = (
|
||||
sess: Pick<
|
||||
SessionStatus,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
|
||||
|
||||
export type LinkChannelContext = {
|
||||
linked: boolean;
|
||||
@@ -15,17 +15,8 @@ export async function resolveLinkChannelContext(
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<LinkChannelContext | null> {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
||||
const configured = plugin.config.isConfigured
|
||||
? await plugin.config.isConfigured(account, cfg)
|
||||
: true;
|
||||
const { defaultAccountId, account, enabled, configured } =
|
||||
await resolveDefaultChannelAccountContext(plugin, cfg);
|
||||
const snapshot = plugin.config.describeAccount
|
||||
? plugin.config.describeAccount(account, cfg)
|
||||
: ({
|
||||
|
||||
16
src/commands/text-format.test.ts
Normal file
16
src/commands/text-format.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shortenText } from "./text-format.js";
|
||||
|
||||
describe("shortenText", () => {
|
||||
it("returns original text when it fits", () => {
|
||||
expect(shortenText("openclaw", 16)).toBe("openclaw");
|
||||
});
|
||||
|
||||
it("truncates and appends ellipsis when over limit", () => {
|
||||
expect(shortenText("openclaw-status-output", 10)).toBe("openclaw-…");
|
||||
});
|
||||
|
||||
it("counts multi-byte characters correctly", () => {
|
||||
expect(shortenText("hello🙂world", 7)).toBe("hello🙂…");
|
||||
});
|
||||
});
|
||||
7
src/commands/text-format.ts
Normal file
7
src/commands/text-format.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const shortenText = (value: string, maxLen: number) => {
|
||||
const chars = Array.from(value);
|
||||
if (chars.length <= maxLen) {
|
||||
return value;
|
||||
}
|
||||
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import { resolveHomeDir } from "../utils.js";
|
||||
import { resolveCleanupPlanFromDisk } from "./cleanup-plan.js";
|
||||
import { removePath } from "./cleanup-utils.js";
|
||||
import { removePath, removeStateAndLinkedPaths, removeWorkspaceDirs } from "./cleanup-utils.js";
|
||||
|
||||
type UninstallScope = "service" | "state" | "workspace" | "app";
|
||||
|
||||
@@ -164,19 +164,15 @@ export async function uninstallCommand(runtime: RuntimeEnv, opts: UninstallOptio
|
||||
}
|
||||
|
||||
if (scopes.has("state")) {
|
||||
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
||||
if (!configInsideState) {
|
||||
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||
}
|
||||
if (!oauthInsideState) {
|
||||
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
||||
}
|
||||
await removeStateAndLinkedPaths(
|
||||
{ stateDir, configPath, oauthDir, configInsideState, oauthInsideState },
|
||||
runtime,
|
||||
{ dryRun },
|
||||
);
|
||||
}
|
||||
|
||||
if (scopes.has("workspace")) {
|
||||
for (const workspace of workspaceDirs) {
|
||||
await removePath(workspace, runtime, { dryRun, label: workspace });
|
||||
}
|
||||
await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
|
||||
}
|
||||
|
||||
if (scopes.has("app")) {
|
||||
|
||||
Reference in New Issue
Block a user