Merge branch 'main' into qianfan

This commit is contained in:
ide-rea
2026-02-04 22:39:13 +08:00
committed by GitHub
153 changed files with 4282 additions and 1535 deletions

View File

@@ -169,7 +169,11 @@ export async function resolveApiKeyForProfile(params: {
}
if (cred.type === "api_key") {
return { apiKey: cred.key, provider: cred.provider, email: cred.email };
const key = cred.key?.trim();
if (!key) {
return null;
}
return { apiKey: key, provider: cred.provider, email: cred.email };
}
if (cred.type === "token") {
const token = cred.token?.trim();

View File

@@ -4,8 +4,10 @@ import type { OpenClawConfig } from "../../config/config.js";
export type ApiKeyCredential = {
type: "api_key";
provider: string;
key: string;
key?: string;
email?: string;
/** Optional provider-specific metadata (e.g., account IDs, gateway IDs). */
metadata?: Record<string, string>;
};
export type TokenCredential = {

View File

@@ -0,0 +1,44 @@
import type { ModelDefinitionConfig } from "../config/types.js";
export const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway";
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-5";
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`;
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000;
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS = 64_000;
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_COST = {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
};
export function buildCloudflareAiGatewayModelDefinition(params?: {
id?: string;
name?: string;
reasoning?: boolean;
input?: Array<"text" | "image">;
}): ModelDefinitionConfig {
const id = params?.id?.trim() || CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID;
return {
id,
name: params?.name ?? "Claude Sonnet 4.5",
reasoning: params?.reasoning ?? true,
input: params?.input ?? ["text", "image"],
cost: CLOUDFLARE_AI_GATEWAY_DEFAULT_COST,
contextWindow: CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
maxTokens: CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS,
};
}
export function resolveCloudflareAiGatewayBaseUrl(params: {
accountId: string;
gatewayId: string;
}): string {
const accountId = params.accountId.trim();
const gatewayId = params.gatewayId.trim();
if (!accountId || !gatewayId) {
return "";
}
return `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`;
}

View File

@@ -293,6 +293,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
xai: "XAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
"cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
moonshot: "MOONSHOT_API_KEY",
minimax: "MINIMAX_API_KEY",
xiaomi: "XIAOMI_API_KEY",

View File

@@ -6,6 +6,10 @@ import {
} from "../providers/github-copilot-token.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import {
buildCloudflareAiGatewayModelDefinition,
resolveCloudflareAiGatewayBaseUrl,
} from "./cloudflare-ai-gateway.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
import {
buildSyntheticModelDefinition,
@@ -482,6 +486,34 @@ export async function resolveImplicitProviders(params: {
providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey };
}
const cloudflareProfiles = listProfilesForProvider(authStore, "cloudflare-ai-gateway");
for (const profileId of cloudflareProfiles) {
const cred = authStore.profiles[profileId];
if (cred?.type !== "api_key") {
continue;
}
const accountId = cred.metadata?.accountId?.trim();
const gatewayId = cred.metadata?.gatewayId?.trim();
if (!accountId || !gatewayId) {
continue;
}
const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId });
if (!baseUrl) {
continue;
}
const apiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway") ?? cred.key?.trim() ?? "";
if (!apiKey) {
continue;
}
providers["cloudflare-ai-gateway"] = {
baseUrl,
api: "anthropic-messages",
apiKey,
models: [buildCloudflareAiGatewayModelDefinition()],
};
break;
}
// Ollama provider - only add if explicitly configured
const ollamaKey =
resolveEnvApiKeyVarName("ollama") ??

View File

@@ -53,6 +53,10 @@ export function createOpenClawTools(options?: {
modelHasVision?: boolean;
/** Explicit agent ID override for cron/hook sessions. */
requesterAgentIdOverride?: string;
/** Require explicit message targets (no implicit last-route sends). */
requireExplicitMessageTarget?: boolean;
/** If true, omit the message tool from the tool list. */
disableMessageTool?: boolean;
}): AnyAgentTool[] {
const imageTool = options?.agentDir?.trim()
? createImageTool({
@@ -70,6 +74,20 @@ export function createOpenClawTools(options?: {
config: options?.config,
sandboxed: options?.sandboxed,
});
const messageTool = options?.disableMessageTool
? null
: createMessageTool({
agentAccountId: options?.agentAccountId,
agentSessionKey: options?.agentSessionKey,
config: options?.config,
currentChannelId: options?.currentChannelId,
currentChannelProvider: options?.agentChannel,
currentThreadTs: options?.currentThreadTs,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
sandboxRoot: options?.sandboxRoot,
requireExplicitTarget: options?.requireExplicitMessageTarget,
});
const tools: AnyAgentTool[] = [
createBrowserTool({
sandboxBridgeUrl: options?.sandboxBrowserBridgeUrl,
@@ -83,17 +101,7 @@ export function createOpenClawTools(options?: {
createCronTool({
agentSessionKey: options?.agentSessionKey,
}),
createMessageTool({
agentAccountId: options?.agentAccountId,
agentSessionKey: options?.agentSessionKey,
config: options?.config,
currentChannelId: options?.currentChannelId,
currentChannelProvider: options?.agentChannel,
currentThreadTs: options?.currentThreadTs,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
sandboxRoot: options?.sandboxRoot,
}),
...(messageTool ? [messageTool] : []),
createTtsTool({
agentChannel: options?.agentChannel,
config: options?.config,

View File

@@ -238,6 +238,9 @@ export async function runEmbeddedAttempt(
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
disableMessageTool: params.disableMessageTool,
});
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
logToolSchemasForGoogle({ tools, provider: params.provider });

View File

@@ -47,6 +47,10 @@ export type RunEmbeddedPiAgentParams = {
replyToMode?: "off" | "first" | "all";
/** Mutable ref to track if a reply was sent (for "first" mode). */
hasRepliedRef?: { value: boolean };
/** Require explicit message tool targets (no implicit last-route sends). */
requireExplicitMessageTarget?: boolean;
/** If true, omit the message tool from the tool list. */
disableMessageTool?: boolean;
sessionFile: string;
workspaceDir: string;
agentDir?: string;

View File

@@ -78,6 +78,10 @@ export type EmbeddedRunAttemptParams = {
onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
/** Require explicit message tool targets (no implicit last-route sends). */
requireExplicitMessageTarget?: boolean;
/** If true, omit the message tool from the tool list. */
disableMessageTool?: boolean;
extraSystemPrompt?: string;
streamParams?: AgentStreamParams;
ownerNumbers?: string[];

View File

@@ -157,6 +157,10 @@ export function createOpenClawCodingTools(options?: {
hasRepliedRef?: { value: boolean };
/** If true, the model has native vision capability */
modelHasVision?: boolean;
/** Require explicit message targets (no implicit last-route sends). */
requireExplicitMessageTarget?: boolean;
/** If true, omit the message tool from the tool list. */
disableMessageTool?: boolean;
}): AnyAgentTool[] {
const execToolName = "exec";
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
@@ -348,6 +352,8 @@ export function createOpenClawCodingTools(options?: {
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
modelHasVision: options?.modelHasVision,
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
requesterAgentIdOverride: agentId,
}),
];

View File

@@ -323,10 +323,10 @@ export function buildSubagentSystemPrompt(params: {
"",
"## What You DON'T Do",
"- NO user conversations (that's main agent's job)",
"- NO external messages (email, tweets, etc.) unless explicitly tasked",
"- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel",
"- NO cron jobs or persistent state",
"- NO pretending to be the main agent",
"- NO using the `message` tool directly",
"- Only use the `message` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the main agent deliver it",
"",
"## Session Context",
params.label ? `- Label: ${params.label}` : undefined,

View File

@@ -82,7 +82,9 @@ describe("cron tool", () => {
expect(call.method).toBe("cron.add");
expect(call.params).toEqual({
name: "wake-up",
schedule: { kind: "at", atMs: 123 },
enabled: true,
deleteAfterRun: true,
schedule: { kind: "at", at: new Date(123).toISOString() },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: { kind: "systemEvent", text: "hello" },
@@ -95,7 +97,7 @@ describe("cron tool", () => {
action: "add",
job: {
name: "wake-up",
schedule: { atMs: 123 },
schedule: { at: new Date(123).toISOString() },
agentId: null,
},
});
@@ -126,7 +128,7 @@ describe("cron tool", () => {
contextMessages: 3,
job: {
name: "reminder",
schedule: { atMs: 123 },
schedule: { at: new Date(123).toISOString() },
payload: { kind: "systemEvent", text: "Reminder: the thing." },
},
});
@@ -163,7 +165,7 @@ describe("cron tool", () => {
contextMessages: 20,
job: {
name: "reminder",
schedule: { atMs: 123 },
schedule: { at: new Date(123).toISOString() },
payload: { kind: "systemEvent", text: "Reminder: the thing." },
},
});
@@ -194,7 +196,7 @@ describe("cron tool", () => {
action: "add",
job: {
name: "reminder",
schedule: { atMs: 123 },
schedule: { at: new Date(123).toISOString() },
payload: { text: "Reminder: the thing." },
},
});
@@ -218,7 +220,7 @@ describe("cron tool", () => {
action: "add",
job: {
name: "reminder",
schedule: { atMs: 123 },
schedule: { at: new Date(123).toISOString() },
agentId: null,
payload: { kind: "systemEvent", text: "Reminder: the thing." },
},

View File

@@ -174,27 +174,36 @@ JOB SCHEMA (for add action):
"name": "string (optional)",
"schedule": { ... }, // Required: when to run
"payload": { ... }, // Required: what to execute
"delivery": { ... }, // Optional: announce summary (isolated only)
"sessionTarget": "main" | "isolated", // Required
"enabled": true | false // Optional, default true
}
SCHEDULE TYPES (schedule.kind):
- "at": One-shot at absolute time
{ "kind": "at", "atMs": <unix-ms-timestamp> }
{ "kind": "at", "at": "<ISO-8601 timestamp>" }
- "every": Recurring interval
{ "kind": "every", "everyMs": <interval-ms>, "anchorMs": <optional-start-ms> }
- "cron": Cron expression
{ "kind": "cron", "expr": "<cron-expression>", "tz": "<optional-timezone>" }
ISO timestamps without an explicit timezone are treated as UTC.
PAYLOAD TYPES (payload.kind):
- "systemEvent": Injects text as system event into session
{ "kind": "systemEvent", "text": "<message>" }
- "agentTurn": Runs agent with message (isolated sessions only)
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional>, "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional> }
DELIVERY (isolated-only, top-level):
{ "mode": "none|announce", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
- Default for isolated agentTurn jobs (when delivery omitted): "announce"
- If the task needs to send to a specific chat/recipient, set delivery.channel/to here; do not call messaging tools inside the run.
CRITICAL CONSTRAINTS:
- sessionTarget="main" REQUIRES payload.kind="systemEvent"
- sessionTarget="isolated" REQUIRES payload.kind="agentTurn"
Default: prefer isolated agentTurn jobs unless the user explicitly wants a main-session system event.
WAKE MODES (for wake action):
- "next-heartbeat" (default): Wake on next heartbeat
@@ -208,7 +217,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
const gatewayOpts: GatewayCallOptions = {
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : 60_000,
};
switch (action) {

View File

@@ -22,7 +22,7 @@ export function resolveGatewayOptions(opts?: GatewayCallOptions) {
const timeoutMs =
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
? Math.max(1, Math.floor(opts.timeoutMs))
: 10_000;
: 30_000;
return { url, token, timeoutMs };
}

View File

@@ -24,6 +24,18 @@ import { channelTargetSchema, channelTargetsSchema, stringEnum } from "../schema
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
const EXPLICIT_TARGET_ACTIONS = new Set<ChannelMessageActionName>([
"send",
"sendWithEffect",
"sendAttachment",
"reply",
"thread-reply",
"broadcast",
]);
function actionNeedsExplicitTarget(action: ChannelMessageActionName): boolean {
return EXPLICIT_TARGET_ACTIONS.has(action);
}
function buildRoutingSchema() {
return {
channel: Type.Optional(Type.String()),
@@ -285,6 +297,7 @@ type MessageToolOptions = {
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
sandboxRoot?: string;
requireExplicitTarget?: boolean;
};
function buildMessageToolSchema(cfg: OpenClawConfig) {
@@ -394,6 +407,20 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
const action = readStringParam(params, "action", {
required: true,
}) as ChannelMessageActionName;
const requireExplicitTarget = options?.requireExplicitTarget === true;
if (requireExplicitTarget && actionNeedsExplicitTarget(action)) {
const explicitTarget =
(typeof params.target === "string" && params.target.trim().length > 0) ||
(typeof params.to === "string" && params.to.trim().length > 0) ||
(typeof params.channelId === "string" && params.channelId.trim().length > 0) ||
(Array.isArray(params.targets) &&
params.targets.some((value) => typeof value === "string" && value.trim().length > 0));
if (!explicitTarget) {
throw new Error(
"Explicit message target required for this run. Provide target/targets (and channel when needed).",
);
}
}
// Validate file paths against sandbox root to prevent host file access.
const sandboxRoot = options?.sandboxRoot;

View File

@@ -104,7 +104,7 @@ function resolveModelAuthLabel(params: {
if (profile.type === "token") {
return `token ${formatApiKeySnippet(profile.token)}${label ? ` (${label})` : ""}`;
}
return `api-key ${formatApiKeySnippet(profile.key)}${label ? ` (${label})` : ""}`;
return `api-key ${formatApiKeySnippet(profile.key ?? "")}${label ? ` (${label})` : ""}`;
}
const envKey = resolveEnvApiKey(providerKey);