deepspider 0.1.0

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 (261) hide show
  1. package/.claude/agents/check.md +122 -0
  2. package/.claude/agents/debug.md +106 -0
  3. package/.claude/agents/dispatch.md +214 -0
  4. package/.claude/agents/implement.md +96 -0
  5. package/.claude/agents/plan.md +396 -0
  6. package/.claude/agents/research.md +120 -0
  7. package/.claude/commands/evolve/merge.md +80 -0
  8. package/.claude/commands/trellis/before-backend-dev.md +13 -0
  9. package/.claude/commands/trellis/before-frontend-dev.md +13 -0
  10. package/.claude/commands/trellis/break-loop.md +107 -0
  11. package/.claude/commands/trellis/check-backend.md +13 -0
  12. package/.claude/commands/trellis/check-cross-layer.md +153 -0
  13. package/.claude/commands/trellis/check-frontend.md +13 -0
  14. package/.claude/commands/trellis/create-command.md +154 -0
  15. package/.claude/commands/trellis/finish-work.md +129 -0
  16. package/.claude/commands/trellis/integrate-skill.md +219 -0
  17. package/.claude/commands/trellis/onboard.md +358 -0
  18. package/.claude/commands/trellis/parallel.md +193 -0
  19. package/.claude/commands/trellis/record-session.md +62 -0
  20. package/.claude/commands/trellis/start.md +280 -0
  21. package/.claude/commands/trellis/update-spec.md +213 -0
  22. package/.claude/hooks/inject-subagent-context.py +758 -0
  23. package/.claude/hooks/ralph-loop.py +374 -0
  24. package/.claude/hooks/session-start.py +126 -0
  25. package/.claude/settings.json +41 -0
  26. package/.claude/skills/deepagents-guide/SKILL.md +428 -0
  27. package/.cursor/commands/trellis-before-backend-dev.md +13 -0
  28. package/.cursor/commands/trellis-before-frontend-dev.md +13 -0
  29. package/.cursor/commands/trellis-break-loop.md +107 -0
  30. package/.cursor/commands/trellis-check-backend.md +13 -0
  31. package/.cursor/commands/trellis-check-cross-layer.md +153 -0
  32. package/.cursor/commands/trellis-check-frontend.md +13 -0
  33. package/.cursor/commands/trellis-create-command.md +154 -0
  34. package/.cursor/commands/trellis-finish-work.md +129 -0
  35. package/.cursor/commands/trellis-integrate-skill.md +219 -0
  36. package/.cursor/commands/trellis-onboard.md +358 -0
  37. package/.cursor/commands/trellis-record-session.md +62 -0
  38. package/.cursor/commands/trellis-start.md +156 -0
  39. package/.cursor/commands/trellis-update-spec.md +213 -0
  40. package/.env.example +11 -0
  41. package/.husky/pre-commit +1 -0
  42. package/.mcp.json +8 -0
  43. package/.trellis/.template-hashes.json +65 -0
  44. package/.trellis/.version +1 -0
  45. package/.trellis/scripts/add-session.sh +384 -0
  46. package/.trellis/scripts/common/developer.sh +129 -0
  47. package/.trellis/scripts/common/git-context.sh +263 -0
  48. package/.trellis/scripts/common/paths.sh +208 -0
  49. package/.trellis/scripts/common/phase.sh +150 -0
  50. package/.trellis/scripts/common/registry.sh +247 -0
  51. package/.trellis/scripts/common/task-queue.sh +142 -0
  52. package/.trellis/scripts/common/task-utils.sh +151 -0
  53. package/.trellis/scripts/common/worktree.sh +128 -0
  54. package/.trellis/scripts/create-bootstrap.sh +299 -0
  55. package/.trellis/scripts/get-context.sh +7 -0
  56. package/.trellis/scripts/get-developer.sh +15 -0
  57. package/.trellis/scripts/init-developer.sh +34 -0
  58. package/.trellis/scripts/multi-agent/cleanup.sh +396 -0
  59. package/.trellis/scripts/multi-agent/create-pr.sh +241 -0
  60. package/.trellis/scripts/multi-agent/plan.sh +207 -0
  61. package/.trellis/scripts/multi-agent/start.sh +310 -0
  62. package/.trellis/scripts/multi-agent/status.sh +828 -0
  63. package/.trellis/scripts/task.sh +1118 -0
  64. package/.trellis/spec/backend/deepagents-guide.md +337 -0
  65. package/.trellis/spec/backend/directory-structure.md +126 -0
  66. package/.trellis/spec/backend/examples/skills/deepagents-guide/README.md +11 -0
  67. package/.trellis/spec/backend/examples/skills/deepagents-guide/agent.js.template +20 -0
  68. package/.trellis/spec/backend/examples/skills/deepagents-guide/skills-config.js.template +13 -0
  69. package/.trellis/spec/backend/examples/skills/deepagents-guide/subagent.js.template +19 -0
  70. package/.trellis/spec/backend/hook-guidelines.md +178 -0
  71. package/.trellis/spec/backend/index.md +36 -0
  72. package/.trellis/spec/backend/quality-guidelines.md +201 -0
  73. package/.trellis/spec/backend/state-management.md +76 -0
  74. package/.trellis/spec/backend/tool-guidelines.md +144 -0
  75. package/.trellis/spec/backend/type-safety.md +71 -0
  76. package/.trellis/spec/guides/code-reuse-thinking-guide.md +92 -0
  77. package/.trellis/spec/guides/cross-layer-thinking-guide.md +94 -0
  78. package/.trellis/spec/guides/index.md +79 -0
  79. package/.trellis/tasks/archive/02-02-evolving-skills/prd.md +61 -0
  80. package/.trellis/tasks/archive/02-02-evolving-skills/task.json +29 -0
  81. package/.trellis/tasks/archive/2026-02/00-bootstrap-guidelines/prd.md +86 -0
  82. package/.trellis/tasks/archive/2026-02/00-bootstrap-guidelines/task.json +27 -0
  83. package/.trellis/tasks/archive/2026-02/02-02-skills-system/check.jsonl +3 -0
  84. package/.trellis/tasks/archive/2026-02/02-02-skills-system/debug.jsonl +2 -0
  85. package/.trellis/tasks/archive/2026-02/02-02-skills-system/implement.jsonl +5 -0
  86. package/.trellis/tasks/archive/2026-02/02-02-skills-system/prd.md +33 -0
  87. package/.trellis/tasks/archive/2026-02/02-02-skills-system/task.json +41 -0
  88. package/.trellis/workflow.md +407 -0
  89. package/.trellis/workspace/index.md +123 -0
  90. package/.trellis/workspace/pony/index.md +40 -0
  91. package/.trellis/workspace/pony/journal-1.md +7 -0
  92. package/.trellis/worktree.yaml +47 -0
  93. package/AGENTS.md +18 -0
  94. package/CLAUDE.md +292 -0
  95. package/README.md +134 -0
  96. package/agents/deepspider.md +142 -0
  97. package/docs/DEBUG.md +42 -0
  98. package/docs/GUIDE.md +334 -0
  99. package/docs/PROMPT.md +60 -0
  100. package/docs/USAGE.md +226 -0
  101. package/eslint.config.js +51 -0
  102. package/package.json +78 -0
  103. package/requirements-crypto.txt +14 -0
  104. package/src/agent/index.js +97 -0
  105. package/src/agent/logger.js +164 -0
  106. package/src/agent/middleware/filterTools.js +64 -0
  107. package/src/agent/middleware/report.js +79 -0
  108. package/src/agent/prompts/system.js +315 -0
  109. package/src/agent/run.js +575 -0
  110. package/src/agent/skills/anti-detect/SKILL.md +28 -0
  111. package/src/agent/skills/anti-detect/evolved.md +12 -0
  112. package/src/agent/skills/captcha/SKILL.md +37 -0
  113. package/src/agent/skills/captcha/evolved.md +12 -0
  114. package/src/agent/skills/config.js +30 -0
  115. package/src/agent/skills/crawler/SKILL.md +9 -0
  116. package/src/agent/skills/crawler/evolved.md +16 -0
  117. package/src/agent/skills/dynamic-analysis/SKILL.md +91 -0
  118. package/src/agent/skills/dynamic-analysis/evolved.md +12 -0
  119. package/src/agent/skills/env/SKILL.md +72 -0
  120. package/src/agent/skills/env/evolved.md +12 -0
  121. package/src/agent/skills/evolve.js +79 -0
  122. package/src/agent/skills/general/SKILL.md +12 -0
  123. package/src/agent/skills/general/evolved.md +12 -0
  124. package/src/agent/skills/js2python/SKILL.md +30 -0
  125. package/src/agent/skills/js2python/evolved.md +13 -0
  126. package/src/agent/skills/report/SKILL.md +21 -0
  127. package/src/agent/skills/report/evolved.md +12 -0
  128. package/src/agent/skills/sandbox/SKILL.md +22 -0
  129. package/src/agent/skills/sandbox/evolved.md +16 -0
  130. package/src/agent/skills/static-analysis/SKILL.md +93 -0
  131. package/src/agent/skills/static-analysis/evolved.md +12 -0
  132. package/src/agent/skills/xpath/SKILL.md +119 -0
  133. package/src/agent/subagents/anti-detect.js +45 -0
  134. package/src/agent/subagents/captcha.js +51 -0
  135. package/src/agent/subagents/crawler.js +138 -0
  136. package/src/agent/subagents/dynamic.js +64 -0
  137. package/src/agent/subagents/env-agent.js +82 -0
  138. package/src/agent/subagents/index.js +37 -0
  139. package/src/agent/subagents/js2python.js +72 -0
  140. package/src/agent/subagents/sandbox.js +55 -0
  141. package/src/agent/subagents/static.js +66 -0
  142. package/src/agent/tools/analysis.js +135 -0
  143. package/src/agent/tools/analyzer.js +85 -0
  144. package/src/agent/tools/anti-detect.js +89 -0
  145. package/src/agent/tools/antidebug.js +64 -0
  146. package/src/agent/tools/async.js +43 -0
  147. package/src/agent/tools/browser.js +324 -0
  148. package/src/agent/tools/captcha.js +223 -0
  149. package/src/agent/tools/capture.js +179 -0
  150. package/src/agent/tools/correlate.js +303 -0
  151. package/src/agent/tools/crawler.js +116 -0
  152. package/src/agent/tools/cryptohook.js +80 -0
  153. package/src/agent/tools/debug.js +246 -0
  154. package/src/agent/tools/deobfuscator.js +90 -0
  155. package/src/agent/tools/env.js +83 -0
  156. package/src/agent/tools/envdump.js +92 -0
  157. package/src/agent/tools/evolve.js +164 -0
  158. package/src/agent/tools/extract.js +114 -0
  159. package/src/agent/tools/extractor.js +54 -0
  160. package/src/agent/tools/file.js +224 -0
  161. package/src/agent/tools/hook.js +84 -0
  162. package/src/agent/tools/hookManager.js +178 -0
  163. package/src/agent/tools/index.js +137 -0
  164. package/src/agent/tools/nodejs.js +101 -0
  165. package/src/agent/tools/patch.js +46 -0
  166. package/src/agent/tools/preprocess.js +71 -0
  167. package/src/agent/tools/profile.js +122 -0
  168. package/src/agent/tools/python.js +627 -0
  169. package/src/agent/tools/report.js +124 -0
  170. package/src/agent/tools/runtime.js +132 -0
  171. package/src/agent/tools/sandbox.js +79 -0
  172. package/src/agent/tools/store.js +73 -0
  173. package/src/agent/tools/trace.js +74 -0
  174. package/src/agent/tools/tracing.js +201 -0
  175. package/src/agent/tools/utils.js +51 -0
  176. package/src/agent/tools/verify.js +184 -0
  177. package/src/agent/tools/webcrack.js +109 -0
  178. package/src/analyzer/ASTAnalyzer.js +387 -0
  179. package/src/analyzer/CallStackAnalyzer.js +379 -0
  180. package/src/analyzer/Deobfuscator.js +289 -0
  181. package/src/analyzer/EncryptionAnalyzer.js +99 -0
  182. package/src/analyzer/index.js +22 -0
  183. package/src/browser/EnvBridge.js +186 -0
  184. package/src/browser/cdp.js +168 -0
  185. package/src/browser/client.js +197 -0
  186. package/src/browser/collector.js +444 -0
  187. package/src/browser/collectors/RequestCryptoLinker.js +109 -0
  188. package/src/browser/collectors/ResponseSearcher.js +107 -0
  189. package/src/browser/collectors/ScriptCollector.js +158 -0
  190. package/src/browser/collectors/index.js +26 -0
  191. package/src/browser/defaultHooks.js +932 -0
  192. package/src/browser/hooks/crypto.js +55 -0
  193. package/src/browser/hooks/index.js +64 -0
  194. package/src/browser/hooks/native.js +9 -0
  195. package/src/browser/hooks/network.js +33 -0
  196. package/src/browser/index.js +42 -0
  197. package/src/browser/interceptors/NetworkInterceptor.js +116 -0
  198. package/src/browser/interceptors/ScriptInterceptor.js +76 -0
  199. package/src/browser/interceptors/index.js +6 -0
  200. package/src/browser/ui/analysisPanel.js +1782 -0
  201. package/src/browser/ui/confirmDialog.js +158 -0
  202. package/src/browser/ui/panel.html +152 -0
  203. package/src/browser/ui/selector.js +170 -0
  204. package/src/config/index.js +5 -0
  205. package/src/config/paths.js +71 -0
  206. package/src/config/patterns/crypto.js +36 -0
  207. package/src/config/profiles/chrome.json +71 -0
  208. package/src/config/profiles/firefox.json +44 -0
  209. package/src/config/profiles/safari.json +38 -0
  210. package/src/core/EnvMonitor.js +200 -0
  211. package/src/core/PatchGenerator.js +278 -0
  212. package/src/core/Sandbox.js +181 -0
  213. package/src/env/AntiAntiDebug.js +111 -0
  214. package/src/env/AsyncHook.js +68 -0
  215. package/src/env/BrowserAPIList.js +265 -0
  216. package/src/env/CookieHook.js +48 -0
  217. package/src/env/CryptoHook.js +205 -0
  218. package/src/env/EnvCodeGenerator.js +157 -0
  219. package/src/env/EnvDumper.js +356 -0
  220. package/src/env/EnvExtractor.js +220 -0
  221. package/src/env/HookBase.js +618 -0
  222. package/src/env/NetworkHook.js +159 -0
  223. package/src/env/modules/bom/history.js +29 -0
  224. package/src/env/modules/bom/location.js +26 -0
  225. package/src/env/modules/bom/navigator.js +70 -0
  226. package/src/env/modules/bom/screen.js +26 -0
  227. package/src/env/modules/bom/storage.js +23 -0
  228. package/src/env/modules/dom/document.js +110 -0
  229. package/src/env/modules/dom/event.js +51 -0
  230. package/src/env/modules/index.js +34 -0
  231. package/src/env/modules/webapi/fetch.js +46 -0
  232. package/src/env/modules/webapi/url.js +47 -0
  233. package/src/env/modules/webapi/xhr.js +48 -0
  234. package/src/index.js +27 -0
  235. package/src/mcp/server.js +89 -0
  236. package/src/store/DataStore.js +708 -0
  237. package/src/store/Store.js +158 -0
  238. package/src/store/Validator.js +24 -0
  239. package/test/analyze.test.js +90 -0
  240. package/test/envdump.test.js +74 -0
  241. package/test/flow.test.js +90 -0
  242. package/test/hooks.test.js +138 -0
  243. package/test/plugin.test.js +35 -0
  244. package/test/refactor-full.test.js +30 -0
  245. package/test/refactor.test.js +21 -0
  246. package/test/samples/obfuscated.js +61 -0
  247. package/test/samples/original.js +66 -0
  248. package/test/samples/v10_eval_chain.js +52 -0
  249. package/test/samples/v11_bytecode_vm.js +81 -0
  250. package/test/samples/v12_polymorphic.js +69 -0
  251. package/test/samples/v1_ob_basic.js +98 -0
  252. package/test/samples/v2_ob_advanced.js +99 -0
  253. package/test/samples/v3_jjencode.js +77 -0
  254. package/test/samples/v4_aaencode.js +73 -0
  255. package/test/samples/v5_control_flow.js +86 -0
  256. package/test/samples/v6_string_encryption.js +71 -0
  257. package/test/samples/v7_jsvmp.js +83 -0
  258. package/test/samples/v8_anti_debug.js +79 -0
  259. package/test/samples/v9_proxy_trap.js +49 -0
  260. package/test/samples.test.js +96 -0
  261. package/test/webcrack.test.js +55 -0
