koishi-plugin-dify-ai 0.1.6 → 0.1.9
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/config.d.ts +8 -5
- package/lib/index.js +362 -170
- package/lib/requester/variables.d.ts +5 -1
- package/lib/types.d.ts +5 -0
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -52,6 +52,9 @@ export type VariableEntry = {
|
|
|
52
52
|
source: 'constant';
|
|
53
53
|
field: string;
|
|
54
54
|
value: string;
|
|
55
|
+
} | {
|
|
56
|
+
source: 'json';
|
|
57
|
+
template: string;
|
|
55
58
|
};
|
|
56
59
|
/** 作用域的一行:维度 + 值 */
|
|
57
60
|
export interface ScopeRow {
|
|
@@ -111,8 +114,8 @@ export interface Config {
|
|
|
111
114
|
}
|
|
112
115
|
export declare const Config: Schema<Config>;
|
|
113
116
|
/**
|
|
114
|
-
*
|
|
115
|
-
* 数量与单文件大小 0 均为不限,大小仍与 Dify 各类型上限取较小值。
|
|
117
|
+
* 上传消息文件配置(含历史形态迁移:0.1.x 布尔、0.1.7 mode)。
|
|
118
|
+
* 数量与单文件大小 0 与 -1 均为不限,大小仍与 Dify 各类型上限取较小值。
|
|
116
119
|
*/
|
|
117
120
|
export declare function resolveUploadConfig(config: Config): {
|
|
118
121
|
enabled: boolean;
|
|
@@ -120,9 +123,9 @@ export declare function resolveUploadConfig(config: Config): {
|
|
|
120
123
|
maxMb: number;
|
|
121
124
|
};
|
|
122
125
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
+
* 引用触发消息配置(含历史形态迁移:0.1.x 布尔与 quoteTrigger 透传、0.1.7 mode、布尔开关新形态)。
|
|
127
|
+
* 迁移判据沿用:旧布尔被透传保留且配置未被用户改动(默认形态)时以旧布尔为准。
|
|
128
|
+
* 阈值 -1 与 0 同为总是引用(比较 elapsed >= 阈值 自然兼容)。
|
|
126
129
|
*/
|
|
127
130
|
export declare function resolveQuoteConfig(config: Config): {
|
|
128
131
|
enabled: boolean;
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,15 @@ var en_US_default = {
|
|
|
49
49
|
|
|
50
50
|
// src/config.ts
|
|
51
51
|
var import_koishi = require("koishi");
|
|
52
|
+
|
|
53
|
+
// src/types.ts
|
|
54
|
+
function isMessageTrigger(type) {
|
|
55
|
+
return type !== "schedule" && type !== "density";
|
|
56
|
+
}
|
|
57
|
+
__name(isMessageTrigger, "isMessageTrigger");
|
|
58
|
+
var JSON_PLACEHOLDER = /\{\{\s*(\w+)(?::(\d+))?\s*\}\}/g;
|
|
59
|
+
|
|
60
|
+
// src/config.ts
|
|
52
61
|
var appTypeSchema = import_koishi.Schema.union([
|
|
53
62
|
import_koishi.Schema.const("auto").description("自动探测"),
|
|
54
63
|
import_koishi.Schema.const("chat").description("Chatbot"),
|
|
@@ -83,7 +92,7 @@ var conditionSchema = import_koishi.Schema.union([
|
|
|
83
92
|
import_koishi.Schema.object({
|
|
84
93
|
type: import_koishi.Schema.const("schedule").required(),
|
|
85
94
|
cron: import_koishi.Schema.string().default("").description("Cron 表达式"),
|
|
86
|
-
intervalMinutes: import_koishi.Schema.number().default(
|
|
95
|
+
intervalMinutes: import_koishi.Schema.number().default(-1).min(-1).description("间隔分钟")
|
|
87
96
|
}).default({ type: "schedule", cron: "", intervalMinutes: 0 }).description("定时"),
|
|
88
97
|
import_koishi.Schema.object({
|
|
89
98
|
type: import_koishi.Schema.const("density").required(),
|
|
@@ -121,7 +130,7 @@ var variableSchema = import_koishi.Schema.union([
|
|
|
121
130
|
import_koishi.Schema.object({
|
|
122
131
|
source: import_koishi.Schema.const("history").required(),
|
|
123
132
|
field: import_koishi.Schema.string().required().description("字段名"),
|
|
124
|
-
limit: import_koishi.Schema.
|
|
133
|
+
limit: import_koishi.Schema.number().default(-1).min(-1).max(100).description("条数,0 用全局")
|
|
125
134
|
}).default({ source: "history", field: "", limit: 0 }).description("历史消息"),
|
|
126
135
|
import_koishi.Schema.object({
|
|
127
136
|
source: import_koishi.Schema.const("meta").required(),
|
|
@@ -132,17 +141,32 @@ var variableSchema = import_koishi.Schema.union([
|
|
|
132
141
|
source: import_koishi.Schema.const("constant").required(),
|
|
133
142
|
field: import_koishi.Schema.string().required().description("字段名"),
|
|
134
143
|
value: import_koishi.Schema.string().role("textarea").required().description("固定文本")
|
|
135
|
-
}).default({ source: "constant", field: "", value: "" }).description("常量文本")
|
|
144
|
+
}).default({ source: "constant", field: "", value: "" }).description("常量文本"),
|
|
145
|
+
import_koishi.Schema.object({
|
|
146
|
+
source: import_koishi.Schema.const("json").required(),
|
|
147
|
+
template: import_koishi.Schema.string().role("textarea").required().description("JSON 对象")
|
|
148
|
+
}).default({ source: "json", template: "" }).description("JSON 对象")
|
|
136
149
|
]);
|
|
137
150
|
var quoteSchema = import_koishi.Schema.union([
|
|
138
|
-
import_koishi.Schema.
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
import_koishi.Schema.intersect([
|
|
152
|
+
import_koishi.Schema.object({
|
|
153
|
+
enabled: import_koishi.Schema.boolean().default(true).description("开启")
|
|
154
|
+
}),
|
|
155
|
+
import_koishi.Schema.union([
|
|
156
|
+
import_koishi.Schema.object({
|
|
157
|
+
enabled: import_koishi.Schema.const(true).required(),
|
|
158
|
+
minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1).description("回复耗时多少秒才引用,0 总是引用")
|
|
159
|
+
}),
|
|
160
|
+
import_koishi.Schema.object({})
|
|
161
|
+
])
|
|
162
|
+
]),
|
|
163
|
+
import_koishi.Schema.boolean(),
|
|
164
|
+
import_koishi.Schema.object({ mode: import_koishi.Schema.const("off").required() }),
|
|
141
165
|
import_koishi.Schema.object({
|
|
142
166
|
mode: import_koishi.Schema.const("on").required(),
|
|
143
|
-
minElapsedSeconds: import_koishi.Schema.
|
|
144
|
-
})
|
|
145
|
-
]);
|
|
167
|
+
minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1)
|
|
168
|
+
})
|
|
169
|
+
]).default({ enabled: true, minElapsedSeconds: -1 });
|
|
146
170
|
var cooldownSchema = import_koishi.Schema.union([
|
|
147
171
|
import_koishi.Schema.object({
|
|
148
172
|
mode: import_koishi.Schema.const("off").required()
|
|
@@ -158,15 +182,27 @@ var cooldownSchema = import_koishi.Schema.union([
|
|
|
158
182
|
}).default({ mode: "sliding", minSeconds: 30, maxSeconds: 120 }).description("滑动窗口")
|
|
159
183
|
]);
|
|
160
184
|
var uploadSchema = import_koishi.Schema.union([
|
|
161
|
-
import_koishi.Schema.
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
import_koishi.Schema.intersect([
|
|
186
|
+
import_koishi.Schema.object({
|
|
187
|
+
enabled: import_koishi.Schema.boolean().default(true).description("开启")
|
|
188
|
+
}),
|
|
189
|
+
import_koishi.Schema.union([
|
|
190
|
+
import_koishi.Schema.object({
|
|
191
|
+
enabled: import_koishi.Schema.const(true).required(),
|
|
192
|
+
maxCount: import_koishi.Schema.number().min(-1).default(-1).description("数量上限,0 不限"),
|
|
193
|
+
maxSizeMb: import_koishi.Schema.number().min(-1).default(-1).description("单文件大小上限 MB,0 不限")
|
|
194
|
+
}),
|
|
195
|
+
import_koishi.Schema.object({})
|
|
196
|
+
])
|
|
197
|
+
]),
|
|
198
|
+
import_koishi.Schema.boolean(),
|
|
199
|
+
import_koishi.Schema.object({ mode: import_koishi.Schema.const("off").required() }),
|
|
164
200
|
import_koishi.Schema.object({
|
|
165
201
|
mode: import_koishi.Schema.const("on").required(),
|
|
166
|
-
maxCount: import_koishi.Schema.
|
|
167
|
-
maxSizeMb: import_koishi.Schema.
|
|
168
|
-
})
|
|
169
|
-
]);
|
|
202
|
+
maxCount: import_koishi.Schema.number().min(-1).default(-1),
|
|
203
|
+
maxSizeMb: import_koishi.Schema.number().min(-1).default(-1)
|
|
204
|
+
})
|
|
205
|
+
]).default({ enabled: true, maxCount: -1, maxSizeMb: -1 });
|
|
170
206
|
var Config = import_koishi.Schema.intersect([
|
|
171
207
|
import_koishi.Schema.object({
|
|
172
208
|
apps: import_koishi.Schema.array(
|
|
@@ -184,7 +220,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
184
220
|
import_koishi.Schema.object({
|
|
185
221
|
name: import_koishi.Schema.string().required().description("规则名"),
|
|
186
222
|
enabled: import_koishi.Schema.boolean().default(true).description("启用"),
|
|
187
|
-
priority: import_koishi.Schema.number().default(
|
|
223
|
+
priority: import_koishi.Schema.number().default(-1).description("优先级,大者先匹配"),
|
|
188
224
|
app: import_koishi.Schema.string().required().description("应用名"),
|
|
189
225
|
conversationScope: import_koishi.Schema.union([
|
|
190
226
|
import_koishi.Schema.const("per-guild").description("按群聊"),
|
|
@@ -213,9 +249,9 @@ var Config = import_koishi.Schema.intersect([
|
|
|
213
249
|
}).description("触发规则"),
|
|
214
250
|
import_koishi.Schema.object({
|
|
215
251
|
parseElements: import_koishi.Schema.boolean().default(false).description("解析回复中的消息元素"),
|
|
216
|
-
quote: quoteSchema.
|
|
217
|
-
maxSegmentLength: import_koishi.Schema.
|
|
218
|
-
uploadFiles: uploadSchema.
|
|
252
|
+
quote: quoteSchema.description("引用触发消息"),
|
|
253
|
+
maxSegmentLength: import_koishi.Schema.number().min(-1).default(-1).description("分段长度,0 不分段"),
|
|
254
|
+
uploadFiles: uploadSchema.description("上传消息中的文件")
|
|
219
255
|
}).description("回复行为"),
|
|
220
256
|
import_koishi.Schema.object({
|
|
221
257
|
requestTimeout: import_koishi.Schema.natural().role("ms").default(3e5).description("请求超时"),
|
|
@@ -308,6 +344,17 @@ function toScope(rows) {
|
|
|
308
344
|
}
|
|
309
345
|
__name(toScope, "toScope");
|
|
310
346
|
function toVariable(entry) {
|
|
347
|
+
if (entry.source === "json") {
|
|
348
|
+
const template = entry.template ?? "";
|
|
349
|
+
if (!template.trim()) return null;
|
|
350
|
+
try {
|
|
351
|
+
const probe = JSON.parse(template.replace(JSON_PLACEHOLDER, "0"));
|
|
352
|
+
if (typeof probe !== "object" || probe === null || Array.isArray(probe)) return null;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return { field: "", source: { type: "json", template } };
|
|
357
|
+
}
|
|
311
358
|
const field = entry.field?.trim();
|
|
312
359
|
if (!field) return null;
|
|
313
360
|
if (entry.source === "constant") {
|
|
@@ -316,51 +363,51 @@ function toVariable(entry) {
|
|
|
316
363
|
}
|
|
317
364
|
if (entry.source === "meta") {
|
|
318
365
|
if (entry.key === "none") return null;
|
|
319
|
-
return { field, source: { type: "meta", key: entry.key
|
|
366
|
+
return { field, source: { type: "meta", key: entry.key } };
|
|
320
367
|
}
|
|
321
368
|
if (entry.source === "history") {
|
|
322
369
|
const limit = Number(entry.limit);
|
|
323
370
|
return { field, source: { type: "history", ...limit > 0 ? { limit } : {} } };
|
|
324
371
|
}
|
|
325
372
|
if (entry.key === "none" || entry.key === "history") return null;
|
|
326
|
-
return { field, source: { type: "session", key: entry.key
|
|
373
|
+
return { field, source: { type: "session", key: entry.key } };
|
|
327
374
|
}
|
|
328
375
|
__name(toVariable, "toVariable");
|
|
329
376
|
function resolveUploadConfig(config) {
|
|
330
|
-
|
|
331
|
-
|
|
377
|
+
const upload = config.uploadFiles;
|
|
378
|
+
if (typeof upload === "boolean") {
|
|
379
|
+
return upload ? { enabled: true, maxCount: 0, maxMb: 0 } : { enabled: false, maxCount: 0, maxMb: 0 };
|
|
380
|
+
}
|
|
381
|
+
if (upload?.mode === "off" || upload?.enabled === false) {
|
|
382
|
+
return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
332
383
|
}
|
|
333
|
-
if (config.uploadFiles?.mode === "off") return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
334
384
|
return {
|
|
335
385
|
enabled: true,
|
|
336
|
-
maxCount:
|
|
337
|
-
maxMb:
|
|
386
|
+
maxCount: Number(upload?.maxCount) || 0,
|
|
387
|
+
maxMb: Number(upload?.maxSizeMb) || 0
|
|
338
388
|
};
|
|
339
389
|
}
|
|
340
390
|
__name(resolveUploadConfig, "resolveUploadConfig");
|
|
341
391
|
function resolveQuoteConfig(config) {
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
392
|
+
const quote = config.quote;
|
|
393
|
+
if (typeof quote === "boolean") return { enabled: quote, minElapsedSeconds: 0 };
|
|
394
|
+
if (quote?.mode === "off") return { enabled: false, minElapsedSeconds: 0 };
|
|
395
|
+
const threshold = Number(quote?.minElapsedSeconds) || 0;
|
|
396
|
+
const untouched = !(threshold > 0) && quote?.enabled !== false;
|
|
397
|
+
if (typeof config.quoteTrigger === "boolean" && untouched) {
|
|
398
|
+
return { enabled: config.quoteTrigger, minElapsedSeconds: 0 };
|
|
399
|
+
}
|
|
400
|
+
return { enabled: quote?.mode === "on" ? true : quote?.enabled !== false, minElapsedSeconds: threshold };
|
|
348
401
|
}
|
|
349
402
|
__name(resolveQuoteConfig, "resolveQuoteConfig");
|
|
350
403
|
function toCooldown(entry) {
|
|
351
404
|
if (entry?.mode === "fixed") {
|
|
352
|
-
|
|
353
|
-
return seconds > 0 ? { mode: "fixed", ms: seconds * 1e3 } : null;
|
|
405
|
+
return { mode: "fixed", ms: Number(entry.seconds) * 1e3 };
|
|
354
406
|
}
|
|
355
407
|
if (entry?.mode === "sliding") {
|
|
356
408
|
const minSeconds = Number(entry.minSeconds);
|
|
357
409
|
const maxSeconds = Number(entry.maxSeconds);
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
mode: "sliding",
|
|
361
|
-
minMs: minSeconds * 1e3,
|
|
362
|
-
maxMs: Math.max(minSeconds, maxSeconds) * 1e3
|
|
363
|
-
};
|
|
410
|
+
return { mode: "sliding", minMs: minSeconds * 1e3, maxMs: Math.max(minSeconds, maxSeconds) * 1e3 };
|
|
364
411
|
}
|
|
365
412
|
return null;
|
|
366
413
|
}
|
|
@@ -371,12 +418,16 @@ function normalizeConfig(config) {
|
|
|
371
418
|
if (app.name?.trim() && !apps.has(app.name)) apps.set(app.name, app);
|
|
372
419
|
}
|
|
373
420
|
const rules = [];
|
|
421
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
374
422
|
for (const entry of config.rules ?? []) {
|
|
375
423
|
if (!entry.name?.trim() || entry.enabled === false) continue;
|
|
424
|
+
const name2 = entry.name.trim();
|
|
425
|
+
if (seenNames.has(name2)) continue;
|
|
426
|
+
seenNames.add(name2);
|
|
376
427
|
const conditions = (entry.conditions ?? []).filter((condition) => condition != null).map(toCondition).filter((condition) => condition != null);
|
|
377
428
|
const variables = (entry.variables ?? []).filter((variable) => variable != null).map(toVariable).filter((variable) => variable != null);
|
|
378
429
|
rules.push({
|
|
379
|
-
name:
|
|
430
|
+
name: name2,
|
|
380
431
|
enabled: true,
|
|
381
432
|
priority: Number(entry.priority) || 0,
|
|
382
433
|
app: entry.app?.trim() ?? "",
|
|
@@ -449,9 +500,6 @@ var DifyClient = class {
|
|
|
449
500
|
getAppInfo() {
|
|
450
501
|
return this.request("GET", "/info");
|
|
451
502
|
}
|
|
452
|
-
getParameters() {
|
|
453
|
-
return this.request("GET", "/parameters");
|
|
454
|
-
}
|
|
455
503
|
/** chat / agent-chat / advanced-chat(conversation_id 传空串表示新建会话) */
|
|
456
504
|
runChat(body) {
|
|
457
505
|
return this.request("POST", "/chat-messages", body);
|
|
@@ -500,6 +548,7 @@ __name(createDifyClient, "createDifyClient");
|
|
|
500
548
|
// src/utils/session-text.ts
|
|
501
549
|
var import_crypto = require("crypto");
|
|
502
550
|
var import_koishi2 = require("koishi");
|
|
551
|
+
var IMAGE_HASH_LENGTH = 8;
|
|
503
552
|
function parseTextSafe(content) {
|
|
504
553
|
if (!content) return "";
|
|
505
554
|
try {
|
|
@@ -519,7 +568,7 @@ __name(extractText, "extractText");
|
|
|
519
568
|
var IMAGE_PLACEHOLDER = "[图片]";
|
|
520
569
|
function imagePlaceholder(url) {
|
|
521
570
|
if (!url) return IMAGE_PLACEHOLDER;
|
|
522
|
-
return `[image:${(0, import_crypto.createHash)("sha1").update(url).digest("hex").slice(0,
|
|
571
|
+
return `[image:${(0, import_crypto.createHash)("sha1").update(url).digest("hex").slice(0, IMAGE_HASH_LENGTH)}]`;
|
|
523
572
|
}
|
|
524
573
|
__name(imagePlaceholder, "imagePlaceholder");
|
|
525
574
|
function annotateDroppedPlaceholders(text, droppedUrls) {
|
|
@@ -531,20 +580,23 @@ function annotateDroppedPlaceholders(text, droppedUrls) {
|
|
|
531
580
|
return result;
|
|
532
581
|
}
|
|
533
582
|
__name(annotateDroppedPlaceholders, "annotateDroppedPlaceholders");
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
583
|
+
function elementsOf(session) {
|
|
584
|
+
if (session.elements?.length) return session.elements;
|
|
585
|
+
try {
|
|
586
|
+
return import_koishi2.h.parse(session.content ?? "");
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
__name(elementsOf, "elementsOf");
|
|
592
|
+
function extractTextWithPlaceholders(session, options = {}) {
|
|
542
593
|
const parts = [];
|
|
543
|
-
for (const element of
|
|
594
|
+
for (const element of elementsOf(session)) {
|
|
544
595
|
if (element.type === "text") {
|
|
545
596
|
const text = element.attrs?.["content"] ?? "";
|
|
546
597
|
if (text) parts.push(text);
|
|
547
598
|
} else if (element.type === "at") {
|
|
599
|
+
if (options.excludeBotAt && element.attrs?.["id"] === session.selfId) continue;
|
|
548
600
|
const name2 = element.attrs?.["name"] ?? element.attrs?.["id"];
|
|
549
601
|
if (name2) parts.push(`@${name2}`);
|
|
550
602
|
} else if (element.type === "img" || element.type === "image") {
|
|
@@ -554,6 +606,16 @@ function extractTextWithPlaceholders(session) {
|
|
|
554
606
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
555
607
|
}
|
|
556
608
|
__name(extractTextWithPlaceholders, "extractTextWithPlaceholders");
|
|
609
|
+
function extractImageUrls(session) {
|
|
610
|
+
const urls = [];
|
|
611
|
+
for (const element of elementsOf(session)) {
|
|
612
|
+
if (element.type !== "img" && element.type !== "image") continue;
|
|
613
|
+
const source = element.attrs?.["url"] ?? element.attrs?.["src"];
|
|
614
|
+
if (typeof source === "string" && source) urls.push(source);
|
|
615
|
+
}
|
|
616
|
+
return urls;
|
|
617
|
+
}
|
|
618
|
+
__name(extractImageUrls, "extractImageUrls");
|
|
557
619
|
|
|
558
620
|
// src/history.ts
|
|
559
621
|
var HISTORY_CAPACITY_MULTIPLIER = 2;
|
|
@@ -575,7 +637,7 @@ function formatHistoryEntries(entries) {
|
|
|
575
637
|
}
|
|
576
638
|
__name(formatHistoryEntries, "formatHistoryEntries");
|
|
577
639
|
function formatHistoryBrief(entries) {
|
|
578
|
-
return entries.map((entry) => entry.messageId ? `[msg:${entry.messageId}] ${entry.
|
|
640
|
+
return entries.map((entry) => entry.messageId ? `[msg:${entry.messageId}] ${entry.contentText}` : entry.contentText).join("\n");
|
|
579
641
|
}
|
|
580
642
|
__name(formatHistoryBrief, "formatHistoryBrief");
|
|
581
643
|
function buildQuote(session) {
|
|
@@ -592,12 +654,15 @@ __name(buildQuote, "buildQuote");
|
|
|
592
654
|
function buildHistoryEntry(session, role = "user") {
|
|
593
655
|
const content = extractTextWithPlaceholders(session);
|
|
594
656
|
if (!content) return null;
|
|
657
|
+
const imageUrls = extractImageUrls(session);
|
|
595
658
|
return {
|
|
596
659
|
role,
|
|
597
660
|
userId: role === "assistant" ? session.selfId : session.userId ?? "未知用户",
|
|
598
|
-
username: role === "assistant" ? "你自己的发言" : session.username || session.userId
|
|
661
|
+
username: role === "assistant" ? "你自己的发言" : session.username || session.userId,
|
|
599
662
|
messageId: session.messageId,
|
|
600
663
|
content,
|
|
664
|
+
contentText: extractTextWithPlaceholders(session, { excludeBotAt: true }),
|
|
665
|
+
...imageUrls.length ? { imageUrls } : {},
|
|
601
666
|
timestamp: session.timestamp ?? Date.now(),
|
|
602
667
|
quote: role === "user" ? buildQuote(session) : void 0
|
|
603
668
|
};
|
|
@@ -635,41 +700,49 @@ function hasAt(session, id) {
|
|
|
635
700
|
return session.elements?.some((element) => element.type === "at" && element.attrs?.["id"] === id) ?? false;
|
|
636
701
|
}
|
|
637
702
|
__name(hasAt, "hasAt");
|
|
703
|
+
var NON_TEXT_PREFIX = new RegExp(`^(?:@\\S+|\\[image:[0-9a-f]{${IMAGE_HASH_LENGTH}}\\]|\\[图片\\]|\\s)*`);
|
|
704
|
+
function stripPrefixKeyword(verbatim, keyword) {
|
|
705
|
+
const prefix = NON_TEXT_PREFIX.exec(verbatim)?.[0] ?? "";
|
|
706
|
+
const rest = verbatim.slice(prefix.length);
|
|
707
|
+
if (!rest.startsWith(keyword)) return null;
|
|
708
|
+
return (prefix + rest.slice(keyword.length)).trim();
|
|
709
|
+
}
|
|
710
|
+
__name(stripPrefixKeyword, "stripPrefixKeyword");
|
|
638
711
|
function evaluateCondition(condition, session) {
|
|
639
712
|
const text = extractText(session);
|
|
640
|
-
const verbatim = extractTextWithPlaceholders(session);
|
|
713
|
+
const verbatim = extractTextWithPlaceholders(session, { excludeBotAt: true });
|
|
641
714
|
switch (condition.type) {
|
|
642
715
|
case "keyword-prefix": {
|
|
643
716
|
const keyword = condition.keywords.find((k) => k && text.startsWith(k));
|
|
644
717
|
if (!keyword) return null;
|
|
645
|
-
const rest = text.slice(keyword.length).trim();
|
|
646
|
-
return { type: "keyword-prefix",
|
|
718
|
+
const rest = verbatim.startsWith(keyword) ? verbatim.slice(keyword.length).trim() : stripPrefixKeyword(verbatim, keyword) ?? text.slice(keyword.length).trim();
|
|
719
|
+
return { type: "keyword-prefix", contentText: rest || verbatim };
|
|
647
720
|
}
|
|
648
721
|
case "keyword-contains": {
|
|
649
722
|
const keyword = condition.keywords.find((k) => k && text.includes(k));
|
|
650
723
|
if (!keyword) return null;
|
|
651
|
-
return { type: "keyword-contains",
|
|
724
|
+
return { type: "keyword-contains", contentText: verbatim };
|
|
652
725
|
}
|
|
653
726
|
case "at-bot": {
|
|
654
727
|
const botId = session.bot?.userId;
|
|
655
728
|
if (!botId) return null;
|
|
656
729
|
if (!session.stripped?.atSelf && !hasAt(session, botId)) return null;
|
|
657
|
-
return { type: "at-bot",
|
|
730
|
+
return { type: "at-bot", contentText: verbatim };
|
|
658
731
|
}
|
|
659
732
|
case "at-user": {
|
|
660
733
|
const isAtAnyUser = condition.userIds.some((id) => hasAt(session, id));
|
|
661
734
|
if (!isAtAnyUser) return null;
|
|
662
|
-
return { type: "at-user",
|
|
735
|
+
return { type: "at-user", contentText: verbatim };
|
|
663
736
|
}
|
|
664
737
|
case "quote-bot": {
|
|
665
738
|
const botId = session.bot?.userId;
|
|
666
739
|
if (!botId || session.quote?.user?.id !== botId) return null;
|
|
667
|
-
return { type: "quote-bot",
|
|
740
|
+
return { type: "quote-bot", contentText: verbatim };
|
|
668
741
|
}
|
|
669
742
|
case "quote-user": {
|
|
670
743
|
const author = session.quote?.user?.id;
|
|
671
744
|
if (!author || !condition.userIds.includes(author)) return null;
|
|
672
|
-
return { type: "quote-user",
|
|
745
|
+
return { type: "quote-user", contentText: verbatim };
|
|
673
746
|
}
|
|
674
747
|
default:
|
|
675
748
|
return null;
|
|
@@ -808,13 +881,7 @@ var RuleScheduler = class {
|
|
|
808
881
|
}
|
|
809
882
|
entries = [];
|
|
810
883
|
sync(rules) {
|
|
811
|
-
|
|
812
|
-
const nextEntries = [];
|
|
813
|
-
for (const rule of rules) {
|
|
814
|
-
const nextRunAt = previous.get(rule.name) ?? this.computeNextRun(rule);
|
|
815
|
-
if (nextRunAt != null) nextEntries.push({ rule, nextRunAt });
|
|
816
|
-
}
|
|
817
|
-
this.entries = nextEntries;
|
|
884
|
+
this.entries = rules.map((rule) => ({ rule, nextRunAt: this.computeNextRun(rule) })).filter((entry) => entry.nextRunAt != null);
|
|
818
885
|
}
|
|
819
886
|
tick() {
|
|
820
887
|
const now = Date.now();
|
|
@@ -864,8 +931,9 @@ var RuleScheduler = class {
|
|
|
864
931
|
|
|
865
932
|
// src/trigger/cooldown.ts
|
|
866
933
|
var BUFFER_CAPACITY = 1024;
|
|
934
|
+
var DENSITY_LANE_PREFIX = "density:";
|
|
867
935
|
function densityLane(index) {
|
|
868
|
-
return
|
|
936
|
+
return `${DENSITY_LANE_PREFIX}${index}`;
|
|
869
937
|
}
|
|
870
938
|
__name(densityLane, "densityLane");
|
|
871
939
|
function laneKeyOf(ruleName, lane) {
|
|
@@ -896,30 +964,33 @@ var TriggerCooldown = class {
|
|
|
896
964
|
});
|
|
897
965
|
}
|
|
898
966
|
isCooling(channelKey, ruleName, lane, now) {
|
|
899
|
-
const state = this.
|
|
967
|
+
const state = this.channels.get(channelKey)?.get(laneKeyOf(ruleName, lane));
|
|
900
968
|
return state != null && now < state.deadline;
|
|
901
969
|
}
|
|
902
|
-
/**
|
|
903
|
-
|
|
904
|
-
if (this.setting?.mode !== "sliding") return;
|
|
905
|
-
const state = this.lanesOf(channelKey).get(laneKeyOf(ruleName, lane));
|
|
906
|
-
if (!state) return;
|
|
970
|
+
/** 滑动窗口顺延:截止推到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
|
|
971
|
+
slide(state, setting, now) {
|
|
907
972
|
state.deadline = Math.min(
|
|
908
|
-
Math.max(state.deadline, now +
|
|
909
|
-
state.fireAt +
|
|
973
|
+
Math.max(state.deadline, now + setting.minMs),
|
|
974
|
+
state.fireAt + setting.maxMs
|
|
910
975
|
);
|
|
911
976
|
}
|
|
912
|
-
/**
|
|
977
|
+
/** 滑动窗口顺延:已到期的通道不复活 */
|
|
978
|
+
extend(channelKey, ruleName, lane, now) {
|
|
979
|
+
const setting = this.setting;
|
|
980
|
+
if (setting?.mode !== "sliding") return;
|
|
981
|
+
const state = this.channels.get(channelKey)?.get(laneKeyOf(ruleName, lane));
|
|
982
|
+
if (!state || now >= state.deadline) return;
|
|
983
|
+
this.slide(state, setting, now);
|
|
984
|
+
}
|
|
985
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动);已到期的不复活 */
|
|
913
986
|
extendDensityLanes(channelKey, now) {
|
|
914
|
-
|
|
987
|
+
const setting = this.setting;
|
|
988
|
+
if (setting?.mode !== "sliding") return;
|
|
915
989
|
const lanes = this.channels.get(channelKey);
|
|
916
990
|
if (!lanes) return;
|
|
917
991
|
for (const [key, state] of lanes) {
|
|
918
|
-
if (!key.includes("\ndensity:")) continue;
|
|
919
|
-
state
|
|
920
|
-
Math.max(state.deadline, now + this.setting.minMs),
|
|
921
|
-
state.fireAt + this.setting.maxMs
|
|
922
|
-
);
|
|
992
|
+
if (!key.includes("\ndensity:") || now >= state.deadline) continue;
|
|
993
|
+
this.slide(state, setting, now);
|
|
923
994
|
}
|
|
924
995
|
}
|
|
925
996
|
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
@@ -931,22 +1002,24 @@ var TriggerCooldown = class {
|
|
|
931
1002
|
state.buffer.splice(0, state.buffer.length - BUFFER_CAPACITY);
|
|
932
1003
|
}
|
|
933
1004
|
}
|
|
934
|
-
/**
|
|
935
|
-
collectExpired(now) {
|
|
1005
|
+
/** 到期扫描:取走到期通道(连缓冲一并交出),由调用方决定是否重新登记;限定频道时只扫该频道 */
|
|
1006
|
+
collectExpired(now, channelKey) {
|
|
936
1007
|
const expired = [];
|
|
937
|
-
|
|
938
|
-
|
|
1008
|
+
const targets = channelKey ? [[channelKey, this.channels.get(channelKey)]] : this.channels;
|
|
1009
|
+
for (const [key, lanes] of targets) {
|
|
1010
|
+
if (!lanes) continue;
|
|
1011
|
+
for (const [laneKey, state] of lanes) {
|
|
939
1012
|
if (now < state.deadline) continue;
|
|
940
|
-
const separator =
|
|
941
|
-
lanes.delete(
|
|
1013
|
+
const separator = laneKey.indexOf("\n");
|
|
1014
|
+
lanes.delete(laneKey);
|
|
942
1015
|
expired.push({
|
|
943
|
-
channelKey,
|
|
944
|
-
ruleName:
|
|
945
|
-
lane:
|
|
1016
|
+
channelKey: key,
|
|
1017
|
+
ruleName: laneKey.slice(0, separator),
|
|
1018
|
+
lane: laneKey.slice(separator + 1),
|
|
946
1019
|
buffer: state.buffer
|
|
947
1020
|
});
|
|
948
1021
|
}
|
|
949
|
-
if (!lanes.size) this.channels.delete(
|
|
1022
|
+
if (!lanes.size) this.channels.delete(key);
|
|
950
1023
|
}
|
|
951
1024
|
return expired;
|
|
952
1025
|
}
|
|
@@ -962,6 +1035,29 @@ var TriggerCooldown = class {
|
|
|
962
1035
|
|
|
963
1036
|
// src/trigger/engine.ts
|
|
964
1037
|
var SCHEDULER_TICK_MS = 1e3;
|
|
1038
|
+
function collectImageUrls(entries) {
|
|
1039
|
+
return entries.flatMap((entry) => entry?.role === "user" ? entry.imageUrls ?? [] : []);
|
|
1040
|
+
}
|
|
1041
|
+
__name(collectImageUrls, "collectImageUrls");
|
|
1042
|
+
function sessionIdentityOf(session) {
|
|
1043
|
+
return {
|
|
1044
|
+
platform: session.platform,
|
|
1045
|
+
selfId: session.selfId,
|
|
1046
|
+
channelId: session.channelId,
|
|
1047
|
+
guildId: session.guildId ?? session.channelId,
|
|
1048
|
+
userId: session.userId
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
__name(sessionIdentityOf, "sessionIdentityOf");
|
|
1052
|
+
function poolIdentityOf(channel) {
|
|
1053
|
+
return {
|
|
1054
|
+
platform: channel.platform,
|
|
1055
|
+
selfId: channel.selfId,
|
|
1056
|
+
channelId: channel.channelId,
|
|
1057
|
+
guildId: channel.guildId
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
__name(poolIdentityOf, "poolIdentityOf");
|
|
965
1061
|
var TriggerEngine = class {
|
|
966
1062
|
constructor(ctx, runner, history, rules, cooldown) {
|
|
967
1063
|
this.ctx = ctx;
|
|
@@ -998,6 +1094,7 @@ var TriggerEngine = class {
|
|
|
998
1094
|
if (this.ctx.bots[session.uid]) return next();
|
|
999
1095
|
if (!session.channelId) return next();
|
|
1000
1096
|
const channelKey = channelKeyOf(session.platform, session.channelId);
|
|
1097
|
+
this.tickCooldown(channelKey);
|
|
1001
1098
|
const entry = buildHistoryEntry(session);
|
|
1002
1099
|
if (entry) {
|
|
1003
1100
|
this.history.recordEntry(channelKey, entry);
|
|
@@ -1013,13 +1110,14 @@ var TriggerEngine = class {
|
|
|
1013
1110
|
}
|
|
1014
1111
|
}
|
|
1015
1112
|
return next(async (nextMiddleware) => {
|
|
1113
|
+
this.tickCooldown(channelKey);
|
|
1016
1114
|
const matched = this.matchRule(session);
|
|
1017
1115
|
if (!matched) return nextMiddleware();
|
|
1018
1116
|
if (this.cooldown.isCooling(channelKey, matched.rule.name, "message", Date.now())) {
|
|
1019
1117
|
this.cooldown.bufferMessage(channelKey, matched.rule.name, {
|
|
1020
1118
|
session,
|
|
1021
1119
|
entry,
|
|
1022
|
-
contentText: matched.condition.
|
|
1120
|
+
contentText: matched.condition.contentText
|
|
1023
1121
|
});
|
|
1024
1122
|
this.cooldown.extend(channelKey, matched.rule.name, "message", Date.now());
|
|
1025
1123
|
return;
|
|
@@ -1050,31 +1148,24 @@ var TriggerEngine = class {
|
|
|
1050
1148
|
this.beginCooldown(channelKeyOf(session.platform, session.channelId), rule.name, "message");
|
|
1051
1149
|
this.run({
|
|
1052
1150
|
session,
|
|
1053
|
-
|
|
1054
|
-
selfId: session.selfId,
|
|
1055
|
-
channelId: session.channelId,
|
|
1056
|
-
guildId: session.guildId ?? session.channelId,
|
|
1057
|
-
userId: session.userId,
|
|
1151
|
+
...sessionIdentityOf(session),
|
|
1058
1152
|
triggerType: condition.type,
|
|
1059
1153
|
rule,
|
|
1060
1154
|
// query 为该条消息的统一格式单行;纯 @ / 纯引用等无文本消息 entry 为空,由请求器占位兜底
|
|
1061
1155
|
queryText: entry ? formatHistoryEntry(entry) : "",
|
|
1062
|
-
contentText: condition.
|
|
1156
|
+
contentText: condition.contentText
|
|
1063
1157
|
});
|
|
1064
1158
|
}
|
|
1065
1159
|
fireDensity(fire, session) {
|
|
1066
1160
|
this.beginCooldown(channelKeyOf(session.platform, session.channelId), fire.rule.name, densityLane(fire.conditionIndex));
|
|
1067
1161
|
this.run({
|
|
1068
1162
|
session,
|
|
1069
|
-
|
|
1070
|
-
selfId: session.selfId,
|
|
1071
|
-
channelId: session.channelId,
|
|
1072
|
-
guildId: session.guildId ?? session.channelId,
|
|
1073
|
-
userId: session.userId,
|
|
1163
|
+
...sessionIdentityOf(session),
|
|
1074
1164
|
triggerType: "density",
|
|
1075
1165
|
rule: fire.rule,
|
|
1076
1166
|
queryText: formatHistoryEntries(fire.entries),
|
|
1077
|
-
contentText: formatHistoryBrief(fire.entries)
|
|
1167
|
+
contentText: formatHistoryBrief(fire.entries),
|
|
1168
|
+
imageUrls: collectImageUrls(fire.entries)
|
|
1078
1169
|
});
|
|
1079
1170
|
}
|
|
1080
1171
|
/** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
|
|
@@ -1085,29 +1176,37 @@ var TriggerEngine = class {
|
|
|
1085
1176
|
this.beginCooldown(channelKeyOf(first.session.platform, first.session.channelId), rule.name, "message");
|
|
1086
1177
|
this.run({
|
|
1087
1178
|
session: first.session,
|
|
1088
|
-
|
|
1089
|
-
selfId: first.session.selfId,
|
|
1090
|
-
channelId: first.session.channelId,
|
|
1091
|
-
guildId: first.session.guildId ?? first.session.channelId,
|
|
1092
|
-
userId: first.session.userId,
|
|
1179
|
+
...sessionIdentityOf(first.session),
|
|
1093
1180
|
triggerType: "merged",
|
|
1094
1181
|
rule,
|
|
1095
1182
|
queryText,
|
|
1096
|
-
contentText
|
|
1183
|
+
contentText,
|
|
1184
|
+
// 补齐缓冲内非首条消息的图片(首条由 session 元素提取,重复由上传入口去重)
|
|
1185
|
+
imageUrls: collectImageUrls(buffer.map((item) => item.entry))
|
|
1097
1186
|
});
|
|
1098
1187
|
}
|
|
1099
1188
|
/** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
|
|
1100
|
-
fireDensityExpiry(rule, conditionIndex, channel) {
|
|
1189
|
+
fireDensityExpiry(rule, conditionIndex, channel, now) {
|
|
1101
1190
|
this.beginCooldown(channelKeyOf(channel.platform, channel.channelId), rule.name, densityLane(conditionIndex));
|
|
1191
|
+
const condition = rule.conditions[conditionIndex];
|
|
1192
|
+
const windowMs = condition.type === "density" ? condition.densityWindowSeconds * 1e3 : 0;
|
|
1193
|
+
let userId;
|
|
1194
|
+
for (let i = channel.entries.length - 1; i >= 0; i--) {
|
|
1195
|
+
const item = channel.entries[i];
|
|
1196
|
+
if (item.role === "user" && now - item.timestamp <= windowMs) {
|
|
1197
|
+
userId = item.userId;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1102
1201
|
this.run({
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
channelId: channel.channelId,
|
|
1106
|
-
guildId: channel.guildId,
|
|
1202
|
+
...poolIdentityOf(channel),
|
|
1203
|
+
userId,
|
|
1107
1204
|
triggerType: "density",
|
|
1108
1205
|
rule,
|
|
1109
1206
|
queryText: formatHistoryEntries(channel.entries),
|
|
1110
|
-
contentText: formatHistoryBrief(channel.entries)
|
|
1207
|
+
contentText: formatHistoryBrief(channel.entries),
|
|
1208
|
+
imageUrls: collectImageUrls(channel.entries),
|
|
1209
|
+
isDirect: channel.isDirect
|
|
1111
1210
|
});
|
|
1112
1211
|
}
|
|
1113
1212
|
/**
|
|
@@ -1117,22 +1216,21 @@ var TriggerEngine = class {
|
|
|
1117
1216
|
fireSchedule(rule) {
|
|
1118
1217
|
for (const channel of this.pool.takeSchedule(rule)) {
|
|
1119
1218
|
this.run({
|
|
1120
|
-
|
|
1121
|
-
selfId: channel.selfId,
|
|
1122
|
-
channelId: channel.channelId,
|
|
1123
|
-
guildId: channel.guildId,
|
|
1219
|
+
...poolIdentityOf(channel),
|
|
1124
1220
|
triggerType: "schedule",
|
|
1125
1221
|
rule,
|
|
1126
1222
|
queryText: formatHistoryEntries(channel.entries),
|
|
1127
|
-
contentText: formatHistoryBrief(channel.entries)
|
|
1223
|
+
contentText: formatHistoryBrief(channel.entries),
|
|
1224
|
+
imageUrls: collectImageUrls(channel.entries),
|
|
1225
|
+
isDirect: channel.isDirect
|
|
1128
1226
|
});
|
|
1129
1227
|
}
|
|
1130
1228
|
}
|
|
1131
|
-
/**
|
|
1132
|
-
tickCooldown() {
|
|
1229
|
+
/** 扫描冷却到期(带频道只扫该频道,消息路径用;每秒 interval 全量扫描):消息通道合并触发,密度通道复查池 */
|
|
1230
|
+
tickCooldown(channelKey) {
|
|
1133
1231
|
if (!this.cooldown.enabled) return;
|
|
1134
1232
|
const now = Date.now();
|
|
1135
|
-
for (const expired of this.cooldown.collectExpired(now)) {
|
|
1233
|
+
for (const expired of this.cooldown.collectExpired(now, channelKey)) {
|
|
1136
1234
|
this.onCooldownExpired(expired, now);
|
|
1137
1235
|
}
|
|
1138
1236
|
}
|
|
@@ -1143,20 +1241,14 @@ var TriggerEngine = class {
|
|
|
1143
1241
|
if (expired.buffer.length) this.fireMerged(rule, expired.buffer);
|
|
1144
1242
|
return;
|
|
1145
1243
|
}
|
|
1146
|
-
const index = Number(expired.lane.slice(
|
|
1244
|
+
const index = Number(expired.lane.slice(DENSITY_LANE_PREFIX.length));
|
|
1147
1245
|
const condition = rule.conditions[index];
|
|
1148
1246
|
if (condition?.type !== "density") return;
|
|
1149
1247
|
const channel = this.pool.takeDensityChannel(rule, condition, expired.channelKey, now);
|
|
1150
|
-
if (channel) this.fireDensityExpiry(rule, index, channel);
|
|
1248
|
+
if (channel) this.fireDensityExpiry(rule, index, channel, now);
|
|
1151
1249
|
}
|
|
1152
1250
|
};
|
|
1153
1251
|
|
|
1154
|
-
// src/types.ts
|
|
1155
|
-
function isMessageTrigger(type) {
|
|
1156
|
-
return type !== "schedule" && type !== "density";
|
|
1157
|
-
}
|
|
1158
|
-
__name(isMessageTrigger, "isMessageTrigger");
|
|
1159
|
-
|
|
1160
1252
|
// src/requester/files.ts
|
|
1161
1253
|
var import_promises = require("fs/promises");
|
|
1162
1254
|
var import_fs = require("fs");
|
|
@@ -1239,11 +1331,26 @@ function extractFileCandidates(session) {
|
|
|
1239
1331
|
return candidates;
|
|
1240
1332
|
}
|
|
1241
1333
|
__name(extractFileCandidates, "extractFileCandidates");
|
|
1334
|
+
function percentDecodeBytes(input) {
|
|
1335
|
+
const bytes = [];
|
|
1336
|
+
for (let i = 0; i < input.length; i++) {
|
|
1337
|
+
const char = input[i];
|
|
1338
|
+
if (char === "%" && /^[0-9a-fA-F]{2}$/.test(input.slice(i + 1, i + 3))) {
|
|
1339
|
+
bytes.push(parseInt(input.slice(i + 1, i + 3), 16));
|
|
1340
|
+
i += 2;
|
|
1341
|
+
} else {
|
|
1342
|
+
bytes.push(char.charCodeAt(0) & 255);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return Buffer.from(bytes);
|
|
1346
|
+
}
|
|
1347
|
+
__name(percentDecodeBytes, "percentDecodeBytes");
|
|
1242
1348
|
function parseDataUrl(source) {
|
|
1243
|
-
const match = source.match(/^data:([
|
|
1349
|
+
const match = source.match(/^data:([^;,]*)((?:;[^;,]*)*)?,(.+)$/);
|
|
1244
1350
|
if (!match) return null;
|
|
1245
|
-
const mimeType = match[1];
|
|
1246
|
-
const
|
|
1351
|
+
const mimeType = match[1] || "application/octet-stream";
|
|
1352
|
+
const isBase64 = /(?:^|;)base64/.test(match[2] ?? "");
|
|
1353
|
+
const buffer = isBase64 ? Buffer.from(match[3], "base64") : percentDecodeBytes(match[3]);
|
|
1247
1354
|
return { buffer, fileName: `file.${guessExtension(mimeType)}`, mimeType };
|
|
1248
1355
|
}
|
|
1249
1356
|
__name(parseDataUrl, "parseDataUrl");
|
|
@@ -1317,19 +1424,28 @@ async function uploadOne(ctx, client, candidate, difyUser, limit) {
|
|
|
1317
1424
|
}
|
|
1318
1425
|
}
|
|
1319
1426
|
__name(uploadOne, "uploadOne");
|
|
1320
|
-
async function
|
|
1321
|
-
const
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1427
|
+
async function uploadCandidates(ctx, client, candidates, difyUser, limit) {
|
|
1428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1429
|
+
const unique = candidates.filter((candidate) => {
|
|
1430
|
+
if (seen.has(candidate.source)) return false;
|
|
1431
|
+
seen.add(candidate.source);
|
|
1432
|
+
return true;
|
|
1433
|
+
});
|
|
1434
|
+
const limited = limit.maxCount > 0 ? unique.slice(0, limit.maxCount) : unique;
|
|
1435
|
+
const dropped = [];
|
|
1436
|
+
const markDropped = /* @__PURE__ */ __name((source) => {
|
|
1437
|
+
if (!dropped.includes(source)) dropped.push(source);
|
|
1438
|
+
}, "markDropped");
|
|
1439
|
+
unique.slice(limited.length).forEach((candidate) => markDropped(candidate.source));
|
|
1324
1440
|
const files = [];
|
|
1325
1441
|
for (const candidate of limited) {
|
|
1326
1442
|
const uploaded = await uploadOne(ctx, client, candidate, difyUser, limit);
|
|
1327
1443
|
if (uploaded) files.push(uploaded);
|
|
1328
|
-
else
|
|
1444
|
+
else markDropped(candidate.source);
|
|
1329
1445
|
}
|
|
1330
1446
|
return { files, dropped };
|
|
1331
1447
|
}
|
|
1332
|
-
__name(
|
|
1448
|
+
__name(uploadCandidates, "uploadCandidates");
|
|
1333
1449
|
|
|
1334
1450
|
// src/requester/variables.ts
|
|
1335
1451
|
function resolveSessionField(key, fireCtx, now) {
|
|
@@ -1358,7 +1474,7 @@ function resolveSessionField(key, fireCtx, now) {
|
|
|
1358
1474
|
case "timestamp":
|
|
1359
1475
|
return now;
|
|
1360
1476
|
case "isDirect":
|
|
1361
|
-
return session?.isDirect ?? false;
|
|
1477
|
+
return session?.isDirect ?? fireCtx.isDirect ?? false;
|
|
1362
1478
|
default:
|
|
1363
1479
|
return "";
|
|
1364
1480
|
}
|
|
@@ -1395,11 +1511,55 @@ function resolveSource(source, fireCtx, history, now) {
|
|
|
1395
1511
|
}
|
|
1396
1512
|
}
|
|
1397
1513
|
__name(resolveSource, "resolveSource");
|
|
1514
|
+
var META_PLACEHOLDERS = /* @__PURE__ */ new Set(["triggerType", "ruleName", "triggerTime"]);
|
|
1515
|
+
function resolvePlaceholderValue(name2, limit, fireCtx, history, now) {
|
|
1516
|
+
if (name2 === "history") {
|
|
1517
|
+
const channelKey = channelKeyOf(fireCtx.platform, fireCtx.channelId);
|
|
1518
|
+
return history.toVariable(history.get(channelKey, limit ?? history.limit));
|
|
1519
|
+
}
|
|
1520
|
+
if (name2 === "isDirect") {
|
|
1521
|
+
return fireCtx.session?.isDirect ?? fireCtx.isDirect ?? false;
|
|
1522
|
+
}
|
|
1523
|
+
if (META_PLACEHOLDERS.has(name2)) {
|
|
1524
|
+
return String(resolveMetaField(name2, fireCtx, now) ?? "");
|
|
1525
|
+
}
|
|
1526
|
+
const value = resolveSessionField(name2, fireCtx, now);
|
|
1527
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
1528
|
+
}
|
|
1529
|
+
__name(resolvePlaceholderValue, "resolvePlaceholderValue");
|
|
1530
|
+
var BARE_PLACEHOLDER = /"\{\{\s*(\w+)(?::(\d+))?\s*\}\}"/g;
|
|
1531
|
+
var escapeJsonText = /* @__PURE__ */ __name((value) => JSON.stringify(value).slice(1, -1), "escapeJsonText");
|
|
1532
|
+
function interpolateTemplate(template, fireCtx, history, now) {
|
|
1533
|
+
const resolve = /* @__PURE__ */ __name((name2, limit) => resolvePlaceholderValue(name2, limit ? Number(limit) : void 0, fireCtx, history, now), "resolve");
|
|
1534
|
+
const native = template.replace(
|
|
1535
|
+
BARE_PLACEHOLDER,
|
|
1536
|
+
(_match, name2, limit) => JSON.stringify(resolve(name2, limit))
|
|
1537
|
+
);
|
|
1538
|
+
return native.replace(
|
|
1539
|
+
JSON_PLACEHOLDER,
|
|
1540
|
+
(_match, name2, limit) => escapeJsonText(String(resolve(name2, limit)))
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
__name(interpolateTemplate, "interpolateTemplate");
|
|
1544
|
+
function resolveJsonObject(template, fireCtx, history, now) {
|
|
1545
|
+
try {
|
|
1546
|
+
const parsed = JSON.parse(interpolateTemplate(template, fireCtx, history, now));
|
|
1547
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
1548
|
+
} catch {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
__name(resolveJsonObject, "resolveJsonObject");
|
|
1398
1553
|
function resolveVariables(rule, fireCtx, history) {
|
|
1399
1554
|
const inputs = {};
|
|
1400
1555
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1401
1556
|
for (const binding of rule.variables) {
|
|
1402
|
-
if (
|
|
1557
|
+
if (binding?.source?.type !== "json") continue;
|
|
1558
|
+
const object = resolveJsonObject(binding.source.template, fireCtx, history, now);
|
|
1559
|
+
if (object) Object.assign(inputs, object);
|
|
1560
|
+
}
|
|
1561
|
+
for (const binding of rule.variables) {
|
|
1562
|
+
if (!binding?.field || binding.source?.type === "json") continue;
|
|
1403
1563
|
const value = resolveSource(binding.source, fireCtx, history, now);
|
|
1404
1564
|
if (value !== void 0) inputs[binding.field] = value;
|
|
1405
1565
|
}
|
|
@@ -1440,7 +1600,7 @@ ${line}` : line;
|
|
|
1440
1600
|
__name(splitText, "splitText");
|
|
1441
1601
|
function renderReply(input) {
|
|
1442
1602
|
const messages = [];
|
|
1443
|
-
const segments = splitText(input.text, input.config.maxSegmentLength
|
|
1603
|
+
const segments = splitText(input.text, input.config.maxSegmentLength);
|
|
1444
1604
|
segments.forEach((segment, index) => {
|
|
1445
1605
|
const elements = [];
|
|
1446
1606
|
if (index === 0 && input.quoteMessageId) {
|
|
@@ -1496,6 +1656,7 @@ function extractTextOutputs(outputs) {
|
|
|
1496
1656
|
__name(extractTextOutputs, "extractTextOutputs");
|
|
1497
1657
|
function parseWorkflowResponse(response) {
|
|
1498
1658
|
const data = response.data;
|
|
1659
|
+
if (!data) throw new DifyApiError("workflow 响应缺少 data", 200);
|
|
1499
1660
|
if (data.error) throw new DifyApiError(data.error, 200);
|
|
1500
1661
|
if (data.status && !SUCCESS_STATUSES.includes(data.status)) {
|
|
1501
1662
|
throw new DifyApiError(`workflow ${data.status}`, 200);
|
|
@@ -1538,11 +1699,13 @@ var RequestRunner = class {
|
|
|
1538
1699
|
uploadedFiles: []
|
|
1539
1700
|
};
|
|
1540
1701
|
const upload = resolveUploadConfig(this.config);
|
|
1541
|
-
if (upload.enabled
|
|
1542
|
-
const
|
|
1702
|
+
if (upload.enabled) {
|
|
1703
|
+
const sessionCandidates = fireCtx.session ? extractFileCandidates(fireCtx.session) : [];
|
|
1704
|
+
const urlCandidates = (fireCtx.imageUrls ?? []).filter((url) => !sessionCandidates.some((candidate) => candidate.source === url)).map((url) => ({ source: url, type: "image" }));
|
|
1705
|
+
const { files, dropped } = await uploadCandidates(
|
|
1543
1706
|
this.ctx,
|
|
1544
1707
|
client,
|
|
1545
|
-
|
|
1708
|
+
[...sessionCandidates, ...urlCandidates],
|
|
1546
1709
|
requestCtx.difyUser,
|
|
1547
1710
|
{ maxCount: upload.maxCount, maxSizeMb: upload.maxMb }
|
|
1548
1711
|
);
|
|
@@ -1566,17 +1729,20 @@ var RequestRunner = class {
|
|
|
1566
1729
|
this.ctx.logger.warn("规则 %s 的请求未返回任何内容", rule.name);
|
|
1567
1730
|
return;
|
|
1568
1731
|
}
|
|
1569
|
-
const quote = resolveQuoteConfig(this.config);
|
|
1570
|
-
const elapsedSeconds = (Date.now() - startedAt) / 1e3;
|
|
1571
|
-
const shouldQuote = quote.enabled && elapsedSeconds >= quote.minElapsedSeconds && isMessageTrigger(fireCtx.triggerType);
|
|
1572
1732
|
const messages = renderReply({
|
|
1573
1733
|
text: reply.text,
|
|
1574
1734
|
files: reply.files,
|
|
1575
|
-
quoteMessageId:
|
|
1735
|
+
quoteMessageId: this.shouldQuoteReply(fireCtx, startedAt) ? fireCtx.session?.messageId : void 0,
|
|
1576
1736
|
config: this.config
|
|
1577
1737
|
});
|
|
1578
1738
|
await this.deliver(fireCtx, messages);
|
|
1579
1739
|
}
|
|
1740
|
+
/** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
|
|
1741
|
+
shouldQuoteReply(fireCtx, startedAt) {
|
|
1742
|
+
const quote = resolveQuoteConfig(this.config);
|
|
1743
|
+
if (!quote.enabled) return false;
|
|
1744
|
+
return (Date.now() - startedAt) / 1e3 >= quote.minElapsedSeconds && isMessageTrigger(fireCtx.triggerType);
|
|
1745
|
+
}
|
|
1580
1746
|
/** 按应用类型分流:workflow/completion 无会话,chat 系续接 conversation_id */
|
|
1581
1747
|
async dispatch(app, client, fireCtx, requestCtx) {
|
|
1582
1748
|
const base = {
|
|
@@ -1614,7 +1780,8 @@ var RequestRunner = class {
|
|
|
1614
1780
|
}
|
|
1615
1781
|
return;
|
|
1616
1782
|
}
|
|
1617
|
-
const
|
|
1783
|
+
const bots = Object.values(this.ctx.bots);
|
|
1784
|
+
const bot = bots.find((b) => b.platform === fireCtx.platform && b.selfId === fireCtx.selfId) ?? bots.find((b) => b.platform === fireCtx.platform);
|
|
1618
1785
|
if (!bot) {
|
|
1619
1786
|
this.ctx.logger.warn("平台 %s 上没有可用 bot,无法发送主动消息", fireCtx.platform);
|
|
1620
1787
|
return;
|
|
@@ -1636,7 +1803,7 @@ var ConversationStore = class {
|
|
|
1636
1803
|
}
|
|
1637
1804
|
/** 粒度归一:bindingKey 与 difyUser 共用,保证两者始终对齐 */
|
|
1638
1805
|
resolveScopeDimension(rule, fireCtx) {
|
|
1639
|
-
if (fireCtx
|
|
1806
|
+
if (this.isDirectOf(fireCtx) || rule.conversationScope === "per-user") return "user";
|
|
1640
1807
|
switch (rule.conversationScope) {
|
|
1641
1808
|
case "global":
|
|
1642
1809
|
return "global";
|
|
@@ -1646,13 +1813,17 @@ var ConversationStore = class {
|
|
|
1646
1813
|
return "guild";
|
|
1647
1814
|
}
|
|
1648
1815
|
}
|
|
1816
|
+
/** 私聊判定:有 session 读 session,无 session 的主动触发读池透传的标记 */
|
|
1817
|
+
isDirectOf(fireCtx) {
|
|
1818
|
+
return fireCtx.session?.isDirect ?? fireCtx.isDirect ?? false;
|
|
1819
|
+
}
|
|
1649
1820
|
/** bindingKey = 应用 ID + 粒度 + 平台身份 */
|
|
1650
1821
|
computeBindingKey(appId, rule, fireCtx) {
|
|
1651
1822
|
const base = `${appId}:${fireCtx.platform}:${fireCtx.selfId}`;
|
|
1652
1823
|
switch (this.resolveScopeDimension(rule, fireCtx)) {
|
|
1653
1824
|
// 群内按「群+用户」区分;私聊用 private 段占位
|
|
1654
1825
|
case "user":
|
|
1655
|
-
return fireCtx
|
|
1826
|
+
return this.isDirectOf(fireCtx) ? `${base}:private:${fireCtx.userId ?? fireCtx.channelId}` : `${base}:user:${fireCtx.guildId ?? fireCtx.channelId}:${fireCtx.userId ?? fireCtx.channelId}`;
|
|
1656
1827
|
case "guild":
|
|
1657
1828
|
return `${base}:guild:${fireCtx.guildId ?? fireCtx.channelId}`;
|
|
1658
1829
|
case "channel":
|
|
@@ -1669,7 +1840,7 @@ var ConversationStore = class {
|
|
|
1669
1840
|
computeDifyUser(rule, fireCtx) {
|
|
1670
1841
|
switch (this.resolveScopeDimension(rule, fireCtx)) {
|
|
1671
1842
|
case "user":
|
|
1672
|
-
return fireCtx
|
|
1843
|
+
return this.isDirectOf(fireCtx) ? `koishi:${fireCtx.platform}:private:${fireCtx.userId ?? fireCtx.channelId}` : `koishi:${fireCtx.platform}:${fireCtx.guildId ?? fireCtx.channelId}:${fireCtx.userId ?? fireCtx.channelId}`;
|
|
1673
1844
|
case "guild":
|
|
1674
1845
|
return `koishi:${fireCtx.platform}:${fireCtx.guildId ?? fireCtx.channelId}`;
|
|
1675
1846
|
case "channel":
|
|
@@ -1709,16 +1880,25 @@ var ConversationStore = class {
|
|
|
1709
1880
|
/**
|
|
1710
1881
|
* 重置匹配的绑定:本地删行 + 尽力删除 Dify 侧会话(失败仅记录)。
|
|
1711
1882
|
* 表已无 guildKey 列,按频道匹配时全表读回后解析 bindingKey 过滤(行数量级很小)。
|
|
1883
|
+
* 覆盖 guild / user(群内)/ channel 段与私聊执行时的 private 段;
|
|
1884
|
+
* global 段跨频道,不随单频道重置。
|
|
1712
1885
|
*/
|
|
1713
1886
|
async resetWhere(filter) {
|
|
1714
1887
|
const rows = await this.ctx.database.get("dify_conversation", {});
|
|
1888
|
+
const splitKey = /* @__PURE__ */ __name((key) => {
|
|
1889
|
+
const separator = key.indexOf(":");
|
|
1890
|
+
return separator < 0 ? [key, ""] : [key.slice(0, separator), key.slice(separator + 1)];
|
|
1891
|
+
}, "splitKey");
|
|
1892
|
+
const [guildPlatform, guildId] = splitKey(filter.guildKey ?? "");
|
|
1893
|
+
const [channelPlatform, channelId] = splitKey(filter.channelKey ?? "");
|
|
1715
1894
|
const matched = rows.filter((row) => {
|
|
1716
1895
|
if (filter.bindingKey) return row.bindingKey === filter.bindingKey;
|
|
1717
1896
|
const { platform, scope, ids } = this.parseBindingKey(row.bindingKey);
|
|
1718
|
-
const [guildPlatform, guildId] = (filter.guildKey ?? "").split(":");
|
|
1719
1897
|
if (platform !== guildPlatform) return false;
|
|
1720
1898
|
if (scope === "guild") return ids[0] === guildId;
|
|
1721
1899
|
if (scope === "user") return ids.length === 2 ? ids[0] === guildId : false;
|
|
1900
|
+
if (scope === "channel") return platform === channelPlatform && ids[0] === channelId;
|
|
1901
|
+
if (scope === "private") return filter.isDirect === true && ids[0] === filter.userId;
|
|
1722
1902
|
return false;
|
|
1723
1903
|
});
|
|
1724
1904
|
let reset = 0;
|
|
@@ -1761,8 +1941,12 @@ __name(defineTables, "defineTables");
|
|
|
1761
1941
|
// src/commands/misc.ts
|
|
1762
1942
|
function registerMiscCommands(parent, conversationStore) {
|
|
1763
1943
|
parent.subcommand(".conv.reset", { authority: 3 }).action(async ({ session }) => {
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1944
|
+
const reset = await conversationStore.resetWhere({
|
|
1945
|
+
guildKey: guildKeyOf(session.platform, session.guildId, session.channelId),
|
|
1946
|
+
channelKey: channelKeyOf(session.platform, session.channelId),
|
|
1947
|
+
isDirect: session.isDirect,
|
|
1948
|
+
userId: session.userId
|
|
1949
|
+
});
|
|
1766
1950
|
if (!reset) return session.text("dify-ai.commands.conv-empty");
|
|
1767
1951
|
return session.text("dify-ai.commands.conv-reset", [reset]);
|
|
1768
1952
|
});
|
|
@@ -1772,7 +1956,9 @@ __name(registerMiscCommands, "registerMiscCommands");
|
|
|
1772
1956
|
// src/index.ts
|
|
1773
1957
|
var name = "dify-ai";
|
|
1774
1958
|
var inject = { required: ["database"] };
|
|
1959
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
1775
1960
|
function apply(ctx, config) {
|
|
1961
|
+
const startedAt = Date.now();
|
|
1776
1962
|
ctx.i18n.define("zh-CN", zh_CN_default);
|
|
1777
1963
|
ctx.i18n.define("en-US", en_US_default);
|
|
1778
1964
|
defineTables(ctx);
|
|
@@ -1782,18 +1968,24 @@ function apply(ctx, config) {
|
|
|
1782
1968
|
const conversationStore = new ConversationStore(ctx, apps);
|
|
1783
1969
|
const runner = new RequestRunner(ctx, config, conversationStore, history, apps, appTypes);
|
|
1784
1970
|
const engine = new TriggerEngine(ctx, runner, history, rules, cooldown);
|
|
1785
|
-
ctx.on("ready",
|
|
1786
|
-
await probeAppTypes(ctx, config, apps, appTypes);
|
|
1971
|
+
ctx.on("ready", () => {
|
|
1787
1972
|
engine.start();
|
|
1973
|
+
ctx.logger.info("插件装配完成,耗时 %dms", Date.now() - startedAt);
|
|
1974
|
+
const probeStart = Date.now();
|
|
1975
|
+
void probeAppTypes(ctx, apps, appTypes).then(() => {
|
|
1976
|
+
ctx.logger.info("应用类型探测完成,耗时 %dms", Date.now() - probeStart);
|
|
1977
|
+
});
|
|
1788
1978
|
});
|
|
1789
1979
|
const admin = ctx.command("dify", { authority: 3 });
|
|
1790
1980
|
registerMiscCommands(admin, conversationStore);
|
|
1791
1981
|
}
|
|
1792
1982
|
__name(apply, "apply");
|
|
1793
|
-
async function probeAppTypes(ctx,
|
|
1983
|
+
async function probeAppTypes(ctx, apps, appTypes) {
|
|
1984
|
+
const autoApps = [...apps.values()].filter((app) => app.appType === "auto");
|
|
1985
|
+
if (!autoApps.length) return;
|
|
1794
1986
|
await Promise.all(
|
|
1795
|
-
|
|
1796
|
-
const client = createDifyClient(ctx, app,
|
|
1987
|
+
autoApps.map(async (app) => {
|
|
1988
|
+
const client = createDifyClient(ctx, app, PROBE_TIMEOUT_MS);
|
|
1797
1989
|
if (!client) return;
|
|
1798
1990
|
try {
|
|
1799
1991
|
const info = await client.getAppInfo();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { FireContext } from '../types';
|
|
2
2
|
import { MessageHistory } from '../history';
|
|
3
3
|
import { DifyRule } from '../types';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 按规则的变量映射表构造 Dify inputs。
|
|
6
|
+
* JSON 对象条目先注入(顶层键展开合并进 inputs),单条映射后注入覆盖同名键——
|
|
7
|
+
* 现有单条配置行为不变,JSON 作为批量默认值。
|
|
8
|
+
*/
|
|
5
9
|
export declare function resolveVariables(rule: DifyRule, fireCtx: FireContext, history: MessageHistory): Record<string, any>;
|
package/lib/types.d.ts
CHANGED
|
@@ -68,7 +68,12 @@ export type VariableSource = {
|
|
|
68
68
|
} | {
|
|
69
69
|
type: 'constant';
|
|
70
70
|
value: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'json';
|
|
73
|
+
template: string;
|
|
71
74
|
};
|
|
75
|
+
/** JSON 对象模板里的占位符:{{变量名}} 或 {{history:条数}}(加载校验与运行时插值共用) */
|
|
76
|
+
export declare const JSON_PLACEHOLDER: RegExp;
|
|
72
77
|
/** 一条变量映射:Dify inputs 字段名 ← 来源 */
|
|
73
78
|
export interface VariableBinding {
|
|
74
79
|
field: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-dify-ai",
|
|
3
3
|
"description": "Invoke Dify apps (chatflow / workflow / completion) in Koishi with triggers and conversation bindings",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"author": "PinkElysia",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/PinkElysiaDev/dify-ai-plugin-for-Koishi#readme",
|