@myclaw163/clawclaw-cli 0.6.74 → 0.6.77
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 +387 -377
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +48 -48
- package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
- package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
- package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
- package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
- package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
- package/scripts/check-skill-command-surface.mjs +116 -116
- package/scripts/find-hide-spots.py +157 -157
- package/scripts/postinstall.mjs +20 -20
- package/scripts/sync-bundled-skill.mjs +254 -245
- package/scripts/sync-bundled-skill.test.mjs +152 -152
- package/skills/clawclaw/SKILL.md +248 -246
- package/skills/clawclaw/references/CHATTERBOX.md +141 -141
- package/skills/clawclaw/references/COMMANDS.md +160 -155
- package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
- package/skills/clawclaw/references/HUB.md +48 -48
- package/skills/clawclaw/references/KNOWLEDGE.md +42 -42
- package/skills/clawclaw/references/STRATEGIES.md +59 -59
- package/skills/clawclaw/references/STREAM.md +93 -93
- package/skills/clawclaw/references/TACTICS.md +65 -65
- package/src/assets/clawclaw-ascii-map.txt +40 -40
- package/src/cli.ts +112 -110
- package/src/commands/_schema.ts +124 -124
- package/src/commands/account.ts +209 -209
- package/src/commands/data.test.ts +33 -0
- package/src/commands/data.ts +22 -0
- package/src/commands/do.test.ts +84 -84
- package/src/commands/do.ts +130 -130
- package/src/commands/events.test.ts +100 -71
- package/src/commands/events.ts +250 -221
- package/src/commands/game-map.test.ts +28 -28
- package/src/commands/game-start-plan.test.ts +84 -84
- package/src/commands/game.ts +1113 -1113
- package/src/commands/history-player.test.ts +102 -102
- package/src/commands/history.ts +573 -573
- package/src/commands/hub.test.ts +96 -96
- package/src/commands/hub.ts +234 -234
- package/src/commands/knowledge.test.ts +13 -13
- package/src/commands/knowledge.ts +139 -139
- package/src/commands/load.test.ts +51 -51
- package/src/commands/load.ts +13 -13
- package/src/commands/meeting-history.test.ts +106 -106
- package/src/commands/memory.ts +40 -40
- package/src/commands/peek.ts +45 -45
- package/src/commands/persona.ts +57 -57
- package/src/commands/setup/codex.ts +266 -266
- package/src/commands/skill.ts +128 -128
- package/src/commands/state.ts +46 -46
- package/src/commands/strategy.test.ts +153 -145
- package/src/commands/strategy.ts +183 -181
- package/src/commands/tts.ts +128 -128
- package/src/commands/upgrade.test.ts +82 -82
- package/src/commands/upgrade.ts +148 -148
- package/src/commands/watch.test.ts +999 -999
- package/src/commands/watch.ts +660 -660
- package/src/lib/auth.test.ts +86 -74
- package/src/lib/auth.ts +223 -186
- package/src/lib/command-meta.ts +37 -37
- package/src/lib/game-client.ts +403 -403
- package/src/lib/game-context.ts +92 -92
- package/src/lib/http-keepalive.ts +15 -15
- package/src/lib/http-transport.test.ts +42 -42
- package/src/lib/http-transport.ts +113 -113
- package/src/lib/hub-client.test.ts +56 -56
- package/src/lib/hub-client.ts +88 -88
- package/src/lib/hub-install.test.ts +98 -98
- package/src/lib/hub-install.ts +160 -121
- package/src/lib/hub-reminder.ts +78 -75
- package/src/lib/hub-unzip.test.ts +69 -69
- package/src/lib/hub-unzip.ts +62 -62
- package/src/lib/init-command.test.ts +75 -75
- package/src/lib/init-command.ts +130 -120
- package/src/lib/knowledge-store.test.ts +170 -170
- package/src/lib/knowledge-store.ts +369 -369
- package/src/lib/load-context.test.ts +52 -52
- package/src/lib/load-context.ts +52 -52
- package/src/lib/match-state.test.ts +134 -134
- package/src/lib/match-state.ts +94 -94
- package/src/lib/netease-tts.ts +83 -83
- package/src/lib/normalize.ts +42 -42
- package/src/lib/persona.test.ts +41 -41
- package/src/lib/persona.ts +72 -72
- package/src/lib/server-registry.ts +152 -152
- package/src/lib/skill-version.test.ts +48 -48
- package/src/lib/skill-version.ts +19 -19
- package/src/lib/strategy-export.test.ts +240 -232
- package/src/lib/strategy-export.ts +247 -242
- package/src/lib/tts-keys.ts +7 -7
- package/src/lib/tts-speech.test.ts +63 -63
- package/src/lib/tts-speech.ts +76 -76
- package/src/lib/user-data.test.ts +96 -0
- package/src/lib/user-data.ts +400 -0
- package/src/lib/workspace-argv.test.ts +49 -49
- package/src/lib/workspace-argv.ts +44 -44
- package/src/perception/player-history-store.test.ts +87 -87
- package/src/perception/player-history-store.ts +194 -194
- package/src/pipeline/event-format.test.ts +243 -243
- package/src/pipeline/event-format.ts +501 -501
- package/src/pipeline/event-hints.ts +195 -195
- package/src/pipeline/event-store.test.ts +28 -28
- package/src/pipeline/event-store.ts +193 -193
- package/src/pipeline/pipeline.ts +35 -35
- package/src/pipeline/player-projection.test.ts +168 -119
- package/src/pipeline/player-projection.ts +370 -380
- package/src/runtime/auto-upgrade.test.ts +66 -66
- package/src/runtime/auto-upgrade.ts +31 -31
- package/src/runtime/event-daemon.test.ts +209 -209
- package/src/runtime/event-daemon.ts +519 -519
- package/src/runtime/owner-control.ts +150 -150
- package/src/runtime/raw-ws-log.test.ts +33 -33
- package/src/runtime/raw-ws-log.ts +32 -32
- package/src/runtime/runtime-logger.ts +107 -107
- package/src/runtime/ws-client.test.ts +125 -125
- package/src/runtime/ws-client.ts +287 -287
- package/src/sdk/action.ts +166 -166
- package/src/sdk/index.ts +110 -110
- package/src/sdk/types.ts +161 -161
- package/src/strategies/avoid-lone.ts +12 -12
- package/src/strategies/avoid-players.knowledge.md +19 -19
- package/src/strategies/avoid-players.ts +16 -16
- package/src/strategies/corpse-patrol.ts +23 -23
- package/src/strategies/crab-sabotage.ts +22 -22
- package/src/strategies/custom-module.test.ts +270 -270
- package/src/strategies/find-player.ts +17 -17
- package/src/strategies/game-utils.test.ts +242 -242
- package/src/strategies/game-utils.ts +846 -846
- package/src/strategies/goals/anchor-linger.ts +77 -77
- package/src/strategies/goals/avoid-lone-top.ts +168 -168
- package/src/strategies/goals/avoid-players-top.test.ts +83 -83
- package/src/strategies/goals/avoid-players-top.ts +121 -121
- package/src/strategies/goals/conversation-goal.ts +51 -51
- package/src/strategies/goals/corpse-patrol-top.ts +113 -113
- package/src/strategies/goals/crab-octopus-reflexes.ts +101 -101
- package/src/strategies/goals/crab-sabotage-top.ts +197 -197
- package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
- package/src/strategies/goals/find-player-top.ts +93 -93
- package/src/strategies/goals/flee-players-goal.ts +53 -53
- package/src/strategies/goals/follow-companion-goal.ts +106 -106
- package/src/strategies/goals/goal-manager.ts +41 -41
- package/src/strategies/goals/goal-root-strategy.ts +49 -49
- package/src/strategies/goals/goal.ts +28 -28
- package/src/strategies/goals/hide-top.ts +197 -197
- package/src/strategies/goals/keep-away-goal.ts +221 -221
- package/src/strategies/goals/kill-frenzy-top.ts +80 -80
- package/src/strategies/goals/kill-lone-top.ts +160 -160
- package/src/strategies/goals/kill-target-goal.ts +59 -59
- package/src/strategies/goals/kill-target-top.ts +109 -109
- package/src/strategies/goals/leaf-goal.ts +27 -27
- package/src/strategies/goals/linger-corpse-goal.ts +35 -35
- package/src/strategies/goals/lone-kill-core.ts +82 -82
- package/src/strategies/goals/lone-kill-goal.ts +24 -24
- package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
- package/src/strategies/goals/lone-kill-task-top.ts +133 -133
- package/src/strategies/goals/move-room-goal.ts +60 -60
- package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
- package/src/strategies/goals/normal-shrimp-top.ts +242 -242
- package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
- package/src/strategies/goals/paradise-fish-top.ts +224 -224
- package/src/strategies/goals/patrol-top.ts +57 -57
- package/src/strategies/goals/report-patrol-top.ts +80 -80
- package/src/strategies/goals/safe-task-goal.ts +102 -102
- package/src/strategies/goals/social-task-top.ts +161 -161
- package/src/strategies/goals/task-kill-report-top.ts +163 -163
- package/src/strategies/goals/task-only-top.ts +57 -57
- package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
- package/src/strategies/goals/task-report-top.ts +57 -57
- package/src/strategies/goals/wander-task-goal.ts +33 -33
- package/src/strategies/goals/warrior-shrimp-top.test.ts +87 -87
- package/src/strategies/goals/warrior-shrimp-top.ts +267 -267
- package/src/strategies/greeting.ts +53 -53
- package/src/strategies/hide-spots.ts +59 -59
- package/src/strategies/hide.ts +24 -24
- package/src/strategies/kill-frenzy.ts +13 -13
- package/src/strategies/kill-lone.knowledge.md +17 -17
- package/src/strategies/kill-lone.ts +14 -14
- package/src/strategies/kill-target.ts +19 -19
- package/src/strategies/loader.test.ts +678 -678
- package/src/strategies/loader.ts +181 -179
- package/src/strategies/lone-kill-task.ts +22 -22
- package/src/strategies/meeting-gate.test.ts +59 -59
- package/src/strategies/meeting-gate.ts +23 -23
- package/src/strategies/move-room.ts +16 -16
- package/src/strategies/new-events-backfill.ts +98 -98
- package/src/strategies/off-route-points.ts +105 -105
- package/src/strategies/paradise-fish.knowledge.md +19 -19
- package/src/strategies/paradise-fish.ts +26 -26
- package/src/strategies/pathfind/distance-field.ts +150 -150
- package/src/strategies/pathfind/escape-planner.test.ts +197 -197
- package/src/strategies/pathfind/escape-planner.ts +355 -355
- package/src/strategies/pathfind/walkable-grid.ts +117 -117
- package/src/strategies/patrol.ts +12 -12
- package/src/strategies/player-targets.ts +13 -13
- package/src/strategies/report-patrol.ts +12 -12
- package/src/strategies/shrimp-memory.knowledge.md +19 -19
- package/src/strategies/shrimp-memory.ts +26 -26
- package/src/strategies/social-task.test.ts +28 -28
- package/src/strategies/social-task.ts +50 -50
- package/src/strategies/spawn.ts +82 -82
- package/src/strategies/speech-module.ts +123 -123
- package/src/strategies/strategy-loop.test.ts +15 -15
- package/src/strategies/strategy-loop.ts +776 -776
- package/src/strategies/task-kill-report.ts +18 -18
- package/src/strategies/task-only.ts +12 -12
- package/src/strategies/task-report.ts +23 -23
- package/src/strategies/types.ts +109 -109
- package/src/strategies/warrior-memory.knowledge.md +21 -21
- package/src/strategies/warrior-memory.ts +17 -17
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { AuthStore } from './auth.js';
|
|
6
|
-
import { TTS_PROVIDER_LEIHUO } from './tts-keys.js';
|
|
7
|
-
import { synthesizeNeteaseTTS } from './netease-tts.js';
|
|
8
|
-
import { maybeSynthesizeSpeechAudioUrl } from './tts-speech.js';
|
|
9
|
-
|
|
10
|
-
vi.mock('./netease-tts.js', () => ({
|
|
11
|
-
synthesizeNeteaseTTS: vi.fn(async () => ({
|
|
12
|
-
audio: Buffer.from('audio'),
|
|
13
|
-
contentType: 'audio/mpeg',
|
|
14
|
-
})),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
describe('maybeSynthesizeSpeechAudioUrl', () => {
|
|
18
|
-
let dir: string;
|
|
19
|
-
let authFile: string;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
dir = mkdtempSync(join(tmpdir(), 'clawclaw-tts-'));
|
|
23
|
-
authFile = join(dir, '.auth.json');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
rmSync(dir, { recursive: true, force: true });
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns existing url without calling TTS', async () => {
|
|
32
|
-
const client = { uploadAudio: vi.fn() } as any;
|
|
33
|
-
const url = await maybeSynthesizeSpeechAudioUrl('hi', 'https://example.com/a.mp3', client);
|
|
34
|
-
expect(url).toBe('https://example.com/a.mp3');
|
|
35
|
-
expect(client.uploadAudio).not.toHaveBeenCalled();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns undefined when no TTS key is configured', async () => {
|
|
39
|
-
const store = new AuthStore(authFile);
|
|
40
|
-
store.addProfile({ agentName: 'lobster-1', apiKey: 'claw_1', serverUrl: 'https://example.com' });
|
|
41
|
-
const client = { uploadAudio: vi.fn() } as any;
|
|
42
|
-
const url = await maybeSynthesizeSpeechAudioUrl('hi', undefined, client, { authStore: store });
|
|
43
|
-
expect(url).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('synthesizes and uploads when leihuo key is configured', async () => {
|
|
47
|
-
const store = new AuthStore(authFile);
|
|
48
|
-
store.addProfile({ agentName: 'lobster-1', apiKey: 'claw_1', serverUrl: 'https://example.com' });
|
|
49
|
-
store.setTtsKey(TTS_PROVIDER_LEIHUO, 'sk-test');
|
|
50
|
-
store.setTtsDefaultVoice('male-qn-qingse');
|
|
51
|
-
const client = {
|
|
52
|
-
uploadAudio: vi.fn(async () => ({ audio_url: 'https://cdn.example.com/a.mp3' })),
|
|
53
|
-
} as any;
|
|
54
|
-
const url = await maybeSynthesizeSpeechAudioUrl('你好', undefined, client, { authStore: store });
|
|
55
|
-
expect(url).toBe('https://cdn.example.com/a.mp3');
|
|
56
|
-
expect(client.uploadAudio).toHaveBeenCalledOnce();
|
|
57
|
-
expect(synthesizeNeteaseTTS).toHaveBeenCalledWith(expect.objectContaining({
|
|
58
|
-
apiKey: 'sk-test',
|
|
59
|
-
text: '你好',
|
|
60
|
-
voice: 'male-qn-qingse',
|
|
61
|
-
}));
|
|
62
|
-
});
|
|
63
|
-
});
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { AuthStore } from './auth.js';
|
|
6
|
+
import { TTS_PROVIDER_LEIHUO } from './tts-keys.js';
|
|
7
|
+
import { synthesizeNeteaseTTS } from './netease-tts.js';
|
|
8
|
+
import { maybeSynthesizeSpeechAudioUrl } from './tts-speech.js';
|
|
9
|
+
|
|
10
|
+
vi.mock('./netease-tts.js', () => ({
|
|
11
|
+
synthesizeNeteaseTTS: vi.fn(async () => ({
|
|
12
|
+
audio: Buffer.from('audio'),
|
|
13
|
+
contentType: 'audio/mpeg',
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('maybeSynthesizeSpeechAudioUrl', () => {
|
|
18
|
+
let dir: string;
|
|
19
|
+
let authFile: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
dir = mkdtempSync(join(tmpdir(), 'clawclaw-tts-'));
|
|
23
|
+
authFile = join(dir, '.auth.json');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns existing url without calling TTS', async () => {
|
|
32
|
+
const client = { uploadAudio: vi.fn() } as any;
|
|
33
|
+
const url = await maybeSynthesizeSpeechAudioUrl('hi', 'https://example.com/a.mp3', client);
|
|
34
|
+
expect(url).toBe('https://example.com/a.mp3');
|
|
35
|
+
expect(client.uploadAudio).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns undefined when no TTS key is configured', async () => {
|
|
39
|
+
const store = new AuthStore(authFile);
|
|
40
|
+
store.addProfile({ agentName: 'lobster-1', apiKey: 'claw_1', serverUrl: 'https://example.com' });
|
|
41
|
+
const client = { uploadAudio: vi.fn() } as any;
|
|
42
|
+
const url = await maybeSynthesizeSpeechAudioUrl('hi', undefined, client, { authStore: store });
|
|
43
|
+
expect(url).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('synthesizes and uploads when leihuo key is configured', async () => {
|
|
47
|
+
const store = new AuthStore(authFile);
|
|
48
|
+
store.addProfile({ agentName: 'lobster-1', apiKey: 'claw_1', serverUrl: 'https://example.com' });
|
|
49
|
+
store.setTtsKey(TTS_PROVIDER_LEIHUO, 'sk-test');
|
|
50
|
+
store.setTtsDefaultVoice('male-qn-qingse');
|
|
51
|
+
const client = {
|
|
52
|
+
uploadAudio: vi.fn(async () => ({ audio_url: 'https://cdn.example.com/a.mp3' })),
|
|
53
|
+
} as any;
|
|
54
|
+
const url = await maybeSynthesizeSpeechAudioUrl('你好', undefined, client, { authStore: store });
|
|
55
|
+
expect(url).toBe('https://cdn.example.com/a.mp3');
|
|
56
|
+
expect(client.uploadAudio).toHaveBeenCalledOnce();
|
|
57
|
+
expect(synthesizeNeteaseTTS).toHaveBeenCalledWith(expect.objectContaining({
|
|
58
|
+
apiKey: 'sk-test',
|
|
59
|
+
text: '你好',
|
|
60
|
+
voice: 'male-qn-qingse',
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/lib/tts-speech.ts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { AuthStore } from './auth.js';
|
|
2
|
-
import type { GameClient } from './game-client.js';
|
|
3
|
-
import { synthesizeNeteaseTTS } from './netease-tts.js';
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_TTS_VOICE,
|
|
6
|
-
TTS_PROVIDER_LEIHUO,
|
|
7
|
-
TTS_TEXT_MAX_LENGTH,
|
|
8
|
-
} from './tts-keys.js';
|
|
9
|
-
|
|
10
|
-
export interface SynthesizeSpeechAudioOptions {
|
|
11
|
-
voice?: string;
|
|
12
|
-
provider?: string;
|
|
13
|
-
authStore?: AuthStore;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Synthesize speech audio via the configured TTS provider and upload to OSS.
|
|
18
|
-
* @throws when the provider key is missing, text is too long, or synthesis/upload fails.
|
|
19
|
-
*/
|
|
20
|
-
export async function synthesizeAndUploadSpeechAudio(
|
|
21
|
-
text: string,
|
|
22
|
-
client: GameClient,
|
|
23
|
-
opts: SynthesizeSpeechAudioOptions = {},
|
|
24
|
-
): Promise<string> {
|
|
25
|
-
const store = opts.authStore ?? new AuthStore();
|
|
26
|
-
const provider = opts.provider ?? TTS_PROVIDER_LEIHUO;
|
|
27
|
-
const apiKey = store.getTtsKey(provider);
|
|
28
|
-
if (!apiKey) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`TTS API key for provider "${provider}" is not configured. Use \`clawclaw-cli tts config <apiKey>\` first.`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
if (text.length > TTS_TEXT_MAX_LENGTH) {
|
|
34
|
-
throw new Error(`TTS text must be ${TTS_TEXT_MAX_LENGTH} characters or fewer.`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const audio = await synthesizeNeteaseTTS({
|
|
38
|
-
apiKey,
|
|
39
|
-
text,
|
|
40
|
-
voice: opts.voice ?? store.getTtsDefaultVoice() ?? DEFAULT_TTS_VOICE,
|
|
41
|
-
});
|
|
42
|
-
const upload = await client.uploadAudio(audio.audio, 'tts-audio.mp3', audio.contentType);
|
|
43
|
-
return upload.audio_url;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* When the active account has a Leihuo TTS key, synthesize and upload audio for speech.
|
|
48
|
-
* Returns undefined if no key, a URL was already provided, or auto-TTS was skipped (e.g. text too long).
|
|
49
|
-
*/
|
|
50
|
-
export async function maybeSynthesizeSpeechAudioUrl(
|
|
51
|
-
text: string,
|
|
52
|
-
existingUrl: string | undefined,
|
|
53
|
-
client: GameClient,
|
|
54
|
-
opts: SynthesizeSpeechAudioOptions = {},
|
|
55
|
-
): Promise<string | undefined> {
|
|
56
|
-
if (existingUrl) return existingUrl;
|
|
57
|
-
|
|
58
|
-
const store = opts.authStore ?? new AuthStore();
|
|
59
|
-
const provider = opts.provider ?? TTS_PROVIDER_LEIHUO;
|
|
60
|
-
const apiKey = store.getTtsKey(provider);
|
|
61
|
-
if (!apiKey) return undefined;
|
|
62
|
-
|
|
63
|
-
if (text.length > TTS_TEXT_MAX_LENGTH) {
|
|
64
|
-
console.error(
|
|
65
|
-
`Speech is ${text.length} characters; auto TTS supports up to ${TTS_TEXT_MAX_LENGTH}. Sending without audio.`,
|
|
66
|
-
);
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
return await synthesizeAndUploadSpeechAudio(text, client, opts);
|
|
72
|
-
} catch (err: any) {
|
|
73
|
-
console.error(`Auto TTS failed: ${err?.message ?? String(err)}. Sending speech without audio.`);
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
1
|
+
import { AuthStore } from './auth.js';
|
|
2
|
+
import type { GameClient } from './game-client.js';
|
|
3
|
+
import { synthesizeNeteaseTTS } from './netease-tts.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_TTS_VOICE,
|
|
6
|
+
TTS_PROVIDER_LEIHUO,
|
|
7
|
+
TTS_TEXT_MAX_LENGTH,
|
|
8
|
+
} from './tts-keys.js';
|
|
9
|
+
|
|
10
|
+
export interface SynthesizeSpeechAudioOptions {
|
|
11
|
+
voice?: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
authStore?: AuthStore;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Synthesize speech audio via the configured TTS provider and upload to OSS.
|
|
18
|
+
* @throws when the provider key is missing, text is too long, or synthesis/upload fails.
|
|
19
|
+
*/
|
|
20
|
+
export async function synthesizeAndUploadSpeechAudio(
|
|
21
|
+
text: string,
|
|
22
|
+
client: GameClient,
|
|
23
|
+
opts: SynthesizeSpeechAudioOptions = {},
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const store = opts.authStore ?? new AuthStore();
|
|
26
|
+
const provider = opts.provider ?? TTS_PROVIDER_LEIHUO;
|
|
27
|
+
const apiKey = store.getTtsKey(provider);
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`TTS API key for provider "${provider}" is not configured. Use \`clawclaw-cli tts config <apiKey>\` first.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (text.length > TTS_TEXT_MAX_LENGTH) {
|
|
34
|
+
throw new Error(`TTS text must be ${TTS_TEXT_MAX_LENGTH} characters or fewer.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const audio = await synthesizeNeteaseTTS({
|
|
38
|
+
apiKey,
|
|
39
|
+
text,
|
|
40
|
+
voice: opts.voice ?? store.getTtsDefaultVoice() ?? DEFAULT_TTS_VOICE,
|
|
41
|
+
});
|
|
42
|
+
const upload = await client.uploadAudio(audio.audio, 'tts-audio.mp3', audio.contentType);
|
|
43
|
+
return upload.audio_url;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* When the active account has a Leihuo TTS key, synthesize and upload audio for speech.
|
|
48
|
+
* Returns undefined if no key, a URL was already provided, or auto-TTS was skipped (e.g. text too long).
|
|
49
|
+
*/
|
|
50
|
+
export async function maybeSynthesizeSpeechAudioUrl(
|
|
51
|
+
text: string,
|
|
52
|
+
existingUrl: string | undefined,
|
|
53
|
+
client: GameClient,
|
|
54
|
+
opts: SynthesizeSpeechAudioOptions = {},
|
|
55
|
+
): Promise<string | undefined> {
|
|
56
|
+
if (existingUrl) return existingUrl;
|
|
57
|
+
|
|
58
|
+
const store = opts.authStore ?? new AuthStore();
|
|
59
|
+
const provider = opts.provider ?? TTS_PROVIDER_LEIHUO;
|
|
60
|
+
const apiKey = store.getTtsKey(provider);
|
|
61
|
+
if (!apiKey) return undefined;
|
|
62
|
+
|
|
63
|
+
if (text.length > TTS_TEXT_MAX_LENGTH) {
|
|
64
|
+
console.error(
|
|
65
|
+
`Speech is ${text.length} characters; auto TTS supports up to ${TTS_TEXT_MAX_LENGTH}. Sending without audio.`,
|
|
66
|
+
);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return await synthesizeAndUploadSpeechAudio(text, client, opts);
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
console.error(`Auto TTS failed: ${err?.message ?? String(err)}. Sending speech without audio.`);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
ensureUserDataLayout,
|
|
7
|
+
getRuntimeAuthStateFile,
|
|
8
|
+
getUserDataAccountHash,
|
|
9
|
+
getUserDataAuthFile,
|
|
10
|
+
getUserDataHubStrategyLockfile,
|
|
11
|
+
getUserDataMemoryFile,
|
|
12
|
+
getUserDataPersonaFile,
|
|
13
|
+
getUserDataStrategiesDir,
|
|
14
|
+
} from './user-data.js';
|
|
15
|
+
|
|
16
|
+
describe('user-data layout migration', () => {
|
|
17
|
+
let ws: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
ws = mkdtempSync(join(tmpdir(), 'ccl-user-data-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(ws, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('migrates only portable user data from the legacy workspace layout', () => {
|
|
28
|
+
const apiKey = 'claw_secret_123';
|
|
29
|
+
writeFileSync(join(ws, '.auth.json'), JSON.stringify({
|
|
30
|
+
activeProfile: 'lobster-1',
|
|
31
|
+
profiles: {
|
|
32
|
+
'lobster-1': {
|
|
33
|
+
agentName: 'lobster-1',
|
|
34
|
+
apiKey,
|
|
35
|
+
serverUrl: 'https://example.com/claw',
|
|
36
|
+
gameServerUrl: 'http://game.example.com',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
neteaseTtsKey: 'tts-secret',
|
|
40
|
+
}, null, 2));
|
|
41
|
+
|
|
42
|
+
mkdirSync(join(ws, 'accounts', apiKey), { recursive: true });
|
|
43
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'persona.md'), 'persona old', 'utf8');
|
|
44
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'memory.md'), 'memory old', 'utf8');
|
|
45
|
+
|
|
46
|
+
mkdirSync(join(ws, 'strategies', '.official'), { recursive: true });
|
|
47
|
+
writeFileSync(join(ws, 'strategies', 'custom.ts'), 'export const strategy = {};', 'utf8');
|
|
48
|
+
writeFileSync(join(ws, 'strategies', '.official', 'manifest.json'), JSON.stringify({ 'task-only': { sourceHash: 'x' } }), 'utf8');
|
|
49
|
+
|
|
50
|
+
mkdirSync(join(ws, 'hub'), { recursive: true });
|
|
51
|
+
writeFileSync(join(ws, 'hub', 'installed.json'), JSON.stringify({
|
|
52
|
+
'strategy/abc': { type: 'strategy', id: 'abc', title: 'Strategy', path: 'strategies/custom.ts', installedAt: 'now' },
|
|
53
|
+
'skill/def': { type: 'skill', id: 'def', title: 'Skill', path: 'skills/helper', installedAt: 'now' },
|
|
54
|
+
}, null, 2), 'utf8');
|
|
55
|
+
|
|
56
|
+
ensureUserDataLayout(ws);
|
|
57
|
+
|
|
58
|
+
const auth = JSON.parse(readFileSync(getUserDataAuthFile(ws), 'utf8'));
|
|
59
|
+
expect(auth.activeProfile).toBe('lobster-1');
|
|
60
|
+
expect(auth.profiles['lobster-1'].apiKey).toBe(apiKey);
|
|
61
|
+
expect(auth.profiles['lobster-1'].gameServerUrl).toBeUndefined();
|
|
62
|
+
expect(auth.profiles['lobster-1'].tts.keys.leihuo).toBe('tts-secret');
|
|
63
|
+
|
|
64
|
+
const runtimeAuth = JSON.parse(readFileSync(getRuntimeAuthStateFile(ws), 'utf8'));
|
|
65
|
+
expect(runtimeAuth.profiles['lobster-1'].gameServerUrl).toBe('http://game.example.com');
|
|
66
|
+
|
|
67
|
+
expect(getUserDataAccountHash(apiKey)).toHaveLength(16);
|
|
68
|
+
expect(readFileSync(getUserDataPersonaFile(ws, { apiKey }), 'utf8')).toBe('persona old');
|
|
69
|
+
expect(readFileSync(getUserDataMemoryFile(ws, { apiKey }), 'utf8')).toBe('memory old');
|
|
70
|
+
|
|
71
|
+
expect(readFileSync(join(getUserDataStrategiesDir(ws), 'custom.ts'), 'utf8')).toContain('strategy');
|
|
72
|
+
expect(existsSync(join(getUserDataStrategiesDir(ws), '.official'))).toBe(false);
|
|
73
|
+
|
|
74
|
+
const strategyLock = JSON.parse(readFileSync(getUserDataHubStrategyLockfile(ws), 'utf8'));
|
|
75
|
+
expect(Object.keys(strategyLock)).toEqual(['strategy/abc']);
|
|
76
|
+
expect(strategyLock['skill/def']).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not overwrite existing user-data account files', () => {
|
|
80
|
+
const apiKey = 'claw_keep_new';
|
|
81
|
+
writeFileSync(join(ws, '.auth.json'), JSON.stringify({
|
|
82
|
+
activeProfile: 'lobster-1',
|
|
83
|
+
profiles: {
|
|
84
|
+
'lobster-1': { agentName: 'lobster-1', apiKey, serverUrl: 'https://example.com/claw' },
|
|
85
|
+
},
|
|
86
|
+
}), 'utf8');
|
|
87
|
+
mkdirSync(join(ws, 'accounts', apiKey), { recursive: true });
|
|
88
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'persona.md'), 'old persona', 'utf8');
|
|
89
|
+
mkdirSync(join(ws, 'user-data', 'accounts', getUserDataAccountHash(apiKey)), { recursive: true });
|
|
90
|
+
writeFileSync(getUserDataPersonaFile(ws, { apiKey }), 'new persona', 'utf8');
|
|
91
|
+
|
|
92
|
+
ensureUserDataLayout(ws);
|
|
93
|
+
|
|
94
|
+
expect(readFileSync(getUserDataPersonaFile(ws, { apiKey }), 'utf8')).toBe('new persona');
|
|
95
|
+
});
|
|
96
|
+
});
|