dsh-messager 0.1.4

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +86 -0
  3. package/README.md +241 -0
  4. package/assets/icon.png +0 -0
  5. package/cordis.patch.yml +11 -0
  6. package/lib/channels/feishu.d.ts +58 -0
  7. package/lib/channels/feishu.d.ts.map +1 -0
  8. package/lib/channels/feishu.js +77 -0
  9. package/lib/channels/feishu.js.map +1 -0
  10. package/lib/channels/system.d.ts +26 -0
  11. package/lib/channels/system.d.ts.map +1 -0
  12. package/lib/channels/system.js +73 -0
  13. package/lib/channels/system.js.map +1 -0
  14. package/lib/client.js +2001 -0
  15. package/lib/client.js.map +1 -0
  16. package/lib/config-route.d.ts +55 -0
  17. package/lib/config-route.d.ts.map +1 -0
  18. package/lib/config-route.js +170 -0
  19. package/lib/config-route.js.map +1 -0
  20. package/lib/config-shared.d.ts +28 -0
  21. package/lib/config-shared.d.ts.map +1 -0
  22. package/lib/config-shared.js +7 -0
  23. package/lib/config-shared.js.map +1 -0
  24. package/lib/config.d.ts +82 -0
  25. package/lib/config.d.ts.map +1 -0
  26. package/lib/config.js +59 -0
  27. package/lib/config.js.map +1 -0
  28. package/lib/index.d.ts +15 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +152 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/notify.d.ts +75 -0
  33. package/lib/notify.d.ts.map +1 -0
  34. package/lib/notify.js +157 -0
  35. package/lib/notify.js.map +1 -0
  36. package/lib/settings.d.ts +23 -0
  37. package/lib/settings.d.ts.map +1 -0
  38. package/lib/settings.js +23 -0
  39. package/lib/settings.js.map +1 -0
  40. package/lib/signals.d.ts +57 -0
  41. package/lib/signals.d.ts.map +1 -0
  42. package/lib/signals.js +52 -0
  43. package/lib/signals.js.map +1 -0
  44. package/lib/templates.d.ts +40 -0
  45. package/lib/templates.d.ts.map +1 -0
  46. package/lib/templates.js +107 -0
  47. package/lib/templates.js.map +1 -0
  48. package/lib/types/client/card-controller.d.ts +171 -0
  49. package/lib/types/client/card-controller.d.ts.map +1 -0
  50. package/lib/types/client/config.d.ts +20 -0
  51. package/lib/types/client/config.d.ts.map +1 -0
  52. package/lib/types/client/diff.d.ts +26 -0
  53. package/lib/types/client/diff.d.ts.map +1 -0
  54. package/lib/types/client/fetch-scope.d.ts +29 -0
  55. package/lib/types/client/fetch-scope.d.ts.map +1 -0
  56. package/lib/types/client/index.d.ts +24 -0
  57. package/lib/types/client/index.d.ts.map +1 -0
  58. package/lib/types/client/locales.d.ts +92 -0
  59. package/lib/types/client/locales.d.ts.map +1 -0
  60. package/lib/types/client/section.d.ts +16 -0
  61. package/lib/types/client/section.d.ts.map +1 -0
  62. package/lib/types/client/settings-form.d.ts +22 -0
  63. package/lib/types/client/settings-form.d.ts.map +1 -0
  64. package/lib/types/config-shared.d.ts +28 -0
  65. package/lib/types/config-shared.d.ts.map +1 -0
  66. package/lib/types/config.d.ts +82 -0
  67. package/lib/types/config.d.ts.map +1 -0
  68. package/package.json +96 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-shared.js","sourceRoot":"","sources":["../src/config-shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,uCAAuC;AACvC,MAAM,CAAC,MAAM,WAAW,GAAG,sBAAsB,CAAA"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * dsh-messager 配置模型。
