@tencent-connect/dsh-qqbot 0.2.0 → 0.3.0

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/config.d.ts +4 -2
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/features/history-store.d.ts +8 -0
  7. package/dist/features/history-store.d.ts.map +1 -0
  8. package/dist/features/history-store.js +24 -0
  9. package/dist/features/history-store.js.map +1 -0
  10. package/dist/gateway/bootstrap.d.ts +12 -0
  11. package/dist/gateway/bootstrap.d.ts.map +1 -0
  12. package/dist/gateway/bootstrap.js +71 -0
  13. package/dist/gateway/bootstrap.js.map +1 -0
  14. package/dist/gateway/index.d.ts +3 -0
  15. package/dist/gateway/index.d.ts.map +1 -0
  16. package/dist/gateway/index.js +3 -0
  17. package/dist/gateway/index.js.map +1 -0
  18. package/dist/gateway/middleware-setup.d.ts +12 -0
  19. package/dist/gateway/middleware-setup.d.ts.map +1 -0
  20. package/dist/gateway/middleware-setup.js +80 -0
  21. package/dist/gateway/middleware-setup.js.map +1 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -128
  25. package/dist/index.js.map +1 -1
  26. package/dist/middleware/attachment.d.ts +11 -0
  27. package/dist/middleware/attachment.d.ts.map +1 -0
  28. package/dist/middleware/attachment.js +17 -0
  29. package/dist/middleware/attachment.js.map +1 -0
  30. package/dist/session/session-manager.d.ts.map +1 -1
  31. package/dist/session/session-manager.js +4 -5
  32. package/dist/session/session-manager.js.map +1 -1
  33. package/dist/setup.js +6 -6
  34. package/dist/setup.js.map +1 -1
  35. package/dist/transport/attachment.d.ts +18 -0
  36. package/dist/transport/attachment.d.ts.map +1 -0
  37. package/dist/transport/attachment.js +106 -0
  38. package/dist/transport/attachment.js.map +1 -0
  39. package/dist/transport/events.d.ts +80 -0
  40. package/dist/transport/events.d.ts.map +1 -0
  41. package/dist/transport/events.js +62 -0
  42. package/dist/transport/events.js.map +1 -0
  43. package/dist/transport/inbound.d.ts +1 -1
  44. package/dist/transport/inbound.d.ts.map +1 -1
  45. package/dist/transport/inbound.js +39 -14
  46. package/dist/transport/inbound.js.map +1 -1
  47. package/dist/transport/outbound-buffer.d.ts +12 -7
  48. package/dist/transport/outbound-buffer.d.ts.map +1 -1
  49. package/dist/transport/outbound-buffer.js +20 -22
  50. package/dist/transport/outbound-buffer.js.map +1 -1
  51. package/dist/transport/outbound.d.ts +7 -6
  52. package/dist/transport/outbound.d.ts.map +1 -1
  53. package/dist/transport/outbound.js +116 -111
  54. package/dist/transport/outbound.js.map +1 -1
  55. package/dist/transport/streaming-writer.d.ts +47 -0
  56. package/dist/transport/streaming-writer.d.ts.map +1 -0
  57. package/dist/transport/streaming-writer.js +110 -0
  58. package/dist/transport/streaming-writer.js.map +1 -0
  59. package/dist/types.d.ts +11 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +6 -4
@@ -1,13 +1,20 @@
1
1
  /**
2
2
  * OutboundBuffer — 出站文本缓冲
3
3
  *
4
- * 收集流式 chunk 并 debounce 发送,独立文件便于单测。
4
+ * 收集流式 chunk,流式优先投递(StreamingWriter),降级为静态发送。
5
+ * 独立文件便于单测。
5
6
  */
6
7
  import type { SessionRecord } from '../session/index.js';
7
8
  import type { Logger, ReplyTarget } from '../types.js';
9
+ /** QQ 流式会话(openStream 返回) */
10
+ export interface StreamSessionLike {
11
+ update(content: string): Promise<unknown>;
12
+ complete(): Promise<unknown>;
13
+ }
8
14
  /** QQ Bot 发送接口 */
9
15
  export interface QQBotSender {
10
16
  sendMarkdown(target: ReplyTarget, content: string): Promise<unknown>;
17
+ openStream(target: ReplyTarget): StreamSessionLike;
11
18
  }
