@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,30 +1,30 @@
1
- <!DOCTYPE html>
2
- <html lang="zh">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>需要审批</title>
6
- <link rel="stylesheet" href="./style/approval.css">
7
- </head>
8
- <body>
9
- <div class="card" style="border: 2px solid {{riskColor}}">
10
- <div class="header">
11
- <span class="icon">⚠️</span>
12
- <span class="title" style="color: {{riskColor}}">需要审批</span>
13
- </div>
14
- {{toolSection}}
15
- <div class="section">
16
- <div class="label">命令</div>
17
- <div class="value">{{command}}</div>
18
- </div>
19
- {{previewSection}}
20
- <div class="actions">
21
- 回复以下内容进行审批:<br>
22
- <span class="cmd">批准</span> 或 <span class="cmd">通过</span> — 允许一次<br>
23
- <span class="cmd">拒绝</span> 或 <span class="cmd">deny</span> — 拒绝执行<br>
24
- <span class="cmd">本次允许</span> 或 <span class="cmd">session</span> — 本次会话内允许<br>
25
- <span class="cmd">始终允许</span> 或 <span class="cmd">always</span> — 永久允许该命令
26
- </div>
27
- <div class="footer">run: {{runId}}</div>
28
- </div>
29
- </body>
30
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>需要审批</title>
6
+ <link rel="stylesheet" href="./style/approval.css">
7
+ </head>
8
+ <body>
9
+ <div class="card" style="border: 2px solid {{riskColor}}">
10
+ <div class="header">
11
+ <span class="icon">⚠️</span>
12
+ <span class="title" style="color: {{riskColor}}">需要审批</span>
13
+ </div>
14
+ {{toolSection}}
15
+ <div class="section">
16
+ <div class="label">命令</div>
17
+ <div class="value">{{command}}</div>
18
+ </div>
19
+ {{previewSection}}
20
+ <div class="actions">
21
+ 回复以下内容进行审批:<br>
22
+ <span class="cmd">批准</span> 或 <span class="cmd">通过</span> — 允许一次<br>
23
+ <span class="cmd">拒绝</span> 或 <span class="cmd">deny</span> — 拒绝执行<br>
24
+ <span class="cmd">本次允许</span> 或 <span class="cmd">session</span> — 本次会话内允许<br>
25
+ <span class="cmd">始终允许</span> 或 <span class="cmd">always</span> — 永久允许该命令
26
+ </div>
27
+ <div class="footer">run: {{runId}}</div>
28
+ </div>
29
+ </body>
30
+ </html>
@@ -1,21 +1,21 @@
1
- <!DOCTYPE html>
2
- <html lang="zh">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Hermes 执行中</title>
6
- <link rel="stylesheet" href="./style/progress.css">
7
- </head>
8
- <body>
9
- <div class="card">
10
- <div class="header">
11
- <span class="icon">🔧</span>
12
- <span class="title">Hermes 执行中</span>
13
- <span class="elapsed">{{elapsed}}</span>
14
- </div>
15
- {{tools}}
16
- {{current}}
17
- {{preview}}
18
- <div class="footer">{{botName}} · SSE Progress</div>
19
- </div>
20
- </body>
21
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Hermes 执行中</title>
6
+ <link rel="stylesheet" href="./style/progress.css">
7
+ </head>
8
+ <body>
9
+ <div class="card">
10
+ <div class="header">
11
+ <span class="icon">🔧</span>
12
+ <span class="title">Hermes 执行中</span>
13
+ <span class="elapsed">{{elapsed}}</span>
14
+ </div>
15
+ {{tools}}
16
+ {{current}}
17
+ {{preview}}
18
+ <div class="footer">{{botName}} · SSE Progress</div>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -1,81 +1,81 @@
1
- /* Hermes 审批卡片(移植自 qq-hermes-bridge src/renderer.ts,MIT 协议,原作者 Amorter) */
2
- * {
3
- margin: 0;
4
- padding: 0;
5
- box-sizing: border-box;
6
- }
7
-
8
- body {
9
- font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
10
- background: #12101F;
11
- color: #2C2A3A;
12
- padding: 20px;
13
- width: 500px;
14
- }
15
-
16
- .card {
17
- background: #F8F0FF;
18
- border-radius: 12px;
19
- padding: 18px;
20
- }
21
-
22
- .header {
23
- display: flex;
24
- align-items: center;
25
- gap: 10px;
26
- margin-bottom: 14px;
27
- }
28
-
29
- .header .icon {
30
- font-size: 22px;
31
- }
32
-
33
- .header .title {
34
- font-size: 16px;
35
- font-weight: 600;
36
- }
37
-
38
- .section {
39
- margin: 10px 0;
40
- }
41
-
42
- .label {
43
- font-size: 12px;
44
- color: #9A8EA9;
45
- margin-bottom: 4px;
46
- }
47
-
48
- .value {
49
- font-family: monospace;
50
- font-size: 13px;
51
- background: #EADCF8;
52
- padding: 8px;
53
- border-radius: 6px;
54
- word-break: break-all;
55
- max-height: 100px;
56
- overflow: hidden;
57
- }
58
-
59
- .actions {
60
- margin-top: 14px;
61
- padding-top: 10px;
62
- border-top: 1px solid #DBC4E6;
63
- font-size: 13px;
64
- color: #9A8EA9;
65
- line-height: 1.8;
66
- }
67
-
68
- .actions .cmd {
69
- color: #6CD4FF;
70
- font-family: monospace;
71
- background: #EADCF8;
72
- padding: 2px 6px;
73
- border-radius: 4px;
74
- }
75
-
76
- .footer {
77
- margin-top: 10px;
78
- font-size: 11px;
79
- color: #9A8EA9;
80
- text-align: right;
81
- }
1
+ /* Hermes 审批卡片(移植自 qq-hermes-bridge src/renderer.ts,MIT 协议,原作者 Amorter) */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
10
+ background: #12101F;
11
+ color: #2C2A3A;
12
+ padding: 20px;
13
+ width: 500px;
14
+ }
15
+
16
+ .card {
17
+ background: #F8F0FF;
18
+ border-radius: 12px;
19
+ padding: 18px;
20
+ }
21
+
22
+ .header {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 10px;
26
+ margin-bottom: 14px;
27
+ }
28
+
29
+ .header .icon {
30
+ font-size: 22px;
31
+ }
32
+
33
+ .header .title {
34
+ font-size: 16px;
35
+ font-weight: 600;
36
+ }
37
+
38
+ .section {
39
+ margin: 10px 0;
40
+ }
41
+
42
+ .label {
43
+ font-size: 12px;
44
+ color: #9A8EA9;
45
+ margin-bottom: 4px;
46
+ }
47
+
48
+ .value {
49
+ font-family: monospace;
50
+ font-size: 13px;
51
+ background: #EADCF8;
52
+ padding: 8px;
53
+ border-radius: 6px;
54
+ word-break: break-all;
55
+ max-height: 100px;
56
+ overflow: hidden;
57
+ }
58
+
59
+ .actions {
60
+ margin-top: 14px;
61
+ padding-top: 10px;
62
+ border-top: 1px solid #DBC4E6;
63
+ font-size: 13px;
64
+ color: #9A8EA9;
65
+ line-height: 1.8;
66
+ }
67
+
68
+ .actions .cmd {
69
+ color: #6CD4FF;
70
+ font-family: monospace;
71
+ background: #EADCF8;
72
+ padding: 2px 6px;
73
+ border-radius: 4px;
74
+ }
75
+
76
+ .footer {
77
+ margin-top: 10px;
78
+ font-size: 11px;
79
+ color: #9A8EA9;
80
+ text-align: right;
81
+ }
@@ -1,120 +1,120 @@
1
- /* Hermes 进度卡片(移植自 qq-hermes-bridge src/renderer.ts,MIT 协议,原作者 Amorter) */
2
- * {
3
- margin: 0;
4
- padding: 0;
5
- box-sizing: border-box;
6
- }
7
-
8
- body {
9
- font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
10
- background: #12101F;
11
- color: #2C2A3A;
12
- padding: 20px;
13
- width: 500px;
14
- }
15
-
16
- .card {
17
- background: #F8F0FF;
18
- border-radius: 12px;
19
- padding: 18px;
20
- border: 1px solid #D4A5D4;
21
- }
22
-
23
- .header {
24
- display: flex;
25
- align-items: center;
26
- gap: 10px;
27
- margin-bottom: 14px;
28
- padding-bottom: 10px;
29
- border-bottom: 1px solid #DBC4E6;
30
- }
31
-
32
- .header .icon {
33
- font-size: 22px;
34
- }
35
-
36
- .header .title {
37
- font-size: 16px;
38
- font-weight: 600;
39
- color: #FF8BA7;
40
- }
41
-
42
- .header .elapsed {
43
- margin-left: auto;
44
- font-size: 12px;
45
- color: #9A8EA9;
46
- }
47
-
48
- .tool-row {
49
- display: flex;
50
- align-items: center;
51
- gap: 8px;
52
- padding: 4px 0;
53
- font-size: 13px;
54
- }
55
-
56
- .tool-row .icon {
57
- font-size: 14px;
58
- flex-shrink: 0;
59
- }
60
-
61
- .tool-row .name {
62
- color: #6CD4FF;
63
- font-family: monospace;
64
- }
65
-
66
- .tool-row .dur {
67
- color: #9A8EA9;
68
- font-size: 11px;
69
- margin-left: auto;
70
- }
71
-
72
- .preview {
73
- font-size: 11px;
74
- color: #9A8EA9;
75
- padding-left: 24px;
76
- margin-bottom: 4px;
77
- white-space: nowrap;
78
- overflow: hidden;
79
- text-overflow: ellipsis;
80
- }
81
-
82
- .current-tool {
83
- display: flex;
84
- align-items: center;
85
- gap: 8px;
86
- padding: 8px 0;
87
- margin-top: 6px;
88
- border-top: 1px dashed #DBC4E6;
89
- }
90
-
91
- .spinner {
92
- font-size: 16px;
93
- }
94
-
95
- .current-tool .name {
96
- color: #F5C842;
97
- font-family: monospace;
98
- font-size: 13px;
99
- }
100
-
101
- .response-preview {
102
- margin-top: 10px;
103
- padding: 8px;
104
- background: #EADCF8;
105
- border-radius: 6px;
106
- font-size: 12px;
107
- color: #9A8EA9;
108
- max-height: 60px;
109
- overflow: hidden;
110
- word-break: break-all;
111
- }
112
-
113
- .footer {
114
- margin-top: 12px;
115
- padding-top: 8px;
116
- border-top: 1px solid #DBC4E6;
117
- font-size: 11px;
118
- color: #9A8EA9;
119
- text-align: right;
120
- }
1
+ /* Hermes 进度卡片(移植自 qq-hermes-bridge src/renderer.ts,MIT 协议,原作者 Amorter) */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
10
+ background: #12101F;
11
+ color: #2C2A3A;
12
+ padding: 20px;
13
+ width: 500px;
14
+ }
15
+
16
+ .card {
17
+ background: #F8F0FF;
18
+ border-radius: 12px;
19
+ padding: 18px;
20
+ border: 1px solid #D4A5D4;
21
+ }
22
+
23
+ .header {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 10px;
27
+ margin-bottom: 14px;
28
+ padding-bottom: 10px;
29
+ border-bottom: 1px solid #DBC4E6;
30
+ }
31
+
32
+ .header .icon {
33
+ font-size: 22px;
34
+ }
35
+
36
+ .header .title {
37
+ font-size: 16px;
38
+ font-weight: 600;
39
+ color: #FF8BA7;
40
+ }
41
+
42
+ .header .elapsed {
43
+ margin-left: auto;
44
+ font-size: 12px;
45
+ color: #9A8EA9;
46
+ }
47
+
48
+ .tool-row {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 8px;
52
+ padding: 4px 0;
53
+ font-size: 13px;
54
+ }
55
+
56
+ .tool-row .icon {
57
+ font-size: 14px;
58
+ flex-shrink: 0;
59
+ }
60
+
61
+ .tool-row .name {
62
+ color: #6CD4FF;
63
+ font-family: monospace;
64
+ }
65
+
66
+ .tool-row .dur {
67
+ color: #9A8EA9;
68
+ font-size: 11px;
69
+ margin-left: auto;
70
+ }
71
+
72
+ .preview {
73
+ font-size: 11px;
74
+ color: #9A8EA9;
75
+ padding-left: 24px;
76
+ margin-bottom: 4px;
77
+ white-space: nowrap;
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ }
81
+
82
+ .current-tool {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ padding: 8px 0;
87
+ margin-top: 6px;
88
+ border-top: 1px dashed #DBC4E6;
89
+ }
90
+
91
+ .spinner {
92
+ font-size: 16px;
93
+ }
94
+
95
+ .current-tool .name {
96
+ color: #F5C842;
97
+ font-family: monospace;
98
+ font-size: 13px;
99
+ }
100
+
101
+ .response-preview {
102
+ margin-top: 10px;
103
+ padding: 8px;
104
+ background: #EADCF8;
105
+ border-radius: 6px;
106
+ font-size: 12px;
107
+ color: #9A8EA9;
108
+ max-height: 60px;
109
+ overflow: hidden;
110
+ word-break: break-all;
111
+ }
112
+
113
+ .footer {
114
+ margin-top: 12px;
115
+ padding-top: 8px;
116
+ border-top: 1px solid #DBC4E6;
117
+ font-size: 11px;
118
+ color: #9A8EA9;
119
+ text-align: right;
120
+ }
@@ -1,86 +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
- }
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
+ }