@@ -0,0 +1,1782 @@
1
+ /**
2
+ * DeepSpider - 分析面板 UI
3
+ * 选择器模式 + 对话交互
4
+ */
5
+
6
+ export function getAnalysisPanelScript() {
7
+ return `
8
+ (function() {
9
+ const isTopWindow = window === window.top;
10
+ const deepspider = window.__deepspider__;
11
+
12
+ // ========== iframe 中的选择器逻辑 ==========
13
+ if (!isTopWindow) {
14
+ if (window.__deepspider_iframe_selector__) return;
15
+ window.__deepspider_iframe_selector__ = true;
16
+
17
+ let isSelectMode = false;
18
+ let currentElement = null;
19
+ let overlay = null;
20
+ let infoBox = null;
21
+
22
+ // 创建选择器覆盖层
23
+ function createOverlay() {
24
+ if (overlay) return;
25
+ const style = document.createElement('style');
26
+ style.id = 'deepspider-iframe-style';
27
+ style.textContent = \`
28
+ #deepspider-iframe-overlay {
29
+ position: fixed;
30
+ pointer-events: none;
31
+ border: 2px solid #4fc3f7;
32
+ background: rgba(79, 195, 247, 0.1);
33
+ z-index: 2147483646;
34
+ display: none;
35
+ }
36
+ #deepspider-iframe-info {
37
+ position: fixed;
38
+ background: #1e1e1e;
39
+ color: #4fc3f7;
40
+ padding: 4px 8px;
41
+ font-size: 11px;
42
+ border-radius: 3px;
43
+ z-index: 2147483647;
44
+ display: none;
45
+ pointer-events: none;
46
+ }
47
+ \`;
48
+ document.head.appendChild(style);
49
+
50
+ overlay = document.createElement('div');
51
+ overlay.id = 'deepspider-iframe-overlay';
52
+ document.body.appendChild(overlay);
53
+
54
+ infoBox = document.createElement('div');
55
+ infoBox.id = 'deepspider-iframe-info';
56
+ document.body.appendChild(infoBox);
57
+ }
58
+
59
+ function getXPath(el) {
60
+ if (!el) return '';
61
+ if (el.id) return '//*[@id="' + el.id + '"]';
62
+ const parts = [];
63
+ while (el && el.nodeType === 1) {
64
+ let idx = 1, sib = el.previousSibling;
65
+ while (sib) {
66
+ if (sib.nodeType === 1 && sib.tagName === el.tagName) idx++;
67
+ sib = sib.previousSibling;
68
+ }
69
+ parts.unshift(el.tagName.toLowerCase() + '[' + idx + ']');
70
+ el = el.parentNode;
71
+ }
72
+ return '/' + parts.join('/');
73
+ }
74
+
75
+ function onSelectMove(e) {
76
+ const target = document.elementFromPoint(e.clientX, e.clientY);
77
+ if (!target || target.id?.startsWith('deepspider-')) return;
78
+ currentElement = target;
79
+ const rect = target.getBoundingClientRect();
80
+ overlay.style.left = rect.left + 'px';
81
+ overlay.style.top = rect.top + 'px';
82
+ overlay.style.width = rect.width + 'px';
83
+ overlay.style.height = rect.height + 'px';
84
+ overlay.style.display = 'block';
85
+ const tag = target.tagName.toLowerCase();
86
+ const cls = target.className ? '.' + String(target.className).split(' ')[0] : '';
87
+ infoBox.textContent = '[iframe] ' + tag + cls;
88
+ infoBox.style.left = rect.left + 'px';
89
+ infoBox.style.top = Math.max(0, rect.top - 22) + 'px';
90
+ infoBox.style.display = 'block';
91
+ }
92
+
93
+ function onSelectClick(e) {
94
+ if (!currentElement) return;
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ const text = currentElement.innerText?.trim().slice(0, 500) || '';
98
+ const xpath = getXPath(currentElement);
99
+ stopSelectMode();
100
+
101
+ // 发送选中结果到顶层窗口
102
+ window.top.postMessage({
103
+ type: 'deepspider-iframe-selection',
104
+ text,
105
+ xpath,
106
+ iframeSrc: location.href
107
+ }, '*');
108
+ }
109
+
110
+ function onSelectKey(e) {
111
+ if (e.key === 'Escape') stopSelectMode();
112
+ }
113
+
114
+ function startSelectMode() {
115
+ if (isSelectMode) return;
116
+ createOverlay();
117
+ isSelectMode = true;
118
+ document.body.style.cursor = 'crosshair';
119
+ document.addEventListener('mousemove', onSelectMove, true);
120
+ document.addEventListener('click', onSelectClick, true);
121
+ document.addEventListener('keydown', onSelectKey, true);
122
+ }
123
+
124
+ function stopSelectMode() {
125
+ isSelectMode = false;
126
+ document.body.style.cursor = '';
127
+ if (overlay) overlay.style.display = 'none';
128
+ if (infoBox) infoBox.style.display = 'none';
129
+ currentElement = null;
130
+ document.removeEventListener('mousemove', onSelectMove, true);
131
+ document.removeEventListener('click', onSelectClick, true);
132
+ document.removeEventListener('keydown', onSelectKey, true);
133
+ }
134
+
135
+ // 监听来自顶层窗口的消息
136
+ window.addEventListener('message', (e) => {
137
+ if (e.data?.type === 'deepspider-start-select') {
138
+ startSelectMode();
139
+ } else if (e.data?.type === 'deepspider-stop-select') {
140
+ stopSelectMode();
141
+ }
142
+ });
143
+
144
+ return; // iframe 中只注入选择器,不创建面板
145
+ }
146
+
147
+ // ========== 顶层窗口的面板逻辑 ==========
148
+ // 检查 DOM 中是否已存在面板
149
+ if (document.getElementById('deepspider-panel')) return;
150
+ if (window.__deepspider_ui__) return;
151
+ window.__deepspider_ui__ = true;
152
+
153
+ if (!deepspider) {
154
+ console.error('[DeepSpider UI] 需要先加载 DeepSpider Hook');
155
+ return;
156
+ }
157
+
158
+ // 状态 - 从 sessionStorage 恢复消息
159
+ const STORAGE_KEY = 'deepspider_chat_messages';
160
+ const STAGES_STORAGE_KEY = 'deepspider_stages';
161
+ const CURRENT_STAGE_KEY = 'deepspider_current_stage';
162
+ try {
163
+ const saved = sessionStorage.getItem(STORAGE_KEY);
164
+ deepspider.chatMessages = saved ? JSON.parse(saved) : [];
165
+ } catch (e) {
166
+ deepspider.chatMessages = [];
167
+ }
168
+ // 阶段配置 - 支持多阶段爬取流程
169
+ try {
170
+ const savedStages = sessionStorage.getItem(STAGES_STORAGE_KEY);
171
+ deepspider.stages = savedStages ? JSON.parse(savedStages) : [
172
+ { name: 'list', fields: [], entry: null, pagination: null }
173
+ ];
174
+ } catch (e) {
175
+ deepspider.stages = [{ name: 'list', fields: [], entry: null, pagination: null }];
176
+ }
177
+ // 当前选中的阶段
178
+ try {
179
+ const savedCurrentStage = sessionStorage.getItem(CURRENT_STAGE_KEY);
180
+ deepspider.currentStageIndex = savedCurrentStage ? parseInt(savedCurrentStage) : 0;
181
+ } catch (e) {
182
+ deepspider.currentStageIndex = 0;
183
+ }
184
+ let isSelectMode = false;
185
+ let currentElement = null;
186
+
187
+ // 保存消息到 sessionStorage
188
+ function saveMessages() {
189
+ try {
190
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(deepspider.chatMessages));
191
+ } catch (e) {
192
+ console.warn('[DeepSpider] 保存消息失败:', e);
193
+ }
194
+ }
195
+
196
+ // 保存阶段配置到 sessionStorage
197
+ function saveStages() {
198
+ try {
199
+ sessionStorage.setItem(STAGES_STORAGE_KEY, JSON.stringify(deepspider.stages));
200
+ sessionStorage.setItem(CURRENT_STAGE_KEY, String(deepspider.currentStageIndex));
201
+ } catch (e) {
202
+ console.warn('[DeepSpider] 保存阶段配置失败:', e);
203
+ }
204
+ }
205
+
206
+ // 等待 DOM 加载完成后初始化 UI
207
+ function initUI() {
208
+ // 再次检查,防止异步情况下重复创建
209
+ if (document.getElementById('deepspider-panel')) return;
210
+ if (window.__deepspider_ui_init__) return;
211
+ window.__deepspider_ui_init__ = true;
212
+
213
+ // ========== 加载 marked.js ==========
214
+ if (!window.marked) {
215
+ const script = document.createElement('script');
216
+ script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
217
+ script.onload = () => {
218
+ window.marked.setOptions({ breaks: true, gfm: true });
219
+ console.log('[DeepSpider] marked.js loaded');
220
+ };
221
+ document.head.appendChild(script);
222
+ }
223
+
224
+ // ========== 样式 ==========
225
+ const style = document.createElement('style');
226
+ style.textContent = \`
227
+ #deepspider-panel {
228
+ position: fixed;
229
+ top: 20px; right: 20px;
230
+ width: 400px;
231
+ max-height: 70vh;
232
+ background: linear-gradient(180deg, #1e2530 0%, #161b22 100%);
233
+ border: 1px solid rgba(99, 179, 237, 0.2);
234
+ border-radius: 16px;
235
+ box-shadow: 0 16px 48px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05) inset;
236
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
237
+ font-size: 13px;
238
+ color: #c9d1d9;
239
+ z-index: 2147483640;
240
+ display: none;
241
+ flex-direction: column;
242
+ transition: opacity 0.2s, transform 0.2s;
243
+ backdrop-filter: blur(10px);
244
+ }
245
+ #deepspider-panel.visible { display: flex; animation: deepspider-fadein 0.25s ease-out; }
246
+ #deepspider-panel.minimized { max-height: 48px; overflow: hidden; }
247
+ #deepspider-panel.minimized .deepspider-messages,
248
+ #deepspider-panel.minimized .deepspider-input,
249
+ #deepspider-panel.minimized .deepspider-report-btn { display: none !important; }
250
+ @keyframes deepspider-fadein {
251
+ from { opacity: 0; transform: translateY(-12px) scale(0.98); }
252
+ to { opacity: 1; transform: translateY(0) scale(1); }
253
+ }
254
+ .deepspider-header {
255
+ padding: 14px 16px;
256
+ background: linear-gradient(180deg, rgba(99, 179, 237, 0.08) 0%, transparent 100%);
257
+ border-bottom: 1px solid rgba(99, 179, 237, 0.15);
258
+ border-radius: 16px 16px 0 0;
259
+ display: flex;
260
+ justify-content: space-between;
261
+ align-items: center;
262
+ cursor: move;
263
+ user-select: none;
264
+ }
265
+ .deepspider-header-title {
266
+ font-weight: 600;
267
+ font-size: 14px;
268
+ color: #63b3ed;
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 10px;
272
+ letter-spacing: 0.3px;
273
+ }
274
+ .deepspider-status {
275
+ width: 8px; height: 8px;
276
+ border-radius: 50%;
277
+ background: #48bb78;
278
+ box-shadow: 0 0 8px rgba(72, 187, 120, 0.6);
279
+ }
280
+ .deepspider-status.busy {
281
+ background: #ed8936;
282
+ box-shadow: 0 0 8px rgba(237, 137, 54, 0.6);
283
+ animation: deepspider-pulse 1.2s ease-in-out infinite;
284
+ }
285
+ @keyframes deepspider-pulse {
286
+ 0%, 100% { opacity: 1; transform: scale(1); }
287
+ 50% { opacity: 0.5; transform: scale(0.9); }
288
+ }
289
+ .deepspider-header-btns { display: flex; gap: 6px; }
290
+ .deepspider-header-btns button {
291
+ background: rgba(255,255,255,0.05);
292
+ border: 1px solid rgba(255,255,255,0.08);
293
+ color: #8b949e;
294
+ font-size: 15px;
295
+ width: 30px; height: 30px;
296
+ border-radius: 8px;
297
+ cursor: pointer;
298
+ transition: all 0.2s;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ }
303
+ .deepspider-header-btns button:hover { background: rgba(99, 179, 237, 0.15); color: #63b3ed; border-color: rgba(99, 179, 237, 0.3); }
304
+ .deepspider-header-btns button.active { background: linear-gradient(135deg, #63b3ed 0%, #4299e1 100%); color: #fff; border-color: transparent; box-shadow: 0 2px 8px rgba(99, 179, 237, 0.4); }
305
+ .deepspider-report-btn {
306
+ display: none;
307
+ margin: 12px 14px;
308
+ padding: 12px 16px;
309
+ background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
310
+ border: none;
311
+ border-radius: 10px;
312
+ color: #fff;
313
+ font-weight: 600;
314
+ font-size: 13px;
315
+ cursor: pointer;
316
+ text-align: center;
317
+ transition: all 0.2s;
318
+ box-shadow: 0 2px 8px rgba(72, 187, 120, 0.3);
319
+ }
320
+ .deepspider-report-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(72, 187, 120, 0.4); }
321
+ .deepspider-report-btn:active { transform: translateY(0); }
322
+ .deepspider-report-btn.visible { display: block; }
323
+ /* 报告模态框 */
324
+ #deepspider-report-modal {
325
+ display: none;
326
+ position: fixed;
327
+ top: 0; left: 0; right: 0; bottom: 0;
328
+ background: rgba(13, 17, 23, 0.92);
329
+ backdrop-filter: blur(8px);
330
+ z-index: 2147483650;
331
+ justify-content: center;
332
+ align-items: center;
333
+ padding: 24px;
334
+ animation: deepspider-modal-bg 0.2s ease-out;
335
+ }
336
+ @keyframes deepspider-modal-bg {
337
+ from { opacity: 0; }
338
+ to { opacity: 1; }
339
+ }
340
+ #deepspider-report-modal.visible { display: flex; }
341
+ .deepspider-report-container {
342
+ width: 92%;
343
+ max-width: 960px;
344
+ max-height: 88vh;
345
+ background: linear-gradient(180deg, #1e2530 0%, #161b22 100%);
346
+ border: 1px solid rgba(99, 179, 237, 0.2);
347
+ border-radius: 16px;
348
+ box-shadow: 0 24px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05) inset;
349
+ display: flex;
350
+ flex-direction: column;
351
+ overflow: hidden;
352
+ animation: deepspider-modal-in 0.25s ease-out;
353
+ }
354
+ @keyframes deepspider-modal-in {
355
+ from { opacity: 0; transform: scale(0.95) translateY(20px); }
356
+ to { opacity: 1; transform: scale(1) translateY(0); }
357
+ }
358
+ .deepspider-report-header {
359
+ padding: 18px 24px;
360
+ background: linear-gradient(180deg, rgba(99, 179, 237, 0.08) 0%, transparent 100%);
361
+ border-bottom: 1px solid rgba(99, 179, 237, 0.15);
362
+ display: flex;
363
+ justify-content: space-between;
364
+ align-items: center;
365
+ }
366
+ .deepspider-report-header h3 {
367
+ margin: 0;
368
+ color: #63b3ed;
369
+ font-size: 17px;
370
+ font-weight: 600;
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 10px;
374
+ }
375
+ .deepspider-report-close {
376
+ background: rgba(255,255,255,0.05);
377
+ border: 1px solid rgba(255,255,255,0.1);
378
+ color: #8b949e;
379
+ font-size: 20px;
380
+ width: 36px;
381
+ height: 36px;
382
+ border-radius: 10px;
383
+ cursor: pointer;
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ transition: all 0.2s;
388
+ }
389
+ .deepspider-report-close:hover { background: rgba(248, 81, 73, 0.15); color: #f85149; border-color: rgba(248, 81, 73, 0.3); }
390
+ .deepspider-report-content {
391
+ flex: 1;
392
+ overflow-y: auto;
393
+ padding: 28px 32px;
394
+ color: #c9d1d9;
395
+ font-size: 14px;
396
+ line-height: 1.7;
397
+ }
398
+ .deepspider-report-content::-webkit-scrollbar { width: 10px; }
399
+ .deepspider-report-content::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 5px; }
400
+ .deepspider-report-content::-webkit-scrollbar-thumb { background: rgba(99, 179, 237, 0.3); border-radius: 5px; }
401
+ .deepspider-report-content::-webkit-scrollbar-thumb:hover { background: rgba(99, 179, 237, 0.5); }
402
+ .deepspider-report-content h1, .deepspider-report-content h2, .deepspider-report-content h3 {
403
+ color: #63b3ed;
404
+ margin-top: 1.8em;
405
+ margin-bottom: 0.6em;
406
+ font-weight: 600;
407
+ }
408
+ .deepspider-report-content h1 { font-size: 24px; border-bottom: 1px solid rgba(99, 179, 237, 0.2); padding-bottom: 12px; }
409
+ .deepspider-report-content h2 { font-size: 20px; }
410
+ .deepspider-report-content h3 { font-size: 16px; color: #8b949e; }
411
+ .deepspider-report-content h1:first-child { margin-top: 0; }
412
+ .deepspider-report-content p { margin: 12px 0; }
413
+ .deepspider-report-content ul, .deepspider-report-content ol { margin: 12px 0; padding-left: 24px; }
414
+ .deepspider-report-content li { margin: 6px 0; }
415
+ .deepspider-report-content strong { color: #e6edf3; font-weight: 600; }
416
+ /* 代码块容器 - 支持复制 */
417
+ .deepspider-code-block {
418
+ position: relative;
419
+ margin: 16px 0;
420
+ border-radius: 10px;
421
+ overflow: hidden;
422
+ background: #0d1117;
423
+ border: 1px solid rgba(99, 179, 237, 0.15);
424
+ }
425
+ .deepspider-code-header {
426
+ display: flex;
427
+ justify-content: space-between;
428
+ align-items: center;
429
+ padding: 8px 12px;
430
+ background: rgba(99, 179, 237, 0.08);
431
+ border-bottom: 1px solid rgba(99, 179, 237, 0.1);
432
+ }
433
+ .deepspider-code-lang {
434
+ font-size: 11px;
435
+ color: #8b949e;
436
+ text-transform: uppercase;
437
+ letter-spacing: 0.5px;
438
+ font-weight: 500;
439
+ }
440
+ .deepspider-copy-btn {
441
+ background: rgba(255,255,255,0.08);
442
+ border: 1px solid rgba(255,255,255,0.1);
443
+ color: #8b949e;
444
+ font-size: 11px;
445
+ padding: 4px 10px;
446
+ border-radius: 6px;
447
+ cursor: pointer;
448
+ transition: all 0.2s;
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 4px;
452
+ }
453
+ .deepspider-copy-btn:hover { background: rgba(99, 179, 237, 0.2); color: #63b3ed; border-color: rgba(99, 179, 237, 0.3); }
454
+ .deepspider-copy-btn.copied { background: rgba(72, 187, 120, 0.2); color: #48bb78; border-color: rgba(72, 187, 120, 0.3); }
455
+ .deepspider-report-content pre {
456
+ background: transparent;
457
+ padding: 16px;
458
+ margin: 0;
459
+ overflow-x: auto;
460
+ font-size: 13px;
461
+ line-height: 1.5;
462
+ }
463
+ .deepspider-report-content code {
464
+ background: rgba(99, 179, 237, 0.1);
465
+ padding: 3px 8px;
466
+ border-radius: 6px;
467
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
468
+ font-size: 13px;
469
+ color: #79c0ff;
470
+ }
471
+ .deepspider-report-content pre code { background: transparent; padding: 0; color: #c9d1d9; }
472
+ .deepspider-report-content table {
473
+ width: 100%;
474
+ border-collapse: collapse;
475
+ margin: 16px 0;
476
+ border-radius: 8px;
477
+ overflow: hidden;
478
+ border: 1px solid rgba(99, 179, 237, 0.15);
479
+ }
480
+ .deepspider-report-content th, .deepspider-report-content td {
481
+ border: 1px solid rgba(99, 179, 237, 0.1);
482
+ padding: 12px 16px;
483
+ text-align: left;
484
+ }
485
+ .deepspider-report-content th { background: rgba(99, 179, 237, 0.08); color: #63b3ed; font-weight: 600; }
486
+ .deepspider-report-content tr:hover td { background: rgba(99, 179, 237, 0.03); }
487
+ .deepspider-messages {
488
+ flex: 1;
489
+ overflow-y: auto;
490
+ padding: 14px;
491
+ max-height: 400px;
492
+ min-height: 120px;
493
+ background: rgba(0,0,0,0.15);
494
+ }
495
+ .deepspider-messages::-webkit-scrollbar { width: 6px; }
496
+ .deepspider-messages::-webkit-scrollbar-track { background: transparent; }
497
+ .deepspider-messages::-webkit-scrollbar-thumb { background: rgba(99, 179, 237, 0.2); border-radius: 3px; }
498
+ .deepspider-messages::-webkit-scrollbar-thumb:hover { background: rgba(99, 179, 237, 0.4); }
499
+ .deepspider-empty {
500
+ text-align: center;
501
+ color: #8b949e;
502
+ padding: 40px 20px;
503
+ font-size: 13px;
504
+ line-height: 1.7;
505
+ }
506
+ .deepspider-empty-icon { font-size: 36px; margin-bottom: 14px; opacity: 0.6; }
507
+ .deepspider-msg {
508
+ margin-bottom: 12px;
509
+ padding: 12px 14px;
510
+ border-radius: 12px;
511
+ line-height: 1.6;
512
+ word-break: break-word;
513
+ animation: deepspider-msg-in 0.25s ease-out;
514
+ }
515
+ .deepspider-msg pre {
516
+ background: #0d1117;
517
+ border: 1px solid rgba(99, 179, 237, 0.15);
518
+ border-radius: 8px;
519
+ padding: 12px;
520
+ overflow-x: auto;
521
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
522
+ font-size: 12px;
523
+ margin: 10px 0;
524
+ white-space: pre;
525
+ }
526
+ .deepspider-msg code {
527
+ background: rgba(99, 179, 237, 0.12);
528
+ padding: 2px 6px;
529
+ border-radius: 4px;
530
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
531
+ font-size: 12px;
532
+ color: #79c0ff;
533
+ }
534
+ .deepspider-msg pre code {
535
+ background: none;
536
+ padding: 0;
537
+ color: #c9d1d9;
538
+ }
539
+ .deepspider-msg ul, .deepspider-msg ol {
540
+ margin: 8px 0;
541
+ padding-left: 20px;
542
+ }
543
+ .deepspider-msg li { margin: 4px 0; }
544
+ .deepspider-msg h1, .deepspider-msg h2, .deepspider-msg h3 {
545
+ margin: 14px 0 8px;
546
+ font-weight: 600;
547
+ color: #63b3ed;
548
+ }
549
+ .deepspider-msg h1 { font-size: 16px; }
550
+ .deepspider-msg h2 { font-size: 15px; }
551
+ .deepspider-msg h3 { font-size: 14px; }
552
+ .deepspider-msg p { margin: 6px 0; }
553
+ .deepspider-msg strong { font-weight: 600; color: #e6edf3; }
554
+ .deepspider-msg em { font-style: italic; }
555
+ @keyframes deepspider-msg-in {
556
+ from { opacity: 0; transform: translateY(8px); }
557
+ to { opacity: 1; transform: translateY(0); }
558
+ }
559
+ .deepspider-msg-user {
560
+ background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
561
+ margin-left: 40px;
562
+ color: #fff;
563
+ box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
564
+ }
565
+ .deepspider-msg-assistant {
566
+ background: rgba(99, 179, 237, 0.08);
567
+ margin-right: 40px;
568
+ border: 1px solid rgba(99, 179, 237, 0.15);
569
+ }
570
+ .deepspider-msg-system {
571
+ background: transparent;
572
+ text-align: center;
573
+ font-size: 12px;
574
+ color: #8b949e;
575
+ padding: 8px;
576
+ }
577
+ .deepspider-input {
578
+ padding: 14px;
579
+ border-top: 1px solid rgba(99, 179, 237, 0.15);
580
+ display: flex;
581
+ gap: 10px;
582
+ background: rgba(0,0,0,0.2);
583
+ border-radius: 0 0 16px 16px;
584
+ }
585
+ .deepspider-input textarea {
586
+ flex: 1;
587
+ padding: 12px 14px;
588
+ background: rgba(255,255,255,0.05);
589
+ border: 1px solid rgba(255,255,255,0.1);
590
+ border-radius: 10px;
591
+ color: #c9d1d9;
592
+ resize: none;
593
+ font-size: 13px;
594
+ font-family: inherit;
595
+ transition: all 0.2s;
596
+ outline: none;
597
+ }
598
+ .deepspider-input textarea:focus {
599
+ border-color: rgba(99, 179, 237, 0.5);
600
+ box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.15);
601
+ background: rgba(255,255,255,0.08);
602
+ }
603
+ .deepspider-input textarea::placeholder { color: #6e7681; }
604
+ .deepspider-input button {
605
+ padding: 12px 18px;
606
+ background: linear-gradient(135deg, #63b3ed 0%, #4299e1 100%);
607
+ border: none;
608
+ border-radius: 10px;
609
+ color: #fff;
610
+ cursor: pointer;
611
+ font-weight: 600;
612
+ font-size: 13px;
613
+ transition: all 0.2s;
614
+ box-shadow: 0 2px 8px rgba(99, 179, 237, 0.3);
615
+ }
616
+ .deepspider-input button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(99, 179, 237, 0.4); }
617
+ .deepspider-input button:active:not(:disabled) { transform: translateY(0); }
618
+ .deepspider-input button:disabled { background: rgba(255,255,255,0.1); color: #6e7681; cursor: not-allowed; box-shadow: none; }
619
+ #deepspider-overlay {
620
+ position: fixed;
621
+ pointer-events: none;
622
+ border: 2px solid #63b3ed;
623
+ background: rgba(99, 179, 237, 0.12);
624
+ z-index: 2147483646;
625
+ display: none;
626
+ border-radius: 6px;
627
+ transition: all 0.1s ease-out;
628
+ box-shadow: 0 0 0 4px rgba(99, 179, 237, 0.15);
629
+ }
630
+ #deepspider-info {
631
+ position: fixed;
632
+ background: linear-gradient(135deg, #1e2530 0%, #161b22 100%);
633
+ color: #63b3ed;
634
+ padding: 8px 12px;
635
+ font-size: 11px;
636
+ font-weight: 600;
637
+ border-radius: 6px;
638
+ z-index: 2147483647;
639
+ display: none;
640
+ pointer-events: none;
641
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
642
+ border: 1px solid rgba(99, 179, 237, 0.2);
643
+ }
644
+ /* 元素选择后的操作菜单 */
645
+ #deepspider-action-modal {
646
+ display: none;
647
+ position: fixed;
648
+ top: 0; left: 0; right: 0; bottom: 0;
649
+ background: rgba(13, 17, 23, 0.85);
650
+ backdrop-filter: blur(4px);
651
+ z-index: 2147483648;
652
+ justify-content: center;
653
+ align-items: center;
654
+ padding: 24px;
655
+ }
656
+ #deepspider-action-modal.visible { display: flex; }
657
+ #deepspider-config-modal.visible { display: flex !important; }
658
+ .deepspider-action-container {
659
+ width: 420px;
660
+ max-height: 80vh;
661
+ background: linear-gradient(180deg, #1e2530 0%, #161b22 100%);
662
+ border: 1px solid rgba(99, 179, 237, 0.2);
663
+ border-radius: 16px;
664
+ box-shadow: 0 24px 80px rgba(0,0,0,0.5);
665
+ overflow: hidden;
666
+ animation: deepspider-modal-in 0.2s ease-out;
667
+ }
668
+ .deepspider-action-header {
669
+ padding: 16px 20px;
670
+ background: linear-gradient(180deg, rgba(99, 179, 237, 0.08) 0%, transparent 100%);
671
+ border-bottom: 1px solid rgba(99, 179, 237, 0.15);
672
+ display: flex;
673
+ justify-content: space-between;
674
+ align-items: center;
675
+ }
676
+ .deepspider-action-header h4 {
677
+ margin: 0;
678
+ color: #63b3ed;
679
+ font-size: 15px;
680
+ font-weight: 600;
681
+ }
682
+ .deepspider-action-close {
683
+ background: transparent;
684
+ border: none;
685
+ color: #8b949e;
686
+ font-size: 20px;
687
+ cursor: pointer;
688
+ padding: 4px 8px;
689
+ }
690
+ .deepspider-action-close:hover { color: #f85149; }
691
+ .deepspider-action-content {
692
+ padding: 16px 20px;
693
+ max-height: 60vh;
694
+ overflow-y: auto;
695
+ }
696
+ .deepspider-action-preview {
697
+ background: rgba(0,0,0,0.3);
698
+ border: 1px solid rgba(99, 179, 237, 0.1);
699
+ border-radius: 8px;
700
+ padding: 12px;
701
+ margin-bottom: 16px;
702
+ font-size: 12px;
703
+ color: #8b949e;
704
+ max-height: 80px;
705
+ overflow: hidden;
706
+ }
707
+ .deepspider-action-preview .xpath {
708
+ color: #79c0ff;
709
+ font-family: monospace;
710
+ font-size: 11px;
711
+ margin-top: 6px;
712
+ word-break: break-all;
713
+ }
714
+ .deepspider-action-section {
715
+ margin-bottom: 16px;
716
+ }
717
+ .deepspider-action-section label {
718
+ display: block;
719
+ color: #8b949e;
720
+ font-size: 12px;
721
+ margin-bottom: 8px;
722
+ }
723
+ .deepspider-action-input {
724
+ width: 100%;
725
+ padding: 10px 12px;
726
+ background: rgba(255,255,255,0.05);
727
+ border: 1px solid rgba(255,255,255,0.1);
728
+ border-radius: 8px;
729
+ color: #c9d1d9;
730
+ font-size: 13px;
731
+ outline: none;
732
+ box-sizing: border-box;
733
+ }
734
+ .deepspider-action-input:focus {
735
+ border-color: rgba(99, 179, 237, 0.5);
736
+ }
737
+ .deepspider-action-select {
738
+ width: 100%;
739
+ padding: 10px 12px;
740
+ background: rgba(255,255,255,0.05);
741
+ border: 1px solid rgba(255,255,255,0.1);
742
+ border-radius: 8px;
743
+ color: #c9d1d9;
744
+ font-size: 13px;
745
+ outline: none;
746
+ cursor: pointer;
747
+ }
748
+ .deepspider-action-btns {
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 10px;
752
+ margin-top: 16px;
753
+ }
754
+ .deepspider-action-btn {
755
+ padding: 12px 16px;
756
+ border: 1px solid rgba(255,255,255,0.1);
757
+ border-radius: 10px;
758
+ background: rgba(255,255,255,0.05);
759
+ color: #c9d1d9;
760
+ font-size: 13px;
761
+ cursor: pointer;
762
+ text-align: left;
763
+ transition: all 0.2s;
764
+ display: flex;
765
+ align-items: center;
766
+ gap: 10px;
767
+ }
768
+ .deepspider-action-btn:hover {
769
+ background: rgba(99, 179, 237, 0.15);
770
+ border-color: rgba(99, 179, 237, 0.3);
771
+ }
772
+ .deepspider-action-btn.primary {
773
+ background: linear-gradient(135deg, #63b3ed 0%, #4299e1 100%);
774
+ border-color: transparent;
775
+ color: #fff;
776
+ }
777
+ .deepspider-action-btn.primary:hover {
778
+ box-shadow: 0 4px 12px rgba(99, 179, 237, 0.4);
779
+ }
780
+ .deepspider-action-btn.success {
781
+ background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
782
+ border-color: transparent;
783
+ color: #fff;
784
+ }
785
+ .deepspider-action-btn-icon { font-size: 16px; }
786
+ .deepspider-action-btn-text { flex: 1; }
787
+ .deepspider-action-btn-desc {
788
+ font-size: 11px;
789
+ color: rgba(255,255,255,0.6);
790
+ margin-top: 2px;
791
+ }
792
+ \`;
793
+ document.head.appendChild(style);
794
+
795
+ // ========== 创建面板 ==========
796
+ const panel = document.createElement('div');
797
+ panel.id = 'deepspider-panel';
798
+ panel.innerHTML = \`
799
+ <div class="deepspider-header">
800
+ <span class="deepspider-header-title">
801
+ <span class="deepspider-status" id="deepspider-status"></span>
802
+ DeepSpider
803
+ </span>
804
+ <div class="deepspider-header-btns">
805
+ <button id="deepspider-btn-select" title="选择元素分析">&#9678;</button>
806
+ <button id="deepspider-btn-minimize" title="最小化">&#8722;</button>
807
+ <button id="deepspider-btn-close" title="关闭">&times;</button>
808
+ </div>
809
+ </div>
810
+ <button id="deepspider-report-btn" class="deepspider-report-btn">📊 查看分析报告</button>
811
+ <div class="deepspider-messages" id="deepspider-messages">
812
+ <div class="deepspider-empty">
813
+ <div class="deepspider-empty-icon">🔍</div>
814
+ 点击上方 ⦿ 按钮选择页面元素<br>或在下方输入问题开始分析
815
+ </div>
816
+ </div>
817
+ <div class="deepspider-input">
818
+ <textarea id="deepspider-chat-input" placeholder="输入问题,按 Enter 发送..." rows="2"></textarea>
819
+ <button id="deepspider-btn-send">发送</button>
820
+ </div>
821
+ \`;
822
+ document.body.appendChild(panel);
823
+
824
+ // ========== 创建报告模态框 ==========
825
+ const reportModal = document.createElement('div');
826
+ reportModal.id = 'deepspider-report-modal';
827
+ reportModal.innerHTML = \`
828
+ <div class="deepspider-report-container">
829
+ <div class="deepspider-report-header">
830
+ <h3>📊 分析报告</h3>
831
+ <button class="deepspider-report-close" id="deepspider-report-close">&times;</button>
832
+ </div>
833
+ <div class="deepspider-report-content" id="deepspider-report-content"></div>
834
+ </div>
835
+ \`;
836
+ document.body.appendChild(reportModal);
837
+
838
+ // ========== 创建元素操作菜单 ==========
839
+ const actionModal = document.createElement('div');
840
+ actionModal.id = 'deepspider-action-modal';
841
+ actionModal.innerHTML = \`
842
+ <div class="deepspider-action-container">
843
+ <div class="deepspider-action-header">
844
+ <h4>🎯 元素已选中</h4>
845
+ <button class="deepspider-action-close" id="deepspider-action-close">&times;</button>
846
+ </div>
847
+ <div class="deepspider-action-content">
848
+ <div class="deepspider-action-preview" id="deepspider-action-preview">
849
+ <div class="text"></div>
850
+ <div class="xpath"></div>
851
+ </div>
852
+ <div class="deepspider-action-section">
853
+ <label>字段名称(用于爬虫配置)</label>
854
+ <input type="text" class="deepspider-action-input" id="deepspider-field-name" placeholder="例如: title, price, content">
855
+ </div>
856
+ <div class="deepspider-action-section">
857
+ <label>字段类型</label>
858
+ <select class="deepspider-action-select" id="deepspider-field-type">
859
+ <option value="str">文本 (str)</option>
860
+ <option value="url">链接 (url)</option>
861
+ <option value="entry">入口链接 (entry) - 进入下一阶段</option>
862
+ <option value="html">HTML</option>
863
+ <option value="file">文件</option>
864
+ <option value="json">JSON</option>
865
+ </select>
866
+ </div>
867
+ <div class="deepspider-action-section" id="deepspider-entry-target-section" style="display:none;">
868
+ <label>目标阶段</label>
869
+ <select class="deepspider-action-select" id="deepspider-entry-target"></select>
870
+ </div>
871
+ <div class="deepspider-action-btns" id="deepspider-action-btns"></div>
872
+ </div>
873
+ </div>
874
+ \`;
875
+ document.body.appendChild(actionModal);
876
+
877
+ // ========== 创建配置弹窗 ==========
878
+ const configModal = document.createElement('div');
879
+ configModal.id = 'deepspider-config-modal';
880
+ configModal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(13,17,23,0.85);z-index:2147483649;justify-content:center;align-items:center;';
881
+ configModal.innerHTML = \`
882
+ <div style="width:400px;background:linear-gradient(180deg,#1e2530,#161b22);border:1px solid rgba(99,179,237,0.2);border-radius:16px;overflow:hidden;">
883
+ <div style="padding:16px 20px;background:linear-gradient(180deg,rgba(99,179,237,0.08),transparent);border-bottom:1px solid rgba(99,179,237,0.15);display:flex;justify-content:space-between;align-items:center;">
884
+ <h4 style="margin:0;color:#63b3ed;font-size:15px;">⚙️ 配置爬虫</h4>
885
+ <button id="deepspider-config-close" style="background:none;border:none;color:#8b949e;font-size:20px;cursor:pointer;">&times;</button>
886
+ </div>
887
+ <div style="padding:16px 20px;">
888
+ <div style="margin-bottom:16px;">
889
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:6px;">抓取方式</label>
890
+ <select id="deepspider-grab-method" style="width:100%;padding:8px 12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:6px;color:#c9d1d9;font-size:13px;">
891
+ <option value="browser">浏览器渲染 (browser)</option>
892
+ <option value="html">静态HTML (html)</option>
893
+ <option value="api">API请求 (api)</option>
894
+ </select>
895
+ </div>
896
+ <div style="margin-bottom:16px;">
897
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:6px;">最大页数</label>
898
+ <input type="number" id="deepspider-max-page" value="10" style="width:100%;padding:8px 12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:6px;color:#c9d1d9;font-size:13px;box-sizing:border-box;">
899
+ </div>
900
+ <div style="margin-bottom:16px;">
901
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:6px;">下一页按钮 XPath(可选)</label>
902
+ <input type="text" id="deepspider-next-xpath" placeholder="例如: //a[contains(text(),'下一页')]" style="width:100%;padding:8px 12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:6px;color:#c9d1d9;font-size:13px;box-sizing:border-box;">
903
+ </div>
904
+ <button id="deepspider-config-submit" style="width:100%;padding:12px;background:linear-gradient(135deg,#48bb78,#38a169);border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:600;cursor:pointer;">生成爬虫</button>
905
+ </div>
906
+ </div>
907
+ \`;
908
+ document.body.appendChild(configModal);
909
+
910
+ // 配置弹窗事件
911
+ document.getElementById('deepspider-config-close').onclick = () => {
912
+ configModal.classList.remove('visible');
913
+ };
914
+ configModal.addEventListener('click', (e) => {
915
+ if (e.target === configModal) configModal.classList.remove('visible');
916
+ });
917
+ document.getElementById('deepspider-config-submit').onclick = submitConfig;
918
+
919
+ // 操作菜单状态
920
+ let pendingSelection = null;
921
+
922
+ // 关闭操作菜单
923
+ document.getElementById('deepspider-action-close').onclick = () => {
924
+ actionModal.classList.remove('visible');
925
+ pendingSelection = null;
926
+ };
927
+ actionModal.addEventListener('click', (e) => {
928
+ if (e.target === actionModal) {
929
+ actionModal.classList.remove('visible');
930
+ pendingSelection = null;
931
+ }
932
+ });
933
+
934
+ // 显示操作菜单
935
+ function showActionMenu(selection) {
936
+ pendingSelection = selection;
937
+ const preview = document.getElementById('deepspider-action-preview');
938
+ preview.querySelector('.text').textContent = selection.text.slice(0, 100) + (selection.text.length > 100 ? '...' : '');
939
+ preview.querySelector('.xpath').textContent = selection.xpath;
940
+ document.getElementById('deepspider-field-name').value = '';
941
+ document.getElementById('deepspider-field-type').value = 'str';
942
+
943
+ // 更新目标阶段选择器
944
+ const entryTargetSection = document.getElementById('deepspider-entry-target-section');
945
+ const entryTargetSelect = document.getElementById('deepspider-entry-target');
946
+ const fieldTypeSelect = document.getElementById('deepspider-field-type');
947
+
948
+ function updateEntryTargetOptions() {
949
+ entryTargetSelect.innerHTML = deepspider.stages
950
+ .map((s, i) => '<option value="' + i + '"' + (i === deepspider.currentStageIndex ? ' disabled' : '') + '>' + s.name + (i === deepspider.currentStageIndex ? ' (当前)' : '') + '</option>')
951
+ .join('') + '<option value="__new__">+ 新建阶段</option>';
952
+ }
953
+
954
+ fieldTypeSelect.onchange = () => {
955
+ if (fieldTypeSelect.value === 'entry') {
956
+ updateEntryTargetOptions();
957
+ entryTargetSection.style.display = 'block';
958
+ } else {
959
+ entryTargetSection.style.display = 'none';
960
+ }
961
+ };
962
+ entryTargetSection.style.display = 'none';
963
+
964
+ const btnsContainer = document.getElementById('deepspider-action-btns');
965
+ btnsContainer.innerHTML = \`
966
+ <button class="deepspider-action-btn primary" data-action="add-field">
967
+ <span class="deepspider-action-btn-icon">➕</span>
968
+ <div class="deepspider-action-btn-text">
969
+ 添加为字段
970
+ <div class="deepspider-action-btn-desc">添加到爬虫配置,可继续选择更多字段</div>
971
+ </div>
972
+ </button>
973
+ <button class="deepspider-action-btn" data-action="analyze-source">
974
+ <span class="deepspider-action-btn-icon">🔍</span>
975
+ <div class="deepspider-action-btn-text">
976
+ 追踪数据来源
977
+ <div class="deepspider-action-btn-desc">分析该数据从哪个请求返回</div>
978
+ </div>
979
+ </button>
980
+ <button class="deepspider-action-btn" data-action="analyze-crypto">
981
+ <span class="deepspider-action-btn-icon">🔐</span>
982
+ <div class="deepspider-action-btn-text">
983
+ 分析加密逻辑
984
+ <div class="deepspider-action-btn-desc">识别加密算法并生成 Python 代码</div>
985
+ </div>
986
+ </button>
987
+ <button class="deepspider-action-btn" data-action="full-analysis">
988
+ <span class="deepspider-action-btn-icon">📊</span>
989
+ <div class="deepspider-action-btn-text">
990
+ 完整流程分析
991
+ <div class="deepspider-action-btn-desc">追踪来源 + 加密分析 + 生成代码</div>
992
+ </div>
993
+ </button>
994
+ \`;
995
+
996
+ // 绑定按钮事件
997
+ btnsContainer.querySelectorAll('button').forEach(btn => {
998
+ btn.onclick = () => handleAction(btn.dataset.action);
999
+ });
1000
+
1001
+ actionModal.classList.add('visible');
1002
+ }
1003
+
1004
+ // 处理操作菜单的动作
1005
+ function handleAction(action) {
1006
+ if (!pendingSelection) return;
1007
+ const fieldName = document.getElementById('deepspider-field-name').value.trim();
1008
+ const fieldType = document.getElementById('deepspider-field-type').value;
1009
+
1010
+ switch (action) {
1011
+ case 'add-field':
1012
+ addField(fieldName, fieldType);
1013
+ break;
1014
+ case 'analyze-source':
1015
+ sendAnalysis('source', pendingSelection);
1016
+ break;
1017
+ case 'analyze-crypto':
1018
+ sendAnalysis('crypto', pendingSelection);
1019
+ break;
1020
+ case 'full-analysis':
1021
+ sendAnalysis('full', pendingSelection);
1022
+ break;
1023
+ }
1024
+ actionModal.classList.remove('visible');
1025
+ }
1026
+
1027
+ // 创建空阶段对象
1028
+ function createStage(name) {
1029
+ return { name: name, fields: [], entry: null, pagination: null };
1030
+ }
1031
+
1032
+ // 添加字段到当前阶段
1033
+ function addField(name, type) {
1034
+ if (!pendingSelection) return;
1035
+ const currentStage = deepspider.stages[deepspider.currentStageIndex];
1036
+ if (!currentStage) return;
1037
+
1038
+ // 处理入口类型
1039
+ if (type === 'entry') {
1040
+ const targetIndex = document.getElementById('deepspider-entry-target').value;
1041
+ let targetStageName;
1042
+
1043
+ if (targetIndex === '__new__') {
1044
+ const newStageName = 'stage_' + (deepspider.stages.length + 1);
1045
+ deepspider.stages.push(createStage(newStageName));
1046
+ targetStageName = newStageName;
1047
+ } else {
1048
+ const idx = parseInt(targetIndex);
1049
+ if (idx < 0 || idx >= deepspider.stages.length) return;
1050
+ targetStageName = deepspider.stages[idx].name;
1051
+ }
1052
+
1053
+ const entryName = name || 'entry_link';
1054
+ currentStage.entry = {
1055
+ field: entryName,
1056
+ xpath: pendingSelection.xpath,
1057
+ to_stage: targetStageName
1058
+ };
1059
+ saveStages();
1060
+ panel.classList.add('visible');
1061
+ addMessage('system', '✅ 已设置入口: ' + entryName + ' → ' + targetStageName);
1062
+ updateStagesPanel();
1063
+ return;
1064
+ }
1065
+
1066
+ // 普通字段
1067
+ const fieldName = name || 'field_' + (currentStage.fields.length + 1);
1068
+ const field = {
1069
+ name: fieldName,
1070
+ xpath: pendingSelection.xpath,
1071
+ type: type,
1072
+ value: pendingSelection.text.slice(0, 100),
1073
+ time: Date.now()
1074
+ };
1075
+ currentStage.fields.push(field);
1076
+ saveStages();
1077
+
1078
+ panel.classList.add('visible');
1079
+ addMessage('system', '✅ 已添加字段: ' + field.name + ' (阶段: ' + currentStage.name + ')');
1080
+ updateStagesPanel();
1081
+ }
1082
+
1083
+ // 发送分析请求
1084
+ function sendAnalysis(analysisType, selection) {
1085
+ panel.classList.add('visible');
1086
+ const typeLabels = {
1087
+ source: '追踪数据来源',
1088
+ crypto: '分析加密逻辑',
1089
+ full: '完整流程分析'
1090
+ };
1091
+ addMessage('user', typeLabels[analysisType] + ': ' + selection.text.slice(0, 80));
1092
+ addMessage('system', '分析中...');
1093
+
1094
+ if (typeof __deepspider_send__ === 'function') {
1095
+ __deepspider_send__(JSON.stringify({
1096
+ type: 'analysis',
1097
+ analysisType: analysisType,
1098
+ text: selection.text,
1099
+ xpath: selection.xpath,
1100
+ url: location.href,
1101
+ iframeSrc: selection.iframeSrc
1102
+ }));
1103
+ }
1104
+ }
1105
+
1106
+ // 更新阶段面板显示
1107
+ function updateStagesPanel() {
1108
+ let stagesPanel = document.getElementById('deepspider-stages-panel');
1109
+ if (!stagesPanel) {
1110
+ stagesPanel = document.createElement('div');
1111
+ stagesPanel.id = 'deepspider-stages-panel';
1112
+ stagesPanel.style.cssText = 'padding:10px 14px;border-top:1px solid rgba(99,179,237,0.15);background:rgba(0,0,0,0.1);';
1113
+ const inputArea = panel.querySelector('.deepspider-input');
1114
+ panel.insertBefore(stagesPanel, inputArea);
1115
+ }
1116
+
1117
+ const totalFields = deepspider.stages.reduce((sum, s) => sum + s.fields.length, 0);
1118
+ if (totalFields === 0 && !deepspider.stages.some(s => s.entry)) {
1119
+ stagesPanel.style.display = 'none';
1120
+ return;
1121
+ }
1122
+
1123
+ stagesPanel.style.display = 'block';
1124
+ const currentStage = deepspider.stages[deepspider.currentStageIndex];
1125
+
1126
+ // 阶段标签
1127
+ const stageTabs = deepspider.stages.map((s, i) => {
1128
+ const isActive = i === deepspider.currentStageIndex;
1129
+ const fieldCount = s.fields.length;
1130
+ const hasEntry = s.entry ? ' →' : '';
1131
+ return '<span data-stage="' + i + '" style="' +
1132
+ 'background:' + (isActive ? 'rgba(99,179,237,0.3)' : 'rgba(99,179,237,0.1)') + ';' +
1133
+ 'border:1px solid ' + (isActive ? 'rgba(99,179,237,0.5)' : 'rgba(99,179,237,0.2)') + ';' +
1134
+ 'padding:4px 10px;border-radius:6px;font-size:11px;color:#63b3ed;cursor:pointer;' +
1135
+ 'display:inline-flex;align-items:center;gap:4px;">' +
1136
+ s.name + ' (' + fieldCount + ')' + hasEntry + '</span>';
1137
+ }).join('');
1138
+
1139
+ // 当前阶段的字段
1140
+ const fieldTags = currentStage.fields.map((f, i) =>
1141
+ '<span style="background:rgba(72,187,120,0.15);border:1px solid rgba(72,187,120,0.2);' +
1142
+ 'padding:4px 8px;border-radius:6px;font-size:11px;color:#48bb78;' +
1143
+ 'display:inline-flex;align-items:center;gap:4px;">' +
1144
+ f.name + '<span style="cursor:pointer;color:#8b949e;" data-remove="' + i + '">&times;</span></span>'
1145
+ ).join('');
1146
+
1147
+ // 入口显示
1148
+ const entryTag = currentStage.entry ?
1149
+ '<span style="background:rgba(237,137,54,0.15);border:1px solid rgba(237,137,54,0.2);' +
1150
+ 'padding:4px 8px;border-radius:6px;font-size:11px;color:#ed8936;' +
1151
+ 'display:inline-flex;align-items:center;gap:4px;">' +
1152
+ currentStage.entry.field + ' → ' + currentStage.entry.to_stage +
1153
+ '<span style="cursor:pointer;color:#8b949e;" data-remove-entry="1">&times;</span></span>' : '';
1154
+
1155
+ stagesPanel.innerHTML =
1156
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
1157
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;">' + stageTabs +
1158
+ '<span id="deepspider-add-stage" style="background:rgba(255,255,255,0.05);border:1px dashed rgba(255,255,255,0.2);' +
1159
+ 'padding:4px 10px;border-radius:6px;font-size:11px;color:#8b949e;cursor:pointer;">+ 阶段</span></div>' +
1160
+ '<div style="display:flex;gap:6px;">' +
1161
+ '<button id="deepspider-gen-config" style="background:linear-gradient(135deg,#48bb78,#38a169);' +
1162
+ 'border:none;color:#fff;padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;">生成配置</button>' +
1163
+ '<button id="deepspider-clear-all" style="background:rgba(248,81,73,0.2);border:1px solid rgba(248,81,73,0.3);' +
1164
+ 'color:#f85149;padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;">清空</button>' +
1165
+ '</div></div>' +
1166
+ '<div style="margin-bottom:6px;font-size:11px;color:#8b949e;">阶段: ' + currentStage.name + '</div>' +
1167
+ '<div style="display:flex;flex-wrap:wrap;gap:6px;">' + fieldTags + entryTag + '</div>';
1168
+
1169
+ bindStagesPanelEvents(stagesPanel);
1170
+ }
1171
+
1172
+ // 绑定阶段面板事件
1173
+ function bindStagesPanelEvents(stagesPanel) {
1174
+ // 阶段切换
1175
+ stagesPanel.querySelectorAll('[data-stage]').forEach(tab => {
1176
+ tab.onclick = () => {
1177
+ const idx = parseInt(tab.dataset.stage);
1178
+ if (idx >= 0 && idx < deepspider.stages.length) {
1179
+ deepspider.currentStageIndex = idx;
1180
+ saveStages();
1181
+ updateStagesPanel();
1182
+ }
1183
+ };
1184
+ });
1185
+ // 添加阶段
1186
+ document.getElementById('deepspider-add-stage').onclick = addStage;
1187
+ // 生成配置
1188
+ document.getElementById('deepspider-gen-config').onclick = generateConfig;
1189
+ // 清空
1190
+ document.getElementById('deepspider-clear-all').onclick = clearAll;
1191
+ // 移除字段
1192
+ stagesPanel.querySelectorAll('[data-remove]').forEach(btn => {
1193
+ btn.onclick = () => removeField(parseInt(btn.dataset.remove));
1194
+ });
1195
+ // 移除入口
1196
+ stagesPanel.querySelectorAll('[data-remove-entry]').forEach(btn => {
1197
+ btn.onclick = removeEntry;
1198
+ });
1199
+ }
1200
+
1201
+ // 移除当前阶段的字段
1202
+ function removeField(index) {
1203
+ const currentStage = deepspider.stages[deepspider.currentStageIndex];
1204
+ if (!currentStage || index < 0 || index >= currentStage.fields.length) return;
1205
+ currentStage.fields.splice(index, 1);
1206
+ saveStages();
1207
+ updateStagesPanel();
1208
+ }
1209
+
1210
+ // 移除当前阶段的入口
1211
+ function removeEntry() {
1212
+ const currentStage = deepspider.stages[deepspider.currentStageIndex];
1213
+ if (!currentStage) return;
1214
+ currentStage.entry = null;
1215
+ saveStages();
1216
+ updateStagesPanel();
1217
+ }
1218
+
1219
+ // 添加新阶段
1220
+ function addStage() {
1221
+ const name = 'stage_' + (deepspider.stages.length + 1);
1222
+ deepspider.stages.push(createStage(name));
1223
+ deepspider.currentStageIndex = deepspider.stages.length - 1;
1224
+ saveStages();
1225
+ updateStagesPanel();
1226
+ addMessage('system', '✅ 已添加阶段: ' + name);
1227
+ }
1228
+
1229
+ // 清空所有阶段
1230
+ function clearAll() {
1231
+ deepspider.stages = [createStage('list')];
1232
+ deepspider.currentStageIndex = 0;
1233
+ saveStages();
1234
+ updateStagesPanel();
1235
+ }
1236
+
1237
+ // 生成爬虫配置 - 显示配置弹窗
1238
+ function generateConfig() {
1239
+ showConfigModal();
1240
+ }
1241
+
1242
+ // 显示配置弹窗
1243
+ function showConfigModal() {
1244
+ const modal = document.getElementById('deepspider-config-modal');
1245
+ if (!modal) return;
1246
+
1247
+ // 更新阶段分页配置区域
1248
+ const stagesConfigHtml = deepspider.stages.map((stage, i) => {
1249
+ const hasPagination = stage.pagination !== null;
1250
+ return '<div style="margin-bottom:12px;padding:10px;background:rgba(0,0,0,0.2);border-radius:8px;">' +
1251
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
1252
+ '<span style="color:#63b3ed;font-size:12px;font-weight:600;">' + stage.name + '</span>' +
1253
+ '<span style="color:#8b949e;font-size:11px;">' + stage.fields.length + ' 字段' +
1254
+ (stage.entry ? ' → ' + stage.entry.to_stage : '') + '</span></div>' +
1255
+ '<div style="display:flex;gap:8px;align-items:center;">' +
1256
+ '<label style="color:#8b949e;font-size:11px;white-space:nowrap;">分页:</label>' +
1257
+ '<input type="checkbox" data-stage-pagination="' + i + '"' + (hasPagination ? ' checked' : '') + '>' +
1258
+ '<input type="text" data-stage-xpath="' + i + '" placeholder="下一页XPath" ' +
1259
+ 'value="' + (stage.pagination?.next_page_xpath || '') + '" ' +
1260
+ 'style="flex:1;padding:4px 8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);' +
1261
+ 'border-radius:4px;color:#c9d1d9;font-size:11px;' + (hasPagination ? '' : 'opacity:0.5;') + '">' +
1262
+ '<input type="number" data-stage-max="' + i + '" placeholder="页数" ' +
1263
+ 'value="' + (stage.pagination?.max_page || 10) + '" ' +
1264
+ 'style="width:50px;padding:4px 8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);' +
1265
+ 'border-radius:4px;color:#c9d1d9;font-size:11px;' + (hasPagination ? '' : 'opacity:0.5;') + '">' +
1266
+ '</div></div>';
1267
+ }).join('');
1268
+
1269
+ const configContent = modal.querySelector('div > div:last-child');
1270
+ configContent.innerHTML =
1271
+ '<div style="margin-bottom:16px;">' +
1272
+ '<label style="display:block;color:#8b949e;font-size:12px;margin-bottom:6px;">抓取方式</label>' +
1273
+ '<select id="deepspider-grab-method" style="width:100%;padding:8px 12px;background:rgba(255,255,255,0.05);' +
1274
+ 'border:1px solid rgba(255,255,255,0.1);border-radius:6px;color:#c9d1d9;font-size:13px;">' +
1275
+ '<option value="browser">浏览器渲染 (browser)</option>' +
1276
+ '<option value="html">静态HTML (html)</option>' +
1277
+ '<option value="api">API请求 (api)</option></select></div>' +
1278
+ '<div style="margin-bottom:16px;">' +
1279
+ '<label style="display:block;color:#8b949e;font-size:12px;margin-bottom:6px;">阶段配置</label>' +
1280
+ stagesConfigHtml + '</div>' +
1281
+ '<button id="deepspider-config-submit" style="width:100%;padding:12px;' +
1282
+ 'background:linear-gradient(135deg,#48bb78,#38a169);border:none;border-radius:8px;' +
1283
+ 'color:#fff;font-size:13px;font-weight:600;cursor:pointer;">生成爬虫</button>';
1284
+
1285
+ // 绑定分页复选框事件
1286
+ configContent.querySelectorAll('[data-stage-pagination]').forEach(checkbox => {
1287
+ checkbox.onchange = () => {
1288
+ const idx = checkbox.dataset.stagePagination;
1289
+ const xpathInput = configContent.querySelector('[data-stage-xpath="' + idx + '"]');
1290
+ const maxInput = configContent.querySelector('[data-stage-max="' + idx + '"]');
1291
+ xpathInput.style.opacity = checkbox.checked ? '1' : '0.5';
1292
+ maxInput.style.opacity = checkbox.checked ? '1' : '0.5';
1293
+ };
1294
+ });
1295
+
1296
+ document.getElementById('deepspider-config-submit').onclick = submitConfig;
1297
+ modal.classList.add('visible');
1298
+ }
1299
+
1300
+ // 提交配置
1301
+ function submitConfig() {
1302
+ const modal = document.getElementById('deepspider-config-modal');
1303
+ const grabMethod = document.getElementById('deepspider-grab-method')?.value || 'browser';
1304
+
1305
+ // 收集各阶段的分页配置
1306
+ deepspider.stages.forEach((stage, i) => {
1307
+ const checkbox = modal.querySelector('[data-stage-pagination="' + i + '"]');
1308
+ const xpathInput = modal.querySelector('[data-stage-xpath="' + i + '"]');
1309
+ const maxInput = modal.querySelector('[data-stage-max="' + i + '"]');
1310
+
1311
+ if (checkbox?.checked && xpathInput?.value) {
1312
+ stage.pagination = {
1313
+ next_page_xpath: xpathInput.value,
1314
+ max_page: parseInt(maxInput?.value) || 10
1315
+ };
1316
+ } else {
1317
+ stage.pagination = null;
1318
+ }
1319
+ });
1320
+ saveStages();
1321
+
1322
+ // 构建阶段化配置
1323
+ const config = {
1324
+ url: location.href,
1325
+ grab_method: grabMethod,
1326
+ stages: deepspider.stages.map(s => ({
1327
+ name: s.name,
1328
+ fields: s.fields.map(f => ({
1329
+ name: f.name,
1330
+ xpath: f.xpath,
1331
+ type: f.type
1332
+ })),
1333
+ entry: s.entry,
1334
+ pagination: s.pagination
1335
+ }))
1336
+ };
1337
+
1338
+ modal?.classList.remove('visible');
1339
+ panel.classList.add('visible');
1340
+
1341
+ const totalFields = deepspider.stages.reduce((sum, s) => sum + s.fields.length, 0);
1342
+ addMessage('user', '生成爬虫配置 (' + deepspider.stages.length + ' 阶段, ' + totalFields + ' 字段)');
1343
+ addMessage('system', '正在生成配置...');
1344
+
1345
+ if (typeof __deepspider_send__ === 'function') {
1346
+ __deepspider_send__(JSON.stringify({
1347
+ type: 'generate-config',
1348
+ config: config,
1349
+ url: location.href
1350
+ }));
1351
+ }
1352
+ }
1353
+
1354
+ // 点击背景关闭模态框
1355
+ reportModal.addEventListener('click', (e) => {
1356
+ if (e.target === reportModal) {
1357
+ reportModal.classList.remove('visible');
1358
+ }
1359
+ });
1360
+
1361
+ // ========== 创建选择器覆盖层 ==========
1362
+ const overlay = document.createElement('div');
1363
+ overlay.id = 'deepspider-overlay';
1364
+ document.body.appendChild(overlay);
1365
+
1366
+ const infoBox = document.createElement('div');
1367
+ infoBox.id = 'deepspider-info';
1368
+ document.body.appendChild(infoBox);
1369
+
1370
+ // ========== 面板拖动 ==========
1371
+ let isDragging = false;
1372
+ let dragOffset = { x: 0, y: 0 };
1373
+ const header = panel.querySelector('.deepspider-header');
1374
+
1375
+ header.addEventListener('mousedown', (e) => {
1376
+ if (e.target.tagName === 'BUTTON') return;
1377
+ isDragging = true;
1378
+ dragOffset.x = e.clientX - panel.offsetLeft;
1379
+ dragOffset.y = e.clientY - panel.offsetTop;
1380
+ });
1381
+
1382
+ document.addEventListener('mousemove', (e) => {
1383
+ if (!isDragging) return;
1384
+ panel.style.left = (e.clientX - dragOffset.x) + 'px';
1385
+ panel.style.top = (e.clientY - dragOffset.y) + 'px';
1386
+ panel.style.right = 'auto';
1387
+ });
1388
+
1389
+ document.addEventListener('mouseup', () => { isDragging = false; });
1390
+
1391
+ // ========== 关闭按钮 ==========
1392
+ document.getElementById('deepspider-btn-close').onclick = () => {
1393
+ panel.classList.remove('visible');
1394
+ };
1395
+
1396
+ // ========== 最小化按钮 ==========
1397
+ const minimizeBtn = document.getElementById('deepspider-btn-minimize');
1398
+ minimizeBtn.onclick = () => {
1399
+ const isMinimized = panel.classList.toggle('minimized');
1400
+ minimizeBtn.innerHTML = isMinimized ? '&#9633;' : '&#8722;';
1401
+ minimizeBtn.title = isMinimized ? '展开' : '最小化';
1402
+ };
1403
+
1404
+ // ========== XPath 生成 ==========
1405
+ function getXPath(el) {
1406
+ if (!el) return '';
1407
+ if (el.id) return '//*[@id="' + el.id + '"]';
1408
+ const parts = [];
1409
+ while (el && el.nodeType === 1) {
1410
+ let idx = 1, sib = el.previousSibling;
1411
+ while (sib) {
1412
+ if (sib.nodeType === 1 && sib.tagName === el.tagName) idx++;
1413
+ sib = sib.previousSibling;
1414
+ }
1415
+ parts.unshift(el.tagName.toLowerCase() + '[' + idx + ']');
1416
+ el = el.parentNode;
1417
+ }
1418
+ return '/' + parts.join('/');
1419
+ }
1420
+
1421
+ // ========== 广播消息到所有 iframe ==========
1422
+ function broadcastToIframes(message) {
1423
+ const iframes = document.querySelectorAll('iframe');
1424
+ iframes.forEach(iframe => {
1425
+ try {
1426
+ iframe.contentWindow.postMessage(message, '*');
1427
+ } catch (e) {
1428
+ // 跨域 iframe 可能无法通信
1429
+ }
1430
+ });
1431
+ }
1432
+
1433
+ // ========== 选择器模式 ==========
1434
+ function startSelectMode() {
1435
+ isSelectMode = true;
1436
+ document.body.style.cursor = 'crosshair';
1437
+ document.getElementById('deepspider-btn-select').classList.add('active');
1438
+ document.addEventListener('mousemove', onSelectMove, true);
1439
+ document.addEventListener('click', onSelectClick, true);
1440
+ document.addEventListener('keydown', onSelectKey, true);
1441
+ // 通知所有 iframe 进入选择模式
1442
+ broadcastToIframes({ type: 'deepspider-start-select' });
1443
+ }
1444
+
1445
+ function stopSelectMode() {
1446
+ isSelectMode = false;
1447
+ document.body.style.cursor = '';
1448
+ document.getElementById('deepspider-btn-select').classList.remove('active');
1449
+ overlay.style.display = 'none';
1450
+ infoBox.style.display = 'none';
1451
+ currentElement = null;
1452
+ document.removeEventListener('mousemove', onSelectMove, true);
1453
+ document.removeEventListener('click', onSelectClick, true);
1454
+ document.removeEventListener('keydown', onSelectKey, true);
1455
+ // 通知所有 iframe 退出选择模式
1456
+ broadcastToIframes({ type: 'deepspider-stop-select' });
1457
+ }
1458
+
1459
+ function onSelectMove(e) {
1460
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1461
+ if (!target || target.id?.startsWith('deepspider-')) return;
1462
+ currentElement = target;
1463
+ const rect = target.getBoundingClientRect();
1464
+ overlay.style.left = rect.left + 'px';
1465
+ overlay.style.top = rect.top + 'px';
1466
+ overlay.style.width = rect.width + 'px';
1467
+ overlay.style.height = rect.height + 'px';
1468
+ overlay.style.display = 'block';
1469
+ const tag = target.tagName.toLowerCase();
1470
+ const cls = target.className ? '.' + String(target.className).split(' ')[0] : '';
1471
+ infoBox.textContent = tag + cls;
1472
+ infoBox.style.left = rect.left + 'px';
1473
+ infoBox.style.top = Math.max(0, rect.top - 22) + 'px';
1474
+ infoBox.style.display = 'block';
1475
+ }
1476
+
1477
+ function onSelectClick(e) {
1478
+ if (!currentElement) return;
1479
+ e.preventDefault();
1480
+ e.stopPropagation();
1481
+ const text = currentElement.innerText?.trim().slice(0, 500) || '';
1482
+ const xpath = getXPath(currentElement);
1483
+ stopSelectMode();
1484
+
1485
+ // 显示操作菜单而不是直接发送分析
1486
+ showActionMenu({ text, xpath, url: location.href });
1487
+ }
1488
+
1489
+ function onSelectKey(e) {
1490
+ if (e.key === 'Escape') stopSelectMode();
1491
+ }
1492
+
1493
+ document.getElementById('deepspider-btn-select').onclick = () => {
1494
+ if (isSelectMode) stopSelectMode();
1495
+ else startSelectMode();
1496
+ };
1497
+
1498
+ // ========== 消息渲染 ==========
1499
+ const messagesEl = document.getElementById('deepspider-messages');
1500
+
1501
+ function addMessage(role, content) {
1502
+ console.log('[DeepSpider UI] addMessage:', role, content?.slice(0, 50));
1503
+ deepspider.chatMessages.push({ role, content, time: Date.now() });
1504
+ saveMessages();
1505
+ renderMessages();
1506
+ }
1507
+
1508
+ function renderMessages() {
1509
+ const msgs = deepspider.chatMessages;
1510
+ if (msgs.length === 0) {
1511
+ messagesEl.innerHTML = \`
1512
+ <div class="deepspider-empty">
1513
+ <div class="deepspider-empty-icon">🔍</div>
1514
+ 点击上方 ⦿ 按钮选择页面元素<br>或在下方输入问题开始分析
1515
+ </div>
1516
+ \`;
1517
+ } else {
1518
+ messagesEl.innerHTML = msgs.map(m => {
1519
+ // assistant 消息使用 Markdown 解析,其他消息转义
1520
+ const content = m.role === 'assistant' ? parseMarkdown(m.content) : escapeHtml(m.content);
1521
+ return '<div class="deepspider-msg deepspider-msg-' + m.role + '">' + content + '</div>';
1522
+ }).join('');
1523
+ }
1524
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1525
+ }
1526
+
1527
+ function escapeHtml(str) {
1528
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1529
+ }
1530
+
1531
+ // 使用 marked.js 解析 Markdown
1532
+ function parseMarkdown(text) {
1533
+ if (!text) return '';
1534
+ // 如果 marked 已加载则使用,否则降级为纯文本
1535
+ if (window.marked && window.marked.parse) {
1536
+ try {
1537
+ return window.marked.parse(text);
1538
+ } catch (e) {
1539
+ console.warn('[DeepSpider] marked parse error:', e);
1540
+ }
1541
+ }
1542
+ // 降级:简单转义
1543
+ return '<p>' + escapeHtml(text).replace(/\\n/g, '<br>') + '</p>';
1544
+ }
1545
+
1546
+ // ========== 对话输入 ==========
1547
+ const chatInput = document.getElementById('deepspider-chat-input');
1548
+ document.getElementById('deepspider-btn-send').onclick = sendChat;
1549
+ chatInput.onkeydown = (e) => {
1550
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); }
1551
+ };
1552
+
1553
+ function sendChat() {
1554
+ const text = chatInput.value.trim();
1555
+ if (!text) return;
1556
+ chatInput.value = '';
1557
+ addMessage('user', text);
1558
+ if (typeof __deepspider_send__ === 'function') {
1559
+ __deepspider_send__(JSON.stringify({ type: 'chat', text }));
1560
+ }
1561
+ }
1562
+
1563
+ // ========== 监听 iframe 选中结果 ==========
1564
+ window.addEventListener('message', (e) => {
1565
+ if (e.data?.type === 'deepspider-iframe-selection') {
1566
+ const { text, xpath, iframeSrc } = e.data;
1567
+ stopSelectMode();
1568
+
1569
+ // 显示操作菜单而不是直接发送分析
1570
+ showActionMenu({ text, xpath, url: location.href, iframeSrc });
1571
+ }
1572
+ });
1573
+
1574
+ // ========== 追加到最后一条消息 ==========
1575
+ function appendToLastMessage(role, text) {
1576
+ console.log('[DeepSpider UI] appendToLastMessage:', role, text?.slice(0, 50));
1577
+ const msgs = deepspider.chatMessages;
1578
+ // 查找最后一条同角色消息
1579
+ for (let i = msgs.length - 1; i >= 0; i--) {
1580
+ if (msgs[i].role === role) {
1581
+ msgs[i].content += text;
1582
+ saveMessages();
1583
+ renderMessages();
1584
+ return true;
1585
+ }
1586
+ }
1587
+ // 没找到则创建新消息
1588
+ addMessage(role, text);
1589
+ return true;
1590
+ }
1591
+
1592
+ // ========== 更新最后一条消息(替换内容) ==========
1593
+ function updateLastMessage(role, content) {
1594
+ const msgs = deepspider.chatMessages;
1595
+ for (let i = msgs.length - 1; i >= 0; i--) {
1596
+ if (msgs[i].role === role) {
1597
+ msgs[i].content = content;
1598
+ saveMessages();
1599
+ renderMessages();
1600
+ return true;
1601
+ }
1602
+ }
1603
+ addMessage(role, content);
1604
+ return true;
1605
+ }
1606
+
1607
+ // ========== 暴露 API ==========
1608
+ let reportPath = null;
1609
+ let reportHtmlContent = null;
1610
+ const reportBtn = document.getElementById('deepspider-report-btn');
1611
+ const statusEl = document.getElementById('deepspider-status');
1612
+ const reportContentEl = document.getElementById('deepspider-report-content');
1613
+ const reportCloseBtn = document.getElementById('deepspider-report-close');
1614
+
1615
+ // 关闭报告模态框
1616
+ reportCloseBtn.onclick = () => {
1617
+ reportModal.classList.remove('visible');
1618
+ };
1619
+
1620
+ // ESC 键关闭模态框
1621
+ document.addEventListener('keydown', (e) => {
1622
+ if (e.key === 'Escape' && reportModal.classList.contains('visible')) {
1623
+ reportModal.classList.remove('visible');
1624
+ }
1625
+ });
1626
+
1627
+ reportBtn.onclick = () => {
1628
+ if (reportHtmlContent) {
1629
+ // 显示模态框并填充内容
1630
+ reportContentEl.innerHTML = reportHtmlContent;
1631
+ // 处理代码块,添加复制按钮
1632
+ processCodeBlocks(reportContentEl);
1633
+ reportModal.classList.add('visible');
1634
+ } else if (reportPath) {
1635
+ // 降级:打开文件
1636
+ window.open('file://' + reportPath, '_blank');
1637
+ }
1638
+ };
1639
+
1640
+ // 处理代码块,添加复制按钮
1641
+ function processCodeBlocks(container) {
1642
+ const preElements = container.querySelectorAll('pre');
1643
+ preElements.forEach((pre) => {
1644
+ // 跳过已处理的
1645
+ if (pre.parentElement?.classList?.contains('deepspider-code-block')) return;
1646
+
1647
+ const code = pre.querySelector('code');
1648
+ const codeText = code ? code.textContent : pre.textContent;
1649
+
1650
+ // 检测语言
1651
+ let lang = 'code';
1652
+ if (code?.className) {
1653
+ const match = code.className.match(/language-(\\w+)/);
1654
+ if (match) lang = match[1];
1655
+ }
1656
+
1657
+ // 创建包装容器
1658
+ const wrapper = document.createElement('div');
1659
+ wrapper.className = 'deepspider-code-block';
1660
+
1661
+ // 创建头部
1662
+ const header = document.createElement('div');
1663
+ header.className = 'deepspider-code-header';
1664
+ header.innerHTML = \`
1665
+ <span class="deepspider-code-lang">\${lang}</span>
1666
+ <button class="deepspider-copy-btn" onclick="this.copyCode()">
1667
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1668
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1669
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1670
+ </svg>
1671
+ 复制
1672
+ </button>
1673
+ \`;
1674
+
1675
+ // 绑定复制功能
1676
+ const copyBtn = header.querySelector('.deepspider-copy-btn');
1677
+ copyBtn.onclick = async () => {
1678
+ try {
1679
+ await navigator.clipboard.writeText(codeText);
1680
+ copyBtn.classList.add('copied');
1681
+ copyBtn.innerHTML = \`
1682
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1683
+ <polyline points="20 6 9 17 4 12"></polyline>
1684
+ </svg>
1685
+ 已复制
1686
+ \`;
1687
+ setTimeout(() => {
1688
+ copyBtn.classList.remove('copied');
1689
+ copyBtn.innerHTML = \`
1690
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1691
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1692
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1693
+ </svg>
1694
+ 复制
1695
+ \`;
1696
+ }, 2000);
1697
+ } catch (e) {
1698
+ console.error('[DeepSpider] 复制失败:', e);
1699
+ }
1700
+ };
1701
+
1702
+ // 组装
1703
+ pre.parentNode.insertBefore(wrapper, pre);
1704
+ wrapper.appendChild(header);
1705
+ wrapper.appendChild(pre);
1706
+ });
1707
+ }
1708
+
1709
+ function showReport(pathOrContent, isHtml = false) {
1710
+ if (isHtml) {
1711
+ // 直接传入 HTML 内容
1712
+ reportHtmlContent = pathOrContent;
1713
+ } else {
1714
+ // 传入文件路径(兼容旧逻辑)
1715
+ reportPath = pathOrContent;
1716
+ }
1717
+ reportBtn.classList.add('visible');
1718
+ addMessage('system', '✅ 分析完成,点击上方按钮查看报告');
1719
+ }
1720
+
1721
+ function setBusy(busy) {
1722
+ if (busy) {
1723
+ statusEl.classList.add('busy');
1724
+ } else {
1725
+ statusEl.classList.remove('busy');
1726
+ }
1727
+ }
1728
+
1729
+ function minimize() {
1730
+ panel.classList.add('minimized');
1731
+ minimizeBtn.innerHTML = '&#9633;';
1732
+ minimizeBtn.title = '展开';
1733
+ }
1734
+
1735
+ function maximize() {
1736
+ panel.classList.remove('minimized');
1737
+ minimizeBtn.innerHTML = '&#8722;';
1738
+ minimizeBtn.title = '最小化';
1739
+ }
1740
+
1741
+ deepspider.showPanel = () => panel.classList.add('visible');
1742
+ deepspider.hidePanel = () => panel.classList.remove('visible');
1743
+ deepspider.addMessage = addMessage;
1744
+ deepspider.appendToLastMessage = appendToLastMessage;
1745
+ deepspider.updateLastMessage = updateLastMessage;
1746
+ deepspider.clearMessages = () => { deepspider.chatMessages = []; saveMessages(); renderMessages(); };
1747
+ deepspider.startSelector = startSelectMode;
1748
+ deepspider.stopSelector = stopSelectMode;
1749
+ deepspider.showReport = showReport;
1750
+ deepspider.setBusy = setBusy;
1751
+ deepspider.minimize = minimize;
1752
+ deepspider.maximize = maximize;
1753
+ deepspider.getStages = () => deepspider.stages;
1754
+ deepspider.clearStages = clearAll;
1755
+
1756
+ // 自动显示面板
1757
+ panel.classList.add('visible');
1758
+ // 渲染恢复的消息
1759
+ renderMessages();
1760
+ // 恢复阶段面板
1761
+ updateStagesPanel();
1762
+ console.log('[DeepSpider UI] 分析面板已加载');
1763
+ }
1764
+
1765
+ // DOM 加载检测
1766
+ if (document.body) {
1767
+ initUI();
1768
+ } else if (document.readyState === 'loading') {
1769
+ document.addEventListener('DOMContentLoaded', initUI);
1770
+ } else {
1771
+ const checkBody = setInterval(() => {
1772
+ if (document.body) {
1773
+ clearInterval(checkBody);
1774
+ initUI();
1775
+ }
1776
+ }, 50);
1777
+ }
1778
+ })();
1779
+ `;
1780
+ }
1781
+
1782
+ export default getAnalysisPanelScript;