koishi-plugin-isthattrue 0.2.2 → 0.2.5

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 (58) hide show
  1. package/README.md +80 -74
  2. package/client/index.ts +325 -0
  3. package/dist/index.js +230 -0
  4. package/lib/agents/deepSearchController.d.ts +39 -0
  5. package/lib/agents/deepSearchController.js +422 -0
  6. package/lib/agents/index.d.ts +1 -2
  7. package/lib/agents/index.js +3 -5
  8. package/lib/agents/subSearchAgent.d.ts +12 -18
  9. package/lib/agents/subSearchAgent.js +56 -29
  10. package/lib/config.d.ts +2 -36
  11. package/lib/config.js +86 -63
  12. package/lib/index.d.ts +10 -6
  13. package/lib/index.js +93 -166
  14. package/lib/services/chatluna.d.ts +11 -63
  15. package/lib/services/chatluna.js +87 -134
  16. package/lib/services/deepSearchTaskService.d.ts +32 -0
  17. package/lib/services/deepSearchTaskService.js +270 -0
  18. package/lib/services/deepSearchTool.d.ts +4 -0
  19. package/lib/services/deepSearchTool.js +222 -0
  20. package/lib/services/factCheckTool.d.ts +4 -0
  21. package/lib/services/factCheckTool.js +347 -0
  22. package/lib/services/grokWebSearch.d.ts +18 -0
  23. package/lib/services/grokWebSearch.js +93 -0
  24. package/lib/services/iterativeSearchAgent.d.ts +22 -0
  25. package/lib/services/iterativeSearchAgent.js +179 -0
  26. package/lib/services/jinaReader.d.ts +15 -0
  27. package/lib/services/jinaReader.js +89 -0
  28. package/lib/services/webFetchTool.d.ts +4 -0
  29. package/lib/services/webFetchTool.js +134 -0
  30. package/lib/types.d.ts +145 -78
  31. package/lib/types.js +0 -3
  32. package/lib/utils/async.d.ts +1 -0
  33. package/lib/utils/async.js +18 -0
  34. package/lib/utils/http.d.ts +2 -0
  35. package/lib/utils/http.js +12 -0
  36. package/lib/utils/model.d.ts +1 -0
  37. package/lib/utils/model.js +20 -0
  38. package/lib/utils/prompts.d.ts +57 -43
  39. package/lib/utils/prompts.js +300 -123
  40. package/lib/utils/search.d.ts +2 -0
  41. package/lib/utils/search.js +32 -0
  42. package/lib/utils/summary.d.ts +6 -0
  43. package/lib/utils/summary.js +69 -0
  44. package/lib/utils/text.d.ts +1 -0
  45. package/lib/utils/text.js +11 -0
  46. package/lib/utils/url.d.ts +3 -10
  47. package/lib/utils/url.js +68 -23
  48. package/package.json +64 -49
  49. package/lib/agents/mainAgent.d.ts +0 -30
  50. package/lib/agents/mainAgent.js +0 -136
  51. package/lib/agents/verifyAgent.d.ts +0 -37
  52. package/lib/agents/verifyAgent.js +0 -160
  53. package/lib/services/chatlunaSearch.d.ts +0 -38
  54. package/lib/services/chatlunaSearch.js +0 -280
  55. package/lib/services/messageParser.d.ts +0 -38
  56. package/lib/services/messageParser.js +0 -184
  57. package/lib/services/tofTool.d.ts +0 -4
  58. package/lib/services/tofTool.js +0 -74
package/README.md CHANGED
@@ -1,103 +1,109 @@
1
- # koishi-plugin-isthattrue
1
+ # koishi-plugin-chatluna-fact-check
2
2
 
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-isthattrue?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-isthattrue)
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-chatluna-fact-check?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-chatluna-fact-check)
4
4
 
5
- 事实核查插件 - 使用多 Agent LLM 架构验证消息真实性
6
-
7
- ## 功能特点
8
-
9
- - 多 Agent 协作:主控 Agent 编排任务,子搜索 Agent 并行检索
10
- - 多搜索源支持:Tavily、Anspire、Kimi、智谱、Chatluna Search
11
- - 支持文本和图片内容(OCR 识别)
12
- - 可引用消息进行核查
13
- - 输出判定结果:TRUE / FALSE / PARTIALLY_TRUE / UNCERTAIN
5
+ 事实核查插件,提供两个核心工具:
6
+ - `fact_check`:快速搜索聚合(默认)
7
+ - `deep_search`:迭代式深度搜索(可选)
14
8
 
