koishi-plugin-dify-ai 0.1.7 → 0.1.10
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 +1 -0
- package/lib/config.d.ts +16 -25
- package/lib/index.js +256 -142
- package/lib/requester/files.d.ts +2 -2
- package/lib/requester/runner.d.ts +10 -0
- package/lib/requester/variables.d.ts +5 -1
- package/lib/state/conversation.d.ts +5 -1
- package/lib/trigger/cooldown.d.ts +4 -2
- package/lib/trigger/pool.d.ts +1 -2
- package/lib/types.d.ts +5 -0
- package/lib/utils/session-text.d.ts +3 -1
- package/package.json +1 -1
package/lib/api/client.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class DifyClient {
|
|
|
11
11
|
private timeout;
|
|
12
12
|
constructor(http: Context['http'], app: DifyClientApp, timeout?: number);
|
|
13
13
|
private buildUrl;
|
|
14
|
+
private authHeaders;
|
|
14
15
|
private buildJsonHeaders;
|
|
15
16
|
private errorFrom;
|
|
16
17
|
/** 非 2xx 时抛出带状态码与 Dify 错误码的 DifyApiError */
|
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 {
|
|
@@ -68,13 +71,6 @@ export interface RuleEntry {
|
|
|
68
71
|
scope: ScopeRow[];
|
|
69
72
|
variables: VariableEntry[];
|
|
70
73
|
}
|
|
71
|
-
/** 引用触发消息的条件分支形态:关闭时空分支,开启时按耗时阈值决定是否引用 */
|
|
72
|
-
export type QuoteEntry = {
|
|
73
|
-
mode: 'off';
|
|
74
|
-
} | {
|
|
75
|
-
mode: 'on';
|
|
76
|
-
minElapsedSeconds: number;
|
|
77
|
-
};
|
|
78
74
|
/** 触发后冷却的条件分支形态 */
|
|
79
75
|
export type CooldownEntry = {
|
|
80
76
|
mode: 'off';
|
|
@@ -86,33 +82,29 @@ export type CooldownEntry = {
|
|
|
86
82
|
minSeconds?: number;
|
|
87
83
|
maxSeconds?: number;
|
|
88
84
|
};
|
|
89
|
-
/** 上传消息文件的条件分支形态:关闭时空分支,开启时限制数量与单文件大小 */
|
|
90
|
-
export type UploadEntry = {
|
|
91
|
-
mode: 'off';
|
|
92
|
-
} | {
|
|
93
|
-
mode: 'on';
|
|
94
|
-
maxCount?: number;
|
|
95
|
-
maxSizeMb?: number;
|
|
96
|
-
};
|
|
97
85
|
export interface Config {
|
|
98
86
|
apps: AppConfigEntry[];
|
|
99
87
|
rules: RuleEntry[];
|
|
100
88
|
parseElements: boolean;
|
|
101
|
-
quote:
|
|
89
|
+
quote: boolean;
|
|
90
|
+
minElapsedSeconds: number;
|
|
102
91
|
maxSegmentLength: number;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
quoteTrigger?: boolean;
|
|
92
|
+
uploadFiles: boolean;
|
|
93
|
+
maxCount: number;
|
|
94
|
+
maxSizeMb: number;
|
|
107
95
|
requestTimeout: number;
|
|
108
96
|
historyLimit: number;
|
|
109
97
|
sendErrors: boolean;
|
|
110
98
|
cooldown: CooldownEntry;
|
|
111
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 上传消息文件:布尔开关驱动分支(开=可配数量与单文件大小上限,关=空分支),官方 union-tagged 写法。
|
|
102
|
+
* 上限默认 -1 规避 0 被表单默认值机制吞掉(-1 与 0 同为不限,仍受 Dify 各类型上限约束)。
|
|
103
|
+
*/
|
|
112
104
|
export declare const Config: Schema<Config>;
|
|
113
105
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
106
|
+
* 上传消息文件配置(平铺开关,默认关闭:显式开启才上传)。数量与单文件大小
|
|
107
|
+
* 0 与 -1 均为不限,大小仍与 Dify 各类型上限取较小值。
|
|
116
108
|
*/
|
|
117
109
|
export declare function resolveUploadConfig(config: Config): {
|
|
118
110
|
enabled: boolean;
|
|
@@ -120,9 +112,8 @@ export declare function resolveUploadConfig(config: Config): {
|
|
|
120
112
|
maxMb: number;
|
|
121
113
|
};
|
|
122
114
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* 此时以旧布尔为准;用户改过新表单(quote 非默认)则新值优先。
|
|
115
|
+
* 引用触发消息配置(平铺开关,含 0.1.x quoteTrigger 透传迁移:
|
|
116
|
+
* 旧布尔被保留且开关与阈值均未被改动时以旧布尔为准)。阈值 -1 与 0 同为总是引用。
|
|
126
117
|
*/
|
|
127
118
|
export declare function resolveQuoteConfig(config: Config): {
|
|
128
119
|
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,16 +141,44 @@ 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
|
-
var
|
|
150
|
+
var legacySwitch = import_koishi.Schema.transform(
|
|
151
|
+
import_koishi.Schema.any(),
|
|
152
|
+
(value) => typeof value === "boolean" ? value : value?.mode !== "off"
|
|
153
|
+
);
|
|
154
|
+
function switchSchema(defaultValue) {
|
|
155
|
+
return import_koishi.Schema.union([import_koishi.Schema.boolean().default(defaultValue), legacySwitch]).default(defaultValue);
|
|
156
|
+
}
|
|
157
|
+
__name(switchSchema, "switchSchema");
|
|
158
|
+
var quoteSwitchSchema = import_koishi.Schema.intersect([
|
|
138
159
|
import_koishi.Schema.object({
|
|
139
|
-
|
|
140
|
-
})
|
|
160
|
+
quote: switchSchema(true).description("引用触发消息")
|
|
161
|
+
}),
|
|
162
|
+
import_koishi.Schema.union([
|
|
163
|
+
import_koishi.Schema.object({
|
|
164
|
+
quote: import_koishi.Schema.const(true),
|
|
165
|
+
minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1).description("回复耗时多少秒才引用,0 总是引用")
|
|
166
|
+
}),
|
|
167
|
+
import_koishi.Schema.object({})
|
|
168
|
+
])
|
|
169
|
+
]);
|
|
170
|
+
var uploadSwitchSchema = import_koishi.Schema.intersect([
|
|
141
171
|
import_koishi.Schema.object({
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
uploadFiles: switchSchema(false).description("上传消息中的文件")
|
|
173
|
+
}),
|
|
174
|
+
import_koishi.Schema.union([
|
|
175
|
+
import_koishi.Schema.object({
|
|
176
|
+
uploadFiles: import_koishi.Schema.const(true).required(),
|
|
177
|
+
maxCount: import_koishi.Schema.number().min(-1).default(-1).description("数量上限,0 不限"),
|
|
178
|
+
maxSizeMb: import_koishi.Schema.number().min(-1).default(-1).description("单文件大小上限 MB,0 不限")
|
|
179
|
+
}),
|
|
180
|
+
import_koishi.Schema.object({})
|
|
181
|
+
])
|
|
145
182
|
]);
|
|
146
183
|
var cooldownSchema = import_koishi.Schema.union([
|
|
147
184
|
import_koishi.Schema.object({
|
|
@@ -157,17 +194,6 @@ var cooldownSchema = import_koishi.Schema.union([
|
|
|
157
194
|
maxSeconds: import_koishi.Schema.natural().default(120).min(1).description("最大冷却秒数")
|
|
158
195
|
}).default({ mode: "sliding", minSeconds: 30, maxSeconds: 120 }).description("滑动窗口")
|
|
159
196
|
]);
|
|
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("开启"),
|
|
169
|
-
import_koishi.Schema.boolean().description("旧版开关")
|
|
170
|
-
]);
|
|
171
197
|
var Config = import_koishi.Schema.intersect([
|
|
172
198
|
import_koishi.Schema.object({
|
|
173
199
|
apps: import_koishi.Schema.array(
|
|
@@ -185,7 +211,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
185
211
|
import_koishi.Schema.object({
|
|
186
212
|
name: import_koishi.Schema.string().required().description("规则名"),
|
|
187
213
|
enabled: import_koishi.Schema.boolean().default(true).description("启用"),
|
|
188
|
-
priority: import_koishi.Schema.number().default(
|
|
214
|
+
priority: import_koishi.Schema.number().default(-1).description("优先级,大者先匹配"),
|
|
189
215
|
app: import_koishi.Schema.string().required().description("应用名"),
|
|
190
216
|
conversationScope: import_koishi.Schema.union([
|
|
191
217
|
import_koishi.Schema.const("per-guild").description("按群聊"),
|
|
@@ -214,10 +240,10 @@ var Config = import_koishi.Schema.intersect([
|
|
|
214
240
|
}).description("触发规则"),
|
|
215
241
|
import_koishi.Schema.object({
|
|
216
242
|
parseElements: import_koishi.Schema.boolean().default(false).description("解析回复中的消息元素"),
|
|
217
|
-
|
|
218
|
-
maxSegmentLength: import_koishi.Schema.natural().default(0).description("分段长度,0 不分段"),
|
|
219
|
-
uploadFiles: uploadSchema.default({ mode: "on", maxCount: 0, maxSizeMb: 0 }).description("上传消息中的文件")
|
|
243
|
+
maxSegmentLength: import_koishi.Schema.number().min(-1).default(-1).description("分段长度,0 不分段")
|
|
220
244
|
}).description("回复行为"),
|
|
245
|
+
quoteSwitchSchema,
|
|
246
|
+
uploadSwitchSchema,
|
|
221
247
|
import_koishi.Schema.object({
|
|
222
248
|
requestTimeout: import_koishi.Schema.natural().role("ms").default(3e5).description("请求超时"),
|
|
223
249
|
historyLimit: import_koishi.Schema.natural().default(20).min(1).description("历史消息条数"),
|
|
@@ -309,6 +335,17 @@ function toScope(rows) {
|
|
|
309
335
|
}
|
|
310
336
|
__name(toScope, "toScope");
|
|
311
337
|
function toVariable(entry) {
|
|
338
|
+
if (entry.source === "json") {
|
|
339
|
+
const template = entry.template ?? "";
|
|
340
|
+
if (!template.trim()) return null;
|
|
341
|
+
try {
|
|
342
|
+
const probe = JSON.parse(template.replace(JSON_PLACEHOLDER, "0"));
|
|
343
|
+
if (typeof probe !== "object" || probe === null || Array.isArray(probe)) return null;
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return { field: "", source: { type: "json", template } };
|
|
348
|
+
}
|
|
312
349
|
const field = entry.field?.trim();
|
|
313
350
|
if (!field) return null;
|
|
314
351
|
if (entry.source === "constant") {
|
|
@@ -328,25 +365,22 @@ function toVariable(entry) {
|
|
|
328
365
|
}
|
|
329
366
|
__name(toVariable, "toVariable");
|
|
330
367
|
function resolveUploadConfig(config) {
|
|
331
|
-
if (
|
|
332
|
-
return config.uploadFiles ? { enabled: true, maxCount: 0, maxMb: 0 } : { enabled: false, maxCount: 0, maxMb: 0 };
|
|
333
|
-
}
|
|
334
|
-
if (config.uploadFiles?.mode === "off") return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
368
|
+
if (config.uploadFiles !== true) return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
335
369
|
return {
|
|
336
370
|
enabled: true,
|
|
337
|
-
maxCount: Number(config.
|
|
338
|
-
maxMb: Number(config.
|
|
371
|
+
maxCount: Number(config.maxCount) || 0,
|
|
372
|
+
maxMb: Number(config.maxSizeMb) || 0
|
|
339
373
|
};
|
|
340
374
|
}
|
|
341
375
|
__name(resolveUploadConfig, "resolveUploadConfig");
|
|
342
376
|
function resolveQuoteConfig(config) {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
377
|
+
const threshold = Number(config.minElapsedSeconds) || 0;
|
|
378
|
+
const untouched = config.quote !== false && !(threshold > 0);
|
|
379
|
+
const legacy = config.quoteTrigger;
|
|
380
|
+
if (typeof legacy === "boolean" && untouched) {
|
|
381
|
+
return { enabled: legacy, minElapsedSeconds: 0 };
|
|
347
382
|
}
|
|
348
|
-
|
|
349
|
-
return { enabled: true, minElapsedSeconds: Number(quote?.minElapsedSeconds) || 0 };
|
|
383
|
+
return { enabled: config.quote !== false, minElapsedSeconds: threshold };
|
|
350
384
|
}
|
|
351
385
|
__name(resolveQuoteConfig, "resolveQuoteConfig");
|
|
352
386
|
function toCooldown(entry) {
|
|
@@ -362,6 +396,7 @@ function toCooldown(entry) {
|
|
|
362
396
|
}
|
|
363
397
|
__name(toCooldown, "toCooldown");
|
|
364
398
|
function normalizeConfig(config) {
|
|
399
|
+
config ??= {};
|
|
365
400
|
const apps = /* @__PURE__ */ new Map();
|
|
366
401
|
for (const app of config.apps ?? []) {
|
|
367
402
|
if (app.name?.trim() && !apps.has(app.name)) apps.set(app.name, app);
|
|
@@ -419,11 +454,11 @@ var DifyClient = class {
|
|
|
419
454
|
buildUrl(path) {
|
|
420
455
|
return this.app.apiURL.replace(/\/+$/, "") + path;
|
|
421
456
|
}
|
|
457
|
+
authHeaders() {
|
|
458
|
+
return { Authorization: `Bearer ${this.app.apiKey}` };
|
|
459
|
+
}
|
|
422
460
|
buildJsonHeaders() {
|
|
423
|
-
return {
|
|
424
|
-
Authorization: `Bearer ${this.app.apiKey}`,
|
|
425
|
-
"Content-Type": "application/json"
|
|
426
|
-
};
|
|
461
|
+
return { ...this.authHeaders(), "Content-Type": "application/json" };
|
|
427
462
|
}
|
|
428
463
|
errorFrom(status, data, statusText = "") {
|
|
429
464
|
const isObject = typeof data === "object" && data !== null;
|
|
@@ -473,7 +508,7 @@ var DifyClient = class {
|
|
|
473
508
|
);
|
|
474
509
|
form.set("user", user);
|
|
475
510
|
const res = await this.http("POST", this.buildUrl("/files/upload"), {
|
|
476
|
-
headers:
|
|
511
|
+
headers: this.authHeaders(),
|
|
477
512
|
data: form,
|
|
478
513
|
timeout: this.timeout,
|
|
479
514
|
validateStatus: /* @__PURE__ */ __name(() => true, "validateStatus")
|
|
@@ -529,6 +564,14 @@ function annotateDroppedPlaceholders(text, droppedUrls) {
|
|
|
529
564
|
return result;
|
|
530
565
|
}
|
|
531
566
|
__name(annotateDroppedPlaceholders, "annotateDroppedPlaceholders");
|
|
567
|
+
function isImageElement(element) {
|
|
568
|
+
return element.type === "img" || element.type === "image";
|
|
569
|
+
}
|
|
570
|
+
__name(isImageElement, "isImageElement");
|
|
571
|
+
function imageSourceOf(element) {
|
|
572
|
+
return element.attrs?.["url"] ?? element.attrs?.["src"];
|
|
573
|
+
}
|
|
574
|
+
__name(imageSourceOf, "imageSourceOf");
|
|
532
575
|
function elementsOf(session) {
|
|
533
576
|
if (session.elements?.length) return session.elements;
|
|
534
577
|
try {
|
|
@@ -548,8 +591,8 @@ function extractTextWithPlaceholders(session, options = {}) {
|
|
|
548
591
|
if (options.excludeBotAt && element.attrs?.["id"] === session.selfId) continue;
|
|
549
592
|
const name2 = element.attrs?.["name"] ?? element.attrs?.["id"];
|
|
550
593
|
if (name2) parts.push(`@${name2}`);
|
|
551
|
-
} else if (element
|
|
552
|
-
parts.push(imagePlaceholder(element
|
|
594
|
+
} else if (isImageElement(element)) {
|
|
595
|
+
parts.push(imagePlaceholder(imageSourceOf(element)));
|
|
553
596
|
}
|
|
554
597
|
}
|
|
555
598
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
@@ -558,8 +601,8 @@ __name(extractTextWithPlaceholders, "extractTextWithPlaceholders");
|
|
|
558
601
|
function extractImageUrls(session) {
|
|
559
602
|
const urls = [];
|
|
560
603
|
for (const element of elementsOf(session)) {
|
|
561
|
-
if (element
|
|
562
|
-
const source = element
|
|
604
|
+
if (!isImageElement(element)) continue;
|
|
605
|
+
const source = imageSourceOf(element);
|
|
563
606
|
if (typeof source === "string" && source) urls.push(source);
|
|
564
607
|
}
|
|
565
608
|
return urls;
|
|
@@ -578,7 +621,8 @@ __name(formatTimestamp, "formatTimestamp");
|
|
|
578
621
|
function formatHistoryEntry(entry) {
|
|
579
622
|
const quote = entry.quote ? `(引用了 [${formatTimestamp(entry.quote.timestamp)}] ${entry.quote.username} (${entry.quote.userId}) 说:${entry.quote.content})` : "";
|
|
580
623
|
const messageId = entry.messageId ? ` [msg:${entry.messageId}]` : "";
|
|
581
|
-
|
|
624
|
+
const separator = quote ? "" : " ";
|
|
625
|
+
return `[${formatTimestamp(entry.timestamp)}] ${entry.username} (${entry.userId})${messageId}${quote}${separator}说:${entry.content}`;
|
|
582
626
|
}
|
|
583
627
|
__name(formatHistoryEntry, "formatHistoryEntry");
|
|
584
628
|
function formatHistoryEntries(entries) {
|
|
@@ -649,7 +693,10 @@ function hasAt(session, id) {
|
|
|
649
693
|
return session.elements?.some((element) => element.type === "at" && element.attrs?.["id"] === id) ?? false;
|
|
650
694
|
}
|
|
651
695
|
__name(hasAt, "hasAt");
|
|
652
|
-
var
|
|
696
|
+
var escapeRegExp = /* @__PURE__ */ __name((text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "escapeRegExp");
|
|
697
|
+
var NON_TEXT_PREFIX = new RegExp(
|
|
698
|
+
`^(?:@\\S+|\\[image:[0-9a-f]{${IMAGE_HASH_LENGTH}}\\]|${escapeRegExp(IMAGE_PLACEHOLDER)}|\\s)*`
|
|
699
|
+
);
|
|
653
700
|
function stripPrefixKeyword(verbatim, keyword) {
|
|
654
701
|
const prefix = NON_TEXT_PREFIX.exec(verbatim)?.[0] ?? "";
|
|
655
702
|
const rest = verbatim.slice(prefix.length);
|
|
@@ -709,8 +756,7 @@ function matchScope(scope, session) {
|
|
|
709
756
|
];
|
|
710
757
|
if (dimensions.some(([allowed, value]) => allowed?.length && !allowed.includes(value))) return false;
|
|
711
758
|
if (scope.excludeUsers?.length && scope.excludeUsers.includes(session.userId)) return false;
|
|
712
|
-
if (scope.isDirect
|
|
713
|
-
if (scope.isDirect === false && session.isDirect) return false;
|
|
759
|
+
if (scope.isDirect != null && scope.isDirect !== !!session.isDirect) return false;
|
|
714
760
|
return true;
|
|
715
761
|
}
|
|
716
762
|
__name(matchScope, "matchScope");
|
|
@@ -790,7 +836,7 @@ var MessagePool = class {
|
|
|
790
836
|
if (skip?.(rule, condition, index)) continue;
|
|
791
837
|
if (this.countQualified(pool, rule, condition, now) >= condition.densityCount) {
|
|
792
838
|
this.channels.delete(key);
|
|
793
|
-
return [{ rule,
|
|
839
|
+
return [{ rule, conditionIndex: index, entries: pool.entries }];
|
|
794
840
|
}
|
|
795
841
|
}
|
|
796
842
|
}
|
|
@@ -881,13 +927,13 @@ var RuleScheduler = class {
|
|
|
881
927
|
// src/trigger/cooldown.ts
|
|
882
928
|
var BUFFER_CAPACITY = 1024;
|
|
883
929
|
var DENSITY_LANE_PREFIX = "density:";
|
|
930
|
+
var LANE_KEY_SEPARATOR = "\n";
|
|
884
931
|
function densityLane(index) {
|
|
885
932
|
return `${DENSITY_LANE_PREFIX}${index}`;
|
|
886
933
|
}
|
|
887
934
|
__name(densityLane, "densityLane");
|
|
888
935
|
function laneKeyOf(ruleName, lane) {
|
|
889
|
-
return `${ruleName}
|
|
890
|
-
${lane}`;
|
|
936
|
+
return `${ruleName}${LANE_KEY_SEPARATOR}${lane}`;
|
|
891
937
|
}
|
|
892
938
|
__name(laneKeyOf, "laneKeyOf");
|
|
893
939
|
var TriggerCooldown = class {
|
|
@@ -923,23 +969,23 @@ var TriggerCooldown = class {
|
|
|
923
969
|
state.fireAt + setting.maxMs
|
|
924
970
|
);
|
|
925
971
|
}
|
|
926
|
-
/**
|
|
972
|
+
/** 滑动窗口顺延:sliding 模式且通道未到期才执行(已到期的不复活) */
|
|
973
|
+
slideIfActive(state, setting, now) {
|
|
974
|
+
if (setting?.mode !== "sliding" || !state || now >= state.deadline) return;
|
|
975
|
+
this.slide(state, setting, now);
|
|
976
|
+
}
|
|
977
|
+
/** 滑动窗口顺延 */
|
|
927
978
|
extend(channelKey, ruleName, lane, now) {
|
|
928
|
-
const setting = this.setting;
|
|
929
|
-
if (setting?.mode !== "sliding") return;
|
|
930
979
|
const state = this.channels.get(channelKey)?.get(laneKeyOf(ruleName, lane));
|
|
931
|
-
|
|
932
|
-
this.slide(state, setting, now);
|
|
980
|
+
this.slideIfActive(state, this.setting, now);
|
|
933
981
|
}
|
|
934
|
-
/**
|
|
982
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动) */
|
|
935
983
|
extendDensityLanes(channelKey, now) {
|
|
936
|
-
const setting = this.setting;
|
|
937
|
-
if (setting?.mode !== "sliding") return;
|
|
938
984
|
const lanes = this.channels.get(channelKey);
|
|
939
985
|
if (!lanes) return;
|
|
940
986
|
for (const [key, state] of lanes) {
|
|
941
|
-
if (!key.includes(
|
|
942
|
-
this.
|
|
987
|
+
if (!key.includes(LANE_KEY_SEPARATOR + DENSITY_LANE_PREFIX)) continue;
|
|
988
|
+
this.slideIfActive(state, this.setting, now);
|
|
943
989
|
}
|
|
944
990
|
}
|
|
945
991
|
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
@@ -959,7 +1005,7 @@ var TriggerCooldown = class {
|
|
|
959
1005
|
if (!lanes) continue;
|
|
960
1006
|
for (const [laneKey, state] of lanes) {
|
|
961
1007
|
if (now < state.deadline) continue;
|
|
962
|
-
const separator = laneKey.indexOf(
|
|
1008
|
+
const separator = laneKey.indexOf(LANE_KEY_SEPARATOR);
|
|
963
1009
|
lanes.delete(laneKey);
|
|
964
1010
|
expired.push({
|
|
965
1011
|
channelKey: key,
|
|
@@ -984,10 +1030,19 @@ var TriggerCooldown = class {
|
|
|
984
1030
|
|
|
985
1031
|
// src/trigger/engine.ts
|
|
986
1032
|
var SCHEDULER_TICK_MS = 1e3;
|
|
1033
|
+
var joinNonEmpty = /* @__PURE__ */ __name((parts) => parts.filter(Boolean).join("\n"), "joinNonEmpty");
|
|
987
1034
|
function collectImageUrls(entries) {
|
|
988
1035
|
return entries.flatMap((entry) => entry?.role === "user" ? entry.imageUrls ?? [] : []);
|
|
989
1036
|
}
|
|
990
1037
|
__name(collectImageUrls, "collectImageUrls");
|
|
1038
|
+
function windowFieldsOf(entries) {
|
|
1039
|
+
return {
|
|
1040
|
+
queryText: formatHistoryEntries(entries),
|
|
1041
|
+
contentText: formatHistoryBrief(entries),
|
|
1042
|
+
imageUrls: collectImageUrls(entries)
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
__name(windowFieldsOf, "windowFieldsOf");
|
|
991
1046
|
function sessionIdentityOf(session) {
|
|
992
1047
|
return {
|
|
993
1048
|
platform: session.platform,
|
|
@@ -1040,8 +1095,7 @@ var TriggerEngine = class {
|
|
|
1040
1095
|
this.ctx.logger.info("已加载 %d 条启用规则", this.rules.length);
|
|
1041
1096
|
}
|
|
1042
1097
|
handleMessage(session, next) {
|
|
1043
|
-
if (this.ctx.bots[session.uid]) return next();
|
|
1044
|
-
if (!session.channelId) return next();
|
|
1098
|
+
if (this.ctx.bots[session.uid] || !session.channelId) return next();
|
|
1045
1099
|
const channelKey = channelKeyOf(session.platform, session.channelId);
|
|
1046
1100
|
this.tickCooldown(channelKey);
|
|
1047
1101
|
const entry = buildHistoryEntry(session);
|
|
@@ -1112,16 +1166,14 @@ var TriggerEngine = class {
|
|
|
1112
1166
|
...sessionIdentityOf(session),
|
|
1113
1167
|
triggerType: "density",
|
|
1114
1168
|
rule: fire.rule,
|
|
1115
|
-
|
|
1116
|
-
contentText: formatHistoryBrief(fire.entries),
|
|
1117
|
-
imageUrls: collectImageUrls(fire.entries)
|
|
1169
|
+
...windowFieldsOf(fire.entries)
|
|
1118
1170
|
});
|
|
1119
1171
|
}
|
|
1120
1172
|
/** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
|
|
1121
1173
|
fireMerged(rule, buffer) {
|
|
1122
1174
|
const first = buffer[0];
|
|
1123
|
-
const queryText = buffer.map((item) => item.entry ? formatHistoryEntry(item.entry) : "")
|
|
1124
|
-
const contentText = buffer.map((item) => item.contentText)
|
|
1175
|
+
const queryText = joinNonEmpty(buffer.map((item) => item.entry ? formatHistoryEntry(item.entry) : ""));
|
|
1176
|
+
const contentText = joinNonEmpty(buffer.map((item) => item.contentText));
|
|
1125
1177
|
this.beginCooldown(channelKeyOf(first.session.platform, first.session.channelId), rule.name, "message");
|
|
1126
1178
|
this.run({
|
|
1127
1179
|
session: first.session,
|
|
@@ -1135,10 +1187,9 @@ var TriggerEngine = class {
|
|
|
1135
1187
|
});
|
|
1136
1188
|
}
|
|
1137
1189
|
/** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
|
|
1138
|
-
fireDensityExpiry(rule, conditionIndex, channel, now) {
|
|
1190
|
+
fireDensityExpiry(rule, condition, conditionIndex, channel, now) {
|
|
1139
1191
|
this.beginCooldown(channelKeyOf(channel.platform, channel.channelId), rule.name, densityLane(conditionIndex));
|
|
1140
|
-
const
|
|
1141
|
-
const windowMs = condition.type === "density" ? condition.densityWindowSeconds * 1e3 : 0;
|
|
1192
|
+
const windowMs = condition.densityWindowSeconds * 1e3;
|
|
1142
1193
|
let userId;
|
|
1143
1194
|
for (let i = channel.entries.length - 1; i >= 0; i--) {
|
|
1144
1195
|
const item = channel.entries[i];
|
|
@@ -1152,9 +1203,7 @@ var TriggerEngine = class {
|
|
|
1152
1203
|
userId,
|
|
1153
1204
|
triggerType: "density",
|
|
1154
1205
|
rule,
|
|
1155
|
-
|
|
1156
|
-
contentText: formatHistoryBrief(channel.entries),
|
|
1157
|
-
imageUrls: collectImageUrls(channel.entries),
|
|
1206
|
+
...windowFieldsOf(channel.entries),
|
|
1158
1207
|
isDirect: channel.isDirect
|
|
1159
1208
|
});
|
|
1160
1209
|
}
|
|
@@ -1168,9 +1217,7 @@ var TriggerEngine = class {
|
|
|
1168
1217
|
...poolIdentityOf(channel),
|
|
1169
1218
|
triggerType: "schedule",
|
|
1170
1219
|
rule,
|
|
1171
|
-
|
|
1172
|
-
contentText: formatHistoryBrief(channel.entries),
|
|
1173
|
-
imageUrls: collectImageUrls(channel.entries),
|
|
1220
|
+
...windowFieldsOf(channel.entries),
|
|
1174
1221
|
isDirect: channel.isDirect
|
|
1175
1222
|
});
|
|
1176
1223
|
}
|
|
@@ -1194,16 +1241,10 @@ var TriggerEngine = class {
|
|
|
1194
1241
|
const condition = rule.conditions[index];
|
|
1195
1242
|
if (condition?.type !== "density") return;
|
|
1196
1243
|
const channel = this.pool.takeDensityChannel(rule, condition, expired.channelKey, now);
|
|
1197
|
-
if (channel) this.fireDensityExpiry(rule, index, channel, now);
|
|
1244
|
+
if (channel) this.fireDensityExpiry(rule, condition, index, channel, now);
|
|
1198
1245
|
}
|
|
1199
1246
|
};
|
|
1200
1247
|
|
|
1201
|
-
// src/types.ts
|
|
1202
|
-
function isMessageTrigger(type) {
|
|
1203
|
-
return type !== "schedule" && type !== "density";
|
|
1204
|
-
}
|
|
1205
|
-
__name(isMessageTrigger, "isMessageTrigger");
|
|
1206
|
-
|
|
1207
1248
|
// src/requester/files.ts
|
|
1208
1249
|
var import_promises = require("fs/promises");
|
|
1209
1250
|
var import_fs = require("fs");
|
|
@@ -1259,6 +1300,10 @@ function guessExtension(mimeType) {
|
|
|
1259
1300
|
return found ? found[0] : "bin";
|
|
1260
1301
|
}
|
|
1261
1302
|
__name(guessExtension, "guessExtension");
|
|
1303
|
+
function fallbackFileName(mimeType) {
|
|
1304
|
+
return `file.${guessExtension(mimeType)}`;
|
|
1305
|
+
}
|
|
1306
|
+
__name(fallbackFileName, "fallbackFileName");
|
|
1262
1307
|
function mimeToFileType(mimeType) {
|
|
1263
1308
|
if (!mimeType) return "custom";
|
|
1264
1309
|
if (mimeType.startsWith("image/")) return "image";
|
|
@@ -1272,15 +1317,11 @@ function mimeToFileType(mimeType) {
|
|
|
1272
1317
|
__name(mimeToFileType, "mimeToFileType");
|
|
1273
1318
|
function extractFileCandidates(session) {
|
|
1274
1319
|
const candidates = [];
|
|
1275
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1276
1320
|
for (const element of session.elements ?? []) {
|
|
1277
1321
|
const difyType = ELEMENT_TYPE[element.type];
|
|
1278
1322
|
if (!difyType) continue;
|
|
1279
|
-
const source = element
|
|
1323
|
+
const source = imageSourceOf(element);
|
|
1280
1324
|
if (typeof source !== "string" || !source) continue;
|
|
1281
|
-
const key = `${difyType}:${source}`;
|
|
1282
|
-
if (seen.has(key)) continue;
|
|
1283
|
-
seen.add(key);
|
|
1284
1325
|
candidates.push({ source, type: difyType });
|
|
1285
1326
|
}
|
|
1286
1327
|
return candidates;
|
|
@@ -1306,7 +1347,7 @@ function parseDataUrl(source) {
|
|
|
1306
1347
|
const mimeType = match[1] || "application/octet-stream";
|
|
1307
1348
|
const isBase64 = /(?:^|;)base64/.test(match[2] ?? "");
|
|
1308
1349
|
const buffer = isBase64 ? Buffer.from(match[3], "base64") : percentDecodeBytes(match[3]);
|
|
1309
|
-
return { buffer, fileName:
|
|
1350
|
+
return { buffer, fileName: fallbackFileName(mimeType), mimeType };
|
|
1310
1351
|
}
|
|
1311
1352
|
__name(parseDataUrl, "parseDataUrl");
|
|
1312
1353
|
async function readLocalFile(ctx, source) {
|
|
@@ -1317,7 +1358,7 @@ async function readLocalFile(ctx, source) {
|
|
|
1317
1358
|
}
|
|
1318
1359
|
const buffer = await (0, import_promises.readFile)(filePath);
|
|
1319
1360
|
const mimeType = guessMime((0, import_path.extname)(filePath));
|
|
1320
|
-
return { buffer, fileName: (0, import_path.basename)(filePath) ||
|
|
1361
|
+
return { buffer, fileName: (0, import_path.basename)(filePath) || fallbackFileName(mimeType), mimeType };
|
|
1321
1362
|
}
|
|
1322
1363
|
__name(readLocalFile, "readLocalFile");
|
|
1323
1364
|
async function fetchRemoteFile(ctx, source) {
|
|
@@ -1333,7 +1374,7 @@ async function fetchRemoteFile(ctx, source) {
|
|
|
1333
1374
|
} catch {
|
|
1334
1375
|
}
|
|
1335
1376
|
const mimeType = guessMime((0, import_path.extname)(fileName));
|
|
1336
|
-
return { buffer, fileName: fileName ||
|
|
1377
|
+
return { buffer, fileName: fileName || fallbackFileName(mimeType), mimeType };
|
|
1337
1378
|
} catch (error) {
|
|
1338
1379
|
ctx.logger.warn("下载远程文件失败:%s (%s)", source, error);
|
|
1339
1380
|
return null;
|
|
@@ -1448,29 +1489,73 @@ function resolveMetaField(key, fireCtx, now) {
|
|
|
1448
1489
|
}
|
|
1449
1490
|
}
|
|
1450
1491
|
__name(resolveMetaField, "resolveMetaField");
|
|
1492
|
+
function historyVariableOf(fireCtx, history, limit) {
|
|
1493
|
+
const channelKey = channelKeyOf(fireCtx.platform, fireCtx.channelId);
|
|
1494
|
+
return history.toVariable(history.get(channelKey, limit ?? history.limit));
|
|
1495
|
+
}
|
|
1496
|
+
__name(historyVariableOf, "historyVariableOf");
|
|
1451
1497
|
function resolveSource(source, fireCtx, history, now) {
|
|
1452
|
-
switch (source
|
|
1498
|
+
switch (source.type) {
|
|
1453
1499
|
case "session":
|
|
1454
1500
|
return resolveSessionField(source.key, fireCtx, now);
|
|
1455
1501
|
case "meta":
|
|
1456
1502
|
return resolveMetaField(source.key, fireCtx, now);
|
|
1457
1503
|
case "constant":
|
|
1458
1504
|
return source.value;
|
|
1459
|
-
case "history":
|
|
1460
|
-
|
|
1461
|
-
const limit = source.limit ?? history.limit;
|
|
1462
|
-
return history.toVariable(history.get(channelKey, limit));
|
|
1463
|
-
}
|
|
1505
|
+
case "history":
|
|
1506
|
+
return historyVariableOf(fireCtx, history, source.limit);
|
|
1464
1507
|
default:
|
|
1465
1508
|
return void 0;
|
|
1466
1509
|
}
|
|
1467
1510
|
}
|
|
1468
1511
|
__name(resolveSource, "resolveSource");
|
|
1512
|
+
var META_PLACEHOLDERS = /* @__PURE__ */ new Set(["triggerType", "ruleName", "triggerTime"]);
|
|
1513
|
+
function resolvePlaceholderValue(name2, limit, fireCtx, history, now) {
|
|
1514
|
+
if (name2 === "history") {
|
|
1515
|
+
return historyVariableOf(fireCtx, history, limit);
|
|
1516
|
+
}
|
|
1517
|
+
if (name2 === "isDirect") {
|
|
1518
|
+
return resolveSessionField("isDirect", fireCtx, now);
|
|
1519
|
+
}
|
|
1520
|
+
if (META_PLACEHOLDERS.has(name2)) {
|
|
1521
|
+
return resolveMetaField(name2, fireCtx, now);
|
|
1522
|
+
}
|
|
1523
|
+
return String(resolveSessionField(name2, fireCtx, now));
|
|
1524
|
+
}
|
|
1525
|
+
__name(resolvePlaceholderValue, "resolvePlaceholderValue");
|
|
1526
|
+
var BARE_PLACEHOLDER = new RegExp(`"${JSON_PLACEHOLDER.source}"`, "g");
|
|
1527
|
+
var escapeJsonText = /* @__PURE__ */ __name((value) => JSON.stringify(value).slice(1, -1), "escapeJsonText");
|
|
1528
|
+
function interpolateTemplate(template, fireCtx, history, now) {
|
|
1529
|
+
const resolve = /* @__PURE__ */ __name((name2, limit) => resolvePlaceholderValue(name2, limit ? Number(limit) : void 0, fireCtx, history, now), "resolve");
|
|
1530
|
+
const bareReplaced = template.replace(
|
|
1531
|
+
BARE_PLACEHOLDER,
|
|
1532
|
+
(_match, name2, limit) => JSON.stringify(resolve(name2, limit))
|
|
1533
|
+
);
|
|
1534
|
+
return bareReplaced.replace(
|
|
1535
|
+
JSON_PLACEHOLDER,
|
|
1536
|
+
(_match, name2, limit) => escapeJsonText(String(resolve(name2, limit)))
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
__name(interpolateTemplate, "interpolateTemplate");
|
|
1540
|
+
function resolveJsonObject(template, fireCtx, history, now) {
|
|
1541
|
+
try {
|
|
1542
|
+
const parsed = JSON.parse(interpolateTemplate(template, fireCtx, history, now));
|
|
1543
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
1544
|
+
} catch {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
__name(resolveJsonObject, "resolveJsonObject");
|
|
1469
1549
|
function resolveVariables(rule, fireCtx, history) {
|
|
1470
1550
|
const inputs = {};
|
|
1471
1551
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1472
1552
|
for (const binding of rule.variables) {
|
|
1473
|
-
if (
|
|
1553
|
+
if (binding.source.type !== "json") continue;
|
|
1554
|
+
const object = resolveJsonObject(binding.source.template, fireCtx, history, now);
|
|
1555
|
+
if (object) Object.assign(inputs, object);
|
|
1556
|
+
}
|
|
1557
|
+
for (const binding of rule.variables) {
|
|
1558
|
+
if (binding.source.type === "json") continue;
|
|
1474
1559
|
const value = resolveSource(binding.source, fireCtx, history, now);
|
|
1475
1560
|
if (value !== void 0) inputs[binding.field] = value;
|
|
1476
1561
|
}
|
|
@@ -1486,12 +1571,13 @@ function splitText(text, maxLength) {
|
|
|
1486
1571
|
const lines = text.split("\n");
|
|
1487
1572
|
const segments = [];
|
|
1488
1573
|
let current = "";
|
|
1574
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
1575
|
+
if (current) segments.push(current);
|
|
1576
|
+
}, "flush");
|
|
1489
1577
|
for (const line of lines) {
|
|
1490
1578
|
if (line.length > maxLength) {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
current = "";
|
|
1494
|
-
}
|
|
1579
|
+
flush();
|
|
1580
|
+
current = "";
|
|
1495
1581
|
for (let i = 0; i < line.length; i += maxLength) {
|
|
1496
1582
|
segments.push(line.slice(i, i + maxLength));
|
|
1497
1583
|
}
|
|
@@ -1505,7 +1591,7 @@ function splitText(text, maxLength) {
|
|
|
1505
1591
|
${line}` : line;
|
|
1506
1592
|
}
|
|
1507
1593
|
}
|
|
1508
|
-
|
|
1594
|
+
flush();
|
|
1509
1595
|
return segments;
|
|
1510
1596
|
}
|
|
1511
1597
|
__name(splitText, "splitText");
|
|
@@ -1609,22 +1695,7 @@ var RequestRunner = class {
|
|
|
1609
1695
|
inputs: resolveVariables(rule, fireCtx, this.history),
|
|
1610
1696
|
uploadedFiles: []
|
|
1611
1697
|
};
|
|
1612
|
-
|
|
1613
|
-
if (upload.enabled) {
|
|
1614
|
-
const sessionCandidates = fireCtx.session ? extractFileCandidates(fireCtx.session) : [];
|
|
1615
|
-
const urlCandidates = (fireCtx.imageUrls ?? []).filter((url) => !sessionCandidates.some((candidate) => candidate.source === url)).map((url) => ({ source: url, type: "image" }));
|
|
1616
|
-
const { files, dropped } = await uploadCandidates(
|
|
1617
|
-
this.ctx,
|
|
1618
|
-
client,
|
|
1619
|
-
[...sessionCandidates, ...urlCandidates],
|
|
1620
|
-
requestCtx.difyUser,
|
|
1621
|
-
{ maxCount: upload.maxCount, maxSizeMb: upload.maxMb }
|
|
1622
|
-
);
|
|
1623
|
-
requestCtx.uploadedFiles = files;
|
|
1624
|
-
if (dropped.length) {
|
|
1625
|
-
fireCtx.queryText = annotateDroppedPlaceholders(fireCtx.queryText, dropped);
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1698
|
+
await this.uploadTriggerFiles(fireCtx, client, requestCtx);
|
|
1628
1699
|
let reply;
|
|
1629
1700
|
try {
|
|
1630
1701
|
reply = await this.dispatch(app, client, fireCtx, requestCtx);
|
|
@@ -1648,6 +1719,30 @@ var RequestRunner = class {
|
|
|
1648
1719
|
});
|
|
1649
1720
|
await this.deliver(fireCtx, messages);
|
|
1650
1721
|
}
|
|
1722
|
+
/**
|
|
1723
|
+
* 文件上传管线:消息触发取会话元素候选(含音视频文件),窗口类触发(合并/密度/定时)
|
|
1724
|
+
* 经 fireCtx.imageUrls 补齐相关消息的图片;同 URL 重复由上传入口去重;
|
|
1725
|
+
* 被丢弃图片在 query 的占位符上注明已丢弃,模型可感知缺图原因。
|
|
1726
|
+
* 注意:workflow 的「文件型输入变量」(inputs 内嵌文件对象)不在本管线范围内,
|
|
1727
|
+
* 顶层 files 已覆盖视觉模型组合输入的主场景。
|
|
1728
|
+
*/
|
|
1729
|
+
async uploadTriggerFiles(fireCtx, client, requestCtx) {
|
|
1730
|
+
const upload = resolveUploadConfig(this.config);
|
|
1731
|
+
if (!upload.enabled) return;
|
|
1732
|
+
const sessionCandidates = fireCtx.session ? extractFileCandidates(fireCtx.session) : [];
|
|
1733
|
+
const urlCandidates = (fireCtx.imageUrls ?? []).map((url) => ({ source: url, type: "image" }));
|
|
1734
|
+
const { files, dropped } = await uploadCandidates(
|
|
1735
|
+
this.ctx,
|
|
1736
|
+
client,
|
|
1737
|
+
[...sessionCandidates, ...urlCandidates],
|
|
1738
|
+
requestCtx.difyUser,
|
|
1739
|
+
{ maxCount: upload.maxCount, maxSizeMb: upload.maxMb }
|
|
1740
|
+
);
|
|
1741
|
+
requestCtx.uploadedFiles = files;
|
|
1742
|
+
if (dropped.length) {
|
|
1743
|
+
fireCtx.queryText = annotateDroppedPlaceholders(fireCtx.queryText, dropped);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1651
1746
|
/** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
|
|
1652
1747
|
shouldQuoteReply(fireCtx, startedAt) {
|
|
1653
1748
|
const quote = resolveQuoteConfig(this.config);
|
|
@@ -1683,12 +1778,16 @@ var RequestRunner = class {
|
|
|
1683
1778
|
}
|
|
1684
1779
|
return { text: response.answer, files: [] };
|
|
1685
1780
|
}
|
|
1781
|
+
/** 逐条发送,单条失败仅告警不中断 */
|
|
1782
|
+
async sendAll(messages, send) {
|
|
1783
|
+
for (const message of messages) {
|
|
1784
|
+
await send(message).catch((error) => this.ctx.logger.warn(error));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1686
1787
|
async deliver(fireCtx, messages) {
|
|
1687
1788
|
if (!messages.length) return;
|
|
1688
1789
|
if (fireCtx.session) {
|
|
1689
|
-
|
|
1690
|
-
await fireCtx.session.send(message).catch((error) => this.ctx.logger.warn(error));
|
|
1691
|
-
}
|
|
1790
|
+
await this.sendAll(messages, (message) => fireCtx.session.send(message));
|
|
1692
1791
|
return;
|
|
1693
1792
|
}
|
|
1694
1793
|
const bots = Object.values(this.ctx.bots);
|
|
@@ -1697,9 +1796,7 @@ var RequestRunner = class {
|
|
|
1697
1796
|
this.ctx.logger.warn("平台 %s 上没有可用 bot,无法发送主动消息", fireCtx.platform);
|
|
1698
1797
|
return;
|
|
1699
1798
|
}
|
|
1700
|
-
|
|
1701
|
-
await bot.sendMessage(fireCtx.channelId, message).catch((error) => this.ctx.logger.warn(error));
|
|
1702
|
-
}
|
|
1799
|
+
await this.sendAll(messages, (message) => bot.sendMessage(fireCtx.channelId, message));
|
|
1703
1800
|
}
|
|
1704
1801
|
};
|
|
1705
1802
|
|
|
@@ -1767,23 +1864,34 @@ var ConversationStore = class {
|
|
|
1767
1864
|
async saveConversationId(bindingKey, difyConversationId) {
|
|
1768
1865
|
await this.ctx.database.upsert("dify_conversation", [{ bindingKey, difyConversationId }]);
|
|
1769
1866
|
}
|
|
1770
|
-
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser
|
|
1867
|
+
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser 用)。
|
|
1868
|
+
* 段位定位假设 appId / platform / selfId 不含冒号(平台名与应用标识按约定不含;
|
|
1869
|
+
* Matrix 的 bot id 含冒号属键格式改造范畴,当前不区分) */
|
|
1771
1870
|
parseBindingKey(bindingKey) {
|
|
1772
|
-
const
|
|
1871
|
+
const segments = bindingKey.split(":");
|
|
1872
|
+
if (segments.length === 2 && segments[1] === "global") {
|
|
1873
|
+
return { appId: segments[0], platform: "", scope: "global", ids: [] };
|
|
1874
|
+
}
|
|
1875
|
+
const [appId, platform, , scope, ...ids] = segments;
|
|
1773
1876
|
return { appId, platform, scope, ids };
|
|
1774
1877
|
}
|
|
1878
|
+
/** scope 之后的 id 片段拼回整体(键内 id 可能内嵌冒号,消费方按语义整体比较) */
|
|
1879
|
+
joinedIdsOf(bindingKey) {
|
|
1880
|
+
return this.parseBindingKey(bindingKey).ids.join(":");
|
|
1881
|
+
}
|
|
1775
1882
|
/** 由 bindingKey 还原删除 Dify 会话所需的 difyUser(与 computeDifyUser 对齐) */
|
|
1776
1883
|
difyUserOfBindingKey(bindingKey) {
|
|
1777
|
-
const { platform, scope
|
|
1884
|
+
const { platform, scope } = this.parseBindingKey(bindingKey);
|
|
1885
|
+
const ids = this.joinedIdsOf(bindingKey);
|
|
1778
1886
|
switch (scope) {
|
|
1779
1887
|
case "global":
|
|
1780
1888
|
return "koishi";
|
|
1781
1889
|
case "private":
|
|
1782
|
-
return `koishi:${platform}:private:${ids
|
|
1890
|
+
return `koishi:${platform}:private:${ids}`;
|
|
1783
1891
|
case "guild":
|
|
1784
1892
|
case "channel":
|
|
1785
1893
|
case "user":
|
|
1786
|
-
return `koishi:${platform}:${ids
|
|
1894
|
+
return `koishi:${platform}:${ids}`;
|
|
1787
1895
|
default:
|
|
1788
1896
|
return `koishi:${platform}`;
|
|
1789
1897
|
}
|
|
@@ -1804,12 +1912,13 @@ var ConversationStore = class {
|
|
|
1804
1912
|
const [channelPlatform, channelId] = splitKey(filter.channelKey ?? "");
|
|
1805
1913
|
const matched = rows.filter((row) => {
|
|
1806
1914
|
if (filter.bindingKey) return row.bindingKey === filter.bindingKey;
|
|
1807
|
-
const { platform, scope
|
|
1915
|
+
const { platform, scope } = this.parseBindingKey(row.bindingKey);
|
|
1808
1916
|
if (platform !== guildPlatform) return false;
|
|
1809
|
-
|
|
1810
|
-
if (scope === "
|
|
1811
|
-
if (scope === "
|
|
1812
|
-
if (scope === "
|
|
1917
|
+
const joinedIds = this.joinedIdsOf(row.bindingKey);
|
|
1918
|
+
if (scope === "guild") return joinedIds === guildId;
|
|
1919
|
+
if (scope === "user") return joinedIds.startsWith(`${guildId}:`);
|
|
1920
|
+
if (scope === "channel") return platform === channelPlatform && joinedIds === channelId;
|
|
1921
|
+
if (scope === "private") return filter.isDirect === true && joinedIds === filter.userId;
|
|
1813
1922
|
return false;
|
|
1814
1923
|
});
|
|
1815
1924
|
let reset = 0;
|
|
@@ -1852,14 +1961,14 @@ __name(defineTables, "defineTables");
|
|
|
1852
1961
|
// src/commands/misc.ts
|
|
1853
1962
|
function registerMiscCommands(parent, conversationStore) {
|
|
1854
1963
|
parent.subcommand(".conv.reset", { authority: 3 }).action(async ({ session }) => {
|
|
1855
|
-
const
|
|
1964
|
+
const resetCount = await conversationStore.resetWhere({
|
|
1856
1965
|
guildKey: guildKeyOf(session.platform, session.guildId, session.channelId),
|
|
1857
1966
|
channelKey: channelKeyOf(session.platform, session.channelId),
|
|
1858
1967
|
isDirect: session.isDirect,
|
|
1859
1968
|
userId: session.userId
|
|
1860
1969
|
});
|
|
1861
|
-
if (!
|
|
1862
|
-
return session.text("dify-ai.commands.conv-reset", [
|
|
1970
|
+
if (!resetCount) return session.text("dify-ai.commands.conv-empty");
|
|
1971
|
+
return session.text("dify-ai.commands.conv-reset", [resetCount]);
|
|
1863
1972
|
});
|
|
1864
1973
|
}
|
|
1865
1974
|
__name(registerMiscCommands, "registerMiscCommands");
|
|
@@ -1869,6 +1978,7 @@ var name = "dify-ai";
|
|
|
1869
1978
|
var inject = { required: ["database"] };
|
|
1870
1979
|
var PROBE_TIMEOUT_MS = 1e4;
|
|
1871
1980
|
function apply(ctx, config) {
|
|
1981
|
+
const startedAt = Date.now();
|
|
1872
1982
|
ctx.i18n.define("zh-CN", zh_CN_default);
|
|
1873
1983
|
ctx.i18n.define("en-US", en_US_default);
|
|
1874
1984
|
defineTables(ctx);
|
|
@@ -1880,7 +1990,11 @@ function apply(ctx, config) {
|
|
|
1880
1990
|
const engine = new TriggerEngine(ctx, runner, history, rules, cooldown);
|
|
1881
1991
|
ctx.on("ready", () => {
|
|
1882
1992
|
engine.start();
|
|
1883
|
-
|
|
1993
|
+
ctx.logger.info("插件装配完成,耗时 %dms", Date.now() - startedAt);
|
|
1994
|
+
const probeStart = Date.now();
|
|
1995
|
+
void probeAppTypes(ctx, apps, appTypes).then(() => {
|
|
1996
|
+
ctx.logger.info("应用类型探测完成,耗时 %dms", Date.now() - probeStart);
|
|
1997
|
+
});
|
|
1884
1998
|
});
|
|
1885
1999
|
const admin = ctx.command("dify", { authority: 3 });
|
|
1886
2000
|
registerMiscCommands(admin, conversationStore);
|
package/lib/requester/files.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ interface FilePayload {
|
|
|
13
13
|
}
|
|
14
14
|
/** mime → Dify 文件类型 */
|
|
15
15
|
export declare function mimeToFileType(mimeType?: string): DifyFileType;
|
|
16
|
-
/**
|
|
16
|
+
/** 从消息元素中提取可上传的文件候选(图片/语音/视频/附件);同源去重由上传入口统一处理 */
|
|
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>;
|
|
@@ -27,6 +27,6 @@ export interface UploadResult {
|
|
|
27
27
|
files: DifyInputFileObject[];
|
|
28
28
|
dropped: string[];
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/** 上传一批候选(入口按来源 URL 去重,再按数量上限截断、逐个上传) */
|
|
31
31
|
export declare function uploadCandidates(ctx: Context, client: DifyClient, candidates: FileCandidate[], difyUser: string, limit: UploadLimit): Promise<UploadResult>;
|
|
32
32
|
export {};
|
|
@@ -28,10 +28,20 @@ export declare class RequestRunner {
|
|
|
28
28
|
/** 应用类型:schema 显式指定优先,auto 时用探测结果(未探测到回退 chat) */
|
|
29
29
|
private appTypeOf;
|
|
30
30
|
run(fireCtx: FireContext): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* 文件上传管线:消息触发取会话元素候选(含音视频文件),窗口类触发(合并/密度/定时)
|
|
33
|
+
* 经 fireCtx.imageUrls 补齐相关消息的图片;同 URL 重复由上传入口去重;
|
|
34
|
+
* 被丢弃图片在 query 的占位符上注明已丢弃,模型可感知缺图原因。
|
|
35
|
+
* 注意:workflow 的「文件型输入变量」(inputs 内嵌文件对象)不在本管线范围内,
|
|
36
|
+
* 顶层 files 已覆盖视觉模型组合输入的主场景。
|
|
37
|
+
*/
|
|
38
|
+
private uploadTriggerFiles;
|
|
31
39
|
/** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
|
|
32
40
|
private shouldQuoteReply;
|
|
33
41
|
/** 按应用类型分流:workflow/completion 无会话,chat 系续接 conversation_id */
|
|
34
42
|
private dispatch;
|
|
43
|
+
/** 逐条发送,单条失败仅告警不中断 */
|
|
44
|
+
private sendAll;
|
|
35
45
|
private deliver;
|
|
36
46
|
}
|
|
37
47
|
export {};
|
|
@@ -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>;
|
|
@@ -27,8 +27,12 @@ export declare class ConversationStore {
|
|
|
27
27
|
computeDifyUser(rule: DifyRule, fireCtx: FireContext): string;
|
|
28
28
|
getConversationId(bindingKey: string): Promise<string | undefined>;
|
|
29
29
|
saveConversationId(bindingKey: string, difyConversationId: string): Promise<void>;
|
|
30
|
-
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser
|
|
30
|
+
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser 用)。
|
|
31
|
+
* 段位定位假设 appId / platform / selfId 不含冒号(平台名与应用标识按约定不含;
|
|
32
|
+
* Matrix 的 bot id 含冒号属键格式改造范畴,当前不区分) */
|
|
31
33
|
private parseBindingKey;
|
|
34
|
+
/** scope 之后的 id 片段拼回整体(键内 id 可能内嵌冒号,消费方按语义整体比较) */
|
|
35
|
+
private joinedIdsOf;
|
|
32
36
|
/** 由 bindingKey 还原删除 Dify 会话所需的 difyUser(与 computeDifyUser 对齐) */
|
|
33
37
|
private difyUserOfBindingKey;
|
|
34
38
|
/**
|
|
@@ -38,9 +38,11 @@ export declare class TriggerCooldown {
|
|
|
38
38
|
isCooling(channelKey: string, ruleName: string, lane: CooldownLane, now: number): boolean;
|
|
39
39
|
/** 滑动窗口顺延:截止推到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
|
|
40
40
|
private slide;
|
|
41
|
-
/**
|
|
41
|
+
/** 滑动窗口顺延:sliding 模式且通道未到期才执行(已到期的不复活) */
|
|
42
|
+
private slideIfActive;
|
|
43
|
+
/** 滑动窗口顺延 */
|
|
42
44
|
extend(channelKey: string, ruleName: string, lane: CooldownLane, now: number): void;
|
|
43
|
-
/**
|
|
45
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动) */
|
|
44
46
|
extendDensityLanes(channelKey: string, now: number): void;
|
|
45
47
|
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
46
48
|
bufferMessage(channelKey: string, ruleName: string, message: PendingMessage): void;
|
package/lib/trigger/pool.d.ts
CHANGED
|
@@ -13,8 +13,7 @@ export interface ChannelPool {
|
|
|
13
13
|
}
|
|
14
14
|
export interface DensityFire {
|
|
15
15
|
rule: DifyRule;
|
|
16
|
-
/**
|
|
17
|
-
condition: DensityCondition;
|
|
16
|
+
/** 命中条件在规则内的下标(密度冷却通道键) */
|
|
18
17
|
conditionIndex: number;
|
|
19
18
|
/** 触发时取走的该频道全池消息(含 bot 行与时效外条目) */
|
|
20
19
|
entries: HistoryEntry[];
|
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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Session } from 'koishi';
|
|
1
|
+
import { h, Session } from 'koishi';
|
|
2
2
|
/** 图片占位符携带的 URL 摘要长度(hex 位) */
|
|
3
3
|
export declare const IMAGE_HASH_LENGTH = 8;
|
|
4
4
|
/**
|
|
@@ -20,6 +20,8 @@ interface ExtractVerbatimOptions {
|
|
|
20
20
|
/** 跳过 @ 机器人自身的渲染(content 变量语义:@bot 是触发结构而非消息内容) */
|
|
21
21
|
excludeBotAt?: boolean;
|
|
22
22
|
}
|
|
23
|
+
/** 媒体元素的源地址(url 或 src 属性,未定形态原样返回由调用方守卫) */
|
|
24
|
+
export declare function imageSourceOf(element: h): unknown;
|
|
23
25
|
/**
|
|
24
26
|
* 消息原貌文本:text 元素按序拼接,@ 元素渲染为 @名字(缺名字用 ID),
|
|
25
27
|
* img/image 元素渲染为 [image:URL摘要]——统一格式行(query/池/历史)用默认形态,
|
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.10",
|
|
5
5
|
"author": "PinkElysia",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/PinkElysiaDev/dify-ai-plugin-for-Koishi#readme",
|