@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.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 (74) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +0 -14
  3. package/package.json +1 -1
  4. package/vendor/gugle-chat-bot/LICENSE +661 -661
  5. package/vendor/gugle-chat-bot/package.json +57 -57
  6. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
  7. package/vendor/gugle-chat-bot/src/api.json +4284 -4284
  8. package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
  9. package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
  10. package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
  11. package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
  12. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
  13. package/vendor/gugle-chat-bot/src/custom.ts +548 -548
  14. package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
  15. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
  16. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
  17. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
  18. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
  19. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
  20. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
  21. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
  22. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
  23. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
  24. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
  25. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
  26. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
  27. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
  28. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
  29. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
  30. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
  31. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
  32. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
  33. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
  34. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
  35. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
  36. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
  37. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
  38. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
  39. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
  40. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
  41. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
  42. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
  43. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
  44. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
  45. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
  46. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
  47. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
  48. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
  49. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
  50. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
  51. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
  52. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
  53. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
  54. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
  55. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
  56. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
  57. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
  58. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
  59. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
  60. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
  61. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
  62. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
  63. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
  64. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
  65. package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
  66. package/vendor/gugle-chat-bot/src/index.ts +695 -695
  67. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
  68. package/vendor/gugle-chat-bot/src/result.json +359 -359
  69. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
  70. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
  71. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
  72. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
  73. package/vendor/gugle-chat-bot/tsconfig.json +46 -46
  74. package/scripts/dump-npm-latest.mjs +0 -30
@@ -1,210 +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
- }
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
+ }
@@ -1,67 +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
- }
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
+ }