15
9
  ## 安装
16
10
 
17
11
  ```bash
18
- npm install koishi-plugin-isthattrue
12
+ npm install koishi-plugin-chatluna-fact-check
19
13
  ```
20
14
 
21
- ## 依赖
15
+ ## 最小配置
16
+
17
+ ```yaml
18
+ chatluna-fact-check:
19
+ api:
20
+ apiKeys:
21
+ - [ollama, '', 'https://ollama.com/api/web_search', true]
22
+ factCheck:
23
+ enableChatlunaSearch: false
24
+ chatlunaSearchModel: ''
25
+ chatlunaSearchDiversifyModel: ''
26
+ timeout: 60000
27
+ maxRetries: 2
28
+ proxyMode: follow-global
29
+ proxyAddress: ''
30
+ logLLMDetails: false
31
+ agent:
32
+ enable: true
33
+ enableQuickTool: true
34
+ quickToolName: fact_check
35
+ grokModel: x-ai/grok-4-1
36
+ deepSearch:
37
+ enable: false
38
+ ```
22
39
 
23
- - [koishi-plugin-chatluna](https://github.com/ChatLunaLab/chatluna) - LLM 服务接入
40
+ ## API Key / Base URL 对照表
24
41
 
25
- ## 使用方法
42
+ 推荐优先在 `api.apiKeys` 表格中集中填写(来源 / key / base url / 启用开关)。
26
43
 
27
- ```
28
- tof [内容] # 核查指定内容
29
- tof # 引用一条消息后使用,核查被引用的消息
30
- ```
44
+ | key | base url | 说明 |
45
+ |---|---|---|
46
+ | `api.apiKeys` 中 `provider=ollama` 的 `apiKey` | `api.apiKeys` 中 `provider=ollama` 的 `baseUrl` | 统一填写 Ollama 凭据(唯一入口);key 留空再回退 `OLLAMA_API_KEY` |
47
+ | `N/A`(`factCheck.enableChatlunaSearch`) | `N/A` | 依赖 `chatluna-search-service` 内部配置,不在本插件配置 API key |
31
48
 
32
- 别名:`鉴定`、`核查`
49
+ ## FactCheck 配置
33
50
 
34
- ## 配置说明
51
+ `factCheck` 负责核查流程的运行参数:
52
+ - Chatluna 搜索模型:`factCheck.chatlunaSearchModel`
53
+ - 搜索关键词多样化:`factCheck.chatlunaSearchDiversifyModel`
54
+ - 搜索集成开关:`factCheck.enableChatlunaSearch`
55
+ - 超时与重试:`factCheck.timeout` / `factCheck.maxRetries`
56
+ - 代理与调试:`factCheck.proxyMode` / `factCheck.proxyAddress` / `factCheck.logLLMDetails`
35
57
 
36
- ### 模型配置
58
+ ### Gemini 搜索模型要求
37
59
 
38
- | 配置项 | 说明 | 推荐 |
39
- |--------|------|------|
40
- | mainModel | 主控 Agent 模型,用于编排和最终判决 | Gemini-3-Flash |
41
- | subSearchModel | 子搜索 Agent 模型,用于深度搜索 | Grok-4-1 |
60
+ `factCheck.chatlunaSearchModel`(及 `agent.geminiModel`)填写 Gemini 模型时,**必须满足以下条件**,否则会报 `Cannot read properties of undefined (reading 'model')` 错误:
42
61
 
43
- ### 搜索 API 配置
62
+ 1. **使用 Gemini 适配器**(`koishi-plugin-chatluna-gemini-adapter`),不可使用 OpenAI 兼容中转。
63
+ 2. **Gemini 适配器内仅开启**以下两项联网工具,其余工具(如代码执行等)关闭:
64
+ - `Google Search`(网络搜索)
65
+ - `URL context`(URL 内容读取)
44
66
 
45
- | 配置项 | 说明 |
46
- |--------|------|
47
- | tavilyApiKey | Tavily API Key(可选) |
48
- | anspireApiKey | Anspire API Key(可选) |
49
- | kimiApiKey | Kimi API Key(可选) |
50
- | zhipuApiKey | 智谱 API Key(可选) |
51
- | chatlunaSearchModel | Chatluna Search 使用的模型 |
52
- | enableChatlunaSearch | 启用 Chatluna 搜索集成 |
53
- | chatlunaSearchDiversifyModel | 搜索关键词多样化模型 |
67
+ > 原因:`chatluna-search-service` `web_search` 工具在 `summaryType` 为 `balanced` 时需要向 LLM 发送 configurable,若适配器或工具配置不兼容,configurable 解析的 `model` 字段会为 undefined 导致崩溃。Gemini 原生适配器 + 仅开启搜索类工具可规避此问题。
54
68
 
55
- ### Agent 配置
69
+ ## 搜索源上下文注入
56
70
 
57
- | 配置项 | 默认值 | 说明 |
58
- |--------|--------|------|
59
- | timeout | 60000 | 单次请求超时时间(毫秒) |
60
- | maxRetries | 2 | 失败重试次数 |
71
+ ### 1) `fact_check` 追加上下文(仅附加参考,不改变最终判定)
61
72
 
62
- ### 其他设置
73
+ - Chatluna Search:`agent.appendChatlunaSearchContext` + `agent.chatlunaSearchContext*`
74
+ - Ollama Search:`agent.appendOllamaSearchContext` + `agent.ollamaSearchContext*`
63
75
 
64
- | 配置项 | 默认值 | 说明 |
65
- |--------|--------|------|
66
- | verbose | false | 显示详细验证过程 |
67
- | outputFormat | auto | 输出格式(auto/markdown/plain) |
68
- | useForwardMessage | true | 使用合并转发消息展示详情(仅 QQ) |
69
- | bypassProxy | false | 绕过系统代理 |
70
- | logLLMDetails | false | 打印 LLM 请求详情(调试用) |
76
+ 触发条件与失败行为:
77
+ - 开关为 `true` 且依赖可用才会执行
78
+ - 超时或调用失败会“跳过该上下文”,不会中断 `fact_check`
71
79
 
72
- ## 工作流程
80
+ ### 2) `deep_search` 迭代来源
73
81
 
74
- 1. 用户发送 `tof` 命令(可引用消息或直接输入内容)
75
- 2. MessageParser 提取文本和图片,如有图片则进行 OCR
76
- 3. 主控 Agent 分析内容,生成搜索计划
77
- 4. 子搜索 Agent 并行执行多源搜索
78
- 5. 主控 Agent 综合搜索结果,输出最终判定
82
+ - `deepSearch.useChatlunaSearchTool`:调用 `web_search`
83
+ - `api.apiKeys` 中启用 `ollama` 行:允许 DeepSearch 调用 Ollama Search
79
84
 
80
- ## 架构
85
+ 失败行为:
86
+ - 工具调用失败时回退到模型搜索
87
+ - 若来源整体不可用,最终报告会降置信度并提示来源不足
81
88
 
89
+ ## DeepSearch(可选)
90
+
91
+ 推荐配置:
92
+
93
+ ```yaml
94
+ chatluna-fact-check:
95
+ deepSearch:
96
+ enable: true
97
+ controllerModel: google/gemini-3-flash
98
+ maxIterations: 3
99
+ perIterationTimeout: 30000
100
+ useChatlunaSearchTool: true
82
101
  ```
83
- src/
84
- ├── index.ts # 插件入口,注册 tof 命令
85
- ├── config.ts # 配置 Schema
86
- ├── types.ts # TypeScript 类型定义
87
- ├── agents/
88
- │ ├── mainAgent.ts # 主控 Agent(编排 + 判决)
89
- │ └── subSearchAgent.ts # 子搜索 Agent(多源检索)
90
- ├── services/
91
- │ ├── chatluna.ts # Chatluna LLM 适配器
92
- │ ├── chatlunaSearch.ts # Chatluna Search 集成
93
- │ ├── messageParser.ts # 消息解析(文本/图片)
94
- │ ├── tavily.ts # Tavily 搜索
95
- │ ├── anspire.ts # Anspire 搜索
96
- │ ├── kimi.ts # Kimi 搜索
97
- │ └── zhipu.ts # 智谱搜索
98
- └── utils/
99
- └── prompts.ts # LLM Prompt 模板
100
- ```
102
+
103
+ ## 调试与排障
104
+
105
+ - `factCheck.proxyMode`:排障建议先设为 `direct`
106
+ - `factCheck.logLLMDetails`:仅排障时打开
101
107
 
102
108
  ## License
103
109
 
@@ -0,0 +1,325 @@
1
+ import { Context } from '@koishijs/client'
2
+ import { computed, defineComponent, inject, onBeforeUnmount, onMounted, type ComputedRef, watch, h } from 'vue'
3
+
4
+ type NavSection = {
5
+ key: 'tools' | 'models' | 'search' | 'services' | 'debug'
6
+ title: string
7
+ }
8
+
9
+ type NavGroup = {
10
+ title: string
11
+ sections: NavSection[]
12
+ }
13
+
14
+ const PLUGIN_NAMES = new Set([
15
+ 'isthattrue', // legacy package name
16
+ 'chatluna-fact-check',
17
+ 'koishi-plugin-isthattrue', // legacy package name
18
+ 'koishi-plugin-chatluna-fact-check',
19
+ ])
20
+
21
+ const NAV_GROUPS: NavGroup[] = [
22
+ {
23
+ title: '工具与模型',
24
+ sections: [
25
+ { key: 'tools', title: '工具注册' },
26
+ { key: 'models', title: 'LLM AI 接入' },
27
+ ],
28
+ },
29
+ {
30
+ title: '搜索与服务',
31
+ sections: [
32
+ { key: 'search', title: '搜索策略' },
33
+ { key: 'services', title: '外部服务' },
34
+ ],
35
+ },
36
+ {
37
+ title: '调试/兼容',
38
+ sections: [
39
+ { key: 'debug', title: '调试与排障' },
40
+ ],
41
+ },
42
+ ]
43
+
44
+ const NAV_SECTIONS: NavSection[] = NAV_GROUPS.flatMap((group) => group.sections)
45
+ const SECTION_TITLE_ALIASES: Record<NavSection['key'], string[]> = {
46
+ tools: ['工具注册', 'Fact Check 工具', 'Deep Search 工具', 'Web Fetch 工具'],
47
+ models: ['LLM AI 接入', '模型接入', 'AI 模型接入'],
48
+ search: ['搜索策略', '搜索配置', '超时配置', '排序与策略', '最大字数'],
49
+ services: ['外部服务', 'Grok 网络搜索', 'Jina Reader 配置'],
50
+ debug: ['调试与排障', '调试'],
51
+ }
52
+
53
+ const STYLE_ID = 'isthattrue-nav-style'
54
+
55
+ function ensureStyle() {
56
+ if (document.getElementById(STYLE_ID)) return
57
+ const style = document.createElement('style')
58
+ style.id = STYLE_ID
59
+ style.textContent = `
60
+ .isthattrue-nav {
61
+ position: fixed;
62
+ top: 260px;
63
+ right: 60px;
64
+ z-index: 1000;
65
+ width: 140px;
66
+ max-width: 90vw;
67
+ user-select: none;
68
+ }
69
+ .isthattrue-nav-header {
70
+ padding: 6px 10px;
71
+ border-radius: 999px;
72
+ border: 1px solid var(--k-color-border, #4b5563);
73
+ background: color-mix(in srgb, var(--k-color-bg, #1f2937) 94%, white);
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: space-between;
77
+ cursor: move;
78
+ touch-action: none;
79
+ }
80
+ .isthattrue-nav-handle {
81
+ color: var(--k-text-light, #9ca3af);
82
+ font-size: 14px;
83
+ line-height: 1;
84
+ }
85
+ .isthattrue-nav-toggle {
86
+ border: none;
87
+ background: transparent;
88
+ color: var(--k-text-light, #9ca3af);
89
+ cursor: pointer;
90
+ padding: 0;
91
+ font-size: 14px;
92
+ line-height: 1;
93
+ }
94
+ .isthattrue-nav-body {
95
+ margin-top: 8px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 2px;
99
+ }
100
+ .isthattrue-nav.collapsed .isthattrue-nav-body {
101
+ display: none;
102
+ }
103
+ .isthattrue-nav-item {
104
+ border: none;
105
+ background: transparent;
106
+ color: var(--k-text, #d1d5db);
107
+ text-align: left;
108
+ padding: 6px 4px;
109
+ cursor: pointer;
110
+ font-size: 14px;
111
+ line-height: 1.4;
112
+ }
113
+ .isthattrue-nav-item:hover {
114
+ color: var(--k-color-primary, #4f7cff);
115
+ }
116
+ .isthattrue-nav-item.active {
117
+ color: var(--k-color-primary, #4f7cff);
118
+ }
119
+ .isthattrue-nav-group {
120
+ margin-top: 4px;
121
+ padding: 6px 4px 2px;
122
+ font-size: 12px;
123
+ font-weight: 600;
124
+ color: var(--k-text-light, #9ca3af);
125
+ opacity: 0.9;
126
+ }
127
+ /* Shrink nested sub-section headers inside intersect groups (e.g. DeepSearch sub-sections) */
128
+ .k-schema-group .k-schema-group .k-schema-header {
129
+ font-size: 0.85em;
130
+ margin-top: 0.4em;
131
+ margin-bottom: 0.2em;
132
+ }
133
+ `
134
+ document.head.appendChild(style)
135
+ }
136
+
137
+ function normalizeText(text: string) {
138
+ return text.replace(/\s+/g, '').trim()
139
+ }
140
+
141
+ function getSectionNodes() {
142
+ return Array.from(document.querySelectorAll<HTMLElement>(
143
+ '.k-schema-section-title, .k-schema-header, h2.k-schema-header'
144
+ ))
145
+ }
146
+
147
+ function findHeaderBySection(section: NavSection) {
148
+ const targets = [section.title, ...(SECTION_TITLE_ALIASES[section.key] || [])]
149
+ .map(item => normalizeText(item))
150
+ .filter(Boolean)
151
+ const headers = getSectionNodes()
152
+ for (const header of headers) {
153
+ const text = normalizeText(header.textContent || '')
154
+ if (!text) continue
155
+ if (targets.some(target => text.includes(target))) return header
156
+ }
157
+ return null
158
+ }
159
+
160
+ function matchSectionByHeaderText(text: string): NavSection | undefined {
161
+ const normalized = normalizeText(text)
162
+ return NAV_SECTIONS.find((section) => {
163
+ const candidates = [section.title, ...(SECTION_TITLE_ALIASES[section.key] || [])]
164
+ .map(item => normalizeText(item))
165
+ .filter(Boolean)
166
+ return candidates.some(candidate => normalized.includes(candidate))
167
+ })
168
+ }
169
+
170
+ function mountFloatingNav() {
171
+ ensureStyle()
172
+
173
+ const existing = document.querySelector<HTMLElement>('.isthattrue-nav')
174
+ existing?.remove()
175
+
176
+ const root = document.createElement('div')
177
+ root.className = 'isthattrue-nav'
178
+ root.innerHTML = `
179
+ <div class="isthattrue-nav-header">
180
+ <span class="isthattrue-nav-handle">⋮⋮</span>
181
+ <button class="isthattrue-nav-toggle" type="button">⌄</button>
182
+ </div>
183
+ <div class="isthattrue-nav-body"></div>
184
+ `
185
+ document.body.appendChild(root)
186
+
187
+ const body = root.querySelector<HTMLElement>('.isthattrue-nav-body')!
188
+ const toggle = root.querySelector<HTMLButtonElement>('.isthattrue-nav-toggle')!
189
+ const header = root.querySelector<HTMLElement>('.isthattrue-nav-header')!
190
+
191
+ const itemMap = new Map<string, HTMLButtonElement>()
192
+ for (const group of NAV_GROUPS) {
193
+ const groupTitle = document.createElement('div')
194
+ groupTitle.className = 'isthattrue-nav-group'
195
+ groupTitle.textContent = group.title
196
+ body.appendChild(groupTitle)
197
+
198
+ for (const section of group.sections) {
199
+ const button = document.createElement('button')
200
+ button.type = 'button'
201
+ button.className = 'isthattrue-nav-item'
202
+ button.textContent = section.title
203
+ button.addEventListener('click', () => {
204
+ const target = findHeaderBySection(section)
205
+ if (target) {
206
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
207
+ }
208
+ })
209
+ body.appendChild(button)
210
+ itemMap.set(section.key, button)
211
+ }
212
+ }
213
+
214
+ toggle.addEventListener('click', (event) => {
215
+ event.stopPropagation()
216
+ const collapsed = root.classList.toggle('collapsed')
217
+ toggle.textContent = collapsed ? '⌃' : '⌄'
218
+ })
219
+
220
+ let dragStartX = 0
221
+ let dragStartY = 0
222
+ let startRight = 0
223
+ let startTop = 0
224
+
225
+ header.addEventListener('pointerdown', (event) => {
226
+ const target = event.target as HTMLElement
227
+ if (target.closest('.isthattrue-nav-toggle')) return
228
+ event.preventDefault()
229
+ header.setPointerCapture(event.pointerId)
230
+ dragStartX = event.clientX
231
+ dragStartY = event.clientY
232
+ startRight = parseFloat(root.style.right || '60')
233
+ startTop = parseFloat(root.style.top || '260')
234
+ })
235
+
236
+ header.addEventListener('pointermove', (event) => {
237
+ if (!header.hasPointerCapture(event.pointerId)) return
238
+ const dx = event.clientX - dragStartX
239
+ const dy = event.clientY - dragStartY
240
+ root.style.top = `${Math.max(0, startTop + dy)}px`
241
+ root.style.right = `${Math.max(0, startRight - dx)}px`
242
+ })
243
+
244
+ const onPointerEnd = (event: PointerEvent) => {
245
+ if (header.hasPointerCapture(event.pointerId)) {
246
+ header.releasePointerCapture(event.pointerId)
247
+ }
248
+ }
249
+ header.addEventListener('pointerup', onPointerEnd)
250
+ header.addEventListener('pointercancel', onPointerEnd)
251
+
252
+ let observer: IntersectionObserver | null = null
253
+ const refreshActive = () => {
254
+ observer?.disconnect()
255
+ observer = new IntersectionObserver((entries) => {
256
+ for (const entry of entries) {
257
+ if (!entry.isIntersecting) continue
258
+ const text = (entry.target.textContent || '').trim()
259
+ const section = matchSectionByHeaderText(text)
260
+ if (!section) continue
261
+ for (const item of itemMap.values()) item.classList.remove('active')
262
+ itemMap.get(section.key)?.classList.add('active')
263
+ break
264
+ }
265
+ }, {
266
+ root: null,
267
+ rootMargin: '-20% 0px -60% 0px',
268
+ threshold: 0,
269
+ })
270
+
271
+ const headers = getSectionNodes()
272
+ for (const node of headers) {
273
+ const text = node.textContent || ''
274
+ if (matchSectionByHeaderText(text)) {
275
+ observer.observe(node)
276
+ }
277
+ }
278
+ }
279
+
280
+ const mutationObserver = new MutationObserver(() => {
281
+ window.setTimeout(refreshActive, 200)
282
+ })
283
+ mutationObserver.observe(document.body, { childList: true, subtree: true })
284
+
285
+ window.setTimeout(refreshActive, 300)
286
+
287
+ return () => {
288
+ observer?.disconnect()
289
+ mutationObserver.disconnect()
290
+ root.remove()
291
+ }
292
+ }
293
+
294
+ const FactCheckDetailsLoader = defineComponent({
295
+ name: 'FactCheckDetailsLoader',
296
+ setup() {
297
+ const pluginName = inject<ComputedRef<string>>('plugin:name')
298
+ const isOwn = computed(() => {
299
+ const current = pluginName?.value
300
+ return !!current && PLUGIN_NAMES.has(current)
301
+ })
302
+
303
+ let dispose: (() => void) | null = null
304
+
305
+ const tryMount = () => {
306
+ dispose?.()
307
+ dispose = null
308
+ if (!isOwn.value) return
309
+ dispose = mountFloatingNav()
310
+ }
311
+
312
+ onMounted(tryMount)
313
+ watch(isOwn, tryMount)
314
+ onBeforeUnmount(() => dispose?.())
315
+ return () => h('div', { style: { display: 'none' } })
316
+ },
317
+ })
318
+
319
+ export default (ctx: Context) => {
320
+ ctx.slot({
321
+ type: 'plugin-details',
322
+ component: FactCheckDetailsLoader,
323
+ order: -999,
324
+ })
325
+ }