@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.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,427 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { Modal, Button, Input, TextArea, Switch, Popconfirm, useToast } from '../ui'
3
+ import { IconPlus, IconDelete, IconSave, IconApi } from '../ui/Icons'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import type { MCPServerConfig } from '@shared/types'
6
+
7
+ interface Props {
8
+ open: boolean
9
+ onClose: () => void
10
+ }
11
+
12
+ /** stdio 默认 JSON 模板 */
13
+ const STDIO_TEMPLATE = JSON.stringify({ command: '', args: [], env: {} }, null, 2)
14
+ /** HTTP 默认 JSON 模板 */
15
+ const HTTP_TEMPLATE = JSON.stringify({ url: '' }, null, 2)
16
+
17
+ /**
18
+ * 从 MCPServerConfig 提取传输相关字段,生成 JSON 文本
19
+ */
20
+ function configToJson(config: MCPServerConfig): string {
21
+ if (config.transport === 'streamableHttp') {
22
+ const payload: Record<string, unknown> = { url: config.url }
23
+ if (config.headers && Object.keys(config.headers).length > 0) {
24
+ payload.headers = config.headers
25
+ }
26
+ return JSON.stringify(payload, null, 2)
27
+ }
28
+ return JSON.stringify(
29
+ { command: config.command, args: config.args, env: config.env },
30
+ null,
31
+ 2,
32
+ )
33
+ }
34
+
35
+ /**
36
+ * 获取左侧列表中的描述文本
37
+ */
38
+ function getServerDescription(server: MCPServerConfig): string {
39
+ if (server.transport === 'streamableHttp') {
40
+ return server.url || '未配置 URL'
41
+ }
42
+ return [server.command, ...server.args].filter(Boolean).join(' ') || '未配置命令'
43
+ }
44
+
45
+ export default function MCPServerModal({ open, onClose }: Props) {
46
+ const toast = useToast()
47
+ const [servers, setServers] = useState<MCPServerConfig[]>([])
48
+ const [selectedId, setSelectedId] = useState<string | null>(null)
49
+ const [editForm, setEditForm] = useState<MCPServerConfig | null>(null)
50
+ const [dirty, setDirty] = useState(false)
51
+
52
+ // 传输类型 + JSON 编辑器状态
53
+ const [transportType, setTransportType] = useState<'stdio' | 'streamableHttp'>('stdio')
54
+ const [jsonText, setJsonText] = useState(STDIO_TEMPLATE)
55
+ const [jsonError, setJsonError] = useState<string | null>(null)
56
+
57
+ const loadAll = useCallback(async () => {
58
+ const list = await window.electronAPI.getMCPServers()
59
+ setServers(list)
60
+ if (!selectedId && list.length > 0) {
61
+ selectServer(list[0])
62
+ }
63
+ }, [selectedId])
64
+
65
+ useEffect(() => {
66
+ if (open) loadAll()
67
+ }, [open]) // eslint-disable-line react-hooks/exhaustive-deps
68
+
69
+ const selectServer = (server: MCPServerConfig) => {
70
+ setSelectedId(server.id)
71
+ setEditForm({ ...server })
72
+ setTransportType(server.transport)
73
+ setJsonText(configToJson(server))
74
+ setJsonError(null)
75
+ setDirty(false)
76
+ }
77
+
78
+ const handleSelect = (id: string) => {
79
+ const s = servers.find((srv) => srv.id === id)
80
+ if (s) selectServer(s)
81
+ }
82
+
83
+ const handleTransportTypeChange = (type: 'stdio' | 'streamableHttp') => {
84
+ setTransportType(type)
85
+ const template = type === 'stdio' ? STDIO_TEMPLATE : HTTP_TEMPLATE
86
+ setJsonText(template)
87
+ setJsonError(null)
88
+ if (editForm) {
89
+ const base = { id: editForm.id, name: editForm.name, enabled: editForm.enabled }
90
+ setEditForm(
91
+ type === 'stdio'
92
+ ? { ...base, transport: 'stdio', command: '', args: [], env: {} }
93
+ : { ...base, transport: 'streamableHttp', url: '' },
94
+ )
95
+ setDirty(true)
96
+ }
97
+ }
98
+
99
+ const handleJsonChange = (text: string) => {
100
+ setJsonText(text)
101
+ setDirty(true)
102
+ try {
103
+ const parsed = JSON.parse(text)
104
+ setJsonError(null)
105
+ // 自动检测传输类型
106
+ if (parsed.url && !parsed.command) {
107
+ setTransportType('streamableHttp')
108
+ } else if (parsed.command && !parsed.url) {
109
+ setTransportType('stdio')
110
+ }
111
+ } catch (e) {
112
+ setJsonError(`JSON 格式错误: ${(e as Error).message}`)
113
+ }
114
+ }
115
+
116
+ const handleNameChange = (name: string) => {
117
+ if (!editForm) return
118
+ setEditForm({ ...editForm, name } as MCPServerConfig)
119
+ setDirty(true)
120
+ }
121
+
122
+ const handleEnabledChange = (enabled: boolean) => {
123
+ if (!editForm) return
124
+ setEditForm({ ...editForm, enabled } as MCPServerConfig)
125
+ setDirty(true)
126
+ }
127
+
128
+ const handleSave = async () => {
129
+ if (!editForm) return
130
+ if (!editForm.name.trim()) {
131
+ toast.warning('请输入服务器名称')
132
+ return
133
+ }
134
+
135
+ let parsed: Record<string, unknown>
136
+ try {
137
+ parsed = JSON.parse(jsonText)
138
+ } catch (e) {
139
+ toast.error(`JSON 格式错误: ${(e as Error).message}`)
140
+ return
141
+ }
142
+
143
+ let serverConfig: MCPServerConfig
144
+ const base = { id: editForm.id, name: editForm.name, enabled: editForm.enabled }
145
+
146
+ if (transportType === 'streamableHttp') {
147
+ const url = parsed.url
148
+ if (typeof url !== 'string' || !url.trim()) {
149
+ toast.warning('请输入服务器 URL')
150
+ return
151
+ }
152
+ try {
153
+ new URL(url)
154
+ } catch {
155
+ toast.warning('URL 格式无效')
156
+ return
157
+ }
158
+ serverConfig = {
159
+ ...base,
160
+ transport: 'streamableHttp',
161
+ url: (url as string).trim(),
162
+ headers: (parsed.headers as Record<string, string>) || undefined,
163
+ }
164
+ } else {
165
+ const command = parsed.command
166
+ if (typeof command !== 'string' || !command.trim()) {
167
+ toast.warning('请输入启动命令')
168
+ return
169
+ }
170
+ serverConfig = {
171
+ ...base,
172
+ transport: 'stdio',
173
+ command: (command as string).trim(),
174
+ args: Array.isArray(parsed.args) ? parsed.args.map(String) : [],
175
+ env: (parsed.env as Record<string, string>) || {},
176
+ }
177
+ }
178
+
179
+ await window.electronAPI.saveMCPServer(serverConfig)
180
+ toast.success('MCP 服务器已保存')
181
+ setDirty(false)
182
+ const list = await window.electronAPI.getMCPServers()
183
+ setServers(list)
184
+ }
185
+
186
+ const handleDelete = async () => {
187
+ if (!editForm) return
188
+ await window.electronAPI.deleteMCPServer(editForm.id)
189
+ toast.success('MCP 服务器已删除')
190
+ setSelectedId(null)
191
+ setEditForm(null)
192
+ setDirty(false)
193
+ const list = await window.electronAPI.getMCPServers()
194
+ setServers(list)
195
+ if (list.length > 0) selectServer(list[0])
196
+ }
197
+
198
+ const handleCreate = () => {
199
+ const id = uuidv4()
200
+ const newServer: MCPServerConfig = {
201
+ id,
202
+ name: '',
203
+ enabled: true,
204
+ transport: 'stdio',
205
+ command: '',
206
+ args: [],
207
+ env: {},
208
+ }
209
+ setSelectedId(id)
210
+ setEditForm(newServer)
211
+ setTransportType('stdio')
212
+ setJsonText(STDIO_TEMPLATE)
213
+ setJsonError(null)
214
+ setDirty(true)
215
+ }
216
+
217
+ return (
218
+ <Modal
219
+ title="MCP 服务器管理"
220
+ open={open}
221
+ onClose={onClose}
222
+ width={800}
223
+ >
224
+ {/* Negate modal body padding so sidebar border runs edge-to-edge */}
225
+ <div style={{ margin: '-16px -24px', display: 'flex', height: 480, overflow: 'hidden' }}>
226
+ {/* Left: server list */}
227
+ <div
228
+ style={{
229
+ width: 200,
230
+ borderRight: '1px solid var(--color-border)',
231
+ overflow: 'auto',
232
+ flexShrink: 0,
233
+ }}
234
+ >
235
+ <div style={{ padding: '8px 12px' }}>
236
+ <Button
237
+ variant="dashed"
238
+ icon={<IconPlus size={13} />}
239
+ block
240
+ size="sm"
241
+ onClick={handleCreate}
242
+ >
243
+ 添加服务器
244
+ </Button>
245
+ </div>
246
+ <div>
247
+ {servers.map((item) => (
248
+ <div
249
+ key={item.id}
250
+ onClick={() => handleSelect(item.id)}
251
+ style={{
252
+ cursor: 'pointer',
253
+ padding: '8px 12px',
254
+ background: selectedId === item.id ? 'var(--color-accent-bg)' : 'transparent',
255
+ borderLeft: selectedId === item.id ? '3px solid var(--color-accent)' : '3px solid transparent',
256
+ }}
257
+ >
258
+ <div style={{ minWidth: 0, width: '100%' }}>
259
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }}>
260
+ <IconApi
261
+ size={12}
262
+ style={{ color: item.enabled ? 'var(--color-success)' : 'var(--text-muted)', flexShrink: 0 }}
263
+ />
264
+ <span style={{
265
+ fontSize: 'var(--font-size-base)',
266
+ flex: 1,
267
+ overflow: 'hidden',
268
+ textOverflow: 'ellipsis',
269
+ whiteSpace: 'nowrap',
270
+ color: 'var(--text-primary)',
271
+ }}>
272
+ {item.name || '未命名'}
273
+ </span>
274
+ </div>
275
+ <span style={{
276
+ fontSize: 'var(--font-size-xs)',
277
+ color: 'var(--text-secondary)',
278
+ overflow: 'hidden',
279
+ textOverflow: 'ellipsis',
280
+ whiteSpace: 'nowrap',
281
+ display: 'block',
282
+ }}>
283
+ {getServerDescription(item)}
284
+ </span>
285
+ </div>
286
+ </div>
287
+ ))}
288
+ </div>
289
+ </div>
290
+
291
+ {/* Right: edit panel */}
292
+ <div
293
+ style={{
294
+ flex: 1,
295
+ padding: 16,
296
+ overflow: 'auto',
297
+ display: 'flex',
298
+ flexDirection: 'column',
299
+ gap: 12,
300
+ }}
301
+ >
302
+ {editForm ? (
303
+ <>
304
+ {/* 名称 + 启用 */}
305
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
306
+ <div style={{ flex: 1 }}>
307
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
308
+ 名称
309
+ </span>
310
+ <Input
311
+ value={editForm.name}
312
+ onChange={(e) => handleNameChange(e.target.value)}
313
+ placeholder="如:文件系统 / 远程搜索"
314
+ inputSize="sm"
315
+ />
316
+ </div>
317
+ <div style={{ paddingTop: 18 }}>
318
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
319
+ <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)' }}>启用</span>
320
+ <Switch
321
+ checked={editForm.enabled}
322
+ onChange={handleEnabledChange}
323
+ />
324
+ </div>
325
+ </div>
326
+ </div>
327
+
328
+ {/* 传输类型选择 */}
329
+ <div>
330
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
331
+ 传输类型
332
+ </span>
333
+ <div style={{
334
+ display: 'flex',
335
+ background: 'var(--color-surface)',
336
+ borderRadius: 6,
337
+ padding: 2,
338
+ width: 'fit-content',
339
+ }}>
340
+ {[
341
+ { label: '本地命令 (stdio)', value: 'stdio' },
342
+ { label: '远程服务 (HTTP)', value: 'streamableHttp' },
343
+ ].map(opt => (
344
+ <button
345
+ key={opt.value}
346
+ type="button"
347
+ onClick={() => handleTransportTypeChange(opt.value as 'stdio' | 'streamableHttp')}
348
+ style={{
349
+ padding: '3px 10px',
350
+ fontSize: 'var(--font-size-sm)',
351
+ border: 'none',
352
+ borderRadius: 4,
353
+ cursor: 'pointer',
354
+ background: transportType === opt.value ? 'var(--color-accent)' : 'transparent',
355
+ color: transportType === opt.value ? 'var(--color-base)' : 'var(--text-secondary)',
356
+ transition: 'all 0.15s',
357
+ }}
358
+ >
359
+ {opt.label}
360
+ </button>
361
+ ))}
362
+ </div>
363
+ </div>
364
+
365
+ {/* JSON 编辑器 */}
366
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
367
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
368
+ 配置 (JSON)
369
+ </span>
370
+ <TextArea
371
+ value={jsonText}
372
+ onChange={(e) => handleJsonChange(e.target.value)}
373
+ autoSize={{ minRows: 6, maxRows: 12 }}
374
+ style={{
375
+ fontSize: 'var(--font-size-sm)',
376
+ fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, monospace",
377
+ lineHeight: '1.6',
378
+ ...(jsonError ? { borderColor: 'var(--color-error)' } : {}),
379
+ }}
380
+ placeholder={
381
+ transportType === 'stdio'
382
+ ? '{\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-xxx"],\n "env": {}\n}'
383
+ : '{\n "url": "https://example.com/mcp",\n "headers": {}\n}'
384
+ }
385
+ />
386
+ {jsonError && (
387
+ <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-error)', marginTop: 2 }}>
388
+ {jsonError}
389
+ </span>
390
+ )}
391
+ </div>
392
+
393
+ {/* 操作按钮 */}
394
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 'auto' }}>
395
+ <Popconfirm title="确定删除该服务器?" onConfirm={handleDelete} okText="确定" cancelText="取消">
396
+ <Button size="sm" variant="danger" icon={<IconDelete size={13} />}>
397
+ 删除
398
+ </Button>
399
+ </Popconfirm>
400
+ <Button
401
+ variant="primary"
402
+ size="sm"
403
+ icon={<IconSave size={13} />}
404
+ onClick={handleSave}
405
+ disabled={!dirty || !!jsonError}
406
+ >
407
+ 保存
408
+ </Button>
409
+ </div>
410
+ </>
411
+ ) : (
412
+ <div
413
+ style={{
414
+ flex: 1,
415
+ display: 'flex',
416
+ alignItems: 'center',
417
+ justifyContent: 'center',
418
+ }}
419
+ >
420
+ <span style={{ color: 'var(--text-secondary)' }}>选择或添加 MCP 服务器</span>
421
+ </div>
422
+ )}
423
+ </div>
424
+ </div>
425
+ </Modal>
426
+ )
427
+ }
@@ -0,0 +1,254 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { Modal, Button, Input, TextArea, Tag, Popconfirm, useToast } from '../ui'
3
+ import { IconPlus, IconUndo, IconDelete, IconSave } from '../ui/Icons'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import type { PromptTemplate } from '@shared/types'
6
+
7
+ interface Props {
8
+ open: boolean
9
+ onClose: () => void
10
+ }
11
+
12
+ export default function PromptTemplateModal({ open, onClose }: Props) {
13
+ const toast = useToast()
14
+ const [templates, setTemplates] = useState<PromptTemplate[]>([])
15
+ const [selectedId, setSelectedId] = useState<string | null>(null)
16
+ const [editForm, setEditForm] = useState<PromptTemplate | null>(null)
17
+ const [dirty, setDirty] = useState(false)
18
+
19
+ const loadAll = useCallback(async () => {
20
+ const list = await window.electronAPI.getPromptTemplates()
21
+ setTemplates(list)
22
+ // Auto-select first if nothing selected
23
+ if (!selectedId && list.length > 0) {
24
+ setSelectedId(list[0].id)
25
+ setEditForm({ ...list[0] })
26
+ }
27
+ }, [selectedId])
28
+
29
+ useEffect(() => {
30
+ if (open) loadAll()
31
+ }, [open]) // eslint-disable-line react-hooks/exhaustive-deps
32
+
33
+ const handleSelect = (id: string) => {
34
+ const t = templates.find((tpl) => tpl.id === id)
35
+ if (!t) return
36
+ setSelectedId(id)
37
+ setEditForm({ ...t })
38
+ setDirty(false)
39
+ }
40
+
41
+ const handleFieldChange = (field: keyof PromptTemplate, value: string) => {
42
+ if (!editForm) return
43
+ setEditForm({ ...editForm, [field]: value })
44
+ setDirty(true)
45
+ }
46
+
47
+ const handleSave = async () => {
48
+ if (!editForm) return
49
+ await window.electronAPI.savePromptTemplate(editForm)
50
+ toast.success('模板已保存')
51
+ setDirty(false)
52
+ await loadAll()
53
+ }
54
+
55
+ const handleReset = async () => {
56
+ if (!editForm || !editForm.isBuiltin) return
57
+ await window.electronAPI.resetPromptTemplate(editForm.id)
58
+ toast.success('已恢复默认')
59
+ const list = await window.electronAPI.getPromptTemplates()
60
+ setTemplates(list)
61
+ const restored = list.find((t) => t.id === editForm.id)
62
+ if (restored) {
63
+ setEditForm({ ...restored })
64
+ setDirty(false)
65
+ }
66
+ }
67
+
68
+ const handleDelete = async () => {
69
+ if (!editForm || editForm.isBuiltin) return
70
+ await window.electronAPI.deletePromptTemplate(editForm.id)
71
+ toast.success('模板已删除')
72
+ setSelectedId(null)
73
+ setEditForm(null)
74
+ setDirty(false)
75
+ const list = await window.electronAPI.getPromptTemplates()
76
+ setTemplates(list)
77
+ if (list.length > 0) {
78
+ setSelectedId(list[0].id)
79
+ setEditForm({ ...list[0] })
80
+ }
81
+ }
82
+
83
+ const handleCreate = () => {
84
+ const newTemplate: PromptTemplate = {
85
+ id: uuidv4(),
86
+ name: '新模板',
87
+ description: '',
88
+ systemPrompt: '你是一位网站协议分析专家。你的任务是分析用户在网站上的操作过程中产生的HTTP请求、JS调用和存储变化,识别其业务场景,并生成结构化的协议分析报告。Be precise and technical. Output in Chinese (Simplified).',
89
+ requirements: '',
90
+ isBuiltin: false,
91
+ isModified: false,
92
+ }
93
+ setSelectedId(newTemplate.id)
94
+ setEditForm(newTemplate)
95
+ setDirty(true)
96
+ }
97
+
98
+ return (
99
+ <Modal
100
+ title="提示词模板管理"
101
+ open={open}
102
+ onClose={onClose}
103
+ width={800}
104
+ >
105
+ {/* Negate modal body padding so sidebar border runs edge-to-edge */}
106
+ <div style={{ margin: '-16px -24px', display: 'flex', height: 520, overflow: 'hidden' }}>
107
+ {/* Left: template list */}
108
+ <div style={{
109
+ width: 200,
110
+ borderRight: '1px solid var(--color-border)',
111
+ overflow: 'auto',
112
+ flexShrink: 0,
113
+ }}>
114
+ <div style={{ padding: '8px 12px' }}>
115
+ <Button
116
+ variant="dashed"
117
+ icon={<IconPlus size={13} />}
118
+ block
119
+ size="sm"
120
+ onClick={handleCreate}
121
+ >
122
+ 新建模板
123
+ </Button>
124
+ </div>
125
+ <div>
126
+ {templates.map((item) => (
127
+ <div
128
+ key={item.id}
129
+ onClick={() => handleSelect(item.id)}
130
+ style={{
131
+ cursor: 'pointer',
132
+ padding: '8px 12px',
133
+ background: selectedId === item.id ? 'var(--color-accent-bg)' : 'transparent',
134
+ borderLeft: selectedId === item.id ? '3px solid var(--color-accent)' : '3px solid transparent',
135
+ }}
136
+ >
137
+ <div style={{ minWidth: 0, width: '100%' }}>
138
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }}>
139
+ <span style={{
140
+ fontSize: 'var(--font-size-base)',
141
+ flex: 1,
142
+ overflow: 'hidden',
143
+ textOverflow: 'ellipsis',
144
+ whiteSpace: 'nowrap',
145
+ color: 'var(--text-primary)',
146
+ }}>
147
+ {item.name}
148
+ </span>
149
+ {item.isBuiltin && (
150
+ <Tag style={{ fontSize: 'var(--font-size-2xs)', lineHeight: '16px', padding: '0 4px' }}>内置</Tag>
151
+ )}
152
+ {item.isModified && (
153
+ <Tag color="orange" style={{ fontSize: 'var(--font-size-2xs)', lineHeight: '16px', padding: '0 4px' }}>已改</Tag>
154
+ )}
155
+ </div>
156
+ <span style={{
157
+ fontSize: 'var(--font-size-xs)',
158
+ color: 'var(--text-secondary)',
159
+ overflow: 'hidden',
160
+ textOverflow: 'ellipsis',
161
+ whiteSpace: 'nowrap',
162
+ display: 'block',
163
+ }}>
164
+ {item.description}
165
+ </span>
166
+ </div>
167
+ </div>
168
+ ))}
169
+ </div>
170
+ </div>
171
+
172
+ {/* Right: edit form */}
173
+ <div style={{ flex: 1, padding: 16, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 12 }}>
174
+ {editForm ? (
175
+ <>
176
+ <div style={{ display: 'flex', gap: 8 }}>
177
+ <div style={{ flex: 1 }}>
178
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
179
+ 名称
180
+ </span>
181
+ <Input
182
+ value={editForm.name}
183
+ onChange={(e) => handleFieldChange('name', e.target.value)}
184
+ inputSize="sm"
185
+ disabled={editForm.isBuiltin}
186
+ />
187
+ </div>
188
+ <div style={{ flex: 2 }}>
189
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
190
+ 描述
191
+ </span>
192
+ <Input
193
+ value={editForm.description}
194
+ onChange={(e) => handleFieldChange('description', e.target.value)}
195
+ inputSize="sm"
196
+ />
197
+ </div>
198
+ </div>
199
+
200
+ <div>
201
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
202
+ System Prompt
203
+ </span>
204
+ <TextArea
205
+ value={editForm.systemPrompt}
206
+ onChange={(e) => handleFieldChange('systemPrompt', e.target.value)}
207
+ autoSize={{ minRows: 3, maxRows: 6 }}
208
+ style={{ fontSize: 'var(--font-size-sm)' }}
209
+ />
210
+ </div>
211
+
212
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
213
+ <span style={{ display: 'block', fontSize: 'var(--font-size-sm)', color: 'var(--text-secondary)', marginBottom: 4 }}>
214
+ 分析要求
215
+ </span>
216
+ <TextArea
217
+ value={editForm.requirements}
218
+ onChange={(e) => handleFieldChange('requirements', e.target.value)}
219
+ style={{ flex: 1, fontSize: 'var(--font-size-sm)', resize: 'none' }}
220
+ />
221
+ </div>
222
+
223
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
224
+ {editForm.isBuiltin && editForm.isModified && (
225
+ <Popconfirm title="确定恢复默认?" onConfirm={handleReset} okText="确定" cancelText="取消">
226
+ <Button size="sm" icon={<IconUndo size={13} />}>恢复默认</Button>
227
+ </Popconfirm>
228
+ )}
229
+ {!editForm.isBuiltin && (
230
+ <Popconfirm title="确定删除该模板?" onConfirm={handleDelete} okText="确定" cancelText="取消">
231
+ <Button size="sm" variant="danger" icon={<IconDelete size={13} />}>删除</Button>
232
+ </Popconfirm>
233
+ )}
234
+ <Button
235
+ variant="primary"
236
+ size="sm"
237
+ icon={<IconSave size={13} />}
238
+ onClick={handleSave}
239
+ disabled={!dirty}
240
+ >
241
+ 保存
242
+ </Button>
243
+ </div>
244
+ </>
245
+ ) : (
246
+ <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
247
+ <span style={{ color: 'var(--text-secondary)' }}>选择或新建模板</span>
248
+ </div>
249
+ )}
250
+ </div>
251
+ </div>
252
+ </Modal>
253
+ )
254
+ }