@keyflow2/keyflow-kit-wx-reply 0.2.9
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/API.md +182 -0
- package/LICENSE +202 -0
- package/UPPER_LAYER_APPS.md +158 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-48.png +0 -0
- package/icons/icon-96.png +0 -0
- package/manifest.json +60 -0
- package/package.json +25 -0
- package/ui/app/index.html +383 -0
- package/ui/app/main.js +1080 -0
- package/ui/app/styles.css +825 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>WX 回复</title>
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="../../../shared/ui/ime-panel.css" />
|
|
9
|
+
<link rel="stylesheet" href="../../../shared/ui/kit-shadcn.css" />
|
|
10
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app" class="wx-shell" v-cloak>
|
|
14
|
+
<div class="wx-toast" v-if="toast.text" :data-kind="toast.kind">{{ toast.text }}</div>
|
|
15
|
+
|
|
16
|
+
<section class="screen screen--picker" v-show="screen === 'picker'">
|
|
17
|
+
<header class="screen__header">
|
|
18
|
+
<h1 class="screen__title">选择回复对象</h1>
|
|
19
|
+
<div class="header-actions">
|
|
20
|
+
<button class="icon-button" type="button" @click="openSettings('picker')" aria-label="服务配置">
|
|
21
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
22
|
+
<rect x="3.5" y="4" width="17" height="6" rx="1.8" />
|
|
23
|
+
<rect x="3.5" y="14" width="17" height="6" rx="1.8" />
|
|
24
|
+
<path d="M8 7h.01M8 17h.01" />
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
<button class="icon-button" type="button" @click="attemptClose()" aria-label="关闭">
|
|
28
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
|
|
29
|
+
<path d="M6 6 18 18M18 6 6 18" />
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<div class="search-box">
|
|
36
|
+
<svg class="search-box__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
37
|
+
<circle cx="11" cy="11" r="7" />
|
|
38
|
+
<path d="m20 20-3.5-3.5" />
|
|
39
|
+
</svg>
|
|
40
|
+
<input
|
|
41
|
+
class="search-box__input"
|
|
42
|
+
type="text"
|
|
43
|
+
placeholder="搜索联系人或群聊..."
|
|
44
|
+
v-model="picker.query"
|
|
45
|
+
@input="onSearchInput($event.target.value)"
|
|
46
|
+
@change="onSearchInput($event.target.value)"
|
|
47
|
+
@compositionend="onSearchInput($event.target.value)"
|
|
48
|
+
@keydown.enter.prevent="triggerSearch($event.target.value)"
|
|
49
|
+
enterkeyhint="search"
|
|
50
|
+
spellcheck="false"
|
|
51
|
+
/>
|
|
52
|
+
<button
|
|
53
|
+
class="search-box__clear"
|
|
54
|
+
type="button"
|
|
55
|
+
v-if="picker.query.trim()"
|
|
56
|
+
@click="clearSearch()"
|
|
57
|
+
aria-label="清空搜索"
|
|
58
|
+
>
|
|
59
|
+
清空
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
class="search-box__action"
|
|
63
|
+
type="button"
|
|
64
|
+
@click="triggerSearch()"
|
|
65
|
+
:data-disabled="canTriggerSearch() ? 'false' : 'true'"
|
|
66
|
+
>
|
|
67
|
+
搜索
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="service-inline" v-if="service.ok === false">
|
|
72
|
+
<div>
|
|
73
|
+
<strong>服务未连接</strong>
|
|
74
|
+
<span>{{ service.lastError || '无法加载联系人与最近会话。' }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<button class="service-inline__action" type="button" @click="openSettings('picker')">去配置</button>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<main class="screen__body">
|
|
80
|
+
<section class="section" v-if="showRecentUsedSection">
|
|
81
|
+
<div class="section__title">
|
|
82
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
83
|
+
<path d="M12 8v5l3 2" />
|
|
84
|
+
<circle cx="12" cy="12" r="8" />
|
|
85
|
+
</svg>
|
|
86
|
+
<span>最近使用</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="contact-strip">
|
|
89
|
+
<button class="avatar-tile" type="button" v-for="contact in picker.recentUsed" :key="'recent-' + contact.username" @click="selectContact(contact)">
|
|
90
|
+
<span class="avatar-frame" :class="{ 'avatar-frame--group': contact.isGroup }">
|
|
91
|
+
<img
|
|
92
|
+
v-if="resolveAvatarSrc(contact) && !isAvatarBroken(contact.username)"
|
|
93
|
+
:src="resolveAvatarSrc(contact)"
|
|
94
|
+
alt=""
|
|
95
|
+
loading="lazy"
|
|
96
|
+
@error="markAvatarBroken(contact.username)"
|
|
97
|
+
/>
|
|
98
|
+
<span v-else>{{ avatarFallback(contact) }}</span>
|
|
99
|
+
</span>
|
|
100
|
+
<span class="avatar-tile__name">{{ contact.displayName }}</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
<section class="section" v-if="picker.query.trim()">
|
|
106
|
+
<div class="section__title">
|
|
107
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
108
|
+
<circle cx="11" cy="11" r="7" />
|
|
109
|
+
<path d="m20 20-3.5-3.5" />
|
|
110
|
+
</svg>
|
|
111
|
+
<span>{{ busy.searching ? '搜索中' : '搜索结果' }}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="session-grid" v-if="picker.searchResults.length">
|
|
114
|
+
<button class="session-tile" type="button" v-for="contact in picker.searchResults" :key="'search-' + contact.username" @click="selectContact(contact)">
|
|
115
|
+
<span class="avatar-frame" :class="{ 'avatar-frame--group': contact.isGroup }">
|
|
116
|
+
<img
|
|
117
|
+
v-if="resolveAvatarSrc(contact) && !isAvatarBroken(contact.username)"
|
|
118
|
+
:src="resolveAvatarSrc(contact)"
|
|
119
|
+
alt=""
|
|
120
|
+
loading="lazy"
|
|
121
|
+
@error="markAvatarBroken(contact.username)"
|
|
122
|
+
/>
|
|
123
|
+
<span v-else>{{ avatarFallback(contact) }}</span>
|
|
124
|
+
</span>
|
|
125
|
+
<span class="session-tile__name">{{ contact.displayName }}</span>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="empty-state empty-state--compact" v-else-if="!busy.searching">
|
|
129
|
+
<p>没有找到匹配联系人。</p>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<section class="section">
|
|
134
|
+
<div class="section__title">
|
|
135
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
136
|
+
<path d="M4 6h16M4 12h16M4 18h10" />
|
|
137
|
+
</svg>
|
|
138
|
+
<span>微信最近会话</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="session-grid" v-if="picker.sessions.length">
|
|
141
|
+
<button class="session-tile" type="button" v-for="contact in picker.sessions" :key="'session-' + contact.username" @click="selectContact(contact)">
|
|
142
|
+
<span class="avatar-frame" :class="{ 'avatar-frame--group': contact.isGroup }">
|
|
143
|
+
<img
|
|
144
|
+
v-if="resolveAvatarSrc(contact) && !isAvatarBroken(contact.username)"
|
|
145
|
+
:src="resolveAvatarSrc(contact)"
|
|
146
|
+
alt=""
|
|
147
|
+
loading="lazy"
|
|
148
|
+
@error="markAvatarBroken(contact.username)"
|
|
149
|
+
/>
|
|
150
|
+
<span v-else>{{ avatarFallback(contact) }}</span>
|
|
151
|
+
<span class="unread-badge" v-if="contact.unread > 0">{{ contact.unreadDisplay }}</span>
|
|
152
|
+
</span>
|
|
153
|
+
<span class="session-tile__name">{{ contact.displayName }}</span>
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="empty-state empty-state--compact" v-else>
|
|
157
|
+
<p>{{ service.ok === false ? '服务不可用,无法拉取最近会话。' : '暂无最近会话。' }}</p>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
</main>
|
|
161
|
+
</section>
|
|
162
|
+
|
|
163
|
+
<section class="screen screen--settings" v-show="screen === 'settings'">
|
|
164
|
+
<header class="subpage-header">
|
|
165
|
+
<button class="back-button" type="button" @click="goBackFromSettings()" aria-label="返回">
|
|
166
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
167
|
+
<path d="m15 18-6-6 6-6" />
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
<h2 class="subpage-title">服务配置</h2>
|
|
171
|
+
</header>
|
|
172
|
+
|
|
173
|
+
<main class="screen__body">
|
|
174
|
+
<article class="config-card">
|
|
175
|
+
<label class="config-label">
|
|
176
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
177
|
+
<rect x="3.5" y="4" width="17" height="6" rx="1.8" />
|
|
178
|
+
<rect x="3.5" y="14" width="17" height="6" rx="1.8" />
|
|
179
|
+
<path d="M8 7h.01M8 17h.01" />
|
|
180
|
+
</svg>
|
|
181
|
+
<span>服务端基址 (Base URL)</span>
|
|
182
|
+
</label>
|
|
183
|
+
|
|
184
|
+
<input class="config-input" type="url" placeholder="http://<HOST:PORT>" v-model="settings.baseUrl" />
|
|
185
|
+
|
|
186
|
+
<p class="config-hint">功能件只走真实后端;探活成功后才会加载联系人、会话和画像数据。</p>
|
|
187
|
+
<p class="config-status" :data-kind="service.ok === false ? 'error' : 'ok'">
|
|
188
|
+
{{ serviceStatusText }}
|
|
189
|
+
</p>
|
|
190
|
+
|
|
191
|
+
<button class="save-button" type="button" @click="saveServiceSettings()" :disabled="busy.saving || busy.probing">
|
|
192
|
+
{{ busy.saving || busy.probing ? '保存中...' : '保存并生效' }}
|
|
193
|
+
</button>
|
|
194
|
+
</article>
|
|
195
|
+
</main>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<section class="screen screen--reply" v-show="screen === 'reply'">
|
|
199
|
+
<header class="subpage-header subpage-header--reply">
|
|
200
|
+
<button class="back-button" type="button" @click="goToPicker()" aria-label="返回">
|
|
201
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
202
|
+
<path d="m15 18-6-6 6-6" />
|
|
203
|
+
</svg>
|
|
204
|
+
</button>
|
|
205
|
+
|
|
206
|
+
<div class="reply-header">
|
|
207
|
+
<h2 class="reply-header__title">发给: {{ compose.contact.displayName }}</h2>
|
|
208
|
+
<div class="reply-header__subtitle">
|
|
209
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
210
|
+
<path d="M4 7.5A2.5 2.5 0 0 1 6.5 5h11A2.5 2.5 0 0 1 20 7.5v6A2.5 2.5 0 0 1 17.5 16H8l-4 3V7.5Z" />
|
|
211
|
+
</svg>
|
|
212
|
+
<span>{{ compose.message || '正在读取最近消息...' }}</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</header>
|
|
216
|
+
|
|
217
|
+
<main class="screen__body screen__body--reply">
|
|
218
|
+
<div class="reply-list" v-if="compose.replies.length">
|
|
219
|
+
<button
|
|
220
|
+
class="reply-card"
|
|
221
|
+
type="button"
|
|
222
|
+
v-for="reply in compose.replies"
|
|
223
|
+
:key="reply.id"
|
|
224
|
+
:class="{ 'reply-card--selected': compose.lastInsertedReplyId === reply.id }"
|
|
225
|
+
@click="applyReply(reply)"
|
|
226
|
+
>
|
|
227
|
+
<span class="reply-card__lead" v-if="replyLeadText">{{ replyLeadText }}</span>
|
|
228
|
+
<span class="reply-card__text">{{ reply.text }}</span>
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="loading-list" v-else-if="busy.generating || busy.loadingConversation">
|
|
233
|
+
<div class="loading-card" v-for="index in 2" :key="'loading-' + index"></div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div class="empty-state" v-else>
|
|
237
|
+
<p>{{ compose.message ? '暂时没生成出候选,点“换一批”再试一次。' : '当前联系人没有读到可回复的最近一句消息。' }}</p>
|
|
238
|
+
</div>
|
|
239
|
+
</main>
|
|
240
|
+
|
|
241
|
+
<div class="reply-dock">
|
|
242
|
+
<div class="advanced-sheet" v-if="compose.advancedOpen">
|
|
243
|
+
<div class="advanced-section">
|
|
244
|
+
<div class="advanced-section__label">
|
|
245
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
246
|
+
<path d="M4 7h16M7 12h10M10 17h4" />
|
|
247
|
+
</svg>
|
|
248
|
+
<span>回复基调</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="segmented-grid">
|
|
251
|
+
<button
|
|
252
|
+
class="segmented-button"
|
|
253
|
+
type="button"
|
|
254
|
+
v-for="tone in toneOptions"
|
|
255
|
+
:key="tone.id"
|
|
256
|
+
:aria-pressed="settings.tonePreset === tone.id"
|
|
257
|
+
:class="{ 'segmented-button--active': settings.tonePreset === tone.id }"
|
|
258
|
+
@click="selectTonePreset(tone.id)"
|
|
259
|
+
>
|
|
260
|
+
{{ tone.label }}
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="advanced-section">
|
|
266
|
+
<div class="advanced-section__label">
|
|
267
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
268
|
+
<path d="M5 7h14M5 12h10M5 17h8" />
|
|
269
|
+
</svg>
|
|
270
|
+
<span>本次回复方向</span>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="intent-editor">
|
|
273
|
+
<input
|
|
274
|
+
class="intent-editor__input"
|
|
275
|
+
type="text"
|
|
276
|
+
placeholder="例如:先安抚对方,再把时间约到明天下午"
|
|
277
|
+
:value="compose.replyIntent"
|
|
278
|
+
@input="onReplyIntentInput($event.target.value)"
|
|
279
|
+
@keydown.enter.prevent="applyReplyIntent()"
|
|
280
|
+
/>
|
|
281
|
+
<button
|
|
282
|
+
class="intent-editor__button"
|
|
283
|
+
type="button"
|
|
284
|
+
@click="applyReplyIntent()"
|
|
285
|
+
:disabled="busy.generating || busy.loadingConversation || !compose.message"
|
|
286
|
+
>
|
|
287
|
+
应用
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
<p class="advanced-section__hint">不想让 AI 泛泛而谈时,在这里直接说明这次回复想达到什么目的。</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div class="advanced-section">
|
|
294
|
+
<div class="advanced-section__label">
|
|
295
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
296
|
+
<path d="M12 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6ZM6.5 20a3.5 3.5 0 0 1 3.5-3.5h4A3.5 3.5 0 0 1 17.5 20" />
|
|
297
|
+
<path d="M5 9a2 2 0 1 1 0 4M19 9a2 2 0 1 0 0 4" />
|
|
298
|
+
</svg>
|
|
299
|
+
<span>个性化</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="segmented-grid segmented-grid--wide">
|
|
302
|
+
<button
|
|
303
|
+
class="segmented-button"
|
|
304
|
+
type="button"
|
|
305
|
+
:aria-pressed="settings.personaMode === 'merge-self'"
|
|
306
|
+
:class="{ 'segmented-button--active': settings.personaMode === 'merge-self' }"
|
|
307
|
+
@click="selectPersonaMode('merge-self')"
|
|
308
|
+
>
|
|
309
|
+
融合我的画像
|
|
310
|
+
</button>
|
|
311
|
+
<button
|
|
312
|
+
class="segmented-button"
|
|
313
|
+
type="button"
|
|
314
|
+
:aria-pressed="settings.personaMode === 'none'"
|
|
315
|
+
:class="{ 'segmented-button--active': settings.personaMode === 'none' }"
|
|
316
|
+
@click="selectPersonaMode('none')"
|
|
317
|
+
>
|
|
318
|
+
不使用画像
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="advanced-section">
|
|
324
|
+
<div class="advanced-section__label">
|
|
325
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
326
|
+
<path d="M8 7h8M6 12h12M4 17h16" />
|
|
327
|
+
</svg>
|
|
328
|
+
<span>聊天上下文</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="count-stepper">
|
|
331
|
+
<button
|
|
332
|
+
class="count-stepper__button"
|
|
333
|
+
type="button"
|
|
334
|
+
@click="decreaseContextMessageCount()"
|
|
335
|
+
:disabled="busy.loadingConversation || busy.generating || settings.contextMessageCount <= 5"
|
|
336
|
+
aria-label="减少聊天上下文条数"
|
|
337
|
+
>
|
|
338
|
+
−
|
|
339
|
+
</button>
|
|
340
|
+
<div class="count-stepper__value">
|
|
341
|
+
<strong>{{ settings.contextMessageCount }}</strong>
|
|
342
|
+
<span>条聊天消息</span>
|
|
343
|
+
</div>
|
|
344
|
+
<button
|
|
345
|
+
class="count-stepper__button"
|
|
346
|
+
type="button"
|
|
347
|
+
@click="increaseContextMessageCount()"
|
|
348
|
+
:disabled="busy.loadingConversation || busy.generating || settings.contextMessageCount >= 50"
|
|
349
|
+
aria-label="增加聊天上下文条数"
|
|
350
|
+
>
|
|
351
|
+
+
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
<p class="advanced-section__hint">{{ contextMessageHint }}</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="dock-actions">
|
|
359
|
+
<button class="dock-button dock-button--secondary" type="button" @click="toggleAdvanced()">
|
|
360
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
361
|
+
<path d="M4 7h16M7 12h10M10 17h4" />
|
|
362
|
+
</svg>
|
|
363
|
+
<span>{{ compose.advancedOpen ? '收起配置' : '高级配置' }}</span>
|
|
364
|
+
</button>
|
|
365
|
+
|
|
366
|
+
<button class="dock-button dock-button--primary" type="button" @click="changeBatch()" :disabled="busy.generating || !compose.message">
|
|
367
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
368
|
+
<path d="M20 11a8 8 0 0 0-14.9-3M4 13a8 8 0 0 0 14.9 3" />
|
|
369
|
+
<path d="M4 4v4h4M20 20v-4h-4" />
|
|
370
|
+
</svg>
|
|
371
|
+
<span>{{ busy.generating ? '生成中' : '换一批' }}</span>
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<script src="../../../../function-kit-runtime-sdk/dist/function-kit-runtime.js"></script>
|
|
379
|
+
<script src="../../../shared/vendor/petite-vue/petite-vue.iife.js"></script>
|
|
380
|
+
<script src="./main.js"></script>
|
|
381
|
+
</body>
|
|
382
|
+
</html>
|
|
383
|
+
|