add vault-backed token lookup for integrations
This commit is contained in:
parent
f83e43ec50
commit
87d573c528
@ -15,7 +15,10 @@ type ProbeBody = {
|
|||||||
gatewayUrl?: string
|
gatewayUrl?: string
|
||||||
gatewayToken?: string
|
gatewayToken?: string
|
||||||
vaultUrl?: string
|
vaultUrl?: string
|
||||||
|
vaultNamespace?: string
|
||||||
vaultToken?: string
|
vaultToken?: string
|
||||||
|
vaultSecretPath?: string
|
||||||
|
vaultSecretKey?: string
|
||||||
apisixUrl?: string
|
apisixUrl?: string
|
||||||
apisixToken?: string
|
apisixToken?: string
|
||||||
}
|
}
|
||||||
@ -34,13 +37,17 @@ function stringValue(value: unknown): string | undefined {
|
|||||||
return typeof value === 'string' && value.trim().length > 0 ? value : 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 {
|
function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClawGatewayClient): string {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return 'Failed to probe OpenClaw gateway.'
|
return 'Failed to probe OpenClaw gateway.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = asRecord(error.details)
|
const details = asRecord(error.details)
|
||||||
const detailCode = stringValue(details.code)
|
const detailCode = gatewayErrorCode(error)
|
||||||
if (detailCode === 'PAIRING_REQUIRED') {
|
if (detailCode === 'PAIRING_REQUIRED') {
|
||||||
const requestId = stringValue(details.requestId)
|
const requestId = stringValue(details.requestId)
|
||||||
const reason = stringValue(details.reason)
|
const reason = stringValue(details.reason)
|
||||||
@ -58,9 +65,14 @@ function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClaw
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function probeOpenClaw(body: ProbeBody): Promise<Response> {
|
async function probeOpenClaw(body: ProbeBody): Promise<Response> {
|
||||||
const config = resolveOpenClawGatewayConfig({
|
const config = await resolveOpenClawGatewayConfig({
|
||||||
gatewayUrl: body.gatewayUrl,
|
gatewayUrl: body.gatewayUrl,
|
||||||
gatewayToken: body.gatewayToken,
|
gatewayToken: body.gatewayToken,
|
||||||
|
vaultUrl: body.vaultUrl,
|
||||||
|
vaultToken: body.vaultToken,
|
||||||
|
vaultNamespace: body.vaultNamespace,
|
||||||
|
vaultSecretPath: body.vaultSecretPath,
|
||||||
|
vaultSecretKey: body.vaultSecretKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!config.gatewayUrl) {
|
if (!config.gatewayUrl) {
|
||||||
@ -95,7 +107,7 @@ async function probeOpenClaw(body: ProbeBody): Promise<Response> {
|
|||||||
gatewayUrl: config.gatewayUrl,
|
gatewayUrl: config.gatewayUrl,
|
||||||
tokenSource: config.tokenSource,
|
tokenSource: config.tokenSource,
|
||||||
error: formatGatewayError(gatewayError, client),
|
error: formatGatewayError(gatewayError, client),
|
||||||
code: gatewayError?.code,
|
code: gatewayErrorCode(gatewayError),
|
||||||
details: gatewayError?.details ?? null,
|
details: gatewayError?.details ?? null,
|
||||||
deviceId: client.deviceId || undefined,
|
deviceId: client.deviceId || undefined,
|
||||||
},
|
},
|
||||||
@ -152,9 +164,13 @@ async function probeVault(body: ProbeBody): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function probeApisix(body: ProbeBody): Promise<Response> {
|
async function probeApisix(body: ProbeBody): Promise<Response> {
|
||||||
const config = resolveApisixProbeConfig({
|
const config = await resolveApisixProbeConfig({
|
||||||
apisixUrl: body.apisixUrl,
|
apisixUrl: body.apisixUrl,
|
||||||
apisixToken: body.apisixToken,
|
apisixToken: body.apisixToken,
|
||||||
|
vaultUrl: body.vaultUrl,
|
||||||
|
vaultToken: body.vaultToken,
|
||||||
|
vaultNamespace: body.vaultNamespace,
|
||||||
|
vaultSecretPath: body.vaultSecretPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!config.apisixUrl) {
|
if (!config.apisixUrl) {
|
||||||
|
|||||||
@ -19,6 +19,11 @@ type BootstrapBody = {
|
|||||||
action: 'bootstrap'
|
action: 'bootstrap'
|
||||||
gatewayUrl?: string
|
gatewayUrl?: string
|
||||||
gatewayToken?: string
|
gatewayToken?: string
|
||||||
|
vaultUrl?: string
|
||||||
|
vaultNamespace?: string
|
||||||
|
vaultToken?: string
|
||||||
|
vaultSecretPath?: string
|
||||||
|
vaultSecretKey?: string
|
||||||
agentId?: string
|
agentId?: string
|
||||||
sessionKey?: string
|
sessionKey?: string
|
||||||
}
|
}
|
||||||
@ -27,6 +32,11 @@ type SendBody = {
|
|||||||
action: 'send'
|
action: 'send'
|
||||||
gatewayUrl?: string
|
gatewayUrl?: string
|
||||||
gatewayToken?: string
|
gatewayToken?: string
|
||||||
|
vaultUrl?: string
|
||||||
|
vaultNamespace?: string
|
||||||
|
vaultToken?: string
|
||||||
|
vaultSecretPath?: string
|
||||||
|
vaultSecretKey?: string
|
||||||
agentId?: string
|
agentId?: string
|
||||||
sessionKey?: string
|
sessionKey?: string
|
||||||
message?: string
|
message?: string
|
||||||
@ -62,13 +72,17 @@ function stringValue(value: unknown): string | undefined {
|
|||||||
return typeof value === 'string' && value.trim().length > 0 ? value : 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 {
|
function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClawGatewayClient): string {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return 'Failed to connect to OpenClaw gateway.'
|
return 'Failed to connect to OpenClaw gateway.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = asRecord(error.details)
|
const details = asRecord(error.details)
|
||||||
const detailCode = stringValue(details.code)
|
const detailCode = gatewayErrorCode(error)
|
||||||
if (detailCode === 'PAIRING_REQUIRED') {
|
if (detailCode === 'PAIRING_REQUIRED') {
|
||||||
const requestId = stringValue(details.requestId)
|
const requestId = stringValue(details.requestId)
|
||||||
const reason = stringValue(details.reason)
|
const reason = stringValue(details.reason)
|
||||||
@ -99,9 +113,14 @@ function resolveSessionKey(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
||||||
const gateway = resolveOpenClawGatewayConfig({
|
const gateway = await resolveOpenClawGatewayConfig({
|
||||||
gatewayUrl: body.gatewayUrl,
|
gatewayUrl: body.gatewayUrl,
|
||||||
gatewayToken: body.gatewayToken,
|
gatewayToken: body.gatewayToken,
|
||||||
|
vaultUrl: body.vaultUrl,
|
||||||
|
vaultToken: body.vaultToken,
|
||||||
|
vaultNamespace: body.vaultNamespace,
|
||||||
|
vaultSecretPath: body.vaultSecretPath,
|
||||||
|
vaultSecretKey: body.vaultSecretKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!gateway.gatewayUrl) {
|
if (!gateway.gatewayUrl) {
|
||||||
@ -149,8 +168,8 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
|||||||
const gatewayError = error instanceof OpenClawGatewayError ? error : null
|
const gatewayError = error instanceof OpenClawGatewayError ? error : null
|
||||||
return jsonError(
|
return jsonError(
|
||||||
formatGatewayError(gatewayError, client),
|
formatGatewayError(gatewayError, client),
|
||||||
gatewayError?.code === 'OFFLINE' ? 503 : 502,
|
gatewayErrorCode(gatewayError) === 'OFFLINE' ? 503 : 502,
|
||||||
gatewayError?.code,
|
gatewayErrorCode(gatewayError),
|
||||||
gatewayError?.details ?? null,
|
gatewayError?.details ?? null,
|
||||||
client.deviceId || undefined,
|
client.deviceId || undefined,
|
||||||
)
|
)
|
||||||
@ -160,9 +179,14 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSend(body: SendBody): Promise<Response> {
|
async function handleSend(body: SendBody): Promise<Response> {
|
||||||
const gateway = resolveOpenClawGatewayConfig({
|
const gateway = await resolveOpenClawGatewayConfig({
|
||||||
gatewayUrl: body.gatewayUrl,
|
gatewayUrl: body.gatewayUrl,
|
||||||
gatewayToken: body.gatewayToken,
|
gatewayToken: body.gatewayToken,
|
||||||
|
vaultUrl: body.vaultUrl,
|
||||||
|
vaultToken: body.vaultToken,
|
||||||
|
vaultNamespace: body.vaultNamespace,
|
||||||
|
vaultSecretPath: body.vaultSecretPath,
|
||||||
|
vaultSecretKey: body.vaultSecretKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!gateway.gatewayUrl) {
|
if (!gateway.gatewayUrl) {
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export function AskAIDialog({
|
|||||||
vaultUrl: "",
|
vaultUrl: "",
|
||||||
vaultNamespace: "",
|
vaultNamespace: "",
|
||||||
vaultTokenConfigured: false,
|
vaultTokenConfigured: false,
|
||||||
|
vaultSecretPath: "",
|
||||||
|
vaultSecretKey: "",
|
||||||
apisixUrl: "",
|
apisixUrl: "",
|
||||||
apisixTokenConfigured: false,
|
apisixTokenConfigured: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import {
|
|||||||
type GatewayChatAttachmentPayload,
|
type GatewayChatAttachmentPayload,
|
||||||
type GatewayChatMessage,
|
type GatewayChatMessage,
|
||||||
type GatewaySessionSummary,
|
type GatewaySessionSummary,
|
||||||
|
type GatewayTokenSource,
|
||||||
type IntegrationDefaults,
|
type IntegrationDefaults,
|
||||||
type OpenClawBootstrapResponse,
|
type OpenClawBootstrapResponse,
|
||||||
type OpenClawStreamEvent,
|
type OpenClawStreamEvent,
|
||||||
@ -211,9 +212,8 @@ export function OpenClawAssistantPane({
|
|||||||
const [gatewayHealth, setGatewayHealth] = useState<Record<string, unknown>>(
|
const [gatewayHealth, setGatewayHealth] = useState<Record<string, unknown>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const [gatewayTokenSource, setGatewayTokenSource] = useState<
|
const [gatewayTokenSource, setGatewayTokenSource] =
|
||||||
"env" | "request" | "none"
|
useState<GatewayTokenSource>("none");
|
||||||
>("none");
|
|
||||||
const [mainSessionKey, setMainSessionKey] = useState("main");
|
const [mainSessionKey, setMainSessionKey] = useState("main");
|
||||||
const [messages, setMessages] = useState<GatewayChatMessage[]>([]);
|
const [messages, setMessages] = useState<GatewayChatMessage[]>([]);
|
||||||
const [sessions, setSessions] = useState<GatewaySessionSummary[]>([]);
|
const [sessions, setSessions] = useState<GatewaySessionSummary[]>([]);
|
||||||
@ -231,6 +231,17 @@ export function OpenClawAssistantPane({
|
|||||||
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
|
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
|
||||||
const openclawUrl = useOpenClawConsoleStore((state) => state.openclawUrl);
|
const openclawUrl = useOpenClawConsoleStore((state) => state.openclawUrl);
|
||||||
const openclawToken = useOpenClawConsoleStore((state) => state.openclawToken);
|
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 assistantMode = useOpenClawConsoleStore((state) => state.assistantMode);
|
||||||
const thinking = useOpenClawConsoleStore((state) => state.thinking);
|
const thinking = useOpenClawConsoleStore((state) => state.thinking);
|
||||||
const selectedAgentId = useOpenClawConsoleStore(
|
const selectedAgentId = useOpenClawConsoleStore(
|
||||||
@ -313,6 +324,7 @@ export function OpenClawAssistantPane({
|
|||||||
),
|
),
|
||||||
envToken: pickCopy(isChinese, "环境变量令牌", "env token"),
|
envToken: pickCopy(isChinese, "环境变量令牌", "env token"),
|
||||||
sessionToken: pickCopy(isChinese, "会话令牌", "session token"),
|
sessionToken: pickCopy(isChinese, "会话令牌", "session token"),
|
||||||
|
vaultToken: pickCopy(isChinese, "Vault 令牌", "vault token"),
|
||||||
noToken: pickCopy(isChinese, "无令牌", "no token"),
|
noToken: pickCopy(isChinese, "无令牌", "no token"),
|
||||||
mainAgent: pickCopy(isChinese, "主助手", "Main agent"),
|
mainAgent: pickCopy(isChinese, "主助手", "Main agent"),
|
||||||
reconnect: pickCopy(isChinese, "重新连接", "Reconnect"),
|
reconnect: pickCopy(isChinese, "重新连接", "Reconnect"),
|
||||||
@ -425,6 +437,11 @@ export function OpenClawAssistantPane({
|
|||||||
action: "bootstrap",
|
action: "bootstrap",
|
||||||
gatewayUrl: openclawUrl,
|
gatewayUrl: openclawUrl,
|
||||||
gatewayToken: openclawToken,
|
gatewayToken: openclawToken,
|
||||||
|
vaultUrl,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultToken,
|
||||||
|
vaultSecretPath,
|
||||||
|
vaultSecretKey,
|
||||||
agentId: nextAgentId ?? selectedAgentId,
|
agentId: nextAgentId ?? selectedAgentId,
|
||||||
sessionKey: nextSessionKey ?? selectedSessionKey,
|
sessionKey: nextSessionKey ?? selectedSessionKey,
|
||||||
}),
|
}),
|
||||||
@ -463,6 +480,11 @@ export function OpenClawAssistantPane({
|
|||||||
copy.serverMissing,
|
copy.serverMissing,
|
||||||
openclawToken,
|
openclawToken,
|
||||||
openclawUrl,
|
openclawUrl,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultSecretKey,
|
||||||
|
vaultSecretPath,
|
||||||
|
vaultToken,
|
||||||
|
vaultUrl,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
selectedSessionKey,
|
selectedSessionKey,
|
||||||
setSelectedSessionKey,
|
setSelectedSessionKey,
|
||||||
@ -564,6 +586,11 @@ export function OpenClawAssistantPane({
|
|||||||
action: "send",
|
action: "send",
|
||||||
gatewayUrl: openclawUrl,
|
gatewayUrl: openclawUrl,
|
||||||
gatewayToken: openclawToken,
|
gatewayToken: openclawToken,
|
||||||
|
vaultUrl,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultToken,
|
||||||
|
vaultSecretPath,
|
||||||
|
vaultSecretKey,
|
||||||
agentId: effectiveAgentId,
|
agentId: effectiveAgentId,
|
||||||
sessionKey: effectiveSessionKey,
|
sessionKey: effectiveSessionKey,
|
||||||
message: prompt,
|
message: prompt,
|
||||||
@ -642,6 +669,11 @@ export function OpenClawAssistantPane({
|
|||||||
mainSessionKey,
|
mainSessionKey,
|
||||||
openclawToken,
|
openclawToken,
|
||||||
openclawUrl,
|
openclawUrl,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultSecretKey,
|
||||||
|
vaultSecretPath,
|
||||||
|
vaultToken,
|
||||||
|
vaultUrl,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
selectedSessionKey,
|
selectedSessionKey,
|
||||||
setSelectedAgentId,
|
setSelectedAgentId,
|
||||||
@ -731,6 +763,8 @@ export function OpenClawAssistantPane({
|
|||||||
<span className="text-[var(--color-text-subtle)]/60">·</span>
|
<span className="text-[var(--color-text-subtle)]/60">·</span>
|
||||||
{gatewayTokenSource === "env"
|
{gatewayTokenSource === "env"
|
||||||
? copy.envToken
|
? copy.envToken
|
||||||
|
: gatewayTokenSource === "vault"
|
||||||
|
? copy.vaultToken
|
||||||
: gatewayTokenSource === "request"
|
: gatewayTokenSource === "request"
|
||||||
? copy.sessionToken
|
? copy.sessionToken
|
||||||
: copy.noToken}
|
: copy.noToken}
|
||||||
|
|||||||
@ -429,6 +429,9 @@ export function XWorkmateWorkspacePage({
|
|||||||
(state) => state.vaultNamespace,
|
(state) => state.vaultNamespace,
|
||||||
);
|
);
|
||||||
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
||||||
|
const vaultSecretPath = useOpenClawConsoleStore(
|
||||||
|
(state) => state.vaultSecretPath,
|
||||||
|
);
|
||||||
const apisixUrl = useOpenClawConsoleStore((state) => state.apisixUrl);
|
const apisixUrl = useOpenClawConsoleStore((state) => state.apisixUrl);
|
||||||
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
||||||
const assistantMode = useOpenClawConsoleStore((state) => state.assistantMode);
|
const assistantMode = useOpenClawConsoleStore((state) => state.assistantMode);
|
||||||
@ -493,6 +496,33 @@ export function XWorkmateWorkspacePage({
|
|||||||
const configuredIntegrationCount = integrationRows.filter(
|
const configuredIntegrationCount = integrationRows.filter(
|
||||||
(item) => item.ok,
|
(item) => item.ok,
|
||||||
).length;
|
).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[]>(
|
const tasksOverview = useMemo<OverviewMetric[]>(
|
||||||
() => [
|
() => [
|
||||||
@ -520,10 +550,7 @@ export function XWorkmateWorkspacePage({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: pickCopy(isChinese, "失败", "Failed"),
|
label: pickCopy(isChinese, "失败", "Failed"),
|
||||||
value:
|
value: hasOpenClawCredential ? "0" : "pairing",
|
||||||
openclawToken.trim() || defaults.openclawTokenConfigured
|
|
||||||
? "0"
|
|
||||||
: "pairing",
|
|
||||||
caption: pickCopy(
|
caption: pickCopy(
|
||||||
isChinese,
|
isChinese,
|
||||||
"未配置 shared token 时优先走 pairing",
|
"未配置 shared token 时优先走 pairing",
|
||||||
@ -543,10 +570,9 @@ export function XWorkmateWorkspacePage({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
defaults.openclawTokenConfigured,
|
hasOpenClawCredential,
|
||||||
isChinese,
|
isChinese,
|
||||||
openclawEndpoint,
|
openclawEndpoint,
|
||||||
openclawToken,
|
|
||||||
selectedSessionKey,
|
selectedSessionKey,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -618,7 +644,7 @@ export function XWorkmateWorkspacePage({
|
|||||||
label: pickCopy(isChinese, "Token 引用", "Token Refs"),
|
label: pickCopy(isChinese, "Token 引用", "Token Refs"),
|
||||||
value: `${
|
value: `${
|
||||||
[
|
[
|
||||||
openclawToken.trim() || defaults.openclawTokenConfigured,
|
hasOpenClawCredential,
|
||||||
vaultToken.trim() || defaults.vaultTokenConfigured,
|
vaultToken.trim() || defaults.vaultTokenConfigured,
|
||||||
apisixToken.trim() || defaults.apisixTokenConfigured,
|
apisixToken.trim() || defaults.apisixTokenConfigured,
|
||||||
].filter(Boolean).length
|
].filter(Boolean).length
|
||||||
@ -655,10 +681,9 @@ export function XWorkmateWorkspacePage({
|
|||||||
apisixToken,
|
apisixToken,
|
||||||
configuredIntegrationCount,
|
configuredIntegrationCount,
|
||||||
defaults.apisixTokenConfigured,
|
defaults.apisixTokenConfigured,
|
||||||
defaults.openclawTokenConfigured,
|
hasOpenClawCredential,
|
||||||
defaults.vaultTokenConfigured,
|
defaults.vaultTokenConfigured,
|
||||||
isChinese,
|
isChinese,
|
||||||
openclawToken,
|
|
||||||
vaultEndpoint,
|
vaultEndpoint,
|
||||||
vaultToken,
|
vaultToken,
|
||||||
],
|
],
|
||||||
@ -1438,8 +1463,10 @@ export function XWorkmateWorkspacePage({
|
|||||||
)
|
)
|
||||||
: pickCopy(
|
: pickCopy(
|
||||||
isChinese,
|
isChinese,
|
||||||
"优先 env / pairing",
|
hasVaultBackedToken ? "优先 Vault / env" : "优先 env / pairing",
|
||||||
"env / pairing preferred",
|
hasVaultBackedToken
|
||||||
|
? "vault / env preferred"
|
||||||
|
: "env / pairing preferred",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ok={Boolean((openclawUrl || defaults.openclawUrl).trim())}
|
ok={Boolean((openclawUrl || defaults.openclawUrl).trim())}
|
||||||
@ -1642,13 +1669,13 @@ export function XWorkmateWorkspacePage({
|
|||||||
title={pickCopy(isChinese, "当前配置源", "Current config sources")}
|
title={pickCopy(isChinese, "当前配置源", "Current config sources")}
|
||||||
description={pickCopy(
|
description={pickCopy(
|
||||||
isChinese,
|
isChinese,
|
||||||
"区分 env 与 session。",
|
"区分 session / env / vault / pairing。",
|
||||||
"Separate env from session overrides.",
|
"Separate session, env, vault, and pairing sources.",
|
||||||
)}
|
)}
|
||||||
body={[
|
body={[
|
||||||
`OpenClaw: ${openclawToken.trim() ? "session" : defaults.openclawTokenConfigured ? "env" : "pairing only"}`,
|
`OpenClaw: ${openclawConfigSource}`,
|
||||||
`Vault: ${vaultToken.trim() ? "session" : defaults.vaultTokenConfigured ? "env" : "manual"}`,
|
`Vault: ${vaultConfigSource}`,
|
||||||
`APISIX: ${apisixToken.trim() ? "session" : defaults.apisixTokenConfigured ? "env" : "manual"}`,
|
`APISIX: ${apisixConfigSource}`,
|
||||||
].join(" · ")}
|
].join(" · ")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export type AssistantMode = 'ask' | 'craft' | 'plan'
|
|||||||
|
|
||||||
export type ThinkingLevel = 'low' | 'medium' | 'high' | 'max'
|
export type ThinkingLevel = 'low' | 'medium' | 'high' | 'max'
|
||||||
|
|
||||||
export type GatewayTokenSource = 'env' | 'request' | 'none'
|
export type GatewayTokenSource = 'env' | 'request' | 'vault' | 'none'
|
||||||
|
|
||||||
export type GatewayAgentSummary = {
|
export type GatewayAgentSummary = {
|
||||||
id: string
|
id: string
|
||||||
@ -83,6 +83,8 @@ export type IntegrationDefaults = {
|
|||||||
vaultUrl: string
|
vaultUrl: string
|
||||||
vaultNamespace: string
|
vaultNamespace: string
|
||||||
vaultTokenConfigured: boolean
|
vaultTokenConfigured: boolean
|
||||||
|
vaultSecretPath: string
|
||||||
|
vaultSecretKey: string
|
||||||
apisixUrl: string
|
apisixUrl: string
|
||||||
apisixTokenConfigured: boolean
|
apisixTokenConfigured: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,9 @@ type ProbeState = {
|
|||||||
tokenSource?: string;
|
tokenSource?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
code?: string;
|
||||||
|
details?: Record<string, unknown> | null;
|
||||||
|
deviceId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IntegrationsConsoleProps = {
|
type IntegrationsConsoleProps = {
|
||||||
@ -80,12 +83,18 @@ function inputClassName(type: "input" | "textarea" = "input"): string {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringValue(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const EMPTY_DEFAULTS: IntegrationDefaults = {
|
const EMPTY_DEFAULTS: IntegrationDefaults = {
|
||||||
openclawUrl: "",
|
openclawUrl: "",
|
||||||
openclawTokenConfigured: false,
|
openclawTokenConfigured: false,
|
||||||
vaultUrl: "",
|
vaultUrl: "",
|
||||||
vaultNamespace: "",
|
vaultNamespace: "",
|
||||||
vaultTokenConfigured: false,
|
vaultTokenConfigured: false,
|
||||||
|
vaultSecretPath: "",
|
||||||
|
vaultSecretKey: "",
|
||||||
apisixUrl: "",
|
apisixUrl: "",
|
||||||
apisixTokenConfigured: false,
|
apisixTokenConfigured: false,
|
||||||
};
|
};
|
||||||
@ -115,6 +124,12 @@ export function IntegrationsConsole({
|
|||||||
(state) => state.vaultNamespace,
|
(state) => state.vaultNamespace,
|
||||||
);
|
);
|
||||||
const vaultToken = useOpenClawConsoleStore((state) => state.vaultToken);
|
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 apisixUrl = useOpenClawConsoleStore((state) => state.apisixUrl);
|
||||||
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
const apisixToken = useOpenClawConsoleStore((state) => state.apisixToken);
|
||||||
const setOpenclawUrl = useOpenClawConsoleStore(
|
const setOpenclawUrl = useOpenClawConsoleStore(
|
||||||
@ -128,6 +143,12 @@ export function IntegrationsConsole({
|
|||||||
(state) => state.setVaultNamespace,
|
(state) => state.setVaultNamespace,
|
||||||
);
|
);
|
||||||
const setVaultToken = useOpenClawConsoleStore((state) => state.setVaultToken);
|
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 setApisixUrl = useOpenClawConsoleStore((state) => state.setApisixUrl);
|
||||||
const setApisixToken = useOpenClawConsoleStore(
|
const setApisixToken = useOpenClawConsoleStore(
|
||||||
(state) => state.setApisixToken,
|
(state) => state.setApisixToken,
|
||||||
@ -175,6 +196,7 @@ export function IntegrationsConsole({
|
|||||||
configured: Boolean(openclawUrl.trim()),
|
configured: Boolean(openclawUrl.trim()),
|
||||||
tokenConfigured:
|
tokenConfigured:
|
||||||
resolvedDefaults.openclawTokenConfigured ||
|
resolvedDefaults.openclawTokenConfigured ||
|
||||||
|
Boolean(vaultSecretPath.trim() && (vaultToken.trim() || resolvedDefaults.vaultTokenConfigured)) ||
|
||||||
Boolean(openclawToken.trim()),
|
Boolean(openclawToken.trim()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -201,6 +223,7 @@ export function IntegrationsConsole({
|
|||||||
resolvedDefaults.openclawTokenConfigured,
|
resolvedDefaults.openclawTokenConfigured,
|
||||||
resolvedDefaults.vaultTokenConfigured,
|
resolvedDefaults.vaultTokenConfigured,
|
||||||
vaultToken,
|
vaultToken,
|
||||||
|
vaultSecretPath,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -219,7 +242,10 @@ export function IntegrationsConsole({
|
|||||||
gatewayUrl: openclawUrl,
|
gatewayUrl: openclawUrl,
|
||||||
gatewayToken: openclawToken,
|
gatewayToken: openclawToken,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
|
vaultNamespace,
|
||||||
vaultToken,
|
vaultToken,
|
||||||
|
vaultSecretPath,
|
||||||
|
vaultSecretKey,
|
||||||
apisixUrl,
|
apisixUrl,
|
||||||
apisixToken,
|
apisixToken,
|
||||||
}),
|
}),
|
||||||
@ -230,10 +256,13 @@ export function IntegrationsConsole({
|
|||||||
...current,
|
...current,
|
||||||
[target]: {
|
[target]: {
|
||||||
ok: Boolean(payload.ok),
|
ok: Boolean(payload.ok),
|
||||||
status: payload.status,
|
status: payload.status ?? response.status,
|
||||||
tokenSource: payload.tokenSource,
|
tokenSource: payload.tokenSource,
|
||||||
body: payload.body,
|
body: payload.body,
|
||||||
error: payload.error,
|
error: payload.error,
|
||||||
|
code: payload.code,
|
||||||
|
details: payload.details ?? null,
|
||||||
|
deviceId: payload.deviceId,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -242,6 +271,7 @@ export function IntegrationsConsole({
|
|||||||
[target]: {
|
[target]: {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: error instanceof Error ? error.message : "Probe failed.",
|
error: error instanceof Error ? error.message : "Probe failed.",
|
||||||
|
details: null,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
@ -376,17 +406,86 @@ export function IntegrationsConsole({
|
|||||||
<span className="text-xs text-[var(--color-text-subtle)]">
|
<span className="text-xs text-[var(--color-text-subtle)]">
|
||||||
token source:{" "}
|
token source:{" "}
|
||||||
{probeResults.openclaw.tokenSource ||
|
{probeResults.openclaw.tokenSource ||
|
||||||
(resolvedDefaults.openclawTokenConfigured ? "env" : "session")}
|
(openclawToken.trim()
|
||||||
|
? "request"
|
||||||
|
: resolvedDefaults.openclawTokenConfigured
|
||||||
|
? "env"
|
||||||
|
: vaultSecretPath.trim() &&
|
||||||
|
(vaultToken.trim() || resolvedDefaults.vaultTokenConfigured)
|
||||||
|
? "vault"
|
||||||
|
: "none")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{probeResults.openclaw.error ? (
|
{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)]">
|
<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)]">
|
||||||
{probeResults.openclaw.error}
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Card>
|
</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">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
<Card className="space-y-5">
|
<Card className="space-y-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@ -87,40 +87,182 @@ export function getConsoleIntegrationDefaults(): IntegrationDefaults {
|
|||||||
vaultUrl: normalizeHttpUrl(readEnvValue(...VAULT_URL_KEYS)),
|
vaultUrl: normalizeHttpUrl(readEnvValue(...VAULT_URL_KEYS)),
|
||||||
vaultNamespace: readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
vaultNamespace: readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
||||||
vaultTokenConfigured: Boolean(readEnvValue(...VAULT_TOKEN_KEYS)),
|
vaultTokenConfigured: Boolean(readEnvValue(...VAULT_TOKEN_KEYS)),
|
||||||
|
vaultSecretPath: '',
|
||||||
|
vaultSecretKey: '',
|
||||||
apisixUrl: normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS)),
|
apisixUrl: normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS)),
|
||||||
apisixTokenConfigured: Boolean(readEnvValue(...APISIX_TOKEN_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
|
gatewayUrl?: string
|
||||||
gatewayToken?: 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 requestUrl = normalizeWsUrl(overrides?.gatewayUrl)
|
||||||
const envUrl = normalizeWsUrl(readEnvValue(...OPENCLAW_URL_KEYS))
|
const envUrl = normalizeWsUrl(readEnvValue(...OPENCLAW_URL_KEYS))
|
||||||
const requestToken = overrides?.gatewayToken?.trim() ?? ''
|
const resolvedToken = await resolveVaultBackedToken({
|
||||||
const envToken = readEnvValue(...OPENCLAW_TOKEN_KEYS) ?? ''
|
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 {
|
return {
|
||||||
gatewayUrl: requestUrl || envUrl,
|
gatewayUrl: requestUrl || envUrl,
|
||||||
gatewayToken: requestToken || envToken,
|
gatewayToken: resolvedToken.token,
|
||||||
tokenSource: requestToken ? 'request' : envToken ? 'env' : 'none',
|
tokenSource: resolvedToken.tokenSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveApisixProbeConfig(overrides?: {
|
export async function resolveApisixProbeConfig(overrides?: {
|
||||||
apisixUrl?: string
|
apisixUrl?: string
|
||||||
apisixToken?: 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 requestUrl = normalizeHttpUrl(overrides?.apisixUrl)
|
||||||
const envUrl = normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS))
|
const envUrl = normalizeHttpUrl(readEnvValue(...APISIX_URL_KEYS))
|
||||||
const requestToken = overrides?.apisixToken?.trim() ?? ''
|
const resolvedToken = await resolveVaultBackedToken({
|
||||||
const envToken = readEnvValue(...APISIX_TOKEN_KEYS) ?? ''
|
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 {
|
return {
|
||||||
apisixUrl: requestUrl || envUrl,
|
apisixUrl: requestUrl || envUrl,
|
||||||
apisixToken: requestToken || envToken,
|
apisixToken: resolvedToken.token,
|
||||||
tokenSource: requestToken ? 'request' : envToken ? 'env' : 'none',
|
tokenSource: resolvedToken.tokenSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ type StoredDeviceIdentity = {
|
|||||||
const OPENCLAW_STATE_DIR = path.join(process.cwd(), '.console-state', 'openclaw')
|
const OPENCLAW_STATE_DIR = path.join(process.cwd(), '.console-state', 'openclaw')
|
||||||
const DEVICE_IDENTITY_FILE = path.join(OPENCLAW_STATE_DIR, 'gateway-device-identity.json')
|
const DEVICE_IDENTITY_FILE = path.join(OPENCLAW_STATE_DIR, 'gateway-device-identity.json')
|
||||||
let deviceIdentityPromise: Promise<StoredDeviceIdentity> | null = null
|
let deviceIdentityPromise: Promise<StoredDeviceIdentity> | null = null
|
||||||
|
let memoryDeviceIdentity: StoredDeviceIdentity | null = null
|
||||||
|
const memoryDeviceTokens = new Map<string, string>()
|
||||||
|
|
||||||
function asStoredDeviceIdentity(value: unknown): StoredDeviceIdentity | null {
|
function asStoredDeviceIdentity(value: unknown): StoredDeviceIdentity | null {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
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> {
|
export async function loadOrCreateOpenClawDeviceIdentity(): Promise<StoredDeviceIdentity> {
|
||||||
|
if (memoryDeviceIdentity) {
|
||||||
|
return memoryDeviceIdentity
|
||||||
|
}
|
||||||
|
|
||||||
if (!deviceIdentityPromise) {
|
if (!deviceIdentityPromise) {
|
||||||
deviceIdentityPromise = (async () => {
|
deviceIdentityPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(DEVICE_IDENTITY_FILE, 'utf8')
|
const raw = await readFile(DEVICE_IDENTITY_FILE, 'utf8')
|
||||||
const parsed = asStoredDeviceIdentity(JSON.parse(raw))
|
const parsed = asStoredDeviceIdentity(JSON.parse(raw))
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
|
memoryDeviceIdentity = parsed
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -94,7 +101,15 @@ export async function loadOrCreateOpenClawDeviceIdentity(): Promise<StoredDevice
|
|||||||
createdAtMs: Date.now(),
|
createdAtMs: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeIdentity(identity)
|
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
|
return identity
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@ -111,6 +126,11 @@ export async function loadOpenClawDeviceToken(params: {
|
|||||||
deviceId: string
|
deviceId: string
|
||||||
role?: string
|
role?: string
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
|
const memoryToken = memoryDeviceTokens.get(`${params.deviceId}:${params.role?.trim() || 'operator'}`)
|
||||||
|
if (memoryToken) {
|
||||||
|
return memoryToken
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value = await readFile(deviceTokenFile(params.deviceId, params.role), 'utf8')
|
const value = await readFile(deviceTokenFile(params.deviceId, params.role), 'utf8')
|
||||||
return value.trim()
|
return value.trim()
|
||||||
@ -124,14 +144,23 @@ export async function saveOpenClawDeviceToken(params: {
|
|||||||
role?: string
|
role?: string
|
||||||
token: string
|
token: string
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await ensureStateDirectory()
|
const key = `${params.deviceId}:${params.role?.trim() || 'operator'}`
|
||||||
await writeFile(deviceTokenFile(params.deviceId, params.role), `${params.token.trim()}\n`, 'utf8')
|
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: {
|
export async function clearOpenClawDeviceToken(params: {
|
||||||
deviceId: string
|
deviceId: string
|
||||||
role?: string
|
role?: string
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
memoryDeviceTokens.delete(`${params.deviceId}:${params.role?.trim() || 'operator'}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await rm(deviceTokenFile(params.deviceId, params.role), { force: true })
|
await rm(deviceTokenFile(params.deviceId, params.role), { force: true })
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ type OpenClawConsoleState = {
|
|||||||
vaultUrl: string
|
vaultUrl: string
|
||||||
vaultNamespace: string
|
vaultNamespace: string
|
||||||
vaultToken: string
|
vaultToken: string
|
||||||
|
vaultSecretPath: string
|
||||||
|
vaultSecretKey: string
|
||||||
apisixUrl: string
|
apisixUrl: string
|
||||||
apisixToken: string
|
apisixToken: string
|
||||||
assistantMode: AssistantMode
|
assistantMode: AssistantMode
|
||||||
@ -24,6 +26,8 @@ type OpenClawConsoleState = {
|
|||||||
setVaultUrl: (value: string) => void
|
setVaultUrl: (value: string) => void
|
||||||
setVaultNamespace: (value: string) => void
|
setVaultNamespace: (value: string) => void
|
||||||
setVaultToken: (value: string) => void
|
setVaultToken: (value: string) => void
|
||||||
|
setVaultSecretPath: (value: string) => void
|
||||||
|
setVaultSecretKey: (value: string) => void
|
||||||
setApisixUrl: (value: string) => void
|
setApisixUrl: (value: string) => void
|
||||||
setApisixToken: (value: string) => void
|
setApisixToken: (value: string) => void
|
||||||
setAssistantMode: (value: AssistantMode) => void
|
setAssistantMode: (value: AssistantMode) => void
|
||||||
@ -41,6 +45,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
|||||||
vaultUrl: '',
|
vaultUrl: '',
|
||||||
vaultNamespace: '',
|
vaultNamespace: '',
|
||||||
vaultToken: '',
|
vaultToken: '',
|
||||||
|
vaultSecretPath: '',
|
||||||
|
vaultSecretKey: '',
|
||||||
apisixUrl: '',
|
apisixUrl: '',
|
||||||
apisixToken: '',
|
apisixToken: '',
|
||||||
assistantMode: 'ask',
|
assistantMode: 'ask',
|
||||||
@ -54,6 +60,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
|||||||
openclawUrl: current.openclawUrl || defaults.openclawUrl,
|
openclawUrl: current.openclawUrl || defaults.openclawUrl,
|
||||||
vaultUrl: current.vaultUrl || defaults.vaultUrl,
|
vaultUrl: current.vaultUrl || defaults.vaultUrl,
|
||||||
vaultNamespace: current.vaultNamespace || defaults.vaultNamespace,
|
vaultNamespace: current.vaultNamespace || defaults.vaultNamespace,
|
||||||
|
vaultSecretPath: current.vaultSecretPath || defaults.vaultSecretPath,
|
||||||
|
vaultSecretKey: current.vaultSecretKey || defaults.vaultSecretKey,
|
||||||
apisixUrl: current.apisixUrl || defaults.apisixUrl,
|
apisixUrl: current.apisixUrl || defaults.apisixUrl,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -62,6 +70,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
|||||||
setVaultUrl: (vaultUrl) => set({ vaultUrl }),
|
setVaultUrl: (vaultUrl) => set({ vaultUrl }),
|
||||||
setVaultNamespace: (vaultNamespace) => set({ vaultNamespace }),
|
setVaultNamespace: (vaultNamespace) => set({ vaultNamespace }),
|
||||||
setVaultToken: (vaultToken) => set({ vaultToken }),
|
setVaultToken: (vaultToken) => set({ vaultToken }),
|
||||||
|
setVaultSecretPath: (vaultSecretPath) => set({ vaultSecretPath }),
|
||||||
|
setVaultSecretKey: (vaultSecretKey) => set({ vaultSecretKey }),
|
||||||
setApisixUrl: (apisixUrl) => set({ apisixUrl }),
|
setApisixUrl: (apisixUrl) => set({ apisixUrl }),
|
||||||
setApisixToken: (apisixToken) => set({ apisixToken }),
|
setApisixToken: (apisixToken) => set({ apisixToken }),
|
||||||
setAssistantMode: (assistantMode) => set({ assistantMode }),
|
setAssistantMode: (assistantMode) => set({ assistantMode }),
|
||||||
@ -78,6 +88,8 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
|
|||||||
vaultUrl: state.vaultUrl,
|
vaultUrl: state.vaultUrl,
|
||||||
vaultNamespace: state.vaultNamespace,
|
vaultNamespace: state.vaultNamespace,
|
||||||
vaultToken: state.vaultToken,
|
vaultToken: state.vaultToken,
|
||||||
|
vaultSecretPath: state.vaultSecretPath,
|
||||||
|
vaultSecretKey: state.vaultSecretKey,
|
||||||
apisixUrl: state.apisixUrl,
|
apisixUrl: state.apisixUrl,
|
||||||
apisixToken: state.apisixToken,
|
apisixToken: state.apisixToken,
|
||||||
assistantMode: state.assistantMode,
|
assistantMode: state.assistantMode,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user