重构 Grok SSO 上传到 webchat2api

This commit is contained in:
QLHazyCoder
2026-05-23 20:41:22 +08:00
parent f856910e5b
commit c649d43ee6
20 changed files with 957 additions and 97 deletions
@@ -0,0 +1,311 @@
(function attachBackgroundGrokPublisherWebchat2Api(root, factory) {
root.MultiPageBackgroundGrokPublisherWebchat2Api = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokPublisherWebchat2ApiModule(root) {
const grokStateApi = root?.MultiPageBackgroundGrokState || null;
const WEBCHAT2API_INJECT_PATH = '/api/remote-account/inject';
const DEFAULT_SOURCE_ID = 'flowpilot-grok-sso';
const DEFAULT_SOURCE_NAME = 'FlowPilot Grok SSO';
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map((entry) => cloneValue(entry));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
);
}
return value;
}
function deepMerge(baseValue, patchValue) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = deepMerge(baseObject[key], value);
});
return next;
}
function cleanString(value = '') {
return String(value ?? '').trim();
}
function getErrorMessage(error) {
return error instanceof Error ? error.message : cleanString(error) || '未知错误';
}
async function readResponse(response) {
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_error) {
json = null;
}
return { text, json };
}
function readWebchat2ApiResponseMessage(body = {}, fallback = '') {
return cleanString(
body?.json?.error?.message
|| body?.json?.error
|| body?.json?.message
|| fallback
);
}
function normalizeWebchat2ApiBaseUrl(value = '') {
const rawUrl = cleanString(value);
if (!rawUrl) {
throw new Error('缺少 webchat2api 地址。');
}
const withProtocol = /^https?:\/\//i.test(rawUrl) ? rawUrl : `http://${rawUrl}`;
let parsed = null;
try {
parsed = new URL(withProtocol);
} catch (_error) {
throw new Error('webchat2api 地址格式无效,请检查配置。');
}
if (!/^https?:$/.test(parsed.protocol)) {
throw new Error('webchat2api 地址只支持 http 或 https。');
}
return parsed.origin;
}
function buildWebchat2ApiInjectUrl(value = '') {
return `${normalizeWebchat2ApiBaseUrl(value)}${WEBCHAT2API_INJECT_PATH}`;
}
function normalizeWebchat2ApiAdminKey(value = '') {
return cleanString(value);
}
function readGrokRuntime(state = {}) {
return grokStateApi?.ensureRuntimeState
? grokStateApi.ensureRuntimeState(state)
: (isPlainObject(state?.runtimeState?.flowState?.grok)
? state.runtimeState.flowState.grok
: (isPlainObject(state?.flowState?.grok) ? state.flowState.grok : {}));
}
function buildCanonicalRuntimePatch(currentState = {}, nextRuntimeState = {}) {
if (typeof grokStateApi?.buildRuntimeStatePatch === 'function') {
return grokStateApi.buildRuntimeStatePatch(currentState, nextRuntimeState);
}
const baseRuntimeState = isPlainObject(currentState?.runtimeState)
? cloneValue(currentState.runtimeState)
: {};
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
? cloneValue(baseRuntimeState.flowState)
: {};
return {
runtimeState: {
...baseRuntimeState,
flowState: {
...baseFlowState,
grok: deepMerge(readGrokRuntime(currentState), nextRuntimeState),
},
},
};
}
function mergeRuntimePatch(currentState = {}, patch = {}) {
return buildCanonicalRuntimePatch(
currentState,
deepMerge(readGrokRuntime(currentState), patch)
);
}
function resolveGrokWebchat2ApiConfig(state = {}) {
const nestedConfig = state?.settingsState?.flows?.grok?.targets?.webchat2api || {};
return {
baseUrl: cleanString(nestedConfig.baseUrl || state?.grokWebchat2ApiUrl),
apiKey: normalizeWebchat2ApiAdminKey(nestedConfig.apiKey ?? state?.grokWebchat2ApiAdminKey ?? ''),
};
}
function resolveGrokSsoCookie(state = {}) {
const runtimeState = readGrokRuntime(state);
return cleanString(runtimeState?.sso?.currentCookie || state?.grokSsoCookie);
}
function buildGrokSsoInjectPayload(ssoCookie = '') {
const normalizedCookie = cleanString(ssoCookie);
if (!normalizedCookie) {
throw new Error('缺少 Grok SSO Cookie,请先完成步骤 5。');
}
return {
accounts: [{
token: normalizedCookie,
provider: 'grok',
type: 'sso',
}],
strategy: 'merge',
source_id: DEFAULT_SOURCE_ID,
source_name: DEFAULT_SOURCE_NAME,
provider: 'grok',
};
}
async function uploadGrokSsoToWebchat2Api(baseUrl, apiKey, ssoCookie, fetchImpl) {
const endpointUrl = buildWebchat2ApiInjectUrl(baseUrl);
const normalizedApiKey = normalizeWebchat2ApiAdminKey(apiKey);
if (!normalizedApiKey) {
throw new Error('缺少 webchat2api 管理密钥。');
}
const response = await fetchImpl(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${normalizedApiKey}`,
},
body: JSON.stringify(buildGrokSsoInjectPayload(ssoCookie)),
});
const body = await readResponse(response);
if (!response.ok) {
const message = readWebchat2ApiResponseMessage(body, response.statusText) || `HTTP ${response.status}`;
throw new Error(`webchat2api SSO 上传失败:${message}`);
}
if (isPlainObject(body.json) && Object.prototype.hasOwnProperty.call(body.json, 'code') && Number(body.json.code) !== 0) {
const message = readWebchat2ApiResponseMessage(body, `code=${body.json.code}`);
throw new Error(`webchat2api SSO 上传失败:${message}`);
}
return {
endpointUrl,
message: readWebchat2ApiResponseMessage(body, '') || '上传成功',
raw: body.json,
};
}
function createGrokWebchat2ApiPublisher(deps = {}) {
const {
addLog = async () => {},
completeNodeFromBackground,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getState = async () => ({}),
setState = async () => {},
} = deps;
if (typeof completeNodeFromBackground !== 'function') {
throw new Error('Grok webchat2api publisher requires completeNodeFromBackground.');
}
if (typeof fetchImpl !== 'function') {
throw new Error('Grok webchat2api publisher requires fetch support.');
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function applyRuntimeState(currentState = {}, patch = {}) {
const nextPatch = mergeRuntimePatch(currentState, patch);
await setState(nextPatch);
return nextPatch;
}
async function persistFailure(currentState = {}, message = '', targetUrl = '') {
const uploadPatch = {
status: 'error',
uploadedAt: 0,
message,
};
const normalizedTargetUrl = cleanString(targetUrl);
if (normalizedTargetUrl) {
uploadPatch.targetUrl = normalizedTargetUrl;
}
const nextPatch = mergeRuntimePatch(currentState, {
session: {
lastError: message,
},
upload: uploadPatch,
});
await setState(nextPatch);
}
async function executeGrokUploadSsoToWebchat2Api(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-upload-sso-to-webchat2api';
const currentState = await getState();
let failureTargetUrl = '';
try {
const targetConfig = resolveGrokWebchat2ApiConfig(currentState);
const endpointUrl = buildWebchat2ApiInjectUrl(targetConfig.baseUrl);
failureTargetUrl = endpointUrl;
const apiKey = normalizeWebchat2ApiAdminKey(targetConfig.apiKey);
if (!apiKey) {
throw new Error('缺少 webchat2api 管理密钥。');
}
const ssoCookie = resolveGrokSsoCookie(currentState);
if (!ssoCookie) {
throw new Error('缺少 Grok SSO Cookie,请先完成步骤 5。');
}
await applyRuntimeState(currentState, {
session: {
lastError: '',
},
upload: {
status: 'uploading',
uploadedAt: 0,
message: '',
targetUrl: endpointUrl,
},
});
await log('步骤 6:正在上传 Grok SSO 到 webchat2api...', 'info', nodeId);
const uploadResult = await uploadGrokSsoToWebchat2Api(
targetConfig.baseUrl,
apiKey,
ssoCookie,
fetchImpl
);
const uploadedAt = Date.now();
const payload = await applyRuntimeState(currentState, {
session: {
lastError: '',
},
upload: {
status: 'uploaded',
uploadedAt,
message: uploadResult.message || '上传成功',
targetUrl: uploadResult.endpointUrl,
},
});
await log(`步骤 6Grok SSO 已上传到 webchat2api,状态:${uploadResult.message || '上传成功'}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
} catch (error) {
const message = getErrorMessage(error);
await persistFailure(currentState, message, failureTargetUrl);
await log(`步骤 6${message}`, 'error', nodeId);
throw error;
}
}
return {
executeGrokUploadSsoToWebchat2Api,
};
}
return {
buildGrokSsoInjectPayload,
buildWebchat2ApiInjectUrl,
createGrokWebchat2ApiPublisher,
normalizeWebchat2ApiBaseUrl,
uploadGrokSsoToWebchat2Api,
};
});
+12 -11
View File
@@ -613,22 +613,17 @@
throw new Error('未找到 x.ai/grok sso Cookie。');
}
const latestState = await getState();
const existingSsoCookies = Array.isArray(latestState?.grokSsoCookies)
? latestState.grokSsoCookies
.map((entry) => cleanString(entry))
.filter(Boolean)
: [];
const nextSsoCookies = existingSsoCookies.includes(ssoCookie)
? existingSsoCookies
: [...existingSsoCookies, ssoCookie];
const completedAt = Date.now();
const completionPatch = {
grokSsoCookie: ssoCookie,
grokSsoCookies: nextSsoCookies,
grokSsoCookies: [ssoCookie],
grokSsoExtractedAt: completedAt,
grokCompletedAt: completedAt,
grokRegisterStatus: 'completed',
grokWebchat2ApiUploadStatus: '',
grokWebchat2ApiUploadedAt: 0,
grokWebchat2ApiUploadMessage: '',
grokWebchat2ApiTargetUrl: '',
...buildGrokRuntimePatch({
register: {
status: 'completed',
@@ -636,9 +631,15 @@
},
sso: {
currentCookie: ssoCookie,
cookies: nextSsoCookies,
cookies: [ssoCookie],
extractedAt: completedAt,
},
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
session: {
lastError: '',
},
+71 -24
View File
@@ -39,6 +39,26 @@
return String(value ?? '').trim();
}
function assignCleanString(target = {}, key = '', value = '') {
const normalized = cleanString(value);
if (normalized) {
target[key] = normalized;
}
}
function assignPositiveInteger(target = {}, key = '', value) {
const numeric = Math.floor(Number(value));
if (Number.isInteger(numeric) && numeric > 0) {
target[key] = numeric;
}
}
function assignNonEmptyArray(target = {}, key = '', value) {
if (Array.isArray(value) && value.length) {
target[key] = value;
}
}
function normalizeInteger(value, fallback = 0) {
const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback;
@@ -87,6 +107,12 @@
cookies: [],
extractedAt: 0,
},
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
};
}
@@ -115,6 +141,12 @@
cookies: normalizeSsoCookies(merged.sso?.cookies || merged.ssoCookies),
extractedAt: Math.max(0, normalizeInteger(merged.sso?.extractedAt)),
},
upload: {
status: cleanString(merged.upload?.status),
uploadedAt: Math.max(0, normalizeInteger(merged.upload?.uploadedAt)),
message: cleanString(merged.upload?.message),
targetUrl: cleanString(merged.upload?.targetUrl),
},
};
}
@@ -152,6 +184,10 @@
grokSsoCookie: normalizedRuntimeState.sso.currentCookie,
grokSsoCookies: normalizedRuntimeState.sso.cookies,
grokSsoExtractedAt: normalizedRuntimeState.sso.extractedAt,
grokWebchat2ApiUploadStatus: normalizedRuntimeState.upload.status,
grokWebchat2ApiUploadedAt: normalizedRuntimeState.upload.uploadedAt,
grokWebchat2ApiUploadMessage: normalizedRuntimeState.upload.message,
grokWebchat2ApiTargetUrl: normalizedRuntimeState.upload.targetUrl,
};
}
@@ -163,27 +199,29 @@
? state.flowState.grok
: {};
const flatRuntime = {
session: {
registerTabId: state.grokRegisterTabId,
pageState: state.grokPageState,
pageUrl: state.grokPageUrl || state.grokPostVerificationUrl,
},
register: {
email: state.grokEmail || state.email,
firstName: state.grokFirstName,
lastName: state.grokLastName,
password: state.grokPassword,
verificationRequestedAt: state.grokVerificationRequestedAt,
verificationCode: state.grokVerificationCode,
status: state.grokRegisterStatus,
completedAt: state.grokCompletedAt,
},
sso: {
currentCookie: state.grokSsoCookie,
cookies: state.grokSsoCookies,
extractedAt: state.grokSsoExtractedAt || state.grokCompletedAt,
},
session: {},
register: {},
sso: {},
upload: {},
};
assignPositiveInteger(flatRuntime.session, 'registerTabId', state.grokRegisterTabId);
assignCleanString(flatRuntime.session, 'pageState', state.grokPageState);
assignCleanString(flatRuntime.session, 'pageUrl', state.grokPageUrl || state.grokPostVerificationUrl);
assignCleanString(flatRuntime.register, 'email', state.grokEmail || state.email);
assignCleanString(flatRuntime.register, 'firstName', state.grokFirstName);
assignCleanString(flatRuntime.register, 'lastName', state.grokLastName);
assignCleanString(flatRuntime.register, 'password', state.grokPassword);
assignPositiveInteger(flatRuntime.register, 'verificationRequestedAt', state.grokVerificationRequestedAt);
assignCleanString(flatRuntime.register, 'verificationCode', state.grokVerificationCode);
assignCleanString(flatRuntime.register, 'status', state.grokRegisterStatus);
assignPositiveInteger(flatRuntime.register, 'completedAt', state.grokCompletedAt);
assignCleanString(flatRuntime.sso, 'currentCookie', state.grokSsoCookie);
assignNonEmptyArray(flatRuntime.sso, 'cookies', state.grokSsoCookies);
assignPositiveInteger(flatRuntime.sso, 'extractedAt', state.grokSsoExtractedAt || state.grokCompletedAt);
assignCleanString(flatRuntime.upload, 'status', state.grokWebchat2ApiUploadStatus);
assignPositiveInteger(flatRuntime.upload, 'uploadedAt', state.grokWebchat2ApiUploadedAt);
assignCleanString(flatRuntime.upload, 'message', state.grokWebchat2ApiUploadMessage);
assignCleanString(flatRuntime.upload, 'targetUrl', state.grokWebchat2ApiTargetUrl);
return normalizeRuntimeState(deepMerge(deepMerge(runtimeFlowState.grok || {}, legacyFlowState), flatRuntime));
}
@@ -346,6 +384,18 @@
extractedAt: payload.grokSsoExtractedAt || payload.grokCompletedAt || 0,
};
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadStatus')) {
patch.upload = { ...(patch.upload || {}), status: payload.grokWebchat2ApiUploadStatus };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadedAt')) {
patch.upload = { ...(patch.upload || {}), uploadedAt: payload.grokWebchat2ApiUploadedAt };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadMessage')) {
patch.upload = { ...(patch.upload || {}), message: payload.grokWebchat2ApiUploadMessage };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiTargetUrl')) {
patch.upload = { ...(patch.upload || {}), targetUrl: payload.grokWebchat2ApiTargetUrl };
}
if (!Object.keys(patch).length) {
return {};
}
@@ -353,10 +403,7 @@
}
function buildFreshKeepState(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
const nextRuntimeState = buildDefaultRuntimeState();
nextRuntimeState.sso = currentRuntimeState.sso || buildDefaultRuntimeState().sso;
return buildRuntimeStatePatch(currentState, nextRuntimeState);
return buildRuntimeStatePatch(currentState, buildDefaultRuntimeState());
}
return {
+20 -1
View File
@@ -43,6 +43,10 @@
groups: [
'grok-target-webchat2api',
],
defaultState: {
baseUrl: '',
apiKey: '',
},
},
},
publicationTargets: {},
@@ -106,14 +110,26 @@
'grok-extract-sso-cookie',
],
},
'flows/grok/background/publisher-webchat2api': {
sourceId: 'grok-webchat2api',
commands: [
'grok-upload-sso-to-webchat2api',
],
},
},
defaultTargetId: 'webchat2api',
settingsDefaults: {
targets: {
webchat2api: {
baseUrl: '',
apiKey: '',
},
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 5,
toStep: 6,
},
},
},
@@ -122,6 +138,8 @@
id: 'grok-target-webchat2api',
label: 'webchat2api',
rowIds: [
'row-grok-webchat2api-url',
'row-grok-webchat2api-key',
'row-grok-sso-settings',
],
},
@@ -131,6 +149,7 @@
rowIds: [
'row-grok-register-status',
'row-grok-sso-status',
'row-grok-webchat2api-upload-status',
],
},
},
+10
View File
@@ -64,6 +64,16 @@
command: 'grok-extract-sso-cookie',
flowId: 'grok',
},
{
id: 6,
order: 60,
key: 'grok-upload-sso-to-webchat2api',
title: '上传 SSO 到 webchat2api',
sourceId: 'grok-webchat2api',
driverId: 'flows/grok/background/publisher-webchat2api',
command: 'grok-upload-sso-to-webchat2api',
flowId: 'grok',
},
],
});