@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044228975

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 (150) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE.md +18 -0
  3. package/README.md +136 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/bot.cjs +8314 -0
  6. package/lib/bot.cjs.map +7 -0
  7. package/lib/build.json +1 -0
  8. package/lib/client.js +46 -0
  9. package/lib/host/agent.d.ts +21 -0
  10. package/lib/host/agent.js +151 -0
  11. package/lib/host/agent.js.map +1 -0
  12. package/lib/host/bot.d.ts +17 -0
  13. package/lib/host/bot.js +158 -0
  14. package/lib/host/bot.js.map +1 -0
  15. package/lib/host/client.d.ts +2 -0
  16. package/lib/host/client.js +19 -0
  17. package/lib/host/client.js.map +1 -0
  18. package/lib/host/config.d.ts +24 -0
  19. package/lib/host/config.js +192 -0
  20. package/lib/host/config.js.map +1 -0
  21. package/lib/host/files.d.ts +29 -0
  22. package/lib/host/files.js +188 -0
  23. package/lib/host/files.js.map +1 -0
  24. package/lib/host/gateway.d.ts +33 -0
  25. package/lib/host/gateway.js +237 -0
  26. package/lib/host/gateway.js.map +1 -0
  27. package/lib/host/github.d.ts +47 -0
  28. package/lib/host/github.js +219 -0
  29. package/lib/host/github.js.map +1 -0
  30. package/lib/host/http.d.ts +4 -0
  31. package/lib/host/http.js +41 -0
  32. package/lib/host/http.js.map +1 -0
  33. package/lib/host/index.d.ts +32 -0
  34. package/lib/host/index.js +192 -0
  35. package/lib/host/index.js.map +1 -0
  36. package/lib/host/inference-worker.d.ts +1 -0
  37. package/lib/host/inference-worker.js +72 -0
  38. package/lib/host/inference-worker.js.map +1 -0
  39. package/lib/host/mcp.d.ts +138 -0
  40. package/lib/host/mcp.js +104 -0
  41. package/lib/host/mcp.js.map +1 -0
  42. package/lib/host/memory.d.ts +26 -0
  43. package/lib/host/memory.js +155 -0
  44. package/lib/host/memory.js.map +1 -0
  45. package/lib/host/panel.d.ts +1 -0
  46. package/lib/host/panel.html +174 -0
  47. package/lib/host/panel.js +388 -0
  48. package/lib/host/panel.js.map +1 -0
  49. package/lib/host/runtime.d.ts +21 -0
  50. package/lib/host/runtime.js +145 -0
  51. package/lib/host/runtime.js.map +1 -0
  52. package/lib/host/types.d.ts +112 -0
  53. package/lib/host/types.js +2 -0
  54. package/lib/host/types.js.map +1 -0
  55. package/package.json +94 -4
  56. package/scripts/build.mjs +90 -0
  57. package/scripts/check.mjs +13 -0
  58. package/scripts/dump-npm-latest.mjs +30 -0
  59. package/scripts/model-smoke.mjs +25 -0
  60. package/src/agent.ts +203 -0
  61. package/src/bot.ts +173 -0
  62. package/src/client.ts +20 -0
  63. package/src/config.ts +217 -0
  64. package/src/files.ts +206 -0
  65. package/src/gateway.ts +276 -0
  66. package/src/github.ts +263 -0
  67. package/src/http.ts +41 -0
  68. package/src/index.ts +227 -0
  69. package/src/inference-worker.ts +89 -0
  70. package/src/mcp.ts +112 -0
  71. package/src/memory.ts +178 -0
  72. package/src/panel.html +174 -0
  73. package/src/panel.ts +440 -0
  74. package/src/runtime.ts +164 -0
  75. package/src/types.ts +106 -0
  76. package/test/core.test.mjs +279 -0
  77. package/test/fixtures/mcp-server.mjs +14 -0
  78. package/test/integration.test.mjs +253 -0
  79. package/test/panel.test.mjs +73 -0
  80. package/tsconfig.json +18 -0
  81. package/vendor/gugle-chat-bot/LICENSE +661 -0
  82. package/vendor/gugle-chat-bot/package.json +57 -0
  83. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
  84. package/vendor/gugle-chat-bot/src/api.json +4285 -0
  85. package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
  86. package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
  87. package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
  88. package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
  89. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
  90. package/vendor/gugle-chat-bot/src/custom.ts +548 -0
  91. package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
  92. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
  93. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
  94. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
  95. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
  96. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
  97. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
  98. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
  99. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
  100. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
  101. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
  102. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
  103. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
  104. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
  105. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
  106. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
  107. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
  108. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
  109. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
  110. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
  111. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
  112. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
  113. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
  114. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
  115. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
  116. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
  117. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
  118. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
  119. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
  120. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
  121. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
  122. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
  123. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
  124. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
  125. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
  126. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
  127. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
  128. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
  129. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
  130. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
  131. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
  132. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
  133. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
  134. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
  135. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
  136. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
  137. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
  138. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
  139. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
  140. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
  141. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
  142. package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
  143. package/vendor/gugle-chat-bot/src/index.ts +695 -0
  144. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
  145. package/vendor/gugle-chat-bot/src/result.json +359 -0
  146. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
  147. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
  148. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
  149. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
  150. package/vendor/gugle-chat-bot/tsconfig.json +46 -0
