koishi-plugin-isthattrue 0.2.0 → 0.2.2
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 +104 -104
- package/lib/agents/index.js +9 -0
- package/lib/agents/mainAgent.js +136 -0
- package/lib/agents/subSearchAgent.js +73 -0
- package/lib/agents/verifyAgent.js +160 -0
- package/lib/config.js +72 -0
- package/lib/index.js +182 -1393
- package/lib/services/chatluna.js +187 -0
- package/lib/services/chatlunaSearch.js +280 -0
- package/lib/services/messageParser.js +184 -0
- package/lib/services/tofTool.d.ts +3 -2
- package/lib/services/tofTool.js +74 -0
- package/lib/types.js +13 -0
- package/lib/utils/prompts.js +255 -0
- package/lib/utils/url.js +29 -0
- package/package.json +49 -49
- package/lib/agents/searchAgent.d.ts +0 -53
- package/lib/services/anspire.d.ts +0 -30
- package/lib/services/grok.d.ts +0 -32
- package/lib/services/kimi.d.ts +0 -22
- package/lib/services/tavily.d.ts +0 -30
- package/lib/services/zhipu.d.ts +0 -22
package/README.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# koishi-plugin-isthattrue
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/koishi-plugin-isthattrue)
|
|
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
|
|
14
|
-
|
|
15
|
-
## 安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install koishi-plugin-isthattrue
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 依赖
|
|
22
|
-
|
|
23
|
-
- [koishi-plugin-chatluna](https://github.com/ChatLunaLab/chatluna) - LLM 服务接入
|
|
24
|
-
|
|
25
|
-
## 使用方法
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
tof [内容] # 核查指定内容
|
|
29
|
-
tof # 引用一条消息后使用,核查被引用的消息
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
别名:`鉴定`、`核查`
|
|
33
|
-
|
|
34
|
-
## 配置说明
|
|
35
|
-
|
|
36
|
-
### 模型配置
|
|
37
|
-
|
|
38
|
-
| 配置项 | 说明 | 推荐 |
|
|
39
|
-
|--------|------|------|
|
|
40
|
-
| mainModel | 主控 Agent 模型,用于编排和最终判决 | Gemini-3-Flash |
|
|
41
|
-
| subSearchModel | 子搜索 Agent 模型,用于深度搜索 | Grok-4-1 |
|
|
42
|
-
|
|
43
|
-
### 搜索 API 配置
|
|
44
|
-
|
|
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 | 搜索关键词多样化模型 |
|
|
54
|
-
|
|
55
|
-
### Agent 配置
|
|
56
|
-
|
|
57
|
-
| 配置项 | 默认值 | 说明 |
|
|
58
|
-
|--------|--------|------|
|
|
59
|
-
| timeout | 60000 | 单次请求超时时间(毫秒) |
|
|
60
|
-
| maxRetries | 2 | 失败重试次数 |
|
|
61
|
-
|
|
62
|
-
### 其他设置
|
|
63
|
-
|
|
64
|
-
| 配置项 | 默认值 | 说明 |
|
|
65
|
-
|--------|--------|------|
|
|
66
|
-
| verbose | false | 显示详细验证过程 |
|
|
67
|
-
| outputFormat | auto | 输出格式(auto/markdown/plain) |
|
|
68
|
-
| useForwardMessage | true | 使用合并转发消息展示详情(仅 QQ) |
|
|
69
|
-
| bypassProxy | false | 绕过系统代理 |
|
|
70
|
-
| logLLMDetails | false | 打印 LLM 请求详情(调试用) |
|
|
71
|
-
|
|
72
|
-
## 工作流程
|
|
73
|
-
|
|
74
|
-
1. 用户发送 `tof` 命令(可引用消息或直接输入内容)
|
|
75
|
-
2. MessageParser 提取文本和图片,如有图片则进行 OCR
|
|
76
|
-
3. 主控 Agent 分析内容,生成搜索计划
|
|
77
|
-
4. 子搜索 Agent 并行执行多源搜索
|
|
78
|
-
5. 主控 Agent 综合搜索结果,输出最终判定
|
|
79
|
-
|
|
80
|
-
## 架构
|
|
81
|
-
|
|
82
|
-
```
|
|
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
|
-
```
|
|
101
|
-
|
|
102
|
-
## License
|
|
103
|
-
|
|
104
|
-
MIT
|
|
1
|
+
# koishi-plugin-isthattrue
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-isthattrue)
|
|
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
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install koishi-plugin-isthattrue
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 依赖
|
|
22
|
+
|
|
23
|
+
- [koishi-plugin-chatluna](https://github.com/ChatLunaLab/chatluna) - LLM 服务接入
|
|
24
|
+
|
|
25
|
+
## 使用方法
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
tof [内容] # 核查指定内容
|
|
29
|
+
tof # 引用一条消息后使用,核查被引用的消息
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
别名:`鉴定`、`核查`
|
|
33
|
+
|
|
34
|
+
## 配置说明
|
|
35
|
+
|
|
36
|
+
### 模型配置
|
|
37
|
+
|
|
38
|
+
| 配置项 | 说明 | 推荐 |
|
|
39
|
+
|--------|------|------|
|
|
40
|
+
| mainModel | 主控 Agent 模型,用于编排和最终判决 | Gemini-3-Flash |
|
|
41
|
+
| subSearchModel | 子搜索 Agent 模型,用于深度搜索 | Grok-4-1 |
|
|
42
|
+
|
|
43
|
+
### 搜索 API 配置
|
|
44
|
+
|
|
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 | 搜索关键词多样化模型 |
|
|
54
|
+
|
|
55
|
+
### Agent 配置
|
|
56
|
+
|
|
57
|
+
| 配置项 | 默认值 | 说明 |
|
|
58
|
+
|--------|--------|------|
|
|
59
|
+
| timeout | 60000 | 单次请求超时时间(毫秒) |
|
|
60
|
+
| maxRetries | 2 | 失败重试次数 |
|
|
61
|
+
|
|
62
|
+
### 其他设置
|
|
63
|
+
|
|
64
|
+
| 配置项 | 默认值 | 说明 |
|
|
65
|
+
|--------|--------|------|
|
|
66
|
+
| verbose | false | 显示详细验证过程 |
|
|
67
|
+
| outputFormat | auto | 输出格式(auto/markdown/plain) |
|
|
68
|
+
| useForwardMessage | true | 使用合并转发消息展示详情(仅 QQ) |
|
|
69
|
+
| bypassProxy | false | 绕过系统代理 |
|
|
70
|
+
| logLLMDetails | false | 打印 LLM 请求详情(调试用) |
|
|
71
|
+
|
|
72
|
+
## 工作流程
|
|
73
|
+
|
|
74
|
+
1. 用户发送 `tof` 命令(可引用消息或直接输入内容)
|
|
75
|
+
2. MessageParser 提取文本和图片,如有图片则进行 OCR
|
|
76
|
+
3. 主控 Agent 分析内容,生成搜索计划
|
|
77
|
+
4. 子搜索 Agent 并行执行多源搜索
|
|
78
|
+
5. 主控 Agent 综合搜索结果,输出最终判定
|
|
79
|
+
|
|
80
|
+
## 架构
|
|
81
|
+
|
|
82
|
+
```
|
|
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
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SubSearchAgent = exports.MainAgent = exports.VerifyAgent = void 0;
|
|
4
|
+
var verifyAgent_1 = require("./verifyAgent");
|
|
5
|
+
Object.defineProperty(exports, "VerifyAgent", { enumerable: true, get: function () { return verifyAgent_1.VerifyAgent; } });
|
|
6
|
+
var mainAgent_1 = require("./mainAgent");
|
|
7
|
+
Object.defineProperty(exports, "MainAgent", { enumerable: true, get: function () { return mainAgent_1.MainAgent; } });
|
|
8
|
+
var subSearchAgent_1 = require("./subSearchAgent");
|
|
9
|
+
Object.defineProperty(exports, "SubSearchAgent", { enumerable: true, get: function () { return subSearchAgent_1.SubSearchAgent; } });
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MainAgent = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const subSearchAgent_1 = require("./subSearchAgent");
|
|
6
|
+
const verifyAgent_1 = require("./verifyAgent");
|
|
7
|
+
const chatlunaSearch_1 = require("../services/chatlunaSearch");
|
|
8
|
+
const chatluna_1 = require("../services/chatluna");
|
|
9
|
+
const messageParser_1 = require("../services/messageParser");
|
|
10
|
+
const prompts_1 = require("../utils/prompts");
|
|
11
|
+
/**
|
|
12
|
+
* 主控 Agent
|
|
13
|
+
* 流程:并行搜索 (Chatluna + Grok) -> URL处理 -> Gemini判决
|
|
14
|
+
*/
|
|
15
|
+
class MainAgent {
|
|
16
|
+
constructor(ctx, config) {
|
|
17
|
+
this.ctx = ctx;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.subSearchAgent = new subSearchAgent_1.SubSearchAgent(ctx, config);
|
|
20
|
+
this.chatlunaSearchAgent = new chatlunaSearch_1.ChatlunaSearchAgent(ctx, config);
|
|
21
|
+
this.verifyAgent = new verifyAgent_1.VerifyAgent(ctx, config);
|
|
22
|
+
this.chatluna = new chatluna_1.ChatlunaAdapter(ctx, config);
|
|
23
|
+
this.messageParser = new messageParser_1.MessageParser(ctx);
|
|
24
|
+
this.logger = ctx.logger('isthattrue');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 执行完整的核查流程
|
|
28
|
+
*/
|
|
29
|
+
async verify(content) {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
this.logger.info('开始主控 Agent 核查流程...');
|
|
32
|
+
try {
|
|
33
|
+
// 准备图片 base64
|
|
34
|
+
let imageBase64List = [];
|
|
35
|
+
if (content.images.length > 0) {
|
|
36
|
+
this.logger.info(`[Phase 0] 处理 ${content.images.length} 张图片...`);
|
|
37
|
+
const prepared = await this.messageParser.prepareForLLM(content);
|
|
38
|
+
imageBase64List = prepared.imageBase64List;
|
|
39
|
+
this.logger.info(`[Phase 0] 成功转换 ${imageBase64List.length} 张图片为 base64`);
|
|
40
|
+
}
|
|
41
|
+
// 确定搜索用的文本
|
|
42
|
+
let searchText = content.text;
|
|
43
|
+
// 如果是纯图片(没有文本),先让 Gemini 描述图片
|
|
44
|
+
if (!content.text.trim() && imageBase64List.length > 0) {
|
|
45
|
+
this.logger.info('[Phase 0] 纯图片输入,提取图片描述...');
|
|
46
|
+
searchText = await this.extractImageDescription(imageBase64List);
|
|
47
|
+
this.logger.info(`[Phase 0] 图片描述:${searchText.substring(0, 100)}...`);
|
|
48
|
+
}
|
|
49
|
+
// Phase 1+2: 并行执行搜索
|
|
50
|
+
this.logger.info('[Phase 1+2] 并行搜索中 (Chatluna + Grok)...');
|
|
51
|
+
const searchPromises = [];
|
|
52
|
+
// Chatluna 搜索 (通用网页)
|
|
53
|
+
if (this.chatlunaSearchAgent.isAvailable()) {
|
|
54
|
+
searchPromises.push(this.withTimeout(this.chatlunaSearchAgent.search(searchText), this.config.timeout, 'ChatlunaSearch'));
|
|
55
|
+
}
|
|
56
|
+
// Grok 深度搜索 (独立,专注 X/Twitter)
|
|
57
|
+
searchPromises.push(this.withTimeout(this.subSearchAgent.deepSearch(searchText), this.config.timeout, 'GrokSearch'));
|
|
58
|
+
const results = await Promise.allSettled(searchPromises);
|
|
59
|
+
// 收集成功的结果
|
|
60
|
+
const searchResults = results
|
|
61
|
+
.filter((r) => r.status === 'fulfilled' && r.value !== null)
|
|
62
|
+
.map(r => r.value);
|
|
63
|
+
// 记录失败的搜索
|
|
64
|
+
results.forEach((r, i) => {
|
|
65
|
+
if (r.status === 'rejected') {
|
|
66
|
+
this.logger.warn(`搜索 ${i === 0 ? 'Chatluna' : 'Grok'} 失败: ${r.reason}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
this.logger.info(`[Phase 1+2] 搜索完成,成功 ${searchResults.length} 个`);
|
|
70
|
+
// 如果没有任何搜索结果,返回不确定
|
|
71
|
+
if (searchResults.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
originalContent: content,
|
|
74
|
+
searchResults: [],
|
|
75
|
+
verdict: types_1.Verdict.UNCERTAIN,
|
|
76
|
+
reasoning: '所有搜索都失败了,无法验证',
|
|
77
|
+
sources: [],
|
|
78
|
+
confidence: 0,
|
|
79
|
+
processingTime: Date.now() - startTime,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Phase 3: Gemini 综合判决 (传递原图)
|
|
83
|
+
this.logger.info('[Phase 3] Gemini 判决中...');
|
|
84
|
+
const finalResult = await this.verifyAgent.verify(content, searchResults, imageBase64List);
|
|
85
|
+
return {
|
|
86
|
+
...finalResult,
|
|
87
|
+
processingTime: Date.now() - startTime,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.logger.error('主控 Agent 流程出错:', error);
|
|
92
|
+
return {
|
|
93
|
+
originalContent: content,
|
|
94
|
+
searchResults: [],
|
|
95
|
+
verdict: types_1.Verdict.UNCERTAIN,
|
|
96
|
+
reasoning: `流程执行失败: ${error.message}`,
|
|
97
|
+
sources: [],
|
|
98
|
+
confidence: 0,
|
|
99
|
+
processingTime: Date.now() - startTime,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 带超时的 Promise 包装
|
|
105
|
+
*/
|
|
106
|
+
async withTimeout(promise, timeout, name) {
|
|
107
|
+
try {
|
|
108
|
+
return await Promise.race([
|
|
109
|
+
promise,
|
|
110
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} 超时`)), timeout))
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.logger.warn(`[${name}] 失败: ${error.message}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 从图片中提取描述(用于纯图片输入场景)
|
|
120
|
+
*/
|
|
121
|
+
async extractImageDescription(images) {
|
|
122
|
+
try {
|
|
123
|
+
const response = await this.chatluna.chat({
|
|
124
|
+
model: this.config.mainModel,
|
|
125
|
+
message: prompts_1.IMAGE_DESCRIPTION_PROMPT,
|
|
126
|
+
images: images,
|
|
127
|
+
});
|
|
128
|
+
return response.content;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.logger.error('图片描述提取失败:', error);
|
|
132
|
+
return '图片内容需要验证';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.MainAgent = MainAgent;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SubSearchAgent = void 0;
|
|
4
|
+
const chatluna_1 = require("../services/chatluna");
|
|
5
|
+
const prompts_1 = require("../utils/prompts");
|
|
6
|
+
/**
|
|
7
|
+
* 子搜索 Agent
|
|
8
|
+
* 专门负责深度搜索(主要使用 Grok,擅长 X/Twitter 搜索)
|
|
9
|
+
* 独立搜索,不依赖其他搜索结果
|
|
10
|
+
*/
|
|
11
|
+
class SubSearchAgent {
|
|
12
|
+
constructor(ctx, config) {
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.chatluna = new chatluna_1.ChatlunaAdapter(ctx, config);
|
|
16
|
+
this.logger = ctx.logger('isthattrue');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 执行深度搜索
|
|
20
|
+
* @param claim 原始声明文本
|
|
21
|
+
*/
|
|
22
|
+
async deepSearch(claim) {
|
|
23
|
+
this.logger.info(`[SubSearchAgent] 开始深度搜索,模型: ${this.config.subSearchModel}`);
|
|
24
|
+
try {
|
|
25
|
+
const response = await this.chatluna.chatWithRetry({
|
|
26
|
+
model: this.config.subSearchModel,
|
|
27
|
+
message: (0, prompts_1.buildSubSearchPrompt)(claim),
|
|
28
|
+
systemPrompt: prompts_1.SUB_SEARCH_AGENT_SYSTEM_PROMPT,
|
|
29
|
+
enableSearch: true,
|
|
30
|
+
}, this.config.maxRetries);
|
|
31
|
+
// 解析响应
|
|
32
|
+
const parsed = this.parseResponse(response.content);
|
|
33
|
+
return {
|
|
34
|
+
agentId: 'grok-deep-search',
|
|
35
|
+
perspective: 'Grok 深度搜索 (X/Twitter)',
|
|
36
|
+
findings: parsed.findings || response.content,
|
|
37
|
+
sources: parsed.sources || response.sources || [],
|
|
38
|
+
confidence: parsed.confidence || 0.8,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
this.logger.error('[SubSearchAgent] 搜索失败:', error);
|
|
43
|
+
return {
|
|
44
|
+
agentId: 'grok-deep-search',
|
|
45
|
+
perspective: 'Grok 深度搜索 (X/Twitter)',
|
|
46
|
+
findings: `深度搜索失败: ${error.message}`,
|
|
47
|
+
sources: [],
|
|
48
|
+
confidence: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
parseResponse(content) {
|
|
53
|
+
try {
|
|
54
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
|
|
55
|
+
let parsed;
|
|
56
|
+
if (jsonMatch) {
|
|
57
|
+
parsed = JSON.parse(jsonMatch[1]);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
parsed = JSON.parse(content);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
findings: parsed.findings,
|
|
64
|
+
sources: parsed.sources,
|
|
65
|
+
confidence: parsed.confidence,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.SubSearchAgent = SubSearchAgent;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VerifyAgent = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const chatluna_1 = require("../services/chatluna");
|
|
6
|
+
const prompts_1 = require("../utils/prompts");
|
|
7
|
+
/**
|
|
8
|
+
* 验证Agent
|
|
9
|
+
* 负责综合搜索结果并做出最终判决
|
|
10
|
+
*/
|
|
11
|
+
class VerifyAgent {
|
|
12
|
+
constructor(ctx, config) {
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.chatluna = new chatluna_1.ChatlunaAdapter(ctx, config);
|
|
16
|
+
this.logger = ctx.logger('isthattrue');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 执行验证判决
|
|
20
|
+
* @param originalContent 原始消息内容
|
|
21
|
+
* @param searchResults 搜索结果
|
|
22
|
+
* @param images 可选的图片 base64 列表(多模态验证)
|
|
23
|
+
*/
|
|
24
|
+
async verify(originalContent, searchResults, images) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const hasImages = images && images.length > 0;
|
|
27
|
+
this.logger.info(`开始综合验证...${hasImages ? ' (包含图片)' : ''}`);
|
|
28
|
+
try {
|
|
29
|
+
// 构建验证请求
|
|
30
|
+
const prompt = (0, prompts_1.buildVerifyPrompt)(originalContent.text, searchResults.map(r => ({
|
|
31
|
+
perspective: r.perspective,
|
|
32
|
+
findings: r.findings,
|
|
33
|
+
sources: r.sources,
|
|
34
|
+
})), hasImages // 传递是否有图片
|
|
35
|
+
);
|
|
36
|
+
// 选择系统提示词(多模态或普通)
|
|
37
|
+
const systemPrompt = hasImages
|
|
38
|
+
? prompts_1.VERIFY_AGENT_SYSTEM_PROMPT_MULTIMODAL
|
|
39
|
+
: prompts_1.VERIFY_AGENT_SYSTEM_PROMPT;
|
|
40
|
+
// 调用低幻觉率模型进行验证
|
|
41
|
+
const response = await this.chatluna.chatWithRetry({
|
|
42
|
+
model: this.config.mainModel,
|
|
43
|
+
message: prompt,
|
|
44
|
+
systemPrompt: systemPrompt,
|
|
45
|
+
images: images, // 传递图片
|
|
46
|
+
}, this.config.maxRetries);
|
|
47
|
+
// 解析验证结果
|
|
48
|
+
const parsed = this.parseVerifyResponse(response.content);
|
|
49
|
+
const processingTime = Date.now() - startTime;
|
|
50
|
+
const result = {
|
|
51
|
+
originalContent,
|
|
52
|
+
searchResults,
|
|
53
|
+
verdict: parsed.verdict,
|
|
54
|
+
reasoning: parsed.reasoning,
|
|
55
|
+
sources: this.aggregateSources(searchResults, parsed.sources),
|
|
56
|
+
confidence: parsed.confidence,
|
|
57
|
+
processingTime,
|
|
58
|
+
};
|
|
59
|
+
this.logger.info(`验证完成,判决: ${result.verdict},可信度: ${result.confidence}`);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.logger.error('验证失败:', error);
|
|
64
|
+
return {
|
|
65
|
+
originalContent,
|
|
66
|
+
searchResults,
|
|
67
|
+
verdict: types_1.Verdict.UNCERTAIN,
|
|
68
|
+
reasoning: `验证过程发生错误: ${error.message}`,
|
|
69
|
+
sources: this.aggregateSources(searchResults, []),
|
|
70
|
+
confidence: 0,
|
|
71
|
+
processingTime: Date.now() - startTime,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 解析验证响应
|
|
77
|
+
*/
|
|
78
|
+
parseVerifyResponse(content) {
|
|
79
|
+
try {
|
|
80
|
+
// 提取JSON块
|
|
81
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
|
|
82
|
+
let parsed;
|
|
83
|
+
if (jsonMatch) {
|
|
84
|
+
parsed = JSON.parse(jsonMatch[1]);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// 尝试直接解析
|
|
88
|
+
parsed = JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
verdict: this.normalizeVerdict(parsed.verdict),
|
|
92
|
+
reasoning: parsed.reasoning || parsed.key_evidence || '无详细说明',
|
|
93
|
+
sources: parsed.sources || [],
|
|
94
|
+
confidence: parsed.confidence || 0.5,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// 解析失败,尝试从文本中提取判决
|
|
99
|
+
return {
|
|
100
|
+
verdict: this.extractVerdictFromText(content),
|
|
101
|
+
reasoning: content,
|
|
102
|
+
sources: [],
|
|
103
|
+
confidence: 0.3,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 标准化判决结果
|
|
109
|
+
*/
|
|
110
|
+
normalizeVerdict(verdict) {
|
|
111
|
+
const normalized = verdict?.toLowerCase()?.trim();
|
|
112
|
+
const mapping = {
|
|
113
|
+
'true': types_1.Verdict.TRUE,
|
|
114
|
+
'真实': types_1.Verdict.TRUE,
|
|
115
|
+
'正确': types_1.Verdict.TRUE,
|
|
116
|
+
'false': types_1.Verdict.FALSE,
|
|
117
|
+
'虚假': types_1.Verdict.FALSE,
|
|
118
|
+
'错误': types_1.Verdict.FALSE,
|
|
119
|
+
'partially_true': types_1.Verdict.PARTIALLY_TRUE,
|
|
120
|
+
'partial': types_1.Verdict.PARTIALLY_TRUE,
|
|
121
|
+
'部分真实': types_1.Verdict.PARTIALLY_TRUE,
|
|
122
|
+
'uncertain': types_1.Verdict.UNCERTAIN,
|
|
123
|
+
'不确定': types_1.Verdict.UNCERTAIN,
|
|
124
|
+
'无法确定': types_1.Verdict.UNCERTAIN,
|
|
125
|
+
};
|
|
126
|
+
return mapping[normalized] || types_1.Verdict.UNCERTAIN;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 从文本中提取判决
|
|
130
|
+
*/
|
|
131
|
+
extractVerdictFromText(text) {
|
|
132
|
+
const lower = text.toLowerCase();
|
|
133
|
+
if (lower.includes('虚假') || lower.includes('false') || lower.includes('错误')) {
|
|
134
|
+
return types_1.Verdict.FALSE;
|
|
135
|
+
}
|
|
136
|
+
if (lower.includes('部分真实') || lower.includes('partially')) {
|
|
137
|
+
return types_1.Verdict.PARTIALLY_TRUE;
|
|
138
|
+
}
|
|
139
|
+
if (lower.includes('真实') || lower.includes('true') || lower.includes('正确')) {
|
|
140
|
+
return types_1.Verdict.TRUE;
|
|
141
|
+
}
|
|
142
|
+
return types_1.Verdict.UNCERTAIN;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 汇总所有来源
|
|
146
|
+
*/
|
|
147
|
+
aggregateSources(searchResults, verifySources) {
|
|
148
|
+
const allSources = new Set();
|
|
149
|
+
for (const result of searchResults) {
|
|
150
|
+
for (const source of result.sources) {
|
|
151
|
+
allSources.add(source);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const source of verifySources) {
|
|
155
|
+
allSources.add(source);
|
|
156
|
+
}
|
|
157
|
+
return [...allSources];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.VerifyAgent = VerifyAgent;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
// 使用 Chatluna 的动态模型选择器
|
|
6
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
7
|
+
koishi_1.Schema.object({
|
|
8
|
+
mainModel: koishi_1.Schema.dynamic('model')
|
|
9
|
+
.default('google/gemini-3-flash')
|
|
10
|
+
.description('主控 Agent 模型 (用于编排和最终判决,推荐 Gemini-3-Flash)'),
|
|
11
|
+
subSearchModel: koishi_1.Schema.dynamic('model')
|
|
12
|
+
.default('x-ai/grok-4-1')
|
|
13
|
+
.description('子搜索 Agent 模型 (用于深度搜索,推荐 Grok-4-1)'),
|
|
14
|
+
}).description('模型配置'),
|
|
15
|
+
koishi_1.Schema.object({
|
|
16
|
+
chatlunaSearchModel: koishi_1.Schema.dynamic('model')
|
|
17
|
+
.default('')
|
|
18
|
+
.description('Chatluna Search 使用的模型 (可选,用于调用 chatluna-search-service)'),
|
|
19
|
+
enableChatlunaSearch: koishi_1.Schema.boolean()
|
|
20
|
+
.default(true)
|
|
21
|
+
.description('启用 Chatluna 搜索集成'),
|
|
22
|
+
chatlunaSearchDiversifyModel: koishi_1.Schema.dynamic('model')
|
|
23
|
+
.default('')
|
|
24
|
+
.description('搜索关键词多样化模型 (可选,推荐 Gemini 2.5 Flash Lite)'),
|
|
25
|
+
}).description('搜索配置'),
|
|
26
|
+
koishi_1.Schema.object({
|
|
27
|
+
timeout: koishi_1.Schema.number()
|
|
28
|
+
.min(10000)
|
|
29
|
+
.max(300000)
|
|
30
|
+
.default(60000)
|
|
31
|
+
.description('单次请求超时时间(毫秒)'),
|
|
32
|
+
maxRetries: koishi_1.Schema.number()
|
|
33
|
+
.min(0)
|
|
34
|
+
.max(5)
|
|
35
|
+
.default(2)
|
|
36
|
+
.description('失败重试次数'),
|
|
37
|
+
}).description('Agent配置'),
|
|
38
|
+
koishi_1.Schema.object({
|
|
39
|
+
verbose: koishi_1.Schema.boolean()
|
|
40
|
+
.default(false)
|
|
41
|
+
.description('显示详细验证过程 (进度提示)'),
|
|
42
|
+
outputFormat: koishi_1.Schema.union([
|
|
43
|
+
koishi_1.Schema.const('auto').description('自动 (QQ使用纯文本)'),
|
|
44
|
+
koishi_1.Schema.const('markdown').description('Markdown'),
|
|
45
|
+
koishi_1.Schema.const('plain').description('纯文本'),
|
|
46
|
+
]).default('auto').description('输出格式'),
|
|
47
|
+
useForwardMessage: koishi_1.Schema.boolean()
|
|
48
|
+
.default(true)
|
|
49
|
+
.description('使用合并转发消息展示详情 (仅支持QQ)'),
|
|
50
|
+
forwardMaxNodes: koishi_1.Schema.number()
|
|
51
|
+
.min(0)
|
|
52
|
+
.max(99)
|
|
53
|
+
.default(8)
|
|
54
|
+
.description('合并转发最大节点数,超过则回退普通消息(0 表示直接回退)'),
|
|
55
|
+
forwardMaxTotalChars: koishi_1.Schema.number()
|
|
56
|
+
.min(0)
|
|
57
|
+
.max(20000)
|
|
58
|
+
.default(3000)
|
|
59
|
+
.description('合并转发总字符数上限,超过则回退普通消息(0 表示直接回退)'),
|
|
60
|
+
forwardMaxSegmentChars: koishi_1.Schema.number()
|
|
61
|
+
.min(50)
|
|
62
|
+
.max(2000)
|
|
63
|
+
.default(500)
|
|
64
|
+
.description('合并转发单节点字符数上限'),
|
|
65
|
+
bypassProxy: koishi_1.Schema.boolean()
|
|
66
|
+
.default(false)
|
|
67
|
+
.description('是否绕过系统代理'),
|
|
68
|
+
logLLMDetails: koishi_1.Schema.boolean()
|
|
69
|
+
.default(false)
|
|
70
|
+
.description('是否打印 LLM 请求体和响应详情 (Debug用)'),
|
|
71
|
+
}).description('其他设置'),
|
|
72
|
+
]);
|