mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "yakit-chrome-client",
|
"name": "yakit-chrome-client",
|
||||||
"description": "Yakit Browser Extension",
|
"description": "Yakit Browser Extension",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "[email protected]",
|
"packageManager": "[email protected]",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
validateBrowserTransformRecovery,
|
validateBrowserTransformRecovery,
|
||||||
} from '@/features/browser-transform/service';
|
} from '@/features/browser-transform/service';
|
||||||
import {
|
import {
|
||||||
|
discardBrowserTransformValidation,
|
||||||
latestBrowserTransformValidation,
|
latestBrowserTransformValidation,
|
||||||
proposeBrowserTransformProfile,
|
proposeBrowserTransformProfile,
|
||||||
validateInferredBrowserTransformProfile,
|
validateInferredBrowserTransformProfile,
|
||||||
@@ -60,6 +61,28 @@ export const handleTransformRequest: BackgroundRequestHandler = async (request,
|
|||||||
await requiredRequestTarget(request.payload, sender),
|
await requiredRequestTarget(request.payload, sender),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
case 'analysis.profile.validation.resolve': {
|
||||||
|
const input = request.payload;
|
||||||
|
const target = await requiredRequestTarget(input, sender);
|
||||||
|
const draft = await latestBrowserTransformValidation(target);
|
||||||
|
if (!draft || draft.id !== input.validationId) {
|
||||||
|
throw new Error('验证草稿不存在或已经过期,请重新生成并验证');
|
||||||
|
}
|
||||||
|
if (input.outcome === 'discard') {
|
||||||
|
await discardBrowserTransformValidation(target, draft.id);
|
||||||
|
return ok(null);
|
||||||
|
}
|
||||||
|
const profile = await saveBrowserTransformProfile(draft.profile);
|
||||||
|
await discardBrowserTransformValidation(target, draft.id);
|
||||||
|
void appendAuditEvent({
|
||||||
|
category: 'capability',
|
||||||
|
action: 'analysis.profile.validation.save',
|
||||||
|
outcome: 'success',
|
||||||
|
targetTabId: profile.target.tabId,
|
||||||
|
summary: profile.name,
|
||||||
|
});
|
||||||
|
return ok(profile);
|
||||||
|
}
|
||||||
case 'transform.profile.list': {
|
case 'transform.profile.list': {
|
||||||
const input = request.payload;
|
const input = request.payload;
|
||||||
const target = input.tabId ? await requiredRequestTarget(input, sender) : undefined;
|
const target = input.tabId ? await requiredRequestTarget(input, sender) : undefined;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
BROWSER_TRANSFORM_VALIDATION_DRAFT_MAX_BYTES,
|
BROWSER_TRANSFORM_VALIDATION_DRAFT_MAX_BYTES,
|
||||||
compareBrowserPackets,
|
compareBrowserPackets,
|
||||||
comparePacketWithInferenceCandidate,
|
comparePacketWithInferenceCandidate,
|
||||||
|
discardBrowserTransformValidation,
|
||||||
inspectRecordingEvidence,
|
inspectRecordingEvidence,
|
||||||
listRecordingTraces,
|
listRecordingTraces,
|
||||||
promoteObservedEnvelopeCallable,
|
promoteObservedEnvelopeCallable,
|
||||||
@@ -75,6 +76,13 @@ function formCandidate(): BrowserProfileInferenceCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('browser analysis deterministic tools', () => {
|
describe('browser analysis deterministic tools', () => {
|
||||||
|
it('rejects confirmation for a missing or expired validation draft', async () => {
|
||||||
|
await expect(discardBrowserTransformValidation(
|
||||||
|
{ tabId: 1, frameId: 0, documentId: 'document-1' },
|
||||||
|
'validation-missing',
|
||||||
|
)).rejects.toThrow(/不存在或已经过期/);
|
||||||
|
});
|
||||||
|
|
||||||
it('bounds validation drafts before session persistence', () => {
|
it('bounds validation drafts before session persistence', () => {
|
||||||
const draft = {
|
const draft = {
|
||||||
contractVersion: 1,
|
contractVersion: 1,
|
||||||
|
|||||||
@@ -478,6 +478,26 @@ export async function latestBrowserTransformValidation(
|
|||||||
return draft || memoryValidationDrafts.get(key) || null;
|
return draft || memoryValidationDrafts.get(key) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function discardBrowserTransformValidation(
|
||||||
|
target: BrowserTarget,
|
||||||
|
validationId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const key = validationDraftKey(target);
|
||||||
|
let discarded = false;
|
||||||
|
validationDraftStorageQueue = validationDraftStorageQueue.then(async () => {
|
||||||
|
const drafts = pruneValidationDrafts(await readStoredValidationDrafts());
|
||||||
|
if (drafts[key]?.id === validationId) {
|
||||||
|
delete drafts[key];
|
||||||
|
discarded = true;
|
||||||
|
}
|
||||||
|
await writeStoredValidationDrafts(drafts);
|
||||||
|
});
|
||||||
|
await validationDraftStorageQueue;
|
||||||
|
if (!discarded) {
|
||||||
|
throw new ExtensionError('validation_draft_stale', '验证草稿不存在或已经过期,请重新生成并验证');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formValueType(value: string): string {
|
function formValueType(value: string): string {
|
||||||
if (!value) return 'empty';
|
if (!value) return 'empty';
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -1109,7 +1129,7 @@ export async function proposeBrowserTransformProfile(
|
|||||||
? callable.transaction ? 'captured-request-transaction' : 'validated-callable-envelope'
|
? callable.transaction ? 'captured-request-transaction' : 'validated-callable-envelope'
|
||||||
: 'recording-evidence',
|
: 'recording-evidence',
|
||||||
},
|
},
|
||||||
next: '调用 profile.validate;验证成功后由用户确认保存,AI 不直接持久化配置',
|
next: '调用 profile.validate;验证成功后由用户在插件中确认保存,AI 不直接持久化配置',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,7 +1206,7 @@ export async function validateBrowserTransformProposal(
|
|||||||
} : undefined,
|
} : undefined,
|
||||||
next: comparison
|
next: comparison
|
||||||
? comparison.equivalent
|
? comparison.equivalent
|
||||||
? '确定性验证通过;Yakit 已收到待用户确认的明文网关草稿'
|
? '确定性验证通过;插件已生成待用户确认的明文网关草稿'
|
||||||
: '数据包对比未通过;检查输入映射或重新选择页面函数'
|
: '数据包对比未通过;检查输入映射或重新选择页面函数'
|
||||||
: 'Pipeline 已真实回放并生成待确认草稿;如需更强证明,请提供一份浏览器线上请求进行结构对比',
|
: 'Pipeline 已真实回放并生成待确认草稿;如需更强证明,请提供一份浏览器线上请求进行结构对比',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
ActiveTabInfo, BrowserPageCallable, BrowserRecordingEvent, BrowserTransformBuiltinOperation,
|
ActiveTabInfo, BrowserPageCallable, BrowserRecordingEvent, BrowserTransformBuiltinOperation,
|
||||||
BrowserTransformDirection,
|
BrowserTransformDirection,
|
||||||
BrowserTransformNodeReference, BrowserTransformPipelineNode, BrowserTransformProfile,
|
BrowserTransformNodeReference, BrowserTransformPipelineNode, BrowserTransformProfile,
|
||||||
BrowserTransformProfileInput, BrowserProfileInferenceCandidate,
|
BrowserTransformProfileInput, BrowserProfileInferenceCandidate, BrowserTransformValidationDraft,
|
||||||
} from '@/types/models';
|
} from '@/types/models';
|
||||||
import {
|
import {
|
||||||
callableEnvelopeDescription, compileGuidedTransform, defaultGuidedTransform, guidedOutputDescription, parseGuidedTransform,
|
callableEnvelopeDescription, compileGuidedTransform, defaultGuidedTransform, guidedOutputDescription, parseGuidedTransform,
|
||||||
@@ -257,6 +257,7 @@ export function BrowserTransformWorkspace({
|
|||||||
INITIAL_TRANSFORM_WORKSPACE_STATE,
|
INITIAL_TRANSFORM_WORKSPACE_STATE,
|
||||||
);
|
);
|
||||||
const [workspaceView, setWorkspaceView] = useState<'flow' | 'configure'>('flow');
|
const [workspaceView, setWorkspaceView] = useState<'flow' | 'configure'>('flow');
|
||||||
|
const [pendingValidation, setPendingValidation] = useState<BrowserTransformValidationDraft | null>(null);
|
||||||
const {
|
const {
|
||||||
profiles, callables, selectedProfileId, draft, directionName, loadError,
|
profiles, callables, selectedProfileId, draft, directionName, loadError,
|
||||||
testMethod, testUrl, testHeaders, testBody, testSample, testResult, testError,
|
testMethod, testUrl, testHeaders, testBody, testSample, testResult, testError,
|
||||||
@@ -351,11 +352,28 @@ export function BrowserTransformWorkspace({
|
|||||||
}
|
}
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
|
const loadPendingValidation = useCallback(async () => {
|
||||||
|
if (!tab) {
|
||||||
|
setPendingValidation(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setPendingValidation(await request('analysis.profile.validation.latest', { tabId: tab.id, frameId: 0 }));
|
||||||
|
} catch {
|
||||||
|
setPendingValidation(null);
|
||||||
|
}
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
workspaceMounted.current = true;
|
workspaceMounted.current = true;
|
||||||
return () => { workspaceMounted.current = false; };
|
return () => { workspaceMounted.current = false; };
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => { void load(); }, [load]);
|
useEffect(() => { void load(); }, [load]);
|
||||||
|
useEffect(() => {
|
||||||
|
void loadPendingValidation();
|
||||||
|
const timer = setInterval(() => void loadPendingValidation(), 2_000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [loadPendingValidation]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recoveryRevision > 0) void load();
|
if (recoveryRevision > 0) void load();
|
||||||
}, [load, recoveryRevision]);
|
}, [load, recoveryRevision]);
|
||||||
@@ -731,6 +749,22 @@ export function BrowserTransformWorkspace({
|
|||||||
await load();
|
await load();
|
||||||
}, '已取消本次恢复结果,旧网关继续保持停用');
|
}, '已取消本次恢复结果,旧网关继续保持停用');
|
||||||
|
|
||||||
|
const resolvePendingValidation = (outcome: 'save' | 'discard') => run(async () => {
|
||||||
|
if (!tab || !pendingValidation) return;
|
||||||
|
const profile = await request('analysis.profile.validation.resolve', {
|
||||||
|
tabId: tab.id,
|
||||||
|
frameId: 0,
|
||||||
|
validationId: pendingValidation.id,
|
||||||
|
outcome,
|
||||||
|
});
|
||||||
|
setPendingValidation(null);
|
||||||
|
if (!profile) return;
|
||||||
|
setProfiles((current) => [profile, ...current.filter((item) => item.id !== profile.id)]);
|
||||||
|
setSelectedProfileId(profile.id);
|
||||||
|
setDraft(toInput(profile));
|
||||||
|
setWorkspaceView('flow');
|
||||||
|
}, outcome === 'save' ? '明文网关已保存' : '验证草稿已放弃');
|
||||||
|
|
||||||
const execute = async () => {
|
const execute = async () => {
|
||||||
if (!draft?.id || dirty) { setTestError('请先保存当前 Pipeline'); return; }
|
if (!draft?.id || dirty) { setTestError('请先保存当前 Pipeline'); return; }
|
||||||
setTestError('');
|
setTestError('');
|
||||||
@@ -762,6 +796,18 @@ export function BrowserTransformWorkspace({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="transform-editor">
|
<main className="transform-editor">
|
||||||
|
{pendingValidation && <section className="transform-validation-pending" role="status">
|
||||||
|
<span className="transform-validation-pending__mark"><CheckCircle2 size={16} /></span>
|
||||||
|
<div>
|
||||||
|
<small>Agent 已完成本地验证 · {pendingValidation.proofLevel === 'exact' ? '报文一致' : pendingValidation.proofLevel === 'structure' ? '结构一致' : '执行通过'}</small>
|
||||||
|
<strong>{pendingValidation.profile.name}</strong>
|
||||||
|
<p>{pendingValidation.profile.origin} · {pendingValidation.profile.request.enabled ? '请求加密' : '响应解密'} · {Math.max(1, Math.ceil((pendingValidation.expiresAt - Date.now()) / 60_000))} 分钟后过期</p>
|
||||||
|
</div>
|
||||||
|
<div className="transform-validation-pending__actions">
|
||||||
|
<Button size="sm" variant="ghost" disabled={busy} onClick={() => void resolvePendingValidation('discard')}>放弃</Button>
|
||||||
|
<Button size="sm" variant="primary" disabled={busy} onClick={() => void resolvePendingValidation('save')}><Save size={13} />确认保存</Button>
|
||||||
|
</div>
|
||||||
|
</section>}
|
||||||
{!draft ? <div className="transform-editor-empty"><Link2 size={24} /><strong>建立明文与线上报文的转换链路</strong>{callables.length ? <Button variant="primary" onClick={create}><CirclePlus size={14} />新建 Pipeline</Button> : <Button variant="primary" onClick={onOpenCapture}><Code2 size={14} />{deepCaptureAvailable ? '先捕获页面函数' : '回到录制并保存页面函数'}</Button>}</div> : <>
|
{!draft ? <div className="transform-editor-empty"><Link2 size={24} /><strong>建立明文与线上报文的转换链路</strong>{callables.length ? <Button variant="primary" onClick={create}><CirclePlus size={14} />新建 Pipeline</Button> : <Button variant="primary" onClick={onOpenCapture}><Code2 size={14} />{deepCaptureAvailable ? '先捕获页面函数' : '回到录制并保存页面函数'}</Button>}</div> : <>
|
||||||
<header className="transform-editor-head">
|
<header className="transform-editor-head">
|
||||||
<div>{workspaceView === 'flow' && savedProfile
|
<div>{workspaceView === 'flow' && savedProfile
|
||||||
|
|||||||
@@ -63,6 +63,16 @@
|
|||||||
.transform-callable-confirm > div { display: flex; justify-content: flex-end; gap: 6px; }
|
.transform-callable-confirm > div { display: flex; justify-content: flex-end; gap: 6px; }
|
||||||
|
|
||||||
.transform-editor { max-height: 820px; overflow: auto; display: grid; align-content: start; border-right: 1px solid var(--border); }
|
.transform-editor { max-height: 820px; overflow: auto; display: grid; align-content: start; border-right: 1px solid var(--border); }
|
||||||
|
.transform-validation-pending { min-width: 0; min-height: 72px; padding: 11px 14px; display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; align-items: center; gap: 10px; border-bottom: 1px solid color-mix(in srgb, var(--success) 28%, var(--border)); background: color-mix(in srgb, var(--success-soft) 68%, var(--surface)); }
|
||||||
|
.transform-validation-pending__mark { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: var(--success-soft); color: var(--success); }
|
||||||
|
.transform-validation-pending > div { min-width: 0; }
|
||||||
|
.transform-validation-pending small,
|
||||||
|
.transform-validation-pending strong,
|
||||||
|
.transform-validation-pending p { display: block; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.transform-validation-pending small { color: var(--success); font-size: 10px; font-weight: 650; }
|
||||||
|
.transform-validation-pending strong { margin-top: 3px; font-size: var(--text-sm); }
|
||||||
|
.transform-validation-pending p { margin-top: 2px; color: var(--muted); font-size: var(--text-xs); }
|
||||||
|
.transform-validation-pending__actions { display: flex; align-items: center; gap: 6px; }
|
||||||
.transform-editor-empty { min-height: 520px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 11px; color: var(--muted); text-align: center; }
|
.transform-editor-empty { min-height: 520px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 11px; color: var(--muted); text-align: center; }
|
||||||
.transform-editor-empty strong { color: var(--foreground); font-size: var(--text-md); }
|
.transform-editor-empty strong { color: var(--foreground); font-size: var(--text-md); }
|
||||||
.transform-editor-head { min-height: 64px; padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; gap: 14px; border-bottom: 1px solid var(--border); }
|
.transform-editor-head { min-height: 64px; padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; gap: 14px; border-bottom: 1px solid var(--border); }
|
||||||
@@ -438,6 +448,8 @@
|
|||||||
.transform-route label:last-child { grid-column: 1 / -1; }
|
.transform-route label:last-child { grid-column: 1 / -1; }
|
||||||
.transform-recovery { grid-template-columns: 30px minmax(0, 1fr); }
|
.transform-recovery { grid-template-columns: 30px minmax(0, 1fr); }
|
||||||
.transform-recovery__actions { grid-column: 2; justify-content: flex-start; flex-wrap: wrap; }
|
.transform-recovery__actions { grid-column: 2; justify-content: flex-start; flex-wrap: wrap; }
|
||||||
|
.transform-validation-pending { grid-template-columns: 30px minmax(0, 1fr); }
|
||||||
|
.transform-validation-pending__actions { grid-column: 2; justify-content: flex-start; }
|
||||||
.transform-step-fields { grid-template-columns: minmax(0, 1fr); }
|
.transform-step-fields { grid-template-columns: minmax(0, 1fr); }
|
||||||
.transform-step-fields .transform-step-name { grid-column: auto; }
|
.transform-step-fields .transform-step-name { grid-column: auto; }
|
||||||
.transform-output-list > div { grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr) 32px; }
|
.transform-output-list > div { grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr) 32px; }
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
BrowserTransformExecuteInput,
|
BrowserTransformExecuteInput,
|
||||||
BrowserTransformPacket,
|
BrowserTransformPacket,
|
||||||
BrowserTransformProfileInput,
|
|
||||||
} from '@/types/models';
|
} from '@/types/models';
|
||||||
import { browser } from 'wxt/browser';
|
|
||||||
import type { CapabilityDomainHandler } from '../capability-context';
|
import type { CapabilityDomainHandler } from '../capability-context';
|
||||||
import { allowedTarget, requireScope } from '../capability-context';
|
import { allowedTarget, requireScope } from '../capability-context';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +13,6 @@ import {
|
|||||||
getBrowserTransformRecovery,
|
getBrowserTransformRecovery,
|
||||||
listBrowserTransformProfiles,
|
listBrowserTransformProfiles,
|
||||||
resetBrowserTransformRecovery,
|
resetBrowserTransformRecovery,
|
||||||
saveBrowserTransformProfile,
|
|
||||||
startBrowserTransformRecovery,
|
startBrowserTransformRecovery,
|
||||||
validateBrowserTransformRecovery,
|
validateBrowserTransformRecovery,
|
||||||
} from '@/features/browser-transform/service';
|
} from '@/features/browser-transform/service';
|
||||||
@@ -25,7 +22,6 @@ import {
|
|||||||
proposeBrowserTransformProfile,
|
proposeBrowserTransformProfile,
|
||||||
validateInferredBrowserTransformProfile,
|
validateInferredBrowserTransformProfile,
|
||||||
} from '@/features/browser-analysis/service';
|
} from '@/features/browser-analysis/service';
|
||||||
import { ExtensionError } from '@/shared/errors';
|
|
||||||
import { TRANSFORM_CAPABILITY_DOMAIN } from '../capability-domains';
|
import { TRANSFORM_CAPABILITY_DOMAIN } from '../capability-domains';
|
||||||
|
|
||||||
export const transformCapabilityHandler: CapabilityDomainHandler = {
|
export const transformCapabilityHandler: CapabilityDomainHandler = {
|
||||||
@@ -122,15 +118,6 @@ export const transformCapabilityHandler: CapabilityDomainHandler = {
|
|||||||
}));
|
}));
|
||||||
return visible.filter(Boolean);
|
return visible.filter(Boolean);
|
||||||
}
|
}
|
||||||
if (method === 'browser.transform.profile.save') {
|
|
||||||
const profileInput = input as unknown as BrowserTransformProfileInput;
|
|
||||||
const target = await allowedTarget(grant, profileInput.target);
|
|
||||||
const frame = await browser.webNavigation.getFrame(target);
|
|
||||||
if (!frame?.url || profileInput.origin !== new URL(frame.url).origin) {
|
|
||||||
throw new ExtensionError('target_denied', '转换配置来源与当前页面不一致');
|
|
||||||
}
|
|
||||||
return saveBrowserTransformProfile({ ...profileInput, target });
|
|
||||||
}
|
|
||||||
if (method === 'browser.transform.profile.delete') {
|
if (method === 'browser.transform.profile.delete') {
|
||||||
const profile = await getBrowserTransformProfile(String(input.id || ''));
|
const profile = await getBrowserTransformProfile(String(input.id || ''));
|
||||||
await allowedTarget(grant, profile.target);
|
await allowedTarget(grant, profile.target);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { BridgePublicKey } from '@/types/models';
|
|||||||
import {
|
import {
|
||||||
browserTransformExecuteSchema,
|
browserTransformExecuteSchema,
|
||||||
browserTransformPacketSchema,
|
browserTransformPacketSchema,
|
||||||
browserTransformProfileInputSchema,
|
|
||||||
} from './transform';
|
} from './transform';
|
||||||
|
|
||||||
export const BRIDGE_PROTOCOL_VERSION = 3;
|
export const BRIDGE_PROTOCOL_VERSION = 3;
|
||||||
@@ -237,7 +236,6 @@ export const capabilityParams = {
|
|||||||
'browser.deep_capture.resume': v.optional(v.strictObject(targetFields)),
|
'browser.deep_capture.resume': v.optional(v.strictObject(targetFields)),
|
||||||
'browser.deep_capture.detach': v.optional(v.strictObject(targetFields)),
|
'browser.deep_capture.detach': v.optional(v.strictObject(targetFields)),
|
||||||
'browser.transform.profile.list': v.optional(v.strictObject(targetFields)),
|
'browser.transform.profile.list': v.optional(v.strictObject(targetFields)),
|
||||||
'browser.transform.profile.save': browserTransformProfileInputSchema,
|
|
||||||
'browser.transform.profile.delete': v.strictObject({ id }),
|
'browser.transform.profile.delete': v.strictObject({ id }),
|
||||||
'browser.transform.recovery.get': v.strictObject({ id }),
|
'browser.transform.recovery.get': v.strictObject({ id }),
|
||||||
'browser.transform.recovery.start': v.strictObject({ id }),
|
'browser.transform.recovery.start': v.strictObject({ id }),
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ describe('versioned Bridge capability catalog', () => {
|
|||||||
expect(capabilityVisibleToAgent('browser.handoff.resolve')).toBe(false);
|
expect(capabilityVisibleToAgent('browser.handoff.resolve')).toBe(false);
|
||||||
expect(capabilityVisibleToAgent('browser.thumbnail')).toBe(false);
|
expect(capabilityVisibleToAgent('browser.thumbnail')).toBe(false);
|
||||||
expect(capabilityVisibleToAgent('browser.context')).toBe(true);
|
expect(capabilityVisibleToAgent('browser.context')).toBe(true);
|
||||||
|
expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.profile.save')).toBeUndefined();
|
||||||
|
expect(catalog.capabilities.find((capability) => capability.method === 'proxy.switch')?.summary)
|
||||||
|
.toContain('不会生成、启用或执行 Transform Profile');
|
||||||
|
expect(catalog.capabilities.find((capability) => capability.method === 'browser.profile.validate')?.summary)
|
||||||
|
.toContain('用户在插件本地确认保存');
|
||||||
expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.recovery.capture')).toMatchObject({
|
expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.recovery.capture')).toMatchObject({
|
||||||
access: 'dangerous',
|
access: 'dangerous',
|
||||||
scopes: ['browser.transform.manage', 'browser.debugger.control', 'browser.callable.execute'],
|
scopes: ['browser.transform.manage', 'browser.debugger.control', 'browser.callable.execute'],
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.network.start': {
|
'browser.network.start': {
|
||||||
domain: 'network', access: 'control', summary: '启动有界网络捕获,可选采集请求头和 Body',
|
domain: 'network', access: 'control', summary: '启动 DevTools 网络观察,只采集页面请求;不会生成或执行明文网关',
|
||||||
scopes: ['browser.network.capture'],
|
scopes: ['browser.network.capture'],
|
||||||
conditionalScopes: [{ scope: 'browser.network.sensitive.read', when: 'captureHeaders=true or captureBody=true' }],
|
conditionalScopes: [{ scope: 'browser.network.sensitive.read', when: 'captureHeaders=true or captureBody=true' }],
|
||||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
@@ -138,7 +138,7 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.network.list': {
|
'browser.network.list': {
|
||||||
domain: 'network', access: 'read', summary: '列出已捕获请求,敏感字段仍由 Agent 操作审核策略保护',
|
domain: 'network', access: 'read', summary: '列出 DevTools 已捕获请求,适合观察流量;需要转换加密报文时改用 transform 域',
|
||||||
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.network.clear': {
|
'browser.network.clear': {
|
||||||
@@ -162,7 +162,7 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.network.sensitive.read'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
scopes: ['browser.network.sensitive.read'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.recording.start': {
|
'browser.recording.start': {
|
||||||
domain: 'recording', access: 'control', summary: '开始业务 Trace 录制,可选采集有界值预览',
|
domain: 'recording', access: 'control', summary: '开始业务 Trace 录制;生成新明文网关时先录制一次真实业务操作,再检查候选证据',
|
||||||
scopes: ['browser.recording.control'],
|
scopes: ['browser.recording.control'],
|
||||||
conditionalScopes: [{ scope: 'browser.recording.sensitive.read', when: 'captureValues=true' }],
|
conditionalScopes: [{ scope: 'browser.recording.sensitive.read', when: 'captureValues=true' }],
|
||||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
@@ -194,7 +194,7 @@ const CAPABILITY_METADATA = {
|
|||||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.callable.create': {
|
'browser.callable.create': {
|
||||||
domain: 'callable', access: 'execute', summary: '从录制句柄或深度捕获 Frame 创建页面函数',
|
domain: 'callable', access: 'execute', summary: '从录制句柄或深度捕获 Frame 创建页面函数;生成明文网关 Profile 前需要得到可回放函数',
|
||||||
scopes: ['browser.callable.execute'],
|
scopes: ['browser.callable.execute'],
|
||||||
conditionalScopes: [{ scope: 'browser.debugger.control', when: 'source=deep-capture' }],
|
conditionalScopes: [{ scope: 'browser.debugger.control', when: 'source=deep-capture' }],
|
||||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
@@ -240,13 +240,9 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.debugger.control'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
scopes: ['browser.debugger.control'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.transform.profile.list': {
|
'browser.transform.profile.list': {
|
||||||
domain: 'transform', access: 'read', summary: '列出目标页面可见的明文网关 Profile',
|
domain: 'transform', access: 'read', summary: '明文网关入口:先列出目标页面已有 Profile;已有配置可直接用 transform.execute,无配置再走录制、提案和验证',
|
||||||
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.transform.profile.save': {
|
|
||||||
domain: 'transform', access: 'dangerous', summary: '保存或更新完整 Transform Profile',
|
|
||||||
scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS,
|
|
||||||
},
|
|
||||||
'browser.transform.profile.delete': {
|
'browser.transform.profile.delete': {
|
||||||
domain: 'transform', access: 'write', summary: '删除 Transform Profile',
|
domain: 'transform', access: 'write', summary: '删除 Transform Profile',
|
||||||
scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
@@ -278,7 +274,7 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.transform.execute': {
|
'browser.transform.execute': {
|
||||||
domain: 'transform', access: 'execute', summary: '对 HTTP 报文应用已保存的请求或响应转换',
|
domain: 'transform', access: 'execute', summary: '使用已保存的 Profile 对 HTTP 报文执行请求加密或响应解密;它不是网络代理切换',
|
||||||
scopes: ['browser.transform.execute'], targetMode: 'profile', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
scopes: ['browser.transform.execute'], targetMode: 'profile', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.packet.compare': {
|
'browser.packet.compare': {
|
||||||
@@ -286,16 +282,16 @@ const CAPABILITY_METADATA = {
|
|||||||
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.profile.propose': {
|
'browser.profile.propose': {
|
||||||
domain: 'transform', access: 'read', summary: '从候选证据和页面函数确定性编译 Profile 提案',
|
domain: 'transform', access: 'read', summary: '从录制候选和页面函数编译未保存的 Profile 提案;下一步必须调用 profile.validate',
|
||||||
scopes: ['browser.transform.read', 'browser.recording.read'],
|
scopes: ['browser.transform.read', 'browser.recording.read'],
|
||||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.profile.validation.latest': {
|
'browser.profile.validation.latest': {
|
||||||
domain: 'transform', access: 'read', summary: '读取当前文档最近的短时验证草稿',
|
domain: 'transform', access: 'read', summary: '读取当前文档最近的短时验证草稿及本地确认状态;草稿过期后需重新验证',
|
||||||
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'browser.profile.validate': {
|
'browser.profile.validate': {
|
||||||
domain: 'transform', access: 'execute', summary: '重新编译并执行 Profile,再与候选或报文证据比较',
|
domain: 'transform', access: 'execute', summary: '确定性执行 Profile 提案并与证据比较;成功后只生成短时草稿,必须由用户在插件本地确认保存',
|
||||||
scopes: ['browser.transform.execute', 'browser.recording.read'],
|
scopes: ['browser.transform.execute', 'browser.recording.read'],
|
||||||
targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
@@ -310,11 +306,11 @@ const CAPABILITY_METADATA = {
|
|||||||
targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'proxy.list': {
|
'proxy.list': {
|
||||||
domain: 'proxy', access: 'read', summary: '列出扩展代理 Profile',
|
domain: 'proxy', access: 'read', summary: '列出 Chrome 网络代理 Profile,仅用于流量路由;不是页面加解密或明文网关',
|
||||||
scopes: ['browser.proxy.read'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.proxy.read'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
'proxy.switch': {
|
'proxy.switch': {
|
||||||
domain: 'proxy', access: 'write', summary: '切换当前代理 Profile',
|
domain: 'proxy', access: 'write', summary: '切换 Chrome 流量代理,仅改变网络路由;不会生成、启用或执行 Transform Profile',
|
||||||
scopes: ['browser.proxy.write'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
scopes: ['browser.proxy.write'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
} satisfies Record<BridgeCapabilityMethod, CapabilityMetadata>;
|
} satisfies Record<BridgeCapabilityMethod, CapabilityMetadata>;
|
||||||
|
|||||||
@@ -316,6 +316,14 @@ describe('extension request schemas', () => {
|
|||||||
action: 'analysis.profile.validation.latest',
|
action: 'analysis.profile.validation.latest',
|
||||||
payload: { tabId: 12, frameId: 0 },
|
payload: { tabId: 12, frameId: 0 },
|
||||||
}).action).toBe('analysis.profile.validation.latest');
|
}).action).toBe('analysis.profile.validation.latest');
|
||||||
|
expect(parseExtensionRequest({
|
||||||
|
action: 'analysis.profile.validation.resolve',
|
||||||
|
payload: { tabId: 12, frameId: 0, validationId: 'validation-1', outcome: 'save' },
|
||||||
|
}).action).toBe('analysis.profile.validation.resolve');
|
||||||
|
expect(() => parseExtensionRequest({
|
||||||
|
action: 'analysis.profile.validation.resolve',
|
||||||
|
payload: { tabId: 12, frameId: 0, validationId: 'validation-1', outcome: 'approve' },
|
||||||
|
})).toThrow();
|
||||||
expect(parseExtensionRequest({
|
expect(parseExtensionRequest({
|
||||||
action: 'transform.execute',
|
action: 'transform.execute',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -440,6 +440,11 @@ const payloadSchemas = {
|
|||||||
'analysis.profile.propose': capabilityParams['browser.profile.propose'],
|
'analysis.profile.propose': capabilityParams['browser.profile.propose'],
|
||||||
'analysis.profile.validate': capabilityParams['browser.profile.validate'],
|
'analysis.profile.validate': capabilityParams['browser.profile.validate'],
|
||||||
'analysis.profile.validation.latest': capabilityParams['browser.profile.validation.latest'],
|
'analysis.profile.validation.latest': capabilityParams['browser.profile.validation.latest'],
|
||||||
|
'analysis.profile.validation.resolve': v.strictObject({
|
||||||
|
...targetFields,
|
||||||
|
validationId: id,
|
||||||
|
outcome: v.picklist(['save', 'discard']),
|
||||||
|
}),
|
||||||
'transform.profile.list': v.strictObject(targetFields),
|
'transform.profile.list': v.strictObject(targetFields),
|
||||||
'transform.profile.save': browserTransformProfileInputSchema,
|
'transform.profile.save': browserTransformProfileInputSchema,
|
||||||
'transform.profile.delete': v.strictObject({ id }),
|
'transform.profile.delete': v.strictObject({ id }),
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ export interface ExtensionRequestMap {
|
|||||||
input: { tabId?: number; frameId?: number; documentId?: string };
|
input: { tabId?: number; frameId?: number; documentId?: string };
|
||||||
output: BrowserTransformValidationDraft | null;
|
output: BrowserTransformValidationDraft | null;
|
||||||
};
|
};
|
||||||
|
'analysis.profile.validation.resolve': {
|
||||||
|
input: {
|
||||||
|
tabId?: number;
|
||||||
|
frameId?: number;
|
||||||
|
documentId?: string;
|
||||||
|
validationId: string;
|
||||||
|
outcome: 'save' | 'discard';
|
||||||
|
};
|
||||||
|
output: BrowserTransformProfile | null;
|
||||||
|
};
|
||||||
'transform.profile.list': { input: { tabId?: number; frameId?: number; documentId?: string }; output: BrowserTransformProfile[] };
|
'transform.profile.list': { input: { tabId?: number; frameId?: number; documentId?: string }; output: BrowserTransformProfile[] };
|
||||||
'transform.profile.save': { input: BrowserTransformProfileInput; output: BrowserTransformProfile };
|
'transform.profile.save': { input: BrowserTransformProfileInput; output: BrowserTransformProfile };
|
||||||
'transform.profile.delete': { input: { id: string }; output: BrowserTransformProfile[] };
|
'transform.profile.delete': { input: { id: string }; output: BrowserTransformProfile[] };
|
||||||
|
|||||||
Reference in New Issue
Block a user