koishi-plugin-dify-ai 0.1.9 → 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 +13 -25
- package/lib/index.js +188 -168
- package/lib/requester/files.d.ts +2 -2
- package/lib/requester/runner.d.ts +10 -0
- 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/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
|
@@ -71,13 +71,6 @@ export interface RuleEntry {
|
|
|
71
71
|
scope: ScopeRow[];
|
|
72
72
|
variables: VariableEntry[];
|
|
73
73
|
}
|
|
74
|
-
/** 引用触发消息的条件分支形态:关闭时空分支,开启时按耗时阈值决定是否引用 */
|
|
75
|
-
export type QuoteEntry = {
|
|
76
|
-
mode: 'off';
|
|
77
|
-
} | {
|
|
78
|
-
mode: 'on';
|
|
79
|
-
minElapsedSeconds: number;
|
|
80
|
-
};
|
|
81
74
|
/** 触发后冷却的条件分支形态 */
|
|
82
75
|
export type CooldownEntry = {
|
|
83
76
|
mode: 'off';
|
|
@@ -89,33 +82,29 @@ export type CooldownEntry = {
|
|
|
89
82
|
minSeconds?: number;
|
|
90
83
|
maxSeconds?: number;
|
|
91
84
|
};
|
|
92
|
-
/** 上传消息文件的条件分支形态:关闭时空分支,开启时限制数量与单文件大小 */
|
|
93
|
-
export type UploadEntry = {
|
|
94
|
-
mode: 'off';
|
|
95
|
-
} | {
|
|
96
|
-
mode: 'on';
|
|
97
|
-
maxCount?: number;
|
|
98
|
-
maxSizeMb?: number;
|
|
99
|
-
};
|
|
100
85
|
export interface Config {
|
|
101
86
|
apps: AppConfigEntry[];
|
|
102
87
|
rules: RuleEntry[];
|
|
103
88
|
parseElements: boolean;
|
|
104
|
-
quote:
|
|
89
|
+
quote: boolean;
|
|
90
|
+
minElapsedSeconds: number;
|
|
105
91
|
maxSegmentLength: number;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
quoteTrigger?: boolean;
|
|
92
|
+
uploadFiles: boolean;
|
|
93
|
+
maxCount: number;
|
|
94
|
+
maxSizeMb: number;
|
|
110
95
|
requestTimeout: number;
|
|
111
96
|
historyLimit: number;
|
|
112
97
|
sendErrors: boolean;
|
|
113
98
|
cooldown: CooldownEntry;
|
|
114
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 上传消息文件:布尔开关驱动分支(开=可配数量与单文件大小上限,关=空分支),官方 union-tagged 写法。
|
|
102
|
+
* 上限默认 -1 规避 0 被表单默认值机制吞掉(-1 与 0 同为不限,仍受 Dify 各类型上限约束)。
|
|
103
|
+
*/
|
|
115
104
|
export declare const Config: Schema<Config>;
|
|
116
105
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
106
|
+
* 上传消息文件配置(平铺开关,默认关闭:显式开启才上传)。数量与单文件大小
|
|
107
|
+
* 0 与 -1 均为不限,大小仍与 Dify 各类型上限取较小值。
|
|
119
108
|
*/
|
|
120
109
|
export declare function resolveUploadConfig(config: Config): {
|
|
121
110
|
enabled: boolean;
|
|
@@ -123,9 +112,8 @@ export declare function resolveUploadConfig(config: Config): {
|
|
|
123
112
|
maxMb: number;
|
|
124
113
|
};
|
|
125
114
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* 阈值 -1 与 0 同为总是引用(比较 elapsed >= 阈值 自然兼容)。
|
|
115
|
+
* 引用触发消息配置(平铺开关,含 0.1.x quoteTrigger 透传迁移:
|
|
116
|
+
* 旧布尔被保留且开关与阈值均未被改动时以旧布尔为准)。阈值 -1 与 0 同为总是引用。
|
|
129
117
|
*/
|
|
130
118
|
export declare function resolveQuoteConfig(config: Config): {
|
|
131
119
|
enabled: boolean;
|
package/lib/index.js
CHANGED
|
@@ -147,26 +147,39 @@ var variableSchema = import_koishi.Schema.union([
|
|
|
147
147
|
template: import_koishi.Schema.string().role("textarea").required().description("JSON 对象")
|
|
148
148
|
}).default({ source: "json", template: "" }).description("JSON 对象")
|
|
149
149
|
]);
|
|
150
|
-
var
|
|
151
|
-
import_koishi.Schema.
|
|
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([
|
|
159
|
+
import_koishi.Schema.object({
|
|
160
|
+
quote: switchSchema(true).description("引用触发消息")
|
|
161
|
+
}),
|
|
162
|
+
import_koishi.Schema.union([
|
|
152
163
|
import_koishi.Schema.object({
|
|
153
|
-
|
|
164
|
+
quote: import_koishi.Schema.const(true),
|
|
165
|
+
minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1).description("回复耗时多少秒才引用,0 总是引用")
|
|
154
166
|
}),
|
|
155
|
-
import_koishi.Schema.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}),
|
|
160
|
-
import_koishi.Schema.object({})
|
|
161
|
-
])
|
|
162
|
-
]),
|
|
163
|
-
import_koishi.Schema.boolean(),
|
|
164
|
-
import_koishi.Schema.object({ mode: import_koishi.Schema.const("off").required() }),
|
|
167
|
+
import_koishi.Schema.object({})
|
|
168
|
+
])
|
|
169
|
+
]);
|
|
170
|
+
var uploadSwitchSchema = import_koishi.Schema.intersect([
|
|
165
171
|
import_koishi.Schema.object({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
])
|
|
182
|
+
]);
|
|
170
183
|
var cooldownSchema = import_koishi.Schema.union([
|
|
171
184
|
import_koishi.Schema.object({
|
|
172
185
|
mode: import_koishi.Schema.const("off").required()
|
|
@@ -181,28 +194,6 @@ var cooldownSchema = import_koishi.Schema.union([
|
|
|
181
194
|
maxSeconds: import_koishi.Schema.natural().default(120).min(1).description("最大冷却秒数")
|
|
182
195
|
}).default({ mode: "sliding", minSeconds: 30, maxSeconds: 120 }).description("滑动窗口")
|
|
183
196
|
]);
|
|
184
|
-
var uploadSchema = import_koishi.Schema.union([
|
|
185
|
-
import_koishi.Schema.intersect([
|
|
186
|
-
import_koishi.Schema.object({
|
|
187
|
-
enabled: import_koishi.Schema.boolean().default(true).description("开启")
|
|
188
|
-
}),
|
|
189
|
-
import_koishi.Schema.union([
|
|
190
|
-
import_koishi.Schema.object({
|
|
191
|
-
enabled: import_koishi.Schema.const(true).required(),
|
|
192
|
-
maxCount: import_koishi.Schema.number().min(-1).default(-1).description("数量上限,0 不限"),
|
|
193
|
-
maxSizeMb: import_koishi.Schema.number().min(-1).default(-1).description("单文件大小上限 MB,0 不限")
|
|
194
|
-
}),
|
|
195
|
-
import_koishi.Schema.object({})
|
|
196
|
-
])
|
|
197
|
-
]),
|
|
198
|
-
import_koishi.Schema.boolean(),
|
|
199
|
-
import_koishi.Schema.object({ mode: import_koishi.Schema.const("off").required() }),
|
|
200
|
-
import_koishi.Schema.object({
|
|
201
|
-
mode: import_koishi.Schema.const("on").required(),
|
|
202
|
-
maxCount: import_koishi.Schema.number().min(-1).default(-1),
|
|
203
|
-
maxSizeMb: import_koishi.Schema.number().min(-1).default(-1)
|
|
204
|
-
})
|
|
205
|
-
]).default({ enabled: true, maxCount: -1, maxSizeMb: -1 });
|
|
206
197
|
var Config = import_koishi.Schema.intersect([
|
|
207
198
|
import_koishi.Schema.object({
|
|
208
199
|
apps: import_koishi.Schema.array(
|
|
@@ -249,10 +240,10 @@ var Config = import_koishi.Schema.intersect([
|
|
|
249
240
|
}).description("触发规则"),
|
|
250
241
|
import_koishi.Schema.object({
|
|
251
242
|
parseElements: import_koishi.Schema.boolean().default(false).description("解析回复中的消息元素"),
|
|
252
|
-
|
|
253
|
-
maxSegmentLength: import_koishi.Schema.number().min(-1).default(-1).description("分段长度,0 不分段"),
|
|
254
|
-
uploadFiles: uploadSchema.description("上传消息中的文件")
|
|
243
|
+
maxSegmentLength: import_koishi.Schema.number().min(-1).default(-1).description("分段长度,0 不分段")
|
|
255
244
|
}).description("回复行为"),
|
|
245
|
+
quoteSwitchSchema,
|
|
246
|
+
uploadSwitchSchema,
|
|
256
247
|
import_koishi.Schema.object({
|
|
257
248
|
requestTimeout: import_koishi.Schema.natural().role("ms").default(3e5).description("请求超时"),
|
|
258
249
|
historyLimit: import_koishi.Schema.natural().default(20).min(1).description("历史消息条数"),
|
|
@@ -374,30 +365,22 @@ function toVariable(entry) {
|
|
|
374
365
|
}
|
|
375
366
|
__name(toVariable, "toVariable");
|
|
376
367
|
function resolveUploadConfig(config) {
|
|
377
|
-
|
|
378
|
-
if (typeof upload === "boolean") {
|
|
379
|
-
return upload ? { enabled: true, maxCount: 0, maxMb: 0 } : { enabled: false, maxCount: 0, maxMb: 0 };
|
|
380
|
-
}
|
|
381
|
-
if (upload?.mode === "off" || upload?.enabled === false) {
|
|
382
|
-
return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
383
|
-
}
|
|
368
|
+
if (config.uploadFiles !== true) return { enabled: false, maxCount: 0, maxMb: 0 };
|
|
384
369
|
return {
|
|
385
370
|
enabled: true,
|
|
386
|
-
maxCount: Number(
|
|
387
|
-
maxMb: Number(
|
|
371
|
+
maxCount: Number(config.maxCount) || 0,
|
|
372
|
+
maxMb: Number(config.maxSizeMb) || 0
|
|
388
373
|
};
|
|
389
374
|
}
|
|
390
375
|
__name(resolveUploadConfig, "resolveUploadConfig");
|
|
391
376
|
function resolveQuoteConfig(config) {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (typeof config.quoteTrigger === "boolean" && untouched) {
|
|
398
|
-
return { enabled: config.quoteTrigger, minElapsedSeconds: 0 };
|
|
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 };
|
|
399
382
|
}
|
|
400
|
-
return { enabled: quote
|
|
383
|
+
return { enabled: config.quote !== false, minElapsedSeconds: threshold };
|
|
401
384
|
}
|
|
402
385
|
__name(resolveQuoteConfig, "resolveQuoteConfig");
|
|
403
386
|
function toCooldown(entry) {
|
|
@@ -413,6 +396,7 @@ function toCooldown(entry) {
|
|
|
413
396
|
}
|
|
414
397
|
__name(toCooldown, "toCooldown");
|
|
415
398
|
function normalizeConfig(config) {
|
|
399
|
+
config ??= {};
|
|
416
400
|
const apps = /* @__PURE__ */ new Map();
|
|
417
401
|
for (const app of config.apps ?? []) {
|
|
418
402
|
if (app.name?.trim() && !apps.has(app.name)) apps.set(app.name, app);
|
|
@@ -470,11 +454,11 @@ var DifyClient = class {
|
|
|
470
454
|
buildUrl(path) {
|
|
471
455
|
return this.app.apiURL.replace(/\/+$/, "") + path;
|
|
472
456
|
}
|
|
457
|
+
authHeaders() {
|
|
458
|
+
return { Authorization: `Bearer ${this.app.apiKey}` };
|
|
459
|
+
}
|
|
473
460
|
buildJsonHeaders() {
|
|
474
|
-
return {
|
|
475
|
-
Authorization: `Bearer ${this.app.apiKey}`,
|
|
476
|
-
"Content-Type": "application/json"
|
|
477
|
-
};
|
|
461
|
+
return { ...this.authHeaders(), "Content-Type": "application/json" };
|
|
478
462
|
}
|
|
479
463
|
errorFrom(status, data, statusText = "") {
|
|
480
464
|
const isObject = typeof data === "object" && data !== null;
|
|
@@ -524,7 +508,7 @@ var DifyClient = class {
|
|
|
524
508
|
);
|
|
525
509
|
form.set("user", user);
|
|
526
510
|
const res = await this.http("POST", this.buildUrl("/files/upload"), {
|
|
527
|
-
headers:
|
|
511
|
+
headers: this.authHeaders(),
|
|
528
512
|
data: form,
|
|
529
513
|
timeout: this.timeout,
|
|
530
514
|
validateStatus: /* @__PURE__ */ __name(() => true, "validateStatus")
|
|
@@ -580,6 +564,14 @@ function annotateDroppedPlaceholders(text, droppedUrls) {
|
|
|
580
564
|
return result;
|
|
581
565
|
}
|
|
582
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");
|
|
583
575
|
function elementsOf(session) {
|
|
584
576
|
if (session.elements?.length) return session.elements;
|
|
585
577
|
try {
|
|
@@ -599,8 +591,8 @@ function extractTextWithPlaceholders(session, options = {}) {
|
|
|
599
591
|
if (options.excludeBotAt && element.attrs?.["id"] === session.selfId) continue;
|
|
600
592
|
const name2 = element.attrs?.["name"] ?? element.attrs?.["id"];
|
|
601
593
|
if (name2) parts.push(`@${name2}`);
|
|
602
|
-
} else if (element
|
|
603
|
-
parts.push(imagePlaceholder(element
|
|
594
|
+
} else if (isImageElement(element)) {
|
|
595
|
+
parts.push(imagePlaceholder(imageSourceOf(element)));
|
|
604
596
|
}
|
|
605
597
|
}
|
|
606
598
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
@@ -609,8 +601,8 @@ __name(extractTextWithPlaceholders, "extractTextWithPlaceholders");
|
|
|
609
601
|
function extractImageUrls(session) {
|
|
610
602
|
const urls = [];
|
|
611
603
|
for (const element of elementsOf(session)) {
|
|
612
|
-
if (element
|
|
613
|
-
const source = element
|
|
604
|
+
if (!isImageElement(element)) continue;
|
|
605
|
+
const source = imageSourceOf(element);
|
|
614
606
|
if (typeof source === "string" && source) urls.push(source);
|
|
615
607
|
}
|
|
616
608
|
return urls;
|
|
@@ -629,7 +621,8 @@ __name(formatTimestamp, "formatTimestamp");
|
|
|
629
621
|
function formatHistoryEntry(entry) {
|
|
630
622
|
const quote = entry.quote ? `(引用了 [${formatTimestamp(entry.quote.timestamp)}] ${entry.quote.username} (${entry.quote.userId}) 说:${entry.quote.content})` : "";
|
|
631
623
|
const messageId = entry.messageId ? ` [msg:${entry.messageId}]` : "";
|
|
632
|
-
|
|
624
|
+
const separator = quote ? "" : " ";
|
|
625
|
+
return `[${formatTimestamp(entry.timestamp)}] ${entry.username} (${entry.userId})${messageId}${quote}${separator}说:${entry.content}`;
|
|
633
626
|
}
|
|
634
627
|
__name(formatHistoryEntry, "formatHistoryEntry");
|
|
635
628
|
function formatHistoryEntries(entries) {
|
|
@@ -700,7 +693,10 @@ function hasAt(session, id) {
|
|
|
700
693
|
return session.elements?.some((element) => element.type === "at" && element.attrs?.["id"] === id) ?? false;
|
|
701
694
|
}
|
|
702
695
|
__name(hasAt, "hasAt");
|
|
703
|
-
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
|
+
);
|
|
704
700
|
function stripPrefixKeyword(verbatim, keyword) {
|
|
705
701
|
const prefix = NON_TEXT_PREFIX.exec(verbatim)?.[0] ?? "";
|
|
706
702
|
const rest = verbatim.slice(prefix.length);
|
|
@@ -760,8 +756,7 @@ function matchScope(scope, session) {
|
|
|
760
756
|
];
|
|
761
757
|
if (dimensions.some(([allowed, value]) => allowed?.length && !allowed.includes(value))) return false;
|
|
762
758
|
if (scope.excludeUsers?.length && scope.excludeUsers.includes(session.userId)) return false;
|
|
763
|
-
if (scope.isDirect
|
|
764
|
-
if (scope.isDirect === false && session.isDirect) return false;
|
|
759
|
+
if (scope.isDirect != null && scope.isDirect !== !!session.isDirect) return false;
|
|
765
760
|
return true;
|
|
766
761
|
}
|
|
767
762
|
__name(matchScope, "matchScope");
|
|
@@ -841,7 +836,7 @@ var MessagePool = class {
|
|
|
841
836
|
if (skip?.(rule, condition, index)) continue;
|
|
842
837
|
if (this.countQualified(pool, rule, condition, now) >= condition.densityCount) {
|
|
843
838
|
this.channels.delete(key);
|
|
844
|
-
return [{ rule,
|
|
839
|
+
return [{ rule, conditionIndex: index, entries: pool.entries }];
|
|
845
840
|
}
|
|
846
841
|
}
|
|
847
842
|
}
|
|
@@ -932,13 +927,13 @@ var RuleScheduler = class {
|
|
|
932
927
|
// src/trigger/cooldown.ts
|
|
933
928
|
var BUFFER_CAPACITY = 1024;
|
|
934
929
|
var DENSITY_LANE_PREFIX = "density:";
|
|
930
|
+
var LANE_KEY_SEPARATOR = "\n";
|
|
935
931
|
function densityLane(index) {
|
|
936
932
|
return `${DENSITY_LANE_PREFIX}${index}`;
|
|
937
933
|
}
|
|
938
934
|
__name(densityLane, "densityLane");
|
|
939
935
|
function laneKeyOf(ruleName, lane) {
|
|
940
|
-
return `${ruleName}
|
|
941
|
-
${lane}`;
|
|
936
|
+
return `${ruleName}${LANE_KEY_SEPARATOR}${lane}`;
|
|
942
937
|
}
|
|
943
938
|
__name(laneKeyOf, "laneKeyOf");
|
|
944
939
|
var TriggerCooldown = class {
|
|
@@ -974,23 +969,23 @@ var TriggerCooldown = class {
|
|
|
974
969
|
state.fireAt + setting.maxMs
|
|
975
970
|
);
|
|
976
971
|
}
|
|
977
|
-
/**
|
|
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
|
+
/** 滑动窗口顺延 */
|
|
978
978
|
extend(channelKey, ruleName, lane, now) {
|
|
979
|
-
const setting = this.setting;
|
|
980
|
-
if (setting?.mode !== "sliding") return;
|
|
981
979
|
const state = this.channels.get(channelKey)?.get(laneKeyOf(ruleName, lane));
|
|
982
|
-
|
|
983
|
-
this.slide(state, setting, now);
|
|
980
|
+
this.slideIfActive(state, this.setting, now);
|
|
984
981
|
}
|
|
985
|
-
/**
|
|
982
|
+
/** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动) */
|
|
986
983
|
extendDensityLanes(channelKey, now) {
|
|
987
|
-
const setting = this.setting;
|
|
988
|
-
if (setting?.mode !== "sliding") return;
|
|
989
984
|
const lanes = this.channels.get(channelKey);
|
|
990
985
|
if (!lanes) return;
|
|
991
986
|
for (const [key, state] of lanes) {
|
|
992
|
-
if (!key.includes(
|
|
993
|
-
this.
|
|
987
|
+
if (!key.includes(LANE_KEY_SEPARATOR + DENSITY_LANE_PREFIX)) continue;
|
|
988
|
+
this.slideIfActive(state, this.setting, now);
|
|
994
989
|
}
|
|
995
990
|
}
|
|
996
991
|
/** 冷却期内缓存一条命中消息(仅消息通道) */
|
|
@@ -1010,7 +1005,7 @@ var TriggerCooldown = class {
|
|
|
1010
1005
|
if (!lanes) continue;
|
|
1011
1006
|
for (const [laneKey, state] of lanes) {
|
|
1012
1007
|
if (now < state.deadline) continue;
|
|
1013
|
-
const separator = laneKey.indexOf(
|
|
1008
|
+
const separator = laneKey.indexOf(LANE_KEY_SEPARATOR);
|
|
1014
1009
|
lanes.delete(laneKey);
|
|
1015
1010
|
expired.push({
|
|
1016
1011
|
channelKey: key,
|
|
@@ -1035,10 +1030,19 @@ var TriggerCooldown = class {
|
|
|
1035
1030
|
|
|
1036
1031
|
// src/trigger/engine.ts
|
|
1037
1032
|
var SCHEDULER_TICK_MS = 1e3;
|
|
1033
|
+
var joinNonEmpty = /* @__PURE__ */ __name((parts) => parts.filter(Boolean).join("\n"), "joinNonEmpty");
|
|
1038
1034
|
function collectImageUrls(entries) {
|
|
1039
1035
|
return entries.flatMap((entry) => entry?.role === "user" ? entry.imageUrls ?? [] : []);
|
|
1040
1036
|
}
|
|
1041
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");
|
|
1042
1046
|
function sessionIdentityOf(session) {
|
|
1043
1047
|
return {
|
|
1044
1048
|
platform: session.platform,
|
|
@@ -1091,8 +1095,7 @@ var TriggerEngine = class {
|
|
|
1091
1095
|
this.ctx.logger.info("已加载 %d 条启用规则", this.rules.length);
|
|
1092
1096
|
}
|
|
1093
1097
|
handleMessage(session, next) {
|
|
1094
|
-
if (this.ctx.bots[session.uid]) return next();
|
|
1095
|
-
if (!session.channelId) return next();
|
|
1098
|
+
if (this.ctx.bots[session.uid] || !session.channelId) return next();
|
|
1096
1099
|
const channelKey = channelKeyOf(session.platform, session.channelId);
|
|
1097
1100
|
this.tickCooldown(channelKey);
|
|
1098
1101
|
const entry = buildHistoryEntry(session);
|
|
@@ -1163,16 +1166,14 @@ var TriggerEngine = class {
|
|
|
1163
1166
|
...sessionIdentityOf(session),
|
|
1164
1167
|
triggerType: "density",
|
|
1165
1168
|
rule: fire.rule,
|
|
1166
|
-
|
|
1167
|
-
contentText: formatHistoryBrief(fire.entries),
|
|
1168
|
-
imageUrls: collectImageUrls(fire.entries)
|
|
1169
|
+
...windowFieldsOf(fire.entries)
|
|
1169
1170
|
});
|
|
1170
1171
|
}
|
|
1171
1172
|
/** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
|
|
1172
1173
|
fireMerged(rule, buffer) {
|
|
1173
1174
|
const first = buffer[0];
|
|
1174
|
-
const queryText = buffer.map((item) => item.entry ? formatHistoryEntry(item.entry) : "")
|
|
1175
|
-
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));
|
|
1176
1177
|
this.beginCooldown(channelKeyOf(first.session.platform, first.session.channelId), rule.name, "message");
|
|
1177
1178
|
this.run({
|
|
1178
1179
|
session: first.session,
|
|
@@ -1186,10 +1187,9 @@ var TriggerEngine = class {
|
|
|
1186
1187
|
});
|
|
1187
1188
|
}
|
|
1188
1189
|
/** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
|
|
1189
|
-
fireDensityExpiry(rule, conditionIndex, channel, now) {
|
|
1190
|
+
fireDensityExpiry(rule, condition, conditionIndex, channel, now) {
|
|
1190
1191
|
this.beginCooldown(channelKeyOf(channel.platform, channel.channelId), rule.name, densityLane(conditionIndex));
|
|
1191
|
-
const
|
|
1192
|
-
const windowMs = condition.type === "density" ? condition.densityWindowSeconds * 1e3 : 0;
|
|
1192
|
+
const windowMs = condition.densityWindowSeconds * 1e3;
|
|
1193
1193
|
let userId;
|
|
1194
1194
|
for (let i = channel.entries.length - 1; i >= 0; i--) {
|
|
1195
1195
|
const item = channel.entries[i];
|
|
@@ -1203,9 +1203,7 @@ var TriggerEngine = class {
|
|
|
1203
1203
|
userId,
|
|
1204
1204
|
triggerType: "density",
|
|
1205
1205
|
rule,
|
|
1206
|
-
|
|
1207
|
-
contentText: formatHistoryBrief(channel.entries),
|
|
1208
|
-
imageUrls: collectImageUrls(channel.entries),
|
|
1206
|
+
...windowFieldsOf(channel.entries),
|
|
1209
1207
|
isDirect: channel.isDirect
|
|
1210
1208
|
});
|
|
1211
1209
|
}
|
|
@@ -1219,9 +1217,7 @@ var TriggerEngine = class {
|
|
|
1219
1217
|
...poolIdentityOf(channel),
|
|
1220
1218
|
triggerType: "schedule",
|
|
1221
1219
|
rule,
|
|
1222
|
-
|
|
1223
|
-
contentText: formatHistoryBrief(channel.entries),
|
|
1224
|
-
imageUrls: collectImageUrls(channel.entries),
|
|
1220
|
+
...windowFieldsOf(channel.entries),
|
|
1225
1221
|
isDirect: channel.isDirect
|
|
1226
1222
|
});
|
|
1227
1223
|
}
|
|
@@ -1245,7 +1241,7 @@ var TriggerEngine = class {
|
|
|
1245
1241
|
const condition = rule.conditions[index];
|
|
1246
1242
|
if (condition?.type !== "density") return;
|
|
1247
1243
|
const channel = this.pool.takeDensityChannel(rule, condition, expired.channelKey, now);
|
|
1248
|
-
if (channel) this.fireDensityExpiry(rule, index, channel, now);
|
|
1244
|
+
if (channel) this.fireDensityExpiry(rule, condition, index, channel, now);
|
|
1249
1245
|
}
|
|
1250
1246
|
};
|
|
1251
1247
|
|
|
@@ -1304,6 +1300,10 @@ function guessExtension(mimeType) {
|
|
|
1304
1300
|
return found ? found[0] : "bin";
|
|
1305
1301
|
}
|
|
1306
1302
|
__name(guessExtension, "guessExtension");
|
|
1303
|
+
function fallbackFileName(mimeType) {
|
|
1304
|
+
return `file.${guessExtension(mimeType)}`;
|
|
1305
|
+
}
|
|
1306
|
+
__name(fallbackFileName, "fallbackFileName");
|
|
1307
1307
|
function mimeToFileType(mimeType) {
|
|
1308
1308
|
if (!mimeType) return "custom";
|
|
1309
1309
|
if (mimeType.startsWith("image/")) return "image";
|
|
@@ -1317,15 +1317,11 @@ function mimeToFileType(mimeType) {
|
|
|
1317
1317
|
__name(mimeToFileType, "mimeToFileType");
|
|
1318
1318
|
function extractFileCandidates(session) {
|
|
1319
1319
|
const candidates = [];
|
|
1320
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1321
1320
|
for (const element of session.elements ?? []) {
|
|
1322
1321
|
const difyType = ELEMENT_TYPE[element.type];
|
|
1323
1322
|
if (!difyType) continue;
|
|
1324
|
-
const source = element
|
|
1323
|
+
const source = imageSourceOf(element);
|
|
1325
1324
|
if (typeof source !== "string" || !source) continue;
|
|
1326
|
-
const key = `${difyType}:${source}`;
|
|
1327
|
-
if (seen.has(key)) continue;
|
|
1328
|
-
seen.add(key);
|
|
1329
1325
|
candidates.push({ source, type: difyType });
|
|
1330
1326
|
}
|
|
1331
1327
|
return candidates;
|
|
@@ -1351,7 +1347,7 @@ function parseDataUrl(source) {
|
|
|
1351
1347
|
const mimeType = match[1] || "application/octet-stream";
|
|
1352
1348
|
const isBase64 = /(?:^|;)base64/.test(match[2] ?? "");
|
|
1353
1349
|
const buffer = isBase64 ? Buffer.from(match[3], "base64") : percentDecodeBytes(match[3]);
|
|
1354
|
-
return { buffer, fileName:
|
|
1350
|
+
return { buffer, fileName: fallbackFileName(mimeType), mimeType };
|
|
1355
1351
|
}
|
|
1356
1352
|
__name(parseDataUrl, "parseDataUrl");
|
|
1357
1353
|
async function readLocalFile(ctx, source) {
|
|
@@ -1362,7 +1358,7 @@ async function readLocalFile(ctx, source) {
|
|
|
1362
1358
|
}
|
|
1363
1359
|
const buffer = await (0, import_promises.readFile)(filePath);
|
|
1364
1360
|
const mimeType = guessMime((0, import_path.extname)(filePath));
|
|
1365
|
-
return { buffer, fileName: (0, import_path.basename)(filePath) ||
|
|
1361
|
+
return { buffer, fileName: (0, import_path.basename)(filePath) || fallbackFileName(mimeType), mimeType };
|
|
1366
1362
|
}
|
|
1367
1363
|
__name(readLocalFile, "readLocalFile");
|
|
1368
1364
|
async function fetchRemoteFile(ctx, source) {
|
|
@@ -1378,7 +1374,7 @@ async function fetchRemoteFile(ctx, source) {
|
|
|
1378
1374
|
} catch {
|
|
1379
1375
|
}
|
|
1380
1376
|
const mimeType = guessMime((0, import_path.extname)(fileName));
|
|
1381
|
-
return { buffer, fileName: fileName ||
|
|
1377
|
+
return { buffer, fileName: fileName || fallbackFileName(mimeType), mimeType };
|
|
1382
1378
|
} catch (error) {
|
|
1383
1379
|
ctx.logger.warn("下载远程文件失败:%s (%s)", source, error);
|
|
1384
1380
|
return null;
|
|
@@ -1493,19 +1489,21 @@ function resolveMetaField(key, fireCtx, now) {
|
|
|
1493
1489
|
}
|
|
1494
1490
|
}
|
|
1495
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");
|
|
1496
1497
|
function resolveSource(source, fireCtx, history, now) {
|
|
1497
|
-
switch (source
|
|
1498
|
+
switch (source.type) {
|
|
1498
1499
|
case "session":
|
|
1499
1500
|
return resolveSessionField(source.key, fireCtx, now);
|
|
1500
1501
|
case "meta":
|
|
1501
1502
|
return resolveMetaField(source.key, fireCtx, now);
|
|
1502
1503
|
case "constant":
|
|
1503
1504
|
return source.value;
|
|
1504
|
-
case "history":
|
|
1505
|
-
|
|
1506
|
-
const limit = source.limit ?? history.limit;
|
|
1507
|
-
return history.toVariable(history.get(channelKey, limit));
|
|
1508
|
-
}
|
|
1505
|
+
case "history":
|
|
1506
|
+
return historyVariableOf(fireCtx, history, source.limit);
|
|
1509
1507
|
default:
|
|
1510
1508
|
return void 0;
|
|
1511
1509
|
}
|
|
@@ -1514,28 +1512,26 @@ __name(resolveSource, "resolveSource");
|
|
|
1514
1512
|
var META_PLACEHOLDERS = /* @__PURE__ */ new Set(["triggerType", "ruleName", "triggerTime"]);
|
|
1515
1513
|
function resolvePlaceholderValue(name2, limit, fireCtx, history, now) {
|
|
1516
1514
|
if (name2 === "history") {
|
|
1517
|
-
|
|
1518
|
-
return history.toVariable(history.get(channelKey, limit ?? history.limit));
|
|
1515
|
+
return historyVariableOf(fireCtx, history, limit);
|
|
1519
1516
|
}
|
|
1520
1517
|
if (name2 === "isDirect") {
|
|
1521
|
-
return
|
|
1518
|
+
return resolveSessionField("isDirect", fireCtx, now);
|
|
1522
1519
|
}
|
|
1523
1520
|
if (META_PLACEHOLDERS.has(name2)) {
|
|
1524
|
-
return
|
|
1521
|
+
return resolveMetaField(name2, fireCtx, now);
|
|
1525
1522
|
}
|
|
1526
|
-
|
|
1527
|
-
return value === void 0 || value === null ? "" : String(value);
|
|
1523
|
+
return String(resolveSessionField(name2, fireCtx, now));
|
|
1528
1524
|
}
|
|
1529
1525
|
__name(resolvePlaceholderValue, "resolvePlaceholderValue");
|
|
1530
|
-
var BARE_PLACEHOLDER =
|
|
1526
|
+
var BARE_PLACEHOLDER = new RegExp(`"${JSON_PLACEHOLDER.source}"`, "g");
|
|
1531
1527
|
var escapeJsonText = /* @__PURE__ */ __name((value) => JSON.stringify(value).slice(1, -1), "escapeJsonText");
|
|
1532
1528
|
function interpolateTemplate(template, fireCtx, history, now) {
|
|
1533
1529
|
const resolve = /* @__PURE__ */ __name((name2, limit) => resolvePlaceholderValue(name2, limit ? Number(limit) : void 0, fireCtx, history, now), "resolve");
|
|
1534
|
-
const
|
|
1530
|
+
const bareReplaced = template.replace(
|
|
1535
1531
|
BARE_PLACEHOLDER,
|
|
1536
1532
|
(_match, name2, limit) => JSON.stringify(resolve(name2, limit))
|
|
1537
1533
|
);
|
|
1538
|
-
return
|
|
1534
|
+
return bareReplaced.replace(
|
|
1539
1535
|
JSON_PLACEHOLDER,
|
|
1540
1536
|
(_match, name2, limit) => escapeJsonText(String(resolve(name2, limit)))
|
|
1541
1537
|
);
|
|
@@ -1554,12 +1550,12 @@ function resolveVariables(rule, fireCtx, history) {
|
|
|
1554
1550
|
const inputs = {};
|
|
1555
1551
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1556
1552
|
for (const binding of rule.variables) {
|
|
1557
|
-
if (binding
|
|
1553
|
+
if (binding.source.type !== "json") continue;
|
|
1558
1554
|
const object = resolveJsonObject(binding.source.template, fireCtx, history, now);
|
|
1559
1555
|
if (object) Object.assign(inputs, object);
|
|
1560
1556
|
}
|
|
1561
1557
|
for (const binding of rule.variables) {
|
|
1562
|
-
if (
|
|
1558
|
+
if (binding.source.type === "json") continue;
|
|
1563
1559
|
const value = resolveSource(binding.source, fireCtx, history, now);
|
|
1564
1560
|
if (value !== void 0) inputs[binding.field] = value;
|
|
1565
1561
|
}
|
|
@@ -1575,12 +1571,13 @@ function splitText(text, maxLength) {
|
|
|
1575
1571
|
const lines = text.split("\n");
|
|
1576
1572
|
const segments = [];
|
|
1577
1573
|
let current = "";
|
|
1574
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
1575
|
+
if (current) segments.push(current);
|
|
1576
|
+
}, "flush");
|
|
1578
1577
|
for (const line of lines) {
|
|
1579
1578
|
if (line.length > maxLength) {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
current = "";
|
|
1583
|
-
}
|
|
1579
|
+
flush();
|
|
1580
|
+
current = "";
|
|
1584
1581
|
for (let i = 0; i < line.length; i += maxLength) {
|
|
1585
1582
|
segments.push(line.slice(i, i + maxLength));
|
|
1586
1583
|
}
|
|
@@ -1594,7 +1591,7 @@ function splitText(text, maxLength) {
|
|
|
1594
1591
|
${line}` : line;
|
|
1595
1592
|
}
|
|
1596
1593
|
}
|
|
1597
|
-
|
|
1594
|
+
flush();
|
|
1598
1595
|
return segments;
|
|
1599
1596
|
}
|
|
1600
1597
|
__name(splitText, "splitText");
|
|
@@ -1698,22 +1695,7 @@ var RequestRunner = class {
|
|
|
1698
1695
|
inputs: resolveVariables(rule, fireCtx, this.history),
|
|
1699
1696
|
uploadedFiles: []
|
|
1700
1697
|
};
|
|
1701
|
-
|
|
1702
|
-
if (upload.enabled) {
|
|
1703
|
-
const sessionCandidates = fireCtx.session ? extractFileCandidates(fireCtx.session) : [];
|
|
1704
|
-
const urlCandidates = (fireCtx.imageUrls ?? []).filter((url) => !sessionCandidates.some((candidate) => candidate.source === url)).map((url) => ({ source: url, type: "image" }));
|
|
1705
|
-
const { files, dropped } = await uploadCandidates(
|
|
1706
|
-
this.ctx,
|
|
1707
|
-
client,
|
|
1708
|
-
[...sessionCandidates, ...urlCandidates],
|
|
1709
|
-
requestCtx.difyUser,
|
|
1710
|
-
{ maxCount: upload.maxCount, maxSizeMb: upload.maxMb }
|
|
1711
|
-
);
|
|
1712
|
-
requestCtx.uploadedFiles = files;
|
|
1713
|
-
if (dropped.length) {
|
|
1714
|
-
fireCtx.queryText = annotateDroppedPlaceholders(fireCtx.queryText, dropped);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1698
|
+
await this.uploadTriggerFiles(fireCtx, client, requestCtx);
|
|
1717
1699
|
let reply;
|
|
1718
1700
|
try {
|
|
1719
1701
|
reply = await this.dispatch(app, client, fireCtx, requestCtx);
|
|
@@ -1737,6 +1719,30 @@ var RequestRunner = class {
|
|
|
1737
1719
|
});
|
|
1738
1720
|
await this.deliver(fireCtx, messages);
|
|
1739
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
|
+
}
|
|
1740
1746
|
/** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
|
|
1741
1747
|
shouldQuoteReply(fireCtx, startedAt) {
|
|
1742
1748
|
const quote = resolveQuoteConfig(this.config);
|
|
@@ -1772,12 +1778,16 @@ var RequestRunner = class {
|
|
|
1772
1778
|
}
|
|
1773
1779
|
return { text: response.answer, files: [] };
|
|
1774
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
|
+
}
|
|
1775
1787
|
async deliver(fireCtx, messages) {
|
|
1776
1788
|
if (!messages.length) return;
|
|
1777
1789
|
if (fireCtx.session) {
|
|
1778
|
-
|
|
1779
|
-
await fireCtx.session.send(message).catch((error) => this.ctx.logger.warn(error));
|
|
1780
|
-
}
|
|
1790
|
+
await this.sendAll(messages, (message) => fireCtx.session.send(message));
|
|
1781
1791
|
return;
|
|
1782
1792
|
}
|
|
1783
1793
|
const bots = Object.values(this.ctx.bots);
|
|
@@ -1786,9 +1796,7 @@ var RequestRunner = class {
|
|
|
1786
1796
|
this.ctx.logger.warn("平台 %s 上没有可用 bot,无法发送主动消息", fireCtx.platform);
|
|
1787
1797
|
return;
|
|
1788
1798
|
}
|
|
1789
|
-
|
|
1790
|
-
await bot.sendMessage(fireCtx.channelId, message).catch((error) => this.ctx.logger.warn(error));
|
|
1791
|
-
}
|
|
1799
|
+
await this.sendAll(messages, (message) => bot.sendMessage(fireCtx.channelId, message));
|
|
1792
1800
|
}
|
|
1793
1801
|
};
|
|
1794
1802
|
|
|
@@ -1856,23 +1864,34 @@ var ConversationStore = class {
|
|
|
1856
1864
|
async saveConversationId(bindingKey, difyConversationId) {
|
|
1857
1865
|
await this.ctx.database.upsert("dify_conversation", [{ bindingKey, difyConversationId }]);
|
|
1858
1866
|
}
|
|
1859
|
-
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser
|
|
1867
|
+
/** 解析 bindingKey 的构成(重置时反查应用凭据与 difyUser 用)。
|
|
1868
|
+
* 段位定位假设 appId / platform / selfId 不含冒号(平台名与应用标识按约定不含;
|
|
1869
|
+
* Matrix 的 bot id 含冒号属键格式改造范畴,当前不区分) */
|
|
1860
1870
|
parseBindingKey(bindingKey) {
|
|
1861
|
-
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;
|
|
1862
1876
|
return { appId, platform, scope, ids };
|
|
1863
1877
|
}
|
|
1878
|
+
/** scope 之后的 id 片段拼回整体(键内 id 可能内嵌冒号,消费方按语义整体比较) */
|
|
1879
|
+
joinedIdsOf(bindingKey) {
|
|
1880
|
+
return this.parseBindingKey(bindingKey).ids.join(":");
|
|
1881
|
+
}
|
|
1864
1882
|
/** 由 bindingKey 还原删除 Dify 会话所需的 difyUser(与 computeDifyUser 对齐) */
|
|
1865
1883
|
difyUserOfBindingKey(bindingKey) {
|
|
1866
|
-
const { platform, scope
|
|
1884
|
+
const { platform, scope } = this.parseBindingKey(bindingKey);
|
|
1885
|
+
const ids = this.joinedIdsOf(bindingKey);
|
|
1867
1886
|
switch (scope) {
|
|
1868
1887
|
case "global":
|
|
1869
1888
|
return "koishi";
|
|
1870
1889
|
case "private":
|
|
1871
|
-
return `koishi:${platform}:private:${ids
|
|
1890
|
+
return `koishi:${platform}:private:${ids}`;
|
|
1872
1891
|
case "guild":
|
|
1873
1892
|
case "channel":
|
|
1874
1893
|
case "user":
|
|
1875
|
-
return `koishi:${platform}:${ids
|
|
1894
|
+
return `koishi:${platform}:${ids}`;
|
|
1876
1895
|
default:
|
|
1877
1896
|
return `koishi:${platform}`;
|
|
1878
1897
|
}
|
|
@@ -1893,12 +1912,13 @@ var ConversationStore = class {
|
|
|
1893
1912
|
const [channelPlatform, channelId] = splitKey(filter.channelKey ?? "");
|
|
1894
1913
|
const matched = rows.filter((row) => {
|
|
1895
1914
|
if (filter.bindingKey) return row.bindingKey === filter.bindingKey;
|
|
1896
|
-
const { platform, scope
|
|
1915
|
+
const { platform, scope } = this.parseBindingKey(row.bindingKey);
|
|
1897
1916
|
if (platform !== guildPlatform) return false;
|
|
1898
|
-
|
|
1899
|
-
if (scope === "
|
|
1900
|
-
if (scope === "
|
|
1901
|
-
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;
|
|
1902
1922
|
return false;
|
|
1903
1923
|
});
|
|
1904
1924
|
let reset = 0;
|
|
@@ -1941,14 +1961,14 @@ __name(defineTables, "defineTables");
|
|
|
1941
1961
|
// src/commands/misc.ts
|
|
1942
1962
|
function registerMiscCommands(parent, conversationStore) {
|
|
1943
1963
|
parent.subcommand(".conv.reset", { authority: 3 }).action(async ({ session }) => {
|
|
1944
|
-
const
|
|
1964
|
+
const resetCount = await conversationStore.resetWhere({
|
|
1945
1965
|
guildKey: guildKeyOf(session.platform, session.guildId, session.channelId),
|
|
1946
1966
|
channelKey: channelKeyOf(session.platform, session.channelId),
|
|
1947
1967
|
isDirect: session.isDirect,
|
|
1948
1968
|
userId: session.userId
|
|
1949
1969
|
});
|
|
1950
|
-
if (!
|
|
1951
|
-
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]);
|
|
1952
1972
|
});
|
|
1953
1973
|
}
|
|
1954
1974
|
__name(registerMiscCommands, "registerMiscCommands");
|
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 {};
|
|
@@ -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[];
|
|
@@ -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",
|