@mseep/anything-analyzer 3.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { PromptBuilder } from "../../../src/main/ai/prompt-builder";
|
|
3
|
+
import type { AssembledData } from "../../../src/shared/types";
|
|
4
|
+
|
|
5
|
+
// Minimal fixture to satisfy PromptBuilder
|
|
6
|
+
function createMinimalData(): AssembledData {
|
|
7
|
+
return {
|
|
8
|
+
requests: [
|
|
9
|
+
{
|
|
10
|
+
seq: 1,
|
|
11
|
+
method: "GET",
|
|
12
|
+
url: "https://example.com/api/test",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: null,
|
|
15
|
+
status: 200,
|
|
16
|
+
responseHeaders: null,
|
|
17
|
+
responseBody: '{"ok":true}',
|
|
18
|
+
hooks: [],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
storageDiff: {
|
|
22
|
+
cookies: { added: {}, changed: {}, removed: [] },
|
|
23
|
+
localStorage: { added: {}, changed: {}, removed: [] },
|
|
24
|
+
sessionStorage: { added: {}, changed: {}, removed: [] },
|
|
25
|
+
},
|
|
26
|
+
estimatedTokens: 100,
|
|
27
|
+
sceneHints: [],
|
|
28
|
+
streamingRequests: [],
|
|
29
|
+
authChain: [],
|
|
30
|
+
cryptoScripts: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("PromptBuilder", () => {
|
|
35
|
+
const builder = new PromptBuilder();
|
|
36
|
+
const data = createMinimalData();
|
|
37
|
+
const minimalData = data;
|
|
38
|
+
const platform = "example.com";
|
|
39
|
+
|
|
40
|
+
describe("purpose=undefined (default)", () => {
|
|
41
|
+
it("should include default 8 analysis requirements", () => {
|
|
42
|
+
const { user } = builder.build(data, platform);
|
|
43
|
+
expect(user).toContain("场景识别");
|
|
44
|
+
expect(user).toContain("交互流程概述");
|
|
45
|
+
expect(user).toContain("API端点清单");
|
|
46
|
+
expect(user).toContain("鉴权机制分析");
|
|
47
|
+
expect(user).toContain("流式通信分析");
|
|
48
|
+
expect(user).toContain("存储使用分析");
|
|
49
|
+
expect(user).toContain("关键依赖关系");
|
|
50
|
+
expect(user).toContain("复现建议");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('purpose="auto"', () => {
|
|
55
|
+
it("should produce identical output to undefined", () => {
|
|
56
|
+
const defaultResult = builder.build(data, platform);
|
|
57
|
+
const autoResult = builder.build(data, platform, "auto");
|
|
58
|
+
expect(autoResult.system).toBe(defaultResult.system);
|
|
59
|
+
expect(autoResult.user).toBe(defaultResult.user);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('purpose="" (empty string)', () => {
|
|
64
|
+
it("should produce identical output to undefined", () => {
|
|
65
|
+
const defaultResult = builder.build(data, platform);
|
|
66
|
+
const emptyResult = builder.build(data, platform, "");
|
|
67
|
+
expect(emptyResult.system).toBe(defaultResult.system);
|
|
68
|
+
expect(emptyResult.user).toBe(defaultResult.user);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('purpose="reverse-api"', () => {
|
|
73
|
+
it("should replace analysis requirements with reverse-api specific ones", () => {
|
|
74
|
+
const { user } = builder.build(data, platform, "reverse-api");
|
|
75
|
+
expect(user).toContain("完整 API 端点清单");
|
|
76
|
+
expect(user).toContain("鉴权流程");
|
|
77
|
+
expect(user).toContain("请求依赖链");
|
|
78
|
+
expect(user).toContain("数据模型推断");
|
|
79
|
+
expect(user).toContain("复现代码");
|
|
80
|
+
// Should NOT contain default requirements
|
|
81
|
+
expect(user).not.toContain("场景识别:判断用户执行了什么操作");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('purpose="security-audit"', () => {
|
|
86
|
+
it("should replace analysis requirements with security-specific ones", () => {
|
|
87
|
+
const { user } = builder.build(data, platform, "security-audit");
|
|
88
|
+
expect(user).toContain("认证安全");
|
|
89
|
+
expect(user).toContain("敏感数据暴露");
|
|
90
|
+
expect(user).toContain("CSRF/XSS 风险");
|
|
91
|
+
expect(user).toContain("权限控制");
|
|
92
|
+
expect(user).toContain("安全建议");
|
|
93
|
+
expect(user).not.toContain("场景识别:判断用户执行了什么操作");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('purpose="performance"', () => {
|
|
98
|
+
it("should replace analysis requirements with performance-specific ones", () => {
|
|
99
|
+
const { user } = builder.build(data, platform, "performance");
|
|
100
|
+
expect(user).toContain("请求时序分析");
|
|
101
|
+
expect(user).toContain("冗余请求");
|
|
102
|
+
expect(user).toContain("资源优化");
|
|
103
|
+
expect(user).toContain("缓存策略");
|
|
104
|
+
expect(user).toContain("性能建议");
|
|
105
|
+
expect(user).not.toContain("场景识别:判断用户执行了什么操作");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("purpose=custom text", () => {
|
|
110
|
+
it("should prepend custom purpose and keep default requirements as baseline", () => {
|
|
111
|
+
const customText = "分析用户注册流程中的所有加密操作";
|
|
112
|
+
const { user } = builder.build(data, platform, customText);
|
|
113
|
+
expect(user).toContain(
|
|
114
|
+
"用户指定的分析重点:分析用户注册流程中的所有加密操作",
|
|
115
|
+
);
|
|
116
|
+
expect(user).toContain("在完成上述重点分析的同时,也请覆盖以下基础分析");
|
|
117
|
+
// Default requirements should still be present
|
|
118
|
+
expect(user).toContain("场景识别");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should not alter the system prompt for custom text", () => {
|
|
122
|
+
const defaultResult = builder.build(data, platform);
|
|
123
|
+
const customResult = builder.build(data, platform, "自定义分析");
|
|
124
|
+
expect(customResult.system).toBe(defaultResult.system);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return CRYPTO_REVERSE_REQUIREMENTS for crypto-reverse purpose', () => {
|
|
129
|
+
const result = builder.build(minimalData, 'TestPlatform', 'crypto-reverse')
|
|
130
|
+
expect(result.user).toContain('加密算法识别')
|
|
131
|
+
expect(result.user).toContain('加密流程还原')
|
|
132
|
+
expect(result.user).toContain('密钥管理分析')
|
|
133
|
+
expect(result.user).toContain('Python')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should include crypto hooks section in prompt', () => {
|
|
137
|
+
const dataWithCryptoHooks: AssembledData = {
|
|
138
|
+
...minimalData,
|
|
139
|
+
requests: [{
|
|
140
|
+
seq: 1, method: 'POST', url: 'https://example.com/api/login',
|
|
141
|
+
headers: {}, body: null, status: 200,
|
|
142
|
+
responseHeaders: null, responseBody: null,
|
|
143
|
+
hooks: [{
|
|
144
|
+
id: 1, session_id: 's1', timestamp: Date.now(),
|
|
145
|
+
hook_type: 'crypto_lib', function_name: 'CryptoJS.AES.encrypt',
|
|
146
|
+
arguments: '["data","key"]', result: '"U2FsdGVkX1..."',
|
|
147
|
+
call_stack: 'at encrypt (https://example.com/js/api.js:42:10)', related_request_id: null
|
|
148
|
+
}]
|
|
149
|
+
}],
|
|
150
|
+
}
|
|
151
|
+
const result = builder.build(dataWithCryptoHooks, 'TestPlatform')
|
|
152
|
+
expect(result.user).toContain('加密操作记录')
|
|
153
|
+
expect(result.user).toContain('CryptoJS.AES.encrypt')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should include crypto script snippets in prompt', () => {
|
|
157
|
+
const dataWithSnippets: AssembledData = {
|
|
158
|
+
...minimalData,
|
|
159
|
+
cryptoScripts: [{
|
|
160
|
+
scriptUrl: 'https://example.com/js/crypto.js',
|
|
161
|
+
lineRange: [10, 40] as [number, number],
|
|
162
|
+
content: 'function encrypt(data) { return CryptoJS.AES.encrypt(data, key); }',
|
|
163
|
+
matchedPatterns: ['CryptoJS'],
|
|
164
|
+
tier: 1,
|
|
165
|
+
}],
|
|
166
|
+
}
|
|
167
|
+
const result = builder.build(dataWithSnippets, 'TestPlatform')
|
|
168
|
+
expect(result.user).toContain('相关加密代码片段')
|
|
169
|
+
expect(result.user).toContain('crypto.js')
|
|
170
|
+
expect(result.user).toContain('CryptoJS.AES.encrypt')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should show empty placeholders when no crypto data', () => {
|
|
174
|
+
const result = builder.build(minimalData, 'TestPlatform')
|
|
175
|
+
expect(result.user).toContain('(无加密操作记录)')
|
|
176
|
+
expect(result.user).toContain('(无相关加密代码)')
|
|
177
|
+
})
|
|
178
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { SceneDetector } from '../../../src/main/ai/scene-detector'
|
|
3
|
+
import type { FilteredRequest } from '../../../src/shared/types'
|
|
4
|
+
|
|
5
|
+
function makeRequest(overrides: Partial<FilteredRequest> = {}): FilteredRequest {
|
|
6
|
+
return {
|
|
7
|
+
seq: 1,
|
|
8
|
+
method: 'GET',
|
|
9
|
+
url: 'https://example.com/api/data',
|
|
10
|
+
headers: {},
|
|
11
|
+
body: null,
|
|
12
|
+
status: 200,
|
|
13
|
+
responseHeaders: null,
|
|
14
|
+
responseBody: null,
|
|
15
|
+
hooks: [],
|
|
16
|
+
...overrides,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('SceneDetector', () => {
|
|
21
|
+
const detector = new SceneDetector()
|
|
22
|
+
|
|
23
|
+
// 辅助函数:创建 FilteredRequest
|
|
24
|
+
function createRequest(seq: number, method: string, url: string, body?: string, responseBody?: string, headers?: Record<string, string>, responseHeaders?: Record<string, string>): FilteredRequest {
|
|
25
|
+
return {
|
|
26
|
+
seq,
|
|
27
|
+
method,
|
|
28
|
+
url,
|
|
29
|
+
headers: headers || {},
|
|
30
|
+
body: body || null,
|
|
31
|
+
status: 200,
|
|
32
|
+
responseHeaders: responseHeaders || {},
|
|
33
|
+
responseBody: responseBody || null,
|
|
34
|
+
hooks: []
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it('应检测 AI Chat 场景 - SSE 响应', () => {
|
|
39
|
+
const requests: FilteredRequest[] = [
|
|
40
|
+
createRequest(1, 'POST', 'https://api.openai.com/v1/chat/completions', '{"messages":[]}', 'data: {"delta":{"content":"hello"}}', {}, { 'content-type': 'text/event-stream' })
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const hints = detector.detect(requests)
|
|
44
|
+
const aiChatHint = hints.find(h => h.scene === 'ai-chat' && h.confidence === 'high')
|
|
45
|
+
expect(aiChatHint).toBeDefined()
|
|
46
|
+
expect(aiChatHint?.evidence).toContain('SSE')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('应检测 AI Chat 场景 - API 路径特征', () => {
|
|
50
|
+
const requests: FilteredRequest[] = [
|
|
51
|
+
createRequest(1, 'POST', 'https://api.openai.com/v1/chat/completions', '{"messages":[]}', '{"choices":[]}')
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const hints = detector.detect(requests)
|
|
55
|
+
const aiChatHint = hints.find(h => h.scene === 'ai-chat' && h.confidence === 'high' && h.evidence.includes('API'))
|
|
56
|
+
expect(aiChatHint).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('应检测 AI Chat 场景 - 请求体特征(中等置信度)', () => {
|
|
60
|
+
const requests: FilteredRequest[] = [
|
|
61
|
+
createRequest(1, 'POST', 'https://api.example.com/chat', '{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4","temperature":0.7}', '{"response":"..."}')
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
const hints = detector.detect(requests)
|
|
65
|
+
const aiChatHint = hints.find(h => h.scene === 'ai-chat' && h.confidence === 'medium')
|
|
66
|
+
expect(aiChatHint).toBeDefined()
|
|
67
|
+
expect(aiChatHint?.evidence).toContain('AI 典型字段')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('应检测 OAuth 场景', () => {
|
|
71
|
+
const requests: FilteredRequest[] = [
|
|
72
|
+
createRequest(1, 'GET', 'https://auth.example.com/oauth/authorize?redirect_uri=http://localhost:3000', '', '')
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const hints = detector.detect(requests)
|
|
76
|
+
const oauthHint = hints.find(h => h.scene === 'auth-oauth')
|
|
77
|
+
expect(oauthHint).toBeDefined()
|
|
78
|
+
expect(oauthHint?.confidence).toBe('high')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('应检测 Token 鉴权场景 - 响应中的 access_token', () => {
|
|
82
|
+
const requests: FilteredRequest[] = [
|
|
83
|
+
createRequest(1, 'POST', 'https://api.example.com/login', '{"username":"user"}', '{"access_token":"abc123","token_type":"Bearer"}')
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const hints = detector.detect(requests)
|
|
87
|
+
const tokenHint = hints.find(h => h.scene === 'auth-token')
|
|
88
|
+
expect(tokenHint).toBeDefined()
|
|
89
|
+
expect(tokenHint?.confidence).toBe('high')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('应检测 Token 鉴权场景 - Authorization Bearer Header', () => {
|
|
93
|
+
const requests: FilteredRequest[] = [
|
|
94
|
+
createRequest(1, 'GET', 'https://api.example.com/user', '', '', { 'Authorization': 'Bearer eyJhbGc...' }, {})
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
const hints = detector.detect(requests)
|
|
98
|
+
const tokenHint = hints.find(h => h.scene === 'auth-token')
|
|
99
|
+
expect(tokenHint).toBeDefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('应检测注册场景', () => {
|
|
103
|
+
const requests: FilteredRequest[] = [
|
|
104
|
+
createRequest(1, 'POST', 'https://api.example.com/register', '{"email":"user@example.com","password":"123456"}', '{"user_id":"123"}')
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
const hints = detector.detect(requests)
|
|
108
|
+
const regHint = hints.find(h => h.scene === 'registration')
|
|
109
|
+
expect(regHint).toBeDefined()
|
|
110
|
+
expect(regHint?.confidence).toBe('high')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('应检测登录场景', () => {
|
|
114
|
+
const requests: FilteredRequest[] = [
|
|
115
|
+
createRequest(1, 'POST', 'https://api.example.com/login', '{"username":"user","password":"123456"}', '{"token":"abc123"}')
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
const hints = detector.detect(requests)
|
|
119
|
+
const loginHint = hints.find(h => h.scene === 'login')
|
|
120
|
+
expect(loginHint).toBeDefined()
|
|
121
|
+
expect(loginHint?.confidence).toBe('high')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('应检测 WebSocket 场景', () => {
|
|
125
|
+
const requests: FilteredRequest[] = [
|
|
126
|
+
createRequest(1, 'GET', 'ws://api.example.com/socket', '', '', { 'Upgrade': 'websocket', 'Connection': 'Upgrade' }, {})
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
const hints = detector.detect(requests)
|
|
130
|
+
const wsHint = hints.find(h => h.scene === 'websocket')
|
|
131
|
+
expect(wsHint).toBeDefined()
|
|
132
|
+
expect(wsHint?.confidence).toBe('high')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('应检测 SSE 流场景', () => {
|
|
136
|
+
const requests: FilteredRequest[] = [
|
|
137
|
+
createRequest(1, 'GET', 'https://api.example.com/stream', '', 'data: {"event":"message"}', {}, { 'content-type': 'text/event-stream' })
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
const hints = detector.detect(requests)
|
|
141
|
+
const sseHint = hints.find(h => h.scene === 'sse-stream')
|
|
142
|
+
expect(sseHint).toBeDefined()
|
|
143
|
+
expect(sseHint?.confidence).toBe('high')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('应检测通用 JSON API 场景(低置信度)', () => {
|
|
147
|
+
const requests: FilteredRequest[] = [
|
|
148
|
+
createRequest(1, 'GET', 'https://api.example.com/data', '', '{"id":1,"name":"test"}', {}, { 'content-type': 'application/json' })
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
const hints = detector.detect(requests)
|
|
152
|
+
const apiHint = hints.find(h => h.scene === 'api-general')
|
|
153
|
+
expect(apiHint).toBeDefined()
|
|
154
|
+
expect(apiHint?.confidence).toBe('low')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('应处理空请求列表', () => {
|
|
158
|
+
const requests: FilteredRequest[] = []
|
|
159
|
+
const hints = detector.detect(requests)
|
|
160
|
+
expect(hints).toEqual([])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('应支持多个场景同时检测', () => {
|
|
164
|
+
const requests: FilteredRequest[] = [
|
|
165
|
+
createRequest(1, 'POST', 'https://api.example.com/login', '{"username":"user","password":"123"}', '{"access_token":"abc123"}', {}, {}),
|
|
166
|
+
createRequest(2, 'POST', 'https://api.example.com/v1/chat/completions', '{"messages":[{"role":"user","content":"hello"}]}', 'data: {"choices":[]}', { 'Authorization': 'Bearer abc123' }, { 'content-type': 'text/event-stream' })
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
const hints = detector.detect(requests)
|
|
170
|
+
expect(hints.some(h => h.scene === 'login')).toBe(true)
|
|
171
|
+
expect(hints.some(h => h.scene === 'ai-chat')).toBe(true)
|
|
172
|
+
expect(hints.some(h => h.scene === 'auth-token')).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should detect crypto-encryption scene from crypto hooks', () => {
|
|
176
|
+
const req = makeRequest({
|
|
177
|
+
hooks: [{
|
|
178
|
+
id: 1, session_id: 's1', timestamp: Date.now(),
|
|
179
|
+
hook_type: 'crypto', function_name: 'crypto.subtle.encrypt',
|
|
180
|
+
arguments: '[]', result: null, call_stack: null, related_request_id: null,
|
|
181
|
+
}],
|
|
182
|
+
})
|
|
183
|
+
const hints = detector.detect([req])
|
|
184
|
+
expect(hints.some(h => h.scene === 'crypto-encryption')).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should detect crypto-encryption scene from crypto_lib hooks', () => {
|
|
188
|
+
const req = makeRequest({
|
|
189
|
+
hooks: [{
|
|
190
|
+
id: 1, session_id: 's1', timestamp: Date.now(),
|
|
191
|
+
hook_type: 'crypto_lib', function_name: 'CryptoJS.AES.encrypt',
|
|
192
|
+
arguments: '[]', result: null, call_stack: null, related_request_id: null,
|
|
193
|
+
}],
|
|
194
|
+
})
|
|
195
|
+
const hints = detector.detect([req])
|
|
196
|
+
expect(hints.some(h => h.scene === 'crypto-encryption' && h.confidence === 'high')).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should detect crypto-encryption from signature headers', () => {
|
|
200
|
+
const req = makeRequest({
|
|
201
|
+
headers: { 'x-signature': 'abc123', 'content-type': 'application/json' },
|
|
202
|
+
})
|
|
203
|
+
const hints = detector.detect([req])
|
|
204
|
+
expect(hints.some(h => h.scene === 'crypto-encryption' && h.confidence === 'medium')).toBe(true)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should not detect crypto-encryption without crypto indicators', () => {
|
|
208
|
+
const req = makeRequest()
|
|
209
|
+
const hints = detector.detect([req])
|
|
210
|
+
expect(hints.some(h => h.scene === 'crypto-encryption')).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
runMigrations,
|
|
4
|
+
migrateAddStreamingAndWebSocketFlags,
|
|
5
|
+
} from "../../../src/main/db/migrations";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
|
|
11
|
+
type BetterSqlite3Module = typeof import("better-sqlite3");
|
|
12
|
+
type BetterSqlite3Database = import("better-sqlite3").Database;
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
let Database: BetterSqlite3Module | null = null;
|
|
17
|
+
let sqliteLoadError: Error | null = null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
Database = require("better-sqlite3") as BetterSqlite3Module;
|
|
21
|
+
const probe = new Database(":memory:");
|
|
22
|
+
probe.close();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
sqliteLoadError = error as Error;
|
|
25
|
+
Database = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const describeDatabaseMigrations = sqliteLoadError ? describe.skip : describe;
|
|
29
|
+
|
|
30
|
+
describeDatabaseMigrations("Database Migrations", () => {
|
|
31
|
+
let db: BetterSqlite3Database | null = null;
|
|
32
|
+
let dbPath: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
// Create a temporary database file for testing
|
|
36
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-db-"));
|
|
37
|
+
dbPath = path.join(tmpDir, "test.db");
|
|
38
|
+
db = new Database!(dbPath);
|
|
39
|
+
|
|
40
|
+
// Run initial migrations to set up schema
|
|
41
|
+
runMigrations(db);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
// Clean up
|
|
46
|
+
db?.close();
|
|
47
|
+
if (fs.existsSync(dbPath)) {
|
|
48
|
+
fs.unlinkSync(dbPath);
|
|
49
|
+
}
|
|
50
|
+
const tmpDir = path.dirname(dbPath);
|
|
51
|
+
if (fs.existsSync(tmpDir)) {
|
|
52
|
+
fs.rmdirSync(tmpDir);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("应该成功添加 is_streaming 和 is_websocket 列到 requests 表", () => {
|
|
57
|
+
// Check that columns exist after migration
|
|
58
|
+
const tableInfo = db!.prepare("PRAGMA table_info(requests)").all() as any[];
|
|
59
|
+
const columnNames = tableInfo.map((col: any) => col.name);
|
|
60
|
+
|
|
61
|
+
expect(columnNames).toContain("is_streaming");
|
|
62
|
+
expect(columnNames).toContain("is_websocket");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("应该可以重复执行迁移而不抛出错误(幂等性)", () => {
|
|
66
|
+
// First execution (already done in beforeEach via runMigrations)
|
|
67
|
+
// Second execution should not throw
|
|
68
|
+
expect(() => {
|
|
69
|
+
migrateAddStreamingAndWebSocketFlags(db!);
|
|
70
|
+
}).not.toThrow();
|
|
71
|
+
|
|
72
|
+
// Third execution to ensure idempotency
|
|
73
|
+
expect(() => {
|
|
74
|
+
migrateAddStreamingAndWebSocketFlags(db!);
|
|
75
|
+
}).not.toThrow();
|
|
76
|
+
|
|
77
|
+
// Verify columns still exist and have correct defaults
|
|
78
|
+
const tableInfo = db!.prepare("PRAGMA table_info(requests)").all() as any[];
|
|
79
|
+
const isStreamingCol = tableInfo.find(
|
|
80
|
+
(col: any) => col.name === "is_streaming",
|
|
81
|
+
) as any;
|
|
82
|
+
const isWebsocketCol = tableInfo.find(
|
|
83
|
+
(col: any) => col.name === "is_websocket",
|
|
84
|
+
) as any;
|
|
85
|
+
|
|
86
|
+
expect(isStreamingCol).toBeDefined();
|
|
87
|
+
expect(isWebsocketCol).toBeDefined();
|
|
88
|
+
expect(isStreamingCol.dflt_value).toBe("0");
|
|
89
|
+
expect(isWebsocketCol.dflt_value).toBe("0");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("应该保持向后兼容性 - 现有数据应该继续有效", () => {
|
|
93
|
+
// Insert a request without the new columns (simulating old data)
|
|
94
|
+
const sessionId = "session-1";
|
|
95
|
+
const requestId = "req-1";
|
|
96
|
+
|
|
97
|
+
// First, create a session
|
|
98
|
+
db!.prepare(
|
|
99
|
+
`
|
|
100
|
+
INSERT INTO sessions (id, name, created_at)
|
|
101
|
+
VALUES (?, ?, ?)
|
|
102
|
+
`,
|
|
103
|
+
).run(sessionId, "Test Session", Date.now());
|
|
104
|
+
|
|
105
|
+
// Insert a request record
|
|
106
|
+
db!.prepare(
|
|
107
|
+
`
|
|
108
|
+
INSERT INTO requests
|
|
109
|
+
(id, session_id, sequence, timestamp, method, url)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
111
|
+
`,
|
|
112
|
+
).run(requestId, sessionId, 1, Date.now(), "GET", "https://example.com");
|
|
113
|
+
|
|
114
|
+
// Query the request back
|
|
115
|
+
const request = db!
|
|
116
|
+
.prepare("SELECT * FROM requests WHERE id = ?")
|
|
117
|
+
.get(requestId) as any;
|
|
118
|
+
|
|
119
|
+
// Verify the record exists and new columns have default values
|
|
120
|
+
expect(request).toBeDefined();
|
|
121
|
+
expect(request.id).toBe(requestId);
|
|
122
|
+
expect(request.is_streaming).toBe(0);
|
|
123
|
+
expect(request.is_websocket).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (sqliteLoadError) {
|
|
129
|
+
describe("Database Migrations environment", () => {
|
|
130
|
+
it("应该在原生模块不可用时给出明确信号", () => {
|
|
131
|
+
expect(sqliteLoadError?.message).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
function readWorkspaceFile(relativePath: string): string {
|
|
6
|
+
return readFileSync(path.join(process.cwd(), relativePath), "utf8");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("macOS 发布工作流", () => {
|
|
10
|
+
it("应该使用 Node 24 运行时兼容的 Actions 主版本", () => {
|
|
11
|
+
const workflow = readWorkspaceFile(".github/workflows/build.yml");
|
|
12
|
+
|
|
13
|
+
expect(workflow).toContain("uses: actions/checkout@v6");
|
|
14
|
+
expect(workflow).toContain("uses: pnpm/action-setup@v6");
|
|
15
|
+
expect(workflow).toContain("uses: actions/setup-node@v6");
|
|
16
|
+
expect(workflow).toContain("uses: actions/upload-artifact@v7");
|
|
17
|
+
expect(workflow).toContain("uses: actions/download-artifact@v6");
|
|
18
|
+
expect(workflow).toContain("uses: softprops/action-gh-release@v3");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("应该在同一个 macOS 构建中产出 x64 和 arm64 更新元数据", () => {
|
|
22
|
+
const workflow = readWorkspaceFile(".github/workflows/build.yml");
|
|
23
|
+
|
|
24
|
+
expect(workflow).toContain("platform: mac");
|
|
25
|
+
expect(workflow).toContain("npx electron-builder --mac --x64 --arm64 --publish never");
|
|
26
|
+
expect(workflow).toContain("grep -q 'arm64\\.zip' dist/latest-mac.yml");
|
|
27
|
+
expect(workflow).toContain("grep -q 'x64\\.zip' dist/latest-mac.yml");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("应该仅在 macOS 代码签名 secrets 存在时注入并校验签名", () => {
|
|
31
|
+
const workflow = readWorkspaceFile(".github/workflows/build.yml");
|
|
32
|
+
|
|
33
|
+
expect(workflow).toContain("SIGNING_CSC_LINK: ${{ secrets.CSC_LINK }}");
|
|
34
|
+
expect(workflow).toContain('if [ -n "$SIGNING_CSC_LINK" ]; then');
|
|
35
|
+
expect(workflow).toContain('echo "CSC_LINK=$SIGNING_CSC_LINK" >> "$GITHUB_ENV"');
|
|
36
|
+
expect(workflow).toContain('echo "MAC_SIGNING_ENABLED=true" >> "$GITHUB_ENV"');
|
|
37
|
+
expect(workflow).toContain('echo "MAC_SIGNING_ENABLED=false" >> "$GITHUB_ENV"');
|
|
38
|
+
expect(workflow).toContain('if [ "$MAC_SIGNING_ENABLED" != "true" ]; then');
|
|
39
|
+
expect(workflow).toContain("codesign --verify --deep --strict --verbose=2");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("应该为 macOS 构建启用 hardened runtime 和 entitlements", () => {
|
|
43
|
+
const builderConfig = readWorkspaceFile("electron-builder.yml");
|
|
44
|
+
|
|
45
|
+
expect(builderConfig).toContain("hardenedRuntime: true");
|
|
46
|
+
expect(builderConfig).toContain("gatekeeperAssess: false");
|
|
47
|
+
expect(builderConfig).toContain("type: distribution");
|
|
48
|
+
expect(builderConfig).toContain("entitlements: resources/entitlements.mac.plist");
|
|
49
|
+
expect(builderConfig).toContain("entitlementsInherit: resources/entitlements.mac.plist");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("应该提供 Electron 所需的 macOS entitlements", () => {
|
|
53
|
+
const entitlements = readWorkspaceFile("resources/entitlements.mac.plist");
|
|
54
|
+
|
|
55
|
+
expect(entitlements).toContain("com.apple.security.cs.allow-jit");
|
|
56
|
+
expect(entitlements).toContain("com.apple.security.cs.allow-unsigned-executable-memory");
|
|
57
|
+
expect(entitlements).toContain("com.apple.security.cs.disable-library-validation");
|
|
58
|
+
});
|
|
59
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"lib": ["ESNext"],
|
|
8
|
+
"outDir": "./out",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"paths": {
|
|
19
|
+
"@shared/*": ["./src/shared/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/main/**/*", "src/preload/**/*", "src/shared/**/*"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"outDir": "./out",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"paths": {
|
|
20
|
+
"@shared/*": ["./src/shared/*"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/renderer/**/*", "src/shared/**/*"]
|
|
24
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: 'node',
|
|
8
|
+
include: ['tests/**/*.test.ts'],
|
|
9
|
+
alias: {
|
|
10
|
+
'@shared': resolve(__dirname, 'src/shared')
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
})
|