tavern-harness 0.1.22 → 0.1.23
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/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/persistence-server.mjs +35 -0
- package/src/components/SettingsView.tsx +68 -34
- package/src/core/i18n.ts +3 -15
- package/src/db/database.ts +38 -34
- package/src/generated/changelog.json +43 -1
- package/src/theme/global.css +6 -8
- package/src/theme/views.css +5 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
> 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
|
|
4
4
|
> 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
|
|
5
5
|
|
|
6
|
+
## [0.1.23] - 2026-10-26
|
|
7
|
+
|
|
8
|
+
### 中文
|
|
9
|
+
|
|
10
|
+
#### 改进
|
|
11
|
+
|
|
12
|
+
- 生成参数滑杆不再需要先点「解锁」:直接拖动即可调整,只有明显的横向滑动才会改变数值,上下滚动设置页时不会再误改温度、上下文压缩等参数
|
|
13
|
+
- 「重置」按钮不再受锁定状态限制,只要参数不是默认值就能一键恢复
|
|
14
|
+
- 酒馆老板的内置文案本地化明显加快:只读取与它相关的消息和参与者,并一次性批量写入,历史记录较多时也不会再逐条卡顿
|
|
15
|
+
|
|
16
|
+
#### 修复
|
|
17
|
+
|
|
18
|
+
- 移动端中文衬线字体在缺少对应字重的系统上也能正常显示加粗,不再一律退化成常规字重
|
|
19
|
+
- 移动端点按按钮、滑杆、开关时不再出现系统默认的蓝色高亮块
|
|
20
|
+
|
|
21
|
+
### English
|
|
22
|
+
|
|
23
|
+
#### Changed
|
|
24
|
+
|
|
25
|
+
- Generation parameter sliders no longer need to be unlocked first: drag them directly, and only a clearly horizontal drag changes the value, so scrolling the settings page can no longer alter temperature, context compression, and other values by accident
|
|
26
|
+
- The "Reset" button is no longer tied to a lock state and is available whenever the parameters differ from their defaults
|
|
27
|
+
- Localizing the built-in Tavern Keeper text is much faster: only its own messages and participants are read, and they are written in a single batch, so large chat histories no longer stall
|
|
28
|
+
|
|
29
|
+
#### Fixed
|
|
30
|
+
|
|
31
|
+
- Chinese serif text on mobile now renders bold correctly on systems whose fallback fonts lack the requested weight, instead of silently falling back to regular
|
|
32
|
+
- Tapping buttons, sliders, and switches on mobile no longer flashes the system's default blue highlight
|
|
33
|
+
|
|
6
34
|
## [0.1.22] - 2026-10-26
|
|
7
35
|
|
|
8
36
|
### 中文
|
|
@@ -628,6 +656,7 @@
|
|
|
628
656
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
629
657
|
|
|
630
658
|
<!-- 链接定义 -->
|
|
659
|
+
[0.1.23]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.22...v0.1.23
|
|
631
660
|
[0.1.22]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.21...v0.1.22
|
|
632
661
|
[0.1.21]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.20...v0.1.21
|
|
633
662
|
[0.1.20]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.19...v0.1.20
|
package/package.json
CHANGED
package/persistence-server.mjs
CHANGED
|
@@ -141,6 +141,17 @@ function validKey(value) {
|
|
|
141
141
|
return typeof value === 'string' && value.length > 0 && value.length <= 255;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function validBulkUpdates(value) {
|
|
145
|
+
return Array.isArray(value)
|
|
146
|
+
&& value.length <= 10_000
|
|
147
|
+
&& value.every((update) => update
|
|
148
|
+
&& typeof update === 'object'
|
|
149
|
+
&& validKey(update.key)
|
|
150
|
+
&& update.changes
|
|
151
|
+
&& typeof update.changes === 'object'
|
|
152
|
+
&& !Array.isArray(update.changes));
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
function lockState(clientId) {
|
|
145
156
|
return {
|
|
146
157
|
writable: Boolean(writeLock && writeLock.clientId === clientId),
|
|
@@ -188,6 +199,20 @@ const listStartupRecords = database.prepare(`
|
|
|
188
199
|
`);
|
|
189
200
|
const deleteRecord = database.prepare('DELETE FROM app_records WHERE collection = ? AND record_key = ?');
|
|
190
201
|
const clearCollection = database.prepare('DELETE FROM app_records WHERE collection = ?');
|
|
202
|
+
function bulkUpdateRecords(collection, updates) {
|
|
203
|
+
database.exec('BEGIN IMMEDIATE');
|
|
204
|
+
try {
|
|
205
|
+
for (const { key, changes } of updates) {
|
|
206
|
+
const record = getRecord.get(collection, key);
|
|
207
|
+
if (!record) continue;
|
|
208
|
+
saveRecord(collection, key, { ...JSON.parse(record.value_json), ...changes });
|
|
209
|
+
}
|
|
210
|
+
database.exec('COMMIT');
|
|
211
|
+
} catch (error) {
|
|
212
|
+
database.exec('ROLLBACK');
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
191
216
|
const nextNumericRecordKey = database.prepare(`
|
|
192
217
|
SELECT COALESCE(MAX(CAST(record_key AS INTEGER)), 0) + 1 AS next_key
|
|
193
218
|
FROM app_records
|
|
@@ -351,6 +376,16 @@ const server = createServer(async (req, res) => {
|
|
|
351
376
|
json(res, 200, { ok: true, id: allocateNumericRecordId(collection) });
|
|
352
377
|
return;
|
|
353
378
|
}
|
|
379
|
+
if (pathname === '/records/bulk-update') {
|
|
380
|
+
if (!validBulkUpdates(body.updates)) {
|
|
381
|
+
json(res, 400, { ok: false, message: 'Invalid record updates' });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
requireWriter(clientId);
|
|
385
|
+
bulkUpdateRecords(collection, body.updates);
|
|
386
|
+
json(res, 200, { ok: true });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
354
389
|
if (pathname === '/records/put') {
|
|
355
390
|
if (!validKey(body.key)) {
|
|
356
391
|
json(res, 400, { ok: false, message: 'Invalid record key' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo,
|
|
1
|
+
import { memo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
|
|
2
2
|
import { useStore } from '../store/store';
|
|
3
3
|
import { db, DEFAULT_GENERATION_SETTINGS } from '../db/database';
|
|
4
4
|
import { type ThemeMode } from '../theme/theme';
|
|
@@ -24,24 +24,72 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
24
24
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
|
25
25
|
const [resetting, setResetting] = useState(false);
|
|
26
26
|
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const generationSliderGesture = useRef<{
|
|
28
|
+
pointerId: number;
|
|
29
|
+
startX: number;
|
|
30
|
+
startY: number;
|
|
31
|
+
startValue: number;
|
|
32
|
+
direction: 'pending' | 'horizontal' | 'vertical';
|
|
33
|
+
} | null>(null);
|
|
29
34
|
const t = useT();
|
|
30
35
|
const hasUnseenVersion = useHasUnseenVersion();
|
|
31
36
|
|
|
32
37
|
if (!settings) return null;
|
|
33
38
|
|
|
39
|
+
const startGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
|
|
40
|
+
if (event.pointerType !== 'touch') return;
|
|
41
|
+
const input = event.currentTarget;
|
|
42
|
+
generationSliderGesture.current = {
|
|
43
|
+
pointerId: event.pointerId,
|
|
44
|
+
startX: event.clientX,
|
|
45
|
+
startY: event.clientY,
|
|
46
|
+
startValue: Number(input.value),
|
|
47
|
+
direction: 'pending',
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const classifyGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
|
|
52
|
+
const gesture = generationSliderGesture.current;
|
|
53
|
+
if (!gesture || gesture.pointerId !== event.pointerId || gesture.direction !== 'pending') return gesture?.direction;
|
|
54
|
+
|
|
55
|
+
const deltaX = Math.abs(event.clientX - gesture.startX);
|
|
56
|
+
const deltaY = Math.abs(event.clientY - gesture.startY);
|
|
57
|
+
if (Math.max(deltaX, deltaY) < 10) return 'pending';
|
|
58
|
+
|
|
59
|
+
gesture.direction = deltaX > deltaY ? 'horizontal' : 'vertical';
|
|
60
|
+
return gesture.direction;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const endGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
|
|
64
|
+
if (generationSliderGesture.current?.pointerId === event.pointerId) generationSliderGesture.current = null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const updateGenerationSlider = (key: keyof typeof settings, value: number | 'off') => {
|
|
68
|
+
const gesture = generationSliderGesture.current;
|
|
69
|
+
if (!gesture) {
|
|
70
|
+
setSettings({ [key]: value });
|
|
71
|
+
} else if (gesture.direction === 'horizontal') {
|
|
72
|
+
setSettings({ [key]: value });
|
|
73
|
+
} else if (gesture.direction === 'pending') {
|
|
74
|
+
setSettings({ [key]: gesture.startValue });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
34
78
|
// 数字滑杆——数值显示固定在行内,不溢出卡片
|
|
35
79
|
const slider = (key: keyof typeof settings, min: number, max: number, step: number, fmt?: (v: number) => string) => (
|
|
36
80
|
<label className="range-row" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center' }}>
|
|
37
81
|
<input
|
|
38
82
|
type="range"
|
|
83
|
+
className="generation-slider"
|
|
39
84
|
min={min}
|
|
40
85
|
max={max}
|
|
41
86
|
step={step}
|
|
42
87
|
value={settings[key] as number}
|
|
43
|
-
|
|
44
|
-
|
|
88
|
+
onPointerDown={startGenerationSliderGesture}
|
|
89
|
+
onPointerMove={classifyGenerationSliderGesture}
|
|
90
|
+
onPointerUp={endGenerationSliderGesture}
|
|
91
|
+
onPointerCancel={endGenerationSliderGesture}
|
|
92
|
+
onChange={(e) => updateGenerationSlider(key, parseFloat(e.target.value))}
|
|
45
93
|
/>
|
|
46
94
|
<span className="range-val">{fmt ? fmt(settings[key] as number) : (settings[key] as number)}</span>
|
|
47
95
|
</label>
|
|
@@ -55,12 +103,6 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
55
103
|
await setSettings({ ...generationDefaults });
|
|
56
104
|
addToast(t('toast.genParamsReset'));
|
|
57
105
|
};
|
|
58
|
-
const toggleGenerationParamsLock = () => setGenerationParamsLocked((locked) => !locked);
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (activeView !== 'settings') setGenerationParamsLocked(true);
|
|
62
|
-
}, [activeView]);
|
|
63
|
-
|
|
64
106
|
return (
|
|
65
107
|
<div className="view-page">
|
|
66
108
|
<div className="view-col">
|
|
@@ -112,26 +154,15 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
112
154
|
<div className="set-section">
|
|
113
155
|
<div className="settings-section-head">
|
|
114
156
|
<h3>{t('settings.genParams')}</h3>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</button>
|
|
125
|
-
<button
|
|
126
|
-
className="btn btn-sm settings-gen-reset"
|
|
127
|
-
title={t('settings.genParamsResetTitle')}
|
|
128
|
-
aria-label={t('settings.genParamsResetTitle')}
|
|
129
|
-
disabled={generationParamsLocked || isGenerationDefault}
|
|
130
|
-
onClick={() => void resetGenerationParams()}
|
|
131
|
-
>
|
|
132
|
-
<Icon name="refresh" size={12} /> {t('settings.genParamsReset')}
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
157
|
+
<button
|
|
158
|
+
className="btn btn-sm settings-gen-reset"
|
|
159
|
+
title={t('settings.genParamsResetTitle')}
|
|
160
|
+
aria-label={t('settings.genParamsResetTitle')}
|
|
161
|
+
disabled={isGenerationDefault}
|
|
162
|
+
onClick={() => void resetGenerationParams()}
|
|
163
|
+
>
|
|
164
|
+
<Icon name="refresh" size={12} /> {t('settings.genParamsReset')}
|
|
165
|
+
</button>
|
|
135
166
|
</div>
|
|
136
167
|
<table className="table param-table">
|
|
137
168
|
<tbody>
|
|
@@ -193,12 +224,16 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
193
224
|
<label className="range-row" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center' }}>
|
|
194
225
|
<input
|
|
195
226
|
type="range"
|
|
227
|
+
className="generation-slider"
|
|
196
228
|
min="0"
|
|
197
229
|
max="100"
|
|
198
230
|
step="1"
|
|
199
231
|
value={settings.contextCompressionThreshold === 'off' ? 0 : settings.contextCompressionThreshold}
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
onPointerDown={startGenerationSliderGesture}
|
|
233
|
+
onPointerMove={classifyGenerationSliderGesture}
|
|
234
|
+
onPointerUp={endGenerationSliderGesture}
|
|
235
|
+
onPointerCancel={endGenerationSliderGesture}
|
|
236
|
+
onChange={(e) => updateGenerationSlider('contextCompressionThreshold', Number(e.target.value) || 'off')}
|
|
202
237
|
/>
|
|
203
238
|
<span className="range-val">
|
|
204
239
|
{settings.contextCompressionThreshold === 'off' ? t('settings.off') : settings.contextCompressionThreshold}
|
|
@@ -215,7 +250,6 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
215
250
|
<select
|
|
216
251
|
className="select"
|
|
217
252
|
value={settings.reasoningEffort}
|
|
218
|
-
disabled={generationParamsLocked}
|
|
219
253
|
onChange={(e) => setSettings({ reasoningEffort: e.target.value as ReasoningEffort })}
|
|
220
254
|
>
|
|
221
255
|
{['auto', 'off', 'low', 'medium', 'xhigh'].map((r) => <option key={r} value={r}>{r}</option>)}
|
package/src/core/i18n.ts
CHANGED
|
@@ -202,10 +202,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
202
202
|
'settings.yoloMode': 'YOLO 模式',
|
|
203
203
|
'settings.yoloModeDesc': '自动批准工具修改和 Shell 执行,无需逐次确认。仅在信任角色和技能时开启。',
|
|
204
204
|
'settings.genParams': '🎛 生成参数',
|
|
205
|
-
'settings.genParamsLocked': '已锁定',
|
|
206
|
-
'settings.genParamsUnlocked': '已解锁',
|
|
207
|
-
'settings.genParamsLock': '锁定生成参数',
|
|
208
|
-
'settings.genParamsUnlock': '解锁生成参数',
|
|
209
205
|
'settings.genParamsReset': '重置',
|
|
210
206
|
'settings.genParamsResetTitle': '将生成参数恢复为默认值',
|
|
211
207
|
'settings.tempDesc': '创造性与随机性',
|
|
@@ -253,7 +249,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
253
249
|
'builtinNpc.prompt': '神秘的酒馆老板,可以响应客人的任何需求',
|
|
254
250
|
'builtinNpc.greeting': '你来啦!快坐下~',
|
|
255
251
|
'builtinWorldbook.name': '酒馆经营手册(精华版)',
|
|
256
|
-
'settings.providerTitle': '🔌 模型服务 Provider
|
|
252
|
+
'settings.providerTitle': '🔌 模型服务 Provider',
|
|
257
253
|
'settings.noProvider': '还没有配置 Provider,点击下方按钮添加',
|
|
258
254
|
'settings.enable': '启用',
|
|
259
255
|
'settings.disable': '停用',
|
|
@@ -729,10 +725,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
729
725
|
'settings.yoloMode': 'YOLO 模式',
|
|
730
726
|
'settings.yoloModeDesc': '自動批准工具修改和 Shell 執行,無需逐次確認。僅在信任角色和技能時開啟。',
|
|
731
727
|
'settings.genParams': '🎛 產生參數',
|
|
732
|
-
'settings.genParamsLocked': '已鎖定',
|
|
733
|
-
'settings.genParamsUnlocked': '已解鎖',
|
|
734
|
-
'settings.genParamsLock': '鎖定產生參數',
|
|
735
|
-
'settings.genParamsUnlock': '解鎖產生參數',
|
|
736
728
|
'settings.genParamsReset': '重設',
|
|
737
729
|
'settings.genParamsResetTitle': '將產生參數恢復為預設值',
|
|
738
730
|
'settings.tempDesc': '創造性與隨機性',
|
|
@@ -780,7 +772,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
780
772
|
'builtinNpc.prompt': '神秘的酒館老闆,可以回應客人的任何需求',
|
|
781
773
|
'builtinNpc.greeting': '你來啦!快坐下~',
|
|
782
774
|
'builtinWorldbook.name': '酒館經營手冊(精華版)',
|
|
783
|
-
'settings.providerTitle': '🔌 模型服務 Provider
|
|
775
|
+
'settings.providerTitle': '🔌 模型服務 Provider',
|
|
784
776
|
'settings.noProvider': '還沒有設定 Provider,點擊下方按鈕新增',
|
|
785
777
|
'settings.enable': '啟用',
|
|
786
778
|
'settings.disable': '停用',
|
|
@@ -1247,10 +1239,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
1247
1239
|
'settings.yoloMode': 'YOLO mode',
|
|
1248
1240
|
'settings.yoloModeDesc': 'Automatically approve tool changes and Shell commands without asking each time. Only enable for trusted characters and skills.',
|
|
1249
1241
|
'settings.genParams': '🎛 Generation parameters',
|
|
1250
|
-
'settings.genParamsLocked': 'Locked',
|
|
1251
|
-
'settings.genParamsUnlocked': 'Unlocked',
|
|
1252
|
-
'settings.genParamsLock': 'Lock generation parameters',
|
|
1253
|
-
'settings.genParamsUnlock': 'Unlock generation parameters',
|
|
1254
1242
|
'settings.genParamsReset': 'Reset',
|
|
1255
1243
|
'settings.genParamsResetTitle': 'Restore generation parameters to their default values',
|
|
1256
1244
|
'settings.tempDesc': 'Creativity & randomness',
|
|
@@ -1298,7 +1286,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
1298
1286
|
'builtinNpc.prompt': 'A mysterious tavern keeper who can fulfill any request from guests',
|
|
1299
1287
|
'builtinNpc.greeting': 'Welcome! Have a seat~',
|
|
1300
1288
|
'builtinWorldbook.name': 'Tavern Management Handbook (Essentials)',
|
|
1301
|
-
'settings.providerTitle': '🔌 Model Providers
|
|
1289
|
+
'settings.providerTitle': '🔌 Model Providers',
|
|
1302
1290
|
'settings.noProvider': 'No providers configured yet — add one below',
|
|
1303
1291
|
'settings.enable': 'Enabled',
|
|
1304
1292
|
'settings.disable': 'Disabled',
|
package/src/db/database.ts
CHANGED
|
@@ -530,6 +530,17 @@ class RemoteTable<T extends object> {
|
|
|
530
530
|
}
|
|
531
531
|
async bulkAdd(values: T[]): Promise<void> { for (const value of values) await this.add(value); }
|
|
532
532
|
async bulkPut(values: T[]): Promise<void> { for (const value of values) await this.put(value); }
|
|
533
|
+
async bulkUpdate(updates: Array<{ key: unknown; changes: Partial<T> }>): Promise<void> {
|
|
534
|
+
if (updates.length === 0) return;
|
|
535
|
+
await requireWriteLock();
|
|
536
|
+
await persistenceRequest('/records/bulk-update', {
|
|
537
|
+
collection: this.collection,
|
|
538
|
+
updates: updates.map(({ key, changes }) => ({
|
|
539
|
+
key: Array.isArray(key) ? key.join(':') : String(key),
|
|
540
|
+
changes,
|
|
541
|
+
})),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
533
544
|
async put(value: T): Promise<number> { return this.add(value); }
|
|
534
545
|
async update(key: unknown, changes: Partial<T>): Promise<number> {
|
|
535
546
|
await requireWriteLock();
|
|
@@ -797,40 +808,33 @@ export async function localizeBuiltinNpc(): Promise<void> {
|
|
|
797
808
|
if (isPristine(boss.greeting || '')) updates.greeting = tGreeting;
|
|
798
809
|
if (Object.keys(updates).length > 0) {
|
|
799
810
|
await db.npcs.update(boss.id!, updates);
|
|
800
|
-
//
|
|
801
|
-
const sessions = await
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
changes
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
)
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (
|
|
828
|
-
m.speakerParticipantId === boss.id &&
|
|
829
|
-
(m.speakerName === '酒馆老板' || m.speakerName === '酒館老闆' || m.speakerName === 'Tavern Keeper')
|
|
830
|
-
) {
|
|
831
|
-
await db.messages.update(m.id!, { speakerName: tName });
|
|
832
|
-
}
|
|
833
|
-
}
|
|
811
|
+
// 仅请求与酒馆老板关联的记录;批量写入可避免大量历史消息产生逐条网络往返。
|
|
812
|
+
const [sessions, participants, messages] = await Promise.all([
|
|
813
|
+
db.sessions.toArray(),
|
|
814
|
+
db.participants.where('npcId').equals(boss.id!).toArray(),
|
|
815
|
+
db.messages.where('speakerParticipantId').equals(boss.id!).toArray(),
|
|
816
|
+
]);
|
|
817
|
+
await Promise.all([
|
|
818
|
+
db.sessions.bulkUpdate(sessions.flatMap((session) => {
|
|
819
|
+
const changes: Partial<ChatSession> = {};
|
|
820
|
+
if (session.title === '酒馆老板' || session.title === '酒館老闆' || session.title === 'Tavern Keeper') {
|
|
821
|
+
changes.title = tName;
|
|
822
|
+
}
|
|
823
|
+
if (session.lastMessage && isPristine(session.lastMessage)) changes.lastMessage = tGreeting;
|
|
824
|
+
return Object.keys(changes).length > 0 ? [{ key: session.id!, changes }] : [];
|
|
825
|
+
})),
|
|
826
|
+
db.participants.bulkUpdate(participants.flatMap((participant) => (
|
|
827
|
+
participant.kind === 'NPC' &&
|
|
828
|
+
(participant.displayName === '酒馆老板' || participant.displayName === '酒館老闆' || participant.displayName === 'Tavern Keeper')
|
|
829
|
+
? [{ key: [participant.sessionId, participant.participantId], changes: { displayName: tName } }]
|
|
830
|
+
: []
|
|
831
|
+
))),
|
|
832
|
+
db.messages.bulkUpdate(messages.flatMap((message) => (
|
|
833
|
+
message.speakerName === '酒馆老板' || message.speakerName === '酒館老闆' || message.speakerName === 'Tavern Keeper'
|
|
834
|
+
? [{ key: message.id!, changes: { speakerName: tName } }]
|
|
835
|
+
: []
|
|
836
|
+
))),
|
|
837
|
+
]);
|
|
834
838
|
}
|
|
835
839
|
|
|
836
840
|
if (prevLang !== lang) {
|
|
@@ -1,7 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "0.1.
|
|
2
|
+
"latest": "0.1.23",
|
|
3
3
|
"repo": "https://github.com/Rannichan/Tavern-Harness",
|
|
4
4
|
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.1.23",
|
|
7
|
+
"date": "2026-10-26",
|
|
8
|
+
"breaking": false,
|
|
9
|
+
"compare": "v0.1.22...v0.1.23",
|
|
10
|
+
"groups": {
|
|
11
|
+
"zh-CN": [
|
|
12
|
+
{
|
|
13
|
+
"kind": "changed",
|
|
14
|
+
"items": [
|
|
15
|
+
"生成参数滑杆不再需要先点「解锁」:直接拖动即可调整,只有明显的横向滑动才会改变数值,上下滚动设置页时不会再误改温度、上下文压缩等参数",
|
|
16
|
+
"「重置」按钮不再受锁定状态限制,只要参数不是默认值就能一键恢复",
|
|
17
|
+
"酒馆老板的内置文案本地化明显加快:只读取与它相关的消息和参与者,并一次性批量写入,历史记录较多时也不会再逐条卡顿"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"kind": "fixed",
|
|
22
|
+
"items": [
|
|
23
|
+
"移动端中文衬线字体在缺少对应字重的系统上也能正常显示加粗,不再一律退化成常规字重",
|
|
24
|
+
"移动端点按按钮、滑杆、开关时不再出现系统默认的蓝色高亮块"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"en": [
|
|
29
|
+
{
|
|
30
|
+
"kind": "changed",
|
|
31
|
+
"items": [
|
|
32
|
+
"Generation parameter sliders no longer need to be unlocked first: drag them directly, and only a clearly horizontal drag changes the value, so scrolling the settings page can no longer alter temperature, context compression, and other values by accident",
|
|
33
|
+
"The \"Reset\" button is no longer tied to a lock state and is available whenever the parameters differ from their defaults",
|
|
34
|
+
"Localizing the built-in Tavern Keeper text is much faster: only its own messages and participants are read, and they are written in a single batch, so large chat histories no longer stall"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"kind": "fixed",
|
|
39
|
+
"items": [
|
|
40
|
+
"Chinese serif text on mobile now renders bold correctly on systems whose fallback fonts lack the requested weight, instead of silently falling back to regular",
|
|
41
|
+
"Tapping buttons, sliders, and switches on mobile no longer flashes the system's default blue highlight"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
5
47
|
{
|
|
6
48
|
"version": "0.1.22",
|
|
7
49
|
"date": "2026-10-26",
|
package/src/theme/global.css
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
* {
|
|
25
25
|
box-sizing: border-box;
|
|
26
|
+
-webkit-tap-highlight-color: transparent;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
html,
|
|
@@ -85,14 +86,6 @@ button {
|
|
|
85
86
|
white-space: nowrap;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/* Android WebView adds a blue overlay to non-button controls such as switch labels. */
|
|
89
|
-
button,
|
|
90
|
-
a,
|
|
91
|
-
label,
|
|
92
|
-
[role='button'] {
|
|
93
|
-
-webkit-tap-highlight-color: transparent;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
input,
|
|
97
90
|
textarea,
|
|
98
91
|
select {
|
|
@@ -1807,6 +1800,11 @@ hr.sep {
|
|
|
1807
1800
|
height: 36px;
|
|
1808
1801
|
}
|
|
1809
1802
|
|
|
1803
|
+
:root {
|
|
1804
|
+
/* Android 中文衬线回退字体常缺少所需字重;移动端统一允许合成字重。 */
|
|
1805
|
+
font-synthesis: weight;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1810
1808
|
.chat-header .ttitle {
|
|
1811
1809
|
max-width: min(52vw, 260px);
|
|
1812
1810
|
font-size: 15px;
|
package/src/theme/views.css
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
弹窗 / 覆盖层 / 视图页 / 卡片列表
|
|
3
3
|
============================================================ */
|
|
4
4
|
|
|
5
|
+
.generation-slider {
|
|
6
|
+
touch-action: pan-y;
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
.overlay {
|
|
6
10
|
position: fixed;
|
|
7
11
|
inset: 0;
|
|
@@ -625,13 +629,7 @@
|
|
|
625
629
|
justify-content: space-between;
|
|
626
630
|
gap: 12px;
|
|
627
631
|
}
|
|
628
|
-
.settings-gen-
|
|
629
|
-
display: flex;
|
|
630
|
-
flex-shrink: 0;
|
|
631
|
-
gap: 6px;
|
|
632
|
-
}
|
|
633
|
-
.settings-gen-reset,
|
|
634
|
-
.settings-gen-lock {
|
|
632
|
+
.settings-gen-reset {
|
|
635
633
|
flex-shrink: 0;
|
|
636
634
|
}
|
|
637
635
|
.settings-reset-section {
|