tavern-harness 0.1.18 → 0.1.19
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 +25 -0
- package/package.json +1 -1
- package/src/App.tsx +5 -10
- package/src/components/CharactersView.tsx +3 -0
- package/src/components/GameplayDialogs.tsx +3 -0
- package/src/core/achievements.ts +4 -0
- package/src/core/i18n.ts +24 -0
- package/src/core/tools/toolExecutor.ts +4 -0
- package/src/generated/changelog.json +39 -1
- package/src/theme/global.css +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
> 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
|
|
4
4
|
> 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
|
|
5
5
|
|
|
6
|
+
## [0.1.19] - 2026-10-25
|
|
7
|
+
|
|
8
|
+
### 中文
|
|
9
|
+
|
|
10
|
+
#### 新增
|
|
11
|
+
|
|
12
|
+
- 新增四项「首次行动」成就:首次导出游戏(独乐乐不如众乐乐)、首次导入游戏(发现者)、首次创建角色卡或世界书(创造者)、首次创建自定义技能(探索者)
|
|
13
|
+
|
|
14
|
+
#### 改进
|
|
15
|
+
|
|
16
|
+
- 聊天标题栏更简洁:副标题只保留上下文用量,不再重复显示会话模式
|
|
17
|
+
- 手机端标题栏按钮区支持横向滑动,按钮较多时不再被挤出屏幕或误触页面
|
|
18
|
+
|
|
19
|
+
### English
|
|
20
|
+
|
|
21
|
+
#### Added
|
|
22
|
+
|
|
23
|
+
- Four new first-action achievements: export a game (Sharing Is Caring), import a game (Discoverer), create a character card or Lorebook (Creator), and create a custom skill (Explorer)
|
|
24
|
+
|
|
25
|
+
#### Changed
|
|
26
|
+
|
|
27
|
+
- The chat header is cleaner: the subtitle now shows only context usage instead of repeating the session mode
|
|
28
|
+
- On mobile, the header action row scrolls horizontally, so buttons are no longer pushed off screen or trigger page gestures
|
|
29
|
+
|
|
6
30
|
## [0.1.18] - 2026-10-25
|
|
7
31
|
|
|
8
32
|
### 中文
|
|
@@ -528,6 +552,7 @@
|
|
|
528
552
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
529
553
|
|
|
530
554
|
<!-- 链接定义 -->
|
|
555
|
+
[0.1.19]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.18...v0.1.19
|
|
531
556
|
[0.1.18]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.17...v0.1.18
|
|
532
557
|
[0.1.17]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.16...v0.1.17
|
|
533
558
|
[0.1.16]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.15...v0.1.16
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -293,7 +293,6 @@ function SessionHeader({
|
|
|
293
293
|
const [exportOpen, setExportOpen] = useState(false);
|
|
294
294
|
const [showWorkspace, setShowWorkspace] = useState(false);
|
|
295
295
|
|
|
296
|
-
const modeLabel = session.mode === 'STANDARD' ? t('header.modeStandard') : session.mode === 'NPC' ? t('header.modeNpc') : t('header.modeGroup');
|
|
297
296
|
const messages = useStore((s) => s.messages[session.id!] ?? []);
|
|
298
297
|
const contextTokens = useMemo(() => {
|
|
299
298
|
const assistantMessages = messages.filter((message) => message.role === 'assistant' && message.totalTokens > 0);
|
|
@@ -321,15 +320,11 @@ function SessionHeader({
|
|
|
321
320
|
<SessionVisual mode={session.mode} npcName={npcRef?.name ?? undefined} hue={npcRef?.avatarColorOrdinal ?? 0} imageUrl={npcRef?.avatarDataUrl} members={session.mode === 'GROUP' ? groupMemberAvatars : undefined} size="lg" />
|
|
322
321
|
<div className="tinfo">
|
|
323
322
|
<div className="ttitle">{session.title}</div>
|
|
324
|
-
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<span className="tsub-mode">{t('nav.contextUsage', { tokens: formatContextTokens(contextTokens) })}</span>
|
|
330
|
-
</span>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
323
|
+
{contextTokens != null && (
|
|
324
|
+
<div className="tsub">
|
|
325
|
+
<span className="tsub-mode">{t('nav.contextUsage', { tokens: formatContextTokens(contextTokens) })}</span>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
333
328
|
</div>
|
|
334
329
|
<div className="chat-actions">
|
|
335
330
|
<button className="btn-ghost icon-tooltip" title={t(usesPublicWorkspace ? 'header.workspaceRootHint' : 'header.workspaceHint')} onClick={() => setShowWorkspace(true)}>
|
|
@@ -29,6 +29,7 @@ import { textPreview } from '../core/textPreview';
|
|
|
29
29
|
import { AVATAR_STORAGE_SIZE, shrinkAvatarDataUrl } from '../core/avatarThumbnail';
|
|
30
30
|
import { ALL_BUILTIN_TOOL_NAMES } from '../core/toolDefinitions';
|
|
31
31
|
import { useT } from '../core/i18n';
|
|
32
|
+
import { unlockAchievement } from '../core/achievements';
|
|
32
33
|
|
|
33
34
|
const CharacterBookEditor = lazy(() => import('./CharacterBookEditor'));
|
|
34
35
|
|
|
@@ -297,6 +298,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
297
298
|
...npc,
|
|
298
299
|
...npcData,
|
|
299
300
|
});
|
|
301
|
+
await unlockAchievement('creator');
|
|
300
302
|
addToast(t('toast.charCreated', { name }));
|
|
301
303
|
} else {
|
|
302
304
|
await db.npcs.update(npc.id!, npcData);
|
|
@@ -517,6 +519,7 @@ function WorldBookList({ books, onChanged }: {
|
|
|
517
519
|
addToast(t('toast.wbUpdated'));
|
|
518
520
|
} else {
|
|
519
521
|
await db.worldBooks.add({ ...data, imageUri: null, createdAt: Date.now() });
|
|
522
|
+
await unlockAchievement('creator');
|
|
520
523
|
addToast(t('toast.wbCreated'));
|
|
521
524
|
}
|
|
522
525
|
setEditing(null);
|
|
@@ -3,6 +3,7 @@ import { useStore } from '../store/store';
|
|
|
3
3
|
import { Icon, Modal } from './shared';
|
|
4
4
|
import { useT } from '../core/i18n';
|
|
5
5
|
import { exportGameplay, importGameplay } from '../core/gameplay';
|
|
6
|
+
import { unlockAchievement } from '../core/achievements';
|
|
6
7
|
|
|
7
8
|
// ============================================================
|
|
8
9
|
// 游戏导出 / 导入
|
|
@@ -35,6 +36,7 @@ export function GameplayExportModal({
|
|
|
35
36
|
if (result === 'canceled') {
|
|
36
37
|
addToast(t('toast.exportCanceled'));
|
|
37
38
|
} else {
|
|
39
|
+
await unlockAchievement('game-exporter');
|
|
38
40
|
addToast(t('toast.gameplayExported'));
|
|
39
41
|
}
|
|
40
42
|
onClose();
|
|
@@ -125,6 +127,7 @@ export function GameplayImportModal({
|
|
|
125
127
|
setBusy(true);
|
|
126
128
|
try {
|
|
127
129
|
const result = await importGameplay(pendingData.current);
|
|
130
|
+
await unlockAchievement('game-importer');
|
|
128
131
|
pendingData.current = null;
|
|
129
132
|
setSelectedName(null);
|
|
130
133
|
addToast(
|
package/src/core/achievements.ts
CHANGED
|
@@ -22,6 +22,10 @@ export const ACHIEVEMENTS: AchievementDef[] = [
|
|
|
22
22
|
{ id: 'night-owl', name: '午夜不归人', icon: '🌙', threshold: 1_000_000, description: '累计使用 1,000,000 Token' },
|
|
23
23
|
{ id: 'legend', name: '酒馆传奇', icon: '🏆', threshold: 100_000_000, description: '累计使用 100,000,000 Token' },
|
|
24
24
|
{ id: 'th-shareholder', name: 'TH股东', icon: '📈', iconUrl: '/tavern-harness-logo.png', description: '连续点击首页图标 5 次' },
|
|
25
|
+
{ id: 'game-exporter', name: '独乐乐不如众乐乐', icon: '📤', description: '首次导出游戏' },
|
|
26
|
+
{ id: 'game-importer', name: '发现者', icon: '📥', description: '首次导入游戏' },
|
|
27
|
+
{ id: 'creator', name: '创造者', icon: '🎨', description: '首次创建角色卡或世界书' },
|
|
28
|
+
{ id: 'explorer', name: '探索者', icon: '🧩', description: '首次创建自定义技能' },
|
|
25
29
|
];
|
|
26
30
|
|
|
27
31
|
/** 生涯总 token(输入 + 输出) */
|
package/src/core/i18n.ts
CHANGED
|
@@ -384,6 +384,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
384
384
|
'ach.legend.desc': '累计使用 100,000,000 Token',
|
|
385
385
|
'ach.thShareholder.name': 'TH股东',
|
|
386
386
|
'ach.thShareholder.desc': '5 秒内连续点击首页图标 5 次',
|
|
387
|
+
'ach.gameExporter.name': '独乐乐不如众乐乐',
|
|
388
|
+
'ach.gameExporter.desc': '首次导出游戏',
|
|
389
|
+
'ach.gameImporter.name': '发现者',
|
|
390
|
+
'ach.gameImporter.desc': '首次导入游戏',
|
|
391
|
+
'ach.creator.name': '创造者',
|
|
392
|
+
'ach.creator.desc': '首次创建角色卡或世界书',
|
|
393
|
+
'ach.explorer.name': '探索者',
|
|
394
|
+
'ach.explorer.desc': '首次创建自定义技能',
|
|
387
395
|
|
|
388
396
|
// ---- 成就解锁弹窗 ----
|
|
389
397
|
'achModal.title': '🎉 成就解锁!',
|
|
@@ -891,6 +899,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
891
899
|
'ach.legend.desc': '累計使用 100,000,000 Token',
|
|
892
900
|
'ach.thShareholder.name': 'TH股東',
|
|
893
901
|
'ach.thShareholder.desc': '5 秒內連續點擊首頁圖示 5 次',
|
|
902
|
+
'ach.gameExporter.name': '獨樂樂不如眾樂樂',
|
|
903
|
+
'ach.gameExporter.desc': '首次匯出遊戲',
|
|
904
|
+
'ach.gameImporter.name': '發現者',
|
|
905
|
+
'ach.gameImporter.desc': '首次匯入遊戲',
|
|
906
|
+
'ach.creator.name': '創造者',
|
|
907
|
+
'ach.creator.desc': '首次建立角色卡或世界書',
|
|
908
|
+
'ach.explorer.name': '探索者',
|
|
909
|
+
'ach.explorer.desc': '首次建立自訂技能',
|
|
894
910
|
|
|
895
911
|
'achModal.title': '🎉 成就解鎖!',
|
|
896
912
|
'achModal.total': '生涯已累計 {n} Tokens',
|
|
@@ -1392,6 +1408,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
1392
1408
|
'ach.legend.desc': 'Cumulatively used 100,000,000 tokens',
|
|
1393
1409
|
'ach.thShareholder.name': 'TH Shareholder',
|
|
1394
1410
|
'ach.thShareholder.desc': 'Clicked the home icon 5 times within 5 seconds',
|
|
1411
|
+
'ach.gameExporter.name': 'Sharing Is Caring',
|
|
1412
|
+
'ach.gameExporter.desc': 'Exported a game for the first time',
|
|
1413
|
+
'ach.gameImporter.name': 'Discoverer',
|
|
1414
|
+
'ach.gameImporter.desc': 'Imported a game for the first time',
|
|
1415
|
+
'ach.creator.name': 'Creator',
|
|
1416
|
+
'ach.creator.desc': 'Created a character card or Lorebook for the first time',
|
|
1417
|
+
'ach.explorer.name': 'Explorer',
|
|
1418
|
+
'ach.explorer.desc': 'Created a custom skill for the first time',
|
|
1395
1419
|
|
|
1396
1420
|
'achModal.title': '🎉 Achievement unlocked!',
|
|
1397
1421
|
'achModal.total': 'Lifetime total {n} tokens',
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from '../../types/models';
|
|
10
10
|
import { BUILTIN_TOOL_NAMES } from '../toolDefinitions';
|
|
11
11
|
import { translate } from '../i18n';
|
|
12
|
+
import { unlockAchievement } from '../achievements';
|
|
12
13
|
import { rollDice } from './builtinTools';
|
|
13
14
|
import {
|
|
14
15
|
createSessionWorkspaceDir,
|
|
@@ -409,6 +410,7 @@ async function handleCreateSkill(args: Record<string, unknown>): Promise<string>
|
|
|
409
410
|
if (exists) return `ERROR: Skill ${name} already exists`;
|
|
410
411
|
const tool = await createSkillTool(name, String(args.description ?? ''), (args.parameters as Record<string, unknown>) ?? {}, args.execution);
|
|
411
412
|
await db.tools.add(tool);
|
|
413
|
+
await unlockAchievement('explorer');
|
|
412
414
|
return translate('tool.skillCreated', { name });
|
|
413
415
|
}
|
|
414
416
|
|
|
@@ -493,6 +495,7 @@ async function handleCreateCharacter(args: Record<string, unknown>): Promise<str
|
|
|
493
495
|
isBuiltIn: false,
|
|
494
496
|
createdAt: Date.now(),
|
|
495
497
|
});
|
|
498
|
+
await unlockAchievement('creator');
|
|
496
499
|
return translate('tool.characterCreated', { name });
|
|
497
500
|
}
|
|
498
501
|
|
|
@@ -638,6 +641,7 @@ async function handleCreateLorebook(args: Record<string, unknown>): Promise<stri
|
|
|
638
641
|
if (!name || !content) return 'ERROR: name and content are required';
|
|
639
642
|
if (await db.worldBooks.where('name').equals(name).first()) return `ERROR: Lorebook ${name} already exists`;
|
|
640
643
|
await db.worldBooks.add({ name, content: content.slice(0, 10_000), imageUri: null, createdAt: Date.now() });
|
|
644
|
+
await unlockAchievement('creator');
|
|
641
645
|
return translate('tool.lorebookCreated', { name });
|
|
642
646
|
}
|
|
643
647
|
|
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "0.1.
|
|
2
|
+
"latest": "0.1.19",
|
|
3
3
|
"repo": "https://github.com/Rannichan/Tavern-Harness",
|
|
4
4
|
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.1.19",
|
|
7
|
+
"date": "2026-10-25",
|
|
8
|
+
"breaking": false,
|
|
9
|
+
"compare": "v0.1.18...v0.1.19",
|
|
10
|
+
"groups": {
|
|
11
|
+
"zh-CN": [
|
|
12
|
+
{
|
|
13
|
+
"kind": "added",
|
|
14
|
+
"items": [
|
|
15
|
+
"新增四项「首次行动」成就:首次导出游戏(独乐乐不如众乐乐)、首次导入游戏(发现者)、首次创建角色卡或世界书(创造者)、首次创建自定义技能(探索者)"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"kind": "changed",
|
|
20
|
+
"items": [
|
|
21
|
+
"聊天标题栏更简洁:副标题只保留上下文用量,不再重复显示会话模式",
|
|
22
|
+
"手机端标题栏按钮区支持横向滑动,按钮较多时不再被挤出屏幕或误触页面"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"en": [
|
|
27
|
+
{
|
|
28
|
+
"kind": "added",
|
|
29
|
+
"items": [
|
|
30
|
+
"Four new first-action achievements: export a game (Sharing Is Caring), import a game (Discoverer), create a character card or Lorebook (Creator), and create a custom skill (Explorer)"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "changed",
|
|
35
|
+
"items": [
|
|
36
|
+
"The chat header is cleaner: the subtitle now shows only context usage instead of repeating the session mode",
|
|
37
|
+
"On mobile, the header action row scrolls horizontally, so buttons are no longer pushed off screen or trigger page gestures"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
5
43
|
{
|
|
6
44
|
"version": "0.1.18",
|
|
7
45
|
"date": "2026-10-25",
|
package/src/theme/global.css
CHANGED
|
@@ -1804,11 +1804,14 @@ hr.sep {
|
|
|
1804
1804
|
.chat-actions {
|
|
1805
1805
|
display: flex;
|
|
1806
1806
|
flex: 0 1 auto;
|
|
1807
|
+
min-width: 0;
|
|
1807
1808
|
justify-content: flex-end;
|
|
1808
1809
|
gap: 0;
|
|
1809
1810
|
max-width: none;
|
|
1810
1811
|
overflow-x: auto;
|
|
1812
|
+
overscroll-behavior-x: contain;
|
|
1811
1813
|
scrollbar-width: none;
|
|
1814
|
+
touch-action: pan-x;
|
|
1812
1815
|
}
|
|
1813
1816
|
|
|
1814
1817
|
.chat-actions::-webkit-scrollbar {
|