koishi-plugin-isthattrue 0.1.0

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/lib/index.js ADDED
@@ -0,0 +1,1026 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ name: () => name,
27
+ usage: () => usage
28
+ });
29
+ module.exports = __toCommonJS(src_exports);
30
+ var import_koishi3 = require("koishi");
31
+
32
+ // src/services/chatluna.ts
33
+ var import_messages = require("@langchain/core/messages");
34
+ var ChatlunaAdapter = class {
35
+ constructor(ctx, config) {
36
+ this.ctx = ctx;
37
+ this.config = config;
38
+ this.logger = ctx.logger("isthattrue");
39
+ }
40
+ static {
41
+ __name(this, "ChatlunaAdapter");
42
+ }
43
+ logger;
44
+ /**
45
+ * 检查 Chatluna 服务是否可用
46
+ */
47
+ isAvailable() {
48
+ return !!this.ctx.chatluna;
49
+ }
50
+ /**
51
+ * 发送聊天请求
52
+ */
53
+ async chat(request) {
54
+ if (!this.isAvailable()) {
55
+ throw new Error("Chatluna 服务不可用,请确保已安装并启用 koishi-plugin-chatluna");
56
+ }
57
+ const startTime = Date.now();
58
+ let originalProxies = {};
59
+ try {
60
+ if (this.config?.bypassProxy) {
61
+ const proxyVars = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"];
62
+ proxyVars.forEach((v) => {
63
+ originalProxies[v] = process.env[v];
64
+ delete process.env[v];
65
+ });
66
+ this.logger.debug("已临时移除系统代理环境变量");
67
+ } else {
68
+ const proxyVars = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"];
69
+ const activeProxies = proxyVars.filter((v) => process.env[v]).map((v) => `${v}=${process.env[v]}`);
70
+ if (activeProxies.length > 0) {
71
+ this.logger.debug(`当前环境代理: ${activeProxies.join(", ")}`);
72
+ } else {
73
+ this.logger.debug("当前环境未检测到系统代理环境变量");
74
+ }
75
+ }
76
+ const modelRef = await this.ctx.chatluna.createChatModel(request.model);
77
+ const model = modelRef.value;
78
+ if (!model) {
79
+ throw new Error(`无法创建模型: ${request.model},请确保模型已正确配置`);
80
+ }
81
+ const messages = [];
82
+ if (request.systemPrompt) {
83
+ messages.push(new import_messages.SystemMessage(request.systemPrompt));
84
+ }
85
+ let messageContent = request.message;
86
+ if (request.images && request.images.length > 0) {
87
+ messageContent = `[图片内容需要分析]
88
+
89
+ ${request.message}`;
90
+ }
91
+ messages.push(new import_messages.HumanMessage(messageContent));
92
+ if (this.config?.logLLMDetails) {
93
+ this.logger.info(`[LLM Request] Model: ${request.model}
94
+ System: ${request.systemPrompt || "None"}
95
+ Message: ${messageContent}`);
96
+ }
97
+ const response = await model.invoke(messages, {
98
+ temperature: 0.3
99
+ // 低温度以减少幻觉
100
+ });
101
+ if (this.config?.bypassProxy) {
102
+ Object.keys(originalProxies).forEach((v) => {
103
+ if (originalProxies[v] !== void 0) {
104
+ process.env[v] = originalProxies[v];
105
+ }
106
+ });
107
+ }
108
+ const processingTime = Date.now() - startTime;
109
+ this.logger.debug(`Chatluna 请求完成,耗时 ${processingTime}ms`);
110
+ const content = typeof response.content === "string" ? response.content : JSON.stringify(response.content);
111
+ if (this.config?.logLLMDetails) {
112
+ this.logger.info(`[LLM Response] Model: ${request.model}
113
+ Content: ${content}`);
114
+ }
115
+ return {
116
+ content,
117
+ model: request.model,
118
+ sources: this.extractSources(content)
119
+ };
120
+ } catch (error) {
121
+ if (this.config?.bypassProxy) {
122
+ Object.keys(originalProxies).forEach((v) => {
123
+ if (originalProxies[v] !== void 0) {
124
+ process.env[v] = originalProxies[v];
125
+ }
126
+ });
127
+ }
128
+ this.logger.error("Chatluna 请求失败:", error);
129
+ throw error;
130
+ }
131
+ }
132
+ /**
133
+ * 带重试的聊天请求
134
+ */
135
+ async chatWithRetry(request, maxRetries = 2, fallbackModel) {
136
+ let lastError = null;
137
+ let currentModel = request.model;
138
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
139
+ try {
140
+ return await this.chat({
141
+ ...request,
142
+ model: currentModel
143
+ });
144
+ } catch (error) {
145
+ lastError = error;
146
+ this.logger.warn(`请求失败 (尝试 ${attempt + 1}/${maxRetries + 1}):`, error);
147
+ if (attempt === maxRetries - 1 && fallbackModel && fallbackModel !== currentModel) {
148
+ this.logger.info(`切换到备用模型: ${fallbackModel}`);
149
+ currentModel = fallbackModel;
150
+ }
151
+ if (attempt < maxRetries) {
152
+ await this.sleep(1e3 * (attempt + 1));
153
+ }
154
+ }
155
+ }
156
+ throw lastError || new Error("请求失败,已达最大重试次数");
157
+ }
158
+ /**
159
+ * 从响应中提取来源链接
160
+ */
161
+ extractSources(content) {
162
+ const sources = [];
163
+ const urlRegex = /https?:\/\/[^\s\])"']+/g;
164
+ const matches = content.match(urlRegex);
165
+ if (matches) {
166
+ sources.push(...matches);
167
+ }
168
+ const sourceRegex = /\[来源[::]\s*([^\]]+)\]/g;
169
+ let match;
170
+ while ((match = sourceRegex.exec(content)) !== null) {
171
+ sources.push(match[1]);
172
+ }
173
+ return [...new Set(sources)];
174
+ }
175
+ sleep(ms) {
176
+ return new Promise((resolve) => setTimeout(resolve, ms));
177
+ }
178
+ };
179
+
180
+ // src/utils/prompts.ts
181
+ var SEARCH_AGENT_SYSTEM_PROMPT = `你是一个专业的事实核查搜索员。你的任务是针对给定的声明,从特定角度搜索相关信息。
182
+
183
+ ## 你的职责:
184
+ 1. 仔细分析待验证的声明
185
+ 2. 从你被分配的角度进行深度搜索
186
+ 3. 收集相关的证据和来源
187
+ 4. 客观评估找到的信息
188
+
189
+ ## 输出格式:
190
+ 请以JSON格式返回你的搜索结果:
191
+ \`\`\`json
192
+ {
193
+ "perspective": "你的搜索角度",
194
+ "findings": "详细的搜索发现",
195
+ "sources": ["来源1", "来源2"],
196
+ "supports_claim": true/false/null,
197
+ "confidence": 0.0-1.0,
198
+ "key_evidence": "最关键的证据摘要"
199
+ }
200
+ \`\`\`
201
+
202
+ ## 注意事项:
203
+ - 保持客观中立,不要预设立场
204
+ - 如果找不到相关信息,诚实说明
205
+ - 注明信息来源的可信度
206
+ - 区分事实陈述和观点表达`;
207
+ function getSearchPerspectives(count) {
208
+ const perspectives = [
209
+ "官方来源和权威机构",
210
+ "新闻媒体报道",
211
+ "学术研究和专家观点",
212
+ "社交媒体和公众讨论",
213
+ "历史背景和上下文"
214
+ ];
215
+ return perspectives.slice(0, count);
216
+ }
217
+ __name(getSearchPerspectives, "getSearchPerspectives");
218
+ function buildSearchPrompt(content, perspective) {
219
+ return `## 待验证声明:
220
+ "${content}"
221
+
222
+ ## 你的搜索角度: ${perspective}
223
+
224
+ 请从这个角度搜索相关信息,验证上述声明的真实性。注意收集具体的证据和可靠的来源。`;
225
+ }
226
+ __name(buildSearchPrompt, "buildSearchPrompt");
227
+ var VERIFY_AGENT_SYSTEM_PROMPT = `你是一个严谨的事实核查裁判员。你的任务是基于多个搜索Agent收集的证据,做出最终判决。
228
+
229
+ ## 你的职责:
230
+ 1. 综合分析所有搜索结果
231
+ 2. 评估各来源的可信度
232
+ 3. 识别证据之间的一致性和矛盾
233
+ 4. 做出谨慎的最终判决
234
+
235
+ ## 判决类别:
236
+ - TRUE (真实): 有充分可靠证据支持
237
+ - FALSE (虚假): 有充分可靠证据反驳
238
+ - PARTIALLY_TRUE (部分真实): 声明中部分内容属实
239
+ - UNCERTAIN (无法确定): 证据不足或相互矛盾
240
+
241
+ ## 输出格式:
242
+ 请以JSON格式返回你的判决:
243
+ \`\`\`json
244
+ {
245
+ "verdict": "TRUE/FALSE/PARTIALLY_TRUE/UNCERTAIN",
246
+ "confidence": 0.0-1.0,
247
+ "reasoning": "详细的判决理由",
248
+ "key_evidence": "支持判决的关键证据",
249
+ "caveats": "需要注意的限制或例外",
250
+ "sources": ["最可靠的来源列表"]
251
+ }
252
+ \`\`\`
253
+
254
+ ## 判决原则:
255
+ - 遵循"无罪推定"原则,证据不足时倾向于"无法确定"
256
+ - 重视权威来源,但也关注多方交叉验证
257
+ - 考虑信息的时效性
258
+ - 区分事实错误和表述不精确
259
+ - 保持谦逊,承认认知局限`;
260
+ function buildVerifyPrompt(originalContent, searchResults) {
261
+ const resultsText = searchResults.map((r, i) => `### 搜索Agent ${i + 1} (角度: ${r.perspective})
262
+ ${r.findings}
263
+ 来源: ${r.sources.join(", ") || "无"}`).join("\n\n");
264
+ return `## 待验证的原始声明:
265
+ "${originalContent}"
266
+
267
+ ## 各搜索Agent的调查结果:
268
+
269
+ ${resultsText}
270
+
271
+ ---
272
+
273
+ 请综合以上所有搜索结果,对原始声明做出最终判决。`;
274
+ }
275
+ __name(buildVerifyPrompt, "buildVerifyPrompt");
276
+ var OCR_PROMPT = `请识别并提取这张图片中的所有文字内容。
277
+ 如果图片中没有文字,请描述图片的主要内容。
278
+ 直接输出识别结果,不需要额外说明。`;
279
+ function formatVerificationOutput(content, searchResults, verdict, reasoning, sources, confidence, processingTime, format = "markdown") {
280
+ const verdictEmoji = {
281
+ true: "✅ 真实",
282
+ false: "❌ 虚假",
283
+ partially_true: "⚠️ 部分真实",
284
+ uncertain: "❓ 无法确定"
285
+ };
286
+ const confidenceValue = Math.round(confidence * 100);
287
+ const confidenceBar = "█".repeat(Math.round(confidence * 10)) + "░".repeat(10 - Math.round(confidence * 10));
288
+ if (format === "plain") {
289
+ let output2 = `🔍 事实核查结果
290
+
291
+ `;
292
+ output2 += `📋 待验证内容:
293
+ ${content.substring(0, 200)}${content.length > 200 ? "..." : ""}
294
+
295
+ `;
296
+ output2 += `🤖 搜索发现:
297
+ `;
298
+ output2 += searchResults.map((r) => `• ${r.perspective}: ${r.findings.substring(0, 100)}...`).join("\n");
299
+ output2 += `
300
+
301
+ ⚖️ 最终判决: ${verdictEmoji[verdict] || verdict}
302
+ `;
303
+ output2 += `📊 可信度: ${confidenceValue}%
304
+
305
+ `;
306
+ output2 += `📝 判决依据:
307
+ ${reasoning}
308
+ `;
309
+ if (sources.length > 0) {
310
+ output2 += `
311
+ 源:
312
+ `;
313
+ output2 += sources.map((s) => `• ${s}`).join("\n");
314
+ output2 += `
315
+ `;
316
+ }
317
+ output2 += `
318
+ ⏱️ 处理耗时: ${(processingTime / 1e3).toFixed(1)}秒`;
319
+ return output2;
320
+ }
321
+ let output = `🔍 **事实核查结果**
322
+
323
+ 📋 **待验证内容:**
324
+ > ${content.substring(0, 200)}${content.length > 200 ? "..." : ""}
325
+
326
+ ---
327
+
328
+ 🤖 **搜索Agent结果:**
329
+ ${searchResults.map((r) => `• **${r.perspective}**: ${r.findings.substring(0, 100)}...`).join("\n")}
330
+
331
+ ---
332
+
333
+ ⚖️ **最终判决: ${verdictEmoji[verdict] || verdict}**
334
+
335
+ 📊 **可信度:** ${confidenceBar} ${confidenceValue}%
336
+
337
+ 📝 **判决依据:**
338
+ ${reasoning}
339
+ `;
340
+ if (sources.length > 0) {
341
+ output += `
342
+ 🔗 **参考来源:**
343
+ ${sources.map((s) => `• ${s}`).join("\n")}
344
+ `;
345
+ }
346
+ output += `
347
+ ⏱️ *处理耗时: ${(processingTime / 1e3).toFixed(1)}秒*`;
348
+ return output;
349
+ }
350
+ __name(formatVerificationOutput, "formatVerificationOutput");
351
+ var VERDICT_EMOJI = {
352
+ true: "✅ 真实",
353
+ false: "❌ 虚假",
354
+ partially_true: "⚠️ 部分真实",
355
+ uncertain: "❓ 无法确定"
356
+ };
357
+ function formatForwardMessages(content, searchResults, verdict, reasoning, sources, confidence, processingTime) {
358
+ const confidenceValue = Math.round(confidence * 100);
359
+ const summary = `${VERDICT_EMOJI[verdict] || verdict} (${confidenceValue}%)
360
+
361
+ 📋 ${content.substring(0, 100)}${content.length > 100 ? "..." : ""}
362
+
363
+ ⏱️ ${(processingTime / 1e3).toFixed(1)}秒`;
364
+ const details = [];
365
+ details.push(`📝 判决依据
366
+
367
+ ${reasoning}`);
368
+ for (const r of searchResults) {
369
+ details.push(`🔍 ${r.perspective}
370
+
371
+ ${r.findings}`);
372
+ }
373
+ if (sources.length > 0) {
374
+ details.push(`🔗 参考来源
375
+
376
+ ${sources.map((s) => `• ${s}`).join("\n")}`);
377
+ }
378
+ return { summary, details };
379
+ }
380
+ __name(formatForwardMessages, "formatForwardMessages");
381
+
382
+ // src/agents/searchAgent.ts
383
+ var SearchAgent = class {
384
+ constructor(ctx, config, agentIndex, model) {
385
+ this.ctx = ctx;
386
+ this.config = config;
387
+ const perspectives = getSearchPerspectives(config.searchModels.length);
388
+ this.perspective = perspectives[agentIndex] || `角度 ${agentIndex + 1}`;
389
+ this.id = `agent-${agentIndex + 1}`;
390
+ this.model = model;
391
+ this.chatluna = new ChatlunaAdapter(ctx, config);
392
+ }
393
+ static {
394
+ __name(this, "SearchAgent");
395
+ }
396
+ id;
397
+ perspective;
398
+ model;
399
+ chatluna;
400
+ /**
401
+ * 执行搜索
402
+ */
403
+ async search(content) {
404
+ const logger = this.ctx.logger("isthattrue");
405
+ logger.info(`[${this.id}] 开始搜索,模型: ${this.model},角度: ${this.perspective}`);
406
+ const startTime = Date.now();
407
+ try {
408
+ const response = await this.chatluna.chatWithRetry(
409
+ {
410
+ model: this.model,
411
+ message: buildSearchPrompt(content, this.perspective),
412
+ systemPrompt: SEARCH_AGENT_SYSTEM_PROMPT,
413
+ enableSearch: true
414
+ },
415
+ this.config.maxRetries
416
+ );
417
+ const parsed = this.parseResponse(response.content);
418
+ const result = {
419
+ agentId: this.id,
420
+ perspective: this.perspective,
421
+ findings: parsed.findings || response.content,
422
+ sources: parsed.sources || response.sources || [],
423
+ confidence: parsed.confidence || 0.5
424
+ };
425
+ const elapsed = Date.now() - startTime;
426
+ logger.info(`[${this.id}] 搜索完成,耗时 ${elapsed}ms`);
427
+ return result;
428
+ } catch (error) {
429
+ logger.error(`[${this.id}] 搜索失败:`, error);
430
+ return {
431
+ agentId: this.id,
432
+ perspective: this.perspective,
433
+ findings: `搜索失败: ${error.message}`,
434
+ sources: [],
435
+ confidence: 0
436
+ };
437
+ }
438
+ }
439
+ /**
440
+ * 解析Agent响应的JSON
441
+ */
442
+ parseResponse(content) {
443
+ try {
444
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
445
+ if (jsonMatch) {
446
+ const parsed2 = JSON.parse(jsonMatch[1]);
447
+ return {
448
+ findings: parsed2.findings || parsed2.key_evidence,
449
+ sources: parsed2.sources || [],
450
+ confidence: parsed2.confidence
451
+ };
452
+ }
453
+ const parsed = JSON.parse(content);
454
+ return {
455
+ findings: parsed.findings || parsed.key_evidence,
456
+ sources: parsed.sources || [],
457
+ confidence: parsed.confidence
458
+ };
459
+ } catch {
460
+ return {};
461
+ }
462
+ }
463
+ };
464
+ var SearchAgentManager = class {
465
+ constructor(ctx, config) {
466
+ this.ctx = ctx;
467
+ this.config = config;
468
+ this.initAgents();
469
+ }
470
+ static {
471
+ __name(this, "SearchAgentManager");
472
+ }
473
+ agents = [];
474
+ initAgents() {
475
+ const models = this.config.searchModels;
476
+ for (let i = 0; i < models.length; i++) {
477
+ this.agents.push(new SearchAgent(this.ctx, this.config, i, models[i]));
478
+ }
479
+ }
480
+ /**
481
+ * 并行执行所有Agent的搜索
482
+ */
483
+ async searchAll(content) {
484
+ const logger = this.ctx.logger("isthattrue");
485
+ if (this.agents.length === 0) {
486
+ logger.warn("未配置搜索模型");
487
+ return [];
488
+ }
489
+ logger.info(`启动 ${this.agents.length} 个搜索Agent`);
490
+ const results = await Promise.all(
491
+ this.agents.map(
492
+ (agent) => this.withTimeout(
493
+ agent.search(content),
494
+ this.config.timeout,
495
+ agent
496
+ )
497
+ )
498
+ );
499
+ return results;
500
+ }
501
+ /**
502
+ * 带超时的Promise包装
503
+ */
504
+ async withTimeout(promise, timeout, agent) {
505
+ return Promise.race([
506
+ promise,
507
+ new Promise((_, reject) => {
508
+ setTimeout(() => reject(new Error("搜索超时")), timeout);
509
+ })
510
+ ]).catch((error) => ({
511
+ agentId: agent.id,
512
+ perspective: agent.perspective,
513
+ findings: `错误: ${error.message}`,
514
+ sources: [],
515
+ confidence: 0
516
+ }));
517
+ }
518
+ };
519
+
520
+ // src/agents/verifyAgent.ts
521
+ var VerifyAgent = class {
522
+ constructor(ctx, config) {
523
+ this.ctx = ctx;
524
+ this.config = config;
525
+ this.chatluna = new ChatlunaAdapter(ctx, config);
526
+ this.logger = ctx.logger("isthattrue");
527
+ }
528
+ static {
529
+ __name(this, "VerifyAgent");
530
+ }
531
+ chatluna;
532
+ logger;
533
+ /**
534
+ * 执行验证判决
535
+ */
536
+ async verify(originalContent, searchResults) {
537
+ const startTime = Date.now();
538
+ this.logger.info("开始综合验证...");
539
+ try {
540
+ const prompt = buildVerifyPrompt(
541
+ originalContent.text,
542
+ searchResults.map((r) => ({
543
+ perspective: r.perspective,
544
+ findings: r.findings,
545
+ sources: r.sources
546
+ }))
547
+ );
548
+ const response = await this.chatluna.chatWithRetry(
549
+ {
550
+ model: this.config.verifyModel,
551
+ message: prompt,
552
+ systemPrompt: VERIFY_AGENT_SYSTEM_PROMPT
553
+ },
554
+ this.config.maxRetries
555
+ );
556
+ const parsed = this.parseVerifyResponse(response.content);
557
+ const processingTime = Date.now() - startTime;
558
+ const result = {
559
+ originalContent,
560
+ searchResults,
561
+ verdict: parsed.verdict,
562
+ reasoning: parsed.reasoning,
563
+ sources: this.aggregateSources(searchResults, parsed.sources),
564
+ confidence: parsed.confidence,
565
+ processingTime
566
+ };
567
+ this.logger.info(`验证完成,判决: ${result.verdict},可信度: ${result.confidence}`);
568
+ return result;
569
+ } catch (error) {
570
+ this.logger.error("验证失败:", error);
571
+ return {
572
+ originalContent,
573
+ searchResults,
574
+ verdict: "uncertain" /* UNCERTAIN */,
575
+ reasoning: `验证过程发生错误: ${error.message}`,
576
+ sources: this.aggregateSources(searchResults, []),
577
+ confidence: 0,
578
+ processingTime: Date.now() - startTime
579
+ };
580
+ }
581
+ }
582
+ /**
583
+ * 解析验证响应
584
+ */
585
+ parseVerifyResponse(content) {
586
+ try {
587
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
588
+ let parsed;
589
+ if (jsonMatch) {
590
+ parsed = JSON.parse(jsonMatch[1]);
591
+ } else {
592
+ parsed = JSON.parse(content);
593
+ }
594
+ return {
595
+ verdict: this.normalizeVerdict(parsed.verdict),
596
+ reasoning: parsed.reasoning || parsed.key_evidence || "无详细说明",
597
+ sources: parsed.sources || [],
598
+ confidence: parsed.confidence || 0.5
599
+ };
600
+ } catch {
601
+ return {
602
+ verdict: this.extractVerdictFromText(content),
603
+ reasoning: content,
604
+ sources: [],
605
+ confidence: 0.3
606
+ };
607
+ }
608
+ }
609
+ /**
610
+ * 标准化判决结果
611
+ */
612
+ normalizeVerdict(verdict) {
613
+ const normalized = verdict?.toLowerCase()?.trim();
614
+ const mapping = {
615
+ "true": "true" /* TRUE */,
616
+ "真实": "true" /* TRUE */,
617
+ "正确": "true" /* TRUE */,
618
+ "false": "false" /* FALSE */,
619
+ "虚假": "false" /* FALSE */,
620
+ "错误": "false" /* FALSE */,
621
+ "partially_true": "partially_true" /* PARTIALLY_TRUE */,
622
+ "partial": "partially_true" /* PARTIALLY_TRUE */,
623
+ "部分真实": "partially_true" /* PARTIALLY_TRUE */,
624
+ "uncertain": "uncertain" /* UNCERTAIN */,
625
+ "不确定": "uncertain" /* UNCERTAIN */,
626
+ "无法确定": "uncertain" /* UNCERTAIN */
627
+ };
628
+ return mapping[normalized] || "uncertain" /* UNCERTAIN */;
629
+ }
630
+ /**
631
+ * 从文本中提取判决
632
+ */
633
+ extractVerdictFromText(text) {
634
+ const lower = text.toLowerCase();
635
+ if (lower.includes("虚假") || lower.includes("false") || lower.includes("错误")) {
636
+ return "false" /* FALSE */;
637
+ }
638
+ if (lower.includes("部分真实") || lower.includes("partially")) {
639
+ return "partially_true" /* PARTIALLY_TRUE */;
640
+ }
641
+ if (lower.includes("真实") || lower.includes("true") || lower.includes("正确")) {
642
+ return "true" /* TRUE */;
643
+ }
644
+ return "uncertain" /* UNCERTAIN */;
645
+ }
646
+ /**
647
+ * 汇总所有来源
648
+ */
649
+ aggregateSources(searchResults, verifySources) {
650
+ const allSources = /* @__PURE__ */ new Set();
651
+ for (const result of searchResults) {
652
+ for (const source of result.sources) {
653
+ allSources.add(source);
654
+ }
655
+ }
656
+ for (const source of verifySources) {
657
+ allSources.add(source);
658
+ }
659
+ return [...allSources];
660
+ }
661
+ };
662
+
663
+ // src/services/messageParser.ts
664
+ var import_koishi = require("koishi");
665
+ var MessageParser = class {
666
+ constructor(ctx) {
667
+ this.ctx = ctx;
668
+ }
669
+ static {
670
+ __name(this, "MessageParser");
671
+ }
672
+ /**
673
+ * 从会话中提取引用消息的内容
674
+ */
675
+ async parseQuotedMessage(session) {
676
+ const result = {
677
+ text: "",
678
+ images: [],
679
+ hasQuote: false
680
+ };
681
+ const quote = session.quote;
682
+ if (!quote) {
683
+ return null;
684
+ }
685
+ result.hasQuote = true;
686
+ const elements = quote.elements || [];
687
+ for (const element of elements) {
688
+ if (element.type === "text") {
689
+ result.text += element.attrs?.content || "";
690
+ } else if (element.type === "img" || element.type === "image") {
691
+ const src = element.attrs?.src || element.attrs?.url;
692
+ if (src) {
693
+ result.images.push(src);
694
+ }
695
+ }
696
+ }
697
+ if (elements.length === 0 && quote.content) {
698
+ const parsed = this.parseContent(quote.content);
699
+ result.text = parsed.text;
700
+ result.images = parsed.images;
701
+ }
702
+ return result;
703
+ }
704
+ /**
705
+ * 从整个会话中提取可验证内容(优先引用,其次是当前消息)
706
+ */
707
+ async parseSession(session) {
708
+ const quoted = await this.parseQuotedMessage(session);
709
+ if (quoted) return quoted;
710
+ const result = {
711
+ text: "",
712
+ images: [],
713
+ hasQuote: false
714
+ };
715
+ const elements = session.elements || [];
716
+ this.ctx.logger("isthattrue").debug("Parsing session elements:", JSON.stringify(elements));
717
+ for (let i = 0; i < elements.length; i++) {
718
+ const element = elements[i];
719
+ if (element.type === "text") {
720
+ let content = element.attrs?.content || "";
721
+ if (i === 0) {
722
+ content = content.replace(/^[^\s]+\s*/, "");
723
+ }
724
+ result.text += content;
725
+ } else if (element.type === "img" || element.type === "image") {
726
+ const src = element.attrs?.src || element.attrs?.url;
727
+ if (src) {
728
+ result.images.push(src);
729
+ }
730
+ }
731
+ }
732
+ if (result.text.trim() || result.images.length > 0) {
733
+ return result;
734
+ }
735
+ return null;
736
+ }
737
+ /**
738
+ * 解析消息内容字符串
739
+ */
740
+ parseContent(content) {
741
+ const text = [];
742
+ const images = [];
743
+ try {
744
+ const elements = import_koishi.h.parse(content);
745
+ for (const el of elements) {
746
+ if (el.type === "text") {
747
+ text.push(el.attrs?.content || String(el));
748
+ } else if (el.type === "img" || el.type === "image") {
749
+ const src = el.attrs?.src || el.attrs?.url;
750
+ if (src) {
751
+ images.push(src);
752
+ }
753
+ }
754
+ }
755
+ } catch {
756
+ text.push(content);
757
+ }
758
+ return {
759
+ text: text.join(" ").trim(),
760
+ images
761
+ };
762
+ }
763
+ /**
764
+ * 获取图片的base64编码
765
+ */
766
+ async imageToBase64(url) {
767
+ try {
768
+ if (url.startsWith("data:image")) {
769
+ return url.split(",")[1] || url;
770
+ }
771
+ if (url.startsWith("file://")) {
772
+ this.ctx.logger("isthattrue").warn("本地文件暂不支持:", url);
773
+ return null;
774
+ }
775
+ const response = await this.ctx.http.get(url, {
776
+ responseType: "arraybuffer"
777
+ });
778
+ const buffer = Buffer.from(response);
779
+ return buffer.toString("base64");
780
+ } catch (error) {
781
+ this.ctx.logger("isthattrue").error("图片转换失败:", error);
782
+ return null;
783
+ }
784
+ }
785
+ /**
786
+ * 准备消息内容用于LLM处理
787
+ * 将图片转换为base64,合并文本
788
+ */
789
+ async prepareForLLM(content) {
790
+ const imageBase64List = [];
791
+ for (const imageUrl of content.images) {
792
+ const base64 = await this.imageToBase64(imageUrl);
793
+ if (base64) {
794
+ imageBase64List.push(base64);
795
+ }
796
+ }
797
+ return {
798
+ text: content.text,
799
+ imageBase64List
800
+ };
801
+ }
802
+ };
803
+
804
+ // src/config.ts
805
+ var import_koishi2 = require("koishi");
806
+ var Config = import_koishi2.Schema.intersect([
807
+ import_koishi2.Schema.object({
808
+ searchModels: import_koishi2.Schema.array(import_koishi2.Schema.dynamic("model")).default([]).description("搜索用模型列表 (每个模型对应一个搜索Agent,需要支持联网搜索)"),
809
+ verifyModel: import_koishi2.Schema.dynamic("model").default("无").description("验证用模型 (推荐使用低幻觉率模型)")
810
+ }).description("模型配置"),
811
+ import_koishi2.Schema.object({
812
+ timeout: import_koishi2.Schema.number().min(1e4).max(3e5).default(6e4).description("单次请求超时时间(毫秒)"),
813
+ maxRetries: import_koishi2.Schema.number().min(0).max(5).default(2).description("失败重试次数")
814
+ }).description("Agent配置"),
815
+ import_koishi2.Schema.object({
816
+ verbose: import_koishi2.Schema.boolean().default(false).description("显示详细验证过程 (进度提示)"),
817
+ enableOCR: import_koishi2.Schema.boolean().default(true).description("启用图片文字识别"),
818
+ outputFormat: import_koishi2.Schema.union([
819
+ import_koishi2.Schema.const("auto").description("自动 (QQ使用纯文本)"),
820
+ import_koishi2.Schema.const("markdown").description("Markdown"),
821
+ import_koishi2.Schema.const("plain").description("纯文本")
822
+ ]).default("auto").description("输出格式"),
823
+ useForwardMessage: import_koishi2.Schema.boolean().default(true).description("使用合并转发消息展示详情 (仅支持QQ)"),
824
+ bypassProxy: import_koishi2.Schema.boolean().default(false).description("是否绕过系统代理"),
825
+ logLLMDetails: import_koishi2.Schema.boolean().default(false).description("是否打印 LLM 请求体和响应详情 (Debug用)")
826
+ }).description("其他设置")
827
+ ]);
828
+
829
+ // src/index.ts
830
+ var name = "isthattrue";
831
+ var inject = ["chatluna"];
832
+ var usage = `
833
+ ## 事实核查插件
834
+
835
+ 使用多Agent架构对消息进行事实核查验证。
836
+
837
+ ### 使用方法
838
+
839
+ 1. 引用一条需要验证的消息
840
+ 2. 发送 \`tof\` 指令
841
+ 3. 等待验证结果
842
+
843
+ ### 工作流程
844
+
845
+ 1. **解析阶段**: 提取引用消息中的文本和图片
846
+ 2. **搜索阶段**: 多个Agent并行从不同角度搜索信息
847
+ 3. **验证阶段**: 综合搜索结果,由低幻觉率LLM做出判决
848
+
849
+ ### 判决类别
850
+
851
+ - ✅ **真实**: 有充分可靠证据支持
852
+ - ❌ **虚假**: 有充分可靠证据反驳
853
+ - ⚠️ **部分真实**: 声明中部分内容属实
854
+ - ❓ **无法确定**: 证据不足或相互矛盾
855
+ `;
856
+ function apply(ctx, config) {
857
+ const logger = ctx.logger("isthattrue");
858
+ const messageParser = new MessageParser(ctx);
859
+ ctx.command("tof", "验证消息的真实性").alias("真假").alias("事实核查").alias("factcheck").option("verbose", "-v 显示详细过程").action(async ({ session, options }) => {
860
+ logger.info("tof 命令被触发");
861
+ if (!session) {
862
+ logger.warn("session 为空");
863
+ return "无法获取会话信息";
864
+ }
865
+ logger.info(`用户 ${session.userId} 在 ${session.channelId} 触发 tof 命令`);
866
+ logger.debug("Session elements:", JSON.stringify(session.elements));
867
+ const verbose = options?.verbose ?? config.verbose;
868
+ const format = config.outputFormat === "auto" ? session.platform === "qq" ? "plain" : "markdown" : config.outputFormat;
869
+ const chatluna = new ChatlunaAdapter(ctx, config);
870
+ if (!chatluna.isAvailable()) {
871
+ return "❌ Chatluna 服务不可用,请确保已安装并启用 koishi-plugin-chatluna";
872
+ }
873
+ const content = await messageParser.parseSession(session);
874
+ if (!content || !content.text && content.images.length === 0) {
875
+ return "❌ 请提供需要验证的内容\n\n使用方法:\n1. 引用一条消息后发送 tof\n2. 直接发送 tof [文本或图片]";
876
+ }
877
+ if (verbose) {
878
+ await session.send("🔍 正在验证消息真实性,请稍候...");
879
+ }
880
+ try {
881
+ const startTime = Date.now();
882
+ let textToVerify = content.text;
883
+ if (content.images.length > 0 && config.enableOCR) {
884
+ if (verbose) {
885
+ await session.send("📷 正在识别图片内容...");
886
+ }
887
+ try {
888
+ const prepared = await messageParser.prepareForLLM(content);
889
+ if (prepared.imageBase64List.length > 0) {
890
+ const ocrResponse = await chatluna.chat({
891
+ model: config.verifyModel,
892
+ message: OCR_PROMPT,
893
+ images: prepared.imageBase64List
894
+ });
895
+ textToVerify = `${content.text}
896
+
897
+ [图片内容]: ${ocrResponse.content}`;
898
+ }
899
+ } catch (error) {
900
+ logger.warn("OCR识别失败:", error);
901
+ }
902
+ }
903
+ if (!textToVerify.trim()) {
904
+ return "❌ 无法提取可验证的内容";
905
+ }
906
+ if (config.searchModels.length === 0) {
907
+ return "❌ 请先在配置中添加至少一个搜索模型";
908
+ }
909
+ if (verbose) {
910
+ await session.send(`🤖 启动 ${config.searchModels.length} 个搜索Agent...`);
911
+ }
912
+ const searchManager = new SearchAgentManager(ctx, config);
913
+ const searchResults = await searchManager.searchAll(textToVerify);
914
+ if (verbose) {
915
+ const successCount = searchResults.filter((r) => r.confidence > 0).length;
916
+ await session.send(`✅ 搜索完成 (${successCount}/${searchResults.length} 成功)`);
917
+ await session.send("⚖️ 正在综合分析...");
918
+ }
919
+ const verifyAgent = new VerifyAgent(ctx, config);
920
+ const result = await Promise.race([
921
+ verifyAgent.verify(content, searchResults),
922
+ new Promise((_, reject) => {
923
+ setTimeout(() => reject(new Error("验证阶段超时")), config.timeout);
924
+ })
925
+ ]).catch((error) => {
926
+ logger.error("验证超时或出错:", error);
927
+ return {
928
+ originalContent: content,
929
+ searchResults,
930
+ verdict: "uncertain" /* UNCERTAIN */,
931
+ reasoning: `验证超时或出错: ${error.message}`,
932
+ sources: [],
933
+ confidence: 0,
934
+ processingTime: Date.now() - startTime
935
+ };
936
+ });
937
+ const searchResultsForOutput = searchResults.map((r) => ({
938
+ agentId: r.agentId,
939
+ perspective: r.perspective,
940
+ findings: r.findings
941
+ }));
942
+ const useForward = config.useForwardMessage && session.platform === "onebot";
943
+ if (useForward) {
944
+ const { summary, details } = formatForwardMessages(
945
+ textToVerify,
946
+ searchResultsForOutput,
947
+ result.verdict,
948
+ result.reasoning,
949
+ result.sources,
950
+ result.confidence,
951
+ result.processingTime
952
+ );
953
+ const forwardNodes = details.map(
954
+ (detail) => (0, import_koishi3.h)("message", { nickname: "事实核查", userId: session.selfId }, detail)
955
+ );
956
+ await session.send(summary);
957
+ await session.send((0, import_koishi3.h)("message", { forward: true }, forwardNodes));
958
+ return;
959
+ }
960
+ const output = formatVerificationOutput(
961
+ textToVerify,
962
+ searchResultsForOutput,
963
+ result.verdict,
964
+ result.reasoning,
965
+ result.sources,
966
+ result.confidence,
967
+ result.processingTime,
968
+ format
969
+ );
970
+ return output;
971
+ } catch (error) {
972
+ logger.error("验证过程出错:", error);
973
+ return `❌ 验证过程发生错误: ${error.message}`;
974
+ }
975
+ });
976
+ ctx.command("tof.quick <text:text>", "快速验证文本真实性").action(async ({ session }, text) => {
977
+ if (!session) return "无法获取会话信息";
978
+ if (!text?.trim()) return "请提供需要验证的文本";
979
+ const format = config.outputFormat === "auto" ? session.platform === "qq" ? "plain" : "markdown" : config.outputFormat;
980
+ const chatluna = new ChatlunaAdapter(ctx, config);
981
+ if (!chatluna.isAvailable()) {
982
+ return "❌ Chatluna 服务不可用";
983
+ }
984
+ await session.send("🔍 快速验证中...");
985
+ try {
986
+ if (config.searchModels.length === 0) {
987
+ return "❌ 请先在配置中添加至少一个搜索模型";
988
+ }
989
+ const quickConfig = { ...config, searchModels: [config.searchModels[0]] };
990
+ const searchManager = new SearchAgentManager(ctx, quickConfig);
991
+ const searchResults = await searchManager.searchAll(text);
992
+ const verifyAgent = new VerifyAgent(ctx, quickConfig);
993
+ const result = await verifyAgent.verify(
994
+ { text, images: [], hasQuote: false },
995
+ searchResults
996
+ );
997
+ const verdictEmoji = {
998
+ ["true" /* TRUE */]: "✅ 真实",
999
+ ["false" /* FALSE */]: "❌ 虚假",
1000
+ ["partially_true" /* PARTIALLY_TRUE */]: "⚠️ 部分真实",
1001
+ ["uncertain" /* UNCERTAIN */]: "❓ 无法确定"
1002
+ };
1003
+ const confidenceValue = Math.round(result.confidence * 100);
1004
+ const reasoning = result.reasoning.substring(0, 200);
1005
+ if (format === "plain") {
1006
+ return `${verdictEmoji[result.verdict]} (${confidenceValue}%)
1007
+ ${reasoning}`;
1008
+ }
1009
+ return `**${verdictEmoji[result.verdict]}** (${confidenceValue}%)
1010
+
1011
+ ${reasoning}`;
1012
+ } catch (error) {
1013
+ return `❌ 验证失败: ${error.message}`;
1014
+ }
1015
+ });
1016
+ logger.info("isthattrue 插件已加载");
1017
+ }
1018
+ __name(apply, "apply");
1019
+ // Annotate the CommonJS export names for ESM import in node:
1020
+ 0 && (module.exports = {
1021
+ Config,
1022
+ apply,
1023
+ inject,
1024
+ name,
1025
+ usage
1026
+ });