重构 Grok SSO 上传到 webchat2api
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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(`步骤 6:Grok 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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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'/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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 或支持贡献的 flow,Grok 这类本地 SSO 导出 flow 不自动纳入贡献 adapter 缺失检查。
|
- `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flow,Grok 这类 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 接线,以及邮箱池长度与自动轮数之间的联动规则。
|
||||||
|
|||||||
Reference in New Issue
Block a user