agent-loft 0.2.31 → 0.2.32
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.
- package/README.md +8 -3
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +10 -6
- package/src/agents/claude/session.ts +30 -5
- package/src/agents/codex/session.ts +27 -6
- package/src/channels/web/api.ts +4 -0
- package/src/channels/web/http.ts +2 -2
- package/src/channels/web/server.ts +8 -0
- package/src/channels/web/skills-api.ts +225 -0
- package/src/core/chat-mode.ts +3 -3
- package/src/core/command-catalog.ts +8 -1
- package/src/core/commands.ts +3 -0
- package/src/core/hub.ts +54 -5
- package/src/core/personal.ts +3 -1
- package/src/core/skill-runtime.ts +359 -0
- package/src/core/skills-bridge.ts +288 -0
- package/src/core/skills.ts +675 -0
- package/src/core/tasks-bridge.ts +5 -1
- package/src/core/tasks-mcp.ts +11 -0
- package/src/core/types.ts +20 -1
- package/src/core/zip.ts +98 -0
- package/src/main.ts +4 -0
- package/src/mcp/tasks-server.ts +125 -4
- package/web/css/skills.css +368 -0
- package/web/index.html +1 -0
- package/web/lib/dom.js +1 -0
- package/web/lib/ws.js +4 -0
- package/web/state.js +1 -1
- package/web/views/composer.js +15 -3
- package/web/views/layout.js +10 -0
- package/web/views/mention.js +193 -0
- package/web/views/messages.js +2 -2
- package/web/views/sidebar.js +1 -0
- package/web/views/skills.js +703 -0
- package/web/views/tasks.js +11 -2
- package/web/views/topbar.js +3 -3
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/* ---------- 技能页(版式沿用定时任务页的 .tasks-page / .task-card) ---------- */
|
|
2
|
+
|
|
3
|
+
.skill-head-btns {
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.skill-tabs {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 12px;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.skill-tabs .seg {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
padding: 3px;
|
|
19
|
+
border-radius: 10px;
|
|
20
|
+
background: var(--bg-soft);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.skill-tabs .seg button {
|
|
24
|
+
padding: 6px 12px;
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
color: var(--fg-soft);
|
|
28
|
+
white-space: nowrap;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.skill-tabs .seg button.on {
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--fg);
|
|
34
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.skill-search {
|
|
38
|
+
flex: 1;
|
|
39
|
+
min-width: 160px;
|
|
40
|
+
padding: 7px 12px;
|
|
41
|
+
border: 1px solid var(--border);
|
|
42
|
+
border-radius: 10px;
|
|
43
|
+
background: var(--bg-input);
|
|
44
|
+
color: var(--fg);
|
|
45
|
+
font: inherit;
|
|
46
|
+
font-size: 13px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.skill-list-box {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.skill-card .skill-name {
|
|
56
|
+
font-family: var(--mono);
|
|
57
|
+
font-size: 15px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.skill-card .btn {
|
|
61
|
+
padding: 5px 12px;
|
|
62
|
+
font-size: 13px;
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.skill-warn {
|
|
67
|
+
color: var(--danger);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.skill-note {
|
|
71
|
+
margin: 0;
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
line-height: 1.6;
|
|
74
|
+
color: var(--fg-soft);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.skill-empty {
|
|
78
|
+
padding: 32px 0;
|
|
79
|
+
text-align: center;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
color: var(--fg-faint);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.skills-page .badge,
|
|
85
|
+
.skill-detail .badge {
|
|
86
|
+
font-size: 11px;
|
|
87
|
+
padding: 1px 6px;
|
|
88
|
+
border-radius: 6px;
|
|
89
|
+
background: var(--bg-soft);
|
|
90
|
+
color: var(--fg-soft);
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.skill-detail .badge.ok {
|
|
95
|
+
color: var(--ok);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* 查看 */
|
|
99
|
+
.skill-detail {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 14px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.skill-desc {
|
|
106
|
+
margin: 0;
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
line-height: 1.6;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.skill-section h4 {
|
|
112
|
+
margin: 0 0 8px;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
color: var(--fg-soft);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.skill-md {
|
|
119
|
+
padding: 12px 14px;
|
|
120
|
+
border: 1px solid var(--border);
|
|
121
|
+
border-radius: 10px;
|
|
122
|
+
font-size: 14px;
|
|
123
|
+
max-height: 50vh;
|
|
124
|
+
overflow: auto;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.skill-vars {
|
|
128
|
+
margin: 0 0 8px;
|
|
129
|
+
padding: 0;
|
|
130
|
+
list-style: none;
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 6px;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.skill-vars li {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: 6px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.skill-var-desc {
|
|
145
|
+
color: var(--fg-soft);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.skill-files {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 2px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.skill-file {
|
|
155
|
+
display: flex;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
gap: 12px;
|
|
158
|
+
padding: 6px 10px;
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
text-align: left;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.skill-file:hover {
|
|
165
|
+
background: var(--bg-hover);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.skill-file span {
|
|
169
|
+
color: var(--fg-faint);
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.skill-file-preview:empty {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.skill-file-name {
|
|
178
|
+
margin: 10px 0 6px;
|
|
179
|
+
font-family: var(--mono);
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
color: var(--fg-soft);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.skill-file-preview pre,
|
|
185
|
+
.skill-raw {
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 10px 12px;
|
|
188
|
+
border-radius: 10px;
|
|
189
|
+
background: var(--code-bg);
|
|
190
|
+
font-family: var(--mono);
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
line-height: 1.5;
|
|
193
|
+
white-space: pre-wrap;
|
|
194
|
+
word-break: break-word;
|
|
195
|
+
max-height: 50vh;
|
|
196
|
+
overflow: auto;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* 编辑 */
|
|
200
|
+
.skill-body-input {
|
|
201
|
+
font-family: var(--mono) !important;
|
|
202
|
+
font-size: 13px !important;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.skill-var-edit {
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
gap: 6px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.skill-var-edit:empty {
|
|
212
|
+
display: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.skill-var-item {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 4px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.skill-var-hosts[hidden] {
|
|
222
|
+
display: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.skill-var-hosts {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 8px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.skill-var-hosts span {
|
|
232
|
+
flex-shrink: 0;
|
|
233
|
+
font-size: 13px;
|
|
234
|
+
color: var(--fg-soft);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.skill-var-hosts input {
|
|
238
|
+
flex: 1;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
font-family: var(--mono);
|
|
241
|
+
font-size: 13px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.skill-guard {
|
|
245
|
+
color: var(--ok) !important;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.skill-pending {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 8px;
|
|
252
|
+
padding: 8px 10px;
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
border: 1px solid var(--warn-border);
|
|
255
|
+
background: var(--warn-bg);
|
|
256
|
+
font-size: 13px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.skill-pending .btn {
|
|
260
|
+
flex-shrink: 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.skill-var-line {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 6px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.skill-var-line input:not([type="checkbox"]) {
|
|
270
|
+
flex: 1;
|
|
271
|
+
min-width: 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.skill-var-line input:first-child {
|
|
275
|
+
font-family: var(--mono);
|
|
276
|
+
flex: 0 1 220px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.skill-var-line label {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 3px;
|
|
283
|
+
font-size: 13px;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
color: var(--fg-soft);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.skill-var-edit + .btn,
|
|
289
|
+
.skill-var-row .btn,
|
|
290
|
+
.skill-upload-btns .btn {
|
|
291
|
+
align-self: flex-start;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.task-form .btn.ghost,
|
|
295
|
+
.skill-var-row .btn.ghost {
|
|
296
|
+
border-color: transparent;
|
|
297
|
+
background: var(--bg-soft);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.skill-var-row {
|
|
301
|
+
display: flex;
|
|
302
|
+
gap: 6px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.skill-var-row input {
|
|
306
|
+
flex: 1;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
font-family: var(--mono);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.skill-visibility {
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction: column;
|
|
314
|
+
gap: 8px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.skill-people {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-wrap: wrap;
|
|
320
|
+
gap: 6px 14px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.skill-people[hidden] {
|
|
324
|
+
display: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.skill-person {
|
|
328
|
+
display: inline-flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: 5px;
|
|
331
|
+
font-size: 14px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.skill-upload-btns {
|
|
335
|
+
display: flex;
|
|
336
|
+
gap: 8px;
|
|
337
|
+
margin-top: 14px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* @ 补全:在弹窗里的输入框下面弹出 */
|
|
341
|
+
.mention-wrap {
|
|
342
|
+
position: relative;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.slash-menu.below {
|
|
346
|
+
top: calc(100% + 4px);
|
|
347
|
+
bottom: auto;
|
|
348
|
+
max-height: 240px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@media (max-width: 640px) {
|
|
352
|
+
.skills-page .tasks-head {
|
|
353
|
+
flex-direction: column;
|
|
354
|
+
gap: 12px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.skill-head-btns .btn {
|
|
358
|
+
padding: 7px 10px;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.skill-var-line {
|
|
362
|
+
flex-wrap: wrap;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.skill-var-line input:first-child {
|
|
366
|
+
flex: 1 1 100%;
|
|
367
|
+
}
|
|
368
|
+
}
|
package/web/index.html
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<link rel="stylesheet" href="/css/settings.css" />
|
|
26
26
|
<link rel="stylesheet" href="/css/schedule.css" />
|
|
27
27
|
<link rel="stylesheet" href="/css/tasks.css" />
|
|
28
|
+
<link rel="stylesheet" href="/css/skills.css" />
|
|
28
29
|
<link rel="stylesheet" href="/css/mobile.css" />
|
|
29
30
|
</head>
|
|
30
31
|
<body>
|
package/web/lib/dom.js
CHANGED
|
@@ -34,6 +34,7 @@ export const ICON = {
|
|
|
34
34
|
clip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.4 11.6-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg>',
|
|
35
35
|
mic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5"/></svg>',
|
|
36
36
|
more: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>',
|
|
37
|
+
spark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9Z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z"/></svg>',
|
|
37
38
|
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
|
|
38
39
|
down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="m6 9 6 6 6-6"/></svg>',
|
|
39
40
|
gear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg>',
|
package/web/lib/ws.js
CHANGED
|
@@ -201,6 +201,10 @@ function onWsMessage(msg) {
|
|
|
201
201
|
// 自己的定时任务变了:定时任务页开着的话会去重新拉(views/tasks.js)
|
|
202
202
|
document.dispatchEvent(new Event("aim:tasks"));
|
|
203
203
|
return;
|
|
204
|
+
case "skills":
|
|
205
|
+
// 技能变了(自己的、装了的、广场上的):技能页重新拉,@ 补全的缓存作废
|
|
206
|
+
document.dispatchEvent(new Event("aim:skills"));
|
|
207
|
+
return;
|
|
204
208
|
case "notice":
|
|
205
209
|
// 正看着这条会话就不打扰;普通网页靠 Web Push,这里只给桌面端
|
|
206
210
|
if (!desktop) return;
|
package/web/state.js
CHANGED
package/web/views/composer.js
CHANGED
|
@@ -8,6 +8,7 @@ import { addItem, openConversation, openLightbox, renderEmpty, scrollToBottom }
|
|
|
8
8
|
import { renderScheduled } from "./schedule.js";
|
|
9
9
|
import { cancelVoice, SpeechRec, toggleVoice } from "./voice.js";
|
|
10
10
|
import { closeSlashMenu, commandUsage, oneLine, slashKeydown, updateSlashMenu } from "./slash.js";
|
|
11
|
+
import { attachMention } from "./mention.js";
|
|
11
12
|
import { doAction, openModelPicker, openSessions } from "./conv-actions.js";
|
|
12
13
|
|
|
13
14
|
// ---------- 输入框 ----------
|
|
@@ -16,16 +17,21 @@ export function renderComposer() {
|
|
|
16
17
|
cancelVoice();
|
|
17
18
|
ui.input = h("textarea", {
|
|
18
19
|
rows: 1,
|
|
19
|
-
placeholder: "
|
|
20
|
+
placeholder: "随心输入,@ 选技能",
|
|
20
21
|
oninput: () => {
|
|
21
22
|
// 识别过程中用户自己动了输入框,以用户为准,停掉识别
|
|
22
23
|
cancelVoice();
|
|
23
24
|
autosize();
|
|
24
25
|
void updateSlashMenu();
|
|
26
|
+
void ui.mention.update();
|
|
25
27
|
},
|
|
26
|
-
onblur: () =>
|
|
28
|
+
onblur: () =>
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
closeSlashMenu();
|
|
31
|
+
ui.mention.close();
|
|
32
|
+
}, 150),
|
|
27
33
|
onkeydown: (e) => {
|
|
28
|
-
if (slashKeydown(e)) {
|
|
34
|
+
if (slashKeydown(e) || ui.mention.keydown(e)) {
|
|
29
35
|
e.preventDefault();
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
@@ -62,6 +68,10 @@ export function renderComposer() {
|
|
|
62
68
|
ui.chips = h("div", { class: "chips" });
|
|
63
69
|
ui.schedBar = h("div", { class: "sched-bar", hidden: true });
|
|
64
70
|
ui.slashMenu = h("div", { class: "slash-menu", hidden: true });
|
|
71
|
+
ui.mentionMenu = h("div", { class: "slash-menu", hidden: true });
|
|
72
|
+
ui.mention = attachMention(ui.input, ui.mentionMenu, {
|
|
73
|
+
onEmpty: () => import("./skills.js").then((m) => m.openSkills()),
|
|
74
|
+
});
|
|
65
75
|
ui.composer = h(
|
|
66
76
|
"div",
|
|
67
77
|
{
|
|
@@ -78,6 +88,7 @@ export function renderComposer() {
|
|
|
78
88
|
},
|
|
79
89
|
},
|
|
80
90
|
ui.slashMenu,
|
|
91
|
+
ui.mentionMenu,
|
|
81
92
|
ui.chips,
|
|
82
93
|
ui.input,
|
|
83
94
|
h(
|
|
@@ -253,6 +264,7 @@ export async function sendMessage() {
|
|
|
253
264
|
return;
|
|
254
265
|
}
|
|
255
266
|
closeSlashMenu();
|
|
267
|
+
ui.mention.close();
|
|
256
268
|
let key;
|
|
257
269
|
let res;
|
|
258
270
|
setSending(true);
|
package/web/views/layout.js
CHANGED
|
@@ -47,6 +47,14 @@ function openSettings() {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** 技能页:第一次打开时才加载 */
|
|
51
|
+
export function openSkillsView() {
|
|
52
|
+
import("./skills.js").then(
|
|
53
|
+
(m) => m.openSkills(),
|
|
54
|
+
(err) => toast(`技能页加载失败,刷新页面再试:${err.message}`),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
/** 定时任务页:第一次打开时才加载 */
|
|
51
59
|
export function openTasksView() {
|
|
52
60
|
import("./tasks.js").then(
|
|
@@ -81,6 +89,7 @@ export async function boot() {
|
|
|
81
89
|
// 地址里带了会话就打开它;否则打开「最近」里最近活跃的那个对话,一个都没有才进新对话页
|
|
82
90
|
const latest = [...S.convs.values()].filter((c) => !c.hidden).sort((a, b) => b.activeAt - a.activeAt)[0];
|
|
83
91
|
if (fromHash === "tasks") openTasksView();
|
|
92
|
+
else if (fromHash === "skills") openSkillsView();
|
|
84
93
|
else if (fromHash && S.convs.has(fromHash)) await openConversation(fromHash);
|
|
85
94
|
else if (latest) await openConversation(latest.key);
|
|
86
95
|
else newDraft();
|
|
@@ -170,6 +179,7 @@ function renderLayout() {
|
|
|
170
179
|
),
|
|
171
180
|
// 「最近」上面的入口,像 ChatGPT 的 Tasks。当前在任务页时高亮(renderSidebar 里切换)
|
|
172
181
|
(ui.tasksNav = h("button", { class: "btn new-chat side-nav", onclick: () => openTasksView() }, icon("clock"), "定时任务")),
|
|
182
|
+
(ui.skillsNav = h("button", { class: "btn new-chat side-nav", onclick: () => openSkillsView() }, icon("spark"), "技能")),
|
|
173
183
|
ui.convList,
|
|
174
184
|
h(
|
|
175
185
|
"div",
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { fill, h } from "../lib/dom.js";
|
|
2
|
+
import { api } from "../lib/api.js";
|
|
3
|
+
|
|
4
|
+
// ---------- @ 技能补全 ----------
|
|
5
|
+
//
|
|
6
|
+
// 光标前面是「@xxx」(@ 在开头或者前面是空白、标点)时弹出能用的技能,选中后补成「@name 」。
|
|
7
|
+
// 输入框和定时任务的「要做的事」共用:发出去之后由服务端认消息里的 @name(skill-runtime.ts),
|
|
8
|
+
// 前端只负责补全,不用另外传选了哪些。
|
|
9
|
+
// 列表缓存半分钟;技能有变化时服务端推 { t: "skills" },缓存作废。
|
|
10
|
+
|
|
11
|
+
let cache = null;
|
|
12
|
+
document.addEventListener("aim:skills", () => (cache = null));
|
|
13
|
+
|
|
14
|
+
async function loadSkills() {
|
|
15
|
+
if (cache && Date.now() - cache.at < 30_000) return cache.list;
|
|
16
|
+
const data = await api("/api/skills/mention");
|
|
17
|
+
cache = { at: Date.now(), list: data.skills };
|
|
18
|
+
return data.skills;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 光标前的 @ 片段:返回 { start, query },不在 @ 里就是 null */
|
|
22
|
+
function mentionAt(input) {
|
|
23
|
+
const pos = input.selectionStart ?? input.value.length;
|
|
24
|
+
if (pos !== (input.selectionEnd ?? pos)) return null;
|
|
25
|
+
const m = /(^|[\s((,。,.!!??::;;"'“”‘’、])@([a-z0-9_-]*)$/i.exec(input.value.slice(0, pos));
|
|
26
|
+
if (!m) return null;
|
|
27
|
+
return { start: pos - m[2].length - 1, query: m[2].toLowerCase() };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function match(list, q) {
|
|
31
|
+
if (!q) return list;
|
|
32
|
+
const tiers = [[], [], []];
|
|
33
|
+
for (const s of list) {
|
|
34
|
+
if (s.name.startsWith(q)) tiers[0].push(s);
|
|
35
|
+
else if (s.name.includes(q)) tiers[1].push(s);
|
|
36
|
+
else if (s.description.toLowerCase().includes(q)) tiers[2].push(s);
|
|
37
|
+
}
|
|
38
|
+
return tiers.flat();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 给一个输入框挂上 @ 补全。menu 是放菜单的元素(调用方决定放哪、怎么定位,样式用 .slash-menu)。
|
|
43
|
+
* onEmpty:一个技能都没有时,菜单里给个入口(去技能页)。返回 { update, keydown, close }:
|
|
44
|
+
* 输入框的 input / keydown / blur 里分别调用。
|
|
45
|
+
*/
|
|
46
|
+
export function attachMention(input, menu, { onEmpty, onPicked } = {}) {
|
|
47
|
+
const st = { open: false, items: [], index: 0, token: 0, at: null };
|
|
48
|
+
|
|
49
|
+
const close = () => {
|
|
50
|
+
st.open = false;
|
|
51
|
+
st.items = [];
|
|
52
|
+
st.index = 0;
|
|
53
|
+
st.token++;
|
|
54
|
+
menu.hidden = true;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const highlight = () => {
|
|
58
|
+
menu.querySelectorAll(".slash-item").forEach((el, i) => el.classList.toggle("on", i === st.index));
|
|
59
|
+
menu.querySelector(".slash-item.on")?.scrollIntoView({ block: "nearest" });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const pick = (s) => {
|
|
63
|
+
const at = mentionAt(input);
|
|
64
|
+
if (!at) return close();
|
|
65
|
+
const before = input.value.slice(0, at.start);
|
|
66
|
+
const after = input.value.slice(input.selectionStart);
|
|
67
|
+
const insert = `@${s.name}${after.startsWith(" ") ? "" : " "}`;
|
|
68
|
+
input.value = `${before}${insert}${after}`;
|
|
69
|
+
const caret = before.length + insert.length;
|
|
70
|
+
input.setSelectionRange(caret, caret);
|
|
71
|
+
close();
|
|
72
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
73
|
+
input.focus();
|
|
74
|
+
onPicked?.(s);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const render = (items, note) => {
|
|
78
|
+
st.open = true;
|
|
79
|
+
menu.hidden = false;
|
|
80
|
+
if (!items) {
|
|
81
|
+
fill(menu, h("div", { class: "slash-empty" }, note ?? h("span", { class: "spinner" })));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
fill(
|
|
85
|
+
menu,
|
|
86
|
+
items.map((s, i) =>
|
|
87
|
+
h(
|
|
88
|
+
"button",
|
|
89
|
+
{
|
|
90
|
+
class: `slash-item${i === st.index ? " on" : ""}`,
|
|
91
|
+
type: "button",
|
|
92
|
+
onmousedown: (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
pick(s);
|
|
95
|
+
},
|
|
96
|
+
onmousemove: () => {
|
|
97
|
+
if (st.index !== i) {
|
|
98
|
+
st.index = i;
|
|
99
|
+
highlight();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
h(
|
|
104
|
+
"div",
|
|
105
|
+
{ class: "slash-head" },
|
|
106
|
+
h("span", { class: "slash-name" }, `@${s.name}`),
|
|
107
|
+
s.kind === "machine" ? h("span", { class: "badge" }, "本机") : null,
|
|
108
|
+
),
|
|
109
|
+
h("div", { class: "slash-desc" }, s.description || "(无说明)"),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
highlight();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const update = async () => {
|
|
117
|
+
const at = mentionAt(input);
|
|
118
|
+
if (!at) return close();
|
|
119
|
+
const token = ++st.token;
|
|
120
|
+
if (!st.open) render(null);
|
|
121
|
+
let list;
|
|
122
|
+
try {
|
|
123
|
+
list = await loadSkills();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (token === st.token) render(null, `读取技能失败:${err.message}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const now = mentionAt(input);
|
|
129
|
+
if (token !== st.token || !now) return;
|
|
130
|
+
if (list.length === 0) {
|
|
131
|
+
if (now.query || !onEmpty) return close();
|
|
132
|
+
st.items = [];
|
|
133
|
+
st.open = true;
|
|
134
|
+
menu.hidden = false;
|
|
135
|
+
fill(
|
|
136
|
+
menu,
|
|
137
|
+
h(
|
|
138
|
+
"button",
|
|
139
|
+
{
|
|
140
|
+
class: "slash-item on",
|
|
141
|
+
type: "button",
|
|
142
|
+
onmousedown: (e) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
close();
|
|
145
|
+
onEmpty();
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
h("div", { class: "slash-head" }, h("span", { class: "slash-name" }, "还没有技能")),
|
|
149
|
+
h("div", { class: "slash-desc" }, "去「技能」页新建、上传,或者从技能广场安装"),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const items = match(list, now.query);
|
|
155
|
+
if (items.length === 0) return close();
|
|
156
|
+
st.items = items;
|
|
157
|
+
st.index = Math.min(st.index, items.length - 1);
|
|
158
|
+
render(items);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** 菜单开着时接管方向键 / Tab / 回车 / Esc;返回 true 表示已处理 */
|
|
162
|
+
const keydown = (e) => {
|
|
163
|
+
if (!st.open) return false;
|
|
164
|
+
if (e.isComposing) return false;
|
|
165
|
+
if (e.key === "Escape") {
|
|
166
|
+
close();
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (st.items.length === 0) return false;
|
|
170
|
+
switch (e.key) {
|
|
171
|
+
case "ArrowDown":
|
|
172
|
+
st.index = (st.index + 1) % st.items.length;
|
|
173
|
+
highlight();
|
|
174
|
+
return true;
|
|
175
|
+
case "ArrowUp":
|
|
176
|
+
st.index = (st.index - 1 + st.items.length) % st.items.length;
|
|
177
|
+
highlight();
|
|
178
|
+
return true;
|
|
179
|
+
case "Tab":
|
|
180
|
+
pick(st.items[st.index]);
|
|
181
|
+
return true;
|
|
182
|
+
case "Enter":
|
|
183
|
+
if (e.shiftKey) return false;
|
|
184
|
+
pick(st.items[st.index]);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// 光标移动(点击、方向键离开 @ 片段)也要跟着开关
|
|
191
|
+
input.addEventListener("click", () => void update());
|
|
192
|
+
return { update, keydown, close, get open() { return st.open; } };
|
|
193
|
+
}
|
package/web/views/messages.js
CHANGED
|
@@ -13,9 +13,9 @@ import { changeCwd, forkConversation } from "./conv-actions.js";
|
|
|
13
13
|
|
|
14
14
|
// ---------- 打开会话 ----------
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** 从定时任务页、技能页回到对话:把输入框放回来 */
|
|
17
17
|
function leaveTasksView() {
|
|
18
|
-
if (S.view
|
|
18
|
+
if (!S.view) return;
|
|
19
19
|
S.view = null;
|
|
20
20
|
ui.main.classList.remove("tasks-view");
|
|
21
21
|
}
|