| 97 | // ---- Factory ---- |
| 98 | |
| 99 | export function createTabTools(deps: { |
| 100 | sender: MessageSend; |
| 101 | summarize: (content: string, prompt: string) => Promise<string>; |
| 102 | }): { tools: Array<{ definition: ToolDefinition; executor: ToolExecutor }> } { |
| 103 | const { sender, summarize } = deps; |
| 104 | |
| 105 | const getTabContentExecutor: ToolExecutor = { |
| 106 | execute: async (args: Record<string, unknown>) => { |
| 107 | const tabId = requireNumber(args, "tab_id"); |
| 108 | const prompt = args.prompt as string | undefined; |
| 109 | const selector = optionalString(args, "selector"); |
| 110 | const maxLength = optionalNumber(args, "max_length"); |
| 111 | |
| 112 | // 校验目标 tab URL 是否允许操作 |
| 113 | const tabInfo = await chrome.tabs.get(tabId); |
| 114 | assertDomUrlAllowed(tabInfo.url || ""); |
| 115 | |
| 116 | // 注入脚本获取页面 HTML |
| 117 | const removeTags = ["script", "style", "noscript", "svg", "link[rel=stylesheet]", "iframe"]; |
| 118 | |
| 119 | const results = await chrome.scripting.executeScript({ |
| 120 | target: { tabId }, |
| 121 | func: (opts: { selector?: string; removeTags: string[] }) => { |
| 122 | const root = opts.selector ? document.querySelector(opts.selector) : document.documentElement; |
| 123 | if (!root) { |
| 124 | return { |
| 125 | html: null, |
| 126 | title: document.title, |
| 127 | url: location.href, |
| 128 | error: `Element not found: ${opts.selector}`, |
| 129 | }; |
| 130 | } |
| 131 | const clone = root.cloneNode(true) as Element; |
| 132 | for (const tag of opts.removeTags) { |
| 133 | clone.querySelectorAll(tag).forEach((el) => el.remove()); |
| 134 | } |
| 135 | return { html: clone.outerHTML, title: document.title, url: location.href }; |
| 136 | }, |
| 137 | args: [{ selector, removeTags }], |
| 138 | world: "MAIN" as chrome.scripting.ExecutionWorld, |
| 139 | }); |
| 140 | |
| 141 | if (!results || results.length === 0) { |
| 142 | throw new Error("Failed to read tab content"); |
| 143 | } |
| 144 | |
| 145 | const pageData = results[0].result as { html: string | null; title: string; url: string; error?: string }; |
| 146 | |
| 147 | if (pageData.error || !pageData.html) { |
| 148 | return JSON.stringify({ |
| 149 | tab_id: tabId, |
| 150 | url: pageData.url, |
| 151 | title: pageData.title, |
| 152 | content: pageData.error || "No content", |
| 153 | truncated: false, |
| 154 | used_selector: selector || null, |
| 155 | }); |
| 156 | } |