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.
- package/README.md +80 -74
- package/client/index.ts +325 -0
- package/dist/index.js +230 -0
- package/lib/agents/deepSearchController.d.ts +39 -0
- package/lib/agents/deepSearchController.js +422 -0
- package/lib/agents/index.d.ts +1 -2
- package/lib/agents/index.js +3 -5
- package/lib/agents/subSearchAgent.d.ts +12 -18
- package/lib/agents/subSearchAgent.js +56 -29
- package/lib/config.d.ts +2 -36
- package/lib/config.js +86 -63
- package/lib/index.d.ts +10 -6
- package/lib/index.js +93 -166
- package/lib/services/chatluna.d.ts +11 -63
- package/lib/services/chatluna.js +87 -134
- package/lib/services/deepSearchTaskService.d.ts +32 -0
- package/lib/services/deepSearchTaskService.js +270 -0
- package/lib/services/deepSearchTool.d.ts +4 -0
- package/lib/services/deepSearchTool.js +222 -0
- package/lib/services/factCheckTool.d.ts +4 -0
- package/lib/services/factCheckTool.js +347 -0
- package/lib/services/grokWebSearch.d.ts +18 -0
- package/lib/services/grokWebSearch.js +93 -0
- package/lib/services/iterativeSearchAgent.d.ts +22 -0
- package/lib/services/iterativeSearchAgent.js +179 -0
- package/lib/services/jinaReader.d.ts +15 -0
- package/lib/services/jinaReader.js +89 -0
- package/lib/services/webFetchTool.d.ts +4 -0
- package/lib/services/webFetchTool.js +134 -0
- package/lib/types.d.ts +145 -78
- package/lib/types.js +0 -3
- package/lib/utils/async.d.ts +1 -0
- package/lib/utils/async.js +18 -0
- package/lib/utils/http.d.ts +2 -0
- package/lib/utils/http.js +12 -0
- package/lib/utils/model.d.ts +1 -0
- package/lib/utils/model.js +20 -0
- package/lib/utils/prompts.d.ts +57 -43
- package/lib/utils/prompts.js +300 -123
- package/lib/utils/search.d.ts +2 -0
- package/lib/utils/search.js +32 -0
- package/lib/utils/summary.d.ts +6 -0
- package/lib/utils/summary.js +69 -0
- package/lib/utils/text.d.ts +1 -0
- package/lib/utils/text.js +11 -0
- package/lib/utils/url.d.ts +3 -10
- package/lib/utils/url.js +68 -23
- package/package.json +64 -49
- package/lib/agents/mainAgent.d.ts +0 -30
- package/lib/agents/mainAgent.js +0 -136
- package/lib/agents/verifyAgent.d.ts +0 -37
- package/lib/agents/verifyAgent.js +0 -160
- package/lib/services/chatlunaSearch.d.ts +0 -38
- package/lib/services/chatlunaSearch.js +0 -280
- package/lib/services/messageParser.d.ts +0 -38
- package/lib/services/messageParser.js +0 -184
- package/lib/services/tofTool.d.ts +0 -4
- package/lib/services/tofTool.js +0 -74
package/README.md
CHANGED
|
@@ -1,103 +1,109 @@
|
|
|
1
|
-
# koishi-plugin-
|
|
1
|
+
# koishi-plugin-chatluna-fact-check
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/koishi-plugin-chatluna-fact-check)
|
|
4
4
|
|
|
5
|
-
|
|
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-
|
|
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
|
-
|
|
40
|
+
## API Key / Base URL 对照表
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
推荐优先在 `api.apiKeys` 表格中集中填写(来源 / key / base url / 启用开关)。
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
package/client/index.ts
ADDED
|
@@ -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
|
+
}
|