@@ -0,0 +1,210 @@
1
+ import { ConfigStore } from '@/config/manager';
2
+ import { isOperator } from '@/config/features';
3
+
4
+ /**
5
+ * Hermes Agent 桥接配置(移植自 qq-hermes-bridge 的 .env 配置,MIT 协议,原作者 Amorter)。
6
+ *
7
+ * 配置文件:configs/features/hermes.json(v1,ConfigStore 热重载):
8
+ * ```json
9
+ * {
10
+ * "version": 1,
11
+ * "apiUrl": "http://127.0.0.1:8642",
12
+ * "apiKey": "",
13
+ * "botName": "小喵",
14
+ * "groups": [659356928],
15
+ * "admins": [],
16
+ * "requireMention": true,
17
+ * "keywordTriggers": ["小喵"],
18
+ * "replyPerSentence": true,
19
+ * "compactMaxChars": 300,
20
+ * "compactMaxLines": 5,
21
+ * "privateReactionFallbackText": true,
22
+ * "privateReceiptText": "🤔 收到,正在思考…"
23
+ * }
24
+ * ```
25
+ * - `groups`:启用 AI 对话的 QQ 群(空=群聊不启用);互通的 Discord 频道能否触发 AI
26
+ * 也由其桥接的 QQ 群是否在此列表决定;私聊始终启用(受用户黑白名单约束);
27
+ * - `replyPerSentence`:QQ 群聊 AI 回复逐句发送;单条超过 compactMaxChars 字或
28
+ * compactMaxLines 行时改用聊天记录(合并转发)发送,任一项填 0 关闭该维度;
29
+ * - `privateReactionFallbackText`:私聊贴表情失败时改发文字回执(NapCat 对私聊
30
+ * 消息通常不支持 set_msg_emoji_like),AI 首条回复发出后自动撤回;
31
+ * - Bot QQ 号无需配置,取 bot.getLoginInfoSync().user_id(@提及判定);
32
+ * - 系统提示词优先级:configs/SOUL.md > systemPrompt > 空。
33
+ */
34
+
35
+ export interface HermesBridgeConfig {
36
+ version: number;
37
+ /** Hermes API Server 地址 */
38
+ apiUrl: string;
39
+ /** API Key(Hermes 配置了认证时填) */
40
+ apiKey: string;
41
+ /** Bot 名称(卡片落款、合并转发署名等展示用) */
42
+ botName: string;
43
+ /** 启用 AI 对话的 QQ 群(空数组=群聊不启用) */
44
+ groups: number[];
45
+ /** 管理员 QQ 号(与 management operators 合并生效) */
46
+ admins: number[];
47
+ /** 允许私聊/对话的用户(空=全部允许) */
48
+ allowedUsers: number[];
49
+ /** 屏蔽的用户 */
50
+ blockedUsers: number[];
51
+ /** 群聊中是否需要 @bot 才触发 */
52
+ requireMention: boolean;
53
+ /** 关键词触发列表(小写匹配) */
54
+ keywordTriggers: string[];
55
+ /** 进度卡片发送最小间隔(秒) */
56
+ progressRateLimitSec: number;
57
+ /** 是否以图片形式发送进度 */
58
+ progressAsImage: boolean;
59
+ /** 进度卡片最多显示的工具数量 */
60
+ progressMaxTools: number;
61
+ /** 单条消息最大长度(超出切分发送) */
62
+ maxMessageLength: number;
63
+ /** 是否将消息中的图片原图转发给 Hermes(多模态识别) */
64
+ forwardImages: boolean;
65
+ /** 是否按句拆分 AI 回复逐条发送(关闭则整段一次发出) */
66
+ replyPerSentence: boolean;
67
+ /** 单条回复超过该字数改用聊天记录(合并转发)发送;0=关闭该维度 */
68
+ compactMaxChars: number;
69
+ /** 单条回复超过该行数改用聊天记录(合并转发)发送;0=关闭该维度 */
70
+ compactMaxLines: number;
71
+ /** 系统提示词(被 configs/SOUL.md 覆盖) */
72
+ systemPrompt: string;
73
+ /** 保留的历史消息轮数 */
74
+ localHistoryMaxMessages: number;
75
+ /** 是否启用对话历史持久化存储 */
76
+ persistHistoryEnabled: boolean;
77
+ /** 持久化保留的最大消息数 */
78
+ persistHistoryMax: number;
79
+ /** 是否启用命令审批 */
80
+ approvalEnabled: boolean;
81
+ /** 审批超时自动拒绝(秒),0=不超时 */
82
+ approvalTimeoutSec: number;
83
+ /** 是否启用「AI 处理中」表情回应(QQ 贴表情 / Discord reaction,完成后摘除) */
84
+ reactionEnabled: boolean;
85
+ /** QQ 表情回应的 emoji_id(NapCat set_msg_emoji_like;76=👍 强) */
86
+ reactionEmojiQq: string;
87
+ /** Discord 表情回应(unicode emoji) */
88
+ reactionEmojiDiscord: string;
89
+ /**
90
+ * 私聊贴表情失败时改用文字回执(默认 true)。
91
+ * NapCat 对私聊消息通常不支持 set_msg_emoji_like,贴表情会失败;
92
+ * 开启后改发一条简短提示,让用户知道 AI 已收到(群聊不受影响)。
93
+ */
94
+ privateReactionFallbackText: boolean;
95
+ /** 私聊文字回执内容(privateReactionFallbackText 开启且贴表情失败时发送) */
96
+ privateReceiptText: string;
97
+ }
98
+
99
+ export function hermesConfigFactory(): HermesBridgeConfig {
100
+ return {
101
+ version: 1,
102
+ apiUrl: 'http://127.0.0.1:8642',
103
+ apiKey: '',
104
+ botName: '小喵',
105
+ groups: [],
106
+ admins: [],
107
+ allowedUsers: [],
108
+ blockedUsers: [],
109
+ requireMention: true,
110
+ keywordTriggers: [],
111
+ progressRateLimitSec: 15,
112
+ progressAsImage: true,
113
+ progressMaxTools: 12,
114
+ maxMessageLength: 1200,
115
+ forwardImages: true,
116
+ replyPerSentence: true,
117
+ compactMaxChars: 300,
118
+ compactMaxLines: 5,
119
+ systemPrompt: '',
120
+ localHistoryMaxMessages: 24,
121
+ persistHistoryEnabled: true,
122
+ persistHistoryMax: 100,
123
+ approvalEnabled: true,
124
+ approvalTimeoutSec: 300,
125
+ reactionEnabled: true,
126
+ reactionEmojiQq: '76',
127
+ reactionEmojiDiscord: '👀',
128
+ privateReactionFallbackText: true,
129
+ privateReceiptText: '🤔 收到,正在思考…'
130
+ };
131
+ }
132
+
133
+ function asString(raw: unknown, fallback: string): string {
134
+ return typeof raw === 'string' ? raw : fallback;
135
+ }
136
+
137
+ function asBool(raw: unknown, fallback: boolean): boolean {
138
+ return typeof raw === 'boolean' ? raw : fallback;
139
+ }
140
+
141
+ function asPositiveNumber(raw: unknown, fallback: number): number {
142
+ return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : fallback;
143
+ }
144
+
145
+ function asQqList(raw: unknown): number[] {
146
+ if (!Array.isArray(raw)) return [];
147
+ return raw.filter((item): item is number => typeof item === 'number' && Number.isSafeInteger(item) && item > 0);
148
+ }
149
+
150
+ function asStringList(raw: unknown): string[] {
151
+ if (!Array.isArray(raw)) return [];
152
+ return raw.filter((item): item is string => typeof item === 'string' && item.length > 0);
153
+ }
154
+
155
+ export function normalizeHermesConfig(raw: unknown): HermesBridgeConfig | null {
156
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
157
+ const record = raw as Record<string, unknown>;
158
+ const fallback = hermesConfigFactory();
159
+ return {
160
+ version: 1,
161
+ apiUrl: asString(record.apiUrl, fallback.apiUrl),
162
+ apiKey: asString(record.apiKey, fallback.apiKey),
163
+ botName: asString(record.botName, fallback.botName),
164
+ groups: asQqList(record.groups),
165
+ admins: asQqList(record.admins),
166
+ allowedUsers: asQqList(record.allowedUsers),
167
+ blockedUsers: asQqList(record.blockedUsers),
168
+ requireMention: asBool(record.requireMention, fallback.requireMention),
169
+ keywordTriggers: asStringList(record.keywordTriggers).map(keyword => keyword.toLowerCase()),
170
+ progressRateLimitSec: asPositiveNumber(record.progressRateLimitSec, fallback.progressRateLimitSec),
171
+ progressAsImage: asBool(record.progressAsImage, fallback.progressAsImage),
172
+ progressMaxTools: asPositiveNumber(record.progressMaxTools, fallback.progressMaxTools),
173
+ maxMessageLength: asPositiveNumber(record.maxMessageLength, fallback.maxMessageLength),
174
+ forwardImages: asBool(record.forwardImages, fallback.forwardImages),
175
+ replyPerSentence: asBool(record.replyPerSentence, fallback.replyPerSentence),
176
+ compactMaxChars: asPositiveNumber(record.compactMaxChars, fallback.compactMaxChars),
177
+ compactMaxLines: asPositiveNumber(record.compactMaxLines, fallback.compactMaxLines),
178
+ systemPrompt: asString(record.systemPrompt, fallback.systemPrompt),
179
+ localHistoryMaxMessages: asPositiveNumber(record.localHistoryMaxMessages, fallback.localHistoryMaxMessages),
180
+ persistHistoryEnabled: asBool(record.persistHistoryEnabled, fallback.persistHistoryEnabled),
181
+ persistHistoryMax: asPositiveNumber(record.persistHistoryMax, fallback.persistHistoryMax),
182
+ approvalEnabled: asBool(record.approvalEnabled, fallback.approvalEnabled),
183
+ approvalTimeoutSec: asPositiveNumber(record.approvalTimeoutSec, fallback.approvalTimeoutSec),
184
+ reactionEnabled: asBool(record.reactionEnabled, fallback.reactionEnabled),
185
+ reactionEmojiQq: asString(record.reactionEmojiQq, fallback.reactionEmojiQq),
186
+ reactionEmojiDiscord: asString(record.reactionEmojiDiscord, fallback.reactionEmojiDiscord),
187
+ privateReactionFallbackText: asBool(record.privateReactionFallbackText, fallback.privateReactionFallbackText),
188
+ privateReceiptText: asString(record.privateReceiptText, fallback.privateReceiptText)
189
+ };
190
+ }
191
+
192
+ const hermesStore = new ConfigStore<HermesBridgeConfig>({
193
+ path: 'configs/features/hermes.json',
194
+ version: 1,
195
+ factory: hermesConfigFactory,
196
+ normalize: normalizeHermesConfig
197
+ });
198
+
199
+ /** 读取 Hermes 桥接生效配置(热重载) */
200
+ export function getHermesConfig(): HermesBridgeConfig {
201
+ return hermesStore.get();
202
+ }
203
+
204
+ /**
205
+ * 有效管理员判定:hermes 自有 admins ∪ management operators。
206
+ * 与 /pardon、/github allow 等管理命令保持「谁是管理员」口径一致。
207
+ */
208
+ export function isHermesAdmin(qqUserId: number): boolean {
209
+ return getHermesConfig().admins.includes(qqUserId) || isOperator(qqUserId);
210
+ }
@@ -0,0 +1,67 @@
1
+ import { HistoryEntry, RouteInfo } from '@/features/hermes/types';
2
+
3
+ /**
4
+ * 会话上下文组装(纯函数)。
5
+ *
6
+ * 群聊/互通频道的会话历史是多人共享的,若原样平铺提交,模型分不清「谁在跟我
7
+ * 说话、哪些是背景闲聊、我之前回复的是谁」,容易串台认错人。这里在提交时按
8
+ * 当前对话者动态标注归属(存储层保持原样):
9
+ * - 当前对话者的消息:保持 `发送者: 内容` 原样(对话主线);
10
+ * - 其他成员的消息:加 [群聊背景] 前缀(旁听内容,不是对 AI 说的话);
11
+ * - AI 回复过其他成员的话:加 [你回复 xxx 的话] 前缀(避免误认为是对当前
12
+ * 对话者说的)。
13
+ */
14
+
15
+ /** 背景消息前缀:其他成员的闲聊 */
16
+ export const BG_PREFIX = '[群聊背景]';
17
+
18
+ /** 组装提交给 Hermes 的 conversation_history(按当前对话者标注归属) */
19
+ export function buildConversationHistory(
20
+ history: HistoryEntry[],
21
+ currentUserId: string
22
+ ): Array<{ role: string; content: string }> {
23
+ return history.map(entry => {
24
+ if (entry.role === 'user') {
25
+ if (entry.userId !== undefined && entry.userId !== currentUserId) {
26
+ return { role: 'user', content: `${BG_PREFIX} ${entry.content}` };
27
+ }
28
+ return { role: 'user', content: entry.content };
29
+ }
30
+ if (entry.role === 'assistant') {
31
+ if (entry.userId !== undefined && entry.userId !== currentUserId) {
32
+ return { role: 'assistant', content: `[你回复 ${entry.label ?? entry.userId} 的话] ${entry.content}` };
33
+ }
34
+ return { role: 'assistant', content: entry.content };
35
+ }
36
+ return { role: entry.role, content: entry.content };
37
+ });
38
+ }
39
+
40
+ /**
41
+ * 群聊/互通频道的上下文系统提示(QQ 群与 Discord 路由统一口径)。
42
+ * 显式声明当前对话者与历史标注约定,配合 buildConversationHistory 使用。
43
+ */
44
+ export function buildGroupContext(route: RouteInfo, senderLabel: string): string {
45
+ const where =
46
+ route.type === 'group'
47
+ ? `你正在 QQ 群 ${route.groupId} 中(该群与 Discord 频道互通,消息也可能来自 Discord 用户)。`
48
+ : `你正在 QQ 群 ${route.groupId} 互通的 Discord 频道 #${route.discord?.channelName ?? ''} 中(频道与 QQ 群消息互通)。`;
49
+ return [
50
+ where,
51
+ '群聊有多个成员,发送者标识格式为「昵称 (ID)」:纯数字 ID 是 QQ 号,dc: 前缀的是 Discord 用户,请据此区分不同的人。',
52
+ `历史消息中带 ${BG_PREFIX} 前缀的是其他成员的闲聊,不是对你说的话,仅供你了解上下文,不要回应、也不要当成你自己的记忆;带 [你回复 xxx 的话] 前缀的是你之前回复其他成员的内容。`,
53
+ `当前与你对话的是 ${senderLabel},只需回应 TA 的消息。回复简短口语化,符合聊天风格。`
54
+ ].join('\n');
55
+ }
56
+
57
+ /**
58
+ * 消息中的 Issue/PR 编号会被消息管道自动渲染卡片时,给 AI 输入追加注记,
59
+ * 避免 AI 再调用查Issue技能重复发同一张卡片(autoCardCount=0 时原样返回)。
60
+ */
61
+ export function appendAutoCardNote(text: string, autoCardCount: number): string {
62
+ if (autoCardCount <= 0) return text;
63
+ return (
64
+ `${text}\n(系统提示:本条消息中的 ${autoCardCount} 个 Issue/PR 编号已由系统自动生成信息卡片并发出,` +
65
+ `请直接据此回答,不要再调用查Issue技能重复发送卡片)`
66
+ );
67
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { HistoryEntry } from '@/features/hermes/types';
4
+
5
+ /**
6
+ * 聊天记录持久化存储(移植自 qq-hermes-bridge src/history.ts,MIT 协议,原作者 Amorter)。
7
+ * 每个会话以 JSON 文件形式存储在 data/hermes-history/,最多保留 N 条消息,
8
+ * 进程重启后自动恢复。
9
+ */
10
+ export class ChatHistoryStore {
11
+ private readonly dir: string;
12
+ private readonly maxMessages: number;
13
+ private readonly enabled: boolean;
14
+
15
+ /** 内存缓存:key → 历史消息数组 */
16
+ private readonly cache = new Map<string, HistoryEntry[]>();
17
+
18
+ /**
19
+ * @param maxMessages 每个会话最多保留的消息条数
20
+ * @param enabled 是否启用持久化
21
+ */
22
+ public constructor(maxMessages: number, enabled: boolean) {
23
+ this.dir = path.resolve(process.cwd(), 'data', 'hermes-history');
24
+ this.maxMessages = maxMessages;
25
+ this.enabled = enabled;
26
+ if (enabled) {
27
+ fs.mkdirSync(this.dir, { recursive: true });
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 加载会话历史。
33
+ * 优先从内存缓存读取,缓存未命中时从磁盘加载。
34
+ */
35
+ public load(key: string): HistoryEntry[] {
36
+ if (!this.enabled) return [];
37
+ if (this.cache.has(key)) return this.cache.get(key)!;
38
+
39
+ try {
40
+ const filePath = this.filePath(key);
41
+ if (fs.existsSync(filePath)) {
42
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as HistoryEntry[];
43
+ if (Array.isArray(data)) {
44
+ this.cache.set(key, data);
45
+ return data;
46
+ }
47
+ }
48
+ } catch {
49
+ // 文件损坏或格式错误,忽略
50
+ }
51
+ return [];
52
+ }
53
+
54
+ /**
55
+ * 保存会话历史。
56
+ * 自动裁剪到 maxMessages 条,先写内存缓存再落盘。
57
+ */
58
+ public save(key: string, history: HistoryEntry[]): void {
59
+ if (!this.enabled) return;
60
+ const trimmed = history.slice(-this.maxMessages);
61
+ this.cache.set(key, trimmed);
62
+ try {
63
+ fs.writeFileSync(this.filePath(key), JSON.stringify(trimmed, null, 2), 'utf-8');
64
+ } catch {
65
+ // 写入失败不阻塞主流程(磁盘满等极端情况)
66
+ }
67
+ }
68
+
69
+ /** 清除会话的持久化历史 */
70
+ public clear(key: string): void {
71
+ if (!this.enabled) return;
72
+ this.cache.delete(key);
73
+ try {
74
+ const filePath = this.filePath(key);
75
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
76
+ } catch {
77
+ // 忽略
78
+ }
79
+ }
80
+
81
+ /** 根据会话键生成安全的文件名(group:123456 → group_123456.json) */
82
+ private filePath(key: string): string {
83
+ const safe = key.replace(/[<>:"/\\|?*]/g, '_');
84
+ return path.join(this.dir, `${safe}.json`);
85
+ }
86
+ }