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.
@@ -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: QuoteEntry;
89
+ quote: boolean;
90
+ minElapsedSeconds: number;
105
91
  maxSegmentLength: number;
106
- /** boolean 是 0.1.x 的遗留形态,仅迁移读取(true → 开启不限,false → 关闭) */
107
- uploadFiles: UploadEntry | boolean;
108
- /** 0.1.x 的布尔开关,仅用于迁移读取(true → 总是引用,false → 关闭) */
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
- * 上传消息文件配置(含历史形态迁移:0.1.x 布尔、0.1.7 mode)。
118
- * 数量与单文件大小 0 与 -1 均为不限,大小仍与 Dify 各类型上限取较小值。
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
- * 引用触发消息配置(含历史形态迁移:0.1.x 布尔与 quoteTrigger 透传、0.1.7 mode、布尔开关新形态)。
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 quoteSchema = import_koishi.Schema.union([
151
- import_koishi.Schema.intersect([
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
- enabled: import_koishi.Schema.boolean().default(true).description("开启")
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.union([
156
- import_koishi.Schema.object({
157
- enabled: import_koishi.Schema.const(true).required(),
158
- minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1).description("回复耗时多少秒才引用,0 总是引用")
159
- }),
160
- import_koishi.Schema.object({})
161
- ])
162
- ]),
163
- import_koishi.Schema.boolean(),
164
- import_koishi.Schema.object({ mode: import_koishi.Schema.const("off").required() }),
167
+ import_koishi.Schema.object({})
168
+ ])
169
+ ]);
170
+ var uploadSwitchSchema = import_koishi.Schema.intersect([
165
171
  import_koishi.Schema.object({
166
- mode: import_koishi.Schema.const("on").required(),
167
- minElapsedSeconds: import_koishi.Schema.number().min(-1).default(-1)
168
- })
169
- ]).default({ enabled: true, minElapsedSeconds: -1 });
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
- quote: quoteSchema.description("引用触发消息"),
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
- const upload = config.uploadFiles;
378
- if (typeof upload === "boolean") {
379
- return upload ? { enabled: true, maxCount: 0, maxMb: 0 } : { enabled: false, maxCount: 0, maxMb: 0 };
380
- }
381
- if (upload?.mode === "off" || upload?.enabled === false) {
382
- return { enabled: false, maxCount: 0, maxMb: 0 };
383
- }
368
+ if (config.uploadFiles !== true) return { enabled: false, maxCount: 0, maxMb: 0 };
384
369
  return {
385
370
  enabled: true,
386
- maxCount: Number(upload?.maxCount) || 0,
387
- maxMb: Number(upload?.maxSizeMb) || 0
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 quote = config.quote;
393
- if (typeof quote === "boolean") return { enabled: quote, minElapsedSeconds: 0 };
394
- if (quote?.mode === "off") return { enabled: false, minElapsedSeconds: 0 };
395
- const threshold = Number(quote?.minElapsedSeconds) || 0;
396
- const untouched = !(threshold > 0) && quote?.enabled !== false;
397
- if (typeof config.quoteTrigger === "boolean" && untouched) {
398
- return { enabled: config.quoteTrigger, minElapsedSeconds: 0 };
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?.mode === "on" ? true : quote?.enabled !== false, minElapsedSeconds: threshold };
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: { Authorization: `Bearer ${this.app.apiKey}` },
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.type === "img" || element.type === "image") {
603
- parts.push(imagePlaceholder(element.attrs?.["url"] ?? element.attrs?.["src"]));
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.type !== "img" && element.type !== "image") continue;
613
- const source = element.attrs?.["url"] ?? element.attrs?.["src"];
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
- return `[${formatTimestamp(entry.timestamp)}] ${entry.username} (${entry.userId})${messageId}${quote}${quote ? "" : " "}说:${entry.content}`;
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 NON_TEXT_PREFIX = new RegExp(`^(?:@\\S+|\\[image:[0-9a-f]{${IMAGE_HASH_LENGTH}}\\]|\\[图片\\]|\\s)*`);
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 === true && !session.isDirect) return false;
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, condition, conditionIndex: index, entries: pool.entries }];
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
- if (!state || now >= state.deadline) return;
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("\ndensity:") || now >= state.deadline) continue;
993
- this.slide(state, setting, now);
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("\n");
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
- queryText: formatHistoryEntries(fire.entries),
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) : "").filter(Boolean).join("\n");
1175
- const contentText = buffer.map((item) => item.contentText).filter(Boolean).join("\n");
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 condition = rule.conditions[conditionIndex];
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
- queryText: formatHistoryEntries(channel.entries),
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
- queryText: formatHistoryEntries(channel.entries),
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.attrs?.["url"] ?? element.attrs?.["src"];
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: `file.${guessExtension(mimeType)}`, mimeType };
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) || `file.${guessExtension(mimeType)}`, mimeType };
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 || `file.${guessExtension(mimeType)}`, mimeType };
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?.type) {
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
- const channelKey = channelKeyOf(fireCtx.platform, fireCtx.channelId);
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
- const channelKey = channelKeyOf(fireCtx.platform, fireCtx.channelId);
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 fireCtx.session?.isDirect ?? fireCtx.isDirect ?? false;
1518
+ return resolveSessionField("isDirect", fireCtx, now);
1522
1519
  }
1523
1520
  if (META_PLACEHOLDERS.has(name2)) {
1524
- return String(resolveMetaField(name2, fireCtx, now) ?? "");
1521
+ return resolveMetaField(name2, fireCtx, now);
1525
1522
  }
1526
- const value = resolveSessionField(name2, fireCtx, now);
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 = /"\{\{\s*(\w+)(?::(\d+))?\s*\}\}"/g;
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 native = template.replace(
1530
+ const bareReplaced = template.replace(
1535
1531
  BARE_PLACEHOLDER,
1536
1532
  (_match, name2, limit) => JSON.stringify(resolve(name2, limit))
1537
1533
  );
1538
- return native.replace(
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?.source?.type !== "json") continue;
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 (!binding?.field || binding.source?.type === "json") continue;
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
- if (current) {
1581
- segments.push(current);
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
- if (current) segments.push(current);
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
- const upload = resolveUploadConfig(this.config);
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
- for (const message of messages) {
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
- for (const message of messages) {
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 [appId, platform, , scope, ...ids] = bindingKey.split(":");
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, ids } = this.parseBindingKey(bindingKey);
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.join(":")}`;
1890
+ return `koishi:${platform}:private:${ids}`;
1872
1891
  case "guild":
1873
1892
  case "channel":
1874
1893
  case "user":
1875
- return `koishi:${platform}:${ids.join(":")}`;
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, ids } = this.parseBindingKey(row.bindingKey);
1915
+ const { platform, scope } = this.parseBindingKey(row.bindingKey);
1897
1916
  if (platform !== guildPlatform) return false;
1898
- if (scope === "guild") return ids[0] === guildId;
1899
- if (scope === "user") return ids.length === 2 ? ids[0] === guildId : false;
1900
- if (scope === "channel") return platform === channelPlatform && ids[0] === channelId;
1901
- if (scope === "private") return filter.isDirect === true && ids[0] === filter.userId;
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 reset = await conversationStore.resetWhere({
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 (!reset) return session.text("dify-ai.commands.conv-empty");
1951
- return session.text("dify-ai.commands.conv-reset", [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");
@@ -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
- /** 上传一批候选(入口按 type:source 去重,再按数量上限截断、逐个上传) */
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;
@@ -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.9",
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",