koishi-plugin-dify-ai 0.1.4 → 0.1.6
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/api/client.d.ts +0 -1
- package/lib/config.d.ts +57 -4
- package/lib/history.d.ts +7 -1
- package/lib/index.js +381 -57
- package/lib/requester/files.d.ts +12 -2
- package/lib/requester/render.d.ts +1 -0
- package/lib/requester/runner.d.ts +2 -0
- package/lib/state/conversation.d.ts +7 -0
- package/lib/trigger/conditions.d.ts +3 -3
- package/lib/trigger/cooldown.d.ts +50 -0
- package/lib/trigger/engine.d.ts +13 -3
- package/lib/trigger/pool.d.ts +10 -2
- package/lib/types.d.ts +21 -3
- package/lib/utils/session-text.d.ts +19 -10
- package/package.json +1 -1
package/lib/api/client.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ export declare class DifyClient {
|
|
|
18
18
|
/** blocking JSON 请求(本插件只用 blocking 模式) */
|
|
19
19
|
private request;
|
|
20
20
|
getAppInfo(): Promise<DifyAppInfo>;
|
|
21
|
-
getParameters(): Promise<any>;
|
|
22
21
|
/** chat / agent-chat / advanced-chat(conversation_id 传空串表示新建会话) */
|
|
23
22
|
runChat(body: {
|
|
24
23
|
inputs: Record<string, any>;
|
package/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'koishi';
|
|
2
|
-
import { DifyRule } from './types';
|
|
2
|
+
import { CooldownSetting, DifyRule } from './types';
|
|
3
3
|
export type AppTypeSetting = 'auto' | 'chat' | 'agent-chat' | 'advanced-chat' | 'workflow' | 'completion';
|
|
4
4
|
export interface AppConfigEntry {
|
|
5
5
|
name: string;
|
|
@@ -48,6 +48,10 @@ export type VariableEntry = {
|
|
|
48
48
|
source: 'meta';
|
|
49
49
|
field: string;
|
|
50
50
|
key: string;
|
|
51
|
+
} | {
|
|
52
|
+
source: 'constant';
|
|
53
|
+
field: string;
|
|
54
|
+
value: string;
|
|
51
55
|
};
|
|
52
56
|
/** 作用域的一行:维度 + 值 */
|
|
53
57
|
export interface ScopeRow {
|
|
@@ -64,23 +68,72 @@ export interface RuleEntry {
|
|
|
64
68
|
scope: ScopeRow[];
|
|
65
69
|
variables: VariableEntry[];
|
|
66
70
|
}
|
|
71
|
+
/** 引用触发消息的条件分支形态:关闭时空分支,开启时按耗时阈值决定是否引用 */
|
|
72
|
+
export type QuoteEntry = {
|
|
73
|
+
mode: 'off';
|
|
74
|
+
} | {
|
|
75
|
+
mode: 'on';
|
|
76
|
+
minElapsedSeconds: number;
|
|
77
|
+
};
|
|
78
|
+
/** 触发后冷却的条件分支形态 */
|
|
79
|
+
export type CooldownEntry = {
|
|
80
|
+
mode: 'off';
|
|
81
|
+
} | {
|
|
82
|
+
mode: 'fixed';
|
|
83
|
+
seconds?: number;
|
|
84
|
+
} | {
|
|
85
|
+
mode: 'sliding';
|
|
86
|
+
minSeconds?: number;
|
|
87
|
+
maxSeconds?: number;
|
|
88
|
+
};
|
|
89
|
+
/** 上传消息文件的条件分支形态:关闭时空分支,开启时限制数量与单文件大小 */
|
|
90
|
+
export type UploadEntry = {
|
|
91
|
+
mode: 'off';
|
|
92
|
+
} | {
|
|
93
|
+
mode: 'on';
|
|
94
|
+
maxCount?: number;
|
|
95
|
+
maxSizeMb?: number;
|
|
96
|
+
};
|
|
67
97
|
export interface Config {
|
|
68
98
|
apps: AppConfigEntry[];
|
|
69
99
|
rules: RuleEntry[];
|
|
70
100
|
parseElements: boolean;
|
|
71
|
-
|
|
101
|
+
quote: QuoteEntry;
|
|
72
102
|
maxSegmentLength: number;
|
|
73
|
-
|
|
103
|
+
/** boolean 是 0.1.x 的遗留形态,仅迁移读取(true → 开启不限,false → 关闭) */
|
|
104
|
+
uploadFiles: UploadEntry | boolean;
|
|
105
|
+
/** 0.1.x 的布尔开关,仅用于迁移读取(true → 总是引用,false → 关闭) */
|
|
106
|
+
quoteTrigger?: boolean;
|
|
74
107
|
requestTimeout: number;
|
|
75
108
|
historyLimit: number;
|
|
76
109
|
sendErrors: boolean;
|
|
110
|
+
cooldown: CooldownEntry;
|
|
77
111
|
}
|
|
78
112
|
export declare const Config: Schema<Config>;
|
|
79
113
|
/**
|
|
80
|
-
*
|
|
114
|
+
* 上传消息文件配置(含 0.1.x 布尔遗留形态迁移):
|
|
115
|
+
* 数量与单文件大小 0 均为不限,大小仍与 Dify 各类型上限取较小值。
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveUploadConfig(config: Config): {
|
|
118
|
+
enabled: boolean;
|
|
119
|
+
maxCount: number;
|
|
120
|
+
maxMb: number;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* 引用触发消息配置(含 0.1.x 布尔开关迁移):schema 会给旧配置填默认 quote 对象,
|
|
124
|
+
* 因此迁移判据是「旧布尔被配置层透传保留 且 quote 仍是默认形态(用户未在新表单改过)」——
|
|
125
|
+
* 此时以旧布尔为准;用户改过新表单(quote 非默认)则新值优先。
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveQuoteConfig(config: Config): {
|
|
128
|
+
enabled: boolean;
|
|
129
|
+
minElapsedSeconds: number;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* 配置规范化:应用表 + 按优先级降序的规则列表 + 冷却设置。
|
|
81
133
|
* 坏条件/无名变量直接丢弃;引用不存在应用的规则保留但运行时会 warn 跳过。
|
|
82
134
|
*/
|
|
83
135
|
export declare function normalizeConfig(config: Config): {
|
|
84
136
|
apps: Map<string, AppConfigEntry>;
|
|
85
137
|
rules: DifyRule[];
|
|
138
|
+
cooldown: CooldownSetting | null;
|
|
86
139
|
};
|
package/lib/history.d.ts
CHANGED
|
@@ -11,8 +11,12 @@ export interface HistoryEntry {
|
|
|
11
11
|
userId: string;
|
|
12
12
|
username: string;
|
|
13
13
|
messageId?: string;
|
|
14
|
-
/**
|
|
14
|
+
/** 原貌(含 @bot 渲染与图片占位):统一格式行(query/历史变量)用 */
|
|
15
15
|
content: string;
|
|
16
|
+
/** 剔除 @bot 的原貌:content 变量与精简行用 */
|
|
17
|
+
contentText: string;
|
|
18
|
+
/** 图片元素 URL(按元素序,与 content 中 [image:摘要] 序一致):窗口类触发的上传依据 */
|
|
19
|
+
imageUrls?: string[];
|
|
16
20
|
timestamp: number;
|
|
17
21
|
quote?: QuotedMessage;
|
|
18
22
|
}
|
|
@@ -24,6 +28,8 @@ export interface HistoryEntry {
|
|
|
24
28
|
export declare function formatHistoryEntry(entry: HistoryEntry): string;
|
|
25
29
|
/** 多条消息逐行拼接 */
|
|
26
30
|
export declare function formatHistoryEntries(entries: HistoryEntry[]): string;
|
|
31
|
+
/** 精简行:只记消息 ID 与剔除 @bot 的原貌内容,不带时间与作者前缀——窗口触发的 content 变量用 */
|
|
32
|
+
export declare function formatHistoryBrief(entries: HistoryEntry[]): string;
|
|
27
33
|
/** 从会话消息构造记录条目(历史缓冲 / 消息池共用);assistant 行标注为机器人自己的发言 */
|
|
28
34
|
export declare function buildHistoryEntry(session: Session, role?: 'user' | 'assistant'): HistoryEntry | null;
|
|
29
35
|
/**
|
package/lib/index.js
CHANGED
|
@@ -92,6 +92,7 @@ var conditionSchema = import_koishi.Schema.union([
|
|
|
92
92
|
}).default({ type: "density", densityCount: 5, densityWindowSeconds: 60 }).description("消息密度")
|
|
93
93
|
]);
|
|
94
94
|
var sessionKeySchema = import_koishi.Schema.union([
|
|
95
|
+
import_koishi.Schema.const("none").description("无"),
|
|
95
96
|
import_koishi.Schema.const("userId").description("用户 ID"),
|
|
96
97
|
import_koishi.Schema.const("botId").description("Bot ID"),
|
|
97
98
|
import_koishi.Schema.const("guildId").description("群聊 ID"),
|
|
@@ -106,6 +107,7 @@ var sessionKeySchema = import_koishi.Schema.union([
|
|
|
106
107
|
import_koishi.Schema.const("isDirect").description("是否私聊")
|
|
107
108
|
]);
|
|
108
109
|
var metaKeySchema = import_koishi.Schema.union([
|
|
110
|
+
import_koishi.Schema.const("none").description("无"),
|
|
109
111
|
import_koishi.Schema.const("triggerType").description("触发类型"),
|
|
110
112
|
import_koishi.Schema.const("ruleName").description("规则名"),
|
|
111
113
|
import_koishi.Schema.const("triggerTime").description("触发时间")
|
|
@@ -114,8 +116,8 @@ var variableSchema = import_koishi.Schema.union([
|
|
|
114
116
|
import_koishi.Schema.object({
|
|
115
117
|
source: import_koishi.Schema.const("session").required(),
|
|
116
118
|
field: import_koishi.Schema.string().required().description("字段名"),
|
|
117
|
-
key: sessionKeySchema.default("
|
|
118
|
-
}).default({ source: "session", field: "", key: "
|
|
119
|
+
key: sessionKeySchema.default("none").description("变量")
|
|
120
|
+
}).default({ source: "session", field: "", key: "none" }).description("会话字段"),
|
|
119
121
|
import_koishi.Schema.object({
|
|
120
122
|
source: import_koishi.Schema.const("history").required(),
|
|
121
123
|
field: import_koishi.Schema.string().required().description("字段名"),
|
|
@@ -124,8 +126,46 @@ var variableSchema = import_koishi.Schema.union([
|
|
|
124
126
|
import_koishi.Schema.object({
|
|
125
127
|
source: import_koishi.Schema.const("meta").required(),
|
|
126
128
|
field: import_koishi.Schema.string().required().description("字段名"),
|
|
127
|
-
key: metaKeySchema.default("
|
|
128
|
-
}).default({ source: "meta", field: "", key: "
|
|
129
|
+
key: metaKeySchema.default("none").description("变量")
|
|
130
|
+
}).default({ source: "meta", field: "", key: "none" }).description("触发元信息"),
|
|
131
|
+
import_koishi.Schema.object({
|
|
132
|
+
source: import_koishi.Schema.const("constant").required(),
|
|
133
|
+
field: import_koishi.Schema.string().required().description("字段名"),
|
|
134
|
+
value: import_koishi.Schema.string().role("textarea").required().description("固定文本")
|
|
135
|
+
}).default({ source: "constant", field: "", value: "" }).description("常量文本")
|
|
136
|
+
]);
|
|
137
|
+
var quoteSchema = import_koishi.Schema.union([
|
|
138
|
+
import_koishi.Schema.object({
|
|
139
|
+
mode: import_koishi.Schema.const("off").required()
|
|
140
|
+
}).default({ mode: "off" }).description("关闭"),
|
|
141
|
+
import_koishi.Schema.object({
|
|
142
|
+
mode: import_koishi.Schema.const("on").required(),
|
|
143
|
+
minElapsedSeconds: import_koishi.Schema.natural().default(0).description("回复耗时多少秒才引用,0 总是引用")
|
|
144
|
+
}).default({ mode: "on", minElapsedSeconds: 0 }).description("开启")
|
|
145
|
+
]);
|
|
146
|
+
var cooldownSchema = import_koishi.Schema.union([
|
|
147
|
+
import_koishi.Schema.object({
|
|
148
|
+
mode: import_koishi.Schema.const("off").required()
|
|
149
|
+
}).default({ mode: "off" }).description("关闭"),
|
|
150
|
+
import_koishi.Schema.object({
|
|
151
|
+
mode: import_koishi.Schema.const("fixed").required(),
|
|
152
|
+
seconds: import_koishi.Schema.natural().default(30).min(1).description("冷却秒数")
|
|
153
|
+
}).default({ mode: "fixed", seconds: 30 }).description("固定窗口"),
|
|
154
|
+
import_koishi.Schema.object({
|
|
155
|
+
mode: import_koishi.Schema.const("sliding").required(),
|
|
156
|
+
minSeconds: import_koishi.Schema.natural().default(30).min(1).description("最小冷却秒数"),
|
|
157
|
+
maxSeconds: import_koishi.Schema.natural().default(120).min(1).description("最大冷却秒数")
|
|
158
|
+
}).default({ mode: "sliding", minSeconds: 30, maxSeconds: 120 }).description("滑动窗口")
|
|
159
|
+
]);
|
|
160
|
+
var uploadSchema = import_koishi.Schema.union([
|
|
161
|
+
import_koishi.Schema.object({
|
|
162
|
+
mode: import_koishi.Schema.const("off").required()
|
|
163
|
+
}).default({ mode: "off" }).description("关闭"),
|
|
164
|
+
import_koishi.Schema.object({
|
|
165
|
+
mode: import_koishi.Schema.const("on").required(),
|
|
166
|
+
maxCount: import_koishi.Schema.natural().default(0).description("数量上限,0 不限"),
|
|
167
|
+
maxSizeMb: import_koishi.Schema.natural().default(0).description("单文件大小上限 MB,0 不限")
|
|
168
|
+
}).default({ mode: "on", maxCount: 0, maxSizeMb: 0 }).description("开启")
|
|
129
169
|
]);
|
|
130
170
|
var Config = import_koishi.Schema.intersect([
|
|
131
171
|
import_koishi.Schema.object({
|
|
@@ -173,14 +213,15 @@ var Config = import_koishi.Schema.intersect([
|
|
|
173
213
|
}).description("触发规则"),
|
|
174
214
|
import_koishi.Schema.object({
|
|
175
215
|
parseElements: import_koishi.Schema.boolean().default(false).description("解析回复中的消息元素"),
|
|
176
|
-
|
|
216
|
+
quote: quoteSchema.default({ mode: "on", minElapsedSeconds: 0 }).description("引用触发消息"),
|
|
177
217
|
maxSegmentLength: import_koishi.Schema.natural().default(0).description("分段长度,0 不分段"),
|
|
178
|
-
uploadFiles:
|
|
218
|
+
uploadFiles: uploadSchema.default({ mode: "on", maxCount: 0, maxSizeMb: 0 }).description("上传消息中的文件")
|
|
179
219
|
}).description("回复行为"),
|
|
180
220
|
import_koishi.Schema.object({
|
|
181
221
|
requestTimeout: import_koishi.Schema.natural().role("ms").default(3e5).description("请求超时"),
|
|
182
222
|
historyLimit: import_koishi.Schema.natural().default(20).min(1).description("历史消息条数"),
|
|
183
|
-
sendErrors: import_koishi.Schema.boolean().default(false).description("失败时提示错误")
|
|
223
|
+
sendErrors: import_koishi.Schema.boolean().default(false).description("失败时提示错误"),
|
|
224
|
+
cooldown: cooldownSchema.default({ mode: "off" }).description("触发后冷却")
|
|
184
225
|
}).description("运行参数")
|
|
185
226
|
]);
|
|
186
227
|
var CONVERSATION_SCOPES = ["per-user", "per-guild", "per-channel", "global"];
|
|
@@ -269,17 +310,61 @@ __name(toScope, "toScope");
|
|
|
269
310
|
function toVariable(entry) {
|
|
270
311
|
const field = entry.field?.trim();
|
|
271
312
|
if (!field) return null;
|
|
313
|
+
if (entry.source === "constant") {
|
|
314
|
+
if (!entry.value) return null;
|
|
315
|
+
return { field, source: { type: "constant", value: entry.value } };
|
|
316
|
+
}
|
|
272
317
|
if (entry.source === "meta") {
|
|
318
|
+
if (entry.key === "none") return null;
|
|
273
319
|
return { field, source: { type: "meta", key: entry.key || "triggerType" } };
|
|
274
320
|
}
|
|
275
321
|
if (entry.source === "history") {
|
|
276
322
|
const limit = Number(entry.limit);
|
|
277
323
|
return { field, source: { type: "history", ...limit > 0 ? { limit } : {} } };
|
|
278
324
|
}
|
|
279
|
-
if (entry.key === "history") return null;
|
|
325
|
+
if (entry.key === "none" || entry.key === "history") return null;
|
|
280
326
|
return { field, source: { type: "session", key: entry.key || "content" } };
|
|
281
327
|
}
|
|
282
328
|
__name(toVariable, "toVariable");
|
|
329
|
+
function resolveUploadConfig(config) {
|
|
330
|
+
if (typeof config.uploadFiles === "boolean") {
|
|
331
|
+
return config.uploadFiles ? { enabled: true, maxCount: 0, maxMb: 0 } : { enabled: false, maxCount: 0, maxMb: 0 };
|
|
332
|
+
}
|
|
333
|
+
if (config.uploadFiles?.mode === "off") return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
334
|
+
return {
|
|
335
|
+
enabled: true,
|
|
336
|
+
maxCount: Math.max(0, Number(config.uploadFiles?.maxCount) || 0),
|
|
337
|
+
maxMb: Math.max(0, Number(config.uploadFiles?.maxSizeMb) || 0)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
__name(resolveUploadConfig, "resolveUploadConfig");
|
|
341
|
+
function resolveQuoteConfig(config) {
|
|
342
|
+
if (config.quote?.mode === "off") return { enabled: false, minElapsedSeconds: 0 };
|
|
343
|
+
if (config.quote?.mode === "on") {
|
|
344
|
+
return { enabled: true, minElapsedSeconds: Math.max(0, Number(config.quote.minElapsedSeconds) || 0) };
|
|
345
|
+
}
|
|
346
|
+
if (config.quoteTrigger === false) return { enabled: false, minElapsedSeconds: 0 };
|
|
347
|
+
return { enabled: true, minElapsedSeconds: 0 };
|
|
348
|
+
}
|
|
349
|
+
__name(resolveQuoteConfig, "resolveQuoteConfig");
|
|
350
|
+
function toCooldown(entry) {
|
|
351
|
+
if (entry?.mode === "fixed") {
|
|
352
|
+
const seconds = Number(entry.seconds);
|
|
353
|
+
return seconds > 0 ? { mode: "fixed", ms: seconds * 1e3 } : null;
|
|
354
|
+
}
|
|
355
|
+
if (entry?.mode === "sliding") {
|
|
356
|
+
const minSeconds = Number(entry.minSeconds);
|
|
357
|
+
const maxSeconds = Number(entry.maxSeconds);
|
|
358
|
+
if (!(minSeconds > 0) || !(maxSeconds > 0)) return null;
|
|
359
|
+
return {
|
|
360
|
+
mode: "sliding",
|
|
361
|
+
minMs: minSeconds * 1e3,
|
|
362
|
+
maxMs: Math.max(minSeconds, maxSeconds) * 1e3
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
__name(toCooldown, "toCooldown");
|
|
283
368
|
function normalizeConfig(config) {
|
|
284
369
|
const apps = /* @__PURE__ */ new Map();
|
|
285
370
|
for (const app of config.apps ?? []) {
|
|
@@ -302,7 +387,7 @@ function normalizeConfig(config) {
|
|
|
302
387
|
});
|
|
303
388
|
}
|
|
304
389
|
rules.sort((a, b) => b.priority - a.priority);
|
|
305
|
-
return { apps, rules };
|
|
390
|
+
return { apps, rules, cooldown: toCooldown(config.cooldown) };
|
|
306
391
|
}
|
|
307
392
|
__name(normalizeConfig, "normalizeConfig");
|
|
308
393
|
|
|
@@ -413,6 +498,7 @@ function createDifyClient(ctx, app, timeout) {
|
|
|
413
498
|
__name(createDifyClient, "createDifyClient");
|
|
414
499
|
|
|
415
500
|
// src/utils/session-text.ts
|
|
501
|
+
var import_crypto = require("crypto");
|
|
416
502
|
var import_koishi2 = require("koishi");
|
|
417
503
|
function parseTextSafe(content) {
|
|
418
504
|
if (!content) return "";
|
|
@@ -423,15 +509,28 @@ function parseTextSafe(content) {
|
|
|
423
509
|
}
|
|
424
510
|
}
|
|
425
511
|
__name(parseTextSafe, "parseTextSafe");
|
|
426
|
-
function extractText(session
|
|
512
|
+
function extractText(session) {
|
|
427
513
|
if (session.elements?.length) {
|
|
428
|
-
|
|
429
|
-
return import_koishi2.h.select(elements, "text").join("").trim();
|
|
514
|
+
return import_koishi2.h.select(session.elements, "text").join("").trim();
|
|
430
515
|
}
|
|
431
516
|
return parseTextSafe(session.content);
|
|
432
517
|
}
|
|
433
518
|
__name(extractText, "extractText");
|
|
434
519
|
var IMAGE_PLACEHOLDER = "[图片]";
|
|
520
|
+
function imagePlaceholder(url) {
|
|
521
|
+
if (!url) return IMAGE_PLACEHOLDER;
|
|
522
|
+
return `[image:${(0, import_crypto.createHash)("sha1").update(url).digest("hex").slice(0, 8)}]`;
|
|
523
|
+
}
|
|
524
|
+
__name(imagePlaceholder, "imagePlaceholder");
|
|
525
|
+
function annotateDroppedPlaceholders(text, droppedUrls) {
|
|
526
|
+
let result = text;
|
|
527
|
+
for (const url of droppedUrls) {
|
|
528
|
+
const placeholder = imagePlaceholder(url);
|
|
529
|
+
result = result.split(placeholder).join(`${placeholder}(已丢弃)`);
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
__name(annotateDroppedPlaceholders, "annotateDroppedPlaceholders");
|
|
435
534
|
function extractTextWithPlaceholders(session) {
|
|
436
535
|
const elements = session.elements?.length ? session.elements : (() => {
|
|
437
536
|
try {
|
|
@@ -445,8 +544,11 @@ function extractTextWithPlaceholders(session) {
|
|
|
445
544
|
if (element.type === "text") {
|
|
446
545
|
const text = element.attrs?.["content"] ?? "";
|
|
447
546
|
if (text) parts.push(text);
|
|
547
|
+
} else if (element.type === "at") {
|
|
548
|
+
const name2 = element.attrs?.["name"] ?? element.attrs?.["id"];
|
|
549
|
+
if (name2) parts.push(`@${name2}`);
|
|
448
550
|
} else if (element.type === "img" || element.type === "image") {
|
|
449
|
-
parts.push(
|
|
551
|
+
parts.push(imagePlaceholder(element.attrs?.["url"] ?? element.attrs?.["src"]));
|
|
450
552
|
}
|
|
451
553
|
}
|
|
452
554
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
@@ -472,6 +574,10 @@ function formatHistoryEntries(entries) {
|
|
|
472
574
|
return entries.map(formatHistoryEntry).join("\n");
|
|
473
575
|
}
|
|
474
576
|
__name(formatHistoryEntries, "formatHistoryEntries");
|
|
577
|
+
function formatHistoryBrief(entries) {
|
|
578
|
+
return entries.map((entry) => entry.messageId ? `[msg:${entry.messageId}] ${entry.content}` : entry.content).join("\n");
|
|
579
|
+
}
|
|
580
|
+
__name(formatHistoryBrief, "formatHistoryBrief");
|
|
475
581
|
function buildQuote(session) {
|
|
476
582
|
const quote = session.quote;
|
|
477
583
|
if (!quote?.user?.id) return void 0;
|
|
@@ -531,38 +637,39 @@ function hasAt(session, id) {
|
|
|
531
637
|
__name(hasAt, "hasAt");
|
|
532
638
|
function evaluateCondition(condition, session) {
|
|
533
639
|
const text = extractText(session);
|
|
640
|
+
const verbatim = extractTextWithPlaceholders(session);
|
|
534
641
|
switch (condition.type) {
|
|
535
642
|
case "keyword-prefix": {
|
|
536
643
|
const keyword = condition.keywords.find((k) => k && text.startsWith(k));
|
|
537
644
|
if (!keyword) return null;
|
|
538
645
|
const rest = text.slice(keyword.length).trim();
|
|
539
|
-
return { type: "keyword-prefix", queryText: rest ||
|
|
646
|
+
return { type: "keyword-prefix", queryText: rest || verbatim };
|
|
540
647
|
}
|
|
541
648
|
case "keyword-contains": {
|
|
542
649
|
const keyword = condition.keywords.find((k) => k && text.includes(k));
|
|
543
650
|
if (!keyword) return null;
|
|
544
|
-
return { type: "keyword-contains", queryText:
|
|
651
|
+
return { type: "keyword-contains", queryText: verbatim };
|
|
545
652
|
}
|
|
546
653
|
case "at-bot": {
|
|
547
654
|
const botId = session.bot?.userId;
|
|
548
655
|
if (!botId) return null;
|
|
549
656
|
if (!session.stripped?.atSelf && !hasAt(session, botId)) return null;
|
|
550
|
-
return { type: "at-bot", queryText:
|
|
657
|
+
return { type: "at-bot", queryText: verbatim };
|
|
551
658
|
}
|
|
552
659
|
case "at-user": {
|
|
553
660
|
const isAtAnyUser = condition.userIds.some((id) => hasAt(session, id));
|
|
554
661
|
if (!isAtAnyUser) return null;
|
|
555
|
-
return { type: "at-user", queryText:
|
|
662
|
+
return { type: "at-user", queryText: verbatim };
|
|
556
663
|
}
|
|
557
664
|
case "quote-bot": {
|
|
558
665
|
const botId = session.bot?.userId;
|
|
559
666
|
if (!botId || session.quote?.user?.id !== botId) return null;
|
|
560
|
-
return { type: "quote-bot", queryText:
|
|
667
|
+
return { type: "quote-bot", queryText: verbatim };
|
|
561
668
|
}
|
|
562
669
|
case "quote-user": {
|
|
563
670
|
const author = session.quote?.user?.id;
|
|
564
671
|
if (!author || !condition.userIds.includes(author)) return null;
|
|
565
|
-
return { type: "quote-user", queryText:
|
|
672
|
+
return { type: "quote-user", queryText: verbatim };
|
|
566
673
|
}
|
|
567
674
|
default:
|
|
568
675
|
return null;
|
|
@@ -597,7 +704,7 @@ function guildKeyOf(platform, guildId, channelId) {
|
|
|
597
704
|
__name(guildKeyOf, "guildKeyOf");
|
|
598
705
|
|
|
599
706
|
// src/trigger/pool.ts
|
|
600
|
-
var POOL_CAPACITY =
|
|
707
|
+
var POOL_CAPACITY = 1024;
|
|
601
708
|
function scopeSessionOf(pool, entry) {
|
|
602
709
|
return {
|
|
603
710
|
platform: pool.platform,
|
|
@@ -637,30 +744,44 @@ var MessagePool = class {
|
|
|
637
744
|
pool.entries.splice(0, pool.entries.length - POOL_CAPACITY);
|
|
638
745
|
}
|
|
639
746
|
}
|
|
747
|
+
/** 时效内且通过规则作用域的真人条数(bot 行不推高密度,防自激励) */
|
|
748
|
+
countQualified(pool, rule, condition, now) {
|
|
749
|
+
const windowMs = condition.densityWindowSeconds * 1e3;
|
|
750
|
+
return pool.entries.filter(
|
|
751
|
+
(item) => item.role === "user" && now - item.timestamp <= windowMs && matchScope(rule.scope, scopeSessionOf(pool, item))
|
|
752
|
+
).length;
|
|
753
|
+
}
|
|
640
754
|
/**
|
|
641
755
|
* 新消息入池后的密度评估:时效内(相对触发消息)且通过规则作用域的真人条数达到阈值。
|
|
642
756
|
* 触发即取走并清空该频道全池(bot 行不推高密度,防自激励);池清空后其余规则无从触发。
|
|
757
|
+
* skip 返回 true 的条件(冷却中的密度条件)不参与判定。
|
|
643
758
|
*/
|
|
644
|
-
checkDensity(rules, session) {
|
|
759
|
+
checkDensity(rules, session, skip) {
|
|
645
760
|
const key = channelKeyOf(session.platform, session.channelId);
|
|
646
761
|
const pool = this.channels.get(key);
|
|
647
762
|
if (!pool) return [];
|
|
648
763
|
const now = session.timestamp ?? Date.now();
|
|
649
764
|
for (const rule of rules) {
|
|
650
|
-
for (
|
|
765
|
+
for (let index = 0; index < rule.conditions.length; index++) {
|
|
766
|
+
const condition = rule.conditions[index];
|
|
651
767
|
if (condition.type !== "density") continue;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
(item) => item.role === "user" && now - item.timestamp <= windowMs && matchScope(rule.scope, scopeSessionOf(pool, item))
|
|
655
|
-
).length;
|
|
656
|
-
if (count >= condition.densityCount) {
|
|
768
|
+
if (skip?.(rule, condition, index)) continue;
|
|
769
|
+
if (this.countQualified(pool, rule, condition, now) >= condition.densityCount) {
|
|
657
770
|
this.channels.delete(key);
|
|
658
|
-
return [{ rule, entries: pool.entries }];
|
|
771
|
+
return [{ rule, condition, conditionIndex: index, entries: pool.entries }];
|
|
659
772
|
}
|
|
660
773
|
}
|
|
661
774
|
}
|
|
662
775
|
return [];
|
|
663
776
|
}
|
|
777
|
+
/** 冷却到期复查:按条件参数统计该频道池(now 为复查时刻),达标即取走整池,未达标返回 null */
|
|
778
|
+
takeDensityChannel(rule, condition, channelKey, now) {
|
|
779
|
+
const pool = this.channels.get(channelKey);
|
|
780
|
+
if (!pool) return null;
|
|
781
|
+
if (this.countQualified(pool, rule, condition, now) < condition.densityCount) return null;
|
|
782
|
+
this.channels.delete(channelKey);
|
|
783
|
+
return pool;
|
|
784
|
+
}
|
|
664
785
|
/** 定时到点:取走该规则作用域内有消息的所有频道(整池清空,含未过作用域的条目) */
|
|
665
786
|
takeSchedule(rule) {
|
|
666
787
|
const taken = [];
|
|
@@ -741,25 +862,128 @@ var RuleScheduler = class {
|
|
|
741
862
|
}
|
|
742
863
|
};
|
|
743
864
|
|
|
865
|
+
// src/trigger/cooldown.ts
|
|
866
|
+
var BUFFER_CAPACITY = 1024;
|
|
867
|
+
function densityLane(index) {
|
|
868
|
+
return `density:${index}`;
|
|
869
|
+
}
|
|
870
|
+
__name(densityLane, "densityLane");
|
|
871
|
+
function laneKeyOf(ruleName, lane) {
|
|
872
|
+
return `${ruleName}
|
|
873
|
+
${lane}`;
|
|
874
|
+
}
|
|
875
|
+
__name(laneKeyOf, "laneKeyOf");
|
|
876
|
+
var TriggerCooldown = class {
|
|
877
|
+
constructor(setting) {
|
|
878
|
+
this.setting = setting;
|
|
879
|
+
}
|
|
880
|
+
static {
|
|
881
|
+
__name(this, "TriggerCooldown");
|
|
882
|
+
}
|
|
883
|
+
/** channelKey → (规则名+通道 → 冷却态) */
|
|
884
|
+
channels = /* @__PURE__ */ new Map();
|
|
885
|
+
get enabled() {
|
|
886
|
+
return this.setting != null;
|
|
887
|
+
}
|
|
888
|
+
/** 触发发起时登记冷却 */
|
|
889
|
+
begin(channelKey, ruleName, lane, now) {
|
|
890
|
+
if (!this.setting) return;
|
|
891
|
+
const ms = this.setting.mode === "fixed" ? this.setting.ms : this.setting.minMs;
|
|
892
|
+
this.lanesOf(channelKey).set(laneKeyOf(ruleName, lane), {
|
|
893
|
+
fireAt: now,
|
|
894
|
+
deadline: now + ms,
|
|
895
|
+
buffer: []
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
isCooling(channelKey, ruleName, lane, now) {
|
|
899
|
+
const state = this.lanesOf(channelKey).get(laneKeyOf(ruleName, lane));
|
|
900
|
+
return state != null && now < state.deadline;
|
|
901
|
+
}
|
|
902
|
+
/** 滑动窗口顺延:截止时间顺延到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
|
|
903
|
+
extend(channelKey, ruleName, lane, now) {
|
|
904
|
+
if (this.setting?.mode !== "sliding") return;
|
|
905
|
+
const state = this.lanesOf(channelKey).get(laneKeyOf(ruleName, lane));
|
|
906
|
+
if (!state) return;
|
|
907
|
+
state.deadline = Math.min(
|
|
908
|
+
Math.max(state.deadline, now + this.setting.minMs),
|
|
909
|
+
state.fireAt + this.setting.maxMs
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动) */
|
|
913
|
+
extendDensityLanes(channelKey, now) {
|
|
914
|
+
if (this.setting?.mode !== "sliding") return;
|
|
915
|
+
const lanes = this.channels.get(channelKey);
|
|
916
|
+
if (!lanes) return;
|
|
917
|
+
for (const [key, state] of lanes) {
|
|
918
|
+
if (!key.includes("\ndensity:")) continue;
|
|
919
|
+
state.deadline = Math.min(
|
|
920
|
+
Math.max(state.deadline, now + this.setting.minMs),
|
|
921
|
+
state.fireAt + this.setting.maxMs
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
926
|
+
bufferMessage(channelKey, ruleName, message) {
|
|
927
|
+
const state = this.lanesOf(channelKey).get(laneKeyOf(ruleName, "message"));
|
|
928
|
+
if (!state) return;
|
|
929
|
+
state.buffer.push(message);
|
|
930
|
+
if (state.buffer.length > BUFFER_CAPACITY) {
|
|
931
|
+
state.buffer.splice(0, state.buffer.length - BUFFER_CAPACITY);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/** 到期扫描:取走全部到期通道(连缓冲一并交出),由调用方决定是否重新登记 */
|
|
935
|
+
collectExpired(now) {
|
|
936
|
+
const expired = [];
|
|
937
|
+
for (const [channelKey, lanes] of this.channels) {
|
|
938
|
+
for (const [key, state] of lanes) {
|
|
939
|
+
if (now < state.deadline) continue;
|
|
940
|
+
const separator = key.indexOf("\n");
|
|
941
|
+
lanes.delete(key);
|
|
942
|
+
expired.push({
|
|
943
|
+
channelKey,
|
|
944
|
+
ruleName: key.slice(0, separator),
|
|
945
|
+
lane: key.slice(separator + 1),
|
|
946
|
+
buffer: state.buffer
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (!lanes.size) this.channels.delete(channelKey);
|
|
950
|
+
}
|
|
951
|
+
return expired;
|
|
952
|
+
}
|
|
953
|
+
lanesOf(channelKey) {
|
|
954
|
+
let lanes = this.channels.get(channelKey);
|
|
955
|
+
if (!lanes) {
|
|
956
|
+
lanes = /* @__PURE__ */ new Map();
|
|
957
|
+
this.channels.set(channelKey, lanes);
|
|
958
|
+
}
|
|
959
|
+
return lanes;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
744
963
|
// src/trigger/engine.ts
|
|
745
964
|
var SCHEDULER_TICK_MS = 1e3;
|
|
746
965
|
var TriggerEngine = class {
|
|
747
|
-
constructor(ctx, runner, history, rules) {
|
|
966
|
+
constructor(ctx, runner, history, rules, cooldown) {
|
|
748
967
|
this.ctx = ctx;
|
|
749
968
|
this.runner = runner;
|
|
750
969
|
this.history = history;
|
|
751
970
|
this.rules = rules;
|
|
752
971
|
this.scheduler = new RuleScheduler(ctx, (rule) => this.fireSchedule(rule));
|
|
753
972
|
this.pool = new MessagePool();
|
|
973
|
+
this.cooldown = new TriggerCooldown(cooldown);
|
|
754
974
|
}
|
|
755
975
|
static {
|
|
756
976
|
__name(this, "TriggerEngine");
|
|
757
977
|
}
|
|
758
978
|
scheduler;
|
|
759
979
|
pool;
|
|
980
|
+
cooldown;
|
|
760
981
|
start() {
|
|
761
982
|
this.ctx.middleware((session, next) => this.handleMessage(session, next));
|
|
762
|
-
this.ctx.setInterval(() =>
|
|
983
|
+
this.ctx.setInterval(() => {
|
|
984
|
+
this.scheduler.tick();
|
|
985
|
+
this.tickCooldown();
|
|
986
|
+
}, SCHEDULER_TICK_MS);
|
|
763
987
|
this.ctx.on("send", (session) => {
|
|
764
988
|
const entry = session.channelId ? buildHistoryEntry(session, "assistant") : null;
|
|
765
989
|
if (!entry) return;
|
|
@@ -773,17 +997,33 @@ var TriggerEngine = class {
|
|
|
773
997
|
handleMessage(session, next) {
|
|
774
998
|
if (this.ctx.bots[session.uid]) return next();
|
|
775
999
|
if (!session.channelId) return next();
|
|
1000
|
+
const channelKey = channelKeyOf(session.platform, session.channelId);
|
|
776
1001
|
const entry = buildHistoryEntry(session);
|
|
777
1002
|
if (entry) {
|
|
778
|
-
this.history.recordEntry(
|
|
1003
|
+
this.history.recordEntry(channelKey, entry);
|
|
779
1004
|
this.pool.record(session, entry);
|
|
780
|
-
|
|
781
|
-
|
|
1005
|
+
const now = session.timestamp ?? Date.now();
|
|
1006
|
+
this.cooldown.extendDensityLanes(channelKey, now);
|
|
1007
|
+
for (const fire of this.pool.checkDensity(
|
|
1008
|
+
this.rules,
|
|
1009
|
+
session,
|
|
1010
|
+
(rule, _condition, index) => this.cooldown.isCooling(channelKey, rule.name, densityLane(index), now)
|
|
1011
|
+
)) {
|
|
1012
|
+
this.fireDensity(fire, session);
|
|
782
1013
|
}
|
|
783
1014
|
}
|
|
784
1015
|
return next(async (nextMiddleware) => {
|
|
785
1016
|
const matched = this.matchRule(session);
|
|
786
1017
|
if (!matched) return nextMiddleware();
|
|
1018
|
+
if (this.cooldown.isCooling(channelKey, matched.rule.name, "message", Date.now())) {
|
|
1019
|
+
this.cooldown.bufferMessage(channelKey, matched.rule.name, {
|
|
1020
|
+
session,
|
|
1021
|
+
entry,
|
|
1022
|
+
contentText: matched.condition.queryText
|
|
1023
|
+
});
|
|
1024
|
+
this.cooldown.extend(channelKey, matched.rule.name, "message", Date.now());
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
787
1027
|
this.fireMessage(matched.rule, session, matched.condition, entry);
|
|
788
1028
|
});
|
|
789
1029
|
}
|
|
@@ -802,7 +1042,12 @@ var TriggerEngine = class {
|
|
|
802
1042
|
run(fireCtx) {
|
|
803
1043
|
void this.runner.run(fireCtx).catch((error) => this.ctx.logger.warn(error));
|
|
804
1044
|
}
|
|
1045
|
+
/** 登记冷却(定时触发不调用;冷却关闭时零开销直退) */
|
|
1046
|
+
beginCooldown(channelKey, ruleName, lane) {
|
|
1047
|
+
this.cooldown.begin(channelKey, ruleName, lane, Date.now());
|
|
1048
|
+
}
|
|
805
1049
|
fireMessage(rule, session, condition, entry) {
|
|
1050
|
+
this.beginCooldown(channelKeyOf(session.platform, session.channelId), rule.name, "message");
|
|
806
1051
|
this.run({
|
|
807
1052
|
session,
|
|
808
1053
|
platform: session.platform,
|
|
@@ -817,8 +1062,8 @@ var TriggerEngine = class {
|
|
|
817
1062
|
contentText: condition.queryText
|
|
818
1063
|
});
|
|
819
1064
|
}
|
|
820
|
-
fireDensity(
|
|
821
|
-
|
|
1065
|
+
fireDensity(fire, session) {
|
|
1066
|
+
this.beginCooldown(channelKeyOf(session.platform, session.channelId), fire.rule.name, densityLane(fire.conditionIndex));
|
|
822
1067
|
this.run({
|
|
823
1068
|
session,
|
|
824
1069
|
platform: session.platform,
|
|
@@ -827,18 +1072,50 @@ var TriggerEngine = class {
|
|
|
827
1072
|
guildId: session.guildId ?? session.channelId,
|
|
828
1073
|
userId: session.userId,
|
|
829
1074
|
triggerType: "density",
|
|
1075
|
+
rule: fire.rule,
|
|
1076
|
+
queryText: formatHistoryEntries(fire.entries),
|
|
1077
|
+
contentText: formatHistoryBrief(fire.entries)
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
|
|
1081
|
+
fireMerged(rule, buffer) {
|
|
1082
|
+
const first = buffer[0];
|
|
1083
|
+
const queryText = buffer.map((item) => item.entry ? formatHistoryEntry(item.entry) : "").filter(Boolean).join("\n");
|
|
1084
|
+
const contentText = buffer.map((item) => item.contentText).filter(Boolean).join("\n");
|
|
1085
|
+
this.beginCooldown(channelKeyOf(first.session.platform, first.session.channelId), rule.name, "message");
|
|
1086
|
+
this.run({
|
|
1087
|
+
session: first.session,
|
|
1088
|
+
platform: first.session.platform,
|
|
1089
|
+
selfId: first.session.selfId,
|
|
1090
|
+
channelId: first.session.channelId,
|
|
1091
|
+
guildId: first.session.guildId ?? first.session.channelId,
|
|
1092
|
+
userId: first.session.userId,
|
|
1093
|
+
triggerType: "merged",
|
|
830
1094
|
rule,
|
|
831
|
-
queryText
|
|
832
|
-
contentText
|
|
1095
|
+
queryText,
|
|
1096
|
+
contentText
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
/** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
|
|
1100
|
+
fireDensityExpiry(rule, conditionIndex, channel) {
|
|
1101
|
+
this.beginCooldown(channelKeyOf(channel.platform, channel.channelId), rule.name, densityLane(conditionIndex));
|
|
1102
|
+
this.run({
|
|
1103
|
+
platform: channel.platform,
|
|
1104
|
+
selfId: channel.selfId,
|
|
1105
|
+
channelId: channel.channelId,
|
|
1106
|
+
guildId: channel.guildId,
|
|
1107
|
+
triggerType: "density",
|
|
1108
|
+
rule,
|
|
1109
|
+
queryText: formatHistoryEntries(channel.entries),
|
|
1110
|
+
contentText: formatHistoryBrief(channel.entries)
|
|
833
1111
|
});
|
|
834
1112
|
}
|
|
835
1113
|
/**
|
|
836
1114
|
* 定时触发(消息摘要):把消息池内该规则作用域有消息的每个频道各发一次请求(取走即清空全池)。
|
|
837
|
-
*
|
|
1115
|
+
* 频道身份取自池内最近一条消息;无消息的频道自然跳过。定时不受冷却影响。
|
|
838
1116
|
*/
|
|
839
1117
|
fireSchedule(rule) {
|
|
840
1118
|
for (const channel of this.pool.takeSchedule(rule)) {
|
|
841
|
-
const windowText = formatHistoryEntries(channel.entries);
|
|
842
1119
|
this.run({
|
|
843
1120
|
platform: channel.platform,
|
|
844
1121
|
selfId: channel.selfId,
|
|
@@ -846,13 +1123,40 @@ var TriggerEngine = class {
|
|
|
846
1123
|
guildId: channel.guildId,
|
|
847
1124
|
triggerType: "schedule",
|
|
848
1125
|
rule,
|
|
849
|
-
queryText:
|
|
850
|
-
contentText:
|
|
1126
|
+
queryText: formatHistoryEntries(channel.entries),
|
|
1127
|
+
contentText: formatHistoryBrief(channel.entries)
|
|
851
1128
|
});
|
|
852
1129
|
}
|
|
853
1130
|
}
|
|
1131
|
+
/** 每秒扫描冷却到期:消息通道合并触发,密度通道复查池 */
|
|
1132
|
+
tickCooldown() {
|
|
1133
|
+
if (!this.cooldown.enabled) return;
|
|
1134
|
+
const now = Date.now();
|
|
1135
|
+
for (const expired of this.cooldown.collectExpired(now)) {
|
|
1136
|
+
this.onCooldownExpired(expired, now);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
onCooldownExpired(expired, now) {
|
|
1140
|
+
const rule = this.rules.find((item) => item.name === expired.ruleName);
|
|
1141
|
+
if (!rule) return;
|
|
1142
|
+
if (expired.lane === "message") {
|
|
1143
|
+
if (expired.buffer.length) this.fireMerged(rule, expired.buffer);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const index = Number(expired.lane.slice("density:".length));
|
|
1147
|
+
const condition = rule.conditions[index];
|
|
1148
|
+
if (condition?.type !== "density") return;
|
|
1149
|
+
const channel = this.pool.takeDensityChannel(rule, condition, expired.channelKey, now);
|
|
1150
|
+
if (channel) this.fireDensityExpiry(rule, index, channel);
|
|
1151
|
+
}
|
|
854
1152
|
};
|
|
855
1153
|
|
|
1154
|
+
// src/types.ts
|
|
1155
|
+
function isMessageTrigger(type) {
|
|
1156
|
+
return type !== "schedule" && type !== "density";
|
|
1157
|
+
}
|
|
1158
|
+
__name(isMessageTrigger, "isMessageTrigger");
|
|
1159
|
+
|
|
856
1160
|
// src/requester/files.ts
|
|
857
1161
|
var import_promises = require("fs/promises");
|
|
858
1162
|
var import_fs = require("fs");
|
|
@@ -986,19 +1290,19 @@ function resolveFileType(mimeType, elementType) {
|
|
|
986
1290
|
return mime === "custom" && elementType !== "custom" ? elementType : mime;
|
|
987
1291
|
}
|
|
988
1292
|
__name(resolveFileType, "resolveFileType");
|
|
989
|
-
async function uploadOne(ctx, client, candidate, difyUser) {
|
|
1293
|
+
async function uploadOne(ctx, client, candidate, difyUser, limit) {
|
|
990
1294
|
try {
|
|
991
1295
|
const payload = await resolveFilePayload(ctx, candidate);
|
|
992
1296
|
if (!payload) return null;
|
|
993
1297
|
const type = resolveFileType(payload.mimeType, candidate.type);
|
|
994
|
-
const
|
|
995
|
-
|
|
1298
|
+
const maxBytes = limit.maxSizeMb > 0 ? limit.maxSizeMb * MB : Infinity;
|
|
1299
|
+
const sizeLimit = Math.min(SIZE_LIMITS[type], maxBytes);
|
|
1300
|
+
if (payload.buffer.length > sizeLimit) {
|
|
996
1301
|
ctx.logger.warn(
|
|
997
|
-
"文件 %s (%dMB)
|
|
1302
|
+
"文件 %s (%dMB) 超过上限 %dMB,跳过",
|
|
998
1303
|
payload.fileName,
|
|
999
1304
|
Math.round(payload.buffer.length / MB),
|
|
1000
|
-
|
|
1001
|
-
Math.round(limit / MB)
|
|
1305
|
+
Math.round(sizeLimit / MB)
|
|
1002
1306
|
);
|
|
1003
1307
|
return null;
|
|
1004
1308
|
}
|
|
@@ -1013,14 +1317,17 @@ async function uploadOne(ctx, client, candidate, difyUser) {
|
|
|
1013
1317
|
}
|
|
1014
1318
|
}
|
|
1015
1319
|
__name(uploadOne, "uploadOne");
|
|
1016
|
-
async function uploadMessageFiles(ctx, client, session, difyUser) {
|
|
1320
|
+
async function uploadMessageFiles(ctx, client, session, difyUser, limit) {
|
|
1017
1321
|
const candidates = extractFileCandidates(session);
|
|
1322
|
+
const limited = limit.maxCount > 0 ? candidates.slice(0, limit.maxCount) : candidates;
|
|
1323
|
+
const dropped = candidates.slice(limited.length).map((candidate) => candidate.source);
|
|
1018
1324
|
const files = [];
|
|
1019
|
-
for (const candidate of
|
|
1020
|
-
const uploaded = await uploadOne(ctx, client, candidate, difyUser);
|
|
1325
|
+
for (const candidate of limited) {
|
|
1326
|
+
const uploaded = await uploadOne(ctx, client, candidate, difyUser, limit);
|
|
1021
1327
|
if (uploaded) files.push(uploaded);
|
|
1328
|
+
else dropped.push(candidate.source);
|
|
1022
1329
|
}
|
|
1023
|
-
return files;
|
|
1330
|
+
return { files, dropped };
|
|
1024
1331
|
}
|
|
1025
1332
|
__name(uploadMessageFiles, "uploadMessageFiles");
|
|
1026
1333
|
|
|
@@ -1076,6 +1383,8 @@ function resolveSource(source, fireCtx, history, now) {
|
|
|
1076
1383
|
return resolveSessionField(source.key, fireCtx, now);
|
|
1077
1384
|
case "meta":
|
|
1078
1385
|
return resolveMetaField(source.key, fireCtx, now);
|
|
1386
|
+
case "constant":
|
|
1387
|
+
return source.value;
|
|
1079
1388
|
case "history": {
|
|
1080
1389
|
const channelKey = channelKeyOf(fireCtx.platform, fireCtx.channelId);
|
|
1081
1390
|
const limit = source.limit ?? history.limit;
|
|
@@ -1134,7 +1443,7 @@ function renderReply(input) {
|
|
|
1134
1443
|
const segments = splitText(input.text, input.config.maxSegmentLength ?? 0);
|
|
1135
1444
|
segments.forEach((segment, index) => {
|
|
1136
1445
|
const elements = [];
|
|
1137
|
-
if (index === 0 && input.
|
|
1446
|
+
if (index === 0 && input.quoteMessageId) {
|
|
1138
1447
|
elements.push(import_koishi3.h.quote(input.quoteMessageId));
|
|
1139
1448
|
}
|
|
1140
1449
|
if (input.config.parseElements) {
|
|
@@ -1213,6 +1522,7 @@ var RequestRunner = class {
|
|
|
1213
1522
|
return app.appType !== "auto" ? app.appType : this.appTypes.get(app.name) ?? FALLBACK_APP_TYPE;
|
|
1214
1523
|
}
|
|
1215
1524
|
async run(fireCtx) {
|
|
1525
|
+
const startedAt = Date.now();
|
|
1216
1526
|
const { rule } = fireCtx;
|
|
1217
1527
|
const app = this.apps.get(rule.app);
|
|
1218
1528
|
if (!app) {
|
|
@@ -1227,8 +1537,19 @@ var RequestRunner = class {
|
|
|
1227
1537
|
inputs: resolveVariables(rule, fireCtx, this.history),
|
|
1228
1538
|
uploadedFiles: []
|
|
1229
1539
|
};
|
|
1230
|
-
|
|
1231
|
-
|
|
1540
|
+
const upload = resolveUploadConfig(this.config);
|
|
1541
|
+
if (upload.enabled && fireCtx.session) {
|
|
1542
|
+
const { files, dropped } = await uploadMessageFiles(
|
|
1543
|
+
this.ctx,
|
|
1544
|
+
client,
|
|
1545
|
+
fireCtx.session,
|
|
1546
|
+
requestCtx.difyUser,
|
|
1547
|
+
{ maxCount: upload.maxCount, maxSizeMb: upload.maxMb }
|
|
1548
|
+
);
|
|
1549
|
+
requestCtx.uploadedFiles = files;
|
|
1550
|
+
if (dropped.length) {
|
|
1551
|
+
fireCtx.queryText = annotateDroppedPlaceholders(fireCtx.queryText, dropped);
|
|
1552
|
+
}
|
|
1232
1553
|
}
|
|
1233
1554
|
let reply;
|
|
1234
1555
|
try {
|
|
@@ -1245,10 +1566,13 @@ var RequestRunner = class {
|
|
|
1245
1566
|
this.ctx.logger.warn("规则 %s 的请求未返回任何内容", rule.name);
|
|
1246
1567
|
return;
|
|
1247
1568
|
}
|
|
1569
|
+
const quote = resolveQuoteConfig(this.config);
|
|
1570
|
+
const elapsedSeconds = (Date.now() - startedAt) / 1e3;
|
|
1571
|
+
const shouldQuote = quote.enabled && elapsedSeconds >= quote.minElapsedSeconds && isMessageTrigger(fireCtx.triggerType);
|
|
1248
1572
|
const messages = renderReply({
|
|
1249
1573
|
text: reply.text,
|
|
1250
1574
|
files: reply.files,
|
|
1251
|
-
quoteMessageId: fireCtx.session?.messageId,
|
|
1575
|
+
quoteMessageId: shouldQuote ? fireCtx.session?.messageId : void 0,
|
|
1252
1576
|
config: this.config
|
|
1253
1577
|
});
|
|
1254
1578
|
await this.deliver(fireCtx, messages);
|
|
@@ -1452,12 +1776,12 @@ function apply(ctx, config) {
|
|
|
1452
1776
|
ctx.i18n.define("zh-CN", zh_CN_default);
|
|
1453
1777
|
ctx.i18n.define("en-US", en_US_default);
|
|
1454
1778
|
defineTables(ctx);
|
|
1455
|
-
const { apps, rules } = normalizeConfig(config);
|
|
1779
|
+
const { apps, rules, cooldown } = normalizeConfig(config);
|
|
1456
1780
|
const appTypes = /* @__PURE__ */ new Map();
|
|
1457
1781
|
const history = new MessageHistory(config.historyLimit);
|
|
1458
1782
|
const conversationStore = new ConversationStore(ctx, apps);
|
|
1459
1783
|
const runner = new RequestRunner(ctx, config, conversationStore, history, apps, appTypes);
|
|
1460
|
-
const engine = new TriggerEngine(ctx, runner, history, rules);
|
|
1784
|
+
const engine = new TriggerEngine(ctx, runner, history, rules, cooldown);
|
|
1461
1785
|
ctx.on("ready", async () => {
|
|
1462
1786
|
await probeAppTypes(ctx, config, apps, appTypes);
|
|
1463
1787
|
engine.start();
|
package/lib/requester/files.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export declare function mimeToFileType(mimeType?: string): DifyFileType;
|
|
|
17
17
|
export declare function extractFileCandidates(session: Session): FileCandidate[];
|
|
18
18
|
/** 三源归一:data URL / 远程 URL / 本地路径 → {buffer, fileName, mimeType}(冒烟测试直测) */
|
|
19
19
|
export declare function resolveFilePayload(ctx: Context, candidate: FileCandidate): Promise<FilePayload | null>;
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
20
|
+
/** 上传数量与单文件大小限制(0 不限;大小与 Dify 各类型上限取较小值) */
|
|
21
|
+
export interface UploadLimit {
|
|
22
|
+
maxCount: number;
|
|
23
|
+
maxSizeMb: number;
|
|
24
|
+
}
|
|
25
|
+
/** 一次上传的产物:成功的 files 载荷 + 被丢弃候选的来源 URL(截断/超限/失败统一记录,供占位符注记) */
|
|
26
|
+
export interface UploadResult {
|
|
27
|
+
files: DifyInputFileObject[];
|
|
28
|
+
dropped: string[];
|
|
29
|
+
}
|
|
30
|
+
/** 上传一批候选(入口按 type:source 去重,再按数量上限截断、逐个上传) */
|
|
31
|
+
export declare function uploadCandidates(ctx: Context, client: DifyClient, candidates: FileCandidate[], difyUser: string, limit: UploadLimit): Promise<UploadResult>;
|
|
22
32
|
export {};
|
|
@@ -28,6 +28,8 @@ export declare class RequestRunner {
|
|
|
28
28
|
/** 应用类型:schema 显式指定优先,auto 时用探测结果(未探测到回退 chat) */
|
|
29
29
|
private appTypeOf;
|
|
30
30
|
run(fireCtx: FireContext): Promise<void>;
|
|
31
|
+
/** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
|
|
32
|
+
private shouldQuoteReply;
|
|
31
33
|
/** 按应用类型分流:workflow/completion 无会话,chat 系续接 conversation_id */
|
|
32
34
|
private dispatch;
|
|
33
35
|
private deliver;
|
|
@@ -15,6 +15,8 @@ export declare class ConversationStore {
|
|
|
15
15
|
apps: Map<string, AppConfigEntry>);
|
|
16
16
|
/** 粒度归一:bindingKey 与 difyUser 共用,保证两者始终对齐 */
|
|
17
17
|
private resolveScopeDimension;
|
|
18
|
+
/** 私聊判定:有 session 读 session,无 session 的主动触发读池透传的标记 */
|
|
19
|
+
private isDirectOf;
|
|
18
20
|
/** bindingKey = 应用 ID + 粒度 + 平台身份 */
|
|
19
21
|
computeBindingKey(appId: string, rule: DifyRule, fireCtx: FireContext): string;
|
|
20
22
|
/**
|
|
@@ -32,10 +34,15 @@ export declare class ConversationStore {
|
|
|
32
34
|
/**
|
|
33
35
|
* 重置匹配的绑定:本地删行 + 尽力删除 Dify 侧会话(失败仅记录)。
|
|
34
36
|
* 表已无 guildKey 列,按频道匹配时全表读回后解析 bindingKey 过滤(行数量级很小)。
|
|
37
|
+
* 覆盖 guild / user(群内)/ channel 段与私聊执行时的 private 段;
|
|
38
|
+
* global 段跨频道,不随单频道重置。
|
|
35
39
|
*/
|
|
36
40
|
resetWhere(filter: {
|
|
37
41
|
bindingKey?: string;
|
|
38
42
|
guildKey?: string;
|
|
43
|
+
channelKey?: string;
|
|
44
|
+
isDirect?: boolean;
|
|
45
|
+
userId?: string;
|
|
39
46
|
}): Promise<number>;
|
|
40
47
|
/** 按应用 ID(apps 配置的 appId 字段)反查应用 */
|
|
41
48
|
private findByAppId;
|
|
@@ -2,10 +2,10 @@ import { Session } from 'koishi';
|
|
|
2
2
|
import { MessageTriggerType, TriggerCondition, TriggerScope } from '../types';
|
|
3
3
|
export interface ConditionMatched {
|
|
4
4
|
type: MessageTriggerType;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** content 变量的剥离文本,保留 @ 与图片占位(query 由引擎按统一消息格式另行构造) */
|
|
6
|
+
contentText: string;
|
|
7
7
|
}
|
|
8
|
-
/** 评估单个消息条件;返回 null
|
|
8
|
+
/** 评估单个消息条件;返回 null 表示未命中(匹配基于纯文本,@ 元素不参与判定) */
|
|
9
9
|
export declare function evaluateCondition(condition: TriggerCondition, session: Session): ConditionMatched | null;
|
|
10
10
|
/** 作用域过滤:留空的维度不做限制 */
|
|
11
11
|
export declare function matchScope(scope: TriggerScope, session: Session): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Session } from 'koishi';
|
|
2
|
+
import { CooldownSetting } from '../types';
|
|
3
|
+
import { HistoryEntry } from '../history';
|
|
4
|
+
/** 冷却期内缓存的一条触发消息(entry 供统一格式渲染,纯 @ 等无文本消息为空) */
|
|
5
|
+
export interface PendingMessage {
|
|
6
|
+
session: Session;
|
|
7
|
+
entry: HistoryEntry | null;
|
|
8
|
+
/** 该条消息命中条件时剥离出的文本(contentText 用) */
|
|
9
|
+
contentText: string;
|
|
10
|
+
}
|
|
11
|
+
/** 冷却通道:message = 规则内全部消息类条件共享;density:N = 第 N 个密度条件独立 */
|
|
12
|
+
export type CooldownLane = 'message' | `density:${number}`;
|
|
13
|
+
/** 密度通道名的下标前缀 */
|
|
14
|
+
export declare const DENSITY_LANE_PREFIX = "density:";
|
|
15
|
+
/** 密度条件的通道名 */
|
|
16
|
+
export declare function densityLane(index: number): CooldownLane;
|
|
17
|
+
/** 到期扫描交出的通道产物 */
|
|
18
|
+
export interface ExpiredCooldown {
|
|
19
|
+
channelKey: string;
|
|
20
|
+
ruleName: string;
|
|
21
|
+
lane: CooldownLane;
|
|
22
|
+
buffer: PendingMessage[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 触发后冷却:从触发发起时刻计时(请求耗时计入冷却)。
|
|
26
|
+
* 消息通道冷却期内命中消息进缓冲并顺延滑动窗口,到期合并为一次触发;
|
|
27
|
+
* 密度通道冷却期即积攒期(池照常记录),全部过路消息顺延,到期复查达标才触发。
|
|
28
|
+
* 定时触发不参与冷却。配置关闭时所有方法零开销直退。
|
|
29
|
+
*/
|
|
30
|
+
export declare class TriggerCooldown {
|
|
31
|
+
private setting;
|
|
32
|
+
/** channelKey → (规则名+通道 → 冷却态) */
|
|
33
|
+
private channels;
|
|
34
|
+
constructor(setting: CooldownSetting | null);
|
|
35
|
+
get enabled(): boolean;
|
|
36
|
+
/** 触发发起时登记冷却 */
|
|
37
|
+
begin(channelKey: string, ruleName: string, lane: CooldownLane, now: number): void;
|
|
38
|
+
isCooling(channelKey: string, ruleName: string, lane: CooldownLane, now: number): boolean;
|
|
39
|
+
/** 滑动窗口顺延:截止推到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
|
|
40
|
+
private slide;
|
|
41
|
+
/** 滑动窗口顺延:已到期的通道不复活 */
|
|
42
|
+
extend(channelKey: string, ruleName: string, lane: CooldownLane, now: number): void;
|
|
43
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动);已到期的不复活 */
|
|
44
|
+
extendDensityLanes(channelKey: string, now: number): void;
|
|
45
|
+
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
46
|
+
bufferMessage(channelKey: string, ruleName: string, message: PendingMessage): void;
|
|
47
|
+
/** 到期扫描:取走到期通道(连缓冲一并交出),由调用方决定是否重新登记;限定频道时只扫该频道 */
|
|
48
|
+
collectExpired(now: number, channelKey?: string): ExpiredCooldown[];
|
|
49
|
+
private lanesOf;
|
|
50
|
+
}
|
package/lib/trigger/engine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
-
import { DifyRule } from '../types';
|
|
2
|
+
import { CooldownSetting, DifyRule } from '../types';
|
|
3
3
|
import { MessageHistory } from '../history';
|
|
4
4
|
import { RequestRunner } from '../requester/runner';
|
|
5
5
|
/**
|
|
@@ -13,17 +13,27 @@ export declare class TriggerEngine {
|
|
|
13
13
|
private rules;
|
|
14
14
|
private scheduler;
|
|
15
15
|
private pool;
|
|
16
|
-
|
|
16
|
+
private cooldown;
|
|
17
|
+
constructor(ctx: Context, runner: RequestRunner, history: MessageHistory, rules: DifyRule[], cooldown: CooldownSetting | null);
|
|
17
18
|
start(): void;
|
|
18
19
|
private handleMessage;
|
|
19
20
|
private matchRule;
|
|
20
21
|
/** 异步执行一次请求;错误由请求器与这里的兜底记录 */
|
|
21
22
|
private run;
|
|
23
|
+
/** 登记冷却(定时触发不调用;冷却关闭时零开销直退) */
|
|
24
|
+
private beginCooldown;
|
|
22
25
|
private fireMessage;
|
|
23
26
|
private fireDensity;
|
|
27
|
+
/** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
|
|
28
|
+
private fireMerged;
|
|
29
|
+
/** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
|
|
30
|
+
private fireDensityExpiry;
|
|
24
31
|
/**
|
|
25
32
|
* 定时触发(消息摘要):把消息池内该规则作用域有消息的每个频道各发一次请求(取走即清空全池)。
|
|
26
|
-
*
|
|
33
|
+
* 频道身份取自池内最近一条消息;无消息的频道自然跳过。定时不受冷却影响。
|
|
27
34
|
*/
|
|
28
35
|
private fireSchedule;
|
|
36
|
+
/** 扫描冷却到期(带频道只扫该频道,消息路径用;每秒 interval 全量扫描):消息通道合并触发,密度通道复查池 */
|
|
37
|
+
private tickCooldown;
|
|
38
|
+
private onCooldownExpired;
|
|
29
39
|
}
|
package/lib/trigger/pool.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Session } from 'koishi';
|
|
2
|
-
import { DifyRule } from '../types';
|
|
2
|
+
import { DensityCondition, DifyRule } from '../types';
|
|
3
3
|
import { HistoryEntry } from '../history';
|
|
4
4
|
/** 一个频道的缓冲池:密度与定时共用,谁先触发谁取走清空 */
|
|
5
5
|
export interface ChannelPool {
|
|
@@ -13,6 +13,9 @@ export interface ChannelPool {
|
|
|
13
13
|
}
|
|
14
14
|
export interface DensityFire {
|
|
15
15
|
rule: DifyRule;
|
|
16
|
+
/** 命中的密度条件与其在规则内的下标(密度冷却通道键) */
|
|
17
|
+
condition: DensityCondition;
|
|
18
|
+
conditionIndex: number;
|
|
16
19
|
/** 触发时取走的该频道全池消息(含 bot 行与时效外条目) */
|
|
17
20
|
entries: HistoryEntry[];
|
|
18
21
|
}
|
|
@@ -25,11 +28,16 @@ export declare class MessagePool {
|
|
|
25
28
|
private channels;
|
|
26
29
|
/** 一条消息(含 bot 自己的发言)入池,全量记录不做规则过滤 */
|
|
27
30
|
record(session: Session, entry: HistoryEntry): void;
|
|
31
|
+
/** 时效内且通过规则作用域的真人条数(bot 行不推高密度,防自激励) */
|
|
32
|
+
private countQualified;
|
|
28
33
|
/**
|
|
29
34
|
* 新消息入池后的密度评估:时效内(相对触发消息)且通过规则作用域的真人条数达到阈值。
|
|
30
35
|
* 触发即取走并清空该频道全池(bot 行不推高密度,防自激励);池清空后其余规则无从触发。
|
|
36
|
+
* skip 返回 true 的条件(冷却中的密度条件)不参与判定。
|
|
31
37
|
*/
|
|
32
|
-
checkDensity(rules: DifyRule[], session: Session): DensityFire[];
|
|
38
|
+
checkDensity(rules: DifyRule[], session: Session, skip?: (rule: DifyRule, condition: DensityCondition, index: number) => boolean): DensityFire[];
|
|
39
|
+
/** 冷却到期复查:按条件参数统计该频道池(now 为复查时刻),达标即取走整池,未达标返回 null */
|
|
40
|
+
takeDensityChannel(rule: DifyRule, condition: DensityCondition, channelKey: string, now: number): ChannelPool | null;
|
|
33
41
|
/** 定时到点:取走该规则作用域内有消息的所有频道(整池清空,含未过作用域的条目) */
|
|
34
42
|
takeSchedule(rule: DifyRule): ChannelPool[];
|
|
35
43
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Session } from 'koishi';
|
|
2
|
-
/**
|
|
3
|
-
export type MessageTriggerType = 'keyword-prefix' | 'keyword-contains' | 'at-bot' | 'at-user' | 'quote-bot' | 'quote-user';
|
|
2
|
+
/** 消息驱动的触发原因(merged = 冷却期内多条触发消息合并为一次触发) */
|
|
3
|
+
export type MessageTriggerType = 'keyword-prefix' | 'keyword-contains' | 'at-bot' | 'at-user' | 'quote-bot' | 'quote-user' | 'merged';
|
|
4
4
|
/** 非消息驱动的触发原因 */
|
|
5
5
|
export type ProactiveTriggerType = 'schedule' | 'density';
|
|
6
6
|
export type TriggerType = MessageTriggerType | ProactiveTriggerType;
|
|
7
|
+
/** 是否由消息触发(合并触发也算):定时与密度触发没有单一触发消息 */
|
|
8
|
+
export declare function isMessageTrigger(type: TriggerType): boolean;
|
|
7
9
|
/** 关键词条件(prefix 触发时剥离关键词作为 query,contains 保留全文) */
|
|
8
10
|
export interface KeywordCondition {
|
|
9
11
|
type: 'keyword-prefix' | 'keyword-contains';
|
|
@@ -63,6 +65,9 @@ export type VariableSource = {
|
|
|
63
65
|
} | {
|
|
64
66
|
type: 'meta';
|
|
65
67
|
key: MetaField;
|
|
68
|
+
} | {
|
|
69
|
+
type: 'constant';
|
|
70
|
+
value: string;
|
|
66
71
|
};
|
|
67
72
|
/** 一条变量映射:Dify inputs 字段名 ← 来源 */
|
|
68
73
|
export interface VariableBinding {
|
|
@@ -71,6 +76,15 @@ export interface VariableBinding {
|
|
|
71
76
|
}
|
|
72
77
|
/** Dify 会话绑定粒度 */
|
|
73
78
|
export type ConversationScope = 'per-user' | 'per-guild' | 'per-channel' | 'global';
|
|
79
|
+
/** 触发后冷却设置(schema 规范化后的内部形态;null 表示关闭) */
|
|
80
|
+
export type CooldownSetting = {
|
|
81
|
+
mode: 'fixed';
|
|
82
|
+
ms: number;
|
|
83
|
+
} | {
|
|
84
|
+
mode: 'sliding';
|
|
85
|
+
minMs: number;
|
|
86
|
+
maxMs: number;
|
|
87
|
+
};
|
|
74
88
|
/** 触发规则(配置页 schema 规范化后的内部形态;身份即 name,按优先级降序排列) */
|
|
75
89
|
export interface DifyRule {
|
|
76
90
|
name: string;
|
|
@@ -96,6 +110,10 @@ export interface FireContext {
|
|
|
96
110
|
rule: DifyRule;
|
|
97
111
|
/** 发给 Dify 的 query:统一消息格式(消息触发为单行,窗口触发为逐行拼接) */
|
|
98
112
|
queryText: string;
|
|
99
|
-
/** content
|
|
113
|
+
/** content 变量的值:消息触发为剥离文本(剔除 @bot),窗口触发为精简行拼接 */
|
|
100
114
|
contentText: string;
|
|
115
|
+
/** 本次触发相关消息的图片 URL(合并/密度/定时的池或缓冲条目合集):供上传管线补齐非首条消息的图片 */
|
|
116
|
+
imageUrls?: string[];
|
|
117
|
+
/** 无 session 的主动触发(定时/密度到期)从消息池透传的私聊标记:会话绑定与 isDirect 变量用 */
|
|
118
|
+
isDirect?: boolean;
|
|
101
119
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Session } from 'koishi';
|
|
2
|
+
/** 图片占位符携带的 URL 摘要长度(hex 位) */
|
|
3
|
+
export declare const IMAGE_HASH_LENGTH = 8;
|
|
2
4
|
/**
|
|
3
5
|
* 会话文本提取的统一入口。此前有四个变体(条件匹配、历史记录、变量、@ 剥离)
|
|
4
6
|
* 各自实现「elements 有则取 text 元素,否则解析 content」,trim 与解析行为已经漂移,
|
|
@@ -6,17 +8,24 @@ import { Session } from 'koishi';
|
|
|
6
8
|
*/
|
|
7
9
|
/** 把可解析的消息文本(含元素标记)转纯文本;解析失败回退原文 */
|
|
8
10
|
export declare function parseTextSafe(content: string | undefined): string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
/** 会话消息纯文本:优先 elements,退回 content 解析;统一 trim */
|
|
14
|
-
export declare function extractText(session: Session, options?: ExtractTextOptions): string;
|
|
15
|
-
/** 消息中的图片元素在文本形态里的占位符 */
|
|
11
|
+
/** 会话消息纯文本:优先 elements,退回 content 解析;统一 trim(@ 元素不产生文本,仅供条件匹配) */
|
|
12
|
+
export declare function extractText(session: Session): string;
|
|
13
|
+
/** 消息中的图片元素在文本形态里的占位符(图片无 URL 时的兜底形态) */
|
|
16
14
|
export declare const IMAGE_PLACEHOLDER = "[\u56FE\u7247]";
|
|
15
|
+
/** 图片占位符带 URL 摘要:同 URL 同标识,query 里的占位符与上传文件可全局对应 */
|
|
16
|
+
export declare function imagePlaceholder(url?: string): string;
|
|
17
|
+
/** 给被丢弃图片的占位符追加已丢弃注记:按 URL 摘要全局定位,无需与元素顺序对齐 */
|
|
18
|
+
export declare function annotateDroppedPlaceholders(text: string, droppedUrls: string[]): string;
|
|
19
|
+
interface ExtractVerbatimOptions {
|
|
20
|
+
/** 跳过 @ 机器人自身的渲染(content 变量语义:@bot 是触发结构而非消息内容) */
|
|
21
|
+
excludeBotAt?: boolean;
|
|
22
|
+
}
|
|
17
23
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
24
|
+
* 消息原貌文本:text 元素按序拼接,@ 元素渲染为 @名字(缺名字用 ID),
|
|
25
|
+
* img/image 元素渲染为 [image:URL摘要]——统一格式行(query/池/历史)用默认形态,
|
|
26
|
+
* content 变量通道用 excludeBotAt 形态剔除 @bot。
|
|
20
27
|
*/
|
|
21
|
-
export declare function extractTextWithPlaceholders(session: Session): string;
|
|
28
|
+
export declare function extractTextWithPlaceholders(session: Session, options?: ExtractVerbatimOptions): string;
|
|
29
|
+
/** 消息中图片元素的 URL 列表(按元素序,与原貌文本中 [image:摘要] 的出现序一致) */
|
|
30
|
+
export declare function extractImageUrls(session: Session): string[];
|
|
22
31
|
export {};
|
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.6",
|
|
5
5
|
"author": "PinkElysia",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/PinkElysiaDev/dify-ai-plugin-for-Koishi#readme",
|