Commit 1ee1ea77 authored by Domi's avatar Domi

feat: sidebar multitab

parent f1552320
......@@ -18,8 +18,10 @@
"lodash-es": "^4.17.21",
"mammoth": "^1.6.0",
"pdfjs-dist": "^4.0.269",
"punycode.js": "^2.3.1",
"tiktoken": "^1.0.11",
"turndown": "^7.1.2",
"ua-parser-js": "^1.0.37",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"vue": "^3.3.4",
......@@ -8752,6 +8754,14 @@
"pump": "^2.0.0"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
......@@ -10273,6 +10283,28 @@
"node": ">=14.17"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/uberproto": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz",
......
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#888"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg>
\ No newline at end of file
{
"data": {
"configVersion": 20240310,
"configVersion": 20240330,
"chatDocSites": [
{
"host": "huggingface.co",
......@@ -135,6 +135,13 @@
],
"search": {
"defaultEngine": "google"
},
"embedView": {
"defaultUA": "",
"hostUA": {
"www.google.com": 1,
"bing.com": 1
}
}
}
}
......@@ -6,13 +6,13 @@
font-weight: normal;
}
a,
/* a,
.green {
text-decoration: none;
/* color: hsla(160, 100%, 37%, 1); */
color: hsla(160, 100%, 37%, 1);
color: #38bdf8;
transition: 0.4s;
}
} */
@media (hover: hover) {
a:hover {
......
......@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.setupOffscreenDocument:
return offscreen.setup()
case MessageType.fromOffscreen:
return offscreen.handleMessage(message)
return offscreen.handleResMsg(message)
case MessageType.invokeRequest:
handleInvokeRequest(message, sender)
break
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "@/utils/Invoke"
import { Invoke } from "@/utils/invoke"
let creating: Promise<void> | null // A global promise to avoid concurrency issues
......@@ -60,7 +60,7 @@ class Offscreen extends Invoke {
return { key, response }
}
public handleMessage(message: any): void {
public handleResMsg(message: any): void {
const { type, key, payload, success } = message
if (type === MessageType.fromOffscreen) {
this.setReturnValue(key, success, payload)
......
......@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) {
const sidebar = contentSidebarMap.get(tabId)
if (sidebar) {
await chrome.storage.session.set({
sidebarUrls: { content: sidebar.url },
sidebarInitUrl: { content: sidebar.url },
})
chrome.tabs.sendMessage(tabId, {
type: MessageType.openContentSidebar,
......
......@@ -32,6 +32,7 @@ const engine = ref(Engine.google)
const engineListVisible = ref(false)
let timerId = -1
let timerId2 = -1
watch(focus, (value) => {
if (value) {
......@@ -103,20 +104,37 @@ function switchEngine(value: Engine) {
engine.value = value
chrome.storage.local.set({ searchEngine: value })
}
function handlePointerLeave() {
timerId2 = window.setTimeout(() => {
engineListVisible.value = false
}, 800)
}
function handlePointerEnter() {
clearTimeout(timerId2)
}
</script>
<template>
<div class="relative h-10 w-full">
<div
class="absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden"
:class="[
'absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden',
'focus-within:shadow-md',
]"
@pointerleave="handlePointerLeave"
@pointerenter="handlePointerEnter"
>
<div aria-label="search box" class="flex px-2 items-center">
<button
class="size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer"
:class="[
'group size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer',
]"
@click="engineListVisible = !engineListVisible"
>
<img
class="size-5"
class="size-5 group-active:scale-90 transition-transform"
:src="engine == Engine.bing ? bingImg : googleImg"
/>
</button>
......
......@@ -11,29 +11,32 @@ defineProps<{
title: string
badge?: "popup" | "sidebar" | "remove"
small?: boolean
url?: string
}>()
defineEmits(["click", "remove"])
</script>
<template>
<button
<a
:class="[
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg py-1 px-0.5 hover:bg-background-soft',
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg ',
'py-1 px-0.5 hover:bg-background-soft text-inherit cursor-pointer',
small ? 'w-[58px]' : 'w-16',
]"
@click="$emit('click')"
@click="(e) => (e.preventDefault(), $emit('click'))"
:href="url"
>
<div class="p-2.5 rounded-full bg-background-soft">
<img
class="size-6 rounded"
class="size-6 rounded pointer-events-none"
:src="icon"
:data-fallback="globeImg"
loading="lazy"
@error="handleImgError"
/>
</div>
<div class="flex flex-col justify-center h-8 w-full">
<div class="flex flex-col justify-center h-8 w-full text-center">
<div class="text-xs max-w-full break-words leading-tight line-clamp-2">
{{ title }}
</div>
......@@ -64,7 +67,7 @@ defineEmits(["click", "remove"])
>
<IconClose class="size-3" />
</div>
</button>
</a>
</template>
<style scoped></style>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue"
import config from "@/assets/config.json"
import { getLocal, updateFrameNetRules } from "@/utils/ext"
import { ContentScriptId, FrameMessageType } from "@/types"
import { updateFrameNetRules } from "@/utils/ext"
import {
ContentScriptId,
FrameMessageType,
WindowName,
WebviewFunc,
} from "@/types"
import { findFrameLoadUrl } from "@/utils/utils"
import { fetchDoc } from "@/content/pip"
import { WebviewInvoke } from "@/utils/invoke"
export type PageInfo = {
url: string
title: string
icon: string
}
const props = defineProps<{
url: string
ua?: string
preloadUrl?: string
preloadCandidates?: string[]
}>()
const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }]
load: [PageInfo]
}>()
defineExpose({
......@@ -24,59 +37,42 @@ defineExpose({
const onceCallback = new Map<string, (e: MessageEvent) => boolean>()
const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates)
const inited = ref(false)
const frameUrl = ref("")
const pageInfo = reactive({ url: "", title: "", icon: "" })
const webviewInvoke = ref<WebviewInvoke>()
watch(
() => props.ua,
async (ua) => {
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: ua,
tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
})
}
)
onMounted(() => {
getLocal({
webviewPatchs: config.data.webviewPatchs,
loadCandidates: config.data.loadCandidates,
}).then(({ webviewPatchs, loadCandidates }) => {
if (webviewPatchs) {
patchs.splice(0, patchs.length, ...webviewPatchs)
}
if (loadCandidates) {
loadUrls.splice(0, loadUrls.length, ...loadCandidates)
}
inited.value = true
})
webviewInvoke.value = new WebviewInvoke("webview", frame.value!)
window.addEventListener("message", handleFrameMessage)
console.log("loadFrame", props.url, props.ua)
loadFrame(props.url, props.ua)
})
onUnmounted(() => {
window.removeEventListener("message", handleFrameMessage)
})
const patch = computed(() => {
const url = inited.value ? props.url : ""
const patch = patchs.find((p) => {
try {
return new RegExp(p.re).test(url)
} catch (e) {
console.error(e)
}
return false
})
return {
url,
ua: patch?.ua || props.ua || "",
l: patch?.l || "",
}
})
watch(patch, async (patch) => {
async function loadFrame(url: string, ua?: string) {
const iframe = frame.value
if (!patch.url || !iframe) return
if (!url || !iframe) return
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: patch.ua,
ua: ua,
tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
})
await chrome.scripting.updateContentScripts([
......@@ -90,7 +86,7 @@ watch(patch, async (patch) => {
},
])
frameUrl.value = patch.url
frameUrl.value = url
pageInfo.url = ""
pageInfo.title = ""
pageInfo.icon = ""
......@@ -108,12 +104,19 @@ watch(patch, async (patch) => {
})
} catch (e) {
console.warn(e)
const url = new URL(props.url)
let loadUrl = url.origin + patch.l
if (!loadUrl) {
const u = await findFrameLoadUrl(loadUrls)
let loadUrl = ""
if (props.preloadUrl) {
const u = new URL(props.preloadUrl || "", props.url)
loadUrl = u.href
}
if (!loadUrl && props.preloadCandidates) {
const u = await findFrameLoadUrl(props.preloadCandidates, props.url)
u && (loadUrl = u)
}
if (!loadUrl) {
console.warn("No preload url found")
return
}
try {
frameUrl.value = loadUrl
......@@ -148,7 +151,7 @@ watch(patch, async (patch) => {
},
"*"
)
})
}
function handleFrameMessage(e: MessageEvent) {
console.log("frame message: ", e, e.source !== frame.value?.contentWindow)
......@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) {
pageInfo.title = e.data.title
pageInfo.icon = e.data.icon
emit("pageInfo", pageInfo)
emit("load", pageInfo)
}
break
case FrameMessageType.invokeResponse:
webviewInvoke.value?.handleResMsg(e.data)
break
}
}
function reload() {
console.log("reload")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.reload,
},
"*"
)
console.log("reload", pageInfo.url)
webviewInvoke.value
?.invoke({
func: WebviewFunc.reload,
args: [],
timeout: 1000,
})
.catch((e) => {
console.warn("reload failed", e)
if (frame.value) {
frame.value.src = pageInfo.url || props.url
}
})
}
function goBack() {
console.log("goBack")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.goBack,
},
"*"
)
webviewInvoke.value?.invoke({
func: WebviewFunc.goBack,
args: [],
})
}
function goForward() {
console.log("goForward")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.goForward,
},
"*"
)
webviewInvoke.value?.invoke({
func: WebviewFunc.goForward,
args: [],
})
}
</script>
<template>
<iframe class="w-full h-full bg-white" ref="frame" :src="frameUrl"></iframe>
<iframe
class="w-full h-full bg-white"
ref="frame"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
:src="frameUrl"
:name="WindowName.webview"
></iframe>
</template>
<style scoped></style>
......@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n"
import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { chatDocPrompt } from "@/utils/const"
import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n()
......@@ -211,4 +211,4 @@ onUnmounted(() => {
</div>
</template>
<style scoped></style>
\ No newline at end of file
<style scoped></style>@/utils/const
\ No newline at end of file
<script setup lang="ts">
import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue"
import { chatDocsPanel } from "@/store/content"
import { contentService } from "@/utils/service"
import { contentInvoke } from "@/utils/invoke"
import { convertBlobToBase64, semanticClip } from "@/utils/utils"
import ScrollView from "@/components/ScrollView.vue"
import IconArrowBack from "@/components/icons/IconArrowBack.vue"
......@@ -21,7 +21,7 @@ import {
getInputValue,
} from "@/utils/dom"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { chatDocPrompt } from "@/utils/const"
import { useI18n } from "@/utils/i18n"
type Sheet = "" | "docSelect" | "promptTemplate"
......@@ -126,7 +126,7 @@ watch(
if (maxInputType !== "token") return
if (!message) return
const tokenLength = await contentService.calcTokens(message)
const tokenLength = await contentInvoke.calcTokens(message)
const rate = (message.length / tokenLength) * 0.95
const exceedMaxInput = tokenLength > maxInput
if (exceedMaxInput) {
......@@ -172,7 +172,7 @@ watch(
if (item.kind == "file" && typeof item.data != "string") {
const url = await convertBlobToBase64(item.data)
const results = await contentService.parseDoc({
const results = await contentInvoke.parseDoc({
key: item.key,
type: item.type,
size: item.data.size,
......@@ -640,4 +640,4 @@ input:hover {
transform: translate(100%, 0);
}
}
</style>
\ No newline at end of file
</style>@/utils/const@/utils/invoke/service@/utils/invokeb
\ No newline at end of file
<template>
<svg
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
aria-hidden="true"
height="200px"
width="200px"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.5 18.75a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path>
<path
fill-rule="evenodd"
d="M8.625.75A3.375 3.375 0 005.25 4.125v15.75a3.375 3.375 0 003.375 3.375h6.75a3.375 3.375 0 003.375-3.375V4.125A3.375 3.375 0 0015.375.75h-6.75zM7.5 4.125C7.5 3.504 8.004 3 8.625 3H9.75v.375c0 .621.504 1.125 1.125 1.125h2.25c.621 0 1.125-.504 1.125-1.125V3h1.125c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-6.75A1.125 1.125 0 017.5 19.875V4.125z"
clip-rule="evenodd"
></path>
</svg>
</template>
......@@ -5,7 +5,7 @@ import { getLocal } from "@/utils/ext"
import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import PageScrollbar from "./PageScrollbar.vue"
import { MessageType } from "@/types"
import { FrameMessageType, MessageType } from "@/types"
import { autoPointerCapture } from "@/utils/dom"
const props = defineProps<{
......@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"])
const sidebarHtml = chrome.runtime.getURL("sidebar.html")
const sidebarUrl = sidebarHtml + "?mode=content"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const extRootUrl = chrome.runtime.getURL("/")
let sheet: CSSStyleSheet | null = null
let invisibleTimer = 0
......@@ -62,6 +63,21 @@ function handleKeyUp(e: KeyboardEvent) {
}
}
function handleMessage(e: MessageEvent) {
if (!extRootUrl.startsWith(e.origin)) {
return
}
// console.log(e)
switch (e.data?.type) {
case FrameMessageType.collapseSidebar:
emit("hide")
break
case FrameMessageType.closeSidebar:
emit("close")
break
}
}
watch(
width,
debounce((value) => {
......@@ -90,6 +106,7 @@ onMounted(() => {
updatePageStyle(width.value)
window.addEventListener("keydown", handleKeydown)
window.addEventListener("keyup", handleKeyUp)
window.addEventListener("message", handleMessage)
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
})
......@@ -99,6 +116,7 @@ onUnmounted(() => {
disablePageStyle()
window.removeEventListener("keydown", handleKeydown)
window.removeEventListener("keyup", handleKeyUp)
window.removeEventListener("message", handleMessage)
chrome.runtime.sendMessage({
type: MessageType.unregisterContentSidebar,
})
......@@ -121,7 +139,10 @@ onUnmounted(() => {
@pointerdown="autoPointerCapture"
@pointermove="pointermove"
></div>
<div class="flex gap-2 items-center justify-between h-8 px-2">
<div
v-if="false"
class="flex gap-2 items-center justify-between h-8 px-2"
>
<button
@click=""
class="size-7 flex items-center justify-center mr-auto"
......
......@@ -31,9 +31,10 @@ const emit = defineEmits({
<div class="flex flex-wrap gap-x-4 gap-y-4 justify-center">
<SiteButton
v-for="item of recentItems"
v-for="item of recentItems.slice(0, 12)"
:icon="item.icon"
:title="item.title"
:url="item.url"
badge="remove"
@click="emit('go', item.url)"
@remove="emit('removeRecentItem', item.url)"
......
......@@ -6,15 +6,20 @@ import {
pipWindow,
sidebarAddon,
} from "@/store/content"
import { ContentEventType, FrameMessageType, MessageType } from "@/types"
import {
ContentEventType,
FrameMessageType,
MessageType,
WebviewFunc,
WindowName,
} from "@/types"
import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
import {
dispatchContentEvent,
addContentEventListener,
removeContentEventListener,
} from "@/content/event"
import { contentService } from "@/utils/service"
import { contentInvoke } from "@/utils/invoke"
import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip"
......@@ -47,7 +52,7 @@ function handleMessage(
})
break
case MessageType.invokeResponse:
contentService.handleMessage(message)
contentInvoke.handleResMsg(message)
break
case MessageType.showChatDocs:
chatDocsPanel.visible = true
......@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) {
type: ContentEventType.escapeLoad,
detail: { url: e.data.url },
})
case FrameMessageType.reload:
return location.reload()
case FrameMessageType.goBack:
return history.back()
case FrameMessageType.goForward:
return history.forward()
case FrameMessageType.invokeRequest:
handleInvokeRequest(e.data, e.source as Window)
break
}
}
async function handleInvokeRequest(message: any, source: Window) {
const { key, func, args } = message
let result = null
let error = null
try {
switch (func) {
case WebviewFunc.reload:
location.reload()
break
case WebviewFunc.goBack:
history.back()
break
case WebviewFunc.goForward:
history.forward()
break
}
} catch (err) {
console.error("invoke error: ", err)
error = err
}
console.log("invoke response: ", result, error)
source.postMessage(
{
type: FrameMessageType.invokeResponse,
key,
success: !error,
payload: !error ? result : error,
},
chrome.runtime.getURL("")
)
}
if (window.self == window.parent) {
if (window.top === window) {
run()
} else {
}
// webview
if (window.top !== window && window.name.startsWith(WindowName.webview)) {
window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage(
{
......
import { ContentEventType } from "@/types"
import { ContentEventType, WindowName } from "@/types"
import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
......@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) {
addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent)
addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent)
if (window.name.startsWith(WindowName.webview)) {
window.parent = window
window.top = window
}
......@@ -21,6 +21,7 @@ import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue"
import { getIsEdge } from "@/utils/ext"
import { handleImgError } from "@/utils/dom"
import { homeUrl, feedbackUrl } from "@/utils/const"
const globeImg = chrome.runtime.getURL("img/globe.svg")
......@@ -151,7 +152,7 @@ async function openSidebar(url = "") {
})
const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({
sidebarUrls: { sidepanel: url },
sidebarInitUrl: { sidepanel: url },
})
await chrome.sidePanel.open({ windowId: win.id! })
......@@ -160,7 +161,7 @@ async function openSidebar(url = "") {
async function openContentSidebar(url = "") {
await chrome.storage.session.set({
sidebarUrls: { content: url },
sidebarInitUrl: { content: url },
})
await chrome.tabs.sendMessage(activeTab.value.id!, {
type: MessageType.openContentSidebar,
......@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) {
}
function feedback() {
open("https://tawk.to/anythingcopilot", "_blank")
open(feedbackUrl, "_blank")
}
function fivestar() {
open(
......@@ -194,7 +195,7 @@ function fivestar() {
}
function goHome() {
open("https://ziziyi.com/copilot", "_blank")
open(homeUrl, "_blank")
}
function showChatDocs() {
......@@ -347,6 +348,7 @@ function showChatDocs() {
:icon="item.icon"
:title="item.title"
:badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'"
:url="item.url"
small
@click="
() => {
......
This diff is collapsed.
......@@ -54,7 +54,18 @@ export enum FrameMessageType {
webviewRun = "anything-copilot_webview-run",
escapeLoad = "anything-copilot_escape-load",
pageInfo = "anything-copilot_page-info",
reload = "anything-copilot_reload",
goBack = "anything-copilot_go-back",
goForward = "anything-copilot_go-forward",
collapseSidebar = "anything-copilot_collapse-sidebar",
closeSidebar = "anything-copilot_close-sidebar",
invokeRequest = "anything-copilot_invoke-request",
invokeResponse = "anything-copilot_invoke-response",
}
export enum WindowName {
webview = "anything-copilot_webview",
}
export enum WebviewFunc {
reload = "reload",
goBack = "goBack",
goForward = "goForward",
}
export const chatDocPrompt = `Here is some relevant reference information. Please refrain from summarizing or responding to specific content until I pose targeted questions. If you comprehend this instruction, kindly reply with "Okay, please continue."`
export const homeUrl = 'https://ziziyi.com/copilot'
export const feedbackUrl = 'https://tawk.to/anythingcopilot'
......@@ -217,7 +217,7 @@ export async function waitFor(
}
export function getPageIcon() {
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel="icon"]')
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]')
for (let item of icons) {
if (item.getAttribute("type") == "image/svg+xml") {
return item.href
......@@ -231,6 +231,13 @@ export function getPageIcon() {
return icons[0].href
}
const touchIcon = document.querySelector<HTMLLinkElement>(
'link[rel="apple-touch-icon"]'
)
if (touchIcon) {
return touchIcon.href
}
return location.origin + "/favicon.ico"
}
......
......@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) {
}
type NetRulesOptions = {
id?: number
ua?: string
tabIds?: number[]
requestDomains?: string[]
initiatorDomains?: string[]
enabled?: boolean
}
export async function updateFrameNetRules(
{ ua, tabIds, initiatorDomains }: NetRulesOptions = {
tabIds: [-1],
initiatorDomains: [chrome.runtime.id],
}
) {
export async function updateFrameNetRules({
id,
ua,
tabIds,
initiatorDomains,
requestDomains,
enabled,
}: NetRulesOptions) {
ua = ua || navigator.userAgent
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [1],
addRules: [
{
id: 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
{
header: "X-Frame-Options",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
removeRuleIds: [id || 1],
addRules:
enabled != false
? [
{
header: "Content-Security-Policy",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
id: id || 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType
.MODIFY_HEADERS,
responseHeaders: [
{
header: "X-Frame-Options",
operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
{
header: "Content-Security-Policy",
operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
{
header: "Sec-Fetch-Dest",
value: "document",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
},
]
: [],
})
}
export async function updateUANetRules({
id,
ua,
requestDomains,
initiatorDomains,
tabIds,
enabled,
}: NetRulesOptions & { id: number }) {
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [id],
addRules:
enabled != false
? [
{
header: "Sec-Fetch-Dest",
value: "document",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
id,
priority: 2,
action: {
type: chrome.declarativeNetRequest.RuleActionType
.MODIFY_HEADERS,
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
},
],
},
condition: {
resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
tabIds,
},
},
],
]
: [],
})
}
......@@ -250,12 +310,13 @@ export function isProtectedUrl(url: string) {
}
const isEdge = getIsEdge()
if (isEdge && u.hostname == "microsoftedge.microsoft.com") {
return true
}
if (u.hostname == "chrome.google.com") {
if (
["chromewebstore.google.com", "chrome.google.com"].includes(u.hostname)
) {
return true
}
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "./Invoke"
class Service extends Invoke {
class ContentInvoke extends Invoke {
public async send(req: any) {
const key = this.key
const response = await chrome.runtime.sendMessage({
......@@ -13,7 +13,7 @@ class Service extends Invoke {
return { key, response }
}
public handleMessage(message: any) {
public handleResMsg(message: any) {
if (message?.type === MessageType.invokeResponse) {
const { key, success, payload } = message
this.setReturnValue(key, success, payload)
......@@ -42,6 +42,6 @@ class Service extends Invoke {
}
}
const contentService = new Service("content")
const contentInvoke = new ContentInvoke("content")
export { Service, contentService }
export { ContentInvoke, contentInvoke }
......@@ -6,6 +6,13 @@ type CallbackItem = {
interface InvokeReq {
func: string
args: any[]
timeout?: number
}
interface InvokeRes {
key: string
success: boolean
value: any
}
abstract class Invoke {
......@@ -25,14 +32,14 @@ abstract class Invoke {
return `${this.name}-${this.uniqueId}-${this.count++}`
}
protected getReturnValue(key: string, req: InvokeReq, timeout?: number) {
protected getReturnValue(key: string, req: InvokeReq) {
let timer: any
const { func, timeout } = req
const promise = new Promise<any>((resolve, reject) => {
this.pendingCallback[key] = { resolve, reject }
if (timeout) {
timer = setTimeout(
() =>
reject(`"${this.name}" invoke timeout: ${req.func} key: ${key}`),
() => reject(`"${this.name}" invoke timeout: ${func} key: ${key}`),
timeout
)
}
......@@ -46,11 +53,11 @@ abstract class Invoke {
return promise
}
protected setReturnValue(key: string, success: boolean, payload: any) {
protected setReturnValue(key: string, success: boolean, value: any) {
const callback = this.pendingCallback[key]
if (callback) {
const fn = success != false ? callback.resolve : callback.reject
fn(payload)
fn(value)
} else {
console.error(`unknown invoke callback message: ${key}`)
console.log(this.pendingCallback)
......@@ -58,11 +65,12 @@ abstract class Invoke {
}
abstract send(req: InvokeReq): Promise<{ key: string; response: any }>
abstract handleMessage(message: InvokeReq): void
abstract handleResMsg(message: InvokeRes): void
public async invoke<T = any>(req: InvokeReq, timeout = 20000): Promise<T> {
public async invoke<T = any>(req: InvokeReq): Promise<T> {
const { key } = await this.send(req)
return this.getReturnValue(key, req, timeout)
req.timeout = req.timeout || 20000
return this.getReturnValue(key, req)
}
}
......
import { FrameMessageType } from "@/types"
import Invoke from "./Invoke"
class WebviewInvoke extends Invoke {
private frame: HTMLIFrameElement | null = null
constructor(name: string, frame: HTMLIFrameElement) {
super(name)
this.frame = frame
}
public async send(req: any) {
const key = this.key
const win = this.frame?.contentWindow
if (!win) {
console.warn("WebviewInvoke: frame not ready", this.frame)
}
win?.postMessage({ type: FrameMessageType.invokeRequest, key, ...req }, "*")
return { key, response: null }
}
public handleResMsg(message: any) {
if (message?.type === FrameMessageType.invokeResponse) {
const { key, success, payload } = message
this.setReturnValue(key, success, payload)
}
}
}
export { WebviewInvoke }
export { Invoke } from "./Invoke"
export { ContentInvoke, contentInvoke } from "./ContentInvoke"
export { WebviewInvoke } from "./WebviewInvoke"
......@@ -52,7 +52,10 @@ export const semanticClip = (text: string, maxLength: number) => {
}
/** Find URL from the same origin that can be embedded */
export async function findFrameLoadUrl(urls: string[]): Promise<string> {
export async function findFrameLoadUrl(
urls: string[],
base?: string
): Promise<string> {
const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null
......@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
return !csp.includes("frame-ancestors")
}
const promises = urls.map((url) =>
fetch(url, {
const promises = urls.map((url) => {
const u = new URL(url, base)
return fetch(u.href, {
signal: abortController.signal,
})
.then((res) => {
......@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
const xFrameOptions = h.get("X-Frame-Options")
const csp = h.get("Content-Security-Policy")
if (!xFrameOptions && checkCSP(csp)) {
resolve && resolve(url)
resolve && resolve(u.href)
abortController.abort()
}
})
.catch(() => {})
)
})
Promise.all(promises).then(() => {
resolve && resolve("")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment