add vault-backed token lookup for integrations
This commit is contained in:
parent
f83e43ec50
commit
87d573c528
@ -15,7 +15,10 @@ type ProbeBody = {
|
||||
gatewayUrl?: string
|
||||
gatewayToken?: string
|
||||
vaultUrl?: string
|
||||
vaultNamespace?: string
|
||||
vaultToken?: string
|
||||
vaultSecretPath?: string
|
||||
vaultSecretKey?: string
|
||||
apisixUrl?: string
|
||||
apisixToken?: string
|
||||
}
|
||||
@ -34,13 +37,17 @@ function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function gatewayErrorCode(error: OpenClawGatewayError | null): string | undefined {
|
||||
return error?.code ?? stringValue(asRecord(error?.details).code)
|
||||
}
|
||||
|
||||
function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClawGatewayClient): string {
|
||||
if (!error) {
|
||||
return 'Failed to probe OpenClaw gateway.'
|
||||
}
|
||||
|
||||
const details = asRecord(error.details)
|
||||
const detailCode = stringValue(details.code)
|
||||
const detailCode = gatewayErrorCode(error)
|
||||
if (detailCode === 'PAIRING_REQUIRED') {
|
||||
const requestId = stringValue(details.requestId)
|
||||
const reason = stringValue(details.reason)
|
||||
@ -58,9 +65,14 @@ function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClaw
|
||||
}
|
||||
|
||||
async function probeOpenClaw(body: ProbeBody): Promise<Response> {
|
||||
const config = resolveOpenClawGatewayConfig({
|
||||
const config = await resolveOpenClawGatewayConfig({
|
||||
gatewayUrl: body.gatewayUrl,
|
||||
gatewayToken: body.gatewayToken,
|
||||
vaultUrl: body.vaultUrl,
|
||||
vaultToken: body.vaultToken,
|
||||
vaultNamespace: body.vaultNamespace,
|
||||
vaultSecretPath: body.vaultSecretPath,
|
||||
vaultSecretKey: body.vaultSecretKey,
|
||||
})
|
||||
|
||||
if (!config.gatewayUrl) {
|
||||
@ -95,7 +107,7 @@ async function probeOpenClaw(body: ProbeBody): Promise<Response> {
|
||||
gatewayUrl: config.gatewayUrl,
|
||||
tokenSource: config.tokenSource,
|
||||
error: formatGatewayError(gatewayError, client),
|
||||
code: gatewayError?.code,
|
||||
code: gatewayErrorCode(gatewayError),
|
||||
details: gatewayError?.details ?? null,
|
||||
deviceId: client.deviceId || undefined,
|
||||
},
|
||||
@ -152,9 +164,13 @@ async function probeVault(body: ProbeBody): Promise<Response> {
|
||||
}
|
||||
|
||||
async function probeApisix(body: ProbeBody): Promise<Response> {
|
||||
const config = resolveApisixProbeConfig({
|
||||
const config = await resolveApisixProbeConfig({
|
||||
apisixUrl: body.apisixUrl,
|
||||
apisixToken: body.apisixToken,
|
||||
vaultUrl: body.vaultUrl,
|
||||
vaultToken: body.vaultToken,
|
||||
vaultNamespace: body.vaultNamespace,
|
||||
vaultSecretPath: body.vaultSecretPath,
|
||||
})
|
||||
|
||||
if (!config.apisixUrl) {
|
||||
|
||||
@ -19,6 +19,11 @@ type BootstrapBody = {
|
||||
action: 'bootstrap'
|
||||
gatewayUrl?: string
|
||||
gatewayToken?: string
|
||||
vaultUrl?: string
|
||||
vaultNamespace?: string
|
||||
vaultToken?: string
|
||||
vaultSecretPath?: string
|
||||
vaultSecretKey?: string
|
||||
agentId?: string
|
||||
sessionKey?: string
|
||||
}
|
||||
@ -27,6 +32,11 @@ type SendBody = {
|
||||
action: 'send'
|
||||
gatewayUrl?: string
|
||||
gatewayToken?: string
|
||||
vaultUrl?: string
|
||||
vaultNamespace?: string
|
||||
vaultToken?: string
|
||||
vaultSecretPath?: string
|
||||
vaultSecretKey?: string
|
||||
agentId?: string
|
||||
sessionKey?: string
|
||||
message?: string
|
||||
@ -62,13 +72,17 @@ function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function gatewayErrorCode(error: OpenClawGatewayError | null): string | undefined {
|
||||
return error?.code ?? stringValue(asRecord(error?.details).code)
|
||||
}
|
||||
|
||||
function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClawGatewayClient): string {
|
||||
if (!error) {
|
||||
return 'Failed to connect to OpenClaw gateway.'
|
||||
}
|
||||
|
||||
const details = asRecord(error.details)
|
||||
const detailCode = stringValue(details.code)
|
||||
const detailCode = gatewayErrorCode(error)
|
||||
if (detailCode === 'PAIRING_REQUIRED') {
|
||||
const requestId = stringValue(details.requestId)
|
||||
const reason = stringValue(details.reason)
|
||||
@ -99,9 +113,14 @@ function resolveSessionKey(params: {
|
||||
}
|
||||
|
||||
async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
||||
const gateway = resolveOpenClawGatewayConfig({
|
||||
const gateway = await resolveOpenClawGatewayConfig({
|
||||
gatewayUrl: body.gatewayUrl,
|
||||
gatewayToken: body.gatewayToken,
|
||||
vaultUrl: body.vaultUrl,
|
||||
vaultToken: body.vaultToken,
|
||||
vaultNamespace: body.vaultNamespace,
|
||||
vaultSecretPath: body.vaultSecretPath,
|
||||
vaultSecretKey: body.vaultSecretKey,
|
||||
})
|
||||
|
||||
if (!gateway.gatewayUrl) {
|
||||
@ -149,8 +168,8 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
||||
const gatewayError = error instanceof OpenClawGatewayError ? error : null
|
||||
return jsonError(
|
||||
formatGatewayError(gatewayError, client),
|
||||
gatewayError?.code === 'OFFLINE' ? 503 : 502,
|
||||
gatewayError?.code,
|
||||
gatewayErrorCode(gatewayError) === 'OFFLINE' ? 503 : 502,
|
||||
gatewayErrorCode(gatewayError),
|
||||
gatewayError?.details ?? null,
|
||||
client.deviceId || undefined,
|
||||
)
|
||||
@ -160,9 +179,14 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
||||
}
|
||||
|
||||
async function handleSend(body: SendBody): Promise<Response> {
|
||||
const gateway = resolveOpenClawGatewayConfig({
|
||||
const gateway = await resolveOpenClawGatewayConfig({
|
||||
gatewayUrl: body.gatewayUrl,
|
||||
gatewayToken: body.gatewayToken,
|
||||
vaultUrl: body.vaultUrl,
|
||||
vaultToken: body.vaultToken,
|
||||
vaultNamespace: body.vaultNamespace,
|
||||
vaultSecretPath: body.vaultSecretPath,
|
||||
vaultSecretKey: body.vaultSecretKey,
|
||||
})
|
||||
|
||||
if (!gateway.gatewayUrl) {
|
||||
|
||||
@ -34,6 +34,8 @@ export function AskAIDialog({
|
||||
vaultUrl: "",
|
||||
vaultNamespace: "",
|
||||
vaultTokenConfigured: false,
|
||||
vaultSecretPath: "",
|
||||
vaultSecretKey: "",
|
||||
apisixUrl: "",
|
||||
apisixTokenConfigured: false,
|
||||
};
|
||||
|
||||
@ -37,6 +37,7 @@ import {
|
||||
type GatewayChatAttachmentPayload,
|
||||
type GatewayChatMessage,
|
||||
type GatewaySessionSummary,
|
||||
type GatewayTokenSource,
|
||||
type IntegrationDefaults,
|
||||
type OpenClawBootstrapResponse,
|
||||
type OpenClawStreamEvent,
|
||||
@ -211,9 +212,8 @@ export function OpenClawAssistantPane({
|
||||
const [gatewayHealth, setGatewayHealth] = useState<Record<string, unknown>>(
|
||||
{},
|
||||
);
|
||||
const [gatewayTokenSource, setGatewayTokenSource] = useState<
|
||||
"env" | "request" | "none"
|
||||
>("none");
|
||||
const [gatewayTokenSource, setGatewayTokenSource] =
|
||||
useState<GatewayTokenSource>("none");
|
||||
const [mainSessionKey, setMainSessionKey] = useState("main");
|
||||
const [messages, setMessages] = useState<GatewayChatMessage[]>([]);
|
||||
const [sessions, setSessions] = useState<GatewaySessionSummary[]>([]);
|
||||
@ -231,6 +231,17 @@ export function OpenClawAssistantPane({
|
||||
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
|
||||
const openclawUrl = useOpenClawConsoleStore((state) => state.openclawUrl);
|
||||
const openclawToken = useOpenClawConsoleStore((state) => state.openclawToken);
|
||||
const vaultUrl = useOpenClawConsoleStore((state) => state.vaultUrl);
|
||||
const vaultNamespace = useOpenClawConsoleStore(
|
||||
(state) => state.vaultNamespace,
|
||||
);
|
||||
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
||||
const vaultSecretPath = useOpenClawConsoleStore(
|
||||
(state) => state.vaultSecretPath,
|
||||
);
|
||||
const vaultSecretKey = useOpenClawConsoleStore(
|
||||
(state) => state.vaultSecretKey,
|
||||
);
|
||||
const assistantMode = useOpenClawConsoleStore((state) => state.assistantMode);
|
||||
const thinking = useOpenClawConsoleStore((state) => state.thinking);
|
||||
const selectedAgentId = useOpenClawConsoleStore(
|
||||
@ -313,6 +324,7 @@ export function OpenClawAssistantPane({
|
||||
),
|
||||
envToken: pickCopy(isChinese, "环境变量令牌", "env token"),
|
||||
sessionToken: pickCopy(isChinese, "会话令牌", "session token"),
|
||||
vaultToken: pickCopy(isChinese, "Vault 令牌", "vault token"),
|
||||
noToken: pickCopy(isChinese, "无令牌", "no token"),
|
||||
mainAgent: pickCopy(isChinese, "主助手", "Main agent"),
|
||||
reconnect: pickCopy(isChinese, "重新连接", "Reconnect"),
|
||||
@ -425,6 +437,11 @@ export function OpenClawAssistantPane({
|
||||
action: "bootstrap",
|
||||
gatewayUrl: openclawUrl,
|
||||
gatewayToken: openclawToken,
|
||||
vaultUrl,
|
||||
vaultNamespace,
|
||||
vaultToken,
|
||||
vaultSecretPath,
|
||||
vaultSecretKey,
|
||||
agentId: nextAgentId ?? selectedAgentId,
|
||||
sessionKey: nextSessionKey ?? selectedSessionKey,
|
||||
}),
|
||||
@ -463,6 +480,11 @@ export function OpenClawAssistantPane({
|
||||
copy.serverMissing,
|
||||
openclawToken,
|
||||
openclawUrl,
|
||||
vaultNamespace,
|
||||
vaultSecretKey,
|
||||
vaultSecretPath,
|
||||
vaultToken,
|
||||
vaultUrl,
|
||||
selectedAgentId,
|
||||
selectedSessionKey,
|
||||
setSelectedSessionKey,
|
||||
@ -564,6 +586,11 @@ export function OpenClawAssistantPane({
|
||||
action: "send",
|
||||
gatewayUrl: openclawUrl,
|
||||
gatewayToken: openclawToken,
|
||||
vaultUrl,
|
||||
vaultNamespace,
|
||||
vaultToken,
|
||||
vaultSecretPath,
|
||||
vaultSecretKey,
|
||||
agentId: effectiveAgentId,
|
||||
sessionKey: effectiveSessionKey,
|
||||
message: prompt,
|
||||
@ -642,6 +669,11 @@ export function OpenClawAssistantPane({
|
||||
mainSessionKey,
|
||||
openclawToken,
|
||||
openclawUrl,
|
||||
vaultNamespace,
|
||||
vaultSecretKey,
|
||||
vaultSecretPath,
|
||||
vaultToken,
|
||||
vaultUrl,
|
||||
selectedAgentId,
|
||||
selectedSessionKey,
|
||||
setSelectedAgentId,
|
||||
@ -731,6 +763,8 @@ export function OpenClawAssistantPane({
|
||||
<span className="text-[var(--color-text-subtle)]/60">·</span>
|
||||
{gatewayTokenSource === "env"
|
||||
? copy.envToken
|
||||
: gatewayTokenSource === "vault"
|
||||
? copy.vaultToken
|
||||
: gatewayTokenSource === "request"
|
||||
? copy.sessionToken
|
||||
: copy.noToken}
|
||||
|
||||
@ -429,6 +429,9 @@ export function XWorkmateWorkspacePage({
|
||||
(state) => state.vaultNamespace,
|
||||
);
|
||||
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
||||
const vaultSecretPath = useOpenClawConsoleStore(
|
||||
(state) => state.vaultSecretPath,
|
||||
);
|
||||
const apisixUrl = useOpenClawConsoleStore((state) => state.apisixUrl);
|
||||
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
||||
const assistantMode = useOpenClawConsoleStore((state) => state.assistantMode);
|
||||
@ -493,6 +496,33 @@ export function XWorkmateWorkspacePage({
|
||||
const configuredIntegrationCount = integrationRows.filter(
|
||||
(item) => item.ok,
|
||||
).length;
|
||||
const hasVaultBackedToken = Boolean(
|
||||
vaultSecretPath.trim() && (vaultToken.trim() || defaults.vaultTokenConfigured),
|
||||
);
|
||||
const hasOpenClawCredential = Boolean(
|
||||
openclawToken.trim() || defaults.openclawTokenConfigured || hasVaultBackedToken,
|
||||
);
|
||||
const openclawConfigSource = openclawToken.trim()
|
||||
? "session"
|
||||
: defaults.openclawTokenConfigured
|
||||
? "env"
|
||||
: hasVaultBackedToken
|
||||
? "vault"
|
||||
: "pairing only";
|
||||
const vaultConfigSource = vaultToken.trim()
|
||||
? "session"
|
||||
: defaults.vaultTokenConfigured
|
||||
? "env"
|
||||
: vaultSecretPath.trim()
|
||||
? "vault path"
|
||||
: "manual";
|
||||
const apisixConfigSource = apisixToken.trim()
|
||||
? "session"
|
||||
: defaults.apisixTokenConfigured
|
||||
? "env"
|
||||
: vaultSecretPath.trim()
|
||||
? "vault path"
|
||||
: "manual";
|
||||
|
||||
const tasksOverview = useMemo<OverviewMetric[]>(
|
||||
() => [
|
||||
@ -520,10 +550,7 @@ export function XWorkmateWorkspacePage({
|
||||
},
|
||||
{
|
||||
label: pickCopy(isChinese, "失败", "Failed"),
|
||||
value:
|
||||
openclawToken.trim() || defaults.openclawTokenConfigured
|
||||
? "0"
|
||||
: "pairing",
|
||||
value: hasOpenClawCredential ? "0" : "pairing",
|
||||
caption: pickCopy(
|
||||
isChinese,
|
||||
"未配置 shared token 时优先走 pairing",
|
||||
@ -543,10 +570,9 @@ export function XWorkmateWorkspacePage({
|
||||
},
|
||||
],
|
||||
[
|
||||
defaults.openclawTokenConfigured,
|
||||
hasOpenClawCredential,
|
||||
isChinese,
|
||||
openclawEndpoint,
|
||||
openclawToken,
|
||||
selectedSessionKey,
|
||||
],
|
||||
);
|
||||
@ -618,7 +644,7 @@ export function XWorkmateWorkspacePage({
|
||||
label: pickCopy(isChinese, "Token 引用", "Token Refs"),
|
||||
value: `${
|
||||
[
|
||||
openclawToken.trim() || defaults.openclawTokenConfigured,
|
||||
hasOpenClawCredential,
|
||||
vaultToken.trim() || defaults.vaultTokenConfigured,
|
||||
apisixToken.trim() || defaults.apisixTokenConfigured,
|
||||
].filter(Boolean).length
|
||||
@ -655,10 +681,9 @@ export function XWorkmateWorkspacePage({
|
||||
apisixToken,
|
||||
configuredIntegrationCount,
|
||||
defaults.apisixTokenConfigured,
|
||||
defaults.openclawTokenConfigured,
|
||||
hasOpenClawCredential,
|
||||
defaults.vaultTokenConfigured,
|
||||
isChinese,
|
||||
openclawToken,
|
||||
vaultEndpoint,
|
||||
vaultToken,
|
||||
],
|
||||
@ -1438,8 +1463,10 @@ export function XWorkmateWorkspacePage({
|
||||
)
|
||||
: pickCopy(
|
||||
isChinese,
|
||||
"优先 env / pairing",
|
||||
"env / pairing preferred",
|
||||
hasVaultBackedToken ? "优先 Vault / env" : "优先 env / pairing",
|
||||
hasVaultBackedToken
|
||||
? "vault / env preferred"
|
||||
: "env / pairing preferred",
|
||||
)
|
||||
}
|
||||
ok={Boolean((openclawUrl || defaults.openclawUrl).trim())}
|
||||
@ -1642,13 +1669,13 @@ export function XWorkmateWorkspacePage({
|
||||
title={pickCopy(isChinese, "当前配置源", "Current config sources")}
|
||||
description={pickCopy(
|
||||
isChinese,
|
||||
"区分 env 与 session。",
|
||||
"Separate env from session overrides.",
|
||||
"区分 session / env / vault / pairing。",
|
||||
"Separate session, env, vault, and pairing sources.",
|
||||
)}
|
||||
body={[
|
||||
`OpenClaw: ${openclawToken.trim() ? "session" : defaults.openclawTokenConfigured ? "env" : "pairing only"}`,
|
||||
`Vault: ${vaultToken.trim() ? "session" : defaults.vaultTokenConfigured ? "env" : "manual"}`,
|
||||
`APISIX: ${apisixToken.trim() ? "session" : defaults.apisixTokenConfigured ? "env" : "manual"}`,
|
||||
`OpenClaw: ${openclawConfigSource}`,
|
||||
`Vault: ${vaultConfigSource}`,
|
||||
`APISIX: ${apisixConfigSource}`,
|
||||
].join(" · ")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@ export type AssistantMode = 'ask' | 'craft' | 'plan'
|
||||
|
||||
export type ThinkingLevel = 'low' | 'medium' | 'high' | 'max'
|
||||
|
||||
export type GatewayTokenSource = 'env' | 'request' | 'none'
|
||||
export type GatewayTokenSource = 'env' | 'request' | 'vault' | 'none'
|
||||
|
||||
export type GatewayAgentSummary = {
|
||||
id: string
|
||||
@ -83,6 +83,8 @@ export type IntegrationDefaults = {
|
||||
vaultUrl: string
|
||||
vaultNamespace: string
|
||||
vaultTokenConfigured: boolean
|
||||
vaultSecretPath: string
|
||||
vaultSecretKey: string
|
||||
apisixUrl: string
|
||||
apisixTokenConfigured: boolean
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ type ProbeState = {
|
||||
tokenSource?: string;
|
||||
body?: string;
|
||||
error?: string;
|
||||
code?: string;
|
||||
details?: Record<string, unknown> | null;
|
||||
deviceId?: string;
|
||||
};
|
||||
|
||||
type IntegrationsConsoleProps = {
|
||||
@ -80,12 +83,18 @@ function inputClassName(type: "input" | "textarea" = "input"): string {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
const EMPTY_DEFAULTS: IntegrationDefaults = {
|
||||
openclawUrl: "",
|
||||
openclawTokenConfigured: false,
|
||||
vaultUrl: "",
|
||||
vaultNamespace: "",
|
||||
vaultTokenConfigured: false,
|
||||
vaultSecretPath: "",
|
||||
vaultSecretKey: "",
|
||||
apisixUrl: "",
|
||||
apisixTokenConfigured: false,
|
||||
};
|
||||
@ -115,6 +124,12 @@ export function IntegrationsConsole({
|
||||
(state) => state.vaultNamespace,
|
||||
);
|
||||
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
||||
const vaultSecretPath = useOpenClawConsoleStore(
|
||||
(state) => state.vaultSecretPath,
|
||||
);
|
||||
const vaultSecretKey = useOpenClawConsoleStore(
|
||||
(state) => state.vaultSecretKey,
|
||||
);
|
||||
const apisixUrl = useOpenClawConsoleStore((state) => state.apisixUrl);
|
||||
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
||||
const setOpenclawUrl = useOpenClawConsoleStore(
|
||||
@ -128,6 +143,12 @@ export function IntegrationsConsole({
|
||||
(state) => state.setVaultNamespace,
|
||||
);
|
||||
const setVaultToken = useOpenClawConsoleStore((state) => state.setVaultToken);
|
||||
const setVaultSecretPath = useOpenClawConsoleStore(
|
||||
(state) => state.setVaultSecretPath,
|
||||
);
|
||||
const setVaultSecretKey = useOpenClawConsoleStore(
|
||||
(state) => state.setVaultSecretKey,
|
||||
);
|
||||
const setApisixUrl = useOpenClawConsoleStore((state) => state.setApisixUrl);
|
||||
const setApisixToken = useOpenClawConsoleStore(
|
||||
(state) => state.setApisixToken,
|
||||
@ -175,6 +196,7 @@ export function IntegrationsConsole({
|
||||
configured: Boolean(openclawUrl.trim()),
|
||||
tokenConfigured:
|
||||
resolvedDefaults.openclawTokenConfigured ||
|
||||
Boolean(vaultSecretPath.trim() && (vaultToken.trim() || resolvedDefaults.vaultTokenConfigured)) ||
|
||||
Boolean(openclawToken.trim()),
|
||||
},
|
||||
{
|
||||
@ -201,6 +223,7 @@ export function IntegrationsConsole({
|
||||
resolvedDefaults.openclawTokenConfigured,
|
||||
resolvedDefaults.vaultTokenConfigured,
|
||||
vaultToken,
|
||||
vaultSecretPath,
|
||||
vaultUrl,
|
||||
],
|
||||
);
|
||||
@ -219,7 +242,10 @@ export function IntegrationsConsole({
|
||||
gatewayUrl: openclawUrl,
|
||||
gatewayToken: openclawToken,
|
||||
vaultUrl,
|
||||
vaultNamespace,
|
||||
vaultToken,
|
||||
vaultSecretPath,
|
||||
vaultSecretKey,
|
||||
apisixUrl,
|
||||
apisixToken,
|
||||
}),
|
||||
@ -230,10 +256,13 @@ export function IntegrationsConsole({
|
||||
...current,
|
||||
[target]: {
|
||||
ok: Boolean(payload.ok),
|
||||
status: payload.status,
|
||||
status: payload.status ?? response.status,
|
||||
tokenSource: payload.tokenSource,
|
||||
body: payload.body,
|
||||
error: payload.error,
|
||||
code: payload.code,
|
||||
details: payload.details ?? null,
|
||||
deviceId: payload.deviceId,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
@ -242,6 +271,7 @@ export function IntegrationsConsole({
|
||||
[target]: {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : "Probe failed.",
|
||||
details: null,
|
||||
},
|
||||
}));
|
||||
} finally {
|
||||
@ -376,17 +406,86 @@ export function IntegrationsConsole({
|
||||
<span className="text-xs text-[var(--color-text-subtle)]">
|
||||
token source:{" "}
|
||||
{probeResults.openclaw.tokenSource ||
|
||||
(resolvedDefaults.openclawTokenConfigured ? "env" : "session")}
|
||||
(openclawToken.trim()
|
||||
? "request"
|
||||
: resolvedDefaults.openclawTokenConfigured
|
||||
? "env"
|
||||
: vaultSecretPath.trim() &&
|
||||
(vaultToken.trim() || resolvedDefaults.vaultTokenConfigured)
|
||||
? "vault"
|
||||
: "none")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{probeResults.openclaw.error ? (
|
||||
<div className="rounded-[var(--radius-xl)] border border-[color:var(--color-danger-border)] bg-[var(--color-danger-muted)]/40 px-4 py-3 text-sm text-[var(--color-danger-foreground)]">
|
||||
{probeResults.openclaw.error}
|
||||
<div className="space-y-2 rounded-[var(--radius-xl)] border border-[color:var(--color-danger-border)] bg-[var(--color-danger-muted)]/40 px-4 py-3 text-sm text-[var(--color-danger-foreground)]">
|
||||
<p>{probeResults.openclaw.error}</p>
|
||||
{probeResults.openclaw.code ? (
|
||||
<p className="text-xs opacity-80">code: {probeResults.openclaw.code}</p>
|
||||
) : null}
|
||||
{probeResults.openclaw.deviceId ? (
|
||||
<p className="text-xs opacity-80">
|
||||
deviceId: {probeResults.openclaw.deviceId}
|
||||
</p>
|
||||
) : null}
|
||||
{stringValue(probeResults.openclaw.details?.requestId) ? (
|
||||
<p className="text-xs opacity-80">
|
||||
requestId: {stringValue(probeResults.openclaw.details?.requestId)}
|
||||
</p>
|
||||
) : null}
|
||||
{stringValue(probeResults.openclaw.details?.reason) ? (
|
||||
<p className="text-xs opacity-80">
|
||||
reason: {stringValue(probeResults.openclaw.details?.reason)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
|
||||
<Card className="space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-[var(--color-primary-muted)] text-[var(--color-primary)]">
|
||||
<ShieldCheck className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold text-[var(--color-heading)]">
|
||||
Vault-backed token lookup
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--color-text-subtle)]">
|
||||
统一从 Vault 读取 OpenClaw / APISIX 等 token。当前会话值仍然优先。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1.2fr_1fr]">
|
||||
<Field
|
||||
label="Vault Secret Path"
|
||||
hint="例如 kv/openclaw。留空则不从 Vault secret 读取 token。"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={vaultSecretPath}
|
||||
onChange={(event) => setVaultSecretPath(event.target.value)}
|
||||
className={inputClassName()}
|
||||
placeholder="kv/openclaw"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Vault Secret Key"
|
||||
hint="可选。默认会优先尝试服务默认 key,再回退到 token。"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={vaultSecretKey}
|
||||
onChange={(event) => setVaultSecretKey(event.target.value)}
|
||||
className={inputClassName()}
|
||||
placeholder="token"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<Card className="space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@ -87,40 +87,182 @@ export function getConsoleIntegrationDefaults(): IntegrationDefaults {
|
||||
vaultUrl: normalizeHttpUrl(readEnvValue(...VAULT_URL_KEYS)),
|
||||
vaultNamespace: readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
||||
vaultTokenConfigured: Boolean(readEnvValue(...VAULT_TOKEN_KEYS)),
|
||||
vaultSecretPath: '',
|
||||
vaultSecretKey: '',
|
||||
apisixUrl: normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS)),
|
||||
apisixTokenConfigured: Boolean(readEnvValue(...APISIX_TOKEN_KEYS)),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOpenClawGatewayConfig(overrides?: {
|
||||
function normalizeSecretPath(value?: string): string {
|
||||
return value?.trim().replace(/^\/+|\/+$/g, '') ?? ''
|
||||
}
|
||||
|
||||
function buildVaultReadUrl(baseUrl: string, secretPath: string): string {
|
||||
const normalizedBase = normalizeHttpUrl(baseUrl)
|
||||
const normalizedPath = normalizeSecretPath(secretPath)
|
||||
|
||||
if (!normalizedBase || !normalizedPath) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (normalizedPath.startsWith('v1/')) {
|
||||
return `${normalizedBase}/${normalizedPath}`
|
||||
}
|
||||
|
||||
const segments = normalizedPath.split('/').filter(Boolean)
|
||||
if (segments.length === 1) {
|
||||
return `${normalizedBase}/v1/kv/data/${segments[0]}`
|
||||
}
|
||||
|
||||
const [mount, ...rest] = segments
|
||||
if (rest[0] === 'data') {
|
||||
return `${normalizedBase}/v1/${segments.join('/')}`
|
||||
}
|
||||
|
||||
return `${normalizedBase}/v1/${mount}/data/${rest.join('/')}`
|
||||
}
|
||||
|
||||
async function readVaultSecret(params: {
|
||||
vaultUrl: string
|
||||
vaultToken: string
|
||||
vaultNamespace?: string
|
||||
secretPath: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const url = buildVaultReadUrl(params.vaultUrl, params.secretPath)
|
||||
if (!url || !params.vaultToken.trim()) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Vault-Token': params.vaultToken.trim(),
|
||||
...(params.vaultNamespace?.trim()
|
||||
? {
|
||||
'X-Vault-Namespace': params.vaultNamespace.trim(),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as Record<string, unknown>
|
||||
const data = payload.data as Record<string, unknown> | undefined
|
||||
const kvV2Data = data?.data
|
||||
|
||||
if (kvV2Data && typeof kvV2Data === 'object' && !Array.isArray(kvV2Data)) {
|
||||
return kvV2Data as Record<string, unknown>
|
||||
}
|
||||
|
||||
return data && typeof data === 'object' && !Array.isArray(data) ? data : {}
|
||||
}
|
||||
|
||||
async function resolveVaultBackedToken(params: {
|
||||
requestToken?: string
|
||||
envToken?: string
|
||||
vaultUrl?: string
|
||||
vaultToken?: string
|
||||
vaultNamespace?: string
|
||||
vaultSecretPath?: string
|
||||
vaultSecretKey?: string
|
||||
fallbackKeys: string[]
|
||||
}): Promise<{ token: string; tokenSource: 'env' | 'request' | 'vault' | 'none' }> {
|
||||
const requestToken = params.requestToken?.trim() ?? ''
|
||||
if (requestToken) {
|
||||
return { token: requestToken, tokenSource: 'request' }
|
||||
}
|
||||
|
||||
const envToken = params.envToken?.trim() ?? ''
|
||||
if (envToken) {
|
||||
return { token: envToken, tokenSource: 'env' }
|
||||
}
|
||||
|
||||
const secretPath = normalizeSecretPath(params.vaultSecretPath)
|
||||
if (!secretPath) {
|
||||
return { token: '', tokenSource: 'none' }
|
||||
}
|
||||
|
||||
const secret = await readVaultSecret({
|
||||
vaultUrl: params.vaultUrl ?? '',
|
||||
vaultToken: params.vaultToken ?? '',
|
||||
vaultNamespace: params.vaultNamespace,
|
||||
secretPath,
|
||||
})
|
||||
|
||||
const candidateKeys = [
|
||||
params.vaultSecretKey?.trim(),
|
||||
...params.fallbackKeys,
|
||||
'token',
|
||||
].filter((value): value is string => Boolean(value && value.trim().length > 0))
|
||||
|
||||
for (const key of candidateKeys) {
|
||||
const value = secret[key]
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
return { token: value.trim(), tokenSource: 'vault' }
|
||||
}
|
||||
}
|
||||
|
||||
return { token: '', tokenSource: 'none' }
|
||||
}
|
||||
|
||||
export async function resolveOpenClawGatewayConfig(overrides?: {
|
||||
gatewayUrl?: string
|
||||
gatewayToken?: string
|
||||
}): { gatewayUrl: string; gatewayToken: string; tokenSource: 'env' | 'request' | 'none' } {
|
||||
vaultUrl?: string
|
||||
vaultToken?: string
|
||||
vaultNamespace?: string
|
||||
vaultSecretPath?: string
|
||||
vaultSecretKey?: string
|
||||
}): Promise<{ gatewayUrl: string; gatewayToken: string; tokenSource: 'env' | 'request' | 'vault' | 'none' }> {
|
||||
const requestUrl = normalizeWsUrl(overrides?.gatewayUrl)
|
||||
const envUrl = normalizeWsUrl(readEnvValue(...OPENCLAW_URL_KEYS))
|
||||
const requestToken = overrides?.gatewayToken?.trim() ?? ''
|
||||
const envToken = readEnvValue(...OPENCLAW_TOKEN_KEYS) ?? ''
|
||||
const resolvedToken = await resolveVaultBackedToken({
|
||||
requestToken: overrides?.gatewayToken,
|
||||
envToken: readEnvValue(...OPENCLAW_TOKEN_KEYS) ?? '',
|
||||
vaultUrl: overrides?.vaultUrl ?? readEnvValue(...VAULT_URL_KEYS) ?? '',
|
||||
vaultToken: overrides?.vaultToken ?? readEnvValue(...VAULT_TOKEN_KEYS) ?? '',
|
||||
vaultNamespace: overrides?.vaultNamespace ?? readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
||||
vaultSecretPath: overrides?.vaultSecretPath,
|
||||
vaultSecretKey: overrides?.vaultSecretKey,
|
||||
fallbackKeys: ['OPENCLAW_GATEWAY_TOKEN'],
|
||||
})
|
||||
|
||||
return {
|
||||
gatewayUrl: requestUrl || envUrl,
|
||||
gatewayToken: requestToken || envToken,
|
||||
tokenSource: requestToken ? 'request' : envToken ? 'env' : 'none',
|
||||
gatewayToken: resolvedToken.token,
|
||||
tokenSource: resolvedToken.tokenSource,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveApisixProbeConfig(overrides?: {
|
||||
export async function resolveApisixProbeConfig(overrides?: {
|
||||
apisixUrl?: string
|
||||
apisixToken?: string
|
||||
}): { apisixUrl: string; apisixToken: string; tokenSource: 'env' | 'request' | 'none' } {
|
||||
vaultUrl?: string
|
||||
vaultToken?: string
|
||||
vaultNamespace?: string
|
||||
vaultSecretPath?: string
|
||||
}): Promise<{ apisixUrl: string; apisixToken: string; tokenSource: 'env' | 'request' | 'vault' | 'none' }> {
|
||||
const requestUrl = normalizeHttpUrl(overrides?.apisixUrl)
|
||||
const envUrl = normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS))
|
||||
const requestToken = overrides?.apisixToken?.trim() ?? ''
|
||||
const envToken = readEnvValue(...APISIX_TOKEN_KEYS) ?? ''
|
||||
const resolvedToken = await resolveVaultBackedToken({
|
||||
requestToken: overrides?.apisixToken,
|
||||
envToken: readEnvValue(...APISIX_TOKEN_KEYS) ?? '',
|
||||
vaultUrl: overrides?.vaultUrl ?? readEnvValue(...VAULT_URL_KEYS) ?? '',
|
||||
vaultToken: overrides?.vaultToken ?? readEnvValue(...VAULT_TOKEN_KEYS) ?? '',
|
||||
vaultNamespace: overrides?.vaultNamespace ?? readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
||||
vaultSecretPath: overrides?.vaultSecretPath,
|
||||
fallbackKeys: ['AI_GATEWAY_ACCESS_TOKEN', 'APISIX_AI_GATEWAY_TOKEN'],
|
||||
})
|
||||
|
||||
return {
|
||||
apisixUrl: requestUrl || envUrl,
|
||||
apisixToken: requestToken || envToken,
|
||||
tokenSource: requestToken ? 'request' : envToken ? 'env' : 'none',
|
||||
apisixToken: resolvedToken.token,
|
||||
tokenSource: resolvedToken.tokenSource,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ type StoredDeviceIdentity = {
|
||||
const OPENCLAW_STATE_DIR = path.join(process.cwd(), '.console-state', 'openclaw')
|
||||
const DEVICE_IDENTITY_FILE = path.join(OPENCLAW_STATE_DIR, 'gateway-device-identity.json')
|
||||
let deviceIdentityPromise: Promise<StoredDeviceIdentity> | null = null
|
||||
let memoryDeviceIdentity: StoredDeviceIdentity | null = null
|
||||
const memoryDeviceTokens = new Map<string, string>()
|
||||
|
||||
function asStoredDeviceIdentity(value: unknown): StoredDeviceIdentity | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
@ -67,12 +69,17 @@ async function writeIdentity(identity: StoredDeviceIdentity): Promise<void> {
|
||||
}
|
||||
|
||||
export async function loadOrCreateOpenClawDeviceIdentity(): Promise<StoredDeviceIdentity> {
|
||||
if (memoryDeviceIdentity) {
|
||||
return memoryDeviceIdentity
|
||||
}
|
||||
|
||||
if (!deviceIdentityPromise) {
|
||||
deviceIdentityPromise = (async () => {
|
||||
try {
|
||||
const raw = await readFile(DEVICE_IDENTITY_FILE, 'utf8')
|
||||
const parsed = asStoredDeviceIdentity(JSON.parse(raw))
|
||||
if (parsed) {
|
||||
memoryDeviceIdentity = parsed
|
||||
return parsed
|
||||
}
|
||||
} catch {
|
||||
@ -94,7 +101,15 @@ export async function loadOrCreateOpenClawDeviceIdentity(): Promise<StoredDevice
|
||||
createdAtMs: Date.now(),
|
||||
}
|
||||
|
||||
memoryDeviceIdentity = identity
|
||||
|
||||
try {
|
||||
await writeIdentity(identity)
|
||||
} catch {
|
||||
// Serverless/preview runtimes may not allow persistent filesystem writes.
|
||||
// Keep an in-memory identity so pairing can still proceed for the current instance.
|
||||
}
|
||||
|
||||
return identity
|
||||
})()
|
||||
}
|
||||
@ -111,6 +126,11 @@ export async function loadOpenClawDeviceToken(params: {
|
||||
deviceId: string
|
||||
role?: string
|
||||
}): Promise<string> {
|
||||
const memoryToken = memoryDeviceTokens.get(`${params.deviceId}:${params.role?.trim() || 'operator'}`)
|
||||
if (memoryToken) {
|
||||
return memoryToken
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await readFile(deviceTokenFile(params.deviceId, params.role), 'utf8')
|
||||
return value.trim()
|
||||
@ -124,14 +144,23 @@ export async function saveOpenClawDeviceToken(params: {
|
||||
role?: string
|
||||
token: string
|
||||
}): Promise<void> {
|
||||
const key = `${params.deviceId}:${params.role?.trim() || 'operator'}`
|
||||
memoryDeviceTokens.set(key, params.token.trim())
|
||||
|
||||
try {
|
||||
await ensureStateDirectory()
|
||||
await writeFile(deviceTokenFile(params.deviceId, params.role), `${params.token.trim()}\n`, 'utf8')
|
||||
} catch {
|
||||
// Keep the token in memory when the runtime does not permit filesystem writes.
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearOpenClawDeviceToken(params: {
|
||||
deviceId: string
|
||||
role?: string
|
||||
}): Promise<void> {
|
||||
memoryDeviceTokens.delete(`${params.deviceId}:${params.role?.trim() || 'operator'}`)
|
||||
|
||||
try {
|
||||
await rm(deviceTokenFile(params.deviceId, params.role), { force: true })
|
||||
} catch {
|
||||
|
||||
@ -12,6 +12,8 @@ type OpenClawConsoleState = {
|
||||
vaultUrl: string
|
||||
vaultNamespace: string
|
||||
vaultToken: string
|
||||
vaultSecretPath: string
|
||||
vaultSecretKey: string
|
||||
apisixUrl: string
|
||||
apisixToken: string
|
||||
assistantMode: AssistantMode
|
||||
@ -24,6 +26,8 @@ type OpenClawConsoleState = {
|
||||
setVaultUrl: (value: string) => void
|
||||
setVaultNamespace: (value: string) => void
|
||||
setVaultToken: (value: string) => void
|
||||
setVaultSecretPath: (value: string) => void
|
||||
setVaultSecretKey: (value: string) => void
|
||||
setApisixUrl: (value: string) => void
|
||||
setApisixToken: (value: string) => void
|
||||
setAssistantMode: (value: AssistantMode) => void
|
||||
@ -41,6 +45,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
||||
vaultUrl: '',
|
||||
vaultNamespace: '',
|
||||
vaultToken: '',
|
||||
vaultSecretPath: '',
|
||||
vaultSecretKey: '',
|
||||
apisixUrl: '',
|
||||
apisixToken: '',
|
||||
assistantMode: 'ask',
|
||||
@ -54,6 +60,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
||||
openclawUrl: current.openclawUrl || defaults.openclawUrl,
|
||||
vaultUrl: current.vaultUrl || defaults.vaultUrl,
|
||||
vaultNamespace: current.vaultNamespace || defaults.vaultNamespace,
|
||||
vaultSecretPath: current.vaultSecretPath || defaults.vaultSecretPath,
|
||||
vaultSecretKey: current.vaultSecretKey || defaults.vaultSecretKey,
|
||||
apisixUrl: current.apisixUrl || defaults.apisixUrl,
|
||||
})
|
||||
},
|
||||
@ -62,6 +70,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
||||
setVaultUrl: (vaultUrl) => set({ vaultUrl }),
|
||||
setVaultNamespace: (vaultNamespace) => set({ vaultNamespace }),
|
||||
setVaultToken: (vaultToken) => set({ vaultToken }),
|
||||
setVaultSecretPath: (vaultSecretPath) => set({ vaultSecretPath }),
|
||||
setVaultSecretKey: (vaultSecretKey) => set({ vaultSecretKey }),
|
||||
setApisixUrl: (apisixUrl) => set({ apisixUrl }),
|
||||
setApisixToken: (apisixToken) => set({ apisixToken }),
|
||||
setAssistantMode: (assistantMode) => set({ assistantMode }),
|
||||
@ -78,6 +88,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
||||
vaultUrl: state.vaultUrl,
|
||||
vaultNamespace: state.vaultNamespace,
|
||||
vaultToken: state.vaultToken,
|
||||
vaultSecretPath: state.vaultSecretPath,
|
||||
vaultSecretKey: state.vaultSecretKey,
|
||||
apisixUrl: state.apisixUrl,
|
||||
apisixToken: state.apisixToken,
|
||||
assistantMode: state.assistantMode,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user