koishi-plugin-dify-ai 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,6 @@ export declare class DifyClient {
18
18
  /** blocking JSON 请求(本插件只用 blocking 模式) */
19
19
  private request;
20
20
  getAppInfo(): Promise<DifyAppInfo>;
21
- getParameters(): Promise<any>;
22
21
  /** chat / agent-chat / advanced-chat(conversation_id 传空串表示新建会话) */
23
22
  runChat(body: {
24
23
  inputs: Record<string, any>;
package/lib/config.d.ts CHANGED
@@ -120,8 +120,9 @@ export declare function resolveUploadConfig(config: Config): {
120
120
  maxMb: number;
121
121
  };
122
122
  /**
123
- * 引用触发消息配置(含 0.1.x 布尔开关迁移):schema 形态缺失时
124
- * 旧 quoteTrigger 若被配置层透传保留则迁移,否则按总是引用处理。
123
+ * 引用触发消息配置(含 0.1.x 布尔开关迁移):schema 会给旧配置填默认 quote 对象,
124
+ * 因此迁移判据是「旧布尔被配置层透传保留 且 quote 仍是默认形态(用户未在新表单改过)」——
125
+ * 此时以旧布尔为准;用户改过新表单(quote 非默认)则新值优先。
125
126
  */
126
127
  export declare function resolveQuoteConfig(config: Config): {
127
128
  enabled: boolean;
package/lib/history.d.ts CHANGED
@@ -11,8 +11,12 @@ export interface HistoryEntry {
11
11
  userId: string;
12
12
  username: string;
13
13
  messageId?: string;
14
- /** 纯文本 + 图片占位符 */
14
+ /** 原貌(含 @bot 渲染与图片占位):统一格式行(query/历史变量)用 */
15
15
  content: string;
16
+ /** 剔除 @bot 的原貌:content 变量与精简行用 */
17
+ contentText: string;
18
+ /** 图片元素 URL(按元素序,与 content 中 [image:摘要] 序一致):窗口类触发的上传依据 */
19
+ imageUrls?: string[];
16
20
  timestamp: number;
17
21
  quote?: QuotedMessage;
18
22
  }
@@ -24,6 +28,8 @@ export interface HistoryEntry {
24
28
  export declare function formatHistoryEntry(entry: HistoryEntry): string;
25
29
  /** 多条消息逐行拼接 */
26
30
  export declare function formatHistoryEntries(entries: HistoryEntry[]): string;
31
+ /** 精简行:只记消息 ID 与剔除 @bot 的原貌内容,不带时间与作者前缀——窗口触发的 content 变量用 */
32
+ export declare function formatHistoryBrief(entries: HistoryEntry[]): string;
27
33
  /** 从会话消息构造记录条目(历史缓冲 / 消息池共用);assistant 行标注为机器人自己的发言 */
28
34
  export declare function buildHistoryEntry(session: Session, role?: 'user' | 'assistant'): HistoryEntry | null;
29
35
  /**
package/lib/index.js CHANGED
@@ -509,10 +509,9 @@ function parseTextSafe(content) {
509
509
  }
510
510
  }
511
511
  __name(parseTextSafe, "parseTextSafe");
512
- function extractText(session, options = {}) {
512
+ function extractText(session) {
513
513
  if (session.elements?.length) {
514
- const elements = options.excludeAt ? session.elements.filter((element) => element.type !== "at") : session.elements;
515
- return import_koishi2.h.select(elements, "text").join("").trim();
514
+ return import_koishi2.h.select(session.elements, "text").join("").trim();
516
515
  }
517
516
  return parseTextSafe(session.content);
518
517
  }
@@ -545,6 +544,9 @@ function extractTextWithPlaceholders(session) {
545
544
  if (element.type === "text") {
546
545
  const text = element.attrs?.["content"] ?? "";
547
546
  if (text) parts.push(text);
547
+ } else if (element.type === "at") {
548
+ const name2 = element.attrs?.["name"] ?? element.attrs?.["id"];
549
+ if (name2) parts.push(`@${name2}`);
548
550
  } else if (element.type === "img" || element.type === "image") {
549
551
  parts.push(imagePlaceholder(element.attrs?.["url"] ?? element.attrs?.["src"]));
550
552
  }
@@ -572,6 +574,10 @@ function formatHistoryEntries(entries) {
572
574
  return entries.map(formatHistoryEntry).join("\n");
573
575
  }
574
576
  __name(formatHistoryEntries, "formatHistoryEntries");
577
+ function formatHistoryBrief(entries) {
578
+ return entries.map((entry) => entry.messageId ? `[msg:${entry.messageId}] ${entry.content}` : entry.content).join("\n");
579
+ }
580
+ __name(formatHistoryBrief, "formatHistoryBrief");
575
581
  function buildQuote(session) {
576
582
  const quote = session.quote;
577
583
  if (!quote?.user?.id) return void 0;
@@ -631,38 +637,39 @@ function hasAt(session, id) {
631
637
  __name(hasAt, "hasAt");
632
638
  function evaluateCondition(condition, session) {
633
639
  const text = extractText(session);
640
+ const verbatim = extractTextWithPlaceholders(session);
634
641
  switch (condition.type) {
635
642
  case "keyword-prefix": {
636
643
  const keyword = condition.keywords.find((k) => k && text.startsWith(k));
637
644
  if (!keyword) return null;
638
645
  const rest = text.slice(keyword.length).trim();
639
- return { type: "keyword-prefix", queryText: rest || text };
646
+ return { type: "keyword-prefix", queryText: rest || verbatim };
640
647
  }
641
648
  case "keyword-contains": {
642
649
  const keyword = condition.keywords.find((k) => k && text.includes(k));
643
650
  if (!keyword) return null;
644
- return { type: "keyword-contains", queryText: text };
651
+ return { type: "keyword-contains", queryText: verbatim };
645
652
  }
646
653
  case "at-bot": {
647
654
  const botId = session.bot?.userId;
648
655
  if (!botId) return null;
649
656
  if (!session.stripped?.atSelf && !hasAt(session, botId)) return null;
650
- return { type: "at-bot", queryText: extractText(session, { excludeAt: true }) };
657
+ return { type: "at-bot", queryText: verbatim };
651
658
  }
652
659
  case "at-user": {
653
660
  const isAtAnyUser = condition.userIds.some((id) => hasAt(session, id));
654
661
  if (!isAtAnyUser) return null;
655
- return { type: "at-user", queryText: text };
662
+ return { type: "at-user", queryText: verbatim };
656
663
  }
657
664
  case "quote-bot": {
658
665
  const botId = session.bot?.userId;
659
666
  if (!botId || session.quote?.user?.id !== botId) return null;
660
- return { type: "quote-bot", queryText: extractText(session, { excludeAt: true }) };
667
+ return { type: "quote-bot", queryText: verbatim };
661
668
  }
662
669
  case "quote-user": {
663
670
  const author = session.quote?.user?.id;
664
671
  if (!author || !condition.userIds.includes(author)) return null;
665
- return { type: "quote-user", queryText: extractText(session, { excludeAt: true }) };
672
+ return { type: "quote-user", queryText: verbatim };
666
673
  }
667
674
  default:
668
675
  return null;
@@ -1057,7 +1064,6 @@ var TriggerEngine = class {
1057
1064
  }
1058
1065
  fireDensity(fire, session) {
1059
1066
  this.beginCooldown(channelKeyOf(session.platform, session.channelId), fire.rule.name, densityLane(fire.conditionIndex));
1060
- const windowText = formatHistoryEntries(fire.entries);
1061
1067
  this.run({
1062
1068
  session,
1063
1069
  platform: session.platform,
@@ -1067,8 +1073,8 @@ var TriggerEngine = class {
1067
1073
  userId: session.userId,
1068
1074
  triggerType: "density",
1069
1075
  rule: fire.rule,
1070
- queryText: windowText,
1071
- contentText: windowText
1076
+ queryText: formatHistoryEntries(fire.entries),
1077
+ contentText: formatHistoryBrief(fire.entries)
1072
1078
  });
1073
1079
  }
1074
1080
  /** 冷却到期:把缓冲内的触发消息合并为一次触发,session 取首条(供投递与文件上传) */
@@ -1093,7 +1099,6 @@ var TriggerEngine = class {
1093
1099
  /** 密度冷却到期复查达标:无当前消息,用池身份构造上下文(投递同定时路径) */
1094
1100
  fireDensityExpiry(rule, conditionIndex, channel) {
1095
1101
  this.beginCooldown(channelKeyOf(channel.platform, channel.channelId), rule.name, densityLane(conditionIndex));
1096
- const windowText = formatHistoryEntries(channel.entries);
1097
1102
  this.run({
1098
1103
  platform: channel.platform,
1099
1104
  selfId: channel.selfId,
@@ -1101,8 +1106,8 @@ var TriggerEngine = class {
1101
1106
  guildId: channel.guildId,
1102
1107
  triggerType: "density",
1103
1108
  rule,
1104
- queryText: windowText,
1105
- contentText: windowText
1109
+ queryText: formatHistoryEntries(channel.entries),
1110
+ contentText: formatHistoryBrief(channel.entries)
1106
1111
  });
1107
1112
  }
1108
1113
  /**
@@ -1111,7 +1116,6 @@ var TriggerEngine = class {
1111
1116
  */
1112
1117
  fireSchedule(rule) {
1113
1118
  for (const channel of this.pool.takeSchedule(rule)) {
1114
- const windowText = formatHistoryEntries(channel.entries);
1115
1119
  this.run({
1116
1120
  platform: channel.platform,
1117
1121
  selfId: channel.selfId,
@@ -1119,8 +1123,8 @@ var TriggerEngine = class {
1119
1123
  guildId: channel.guildId,
1120
1124
  triggerType: "schedule",
1121
1125
  rule,
1122
- queryText: windowText,
1123
- contentText: windowText
1126
+ queryText: formatHistoryEntries(channel.entries),
1127
+ contentText: formatHistoryBrief(channel.entries)
1124
1128
  });
1125
1129
  }
1126
1130
  }
@@ -27,6 +27,6 @@ export interface UploadResult {
27
27
  files: DifyInputFileObject[];
28
28
  dropped: string[];
29
29
  }
30
- /** 提取并上传消息中的文件(先按数量上限截断候选,再逐个上传) */
31
- export declare function uploadMessageFiles(ctx: Context, client: DifyClient, session: Session, difyUser: string, limit: UploadLimit): Promise<UploadResult>;
30
+ /** 上传一批候选(入口按 type:source 去重,再按数量上限截断、逐个上传) */
31
+ export declare function uploadCandidates(ctx: Context, client: DifyClient, candidates: FileCandidate[], difyUser: string, limit: UploadLimit): Promise<UploadResult>;
32
32
  export {};
@@ -28,6 +28,8 @@ export declare class RequestRunner {
28
28
  /** 应用类型:schema 显式指定优先,auto 时用探测结果(未探测到回退 chat) */
29
29
  private appTypeOf;
30
30
  run(fireCtx: FireContext): Promise<void>;
31
+ /** 引用判定:开关开启、回复耗时达到阈值、且由消息触发(定时与密度没有单一触发消息) */
32
+ private shouldQuoteReply;
31
33
  /** 按应用类型分流:workflow/completion 无会话,chat 系续接 conversation_id */
32
34
  private dispatch;
33
35
  private deliver;
@@ -15,6 +15,8 @@ export declare class ConversationStore {
15
15
  apps: Map<string, AppConfigEntry>);
16
16
  /** 粒度归一:bindingKey 与 difyUser 共用,保证两者始终对齐 */
17
17
  private resolveScopeDimension;
18
+ /** 私聊判定:有 session 读 session,无 session 的主动触发读池透传的标记 */
19
+ private isDirectOf;
18
20
  /** bindingKey = 应用 ID + 粒度 + 平台身份 */
19
21
  computeBindingKey(appId: string, rule: DifyRule, fireCtx: FireContext): string;
20
22
  /**
@@ -32,10 +34,15 @@ export declare class ConversationStore {
32
34
  /**
33
35
  * 重置匹配的绑定:本地删行 + 尽力删除 Dify 侧会话(失败仅记录)。
34
36
  * 表已无 guildKey 列,按频道匹配时全表读回后解析 bindingKey 过滤(行数量级很小)。
37
+ * 覆盖 guild / user(群内)/ channel 段与私聊执行时的 private 段;
38
+ * global 段跨频道,不随单频道重置。
35
39
  */
36
40
  resetWhere(filter: {
37
41
  bindingKey?: string;
38
42
  guildKey?: string;
43
+ channelKey?: string;
44
+ isDirect?: boolean;
45
+ userId?: string;
39
46
  }): Promise<number>;
40
47
  /** 按应用 ID(apps 配置的 appId 字段)反查应用 */
41
48
  private findByAppId;
@@ -2,10 +2,10 @@ import { Session } from 'koishi';
2
2
  import { MessageTriggerType, TriggerCondition, TriggerScope } from '../types';
3
3
  export interface ConditionMatched {
4
4
  type: MessageTriggerType;
5
- /** 触发后用作 content 变量的剥离文本(query 由引擎按统一消息格式另行构造) */
6
- queryText: string;
5
+ /** content 变量的剥离文本,保留 @ 与图片占位(query 由引擎按统一消息格式另行构造) */
6
+ contentText: string;
7
7
  }
8
- /** 评估单个消息条件;返回 null 表示未命中 */
8
+ /** 评估单个消息条件;返回 null 表示未命中(匹配基于纯文本,@ 元素不参与判定) */
9
9
  export declare function evaluateCondition(condition: TriggerCondition, session: Session): ConditionMatched | null;
10
10
  /** 作用域过滤:留空的维度不做限制 */
11
11
  export declare function matchScope(scope: TriggerScope, session: Session): boolean;
@@ -10,6 +10,8 @@ export interface PendingMessage {
10
10
  }
11
11
  /** 冷却通道:message = 规则内全部消息类条件共享;density:N = 第 N 个密度条件独立 */
12
12
  export type CooldownLane = 'message' | `density:${number}`;
13
+ /** 密度通道名的下标前缀 */
14
+ export declare const DENSITY_LANE_PREFIX = "density:";
13
15
  /** 密度条件的通道名 */
14
16
  export declare function densityLane(index: number): CooldownLane;
15
17
  /** 到期扫描交出的通道产物 */
@@ -34,13 +36,15 @@ export declare class TriggerCooldown {
34
36
  /** 触发发起时登记冷却 */
35
37
  begin(channelKey: string, ruleName: string, lane: CooldownLane, now: number): void;
36
38
  isCooling(channelKey: string, ruleName: string, lane: CooldownLane, now: number): boolean;
37
- /** 滑动窗口顺延:截止时间顺延到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
39
+ /** 滑动窗口顺延:截止推到「now + 最小窗口」,不超过「触发时刻 + 最大窗口」 */
40
+ private slide;
41
+ /** 滑动窗口顺延:已到期的通道不复活 */
38
42
  extend(channelKey: string, ruleName: string, lane: CooldownLane, now: number): void;
39
- /** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动) */
43
+ /** 该频道所有密度通道由一条过路消息顺延(密度冷却期内的消息积攒驱动窗口滑动);已到期的不复活 */
40
44
  extendDensityLanes(channelKey: string, now: number): void;
41
45
  /** 冷却期内缓存一条命中消息(仅消息通道) */
42
46
  bufferMessage(channelKey: string, ruleName: string, message: PendingMessage): void;
43
- /** 到期扫描:取走全部到期通道(连缓冲一并交出),由调用方决定是否重新登记 */
44
- collectExpired(now: number): ExpiredCooldown[];
47
+ /** 到期扫描:取走到期通道(连缓冲一并交出),由调用方决定是否重新登记;限定频道时只扫该频道 */
48
+ collectExpired(now: number, channelKey?: string): ExpiredCooldown[];
45
49
  private lanesOf;
46
50
  }
@@ -33,7 +33,7 @@ export declare class TriggerEngine {
33
33
  * 频道身份取自池内最近一条消息;无消息的频道自然跳过。定时不受冷却影响。
34
34
  */
35
35
  private fireSchedule;
36
- /** 每秒扫描冷却到期:消息通道合并触发,密度通道复查池 */
36
+ /** 扫描冷却到期(带频道只扫该频道,消息路径用;每秒 interval 全量扫描):消息通道合并触发,密度通道复查池 */
37
37
  private tickCooldown;
38
38
  private onCooldownExpired;
39
39
  }
package/lib/types.d.ts CHANGED
@@ -110,6 +110,10 @@ export interface FireContext {
110
110
  rule: DifyRule;
111
111
  /** 发给 Dify 的 query:统一消息格式(消息触发为单行,窗口触发为逐行拼接) */
112
112
  queryText: string;
113
- /** content 变量的值:消息触发为剥离文本,窗口触发为统一格式拼接 */
113
+ /** content 变量的值:消息触发为剥离文本(剔除 @bot),窗口触发为精简行拼接 */
114
114
  contentText: string;
115
+ /** 本次触发相关消息的图片 URL(合并/密度/定时的池或缓冲条目合集):供上传管线补齐非首条消息的图片 */
116
+ imageUrls?: string[];
117
+ /** 无 session 的主动触发(定时/密度到期)从消息池透传的私聊标记:会话绑定与 isDirect 变量用 */
118
+ isDirect?: boolean;
115
119
  }
@@ -1,4 +1,6 @@
1
1
  import { Session } from 'koishi';
2
+ /** 图片占位符携带的 URL 摘要长度(hex 位) */
3
+ export declare const IMAGE_HASH_LENGTH = 8;
2
4
  /**
3
5
  * 会话文本提取的统一入口。此前有四个变体(条件匹配、历史记录、变量、@ 剥离)
4
6
  * 各自实现「elements 有则取 text 元素,否则解析 content」,trim 与解析行为已经漂移,
@@ -6,21 +8,24 @@ import { Session } from 'koishi';
6
8
  */
7
9
  /** 把可解析的消息文本(含元素标记)转纯文本;解析失败回退原文 */
8
10
  export declare function parseTextSafe(content: string | undefined): string;
9
- interface ExtractTextOptions {
10
- /** 去掉 @ 元素(at-bot 触发的 query 语义) */
11
- excludeAt?: boolean;
12
- }
13
- /** 会话消息纯文本:优先 elements,退回 content 解析;统一 trim */
14
- export declare function extractText(session: Session, options?: ExtractTextOptions): string;
11
+ /** 会话消息纯文本:优先 elements,退回 content 解析;统一 trim(@ 元素不产生文本,仅供条件匹配) */
12
+ export declare function extractText(session: Session): string;
15
13
  /** 消息中的图片元素在文本形态里的占位符(图片无 URL 时的兜底形态) */
16
14
  export declare const IMAGE_PLACEHOLDER = "[\u56FE\u7247]";
17
- /** 图片占位符带 URL 摘要前 8 位:同 URL 同标识,query 里的占位符与上传文件可全局对应 */
15
+ /** 图片占位符带 URL 摘要:同 URL 同标识,query 里的占位符与上传文件可全局对应 */
18
16
  export declare function imagePlaceholder(url?: string): string;
19
17
  /** 给被丢弃图片的占位符追加已丢弃注记:按 URL 摘要全局定位,无需与元素顺序对齐 */
20
18
  export declare function annotateDroppedPlaceholders(text: string, droppedUrls: string[]): string;
19
+ interface ExtractVerbatimOptions {
20
+ /** 跳过 @ 机器人自身的渲染(content 变量语义:@bot 是触发结构而非消息内容) */
21
+ excludeBotAt?: boolean;
22
+ }
21
23
  /**
22
- * 带媒体占位符的消息文本:text 元素按序拼接,img/image 元素渲染为 [image:URL摘要],
23
- * 供聊天记录呈现(历史消息变量 / 定时摘要 / 密度触发共用)。
24
+ * 消息原貌文本:text 元素按序拼接,@ 元素渲染为 @名字(缺名字用 ID),
25
+ * img/image 元素渲染为 [image:URL摘要]——统一格式行(query/池/历史)用默认形态,
26
+ * content 变量通道用 excludeBotAt 形态剔除 @bot。
24
27
  */
25
- export declare function extractTextWithPlaceholders(session: Session): string;
28
+ export declare function extractTextWithPlaceholders(session: Session, options?: ExtractVerbatimOptions): string;
29
+ /** 消息中图片元素的 URL 列表(按元素序,与原貌文本中 [image:摘要] 的出现序一致) */
30
+ export declare function extractImageUrls(session: Session): string[];
26
31
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dify-ai",
3
3
  "description": "Invoke Dify apps (chatflow / workflow / completion) in Koishi with triggers and conversation bindings",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "author": "PinkElysia",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/PinkElysiaDev/dify-ai-plugin-for-Koishi#readme",