@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,86 @@
1
+ import { ApprovalChoice } from '@/features/hermes/types';
2
+
3
+ /**
4
+ * Hermes 触发判定与文本处理的纯函数(抽离自 HermesBridge 以便冒烟测试)。
5
+ */
6
+
7
+ export interface GroupTriggerOptions {
8
+ /** 消息纯文本 */
9
+ text: string;
10
+ /** 是否 @了 bot */
11
+ mentioned: boolean;
12
+ /** 群聊中是否需要 @bot 才触发 */
13
+ requireMention: boolean;
14
+ /** 关键词触发列表(需已转小写) */
15
+ keywordTriggers: string[];
16
+ }
17
+
18
+ export interface TriggerDecision {
19
+ triggered: boolean;
20
+ reason?: string;
21
+ }
22
+
23
+ /**
24
+ * 群聊触发判定。
25
+ * 相对上游新增的守卫:以 / 或 ! 开头的消息视为命令(交给 gugle-command),
26
+ * 除非显式 @bot,否则不触发 AI,避免与命令系统双重响应。
27
+ */
28
+ export function decideGroupTrigger(options: GroupTriggerOptions): TriggerDecision {
29
+ const text = options.text.trim();
30
+ if (options.mentioned) return { triggered: true, reason: 'mention' };
31
+ if (text.startsWith('/') || text.startsWith('!')) return { triggered: false };
32
+ const lower = text.toLowerCase();
33
+ for (const keyword of options.keywordTriggers) {
34
+ if (lower.includes(keyword)) return { triggered: true, reason: `keyword:${keyword}` };
35
+ }
36
+ if (!options.requireMention) return { triggered: true, reason: 'bare' };
37
+ return { triggered: false };
38
+ }
39
+
40
+ /**
41
+ * 解析审批回复文本为审批选择。
42
+ * 注意匹配顺序:始终允许 > 本次允许 > 拒绝 > 批准——
43
+ * 「不允许/不批准」包含「允许/批准」,上游先匹配批准导致否定词不可达,此处修正。
44
+ */
45
+ export function parseApprovalChoice(text: string): ApprovalChoice | null {
46
+ const lower = text.toLowerCase().trim();
47
+ if (['始终允许', 'always', '始终批准', '全部允许'].some(keyword => lower.includes(keyword))) return 'always';
48
+ if (['本次允许', 'session'].some(keyword => lower.includes(keyword))) return 'session';
49
+ if (['拒绝', 'deny', '不批准', '不允许'].some(keyword => lower.includes(keyword))) return 'deny';
50
+ if (['批准', '通过', 'approve', 'ok', '允许'].some(keyword => lower.includes(keyword))) return 'once';
51
+ return null;
52
+ }
53
+
54
+ /** 按最大长度切分消息(优先在换行/空格处断开) */
55
+ export function splitMessageText(text: string, maxLength: number): string[] {
56
+ if (maxLength <= 0 || text.length <= maxLength) return [text];
57
+ const chunks: string[] = [];
58
+ let remaining = text;
59
+ while (remaining.length > 0) {
60
+ if (remaining.length <= maxLength) {
61
+ chunks.push(remaining);
62
+ break;
63
+ }
64
+ let splitIndex = remaining.lastIndexOf('\n', maxLength);
65
+ if (splitIndex < maxLength * 0.3) {
66
+ splitIndex = remaining.lastIndexOf(' ', maxLength);
67
+ }
68
+ if (splitIndex < maxLength * 0.3) {
69
+ splitIndex = maxLength;
70
+ }
71
+ chunks.push(remaining.slice(0, splitIndex));
72
+ remaining = remaining.slice(splitIndex).trimStart();
73
+ }
74
+ return chunks;
75
+ }
76
+
77
+ /** 判断文本是否为停止指令 */
78
+ export function isStopCommand(text: string): boolean {
79
+ return text === '停止' || text.toLowerCase() === 'stop';
80
+ }
81
+
82
+ /** 判断文本是否为清除上下文指令 */
83
+ export function isResetCommand(text: string): boolean {
84
+ const lower = text.toLowerCase();
85
+ return text === '清除上下文' || text === '新对话' || lower === 'new' || lower === 'reset';
86
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Hermes 桥接类型定义(移植自 qq-hermes-bridge src/types.d.ts,MIT 协议,原作者 Amorter)。
3
+ * OneBot 消息段类型复用 @/type,此处只保留 Hermes API / 会话 / 审批 / 技能相关类型。
4
+ */
5
+
6
+ import type { QQBot } from '@/index';
7
+
8
+ // ── 路由 ──
9
+
10
+ /**
11
+ * Discord 路由的发送委托(由 DiscordBridge 注入,随 RouteInfo 传递)。
12
+ * HermesBridge 不直接持有 discord.js Client,经委托向原频道发送回复/图片。
13
+ */
14
+ export interface DiscordRouteContext {
15
+ guildName: string;
16
+ channelName: string;
17
+ /** 发送者是否为 Discord 侧管理员(服务器拥有者/管理服务器/管理员权限) */
18
+ isAdmin: boolean;
19
+ /** 发送文本;quote=true 时首条引用原始消息 */
20
+ send(text: string, quote: boolean): Promise<void>;
21
+ /** 发送 base64 图片(Discord 附件) */
22
+ sendImage(base64: string): Promise<void>;
23
+ /** 摘除机器人对触发消息的「处理中」表情回应(由 DiscordBridge 注入,绑定原消息) */
24
+ removeReaction?(): Promise<void>;
25
+ }
26
+
27
+ /** handleDiscordMessage 的调用参数(DiscordRouteContext + 消息元信息) */
28
+ export interface DiscordChatParams extends DiscordRouteContext {
29
+ /** 该频道桥接的 QQ 群号(会话共享与启用门控) */
30
+ groupId: string;
31
+ /** bot 的 Discord 用户 ID(@提及检测与剔除) */
32
+ botUserId?: string;
33
+ }
34
+
35
+ /** 消息来源路由:QQ 群聊、QQ 私聊或 Discord 频道 */
36
+ export interface RouteInfo {
37
+ type: 'group' | 'user' | 'discord';
38
+ /** QQ 群号;discord 路由为桥接的 QQ 群号(与 QQ 群共享会话上下文) */
39
+ groupId?: string;
40
+ /** QQ 号字符串;discord 路由为 `dc:<Discord用户ID>` */
41
+ userId: string;
42
+ /** discord 路由:频道 ID */
43
+ channelId?: string;
44
+ /** discord 路由:发送委托(DiscordBridge 注入) */
45
+ discord?: DiscordRouteContext;
46
+ }
47
+
48
+ // ── 会话 ──
49
+
50
+ /** 对话历史条目 */
51
+ export interface HistoryEntry {
52
+ role: string;
53
+ content: string;
54
+ /** 发言人(user 消息)或回复对象(assistant 消息)的用户 id */
55
+ userId?: string;
56
+ /** assistant 消息回复对象的发送者标签(用于在历史中标注「你回复 xxx 的话」) */
57
+ label?: string;
58
+ }
59
+
60
+ /** 对话会话 */
61
+ export interface Session {
62
+ history: HistoryEntry[];
63
+ sessionVersion: number;
64
+ }
65
+
66
+ // ── 运行状态 ──
67
+
68
+ /** 活跃运行中的工具调用记录 */
69
+ export interface RunToolRecord {
70
+ name: string;
71
+ duration: number;
72
+ error: boolean;
73
+ preview?: string;
74
+ }
75
+
76
+ /** 活跃运行中正在执行的工具 */
77
+ export interface RunCurrentTool {
78
+ name: string;
79
+ preview?: string;
80
+ startedAt: number;
81
+ }
82
+
83
+ /** 活跃的 Hermes 运行状态 */
84
+ export interface RunState {
85
+ route: RouteInfo;
86
+ tools: RunToolRecord[];
87
+ currentTool: RunCurrentTool | null;
88
+ startedAt: number;
89
+ lastProgressSent: number;
90
+ sendingProgress: boolean;
91
+ messageDelta: string;
92
+ pendingText: string;
93
+ sentTextLength: number;
94
+ lastTextSent: number;
95
+ finalOutput: string;
96
+ userMsgId: number;
97
+ /** 本轮对话发送者的展示标签(assistant 历史据此标注回复对象) */
98
+ senderLabel: string;
99
+ /** 工具结果回喂轮次(0 = 用户消息首轮;达上限后查询结果不再回喂,直接附在回复里) */
100
+ feedbackDepth?: number;
101
+ /** 「处理中」表情回应的持久化记录键(回喂轮转移给下一轮,对话链完全结束时摘除) */
102
+ reactionKey?: string;
103
+ /** 本轮是否已经贴过引用(逐句发送时只有首条单元贴引用,避免每条都重复引用) */
104
+ replySent?: boolean;
105
+ /** 本轮是否以 run.failed 结束(失败文案必须发出,不因已逐句下发而跳过) */
106
+ failed?: boolean;
107
+ /** 私聊文字回执的消息 id(AI 开始回复时撤回;QQ 表情回应失败时的降级方案) */
108
+ receiptId?: number;
109
+ /** 回执是否已转移给查询技能的回喂轮(转移后由下一轮负责撤回) */
110
+ receiptTransferred?: boolean;
111
+ /**
112
+ * 发送串行链:所有逐句/尾段发送都挂到这条链上按序执行。
113
+ * message.delta 的 flush 是 fire-and-forget,若并发执行会互相插队,
114
+ * 导致分条消息顺序错乱(后一批的句子插到前一批未发完的句子之前)。
115
+ */
116
+ sendChain?: Promise<void>;
117
+ stream?: { abort(): void };
118
+ }
119
+
120
+ // ── 审批 ──
121
+
122
+ /** 待审批记录 */
123
+ export interface Approval {
124
+ runId: string;
125
+ route: RouteInfo;
126
+ data: HermesApprovalEvent;
127
+ createdAt: number;
128
+ timeoutTimer?: ReturnType<typeof setTimeout>;
129
+ }
130
+
131
+ /** 审批选择 */
132
+ export type ApprovalChoice = 'once' | 'deny' | 'always' | 'session';
133
+
134
+ // ── Hermes SSE 事件类型 ──
135
+
136
+ export interface HermesToolStartedEvent {
137
+ event: 'tool.started';
138
+ run_id: string;
139
+ timestamp: number;
140
+ tool: string;
141
+ preview?: string;
142
+ }
143
+
144
+ export interface HermesToolCompletedEvent {
145
+ event: 'tool.completed';
146
+ run_id: string;
147
+ timestamp: number;
148
+ tool: string;
149
+ duration: number;
150
+ error: boolean;
151
+ }
152
+
153
+ export interface HermesMessageDeltaEvent {
154
+ event: 'message.delta';
155
+ run_id: string;
156
+ timestamp: number;
157
+ delta: string;
158
+ }
159
+
160
+ export interface HermesApprovalEvent {
161
+ event: 'approval.request';
162
+ run_id: string;
163
+ timestamp: number;
164
+ command: string;
165
+ pattern_key?: string;
166
+ description?: string;
167
+ }
168
+
169
+ export interface HermesRunCompletedEvent {
170
+ event: 'run.completed';
171
+ run_id: string;
172
+ timestamp: number;
173
+ output: string;
174
+ }
175
+
176
+ export interface HermesRunFailedEvent {
177
+ event: 'run.failed';
178
+ run_id: string;
179
+ timestamp: number;
180
+ error: string;
181
+ }
182
+
183
+ export interface HermesReasoningEvent {
184
+ event: 'reasoning.available';
185
+ run_id: string;
186
+ timestamp: number;
187
+ text: string;
188
+ }
189
+
190
+ /** Hermes SSE 事件联合类型 */
191
+ export type HermesSSEEvent =
192
+ | HermesToolStartedEvent
193
+ | HermesToolCompletedEvent
194
+ | HermesMessageDeltaEvent
195
+ | HermesApprovalEvent
196
+ | HermesRunCompletedEvent
197
+ | HermesRunFailedEvent
198
+ | HermesReasoningEvent;
199
+
200
+ /** Hermes SSE 事件回调映射 */
201
+ export interface HermesEventCallbacks {
202
+ 'tool.started'?: (ev: HermesToolStartedEvent) => void;
203
+ 'tool.completed'?: (ev: HermesToolCompletedEvent) => void;
204
+ 'message.delta'?: (ev: HermesMessageDeltaEvent) => void;
205
+ 'approval.request'?: (ev: HermesApprovalEvent) => void;
206
+ 'run.completed'?: (ev: HermesRunCompletedEvent) => void;
207
+ 'run.failed'?: (ev: HermesRunFailedEvent) => void;
208
+ 'reasoning.available'?: (ev: HermesReasoningEvent) => void;
209
+ _end?: () => void;
210
+ _error?: (err: Error) => void;
211
+ _any?: (ev: HermesSSEEvent) => void;
212
+ }
213
+
214
+ // ── 技能系统 ──
215
+
216
+ /** 群管理 API 门面(由 HermesBridge 基于 QQBot 实现后注入技能) */
217
+ export interface GroupAdminApi {
218
+ /** 禁言群成员(durationSec=0 解除) */
219
+ setGroupBan(groupId: string, userId: string, durationSec: number): Promise<unknown>;
220
+ /** 踢出群成员 */
221
+ setGroupKick(groupId: string, userId: string): Promise<unknown>;
222
+ /** 开启/关闭全员禁言 */
223
+ setGroupWholeBan(groupId: string, enable: boolean): Promise<unknown>;
224
+ }
225
+
226
+ /** 技能执行上下文 */
227
+ export interface SkillExecuteContext {
228
+ /** QQBot 实例(卡片类技能经其 axiosInstance/logger 访问外部 API 与渲染) */
229
+ bot: QQBot;
230
+ api: GroupAdminApi;
231
+ route: RouteInfo;
232
+ args: string[];
233
+ }
234
+
235
+ /**
236
+ * 技能执行产出:纯文本摘要字符串,或 `{ message, images }`——
237
+ * images 为待发送的 base64 图片(允许带 base64:// 前缀),由完成处理器逐张发出。
238
+ */
239
+ export type SkillExecution = string | { message: string; images?: string[] };
240
+
241
+ /** 技能定义 */
242
+ export interface Skill {
243
+ name: string;
244
+ usage: string;
245
+ description: string;
246
+ adminOnly: boolean;
247
+ /**
248
+ * true:查询类技能——执行结果不作为摘要发给用户,而是回喂给 AI 整理回答
249
+ * (完成处理器会把结果以 [工具结果] 输入再提交一轮 run,有防循环上限);
250
+ * false/缺省:动作/卡片类——执行摘要附加到回复文本后直接发出。
251
+ */
252
+ feedback?: boolean;
253
+ execute(ctx: SkillExecuteContext): Promise<SkillExecution>;
254
+ }
255
+
256
+ /** 查询类技能的回喂数据 */
257
+ export interface SkillFeedback {
258
+ skill: string;
259
+ data: string;
260
+ }
261
+
262
+ /** 技能执行结果 */
263
+ export type SkillResult =
264
+ | { ok: true; skill: string; message: string; images?: string[] }
265
+ | { ok: false; skill: string; error: string };
266
+
267
+ /** processTags 的返回:清理后的文本 + 全部技能产出的图片 + 查询类技能的回喂数据 */
268
+ export interface ProcessedSkillOutput {
269
+ text: string;
270
+ images: string[];
271
+ feedback: SkillFeedback[];
272
+ }
273
+
274
+ // ── 多模态消息内容 ──
275
+
276
+ /** OpenAI 风格消息内容段(用于多模态 user_message) */
277
+ export type MessageContentPart = { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } };
278
+
279
+ /** formatMessage 的返回:文本 + 提取到的图片列表(均为 base64 data URL) */
280
+ export interface FormattedMessage {
281
+ text: string;
282
+ /** base64 data URL(data:image/...;base64,...) */
283
+ images: string[];
284
+ }
285
+
286
+ // ── OneBot API 响应(功能内使用的最小子集) ──
287
+
288
+ /** get_msg API 返回值 */
289
+ export interface OneBotGetMsgResponse {
290
+ message_id: number;
291
+ sender: {
292
+ user_id: number;
293
+ nickname: string;
294
+ };
295
+ time: number;
296
+ message: import('@/type').Message[];
297
+ }
298
+
299
+ /** get_forward_msg 节点(兼容 NapCat node 格式与 go-cqhttp 旧格式) */
300
+ export interface OneBotForwardNode {
301
+ type?: 'node';
302
+ data?: {
303
+ user_id?: number;
304
+ nickname?: string;
305
+ card?: string;
306
+ time?: number;
307
+ message?: import('@/type').Message[];
308
+ content?: import('@/type').Message[] | string;
309
+ };
310
+ sender?: { user_id?: number; nickname?: string; card?: string };
311
+ time?: number;
312
+ content?: import('@/type').Message[] | string;
313
+ }
314
+
315
+ /** get_forward_msg API 返回值 */
316
+ export interface OneBotGetForwardMsgResponse {
317
+ messages: OneBotForwardNode[];
318
+ }
319
+
320
+ /** get_group_member_info API 返回值 */
321
+ export interface OneBotGroupMemberInfo {
322
+ group_id: number;
323
+ user_id: number;
324
+ nickname: string;
325
+ card: string;
326
+ role: string;
327
+ }
@@ -0,0 +1,156 @@
1
+ import { QQBot } from '@/index';
2
+ import { GroupDecreaseNoticeWSMSG, GroupIncreaseNoticeWSMSG, GroupRequestWSMSG } from '@/type';
3
+ import { EventDataManager } from '@/event';
4
+ import { getFeatureConfig } from '@/config/features';
5
+
6
+ function handleGroupDecreaseNotice(bot: QQBot, msg: GroupDecreaseNoticeWSMSG) {
7
+ if (!getFeatureConfig('management').groups.includes(msg.group_id)) return;
8
+ EventDataManager.getStorage('management', 'ban_list').then(raw => {
9
+ // 归一化后再判重:兼容历史字符串条目,避免同一用户被写成两份(数字 + 字符串)
10
+ const banList = Management.normalizeBanList(raw);
11
+ const userId = Number(msg.user_id);
12
+ if (!Number.isSafeInteger(userId) || userId <= 0) return;
13
+ // 已归一化集合里存在即视为已拉黑;顺带把归一化结果写回,清理历史脏数据
14
+ const alreadyBanned = banList.includes(userId);
15
+ if (alreadyBanned && JSON.stringify(banList) === JSON.stringify(raw)) return;
16
+ if (!alreadyBanned) banList.push(userId);
17
+ EventDataManager.setStorage('management', 'ban_list', banList).then(() => {
18
+ if (alreadyBanned) return; // 仅做数据清理,不再重复播报
19
+ const reason = msg.sub_type === 'kick' ? '被管理员移出群聊' : '退出群聊';
20
+ bot.getUserInfo(msg.user_id).then(userInfo => {
21
+ bot.sendGroupMsg(msg.group_id, [
22
+ {
23
+ type: 'text',
24
+ data: {
25
+ text: `用户 ${userInfo.nickname}(${userInfo.user_id}) 已${reason},现已加入黑名单,将自动拒绝后续加群申请!`
26
+ }
27
+ }
28
+ ]);
29
+ });
30
+ });
31
+ });
32
+ }
33
+
34
+ function handleGroupIncreaseNotice(bot: QQBot, msg: GroupIncreaseNoticeWSMSG) {
35
+ if (!getFeatureConfig('management').groups.includes(msg.group_id)) return;
36
+ EventDataManager.getStorage('management', 'ban_list').then(raw => {
37
+ const banList = Management.normalizeBanList(raw);
38
+ const userId = Number(msg.user_id);
39
+ if (!banList.includes(userId)) return;
40
+ if (msg.sub_type === 'invite' && getFeatureConfig('management').operators?.includes(msg.operator_id)) {
41
+ // 管理员主动邀请 = 视为赦免:移出黑名单
42
+ EventDataManager.setStorage(
43
+ 'management',
44
+ 'ban_list',
45
+ banList.filter(id => id !== userId)
46
+ ).then();
47
+ return;
48
+ }
49
+ bot.kick(msg.group_id, msg.user_id);
50
+ bot.getUserInfo(msg.user_id).then(userInfo => {
51
+ bot.sendGroupMsg(msg.group_id, [
52
+ {
53
+ type: 'text',
54
+ data: {
55
+ text: `黑名单用户 ${userInfo.nickname}(${userInfo.user_id}) 加群,已自动移出!`
56
+ }
57
+ }
58
+ ]);
59
+ });
60
+ });
61
+ }
62
+
63
+ function handleGroupRequest(bot: QQBot, msg: GroupRequestWSMSG) {
64
+ if (!getFeatureConfig('management').groups.includes(msg.group_id)) return;
65
+ if (msg.sub_type !== 'add') return;
66
+ EventDataManager.getStorage('management', 'ban_list').then(raw => {
67
+ const banList = Management.normalizeBanList(raw);
68
+ if (!banList.includes(Number(msg.user_id))) return;
69
+ bot.denyGroupAddRequest(msg.flag, '你已被拉入黑名单,系统自动拒绝!');
70
+ bot.getUserInfo(msg.user_id).then(userInfo => {
71
+ bot.sendGroupMsg(msg.group_id, [
72
+ {
73
+ type: 'text',
74
+ data: {
75
+ text: `黑名单用户 ${userInfo.nickname}(${userInfo.user_id}) 申请加群,已自动拒绝!`
76
+ }
77
+ }
78
+ ]);
79
+ });
80
+ });
81
+ }
82
+
83
+ export class Management {
84
+ public static handleGroupDecreaseNotice(bot: QQBot, msg: GroupDecreaseNoticeWSMSG) {
85
+ handleGroupDecreaseNotice(bot, msg);
86
+ }
87
+
88
+ public static handleGroupIncreaseNotice(bot: QQBot, msg: GroupIncreaseNoticeWSMSG) {
89
+ handleGroupIncreaseNotice(bot, msg);
90
+ }
91
+
92
+ public static handleGroupRequest(bot: QQBot, msg: GroupRequestWSMSG) {
93
+ handleGroupRequest(bot, msg);
94
+ }
95
+
96
+ /**
97
+ * /pardon <QQ号>:把用户从黑名单移除(赦免)。
98
+ * 仅限 management 功能启用的群 + 管理员使用。
99
+ *
100
+ * 权限校验(**全部在此处完成**,不依赖调用方):
101
+ * - QQ 侧 operatorId 为 number → 必须在 management.json `operators` 白名单内;
102
+ * - Discord 侧 operatorId 为 `dc:<用户ID>` → 必须是 management.json `discordOperators`
103
+ * 白名单成员。注意:此前仅校验 `dc:` 前缀即放行,导致被桥接频道里任意成员都能赦免黑名单用户。
104
+ *
105
+ * @returns true=已移除;false=不在黑名单/无权限
106
+ */
107
+ public static async pardon(groupId: number, operatorId: number | string, userId: number): Promise<boolean> {
108
+ const config = getFeatureConfig('management');
109
+ if (!config.groups.includes(groupId)) return false;
110
+ if (!this.isOperatorAllowed(operatorId)) return false;
111
+
112
+ // 黑名单历史数据/不同 OneBot 实现可能写入字符串形态的 QQ 号,
113
+ // 统一按数值比较,避免「明明在黑名单里却删不掉」。
114
+ const banList = this.normalizeBanList(await EventDataManager.getStorage('management', 'ban_list'));
115
+ if (!banList.includes(userId)) return false;
116
+
117
+ const next = banList.filter(id => id !== userId);
118
+ await EventDataManager.setStorage('management', 'ban_list', next);
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * 操作者是否有权执行管理操作。
124
+ *
125
+ * - number(QQ 号)→ operators 白名单;
126
+ * - `dc:` 前缀 → discordOperators 白名单(必须是显式配置的用户);
127
+ * - 其他形态 → 拒绝。
128
+ */
129
+ public static isOperatorAllowed(operatorId: number | string): boolean {
130
+ const config = getFeatureConfig('management');
131
+ if (typeof operatorId === 'number') {
132
+ return config.operators?.includes(operatorId) ?? false;
133
+ }
134
+ if (typeof operatorId === 'string' && /^dc:/i.test(operatorId)) {
135
+ const target = operatorId.slice(3).trim();
136
+ if (!target) return false;
137
+ return (config.discordOperators ?? []).some(item => item.replace(/^dc:/i, '').trim() === target);
138
+ }
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * 归一化黑名单:字符串数字转 number、剔除非法项、去重。
144
+ * 兼容历史脏数据(如被写成 `"2017065007"` 的条目),避免「明明在黑名单里却删不掉」。
145
+ */
146
+ public static normalizeBanList(raw: unknown): number[] {
147
+ if (!Array.isArray(raw)) return [];
148
+ const result: number[] = [];
149
+ for (const item of raw) {
150
+ const value = typeof item === 'string' ? Number(item.trim()) : item;
151
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) continue;
152
+ if (!result.includes(value)) result.push(value);
153
+ }
154
+ return result;
155
+ }
156
+ }
@@ -0,0 +1,127 @@
1
+ import axios, { AxiosResponse } from 'axios';
2
+ import { JSDOM } from 'jsdom';
3
+
4
+ export declare type Version = {
5
+ success: boolean;
6
+ latest: {
7
+ release: string;
8
+ snapshot: string;
9
+ };
10
+ versions: {
11
+ id: string;
12
+ type: string;
13
+ url: string;
14
+ time: string;
15
+ releaseTime: string;
16
+ sha1: string;
17
+ complianceLevel: number;
18
+ }[];
19
+ };
20
+
21
+ export declare type MinecraftServerStatus = {
22
+ online: boolean;
23
+ version: string;
24
+ motd: string;
25
+ players: { online: number; max: number; list: string[] };
26
+ };
27
+
28
+ export declare type WikiResult = {
29
+ success: boolean;
30
+ url: string;
31
+ title: string;
32
+ desc: string;
33
+ };
34
+
35
+ export class MinecraftAPI {
36
+ private static readonly VERSION_MANIFEST_URL: string =
37
+ 'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json';
38
+
39
+ public static async getVersion(): Promise<Version> {
40
+ try {
41
+ const res: AxiosResponse<{
42
+ latest: {
43
+ release: '';
44
+ snapshot: '';
45
+ };
46
+ versions: [];
47
+ }> = await axios.get(MinecraftAPI.VERSION_MANIFEST_URL);
48
+ return {
49
+ success: true,
50
+ ...res.data
51
+ };
52
+ } catch (_) {
53
+ return {
54
+ success: false,
55
+ latest: {
56
+ release: '',
57
+ snapshot: ''
58
+ },
59
+ versions: []
60
+ };
61
+ }
62
+ }
63
+
64
+ public static async getMinecraftServerStatus(ip: string, port?: number): Promise<MinecraftServerStatus> {
65
+ try {
66
+ if (ip.includes(':')) port = undefined;
67
+ const res: AxiosResponse = await axios.get(
68
+ `https://api.mcstatus.io/v2/status/java/${ip}${port ? ':' + port : ''}`
69
+ );
70
+ const result: MinecraftServerStatus = {
71
+ online: res.data.online,
72
+ version: res.data.version.name_clean,
73
+ motd: res.data.motd.clean,
74
+ players: {
75
+ online: res.data.players.online,
76
+ max: res.data.players.max,
77
+ list: []
78
+ }
79
+ };
80
+ for (const player of res.data.players.list as { name_clean: string }[]) {
81
+ result.players.list.push(player.name_clean);
82
+ }
83
+ return result;
84
+ } catch (e) {
85
+ return {
86
+ online: false,
87
+ version: '',
88
+ motd: '',
89
+ players: {
90
+ online: 0,
91
+ max: 0,
92
+ list: []
93
+ }
94
+ };
95
+ }
96
+ }
97
+
98
+ public static async getMinecraftWiki(keyword: string): Promise<WikiResult> {
99
+ try {
100
+ const res: AxiosResponse = await axios.get(`https://zh.minecraft.wiki/w/?search=${keyword}`);
101
+ const dom = new JSDOM(res.data);
102
+ const document = dom.window.document;
103
+ const content = document.getElementById('mw-content-text');
104
+ const result: WikiResult = {
105
+ success: !content?.getElementsByClassName('mw-search-nonefound').length || false,
106
+ url: res.request.res.responseUrl || '',
107
+ title: document.getElementById('firstHeading')?.textContent || '',
108
+ desc: ''
109
+ };
110
+ for (let child of content?.getElementsByClassName('mw-parser-output').item(0)?.children || []) {
111
+ if (child.id == 'toc') break;
112
+ if (child.tagName != 'P') continue;
113
+ if (!child.textContent) continue;
114
+ if (child.textContent == '\n' || child.textContent == '\\n') continue;
115
+ result.desc += '\r\n' + child.textContent.replace('\n', '').replace('\\n', '');
116
+ }
117
+ return result;
118
+ } catch (_) {
119
+ return {
120
+ success: false,
121
+ url: '',
122
+ title: '',
123
+ desc: ''
124
+ };
125
+ }
126
+ }
127
+ }