重构 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
+28
View File
@@ -44,6 +44,7 @@ importScripts(
'flows/kiro/background/desktop-client.js', 'flows/kiro/background/desktop-client.js',
'flows/kiro/background/desktop-authorize-runner.js', 'flows/kiro/background/desktop-authorize-runner.js',
'flows/kiro/background/publisher-kiro-rs.js', 'flows/kiro/background/publisher-kiro-rs.js',
'flows/grok/background/publisher-webchat2api.js',
'background/generated-email-helpers.js', 'background/generated-email-helpers.js',
'background/signup-flow-helpers.js', 'background/signup-flow-helpers.js',
'background/mail-rule-registry.js', 'background/mail-rule-registry.js',
@@ -1264,6 +1265,8 @@ const PERSISTED_SETTING_DEFAULTS = {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID, activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
kiroRsUrl: String(self.MultiPageFlowRegistry?.DEFAULT_KIRO_RS_URL || '').trim(), kiroRsUrl: String(self.MultiPageFlowRegistry?.DEFAULT_KIRO_RS_URL || '').trim(),
kiroRsKey: '', kiroRsKey: '',
grokWebchat2ApiUrl: '',
grokWebchat2ApiAdminKey: '',
vpsUrl: '', vpsUrl: '',
vpsPassword: '', vpsPassword: '',
localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE, localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE,
@@ -1481,6 +1484,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'ipProxyMode', 'ipProxyMode',
'kiroRsUrl', 'kiroRsUrl',
'kiroRsKey', 'kiroRsKey',
'grokWebchat2ApiUrl',
'grokWebchat2ApiAdminKey',
'stepExecutionRangeByFlow', 'stepExecutionRangeByFlow',
]); ]);
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS); const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
@@ -3158,8 +3163,10 @@ function normalizePersistentSettingValue(key, value) {
} }
return String(value || '').trim().toLowerCase() === 'kiro' ? 'kiro' : DEFAULT_ACTIVE_FLOW_ID; return String(value || '').trim().toLowerCase() === 'kiro' ? 'kiro' : DEFAULT_ACTIVE_FLOW_ID;
case 'kiroRsUrl': case 'kiroRsUrl':
case 'grokWebchat2ApiUrl':
return String(value || '').trim(); return String(value || '').trim();
case 'kiroRsKey': case 'kiroRsKey':
case 'grokWebchat2ApiAdminKey':
return String(value || '').trim(); return String(value || '').trim();
case 'vpsUrl': case 'vpsUrl':
return String(value || '').trim(); return String(value || '').trim();
@@ -3835,6 +3842,8 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) {
assignIfUpdated('ipProxyMode', ['services', 'proxy', 'mode']); assignIfUpdated('ipProxyMode', ['services', 'proxy', 'mode']);
assignIfUpdated('kiroRsUrl', ['flows', 'kiro', 'targets', 'kiro-rs', 'baseUrl']); assignIfUpdated('kiroRsUrl', ['flows', 'kiro', 'targets', 'kiro-rs', 'baseUrl']);
assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']); assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']);
assignIfUpdated('grokWebchat2ApiUrl', ['flows', 'grok', 'targets', 'webchat2api', 'baseUrl']);
assignIfUpdated('grokWebchat2ApiAdminKey', ['flows', 'grok', 'targets', 'webchat2api', 'apiKey']);
if (hasUpdate('stepExecutionRangeByFlow') && isPlainObjectValue(updates.stepExecutionRangeByFlow)) { if (hasUpdate('stepExecutionRangeByFlow') && isPlainObjectValue(updates.stepExecutionRangeByFlow)) {
Object.entries(updates.stepExecutionRangeByFlow).forEach(([rawFlowId, range]) => { Object.entries(updates.stepExecutionRangeByFlow).forEach(([rawFlowId, range]) => {
@@ -4367,11 +4376,21 @@ async function clearGrokSsoCookies() {
cookies: [], cookies: [],
extractedAt: 0, extractedAt: 0,
}, },
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
}) })
: { : {
grokSsoCookie: '', grokSsoCookie: '',
grokSsoCookies: [], grokSsoCookies: [],
grokSsoExtractedAt: 0, grokSsoExtractedAt: 0,
grokWebchat2ApiUploadStatus: '',
grokWebchat2ApiUploadedAt: 0,
grokWebchat2ApiUploadMessage: '',
grokWebchat2ApiTargetUrl: '',
}; };
await setState(patch); await setState(patch);
const nextState = await getState(); const nextState = await getState();
@@ -10936,6 +10955,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'grok-submit-verification-code', 'grok-submit-verification-code',
'grok-submit-profile', 'grok-submit-profile',
'grok-extract-sso-cookie', 'grok-extract-sso-cookie',
'grok-upload-sso-to-webchat2api',
]); ]);
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
'fill-password', 'fill-password',
@@ -14016,6 +14036,13 @@ const kiroPublisher = self.MultiPageBackgroundKiroPublisherKiroRs?.createKiroRsP
maybeSubmitFlowContribution, maybeSubmitFlowContribution,
setState, setState,
}); });
const grokWebchat2ApiPublisher = self.MultiPageBackgroundGrokPublisherWebchat2Api?.createGrokWebchat2ApiPublisher({
addLog,
completeNodeFromBackground,
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getState,
setState,
});
const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
addLog, addLog,
chrome, chrome,
@@ -14115,6 +14142,7 @@ const stepExecutorsByKey = {
'grok-submit-verification-code': (state) => grokRegisterRunner.executeGrokSubmitVerificationCode(state), 'grok-submit-verification-code': (state) => grokRegisterRunner.executeGrokSubmitVerificationCode(state),
'grok-submit-profile': (state) => grokRegisterRunner.executeGrokSubmitProfile(state), 'grok-submit-profile': (state) => grokRegisterRunner.executeGrokSubmitProfile(state),
'grok-extract-sso-cookie': (state) => grokRegisterRunner.executeGrokExtractSsoCookie(state), 'grok-extract-sso-cookie': (state) => grokRegisterRunner.executeGrokExtractSsoCookie(state),
'grok-upload-sso-to-webchat2api': (state) => grokWebchat2ApiPublisher.executeGrokUploadSsoToWebchat2Api(state),
}; };
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
addLog, addLog,
+10
View File
@@ -295,6 +295,13 @@
apiKey: String(targetState.apiKey ?? ''), apiKey: String(targetState.apiKey ?? ''),
}; };
} }
if (flowId === 'grok' && targetId === 'webchat2api') {
return {
...targetState,
baseUrl: String(targetState.baseUrl ?? '').trim(),
apiKey: String(targetState.apiKey ?? ''),
};
}
return targetState; return targetState;
} }
@@ -601,6 +608,7 @@
}; };
const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai'); const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai');
const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro'); const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro');
const grokState = normalizedState.flows.grok || buildDefaultFlowSettings('grok');
next.activeFlowId = normalizedState.activeFlowId; next.activeFlowId = normalizedState.activeFlowId;
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId); next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
next.vpsUrl = openaiState.targets.cpa?.vpsUrl || ''; next.vpsUrl = openaiState.targets.cpa?.vpsUrl || '';
@@ -631,6 +639,8 @@
next.ipProxyMode = normalizedState.services.proxy.mode; next.ipProxyMode = normalizedState.services.proxy.mode;
next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || ''; next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || '';
next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || ''; next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || '';
next.grokWebchat2ApiUrl = grokState.targets.webchat2api?.baseUrl || '';
next.grokWebchat2ApiAdminKey = grokState.targets.webchat2api?.apiKey || '';
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState); next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
next.settingsSchemaVersion = normalizedState.schemaVersion; next.settingsSchemaVersion = normalizedState.schemaVersion;
next.settingsState = cloneValue(normalizedState); next.settingsState = cloneValue(normalizedState);
@@ -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。'); 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 completedAt = Date.now();
const completionPatch = { const completionPatch = {
grokSsoCookie: ssoCookie, grokSsoCookie: ssoCookie,
grokSsoCookies: nextSsoCookies, grokSsoCookies: [ssoCookie],
grokSsoExtractedAt: completedAt, grokSsoExtractedAt: completedAt,
grokCompletedAt: completedAt, grokCompletedAt: completedAt,
grokRegisterStatus: 'completed', grokRegisterStatus: 'completed',
grokWebchat2ApiUploadStatus: '',
grokWebchat2ApiUploadedAt: 0,
grokWebchat2ApiUploadMessage: '',
grokWebchat2ApiTargetUrl: '',
...buildGrokRuntimePatch({ ...buildGrokRuntimePatch({
register: { register: {
status: 'completed', status: 'completed',
@@ -636,9 +631,15 @@
}, },
sso: { sso: {
currentCookie: ssoCookie, currentCookie: ssoCookie,
cookies: nextSsoCookies, cookies: [ssoCookie],
extractedAt: completedAt, extractedAt: completedAt,
}, },
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
session: { session: {
lastError: '', lastError: '',
}, },
+71 -24
View File
@@ -39,6 +39,26 @@
return String(value ?? '').trim(); 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) { function normalizeInteger(value, fallback = 0) {
const numeric = Math.floor(Number(value)); const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback; return Number.isInteger(numeric) ? numeric : fallback;
@@ -87,6 +107,12 @@
cookies: [], cookies: [],
extractedAt: 0, extractedAt: 0,
}, },
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
}; };
} }
@@ -115,6 +141,12 @@
cookies: normalizeSsoCookies(merged.sso?.cookies || merged.ssoCookies), cookies: normalizeSsoCookies(merged.sso?.cookies || merged.ssoCookies),
extractedAt: Math.max(0, normalizeInteger(merged.sso?.extractedAt)), 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, grokSsoCookie: normalizedRuntimeState.sso.currentCookie,
grokSsoCookies: normalizedRuntimeState.sso.cookies, grokSsoCookies: normalizedRuntimeState.sso.cookies,
grokSsoExtractedAt: normalizedRuntimeState.sso.extractedAt, 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 ? state.flowState.grok
: {}; : {};
const flatRuntime = { const flatRuntime = {
session: { session: {},
registerTabId: state.grokRegisterTabId, register: {},
pageState: state.grokPageState, sso: {},
pageUrl: state.grokPageUrl || state.grokPostVerificationUrl, upload: {},
},
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,
},
}; };
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)); return normalizeRuntimeState(deepMerge(deepMerge(runtimeFlowState.grok || {}, legacyFlowState), flatRuntime));
} }
@@ -346,6 +384,18 @@
extractedAt: payload.grokSsoExtractedAt || payload.grokCompletedAt || 0, 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) { if (!Object.keys(patch).length) {
return {}; return {};
} }
@@ -353,10 +403,7 @@
} }
function buildFreshKeepState(currentState = {}) { function buildFreshKeepState(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState); return buildRuntimeStatePatch(currentState, buildDefaultRuntimeState());
const nextRuntimeState = buildDefaultRuntimeState();
nextRuntimeState.sso = currentRuntimeState.sso || buildDefaultRuntimeState().sso;
return buildRuntimeStatePatch(currentState, nextRuntimeState);
} }
return { return {
+20 -1
View File
@@ -43,6 +43,10 @@
groups: [ groups: [
'grok-target-webchat2api', 'grok-target-webchat2api',
], ],
defaultState: {
baseUrl: '',
apiKey: '',
},
}, },
}, },
publicationTargets: {}, publicationTargets: {},
@@ -106,14 +110,26 @@
'grok-extract-sso-cookie', 'grok-extract-sso-cookie',
], ],
}, },
'flows/grok/background/publisher-webchat2api': {
sourceId: 'grok-webchat2api',
commands: [
'grok-upload-sso-to-webchat2api',
],
},
}, },
defaultTargetId: 'webchat2api', defaultTargetId: 'webchat2api',
settingsDefaults: { settingsDefaults: {
targets: {
webchat2api: {
baseUrl: '',
apiKey: '',
},
},
autoRun: { autoRun: {
stepExecutionRange: { stepExecutionRange: {
enabled: false, enabled: false,
fromStep: 1, fromStep: 1,
toStep: 5, toStep: 6,
}, },
}, },
}, },
@@ -122,6 +138,8 @@
id: 'grok-target-webchat2api', id: 'grok-target-webchat2api',
label: 'webchat2api', label: 'webchat2api',
rowIds: [ rowIds: [
'row-grok-webchat2api-url',
'row-grok-webchat2api-key',
'row-grok-sso-settings', 'row-grok-sso-settings',
], ],
}, },
@@ -131,6 +149,7 @@
rowIds: [ rowIds: [
'row-grok-register-status', 'row-grok-register-status',
'row-grok-sso-status', 'row-grok-sso-status',
'row-grok-webchat2api-upload-status',
], ],
}, },
}, },
+10
View File
@@ -64,6 +64,16 @@
command: 'grok-extract-sso-cookie', command: 'grok-extract-sso-cookie',
flowId: 'grok', 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',
},
], ],
}); });
+20 -1
View File
@@ -299,12 +299,31 @@
<span class="data-label">SSO 状态</span> <span class="data-label">SSO 状态</span>
<span id="display-grok-sso-status" class="data-value mono">未提取</span> <span id="display-grok-sso-status" class="data-value mono">未提取</span>
</div> </div>
<div class="data-row" id="row-grok-webchat2api-upload-status" style="display:none;">
<span class="data-label">上传状态</span>
<span id="display-grok-webchat2api-upload-status" class="data-value mono">未开始</span>
</div>
<div class="data-row" id="row-grok-webchat2api-url" style="display:none;">
<span class="data-label">webchat2api</span>
<input type="text" id="input-grok-webchat2api-url" class="data-input"
placeholder="请输入 webchat2api 地址,例如 http://localhost:83" />
</div>
<div class="data-row" id="row-grok-webchat2api-key" style="display:none;">
<span class="data-label">管理密钥</span>
<div class="input-with-icon">
<input type="password" id="input-grok-webchat2api-key" class="data-input data-input-with-icon"
placeholder="请输入 webchat2api 管理密钥" />
<button id="btn-toggle-grok-webchat2api-key" class="input-icon-btn" type="button"
data-password-toggle="input-grok-webchat2api-key" data-show-label="显示 webchat2api 管理密钥"
data-hide-label="隐藏 webchat2api 管理密钥" aria-label="显示 webchat2api 管理密钥"
title="显示 webchat2api 管理密钥"></button>
</div>
</div>
<div class="data-row" id="row-grok-sso-settings" style="display:none;"> <div class="data-row" id="row-grok-sso-settings" style="display:none;">
<span class="data-label">SSO Cookie</span> <span class="data-label">SSO Cookie</span>
<div class="data-inline"> <div class="data-inline">
<span id="display-grok-sso-cookie" class="data-value mono">未提取</span> <span id="display-grok-sso-cookie" class="data-value mono">未提取</span>
<button id="btn-copy-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">复制</button> <button id="btn-copy-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">复制</button>
<button id="btn-export-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">导出</button>
<button id="btn-clear-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button> <button id="btn-clear-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
</div> </div>
</div> </div>
+78 -20
View File
@@ -186,6 +186,10 @@ const inputKiroRsKey = document.getElementById('input-kiro-rs-key');
const btnTestKiroRs = document.getElementById('btn-test-kiro-rs'); const btnTestKiroRs = document.getElementById('btn-test-kiro-rs');
const rowKiroRsTestStatus = document.getElementById('row-kiro-rs-test-status'); const rowKiroRsTestStatus = document.getElementById('row-kiro-rs-test-status');
const displayKiroRsTestStatus = document.getElementById('display-kiro-rs-test-status'); const displayKiroRsTestStatus = document.getElementById('display-kiro-rs-test-status');
const rowGrokWebchat2ApiUrl = document.getElementById('row-grok-webchat2api-url');
const inputGrokWebchat2ApiUrl = document.getElementById('input-grok-webchat2api-url');
const rowGrokWebchat2ApiKey = document.getElementById('row-grok-webchat2api-key');
const inputGrokWebchat2ApiKey = document.getElementById('input-grok-webchat2api-key');
const rowKiroWebStatus = document.getElementById('row-kiro-web-status'); const rowKiroWebStatus = document.getElementById('row-kiro-web-status');
const displayKiroWebStatus = document.getElementById('display-kiro-web-status'); const displayKiroWebStatus = document.getElementById('display-kiro-web-status');
const rowKiroLoginUrl = document.getElementById('row-kiro-login-url'); const rowKiroLoginUrl = document.getElementById('row-kiro-login-url');
@@ -196,10 +200,11 @@ const rowGrokRegisterStatus = document.getElementById('row-grok-register-status'
const displayGrokRegisterStatus = document.getElementById('display-grok-register-status'); const displayGrokRegisterStatus = document.getElementById('display-grok-register-status');
const rowGrokSsoStatus = document.getElementById('row-grok-sso-status'); const rowGrokSsoStatus = document.getElementById('row-grok-sso-status');
const displayGrokSsoStatus = document.getElementById('display-grok-sso-status'); const displayGrokSsoStatus = document.getElementById('display-grok-sso-status');
const rowGrokWebchat2ApiUploadStatus = document.getElementById('row-grok-webchat2api-upload-status');
const displayGrokWebchat2ApiUploadStatus = document.getElementById('display-grok-webchat2api-upload-status');
const rowGrokSsoSettings = document.getElementById('row-grok-sso-settings'); const rowGrokSsoSettings = document.getElementById('row-grok-sso-settings');
const displayGrokSsoCookie = document.getElementById('display-grok-sso-cookie'); const displayGrokSsoCookie = document.getElementById('display-grok-sso-cookie');
const btnCopyGrokSso = document.getElementById('btn-copy-grok-sso'); const btnCopyGrokSso = document.getElementById('btn-copy-grok-sso');
const btnExportGrokSso = document.getElementById('btn-export-grok-sso');
const btnClearGrokSso = document.getElementById('btn-clear-grok-sso'); const btnClearGrokSso = document.getElementById('btn-clear-grok-sso');
const rowCustomPassword = document.getElementById('row-custom-password'); const rowCustomPassword = document.getElementById('row-custom-password');
const rowPlusMode = document.getElementById('row-plus-mode'); const rowPlusMode = document.getElementById('row-plus-mode');
@@ -1576,6 +1581,7 @@ const PRIVACY_MASKED_INPUT_IDS = Object.freeze([
'input-sub2api-default-proxy', 'input-sub2api-default-proxy',
'input-codex2api-url', 'input-codex2api-url',
'input-kiro-rs-url', 'input-kiro-rs-url',
'input-grok-webchat2api-url',
'input-gpc-helper-api', 'input-gpc-helper-api',
'input-gpc-helper-phone', 'input-gpc-helper-phone',
'input-gpc-helper-local-sms-url', 'input-gpc-helper-local-sms-url',
@@ -2718,6 +2724,20 @@ function getGrokRegisterStatusLabel(value = '') {
} }
} }
function getGrokWebchat2ApiUploadStatusLabel(value = '') {
const normalized = String(value || '').trim().toLowerCase();
switch (normalized) {
case 'uploading':
return '正在上传';
case 'uploaded':
return '已上传';
case 'error':
return '上传失败';
default:
return String(value || '').trim() || '未开始';
}
}
function renderGrokRuntimeState(state = latestState) { function renderGrokRuntimeState(state = latestState) {
const runtimeState = getGrokRuntimeState(state); const runtimeState = getGrokRuntimeState(state);
const registerStatus = String( const registerStatus = String(
@@ -2737,6 +2757,27 @@ function renderGrokRuntimeState(state = latestState) {
|| state?.grokSsoExtractedAt || state?.grokSsoExtractedAt
|| 0 || 0
) || 0; ) || 0;
const uploadState = runtimeState?.upload || {};
const uploadStatus = String(
uploadState.status
|| state?.grokWebchat2ApiUploadStatus
|| ''
).trim();
const uploadedAt = Number(
uploadState.uploadedAt
|| state?.grokWebchat2ApiUploadedAt
|| 0
) || 0;
const uploadMessage = String(
uploadState.message
|| state?.grokWebchat2ApiUploadMessage
|| ''
).trim();
const uploadTargetUrl = String(
uploadState.targetUrl
|| state?.grokWebchat2ApiTargetUrl
|| ''
).trim();
if (displayGrokRegisterStatus) { if (displayGrokRegisterStatus) {
displayGrokRegisterStatus.textContent = getGrokRegisterStatusLabel(registerStatus); displayGrokRegisterStatus.textContent = getGrokRegisterStatusLabel(registerStatus);
@@ -2750,9 +2791,15 @@ function renderGrokRuntimeState(state = latestState) {
displayGrokSsoCookie.textContent = currentCookie displayGrokSsoCookie.textContent = currentCookie
? `${currentCookie.slice(0, 8)}...${currentCookie.slice(-6)}` ? `${currentCookie.slice(0, 8)}...${currentCookie.slice(-6)}`
: '未提取'; : '未提取';
displayGrokSsoCookie.title = currentCookie ? '已隐藏完整 SSO Cookie,可使用复制或导出' : ''; displayGrokSsoCookie.title = currentCookie ? '已隐藏完整 SSO Cookie,可使用复制' : '';
} }
[btnCopyGrokSso, btnExportGrokSso, btnClearGrokSso].forEach((button) => { if (displayGrokWebchat2ApiUploadStatus) {
const label = getGrokWebchat2ApiUploadStatusLabel(uploadStatus);
const suffix = uploadedAt ? `${new Date(uploadedAt).toLocaleString()}` : '';
displayGrokWebchat2ApiUploadStatus.textContent = `${label}${uploadMessage ? `${uploadMessage}` : ''}${suffix}`;
displayGrokWebchat2ApiUploadStatus.title = uploadTargetUrl || '';
}
[btnCopyGrokSso, btnClearGrokSso].forEach((button) => {
if (button) { if (button) {
button.disabled = cookies.length === 0; button.disabled = cookies.length === 0;
} }
@@ -4915,6 +4962,12 @@ function collectSettingsPayload() {
const currentKiroRsKeyValue = typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey const currentKiroRsKeyValue = typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey
? String(inputKiroRsKey.value ?? '').trim() ? String(inputKiroRsKey.value ?? '').trim()
: null; : null;
const currentGrokWebchat2ApiUrlValue = typeof inputGrokWebchat2ApiUrl !== 'undefined' && inputGrokWebchat2ApiUrl
? String(inputGrokWebchat2ApiUrl.value ?? '').trim()
: null;
const currentGrokWebchat2ApiKeyValue = typeof inputGrokWebchat2ApiKey !== 'undefined' && inputGrokWebchat2ApiKey
? String(inputGrokWebchat2ApiKey.value ?? '').trim()
: null;
const normalizeHostedCheckoutDelaySecondsSafe = typeof normalizePlusHostedCheckoutOauthDelaySeconds === 'function' const normalizeHostedCheckoutDelaySecondsSafe = typeof normalizePlusHostedCheckoutOauthDelaySeconds === 'function'
? normalizePlusHostedCheckoutOauthDelaySeconds ? normalizePlusHostedCheckoutOauthDelaySeconds
: ((value) => { : ((value) => {
@@ -4933,6 +4986,12 @@ function collectSettingsPayload() {
kiroRsKey: currentKiroRsKeyValue !== null kiroRsKey: currentKiroRsKeyValue !== null
? currentKiroRsKeyValue ? currentKiroRsKeyValue
: String(latestState?.kiroRsKey || '').trim(), : String(latestState?.kiroRsKey || '').trim(),
grokWebchat2ApiUrl: currentGrokWebchat2ApiUrlValue !== null
? currentGrokWebchat2ApiUrlValue
: String(latestState?.grokWebchat2ApiUrl || '').trim(),
grokWebchat2ApiAdminKey: currentGrokWebchat2ApiKeyValue !== null
? currentGrokWebchat2ApiKeyValue
: String(latestState?.grokWebchat2ApiAdminKey || '').trim(),
vpsUrl: inputVpsUrl.value.trim(), vpsUrl: inputVpsUrl.value.trim(),
vpsPassword: inputVpsPassword.value, vpsPassword: inputVpsPassword.value,
localCpaStep9Mode: getSelectedLocalCpaStep9Mode(), localCpaStep9Mode: getSelectedLocalCpaStep9Mode(),
@@ -11128,6 +11187,12 @@ function applySettingsState(state) {
if (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey) { if (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey) {
inputKiroRsKey.value = String(state?.kiroRsKey || ''); inputKiroRsKey.value = String(state?.kiroRsKey || '');
} }
if (typeof inputGrokWebchat2ApiUrl !== 'undefined' && inputGrokWebchat2ApiUrl) {
inputGrokWebchat2ApiUrl.value = String(state?.grokWebchat2ApiUrl || '').trim();
}
if (typeof inputGrokWebchat2ApiKey !== 'undefined' && inputGrokWebchat2ApiKey) {
inputGrokWebchat2ApiKey.value = String(state?.grokWebchat2ApiAdminKey || '');
}
if (typeof displayKiroRsTestStatus !== 'undefined' && displayKiroRsTestStatus) { if (typeof displayKiroRsTestStatus !== 'undefined' && displayKiroRsTestStatus) {
displayKiroRsTestStatus.textContent = kiroRsConnectionTestStatusText; displayKiroRsTestStatus.textContent = kiroRsConnectionTestStatusText;
} }
@@ -14053,23 +14118,6 @@ btnCopyGrokSso?.addEventListener('click', async () => {
} }
}); });
btnExportGrokSso?.addEventListener('click', () => {
try {
const cookies = normalizeGrokSsoCookies(latestState);
if (!cookies.length) {
throw new Error('没有可导出的 Grok SSO Cookie。');
}
downloadTextFile(
`${cookies.join('\n')}\n`,
`grok-sso-cookies-${new Date().toISOString().replace(/[:.]/g, '-')}.txt`,
'text/plain;charset=utf-8'
);
showToast('Grok SSO Cookie 已导出。', 'success');
} catch (error) {
showToast(error?.message || '导出 Grok SSO Cookie 失败。', 'error');
}
});
btnClearGrokSso?.addEventListener('click', async () => { btnClearGrokSso?.addEventListener('click', async () => {
try { try {
const cookies = normalizeGrokSsoCookies(latestState); const cookies = normalizeGrokSsoCookies(latestState);
@@ -15776,6 +15824,16 @@ selectPlusAccountAccessStrategy?.addEventListener('change', () => {
}); });
}); });
[inputGrokWebchat2ApiUrl, inputGrokWebchat2ApiKey].forEach((input) => {
input?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
input?.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
});
function syncCurrentIpProxyServiceProfileToLatestState() { function syncCurrentIpProxyServiceProfileToLatestState() {
const selectedService = normalizeIpProxyService( const selectedService = normalizeIpProxyService(
selectIpProxyService?.value || latestState?.ipProxyService || DEFAULT_IP_PROXY_SERVICE selectIpProxyService?.value || latestState?.ipProxyService || DEFAULT_IP_PROXY_SERVICE
@@ -0,0 +1,274 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadPublisherApi() {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const publisherSource = fs.readFileSync('flows/grok/background/publisher-webchat2api.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${publisherSource}; return self;`)(globalScope);
return globalScope.MultiPageBackgroundGrokPublisherWebchat2Api;
}
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
statusText: status >= 200 && status < 300 ? 'OK' : 'Error',
text: async () => JSON.stringify(payload),
};
}
function getGrokRuntime(state = {}) {
return state?.runtimeState?.flowState?.grok || {};
}
test('grok webchat2api publisher exposes helpers and normalizes to origin inject endpoint', () => {
const api = loadPublisherApi();
assert.equal(typeof api?.createGrokWebchat2ApiPublisher, 'function');
assert.equal(
api.buildWebchat2ApiInjectUrl('https://remote.example.com/admin/deep/path'),
'https://remote.example.com/api/remote-account/inject'
);
assert.equal(
api.buildWebchat2ApiInjectUrl('remote.example.com/admin'),
'http://remote.example.com/api/remote-account/inject'
);
});
test('grok webchat2api publisher requires URL, admin key, and SSO cookie', async () => {
const api = loadPublisherApi();
assert.throws(
() => api.buildWebchat2ApiInjectUrl(''),
/缺少 webchat2api 地址/
);
assert.throws(
() => api.buildGrokSsoInjectPayload(''),
/缺少 Grok SSO Cookie/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api('http://remote.example.com', '', 'sso-cookie', async () => createJsonResponse({})),
/缺少 webchat2api 管理密钥/
);
});
test('grok webchat2api publisher posts Grok SSO payload with bearer admin key', async () => {
const api = loadPublisherApi();
const requests = [];
const result = await api.uploadGrokSsoToWebchat2Api(
'https://remote.example.com/admin/deep/path',
' admin-secret ',
'sso-cookie-001',
async (url, options = {}) => {
requests.push({
url,
method: options.method,
authorization: options.headers?.Authorization,
contentType: options.headers?.['Content-Type'],
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, data: { total: 1 }, message: 'ok' });
}
);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].method, 'POST');
assert.equal(requests[0].authorization, 'Bearer admin-secret');
assert.equal(requests[0].contentType, 'application/json');
assert.deepEqual(requests[0].body, {
accounts: [{
token: 'sso-cookie-001',
provider: 'grok',
type: 'sso',
}],
strategy: 'merge',
source_id: 'flowpilot-grok-sso',
source_name: 'FlowPilot Grok SSO',
provider: 'grok',
});
assert.equal(result.endpointUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(result.message, 'ok');
});
test('grok webchat2api publisher accepts code-zero envelopes and rejects HTTP/API errors', async () => {
const api = loadPublisherApi();
const success = await api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 0, data: { added: 1 } })
);
assert.equal(success.message, '上传成功');
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ error: 'invalid admin key' }, 403)
),
/webchat2api SSO 上传失败:invalid admin key/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 4001, message: 'bad token' })
),
/webchat2api SSO 上传失败:bad token/
);
});
test('grok webchat2api executor reads latest state and writes upload runtime without leaking secrets to logs', async () => {
const api = loadPublisherApi();
const requests = [];
const logs = [];
const completed = [];
let liveState = {
grokWebchat2ApiUrl: '',
grokWebchat2ApiAdminKey: '',
grokSsoCookie: '',
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'live-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'live-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
requests.push({
url,
authorization: options.headers?.Authorization,
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, message: 'uploaded' });
},
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await publisher.executeGrokUploadSsoToWebchat2Api({
nodeId: 'grok-upload-sso-to-webchat2api',
grokWebchat2ApiAdminKey: 'stale-key',
grokSsoCookie: 'stale-sso-cookie',
});
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].authorization, 'Bearer live-admin-key');
assert.equal(requests[0].body.accounts[0].token, 'live-sso-cookie');
assert.equal(completed.length, 1);
assert.equal(completed[0].nodeId, 'grok-upload-sso-to-webchat2api');
assert.equal(getGrokRuntime(completed[0].payload).upload.status, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.message, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(typeof getGrokRuntime(completed[0].payload).upload.uploadedAt, 'number');
assert.equal(logs.some(({ message }) => message.includes('live-sso-cookie') || message.includes('live-admin-key')), false);
});
test('grok webchat2api executor persists failure state without completing or leaking secrets', async () => {
const api = loadPublisherApi();
const logs = [];
const completed = [];
let liveState = {
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'secret-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'secret-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async () => createJsonResponse({ error: 'invalid admin key' }, 403),
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await assert.rejects(
() => publisher.executeGrokUploadSsoToWebchat2Api({ nodeId: 'grok-upload-sso-to-webchat2api' }),
/webchat2api SSO 上传失败:invalid admin key/
);
assert.equal(completed.length, 0);
assert.equal(getGrokRuntime(liveState).upload.status, 'error');
assert.equal(getGrokRuntime(liveState).upload.uploadedAt, 0);
assert.equal(getGrokRuntime(liveState).upload.message, 'webchat2api SSO 上传失败:invalid admin key');
assert.equal(getGrokRuntime(liveState).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(logs.some(({ message }) => message.includes('secret-sso-cookie') || message.includes('secret-admin-key')), false);
});
+59 -5
View File
@@ -37,6 +37,12 @@ test('grok state view projects canonical runtime state into legacy flat read fie
cookies: ['cookie-a', 'cookie-b', 'cookie-a'], cookies: ['cookie-a', 'cookie-b', 'cookie-a'],
extractedAt: 3000, extractedAt: 3000,
}, },
upload: {
status: 'uploaded',
uploadedAt: 4000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
}, },
}, },
}, },
@@ -55,9 +61,14 @@ test('grok state view projects canonical runtime state into legacy flat read fie
assert.equal(view.grokSsoCookie, 'cookie-a'); assert.equal(view.grokSsoCookie, 'cookie-a');
assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']); assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']);
assert.equal(view.grokSsoExtractedAt, 3000); assert.equal(view.grokSsoExtractedAt, 3000);
assert.equal(view.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(view.grokWebchat2ApiUploadedAt, 4000);
assert.equal(view.grokWebchat2ApiUploadMessage, 'ok');
assert.equal(view.grokWebchat2ApiTargetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(view.runtimeState.flowState.openai.preserved, true); assert.equal(view.runtimeState.flowState.openai.preserved, true);
assert.equal(view.runtimeState.flowState.grok.register.email, 'user@example.com'); assert.equal(view.runtimeState.flowState.grok.register.email, 'user@example.com');
assert.equal(view.flowState.grok.sso.currentCookie, 'cookie-a'); assert.equal(view.flowState.grok.sso.currentCookie, 'cookie-a');
assert.equal(view.flowState.grok.upload.status, 'uploaded');
assert.equal(view.flows.grok.sso.cookies.length, 2); assert.equal(view.flows.grok.sso.cookies.length, 2);
}); });
@@ -69,6 +80,10 @@ test('grok completion payloads update canonical runtime state and flat compatibi
grokSsoCookie: 'cookie-z', grokSsoCookie: 'cookie-z',
grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'], grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'],
grokCompletedAt: 456, grokCompletedAt: 456,
grokWebchat2ApiUploadStatus: 'uploaded',
grokWebchat2ApiUploadedAt: 789,
grokWebchat2ApiUploadMessage: '上传成功',
grokWebchat2ApiTargetUrl: 'http://remote.example.com/api/remote-account/inject',
}); });
assert.equal(patch.grokEmail, 'grok@example.com'); assert.equal(patch.grokEmail, 'grok@example.com');
@@ -77,11 +92,39 @@ test('grok completion payloads update canonical runtime state and flat compatibi
assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']); assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']);
assert.equal(patch.grokCompletedAt, 456); assert.equal(patch.grokCompletedAt, 456);
assert.equal(patch.grokSsoExtractedAt, 456); assert.equal(patch.grokSsoExtractedAt, 456);
assert.equal(patch.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(patch.grokWebchat2ApiUploadedAt, 789);
assert.equal(patch.grokWebchat2ApiUploadMessage, '上传成功');
assert.equal(patch.grokWebchat2ApiTargetUrl, 'http://remote.example.com/api/remote-account/inject');
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com'); assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z'); assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
assert.equal(patch.runtimeState.flowState.grok.upload.status, 'uploaded');
}); });
test('grok fresh keep-state reset preserves SSO cookies but clears registration runtime', () => { test('grok state keeps canonical runtime values when flat compatibility fields are empty', () => {
const api = loadGrokStateApi();
const runtimeState = api.ensureRuntimeState({
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'canonical-cookie',
cookies: ['canonical-cookie'],
extractedAt: 1234,
},
},
},
},
});
assert.equal(runtimeState.sso.currentCookie, 'canonical-cookie');
assert.deepEqual(runtimeState.sso.cookies, ['canonical-cookie']);
assert.equal(runtimeState.sso.extractedAt, 1234);
});
test('grok fresh keep-state reset clears registration, SSO, and upload runtime', () => {
const api = loadGrokStateApi(); const api = loadGrokStateApi();
const patch = api.buildFreshKeepState({ const patch = api.buildFreshKeepState({
runtimeState: { runtimeState: {
@@ -101,6 +144,12 @@ test('grok fresh keep-state reset preserves SSO cookies but clears registration
cookies: ['cookie-a', 'cookie-b'], cookies: ['cookie-a', 'cookie-b'],
extractedAt: 2000, extractedAt: 2000,
}, },
upload: {
status: 'uploaded',
uploadedAt: 3000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
}, },
}, },
}, },
@@ -111,11 +160,16 @@ test('grok fresh keep-state reset preserves SSO cookies but clears registration
assert.equal(patch.grokEmail, ''); assert.equal(patch.grokEmail, '');
assert.equal(patch.grokRegisterStatus, ''); assert.equal(patch.grokRegisterStatus, '');
assert.equal(patch.grokCompletedAt, 0); assert.equal(patch.grokCompletedAt, 0);
assert.equal(patch.grokSsoCookie, 'cookie-a'); assert.equal(patch.grokSsoCookie, '');
assert.deepEqual(patch.grokSsoCookies, ['cookie-a', 'cookie-b']); assert.deepEqual(patch.grokSsoCookies, []);
assert.equal(patch.grokSsoExtractedAt, 2000); assert.equal(patch.grokSsoExtractedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadStatus, '');
assert.equal(patch.grokWebchat2ApiUploadedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadMessage, '');
assert.equal(patch.grokWebchat2ApiTargetUrl, '');
assert.equal(patch.runtimeState.flowState.grok.register.email, ''); assert.equal(patch.runtimeState.flowState.grok.register.email, '');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-a'); assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, '');
assert.equal(patch.runtimeState.flowState.grok.upload.status, '');
}); });
test('grok downstream reset clears only the state owned by the restarted tail', () => { test('grok downstream reset clears only the state owned by the restarted tail', () => {
+4 -1
View File
@@ -19,12 +19,14 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/); assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/); assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/); assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/background\/publisher-webchat2api\.js/);
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/); assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/); assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/); assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/); assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/); assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
assert.match(source, /const grokWebchat2ApiPublisher = self\.MultiPageBackgroundGrokPublisherWebchat2Api\?\.createGrokWebchat2ApiPublisher\(/);
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/); assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/); assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
@@ -40,6 +42,7 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/); assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/);
assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/); assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/);
assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/); assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
assert.match(source, /'grok-upload-sso-to-webchat2api': \(state\) => grokWebchat2ApiPublisher\.executeGrokUploadSsoToWebchat2Api\(state\)/);
assert.match( assert.match(
source, source,
@@ -47,7 +50,7 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
); );
assert.match( assert.match(
source, source,
/'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie'/ /'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie',[\s\S]*'grok-upload-sso-to-webchat2api'/
); );
}); });
+1 -1
View File
@@ -153,7 +153,7 @@ test('settings schema can project canonical state into a read view without legac
assert.deepEqual(view.stepExecutionRangeByFlow.grok, { assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
enabled: false, enabled: false,
fromStep: 1, fromStep: 1,
toStep: 5, toStep: 6,
}); });
}); });
+14 -5
View File
@@ -110,7 +110,7 @@ test('grok verification runner polls by flow node and submits normalized code',
assert.equal(getGrokRuntime(completedPayload).register.status, 'verified'); assert.equal(getGrokRuntime(completedPayload).register.status, 'verified');
}); });
test('grok SSO extraction accumulates unique cookies without logging the secret value', async () => { test('grok SSO extraction stores only the current cookie without logging the secret value', async () => {
const api = loadGrokRunnerApi(); const api = loadGrokRunnerApi();
const logs = []; const logs = [];
let completedPayload = null; let completedPayload = null;
@@ -118,7 +118,7 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
let currentState = { let currentState = {
activeFlowId: 'grok', activeFlowId: 'grok',
grokRegisterTabId: 202, grokRegisterTabId: 202,
grokSsoCookies: ['existing-cookie', 'new-cookie'], grokSsoCookies: ['old-cookie'],
runtimeState: { runtimeState: {
flowState: { flowState: {
grok: { grok: {
@@ -126,7 +126,13 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
registerTabId: 202, registerTabId: 202,
}, },
sso: { sso: {
cookies: ['existing-cookie', 'new-cookie'], cookies: ['old-cookie'],
},
upload: {
status: 'uploaded',
uploadedAt: 1000,
message: 'old upload',
targetUrl: 'https://old.example.com/api/remote-account/inject',
}, },
}, },
}, },
@@ -169,9 +175,12 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState }); await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState });
assert.equal(completedPayload.grokSsoCookie, 'new-cookie'); assert.equal(completedPayload.grokSsoCookie, 'new-cookie');
assert.deepEqual(completedPayload.grokSsoCookies, ['existing-cookie', 'new-cookie']); assert.deepEqual(completedPayload.grokSsoCookies, ['new-cookie']);
assert.equal(completedPayload.grokWebchat2ApiUploadStatus, '');
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie'); assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['existing-cookie', 'new-cookie']); assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['new-cookie']);
assert.equal(getGrokRuntime(completedPayload).upload.status, '');
assert.equal(getGrokRuntime(completedPayload).upload.targetUrl, '');
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie'); assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false); assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
}); });
+14 -4
View File
@@ -49,15 +49,17 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
'id="row-kiro-upload-status"', 'id="row-kiro-upload-status"',
'id="row-grok-register-status"', 'id="row-grok-register-status"',
'id="row-grok-sso-status"', 'id="row-grok-sso-status"',
'id="row-grok-webchat2api-upload-status"',
'id="display-grok-webchat2api-upload-status"',
'id="row-grok-sso-settings"', 'id="row-grok-sso-settings"',
'id="btn-copy-grok-sso"', 'id="btn-copy-grok-sso"',
'id="btn-export-grok-sso"',
'id="btn-clear-grok-sso"', 'id="btn-clear-grok-sso"',
'<script src="../flows/grok/index.js"></script>', '<script src="../flows/grok/index.js"></script>',
'<script src="../flows/grok/workflow.js"></script>', '<script src="../flows/grok/workflow.js"></script>',
].forEach((snippet) => { ].forEach((snippet) => {
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}); });
assert.doesNotMatch(sidepanelHtml, /id="btn-export-grok-sso"/);
assert.ok( assert.ok(
sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>') sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>')
< sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>') < sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>')
@@ -86,6 +88,7 @@ test('sidepanel renders Grok SSO status from canonical runtime state', () => {
extractFunction(sidepanelSource, 'getGrokRuntimeState'), extractFunction(sidepanelSource, 'getGrokRuntimeState'),
extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'), extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'),
extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'), extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'),
extractFunction(sidepanelSource, 'getGrokWebchat2ApiUploadStatusLabel'),
extractFunction(sidepanelSource, 'renderGrokRuntimeState'), extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
].join('\n'); ].join('\n');
@@ -94,17 +97,17 @@ let latestState = {};
const displayGrokRegisterStatus = { textContent: '' }; const displayGrokRegisterStatus = { textContent: '' };
const displayGrokSsoStatus = { textContent: '' }; const displayGrokSsoStatus = { textContent: '' };
const displayGrokSsoCookie = { textContent: '', title: '' }; const displayGrokSsoCookie = { textContent: '', title: '' };
const displayGrokWebchat2ApiUploadStatus = { textContent: '', title: '' };
const buttons = []; const buttons = [];
const btnCopyGrokSso = { disabled: false }; const btnCopyGrokSso = { disabled: false };
const btnExportGrokSso = { disabled: false };
const btnClearGrokSso = { disabled: false }; const btnClearGrokSso = { disabled: false };
${bundle} ${bundle}
return { return {
displayGrokRegisterStatus, displayGrokRegisterStatus,
displayGrokSsoStatus, displayGrokSsoStatus,
displayGrokSsoCookie, displayGrokSsoCookie,
displayGrokWebchat2ApiUploadStatus,
btnCopyGrokSso, btnCopyGrokSso,
btnExportGrokSso,
btnClearGrokSso, btnClearGrokSso,
renderGrokRuntimeState, renderGrokRuntimeState,
}; };
@@ -120,6 +123,12 @@ return {
cookies: ['1234567890abcdef', 'second-cookie'], cookies: ['1234567890abcdef', 'second-cookie'],
extractedAt: 0, extractedAt: 0,
}, },
upload: {
status: 'uploaded',
uploadedAt: 0,
message: '上传成功',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
}, },
}, },
}, },
@@ -128,8 +137,9 @@ return {
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成'); assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/); assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef'); assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.textContent, '已上传:上传成功');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.title, 'https://remote.example.com/api/remote-account/inject');
assert.equal(api.btnCopyGrokSso.disabled, false); assert.equal(api.btnCopyGrokSso.disabled, false);
assert.equal(api.btnExportGrokSso.disabled, false);
assert.equal(api.btnClearGrokSso.disabled, false); assert.equal(api.btnClearGrokSso.disabled, false);
}); });
+1
View File
@@ -197,4 +197,5 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true); assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true); assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true); assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/publisher-webchat2api', 'grok-upload-sso-to-webchat2api'), true);
}); });
+5 -3
View File
@@ -220,6 +220,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'grok-submit-verification-code', 'grok-submit-verification-code',
'grok-submit-profile', 'grok-submit-profile',
'grok-extract-sso-cookie', 'grok-extract-sso-cookie',
'grok-upload-sso-to-webchat2api',
] ]
); );
assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true); assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true);
@@ -228,10 +229,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code'); assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code');
assert.deepStrictEqual( assert.deepStrictEqual(
grokSteps.map((step) => step.title), grokSteps.map((step) => step.title),
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie'] ['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie', '上传 SSO 到 webchat2api']
); );
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]); assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5, 6]);
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5); assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 6);
assert.deepStrictEqual( assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next), api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
[ [
@@ -239,6 +240,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
['grok-submit-verification-code'], ['grok-submit-verification-code'],
['grok-submit-profile'], ['grok-submit-profile'],
['grok-extract-sso-cookie'], ['grok-extract-sso-cookie'],
['grok-upload-sso-to-webchat2api'],
[], [],
] ]
); );
+12 -10
View File
@@ -14,7 +14,7 @@
- `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路 - `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
- `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路 - `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
- `grok`Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路 - `grok`Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 远程上传链路
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路: 它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
@@ -45,7 +45,7 @@
- 接收后台广播并更新 UI - 接收后台广播并更新 UI
- 动态渲染步骤列表 - 动态渲染步骤列表
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` / `grok` flow 使用各自的 `flows.<flowId>.targetId` - 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` / `grok` flow 使用各自的 `flows.<flowId>.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` SSO 导出状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置 - 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` URL / 管理密钥、SSO 提取与上传状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源 - 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态 - 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
@@ -157,7 +157,7 @@
### 3.4 步骤注册 ### 3.4 步骤注册
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow;`grok` flow 输出固定的 5 节点注册SSO Cookie 提取 workflow。 [data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow;`grok` flow 输出固定的 6 节点注册SSO Cookie 提取`webchat2api` 上传 workflow。
[core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。 [core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。
这意味着: 这意味着:
@@ -216,7 +216,7 @@
- `contributionLastPollAt` - `contributionLastPollAt`
- OAuth 链接 - OAuth 链接
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度 - Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
- Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies` 只作为兼容投影,不应当再形成第二套状态机 - Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso / upload`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies / grokWebchat2ApiUpload*` 只作为兼容投影,不应当再形成第二套状态机;兼容 flat 字段只有非空值才能覆盖 canonical runtime,避免空旧字段反向覆盖本轮 SSO
- 当前轮冻结后的注册方式 `resolvedSignupMethod` - 当前轮冻结后的注册方式 `resolvedSignupMethod`
- 当前统一账号标识 `accountIdentifierType / accountIdentifier` - 当前统一账号标识 `accountIdentifierType / accountIdentifier`
- 当前邮箱 / 密码 - 当前邮箱 / 密码
@@ -771,7 +771,7 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
## 6.3 Grok Flow 链路 ## 6.3 Grok Flow 链路
Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把 `sso` Cookie 作为 `webchat2api` 可用的本地导出产物。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。 Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把本轮提取到的 `sso` Cookie 上传到 `webchat2api` 的远程账号注入接口。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。
链路如下: 链路如下:
@@ -780,15 +780,17 @@ Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把 `sso` Cookie
3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页 3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页
4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交 4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交
5. 步骤 4 填写资料和密码,推进注册完成页状态 5. 步骤 4 填写资料和密码,推进注册完成页状态
6. 步骤 5 先通过 `chrome.cookies.get``x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies` 6. 步骤 5 先通过 `chrome.cookies.get``x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的当前值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies`,其中 `grokSsoCookies` 只保留本轮当前 Cookie,不再累积历史列表
7. sidepanel 只展示掩码后的 SSO 值,复制和导出才使用完整值;清空动作必须发 `CLEAR_GROK_SSO_COOKIES` 给后台,由 `flows/grok/background/state.js` 构造 patch,不能在 sidepanel 直接写 `chrome.storage` 7. 步骤 6 由 `flows/grok/background/publisher-webchat2api.js` 读取 `settingsState.flows.grok.targets.webchat2api.baseUrl / apiKey` 和当前 SSO,向 `${origin}/api/remote-account/inject` 发送 Bearer 管理密钥请求,payload 使用 `accounts[{ token, provider:'grok', type:'sso' }] + strategy:'merge'`
8. 上传结果保存到 `runtimeState.flowState.grok.upload` 并投影到 `grokWebchat2ApiUploadStatus / grokWebchat2ApiUploadedAt / grokWebchat2ApiUploadMessage / grokWebchat2ApiTargetUrl`;缺 URL、缺管理密钥或缺 SSO 都必须报错,不允许静默跳过
9. sidepanel 只展示掩码后的 SSO 值与上传状态;复制按钮仅用于排障,不提供本地文件导出;清空动作必须发 `CLEAR_GROK_SSO_COOKIES` 给后台,由 `flows/grok/background/state.js` 构造 patch,并同步清空 upload 状态,不能在 sidepanel 直接写 `chrome.storage`
这条链路的关键边界是: 这条链路的关键边界是:
- Grok 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路 - Grok 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Grok 自动运行走通用 linear node runner,执行器、运行态、页面驱动邮件规则全部放在 `flows/grok/*` - Grok 自动运行走通用 linear node runner,执行器、运行态、页面驱动邮件规则`webchat2api` 上传器全部放在 `flows/grok/*`
- Grok 内容脚本不能全局 patch 浏览器原型;点击事件需要的坐标只能在局部事件参数里补齐 - Grok 内容脚本不能全局 patch 浏览器原型;点击事件需要的坐标只能在局部事件参数里补齐
- Grok SSO Cookie 是本地导出产物`flows/grok/index.js``publicationTargets` 保持为空,`supportsAccountContribution` 保持为 `false` - Grok SSO 上传是 Grok flow 自己的收尾节点`flows/grok/index.js``publicationTargets` 保持为空,`supportsAccountContribution` 保持为 `false`
- `shared/contribution-registry.js` 的默认发布贡献校验只覆盖声明了发布目标或支持贡献的 flow;Grok 不应被自动纳入贡献 adapter 缺失检查。如果未来要把 Grok 改成正式发布 flow,必须先新增明确的 publication target 与 contribution adapter,再打开对应能力 - `shared/contribution-registry.js` 的默认发布贡献校验只覆盖声明了发布目标或支持贡献的 flow;Grok 不应被自动纳入贡献 adapter 缺失检查。如果未来要把 Grok 改成正式发布 flow,必须先新增明确的 publication target 与 contribution adapter,再打开对应能力
## 7. 邮箱与 provider 链路 ## 7. 邮箱与 provider 链路
@@ -1160,7 +1162,7 @@ Hide My Email 获取与管理链路:
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态 - 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态 - 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口 - fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
- Grok fresh-attempt reset 会清理注册页 runtime,但保留已提取的 `grokSsoCookies`,避免用户导出前被下一轮准备动作清空 - Grok fresh-attempt reset 会清理注册页 runtime、当前 SSO 与 upload 状态,避免下一轮误上传上一轮旧 SSO
5. 执行 `runAutoSequenceFromStep` 5. 执行 `runAutoSequenceFromStep`
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog - 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点 - 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
+3 -3
View File
@@ -171,7 +171,7 @@
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod` - `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod` - 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。 - flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
- 本地导出型 flow、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 导出属于本地导出 flow不能因为有 target 就默认接入贡献模式。 - flow 私有远程上传、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 上传属于 Grok flow 自己的收尾节点,不是 publication target,也不能因为有 target 就默认接入贡献模式。
### 1.7 日志步骤号原则 ### 1.7 日志步骤号原则
@@ -286,7 +286,7 @@
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js` - 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js` - 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败 - 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
- 如果新增 flow 只是本地导出产物,例如 Cookie、JSON、会话文件、手动复制值,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定发布到远端服务,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。 - 如果新增 flow 只是私有收尾产物,例如 Cookie、JSON、会话文件、手动复制值,或只上传到该 flow 自己的专属远端服务,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定把它纳入正式发布 / 贡献体系,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
### 3.4.1 iCloud Hide My Email 维护补充 ### 3.4.1 iCloud Hide My Email 维护补充
@@ -497,7 +497,7 @@ npm test
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交? 15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态? 16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId` 17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`
18. 如果新增或调整 flow target,我有没有确认它到底是本地导出、正式发布还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js` 18. 如果新增或调整 flow target,我有没有确认它到底是 flow 私有收尾目标、正式发布目标还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`
## 9. 完成标准 ## 9. 完成标准
+10 -8
View File
@@ -21,7 +21,7 @@
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。 - `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。
- `LICENSE`:项目许可证文件。 - `LICENSE`:项目许可证文件。
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。 - `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled``stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步”链路。 - `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled``stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步 -> `webchat2api` 上传 6 步”链路。
- `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。 - `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。 - `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。 - `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
@@ -59,8 +59,9 @@
- `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。 - `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
- `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。 - `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。
- `flows/kiro/background/publisher-kiro-rs.js`Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。 - `flows/kiro/background/publisher-kiro-rs.js`Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。
- `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso` 的默认值、归一化、兼容字段投影、节点完成回写、下游重置和 fresh-attempt keep-state 构建。 - `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso / upload` 的默认值、归一化、兼容字段投影、节点完成回写、下游重置和 fresh-attempt keep-state 构建。
- `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、SSO Cookie 提取、去重保存与账号记录收尾。 - `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、当前 SSO Cookie 提取与账号记录收尾。
- `flows/grok/background/publisher-webchat2api.js`Grok 专属 `webchat2api` 上传器,负责地址归一化到 `/api/remote-account/inject`、Bearer 管理密钥请求头、SSO payload 构建、成功/失败响应解析与 Grok upload 运行态回写。
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。 - `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
- `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 - `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
@@ -156,8 +157,8 @@
- `flows/openai/mail-rules.js`OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/openai/mail-rules.js`OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/kiro/mail-rules.js`Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/kiro/mail-rules.js`Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/grok/index.js`Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与本地 SSO 导出属性;该 flow 当前没有 publication target,也不支持贡献 adapter。 - `flows/grok/index.js`Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与 flow 私有远程上传配置;该 flow 当前没有 publication target,也不支持贡献 adapter。
- `flows/grok/workflow.js`Grok workflow 定义,固定输出打开注册页、提交邮箱、提交验证码、提交资料、提取 SSO Cookie个节点。 - `flows/grok/workflow.js`Grok workflow 定义,固定输出打开注册页、提交邮箱、提交验证码、提交资料、提取 SSO Cookie、上传 SSO 到 `webchat2api`个节点。
- `flows/grok/mail-rules.js`Grok flow 的 xAI / Grok 验证码邮件规则定义,负责输出发件人、主题、关键词、`ABC-123` 与 6 位码 pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/grok/mail-rules.js`Grok flow 的 xAI / Grok 验证码邮件规则定义,负责输出发件人、主题、关键词、`ABC-123` 与 6 位码 pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/grok/content/register-page.js`:Grok 注册页内容脚本,负责五个 Grok 节点的 DOM 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。 - `flows/grok/content/register-page.js`:Grok 注册页内容脚本,负责五个 Grok 节点的 DOM 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。
@@ -188,7 +189,7 @@
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。 - `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
- `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro / grok` 三套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。 - `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro / grok` 三套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
- `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。 - `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
- `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flowGrok 这类本地 SSO 导出 flow 不自动纳入贡献 adapter 缺失检查。 - `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flowGrok 这类 flow 私有远程上传链路不自动纳入贡献 adapter 缺失检查。
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows.<flowId>.targetId / targets``stepExecutionRangeByFlow` - `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows.<flowId>.targetId / targets``stepExecutionRangeByFlow`
- `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。 - `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
@@ -281,7 +282,8 @@
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。 - `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
- `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为。 - `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为。
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。 - `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。
- `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO Cookie 保留规则。 - `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO/upload 清理规则。
- `tests/background-grok-publisher-webchat2api.test.js`:测试 Grok `webchat2api` 上传器的地址归一化、必填配置校验、SSO payload、响应处理、运行态回写和敏感值日志保护。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。 - `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。 - `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。 - `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
@@ -369,7 +371,7 @@
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。 - `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。 - `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。 - `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 本地 SSO 导出 flow 不被自动纳入发布贡献检查。 - `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 私有 `webchat2api` 上传 flow 不被自动纳入发布贡献检查。
- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。 - `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。 - `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。 - `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。