3
+ *
4
+ * 本 schema 同时承担两个角色:
5
+ * - host 端的 Loader config(cordis.yml 中该插件行的 `config:`,成为 settings 命名空间的 base 层);
6
+ * - `ctx.settings.register('messager', Config, { base })` 的命名空间 schema,
7
+ * 因此 Web 设置页会自动渲染配置表单,用户层可覆盖 base。
8
+ *
9
+ * 有效值优先级:schema 默认值 → base(cordis.yml)→ 用户层(设置页)。
10
+ */
11
+ import Schema from '@deepseek-ai/schemastery';
12
+ /** 通知内容繁复度。 */
13
+ export type Verbosity = 'minimal' | 'normal' | 'detailed';
14
+ /** 触发时机开关(需求 1:什么时候通知)。 */
15
+ export interface TriggerConfig {
16
+ /** 会话需要交互(审批 / 提问 / 计划待审)时通知。 */
17
+ interaction: boolean;
18
+ /** 任务执行完毕时通知。 */
19
+ completed: boolean;
20
+ /** 任务出错时通知。 */
21
+ error: boolean;
22
+ }
23
+ /** 系统通知通道(OS 级 toast,host 端由 node-notifier 投递)。 */
24
+ export interface SystemChannelConfig {
25
+ enabled: boolean;
26
+ /** 图标:绝对路径(或包内 assets/icon.png 的绝对路径)。 */
27
+ icon?: string;
28
+ verbosity: Verbosity;
29
+ }
30
+ /** 浏览器通知通道(Web Notification API,client 端投递)。 */
31
+ export interface BrowserChannelConfig {
32
+ enabled: boolean;
33
+ /** 图标:URL 或 data URL。 */
34
+ icon?: string;
35
+ /** 仅当页面隐藏/未聚焦时才弹通知,避免看着界面还被打扰。 */
36
+ onlyWhenHidden: boolean;
37
+ verbosity: Verbosity;
38
+ }
39
+ /** 飞书机器人通道(webhook 版,即自定义机器人 webhook,host 端投递)。 */
40
+ export interface FeishuChannelConfig {
41
+ enabled: boolean;
42
+ /** 飞书自定义机器人 webhook 地址。 */
43
+ webhookUrl?: string;
44
+ /** 签名密钥(飞书机器人 webhook“安全设置-签名校验”),配置后按 HMAC-SHA256 签名。 */
45
+ secret?: string;
46
+ /** 单次请求超时(ms)。 */
47
+ timeoutMs: number;
48
+ verbosity: Verbosity;
49
+ }
50
+ /** 去重 / 节流配置。 */
51
+ export interface DedupConfig {
52
+ /** 同一会话同一类触发的冷却时间(ms)。 */
53
+ interactionCooldownMs: number;
54
+ /** 完成通知防抖窗口(ms):等待 turn/end 事件以丰富内容并合并边界抖动。 */
55
+ completedDebounceMs: number;
56
+ /** 每通道每分钟通知上限(防止第三方通道限流/刷屏)。 */
57
+ perChannelPerMinute: number;
58
+ }
59
+ /** 消息内容配置。 */
60
+ export interface MessageConfig {
61
+ /** 标题前缀,例如 "[DSH]"。 */
62
+ titlePrefix?: string;
63
+ /** 正文是否附带会话标题。 */
64
+ includeSessionTitle: boolean;
65
+ /** GUI 地址,用于通知中的“打开”链接/按钮。 */
66
+ guiUrl: string;
67
+ }
68
+ export interface Config {
69
+ triggers: TriggerConfig;
70
+ system: SystemChannelConfig;
71
+ browser: BrowserChannelConfig;
72
+ feishu: FeishuChannelConfig;
73
+ dedup: DedupConfig;
74
+ message: MessageConfig;
75
+ }
76
+ export declare const Config: Schema<Config>;
77
+ /**
78
+ * 用 schema 默认值解析一份配置(输入可省略任意字段)。
79
+ * 与 cordis Loader 的校验同源,保证“默认值 → base → 用户层”的解析一致。
80
+ */
81
+ export declare function resolveConfig(input?: Partial<Config>): Config;
82
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAE7C,eAAe;AACf,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAA;AAEzD,2BAA2B;AAC3B,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,WAAW,EAAE,OAAO,CAAA;IACpB,iBAAiB;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,eAAe;IACf,KAAK,EAAE,OAAO,CAAA;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kCAAkC;IAClC,cAAc,EAAE,OAAO,CAAA;IACvB,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,mDAAmD;AACnD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,iBAAiB;AACjB,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,+CAA+C;IAC/C,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAED,cAAc;AACd,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB;IAClB,mBAAmB,EAAE,OAAO,CAAA;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,aAAa,CAAA;IACvB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,OAAO,EAAE,oBAAoB,CAAA;IAC7B,MAAM,EAAE,mBAAmB,CAAA;IAC3B,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,EAAE,aAAa,CAAA;CACvB;AAID,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAsChC,CAAA;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,OAAO,CAAC,MAAM,CAAM,GAAG,MAAM,CAEjE"}
package/lib/config.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * dsh-messager 配置模型。
3
+ *
4
+ * 本 schema 同时承担两个角色:
5
+ * - host 端的 Loader config(cordis.yml 中该插件行的 `config:`,成为 settings 命名空间的 base 层);
6
+ * - `ctx.settings.register('messager', Config, { base })` 的命名空间 schema,
7
+ * 因此 Web 设置页会自动渲染配置表单,用户层可覆盖 base。
8
+ *
9
+ * 有效值优先级:schema 默认值 → base(cordis.yml)→ 用户层(设置页)。
10
+ */
11
+ import Schema from '@deepseek-ai/schemastery';
12
+ const verbosity = Schema.union(['minimal', 'normal', 'detailed']);
13
+ export const Config = Schema.object({
14
+ triggers: Schema.object({
15
+ interaction: Schema.boolean().default(true),
16
+ completed: Schema.boolean().default(true),
17
+ error: Schema.boolean().default(true),
18
+ }),
19
+ system: Schema.object({
20
+ enabled: Schema.boolean().default(true),
21
+ // 无 default 的字段即可选(undefined 允许)
22
+ // 注意:node-notifier 需要文件路径且该文件必须存在;Linux(notify-send)与
23
+ // macOS(terminal-notifier)对缺失路径可能直接失败而非像 Windows 那样降级,
24
+ // 通道层虽已做存在性校验,仍建议配置有效的绝对路径。
25
+ icon: Schema.string(),
26
+ verbosity: verbosity.default('normal'),
27
+ }),
28
+ browser: Schema.object({
29
+ enabled: Schema.boolean().default(true),
30
+ icon: Schema.string(),
31
+ onlyWhenHidden: Schema.boolean().default(true),
32
+ verbosity: verbosity.default('normal'),
33
+ }),
34
+ feishu: Schema.object({
35
+ enabled: Schema.boolean().default(false),
36
+ webhookUrl: Schema.string(),
37
+ secret: Schema.string().role('secret'),
38
+ timeoutMs: Schema.number().default(5000),
39
+ verbosity: verbosity.default('normal'),
40
+ }),
41
+ dedup: Schema.object({
42
+ interactionCooldownMs: Schema.number().default(10000),
43
+ completedDebounceMs: Schema.number().default(1000),
44
+ perChannelPerMinute: Schema.number().default(20),
45
+ }),
46
+ message: Schema.object({
47
+ titlePrefix: Schema.string(),
48
+ includeSessionTitle: Schema.boolean().default(true),
49
+ guiUrl: Schema.string().default('http://127.0.0.1:3080'),
50
+ }),
51
+ });
52
+ /**
53
+ * 用 schema 默认值解析一份配置(输入可省略任意字段)。
54
+ * 与 cordis Loader 的校验同源,保证“默认值 → base → 用户层”的解析一致。
55
+ */
56
+ export function resolveConfig(input = {}) {
57
+ return Config(input);
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AA0E7C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAU,CAAC,CAAA;AAE1E,MAAM,CAAC,MAAM,MAAM,GAAmB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QAC3C,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACzC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;KACtC,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACvC,iCAAiC;QACjC,sDAAsD;QACtD,uDAAuD;QACvD,4BAA4B;QAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;QACrB,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC;KACvC,CAAC;IACF,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACvC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;QACrB,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QAC9C,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC;KACvC,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;QACxC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACxC,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC;KACvC,CAAC;IACF,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;QACnB,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;QACrD,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QAClD,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;KACjD,CAAC;IACF,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;QAC5B,mBAAmB,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACnD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;KACzD,CAAC;CACH,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAyB,EAAE;IACvD,OAAO,MAAM,CAAC,KAAe,CAAC,CAAA;AAChC,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * dsh-messager —— DeepSeek Harness 通知插件(服务端 host 端)。
3
+ *
4
+ * 功能:会话需要交互(审批/提问/计划待审)、任务完成、任务出错时,
5
+ * 通过系统通知(node-notifier)、飞书机器人(webhook)推送提醒;
6
+ * 浏览器通知由浏览器 client 端(src/client/index.ts)投递。
7
+ *
8
+ * 配置:Loader config(cordis.yml)注册为 settings 命名空间 `messager`
9
+ * 的 base 层,设置页可覆盖;两者变更均热生效。
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import { type Config } from './config.js';
13
+ export declare const name = "dsh-messager";
14
+ export declare function apply(ctx: Context, config: Config): void;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAQxD,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAqDlC,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAqFjD"}
package/lib/index.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * dsh-messager —— DeepSeek Harness 通知插件(服务端 host 端)。
3
+ *
4
+ * 功能:会话需要交互(审批/提问/计划待审)、任务完成、任务出错时,
5
+ * 通过系统通知(node-notifier)、飞书机器人(webhook)推送提醒;
6
+ * 浏览器通知由浏览器 client 端(src/client/index.ts)投递。
7
+ *
8
+ * 配置:Loader config(cordis.yml)注册为 settings 命名空间 `messager`
9
+ * 的 base 层,设置页可覆盖;两者变更均热生效。
10
+ */
11
+ import { resolveConfig } from './config.js';
12
+ import { interactionSignalOf, errorMessageOf, turnEndReasonOf } from './signals.js';
13
+ import { NotificationDispatcher } from './notify.js';
14
+ import { createSystemChannel } from './channels/system.js';
15
+ import { createFeishuChannel } from './channels/feishu.js';
16
+ import { registerMessagerSettings } from './settings.js';
17
+ import { mountConfigRoutes } from './config-route.js';
18
+ export const name = 'dsh-messager';
19
+ /** 按当前配置构建启用的通道。 */
20
+ function buildChannels(config) {
21
+ const channels = [];
22
+ if (config.system.enabled) {
23
+ channels.push(createSystemChannel({
24
+ ...(config.system.icon === undefined ? {} : { icon: config.system.icon }),
25
+ }));
26
+ }
27
+ if (config.feishu.enabled && config.feishu.webhookUrl !== undefined) {
28
+ channels.push(createFeishuChannel({
29
+ webhookUrl: config.feishu.webhookUrl,
30
+ ...(config.feishu.secret === undefined ? {} : { secret: config.feishu.secret }),
31
+ timeoutMs: config.feishu.timeoutMs,
32
+ }));
33
+ }
34
+ return channels;
35
+ }
36
+ /**
37
+ * 从会话事件日志取最近的非空标题。会话日志包含持久化重放的历史事件
38
+ * (含 session/title),因此进程重启后恢复的会话也能取到标题 ——
39
+ * 实时 session/event 只在事件发生时投递一次,不会重放历史。
40
+ */
41
+ function sessionTitleOf(session) {
42
+ const events = session.events;
43
+ for (let index = events.length - 1; index >= 0; index--) {
44
+ const event = events[index];
45
+ if (event === undefined)
46
+ continue;
47
+ if (event.type !== 'session/title')
48
+ continue;
49
+ if (event.data.title === '')
50
+ continue;
51
+ return event.data.title;
52
+ }
53
+ return undefined;
54
+ }
55
+ /** 已扫描过日志且确认无标题的会话(避免对无标题会话反复全量扫描大日志)。 */
56
+ const noTitleSessions = new Set();
57
+ /** 标题兜底:实时 map 缺失时从会话日志补取一次;新标题事件到达时解除负缓存。 */
58
+ function ensureSessionTitle(dispatcher, session) {
59
+ const sessionId = session.id;
60
+ if (dispatcher.hasTitle(sessionId))
61
+ return;
62
+ if (noTitleSessions.has(sessionId))
63
+ return;
64
+ const title = sessionTitleOf(session);
65
+ if (title === undefined) {
66
+ noTitleSessions.add(sessionId);
67
+ return;
68
+ }
69
+ dispatcher.noteSessionTitle(sessionId, title);
70
+ }
71
+ export function apply(ctx, config) {
72
+ // 配置层:settings 服务可能晚于本插件挂载,必须用 ctx.inject 动态接入;
73
+ // 服务未就绪(如 headless profile)期间回退 Loader config 默认值。
74
+ const dispatcher = new NotificationDispatcher({
75
+ config: resolveConfig(config),
76
+ channels: buildChannels(resolveConfig(config)),
77
+ hooks: {
78
+ logWarn: (message) => ctx.logger.warn(`[dsh-messager] ${message}`),
79
+ logDebug: (message) => ctx.logger.debug(`[dsh-messager] ${message}`),
80
+ },
81
+ });
82
+ // 配置热更新:settings 就绪后注册命名空间(base = Loader config),
83
+ // 用户层/设置文档变更 → watch 重建通道;命名空间随注入 scope 自动卸载。
84
+ ctx.inject(['settings'], (settingsCtx) => {
85
+ const settings = registerMessagerSettings(settingsCtx, config);
86
+ const effective = settings.get();
87
+ dispatcher.reconfigure({ config: effective, channels: buildChannels(effective) });
88
+ settingsCtx.effect(() => settings.watch((next) => {
89
+ dispatcher.reconfigure({ config: next, channels: buildChannels(next) });
90
+ }), 'dsh-messager: settings watch');
91
+ });
92
+ // 配置读写路由(webServer 通道):浏览器端经此读写 messager 命名空间,
93
+ // 不受 Web 设置白名单门控(dsh-market 同款「正门」)。仅 Web 环境挂载;
94
+ // headless profile 无 webServer 服务时本 inject 不执行,不影响通知功能。
95
+ ctx.inject(['webServer'], (webCtx) => {
96
+ webCtx.inject(['settings'], (fullCtx) => {
97
+ const settingsService = fullCtx.get('settings');
98
+ if (settingsService === undefined)
99
+ return;
100
+ const disposeRoutes = mountConfigRoutes(fullCtx.webServer, settingsService);
101
+ fullCtx.effect(() => disposeRoutes, 'dsh-messager: config routes');
102
+ });
103
+ });
104
+ // 会话事件:会话标题、交互信号(审批/提问)、turn/end 结束原因。
105
+ ctx.on('session/event', (session, event) => {
106
+ if (event.type === 'session/title') {
107
+ noTitleSessions.delete(session.id); // 新标题到达:解除无标题负缓存
108
+ dispatcher.noteSessionTitle(session.id, event.data.title);
109
+ return;
110
+ }
111
+ const interaction = interactionSignalOf(session.id, event);
112
+ if (interaction !== undefined) {
113
+ ensureSessionTitle(dispatcher, session);
114
+ dispatcher.onSignal(interaction);
115
+ return;
116
+ }
117
+ const turnEnd = turnEndReasonOf(event);
118
+ if (turnEnd !== undefined)
119
+ dispatcher.noteTurnEnd(session.id, turnEnd.reason);
120
+ });
121
+ // 运行状态:running → idle 边界触发“任务完成”;仅根会话(排除子代理噪音)。
122
+ // 注意:子代理判别必须用 origin === 'subagent',不能用 parentSession ——
123
+ // 分叉会话(sessions.fork)的 header 也会携带 parentSession(指向源会话),
124
+ // 但 origin 为空,仍是顶层会话,任务完成后应正常通知。
125
+ const running = new Map();
126
+ ctx.on('agent/status', ({ agent, status }) => {
127
+ const isRunning = status === 'running';
128
+ const previous = running.get(agent.id);
129
+ running.set(agent.id, isRunning);
130
+ if (previous !== true || isRunning)
131
+ return;
132
+ if (agent.session.header.origin === 'subagent')
133
+ return;
134
+ ensureSessionTitle(dispatcher, agent.session);
135
+ dispatcher.onSignal({ kind: 'completed', sessionId: agent.id, seq: Date.now() });
136
+ });
137
+ // 错误:步骤/回合失败。
138
+ ctx.on('agent/error', ({ agent, turn, step, error }) => {
139
+ ensureSessionTitle(dispatcher, agent.session);
140
+ dispatcher.onSignal({
141
+ kind: 'error',
142
+ sessionId: agent.id,
143
+ message: errorMessageOf(error),
144
+ turn,
145
+ step,
146
+ seq: Date.now(),
147
+ });
148
+ });
149
+ ctx.effect(() => () => dispatcher.dispose(), 'dsh-messager: dispatcher timers');
150
+ ctx.logger.info('[dsh-messager] plugin loaded');
151
+ }
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 规则/调度层:过滤(triggers)、冷却、完成防抖、通道限流,
3
+ * 并把 Signal 渲染后分发给启用的通道。通道失败只记日志,绝不抛出。
4
+ */
5
+ import type { Config } from './config.ts';
6
+ import { type NotificationPayload } from './templates.js';
7
+ import type { Signal, TurnEndReason } from './signals.js';
8
+ /** 通知载荷由模板层产出,通道层从此处取类型。 */
9
+ export type { NotificationPayload } from './templates.js';
10
+ /** 通知通道接口:系统(node-notifier)/ 飞书(webhook)/ 浏览器(client 端)皆实现此接口。 */
11
+ export interface NotifyChannel {
12
+ readonly id: string;
13
+ send(payload: NotificationPayload): Promise<void>;
14
+ }
15
+ /** 调度器外部钩子(日志)。 */
16
+ export interface DispatcherHooks {
17
+ logWarn(message: string): void;
18
+ logDebug?(message: string): void;
19
+ }
20
+ /** 通道配置更新(settings watch 回调时整体替换)。 */
21
+ export interface DispatcherConfig {
22
+ config: Config;
23
+ channels: NotifyChannel[];
24
+ }
25
+ /**
26
+ * 通知调度器:
27
+ * - `onSignal`:interaction/error 即时投递;completed 进入防抖窗口
28
+ * (等待 turn/end 合并结束原因,并折叠窗口内的重复边界);
29
+ * - 冷却:同一会话同一触发在 `interactionCooldownMs` 内只投递一次;
30
+ * - 限流:每通道每分钟不超过 `perChannelPerMinute` 条。
31
+ */
32
+ export declare class NotificationDispatcher {
33
+ private config;
34
+ private readonly channels;
35
+ private readonly hooks;
36
+ private readonly now;
37
+ /** sessionId → 会话标题(来自 session/title 事件)。 */
38
+ private readonly titles;
39
+ /** sessionId → 最近一次 turn/end 原因。 */
40
+ private readonly lastTurnEnd;
41
+ /** `${sessionId}:${kind}` → 上次投递时间戳。 */
42
+ private readonly cooldowns;
43
+ /** sessionId → 防抖中的完成信号与定时器。 */
44
+ private readonly pendingCompleted;
45
+ /** channelId → 最近一分钟内的投递时间戳。 */
46
+ private readonly channelWindows;
47
+ constructor(options: DispatcherConfig & {
48
+ hooks: DispatcherHooks;
49
+ now?: () => number;
50
+ });
51
+ /** 配置热更新(settings watch):替换配置与通道,保留标题/冷却等状态。 */
52
+ reconfigure(next: DispatcherConfig): void;
53
+ /** 记录会话标题(session/title 事件)。 */
54
+ noteSessionTitle(sessionId: string, title: string): void;
55
+ /** 是否已记录该会话的标题(缺失时插件可从会话事件日志兜底补取)。 */
56
+ hasTitle(sessionId: string): boolean;
57
+ /** 记录 turn/end 结束原因,并合并到正在防抖的完成信号上。 */
58
+ noteTurnEnd(sessionId: string, reason: TurnEndReason): void;
59
+ /** 信号入口:interaction/error 立即处理;completed 进入防抖。 */
60
+ onSignal(signal: Signal): void;
61
+ private triggerEnabled;
62
+ /** 完成信号防抖:窗口内只保留最新信号,窗口结束才投递。 */
63
+ private debounceCompleted;
64
+ /** 防抖窗口结束:投递(若仍满足冷却)。 */
65
+ flushCompleted(sessionId: string): void;
66
+ /** 冷却检查 + 逐通道渲染 + 分发。 */
67
+ private dispatchIfAllowed;
68
+ /** 每通道每分钟限流。 */
69
+ private allowChannel;
70
+ /** 通道 verbosity:system/feishu 各自配置;未知通道取 normal。 */
71
+ private verbosityFor;
72
+ /** 清空防抖定时器(插件卸载时调用;订阅与通道本身由 effect 清理)。 */
73
+ dispose(): void;
74
+ }
75
+ //# sourceMappingURL=notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,aAAa,CAAA;AACpD,OAAO,EAAiB,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACxE,OAAO,KAAK,EAAE,MAAM,EAAe,aAAa,EAAE,MAAM,cAAc,CAAA;AAEtE,4BAA4B;AAC5B,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEzD,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClD;AAED,mBAAmB;AACnB,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,aAAa,EAAE,CAAA;CAC1B;AAOD;;;;;;GAMG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAElC,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,oCAAoC;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAC/D,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,gCAAgC;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;IACvE,gCAAgC;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;gBAEjD,OAAO,EAAE,gBAAgB,GAAG;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;IAOtF,gDAAgD;IAChD,WAAW,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAKzC,gCAAgC;IAChC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAKxD,sCAAsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIpC,uCAAuC;IACvC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI;IAQ3D,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAY9B,OAAO,CAAC,cAAc;IAMtB,iCAAiC;IACjC,OAAO,CAAC,iBAAiB;IAiBzB,yBAAyB;IACzB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAOvC,yBAAyB;IACzB,OAAO,CAAC,iBAAiB;IA4BzB,gBAAgB;IAChB,OAAO,CAAC,YAAY;IAYpB,oDAAoD;IACpD,OAAO,CAAC,YAAY;IAQpB,2CAA2C;IAC3C,OAAO,IAAI,IAAI;CAIhB"}
package/lib/notify.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 规则/调度层:过滤(triggers)、冷却、完成防抖、通道限流,
3
+ * 并把 Signal 渲染后分发给启用的通道。通道失败只记日志,绝不抛出。
4
+ */
5
+ import { renderPayload } from './templates.js';
6
+ /**
7
+ * 通知调度器:
8
+ * - `onSignal`:interaction/error 即时投递;completed 进入防抖窗口
9
+ * (等待 turn/end 合并结束原因,并折叠窗口内的重复边界);
10
+ * - 冷却:同一会话同一触发在 `interactionCooldownMs` 内只投递一次;
11
+ * - 限流:每通道每分钟不超过 `perChannelPerMinute` 条。
12
+ */
13
+ export class NotificationDispatcher {
14
+ config;
15
+ channels;
16
+ hooks;
17
+ now;
18
+ /** sessionId → 会话标题(来自 session/title 事件)。 */
19
+ titles = new Map();
20
+ /** sessionId → 最近一次 turn/end 原因。 */
21
+ lastTurnEnd = new Map();
22
+ /** `${sessionId}:${kind}` → 上次投递时间戳。 */
23
+ cooldowns = new Map();
24
+ /** sessionId → 防抖中的完成信号与定时器。 */
25
+ pendingCompleted = new Map();
26
+ /** channelId → 最近一分钟内的投递时间戳。 */
27
+ channelWindows = new Map();
28
+ constructor(options) {
29
+ this.config = options.config;
30
+ this.channels = options.channels;
31
+ this.hooks = options.hooks;
32
+ this.now = options.now ?? Date.now;
33
+ }
34
+ /** 配置热更新(settings watch):替换配置与通道,保留标题/冷却等状态。 */
35
+ reconfigure(next) {
36
+ this.config = next.config;
37
+ this.channels.splice(0, this.channels.length, ...next.channels);
38
+ }
39
+ /** 记录会话标题(session/title 事件)。 */
40
+ noteSessionTitle(sessionId, title) {
41
+ if (title === '')
42
+ return;
43
+ this.titles.set(sessionId, title);
44
+ }
45
+ /** 是否已记录该会话的标题(缺失时插件可从会话事件日志兜底补取)。 */
46
+ hasTitle(sessionId) {
47
+ return this.titles.has(sessionId);
48
+ }
49
+ /** 记录 turn/end 结束原因,并合并到正在防抖的完成信号上。 */
50
+ noteTurnEnd(sessionId, reason) {
51
+ this.lastTurnEnd.set(sessionId, reason);
52
+ const pending = this.pendingCompleted.get(sessionId);
53
+ if (pending !== undefined && pending.signal.reason === undefined) {
54
+ pending.signal = { ...pending.signal, reason };
55
+ }
56
+ }
57
+ /** 信号入口:interaction/error 立即处理;completed 进入防抖。 */
58
+ onSignal(signal) {
59
+ if (!this.triggerEnabled(signal.kind)) {
60
+ this.hooks.logDebug?.(`[dispatcher] trigger disabled: ${signal.kind} ${signal.sessionId}`);
61
+ return;
62
+ }
63
+ if (signal.kind === 'completed') {
64
+ this.debounceCompleted(signal);
65
+ return;
66
+ }
67
+ this.dispatchIfAllowed(signal);
68
+ }
69
+ triggerEnabled(kind) {
70
+ if (kind === 'interaction')
71
+ return this.config.triggers.interaction;
72
+ if (kind === 'completed')
73
+ return this.config.triggers.completed;
74
+ return this.config.triggers.error;
75
+ }
76
+ /** 完成信号防抖:窗口内只保留最新信号,窗口结束才投递。 */
77
+ debounceCompleted(signal) {
78
+ const sessionId = signal.sessionId;
79
+ const merged = {
80
+ ...signal,
81
+ ...(this.lastTurnEnd.get(sessionId) === undefined ? {} : { reason: this.lastTurnEnd.get(sessionId) }),
82
+ };
83
+ const existing = this.pendingCompleted.get(sessionId);
84
+ if (existing !== undefined) {
85
+ existing.signal = merged;
86
+ clearTimeout(existing.timer);
87
+ existing.timer = setTimeout(() => this.flushCompleted(sessionId), this.config.dedup.completedDebounceMs);
88
+ return;
89
+ }
90
+ const timer = setTimeout(() => this.flushCompleted(sessionId), this.config.dedup.completedDebounceMs);
91
+ this.pendingCompleted.set(sessionId, { signal: merged, timer });
92
+ }
93
+ /** 防抖窗口结束:投递(若仍满足冷却)。 */
94
+ flushCompleted(sessionId) {
95
+ const pending = this.pendingCompleted.get(sessionId);
96
+ if (pending === undefined)
97
+ return;
98
+ this.pendingCompleted.delete(sessionId);
99
+ this.dispatchIfAllowed(pending.signal);
100
+ }
101
+ /** 冷却检查 + 逐通道渲染 + 分发。 */
102
+ dispatchIfAllowed(signal) {
103
+ const now = this.now();
104
+ const key = `${signal.sessionId}:${signal.kind}`;
105
+ const last = this.cooldowns.get(key);
106
+ if (last !== undefined && now - last < this.config.dedup.interactionCooldownMs) {
107
+ this.hooks.logDebug?.(`[dispatcher] cooldown: skip ${key}`);
108
+ return;
109
+ }
110
+ let dispatched = false;
111
+ for (const channel of this.channels) {
112
+ if (!this.allowChannel(channel.id, now)) {
113
+ this.hooks.logDebug?.(`[dispatcher] rate limited: ${channel.id}`);
114
+ continue;
115
+ }
116
+ dispatched = true;
117
+ const payload = renderPayload({
118
+ signal,
119
+ config: this.config,
120
+ sessionTitle: this.titles.get(signal.sessionId),
121
+ verbosity: this.verbosityFor(channel.id),
122
+ });
123
+ void channel.send(payload).catch((error) => {
124
+ this.hooks.logWarn(`channel "${channel.id}" failed: ${error instanceof Error ? error.message : String(error)}`);
125
+ });
126
+ }
127
+ if (dispatched)
128
+ this.cooldowns.set(key, now);
129
+ }
130
+ /** 每通道每分钟限流。 */
131
+ allowChannel(channelId, now) {
132
+ const windowStart = now - 60_000;
133
+ const window = (this.channelWindows.get(channelId) ?? []).filter(timestamp => timestamp >= windowStart);
134
+ if (window.length >= this.config.dedup.perChannelPerMinute) {
135
+ this.channelWindows.set(channelId, window);
136
+ return false;
137
+ }
138
+ window.push(now);
139
+ this.channelWindows.set(channelId, window);
140
+ return true;
141
+ }
142
+ /** 通道 verbosity:system/feishu 各自配置;未知通道取 normal。 */
143
+ verbosityFor(channelId) {
144
+ switch (channelId) {
145
+ case 'system': return this.config.system.verbosity;
146
+ case 'feishu': return this.config.feishu.verbosity;
147
+ default: return 'normal';
148
+ }
149
+ }
150
+ /** 清空防抖定时器(插件卸载时调用;订阅与通道本身由 effect 清理)。 */
151
+ dispose() {
152
+ for (const pending of this.pendingCompleted.values())
153
+ clearTimeout(pending.timer);
154
+ this.pendingCompleted.clear();
155
+ }
156
+ }
157
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 配置层:把 Loader config(cordis.yml)注册为 settings 命名空间 `messager`
3
+ * 的 base 层,设置页可编辑用户层覆盖。
4
+ *
5
+ * 注意:settings 服务可能晚于本插件加载,必须在 `ctx.inject(['settings'], …)`
6
+ * 回调内调用本模块(见 src/index.ts),不能直接 `ctx.get('settings')`。
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ import { type Config as ConfigShape } from './config.js';
10
+ export interface MessagerSettings {
11
+ /** 当前有效配置(默认值 → base → 用户层)。 */
12
+ get(): ConfigShape;
13
+ /** 监听配置变更(提交后按序调用,返回注销函数)。 */
14
+ watch(callback: (next: ConfigShape, prev: ConfigShape) => void): () => void;
15
+ }
16
+ /**
17
+ * 在 settings 服务就绪的上下文中注册命名空间。
18
+ * @param ctx - 已注入 settings 的上下文(inject 回调的 scope)。
19
+ * @param base - Loader config(cordis.yml 的 config 段)。
20
+ * @returns 设置句柄(注册本身是 effect,随注入 scope 卸载)。
21
+ */
22
+ export declare function registerMessagerSettings(ctx: Context, base: ConfigShape): MessagerSettings;
23
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,EAAU,KAAK,MAAM,IAAI,WAAW,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,GAAG,IAAI,WAAW,CAAA;IAClB,8BAA8B;IAC9B,KAAK,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAM1F"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 配置层:把 Loader config(cordis.yml)注册为 settings 命名空间 `messager`
3
+ * 的 base 层,设置页可编辑用户层覆盖。
4
+ *
5
+ * 注意:settings 服务可能晚于本插件加载,必须在 `ctx.inject(['settings'], …)`
6
+ * 回调内调用本模块(见 src/index.ts),不能直接 `ctx.get('settings')`。
7
+ */
8
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
9
+ import { Config } from './config.js';
10
+ /**
11
+ * 在 settings 服务就绪的上下文中注册命名空间。
12
+ * @param ctx - 已注入 settings 的上下文(inject 回调的 scope)。
13
+ * @param base - Loader config(cordis.yml 的 config 段)。
14
+ * @returns 设置句柄(注册本身是 effect,随注入 scope 卸载)。
15
+ */
16
+ export function registerMessagerSettings(ctx, base) {
17
+ const scope = ctx.settings.register(settingsNamespace('messager'), Config, { base, applies: 'live' });
18
+ return {
19
+ get: () => scope.get(),
20
+ watch: (callback) => scope.watch(callback),
21
+ };
22
+ }
23
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,OAAO,EAAE,MAAM,EAA8B,MAAM,aAAa,CAAA;AAShE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAY,EAAE,IAAiB;IACtE,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;IACrG,OAAO;QACL,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;QACtB,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC;KAC3C,CAAA;AACH,CAAC"}