mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:48:28 +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 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 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";
|
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
|
||||||
|
|
||||||
describe("buildCleanupPlan", () => {
|
describe("buildCleanupPlan", () => {
|
||||||
@@ -50,3 +55,47 @@ describe("applyAgentDefaultPrimaryModel", () => {
|
|||||||
expect(result.next).toBe(cfg);
|
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;
|
skipped?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CleanupResolvedPaths = {
|
||||||
|
stateDir: string;
|
||||||
|
configPath: string;
|
||||||
|
oauthDir: string;
|
||||||
|
configInsideState: boolean;
|
||||||
|
oauthInsideState: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function collectWorkspaceDirs(cfg: OpenClawConfig | undefined): string[] {
|
export function collectWorkspaceDirs(cfg: OpenClawConfig | undefined): string[] {
|
||||||
const dirs = new Set<string>();
|
const dirs = new Set<string>();
|
||||||
const defaults = cfg?.agents?.defaults;
|
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[]> {
|
export async function listAgentSessionDirs(stateDir: string): Promise<string[]> {
|
||||||
const root = path.join(stateDir, "agents");
|
const root = path.join(stateDir, "agents");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
||||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.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 { isLoopbackHost, resolveGatewayBindHost } from "../gateway/net.js";
|
||||||
import { resolveDmAllowState } from "../security/dm-policy-shared.js";
|
import { resolveDmAllowState } from "../security/dm-policy-shared.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
|
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
|
||||||
|
|
||||||
export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
@@ -133,20 +133,11 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
|||||||
if (!plugin.security) {
|
if (!plugin.security) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const accountIds = plugin.config.listAccountIds(cfg);
|
const { defaultAccountId, account, enabled, configured } =
|
||||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
await resolveDefaultChannelAccountContext(plugin, cfg);
|
||||||
plugin,
|
|
||||||
cfg,
|
|
||||||
accountIds,
|
|
||||||
});
|
|
||||||
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
|
||||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const configured = plugin.config.isConfigured
|
|
||||||
? await plugin.config.isConfigured(account, cfg)
|
|
||||||
: true;
|
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,7 @@ import type { MessageActionRunResult } from "../infra/outbound/message-action-ru
|
|||||||
import { formatTargetDisplay } from "../infra/outbound/target-resolver.js";
|
import { formatTargetDisplay } from "../infra/outbound/target-resolver.js";
|
||||||
import { renderTable } from "../terminal/table.js";
|
import { renderTable } from "../terminal/table.js";
|
||||||
import { isRich, theme } from "../terminal/theme.js";
|
import { isRich, theme } from "../terminal/theme.js";
|
||||||
|
import { shortenText } from "./text-format.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("")}…`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveChannelLabel = (channel: ChannelId) =>
|
const resolveChannelLabel = (channel: ChannelId) =>
|
||||||
getChannelPlugin(channel)?.meta.label ?? channel;
|
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;
|
defaultModelId?: string;
|
||||||
},
|
},
|
||||||
): OpenClawConfig {
|
): OpenClawConfig {
|
||||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
const providerState = resolveProviderModelMergeState(cfg, params.providerId);
|
||||||
const existingProvider = providers[params.providerId] as ModelProviderConfig | undefined;
|
|
||||||
|
|
||||||
const existingModels: ModelDefinitionConfig[] = Array.isArray(existingProvider?.models)
|
|
||||||
? existingProvider.models
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const defaultModels = params.defaultModels;
|
const defaultModels = params.defaultModels;
|
||||||
const defaultModelId = params.defaultModelId ?? defaultModels[0]?.id;
|
const defaultModelId = params.defaultModelId ?? defaultModels[0]?.id;
|
||||||
const hasDefaultModel = defaultModelId
|
const hasDefaultModel = defaultModelId
|
||||||
? existingModels.some((model) => model.id === defaultModelId)
|
? providerState.existingModels.some((model) => model.id === defaultModelId)
|
||||||
: true;
|
: true;
|
||||||
const mergedModels =
|
const mergedModels =
|
||||||
existingModels.length > 0
|
providerState.existingModels.length > 0
|
||||||
? hasDefaultModel || defaultModels.length === 0
|
? hasDefaultModel || defaultModels.length === 0
|
||||||
? existingModels
|
? providerState.existingModels
|
||||||
: [...existingModels, ...defaultModels]
|
: [...providerState.existingModels, ...defaultModels]
|
||||||
: defaultModels;
|
: defaultModels;
|
||||||
providers[params.providerId] = buildProviderConfig({
|
return applyProviderConfigWithMergedModels(cfg, {
|
||||||
existingProvider,
|
agentModels: params.agentModels,
|
||||||
|
providerId: params.providerId,
|
||||||
|
providerState,
|
||||||
api: params.api,
|
api: params.api,
|
||||||
baseUrl: params.baseUrl,
|
baseUrl: params.baseUrl,
|
||||||
mergedModels,
|
mergedModels,
|
||||||
fallbackModels: defaultModels,
|
fallbackModels: defaultModels,
|
||||||
});
|
});
|
||||||
|
|
||||||
return applyOnboardAuthAgentModelsAndProviders(cfg, {
|
|
||||||
agentModels: params.agentModels,
|
|
||||||
providers,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyProviderConfigWithDefaultModel(
|
export function applyProviderConfigWithDefaultModel(
|
||||||
@@ -134,33 +126,68 @@ export function applyProviderConfigWithModelCatalog(
|
|||||||
catalogModels: ModelDefinitionConfig[];
|
catalogModels: ModelDefinitionConfig[];
|
||||||
},
|
},
|
||||||
): OpenClawConfig {
|
): OpenClawConfig {
|
||||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
const providerState = resolveProviderModelMergeState(cfg, params.providerId);
|
||||||
const existingProvider = providers[params.providerId] as ModelProviderConfig | undefined;
|
|
||||||
const existingModels: ModelDefinitionConfig[] = Array.isArray(existingProvider?.models)
|
|
||||||
? existingProvider.models
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const catalogModels = params.catalogModels;
|
const catalogModels = params.catalogModels;
|
||||||
const mergedModels =
|
const mergedModels =
|
||||||
existingModels.length > 0
|
providerState.existingModels.length > 0
|
||||||
? [
|
? [
|
||||||
...existingModels,
|
...providerState.existingModels,
|
||||||
...catalogModels.filter(
|
...catalogModels.filter(
|
||||||
(model) => !existingModels.some((existing) => existing.id === model.id),
|
(model) => !providerState.existingModels.some((existing) => existing.id === model.id),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: catalogModels;
|
: catalogModels;
|
||||||
providers[params.providerId] = buildProviderConfig({
|
return applyProviderConfigWithMergedModels(cfg, {
|
||||||
existingProvider,
|
agentModels: params.agentModels,
|
||||||
|
providerId: params.providerId,
|
||||||
|
providerState,
|
||||||
api: params.api,
|
api: params.api,
|
||||||
baseUrl: params.baseUrl,
|
baseUrl: params.baseUrl,
|
||||||
mergedModels,
|
mergedModels,
|
||||||
fallbackModels: catalogModels,
|
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, {
|
return applyOnboardAuthAgentModelsAndProviders(cfg, {
|
||||||
agentModels: params.agentModels,
|
agentModels: params.agentModels,
|
||||||
providers,
|
providers: params.providerState.providers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -383,6 +383,26 @@ async function promptCustomApiModelId(prompter: WizardPrompter): Promise<string>
|
|||||||
).trim();
|
).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(
|
function resolveProviderApi(
|
||||||
compatibility: CustomApiCompatibility,
|
compatibility: CustomApiCompatibility,
|
||||||
): "openai-completions" | "anthropic-messages" {
|
): "openai-completions" | "anthropic-messages" {
|
||||||
@@ -618,17 +638,11 @@ export async function promptCustomApiConfig(params: {
|
|||||||
"Endpoint detection",
|
"Endpoint detection",
|
||||||
);
|
);
|
||||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||||
const retryInput = await promptBaseUrlAndKey({
|
prompter,
|
||||||
prompter,
|
retryChoice,
|
||||||
initialBaseUrl: baseUrl,
|
current: { baseUrl, apiKey, modelId },
|
||||||
});
|
}));
|
||||||
baseUrl = retryInput.baseUrl;
|
|
||||||
apiKey = retryInput.apiKey;
|
|
||||||
}
|
|
||||||
if (retryChoice === "model" || retryChoice === "both") {
|
|
||||||
modelId = await promptCustomApiModelId(prompter);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,17 +667,11 @@ export async function promptCustomApiConfig(params: {
|
|||||||
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
||||||
}
|
}
|
||||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||||
const retryInput = await promptBaseUrlAndKey({
|
prompter,
|
||||||
prompter,
|
retryChoice,
|
||||||
initialBaseUrl: baseUrl,
|
current: { baseUrl, apiKey, modelId },
|
||||||
});
|
}));
|
||||||
baseUrl = retryInput.baseUrl;
|
|
||||||
apiKey = retryInput.apiKey;
|
|
||||||
}
|
|
||||||
if (retryChoice === "model" || retryChoice === "both") {
|
|
||||||
modelId = await promptCustomApiModelId(prompter);
|
|
||||||
}
|
|
||||||
if (compatibilityChoice === "unknown") {
|
if (compatibilityChoice === "unknown") {
|
||||||
compatibility = null;
|
compatibility = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||||||
import { selectStyled } from "../terminal/prompt-select-styled.js";
|
import { selectStyled } from "../terminal/prompt-select-styled.js";
|
||||||
import { stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
import { stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
import { resolveCleanupPlanFromDisk } from "./cleanup-plan.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";
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
|
|
||||||
@@ -129,16 +134,12 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scope === "full") {
|
if (scope === "full") {
|
||||||
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
await removeStateAndLinkedPaths(
|
||||||
if (!configInsideState) {
|
{ stateDir, configPath, oauthDir, configInsideState, oauthInsideState },
|
||||||
await removePath(configPath, runtime, { dryRun, label: configPath });
|
runtime,
|
||||||
}
|
{ dryRun },
|
||||||
if (!oauthInsideState) {
|
);
|
||||||
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
|
||||||
}
|
|
||||||
for (const workspace of workspaceDirs) {
|
|
||||||
await removePath(workspace, runtime, { dryRun, label: workspace });
|
|
||||||
}
|
|
||||||
runtime.log(`Next: ${formatCliCommand("openclaw onboard --install-daemon")}`);
|
runtime.log(`Next: ${formatCliCommand("openclaw onboard --install-daemon")}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
|
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
|
||||||
import { formatRuntimeStatusWithDetails } from "../infra/runtime-status.ts";
|
import { formatRuntimeStatusWithDetails } from "../infra/runtime-status.ts";
|
||||||
import type { SessionStatus } from "./status.types.js";
|
import type { SessionStatus } from "./status.types.js";
|
||||||
|
export { shortenText } from "./text-format.js";
|
||||||
|
|
||||||
export const formatKTokens = (value: number) =>
|
export const formatKTokens = (value: number) =>
|
||||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
`${(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 });
|
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 = (
|
export const formatTokensCompact = (
|
||||||
sess: Pick<
|
sess: Pick<
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
||||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js";
|
import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
|
||||||
|
|
||||||
export type LinkChannelContext = {
|
export type LinkChannelContext = {
|
||||||
linked: boolean;
|
linked: boolean;
|
||||||
@@ -15,17 +15,8 @@ export async function resolveLinkChannelContext(
|
|||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
): Promise<LinkChannelContext | null> {
|
): Promise<LinkChannelContext | null> {
|
||||||
for (const plugin of listChannelPlugins()) {
|
for (const plugin of listChannelPlugins()) {
|
||||||
const accountIds = plugin.config.listAccountIds(cfg);
|
const { defaultAccountId, account, enabled, configured } =
|
||||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
await resolveDefaultChannelAccountContext(plugin, cfg);
|
||||||
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 snapshot = plugin.config.describeAccount
|
const snapshot = plugin.config.describeAccount
|
||||||
? plugin.config.describeAccount(account, cfg)
|
? 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 { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
import { resolveHomeDir } from "../utils.js";
|
import { resolveHomeDir } from "../utils.js";
|
||||||
import { resolveCleanupPlanFromDisk } from "./cleanup-plan.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";
|
type UninstallScope = "service" | "state" | "workspace" | "app";
|
||||||
|
|
||||||
@@ -164,19 +164,15 @@ export async function uninstallCommand(runtime: RuntimeEnv, opts: UninstallOptio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scopes.has("state")) {
|
if (scopes.has("state")) {
|
||||||
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
await removeStateAndLinkedPaths(
|
||||||
if (!configInsideState) {
|
{ stateDir, configPath, oauthDir, configInsideState, oauthInsideState },
|
||||||
await removePath(configPath, runtime, { dryRun, label: configPath });
|
runtime,
|
||||||
}
|
{ dryRun },
|
||||||
if (!oauthInsideState) {
|
);
|
||||||
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopes.has("workspace")) {
|
if (scopes.has("workspace")) {
|
||||||
for (const workspace of workspaceDirs) {
|
await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
|
||||||
await removePath(workspace, runtime, { dryRun, label: workspace });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopes.has("app")) {
|
if (scopes.has("app")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user