12
19
  export declare class OutboundBuffer {
13
20
  private readonly record;
@@ -15,18 +22,16 @@ export declare class OutboundBuffer {
15
22
  private readonly limit;
16
23
  private readonly logger;
17
24
  private buffer;
18
- private timer;
19
25
  private flushing;
20
- constructor(record: SessionRecord, bot: QQBotSender, limit: number, logger: Logger);
26
+ private readonly writer;
27
+ constructor(record: SessionRecord, bot: QQBotSender, limit: number, logger: Logger, streamingEnabled: boolean);
21
28
  /** 追加文本增量 */
22
29
  append(text: string): void;
23
30
  /** 获取当前累积文本 */
24
31
  get text(): string;
25
- /** 安排延迟发送(debounce) */
26
- scheduleSend(delayMs: number): void;
27
- /** 立即发送所有累积文本 */
32
+ /** 发送所有累积文本:流式优先,降级静态 */
28
33
  flush(): Promise<void>;
29
- /** 取消未发送的定时器 */
34
+ /** 取消(异常/丢弃),中止流式并清空缓冲 */
30
35
  cancel(): void;
31
36
  }
32
37
  //# sourceMappingURL=outbound-buffer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-buffer.d.ts","sourceRoot":"","sources":["../../src/transport/outbound-buffer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAGvD,kBAAkB;AAClB,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,qBAAa,cAAc;IAMvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAS;gBAGN,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;IAGjC,aAAa;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1B,eAAe;IACf,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,uBAAuB;IACvB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQnC,iBAAiB;IACX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5B,gBAAgB;IAChB,MAAM,IAAI,IAAI;CAOf"}
1
+ {"version":3,"file":"outbound-buffer.d.ts","sourceRoot":"","sources":["../../src/transport/outbound-buffer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAOvD,6BAA6B;AAC7B,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9B;AAED,kBAAkB;AAClB,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAAC;CACpD;AAED,qBAAa,cAAc;IAMvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;gBAG7B,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAC/B,gBAAgB,EAAE,OAAO;IAO3B,aAAa;IACN,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKjC,eAAe;IACf,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,yBAAyB;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBnC,0BAA0B;IACnB,MAAM,IAAI,IAAI;CAItB"}
@@ -1,64 +1,62 @@
1
1
  import { chunkMarkdownText } from './chunker.js';
2
+ import { StreamingWriter } from './streaming-writer.js';
3
+ /** 流式输出节流间隔(ms):连续 chunk 累积后停顿该间隔才推送 */
4
+ const STREAM_THROTTLE_MS = 200;
2
5
  export class OutboundBuffer {
3
6
  record;
4
7
  bot;
5
8
  limit;
6
9
  logger;
7
10
  buffer = '';
8
- timer = null;
9
11
  flushing = false;
10
- constructor(record, bot, limit, logger) {
12
+ writer;
13
+ constructor(record, bot, limit, logger, streamingEnabled) {
11
14
  this.record = record;
12
15
  this.bot = bot;
13
16
  this.limit = limit;
14
17
  this.logger = logger;
18
+ this.writer = streamingEnabled
19
+ ? new StreamingWriter({ bot, target: record.replyTarget, logger, throttleMs: STREAM_THROTTLE_MS })
20
+ : null;
15
21
  }
16
22
  /** 追加文本增量 */
17
23
  append(text) {
18
24
  this.buffer += text;
25
+ this.writer?.append(text);
19
26
  }
20
27
  /** 获取当前累积文本 */
21
28
  get text() {
22
29
  return this.buffer;
23
30
  }
24
- /** 安排延迟发送(debounce) */
25
- scheduleSend(delayMs) {
26
- if (this.timer)
27
- return;
28
- this.timer = setTimeout(() => {
29
- this.timer = null;
30
- void this.flush();
31
- }, delayMs);
32
- }
33
- /** 立即发送所有累积文本 */
31
+ /** 发送所有累积文本:流式优先,降级静态 */
34
32
  async flush() {
35
33
  if (this.flushing || !this.buffer.trim())
36
34
  return;
37
35
  this.flushing = true;
38
- if (this.timer) {
39
- clearTimeout(this.timer);
40
- this.timer = null;
41
- }
42
36
  try {
37
+ if (this.writer) {
38
+ await this.writer.finish();
39
+ // 流式成功(未降级)→ 直接返回
40
+ if (!this.writer.shouldFallback)
41
+ return;
42
+ }
43
+ // 降级:静态发送(writer 不存在 or 流式失败)
43
44
  const chunks = chunkMarkdownText(this.buffer, this.limit);
44
45
  for (const chunk of chunks) {
45
46
  await this.bot.sendMarkdown(this.record.replyTarget, chunk);
46
47
  }
47
48
  }
48
49
  catch (err) {
49
- this.logger.error(`im-qqbot: sendMarkdown failed: ${err instanceof Error ? err.message : String(err)}`);
50
+ this.logger.error(`im-qqbot: flush failed: ${err instanceof Error ? err.message : String(err)}`);
50
51
  }
51
52
  finally {
52
53
  this.buffer = '';
53
54
  this.flushing = false;
54
55
  }
55
56
  }
56
- /** 取消未发送的定时器 */
57
+ /** 取消(异常/丢弃),中止流式并清空缓冲 */
57
58
  cancel() {
58
- if (this.timer) {
59
- clearTimeout(this.timer);
60
- this.timer = null;
61
- }
59
+ this.writer?.abort();
62
60
  this.buffer = '';
63
61
  }
64
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-buffer.js","sourceRoot":"","sources":["../../src/transport/outbound-buffer.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAOjD,MAAM,OAAO,cAAc;IAMN;IACA;IACA;IACA;IARX,MAAM,GAAG,EAAE,CAAC;IACZ,KAAK,GAAyC,IAAI,CAAC;IACnD,QAAQ,GAAG,KAAK,CAAC;IAEzB,YACmB,MAAqB,EACrB,GAAgB,EAChB,KAAa,EACb,MAAc;QAHd,WAAM,GAAN,MAAM,CAAe;QACrB,QAAG,GAAH,GAAG,CAAa;QAChB,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAQ;IAC9B,CAAC;IAEJ,aAAa;IACb,MAAM,CAAC,IAAY;QACjB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IACtB,CAAC;IAED,eAAe;IACf,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,uBAAuB;IACvB,YAAY,CAAC,OAAe;QAC1B,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC;IAED,iBAAiB;IACjB,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;YAAE,OAAO;QACjD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAErB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1G,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,MAAM;QACJ,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACnB,CAAC;CACF"}
1
+ {"version":3,"file":"outbound-buffer.js","sourceRoot":"","sources":["../../src/transport/outbound-buffer.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,wCAAwC;AACxC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAc/B,MAAM,OAAO,cAAc;IAMN;IACA;IACA;IACA;IARX,MAAM,GAAG,EAAE,CAAC;IACZ,QAAQ,GAAG,KAAK,CAAC;IACR,MAAM,CAAyB;IAEhD,YACmB,MAAqB,EACrB,GAAgB,EAChB,KAAa,EACb,MAAc,EAC/B,gBAAyB;QAJR,WAAM,GAAN,MAAM,CAAe;QACrB,QAAG,GAAH,GAAG,CAAa;QAChB,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAQ;QAG/B,IAAI,CAAC,MAAM,GAAG,gBAAgB;YAC5B,CAAC,CAAC,IAAI,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC;YAClG,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IAED,aAAa;IACN,MAAM,CAAC,IAAY;QACxB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;QACpB,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,eAAe;IACf,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,yBAAyB;IAClB,KAAK,CAAC,KAAK;QAChB,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;YAAE,OAAO;QACjD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAErB,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBAC3B,kBAAkB;gBAClB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc;oBAAE,OAAO;YAC1C,CAAC;YAED,8BAA8B;YAC9B,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IAED,0BAA0B;IACnB,MAAM;QACX,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACnB,CAAC;CACF"}
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * 出站处理器 — dsh session/event → QQ 消息发送
3
+ *
4
+ * 采用路由器模式:OutboundRouter 持有会话级状态(文本缓冲、工具调用记录),
5
+ * 事件解析归一化在 events.ts,路由按事件类型分发到私有方法。
3
6
  */
4
7
  import type { SessionManager } from '../session/index.js';
5
8
  import type { ImQQBotConfig } from '../config.js';
6
9
  import type { Logger } from '../types.js';
7
10
  import { type QQBotSender } from './outbound-buffer.js';
8
11
  import { type ToolsRegistryLike } from './tool-presenter.js';
12
+ import { type RawSessionEvent } from './events.js';
9
13
  export type { QQBotSender } from './outbound-buffer.js';
10
14
  export type { ToolsRegistryLike } from './tool-presenter.js';
11
- /** dsh SessionEvent 简化类型 */
12
- export interface SessionEvent {
13
- type: string;
14
- data: Record<string, unknown>;
15
- }
15
+ /** 出站处理器签名(注册到 ctx.on('session/event')) */
16
+ export type OutboundHandler = (session: SessionLike, event: RawSessionEvent) => void;
16
17
  /** dsh Session 简化类型 */
17
18
  export interface SessionLike {
18
19
  header: {
@@ -25,5 +26,5 @@ export interface SessionLike {
25
26
  * 返回一个 handler 函数,应注册到 ctx.on('session/event', handler)。
26
27
  * toolsRegistry 用于工具结果的结构化展示(参考 dsh-TUI 的 presentResult)。
27
28
  */
28
- export declare function createOutboundHandler(manager: SessionManager, bot: QQBotSender, config: ImQQBotConfig, logger: Logger, toolsRegistry?: ToolsRegistryLike): (session: SessionLike, event: SessionEvent) => void;
29
+ export declare function createOutboundHandler(manager: SessionManager, bot: QQBotSender, config: ImQQBotConfig, logger: Logger, toolsRegistry?: ToolsRegistryLike): OutboundHandler;
29
30
  //# sourceMappingURL=outbound.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/transport/outbound.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAoB,KAAK,iBAAiB,EAAuB,MAAM,qBAAqB,CAAC;AAEpG,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,4BAA4B;AAC5B,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,uBAAuB;AACvB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACxB;AAQD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,iBAAiB,GAChC,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAqCrD"}
1
+ {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/transport/outbound.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAoB,KAAK,iBAAiB,EAAuB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAErF,uBAAuB;AACvB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACxB;AAoJD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,iBAAiB,GAChC,eAAe,CAGjB"}
@@ -1,138 +1,143 @@
1
1
  import { chunkMarkdownText } from './chunker.js';
2
2
  import { OutboundBuffer } from './outbound-buffer.js';
3
3
  import { formatToolResult } from './tool-presenter.js';
4
+ import { parseEvent, extractTurnError, } from './events.js';
5
+ /** 不展示给用户的轮次错误码(底层传输/网络错误,对用户无意义,且常被重试兜住) */
6
+ const SILENT_TURN_ERROR_CODES = new Set(['STREAM_CLOSED']);
4
7
  /**
5
- * 创建出站事件处理器
6
- *
7
- * 返回一个 handler 函数,应注册到 ctx.on('session/event', handler)。
8
- * toolsRegistry 用于工具结果的结构化展示(参考 dsh-TUI 的 presentResult)。
8
+ * 出站路由器:持有会话级状态,按事件类型分发到处理器
9
9
  */
10
- export function createOutboundHandler(manager, bot, config, logger, toolsRegistry) {
11
- const buffers = new Map();
12
- const toolCalls = new Map();
13
- const limit = config.textChunkLimit;
14
- return (session, event) => {
15
- const sessionId = session.header.id;
16
- const record = manager.findBySessionId(sessionId);
17
- if (!record)
10
+ class OutboundRouter {
11
+ manager;
12
+ bot;
13
+ config;
14
+ logger;
15
+ toolsRegistry;
16
+ buffers = new Map();
17
+ toolCalls = new Map();
18
+ constructor(manager, bot, config, logger, toolsRegistry) {
19
+ this.manager = manager;
20
+ this.bot = bot;
21
+ this.config = config;
22
+ this.logger = logger;
23
+ this.toolsRegistry = toolsRegistry;
24
+ }
25
+ /** 事件分发入口 */
26
+ route(session, raw) {
27
+ const event = parseEvent(raw);
28
+ if (event === undefined)
29
+ return;
30
+ const record = this.manager.findBySessionId(session.header.id);
31
+ if (record === undefined)
18
32
  return;
19
33
  switch (event.type) {
20
- case 'assistant/chunk': {
21
- handleChunk(sessionId, record, event, buffers, bot, limit, logger);
34
+ case 'assistant/chunk':
35
+ this.onChunk(session.header.id, record, event);
22
36
  break;
23
- }
24
- case 'assistant/message': {
25
- handleMessage(sessionId, record, event, buffers, bot, limit, logger);
37
+ case 'assistant/message':
38
+ this.onMessage(session.header.id, record, event);
26
39
  break;
27
- }
28
- case 'tool/call': {
29
- handleToolCall(event, toolCalls);
40
+ case 'tool/call':
41
+ this.onToolCall(event);
30
42
  break;
31
- }
32
- case 'tool/result': {
33
- handleToolResult(record, event, toolCalls, bot, config, logger, toolsRegistry);
43
+ case 'tool/result':
44
+ this.onToolResult(record, event);
34
45
  break;
35
- }
36
- case 'turn/end': {
37
- handleTurnEnd(sessionId, buffers, logger);
46
+ case 'turn/end':
47
+ this.onTurnEnd(session.header.id, record, event);
38
48
  break;
39
- }
40
49
  }
41
- };
42
- }
43
- /** 记录工具调用(不发送,避免刷屏,等待结果) */
44
- function handleToolCall(event, toolCalls) {
45
- const data = event.data;
46
- if (!data.callId || !data.name)
47
- return;
48
- toolCalls.set(data.callId, { name: data.name, args: data.arguments ?? '' });
49
- }
50
- /** 格式化并发送工具结果(错误始终发送,成功结果按开关) */
51
- function handleToolResult(record, event, toolCalls, bot, config, logger, toolsRegistry) {
52
- const data = event.data;
53
- const callId = data.message?.source?.callId;
54
- if (!callId)
55
- return;
56
- const call = toolCalls.get(callId);
57
- toolCalls.delete(callId);
58
- if (!call)
59
- return;
60
- // 错误始终展示,成功结果按 config.showToolResults
61
- if (data.error === undefined && !config.showToolResults)
62
- return;
63
- const text = formatToolResult(call.name, call.args, event.data, toolsRegistry, record.agent);
64
- if (!text)
65
- return;
66
- void (async () => {
67
- const chunks = chunkMarkdownText(text, config.textChunkLimit);
68
- for (const chunk of chunks) {
69
- try {
70
- await bot.sendMarkdown(record.replyTarget, chunk);
71
- }
72
- catch (err) {
73
- logger.error(`im-qqbot: sendToolResult failed: ${err instanceof Error ? err.message : String(err)}`);
74
- }
50
+ }
51
+ /** 流式文本增量:累积到会话 buffer */
52
+ onChunk(sessionId, record, event) {
53
+ let buffer = this.buffers.get(sessionId);
54
+ if (buffer === undefined) {
55
+ buffer = new OutboundBuffer(record, this.bot, this.config.textChunkLimit, this.logger, this.shouldStream(record));
56
+ this.buffers.set(sessionId, buffer);
75
57
  }
76
- })();
77
- }
78
- /** 处理流式文本增量 */
79
- function handleChunk(sessionId, record, event, buffers, bot, limit, logger) {
80
- const chunk = event.data.chunk;
81
- if (!chunk || chunk.type !== 'text-delta' || !chunk.text)
82
- return;
83
- let buffer = buffers.get(sessionId);
84
- if (!buffer) {
85
- buffer = new OutboundBuffer(record, bot, limit, logger);
86
- buffers.set(sessionId, buffer);
58
+ buffer.append(event.text);
87
59
  }
88
- buffer.append(chunk.text);
89
- }
90
- /** 处理完整 assistant 消息 */
91
- function handleMessage(sessionId, record, event, buffers, bot, limit, logger) {
92
- const buffer = buffers.get(sessionId);
93
- if (buffer && buffer.text.trim()) {
94
- void buffer.flush();
95
- buffers.delete(sessionId);
96
- return;
60
+ /** 是否启用流式:配置开启 + c2c + 有 msgId(群聊不支持流式) */
61
+ shouldStream(record) {
62
+ return this.config.streaming
63
+ && record.replyTarget.scope === 'c2c'
64
+ && !!record.replyTarget.msgId;
65
+ }
66
+ /** 完整 assistant 消息:有流式 buffer 则 flush,否则直接发送文本块 */
67
+ onMessage(sessionId, record, event) {
68
+ const buffer = this.buffers.get(sessionId);
69
+ if (buffer !== undefined && buffer.text.trim()) {
70
+ void buffer.flush();
71
+ this.buffers.delete(sessionId);
72
+ return;
73
+ }
74
+ const textParts = [];
75
+ for (const block of event.content) {
76
+ if (block.type === 'text' && block.text)
77
+ textParts.push(block.text);
78
+ }
79
+ const fullText = textParts.join('\n');
80
+ if (!fullText.trim())
81
+ return;
82
+ void this.send(record, fullText, 'sendMarkdown');
83
+ this.buffers.delete(sessionId);
84
+ }
85
+ /** 工具调用:仅记录,不发送(避免刷屏,等待结果) */
86
+ onToolCall(event) {
87
+ this.toolCalls.set(event.callId, { name: event.name, args: event.arguments });
88
+ }
89
+ /** 工具结果:错误始终发送,成功结果按开关 */
90
+ onToolResult(record, event) {
91
+ const call = this.toolCalls.get(event.callId);
92
+ this.toolCalls.delete(event.callId);
93
+ if (call === undefined)
94
+ return;
95
+ if (event.error === undefined && !this.config.showToolResults)
96
+ return;
97
+ const text = formatToolResult(call.name, call.args, event.raw, this.toolsRegistry, record.agent);
98
+ if (!text)
99
+ return;
100
+ void this.send(record, text, 'sendToolResult');
97
101
  }
98
- const message = event.data;
99
- const blocks = message?.message?.content;
100
- if (!blocks || !Array.isArray(blocks))
101
- return;
102
- const textParts = [];
103
- for (const block of blocks) {
104
- if (block.type === 'text' && block.text) {
105
- textParts.push(block.text);
102
+ /** 轮次结束:清理 buffer,异常结束时告知用户 */
103
+ onTurnEnd(sessionId, record, event) {
104
+ const buffer = this.buffers.get(sessionId);
105
+ if (buffer !== undefined) {
106
+ if (buffer.text.trim()) {
107
+ void buffer.flush();
108
+ }
109
+ else {
110
+ buffer.cancel();
111
+ }
112
+ this.buffers.delete(sessionId);
113
+ }
114
+ const failure = extractTurnError(event.reason);
115
+ if (failure !== undefined && !SILENT_TURN_ERROR_CODES.has(failure.code)) {
116
+ void this.send(record, `⚠️ 本轮异常结束\n\`${failure.code}\`: ${failure.message}`, 'sendTurnEndError');
106
117
  }
118
+ this.logger.debug(`im-qqbot: turn/end sessionId=${sessionId}`);
107
119
  }
108
- const fullText = textParts.join('\n');
109
- if (!fullText.trim())
110
- return;
111
- const chunks = chunkMarkdownText(fullText, limit);
112
- void (async () => {
120
+ /** 统一发送:切分 + 逐 chunk 发送 + 错误记录 */
121
+ async send(record, text, tag) {
122
+ const chunks = chunkMarkdownText(text, this.config.textChunkLimit);
113
123
  for (const chunk of chunks) {
114
124
  try {
115
- await bot.sendMarkdown(record.replyTarget, chunk);
125
+ await this.bot.sendMarkdown(record.replyTarget, chunk);
116
126
  }
117
127
  catch (err) {
118
- logger.error(`im-qqbot: sendMarkdown failed: ${err instanceof Error ? err.message : String(err)}`);
128
+ this.logger.error(`im-qqbot: ${tag} failed: ${err instanceof Error ? err.message : String(err)}`);
119
129
  }
120
130
  }
121
- })();
122
- buffers.delete(sessionId);
123
- }
124
- /** 处理轮次结束,清理 buffer */
125
- function handleTurnEnd(sessionId, buffers, logger) {
126
- const buffer = buffers.get(sessionId);
127
- if (buffer) {
128
- if (buffer.text.trim()) {
129
- void buffer.flush();
130
- }
131
- else {
132
- buffer.cancel();
133
- }
134
- buffers.delete(sessionId);
135
131
  }
136
- logger.debug(`im-qqbot: turn/end sessionId=${sessionId}`);
132
+ }
133
+ /**
134
+ * 创建出站事件处理器
135
+ *
136
+ * 返回一个 handler 函数,应注册到 ctx.on('session/event', handler)。
137
+ * toolsRegistry 用于工具结果的结构化展示(参考 dsh-TUI 的 presentResult)。
138
+ */
139
+ export function createOutboundHandler(manager, bot, config, logger, toolsRegistry) {
140
+ const router = new OutboundRouter(manager, bot, config, logger, toolsRegistry);
141
+ return (session, event) => router.route(session, event);
137
142
  }
138
143
  //# sourceMappingURL=outbound.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"outbound.js","sourceRoot":"","sources":["../../src/transport/outbound.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outbound.js","sourceRoot":"","sources":["../../src/transport/outbound.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * StreamingWriter — QQ 流式文本写入器
3
+ *
4
+ * 边界条件设计:
5
+ * - 节流(throttle):固定间隔 flush,避免每 token 发 HTTP,同时保证长文本分多次推送
6
+ * - 串行队列(chain):保证 update 顺序,finish 前最后一次文本必达
7
+ * - 降级(sentChunkCount === 0):只有从未成功发送分片才降级静态,
8
+ * 避免"已流式部分 + 静态全量"的重复内容
9
+ * - 终态幂等(finished):finish/abort 后忽略 append,重复 finish 不重复 complete
10
+ * - 中止关闭(abort):abort 时若 session 已打开,串行 complete 关闭,避免悬挂
11
+ */
12
+ import type { Logger, ReplyTarget } from '../types.js';
13
+ import type { QQBotSender } from './outbound-buffer.js';
14
+ export interface StreamingWriterDeps {
15
+ bot: QQBotSender;
16
+ target: ReplyTarget;
17
+ logger: Logger;
18
+ /** 节流间隔(ms):连续 append 累积后,停顿该间隔才 update */
19
+ throttleMs: number;
20
+ }
21
+ export declare class StreamingWriter {
22
+ private readonly deps;
23
+ private fullText;
24
+ private session;
25
+ private sentChunkCount;
26
+ private failed;
27
+ private finished;
28
+ private aborted;
29
+ private throttleTimer;
30
+ private chain;
31
+ constructor(deps: StreamingWriterDeps);
32
+ /** 是否应降级为静态发送:从未成功发送过任何分片 */
33
+ get shouldFallback(): boolean;
34
+ /** 累积全文(降级静态发送时使用) */
35
+ get text(): string;
36
+ /** 追加增量文本,节流(throttle)后 update 全量 */
37
+ append(delta: string): void;
38
+ /** 结束:flush 最终文本 + complete 收尾 */
39
+ finish(): Promise<void>;
40
+ /** 中止(异常/取消):关闭已打开的流式会话,避免悬挂 */
41
+ abort(): void;
42
+ /** 入队一次 update(串行,保证顺序) */
43
+ private enqueue;
44
+ /** 推送当前累积文本(首次 openStream + update 全量) */
45
+ private pushUpdate;
46
+ }
47
+ //# sourceMappingURL=streaming-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-writer.d.ts","sourceRoot":"","sources":["../../src/transport/streaming-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAqB,MAAM,sBAAsB,CAAC;AAE3E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,eAAe;IAUP,OAAO,CAAC,QAAQ,CAAC,IAAI;IATxC,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,KAAK,CAAoC;gBAEb,IAAI,EAAE,mBAAmB;IAE7D,6BAA6B;IAC7B,IAAW,cAAc,IAAI,OAAO,CAEnC;IAED,sBAAsB;IACtB,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,qCAAqC;IAC9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAclC,kCAAkC;IACrB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBpC,gCAAgC;IACzB,KAAK,IAAI,IAAI;IAwBpB,2BAA2B;IAC3B,OAAO,CAAC,OAAO;IAIf,0CAA0C;YAC5B,UAAU;CA2BzB"}