skyloom 1.13.5 → 1.13.7
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/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/guard.d.ts +45 -0
- package/dist/core/agent/guard.d.ts.map +1 -0
- package/dist/core/agent/guard.js +113 -0
- package/dist/core/agent/guard.js.map +1 -0
- package/dist/core/agent.d.ts +17 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +182 -93
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +1 -1
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -0
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +177 -95
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -0
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model self-service tools — let an agent inspect and switch its own LLM.
|
|
3
|
+
*
|
|
4
|
+
* Registered per-agent (same pattern as delegate_to), so the closure knows
|
|
5
|
+
* which agent is asking. The runtime config object is shared by reference
|
|
6
|
+
* with LLMClient, so a switch takes effect on the very next LLM call and is
|
|
7
|
+
* persisted to ~/.skyloom/config.yaml.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ToolDefinition } from '../core/tool';
|
|
11
|
+
import { listProviders, modelsFor, providerLabel } from '../core/catalog';
|
|
12
|
+
import { setAgentModel, clearAgentModel, describeAgentLLM } from '../core/model_config';
|
|
13
|
+
|
|
14
|
+
export function createModelTools(agentName: string, runtimeConfig: any): ToolDefinition[] {
|
|
15
|
+
const listModels: ToolDefinition = {
|
|
16
|
+
name: 'list_models',
|
|
17
|
+
description:
|
|
18
|
+
'List every model available in the catalog (grouped by provider) plus your current model. ' +
|
|
19
|
+
'Call this before set_my_model to pick a valid id.',
|
|
20
|
+
parameters: [],
|
|
21
|
+
handler: async () => {
|
|
22
|
+
const me = describeAgentLLM(runtimeConfig, agentName);
|
|
23
|
+
const lines: string[] = [
|
|
24
|
+
`Current: ${me.model} (${me.source === 'agent' ? 'per-agent override' : 'unified default'})`,
|
|
25
|
+
'',
|
|
26
|
+
];
|
|
27
|
+
for (const p of listProviders()) {
|
|
28
|
+
const models = modelsFor(p);
|
|
29
|
+
if (!models.length) continue;
|
|
30
|
+
lines.push(`${providerLabel(p)}: ${models.map(m => m.id).join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const setMyModel: ToolDefinition = {
|
|
37
|
+
name: 'set_my_model',
|
|
38
|
+
description:
|
|
39
|
+
'Switch the LLM model YOU run on, effective from your next reply and persisted to config. ' +
|
|
40
|
+
'Use when the user asks you to change/upgrade/downgrade your model. ' +
|
|
41
|
+
"Pass model='default' to drop your override and follow the unified default again. " +
|
|
42
|
+
'Call list_models first if unsure of valid ids.',
|
|
43
|
+
parameters: [
|
|
44
|
+
{
|
|
45
|
+
name: 'model',
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: "Catalog model id (e.g. 'deepseek-chat'), or 'default' to clear the override",
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
handler: async (kwargs: Record<string, any>) => {
|
|
52
|
+
const modelId = String(kwargs.model || '').trim();
|
|
53
|
+
if (!modelId) return '✗ model is required';
|
|
54
|
+
|
|
55
|
+
const before = describeAgentLLM(runtimeConfig, agentName);
|
|
56
|
+
if (modelId === 'default' || modelId === 'unified') {
|
|
57
|
+
clearAgentModel(runtimeConfig, agentName);
|
|
58
|
+
const after = describeAgentLLM(runtimeConfig, agentName);
|
|
59
|
+
return `✓ ${agentName} 已回到统一配置: ${before.model} → ${after.model} (default)`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const r = setAgentModel(runtimeConfig, agentName, modelId);
|
|
63
|
+
if (!r.ok) {
|
|
64
|
+
return `✗ '${modelId}' 不在模型目录中。${r.suggestions.length ? '可选: ' + r.suggestions.join(', ') : '先调 list_models 查看可用模型。'}`;
|
|
65
|
+
}
|
|
66
|
+
const keyNote = describeAgentLLM(runtimeConfig, agentName).keySource === 'missing'
|
|
67
|
+
? `\n⚠ 该 provider (${r.provider}) 尚无可用 API key — 提醒用户运行 /apikey set ${r.provider} <key>`
|
|
68
|
+
: '';
|
|
69
|
+
return `✓ ${agentName} 的模型已切换: ${before.model} → ${modelId}${r.provider ? ` (${r.provider})` : ''},下一次回复即生效${keyNote}`;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return [listModels, setMyModel];
|
|
74
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todo_write — agents externalize multi-step task state (Claude Code 式).
|
|
3
|
+
*
|
|
4
|
+
* The agent maintains a checklist in working memory: plan it up front, mark
|
|
5
|
+
* items active/done as it works. The list survives compaction (working
|
|
6
|
+
* memory, not chat history), the CLI renders it live, and the tool's return
|
|
7
|
+
* value keeps the current state visible to the model itself.
|
|
8
|
+
*
|
|
9
|
+
* Whole-list replace semantics: every call passes the complete list. That
|
|
10
|
+
* keeps the tool idempotent and trivially recoverable after a bad call.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ToolDefinition } from '../core/tool';
|
|
14
|
+
|
|
15
|
+
export type TodoStatus = 'pending' | 'active' | 'done';
|
|
16
|
+
export interface TodoItem {
|
|
17
|
+
text: string;
|
|
18
|
+
status: TodoStatus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MAX_ITEMS = 20;
|
|
22
|
+
const STATUSES = new Set<string>(['pending', 'active', 'done']);
|
|
23
|
+
|
|
24
|
+
export const TODO_WORKING_KEY = 'todos';
|
|
25
|
+
|
|
26
|
+
/** Parse + validate the items argument (JSON array). */
|
|
27
|
+
export function parseTodoItems(raw: any): { items: TodoItem[] | null; error: string } {
|
|
28
|
+
let parsed: any = raw;
|
|
29
|
+
if (typeof raw === 'string') {
|
|
30
|
+
try { parsed = JSON.parse(raw); } catch { return { items: null, error: 'items 必须是合法 JSON 数组' }; }
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(parsed)) return { items: null, error: 'items 必须是数组' };
|
|
33
|
+
if (parsed.length > MAX_ITEMS) return { items: null, error: `最多 ${MAX_ITEMS} 项 — 合并粒度` };
|
|
34
|
+
const items: TodoItem[] = [];
|
|
35
|
+
for (const it of parsed) {
|
|
36
|
+
const text = typeof it === 'string' ? it : String(it?.text ?? '').trim();
|
|
37
|
+
const status = typeof it === 'object' && it !== null && STATUSES.has(String(it.status)) ? String(it.status) : 'pending';
|
|
38
|
+
if (!text) return { items: null, error: '存在空的任务项' };
|
|
39
|
+
items.push({ text: text.slice(0, 120), status: status as TodoStatus });
|
|
40
|
+
}
|
|
41
|
+
return { items, error: '' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderTodoList(items: TodoItem[]): string {
|
|
45
|
+
const done = items.filter(i => i.status === 'done').length;
|
|
46
|
+
const lines = items.map(i => {
|
|
47
|
+
const mark = i.status === 'done' ? '✓' : i.status === 'active' ? '◐' : '·';
|
|
48
|
+
return `${mark} ${i.text}`;
|
|
49
|
+
});
|
|
50
|
+
return `任务清单 ${done}/${items.length}\n${lines.join('\n')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createTodoTool(agent: { memory: { setWorking(k: string, v: any): void } }): ToolDefinition {
|
|
54
|
+
return {
|
|
55
|
+
name: 'todo_write',
|
|
56
|
+
description:
|
|
57
|
+
'Maintain your task checklist for multi-step work. Call it FIRST to plan (all pending), ' +
|
|
58
|
+
'then again whenever an item starts (active) or finishes (done) — pass the COMPLETE list each time. ' +
|
|
59
|
+
'items is a JSON array: [{"text":"...","status":"pending|active|done"}, ...]. ' +
|
|
60
|
+
'Use for any task with 3+ steps; skip for trivial one-shot answers.',
|
|
61
|
+
parameters: [
|
|
62
|
+
{
|
|
63
|
+
name: 'items',
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'The complete checklist as a JSON array of {text, status} (status: pending/active/done)',
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
handler: async (kwargs: Record<string, any>) => {
|
|
70
|
+
const { items, error } = parseTodoItems(kwargs.items);
|
|
71
|
+
if (!items) return `✗ ${error}`;
|
|
72
|
+
agent.memory.setWorking(TODO_WORKING_KEY, items);
|
|
73
|
+
return `✓ ${renderTodoList(items)}`;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/web/tts.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
|
|
3
|
-
* Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
|
7
|
-
|
|
8
|
-
export interface VoiceOption {
|
|
9
|
-
key: string;
|
|
10
|
-
name: string;
|
|
11
|
-
desc: string;
|
|
12
|
-
voiceType: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const VOICE_CATALOG: VoiceOption[] = [
|
|
16
|
-
{ key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
|
|
17
|
-
{ key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
|
|
18
|
-
{ key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
|
|
19
|
-
{ key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
|
|
20
|
-
{ key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
|
|
21
|
-
{ key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export interface TTSOptions {
|
|
25
|
-
text: string;
|
|
26
|
-
voice?: string;
|
|
27
|
-
speed?: number;
|
|
28
|
-
pitch?: number;
|
|
29
|
-
apiKey?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface TTSResult {
|
|
33
|
-
success: boolean;
|
|
34
|
-
audioBase64?: string;
|
|
35
|
-
format?: string;
|
|
36
|
-
error?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Convert text to speech using Volcano Engine TTS API.
|
|
41
|
-
*/
|
|
42
|
-
export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
|
|
43
|
-
const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
|
|
44
|
-
|
|
45
|
-
if (!text || !text.trim()) {
|
|
46
|
-
return { success: false, error: 'Text is required' };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
|
|
50
|
-
if (!apiKey) {
|
|
51
|
-
return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const payload = {
|
|
55
|
-
app: { appid: process.env.VOLC_APP_ID || '' },
|
|
56
|
-
user: { uid: 'skyloom' },
|
|
57
|
-
request: {
|
|
58
|
-
reqid: Math.random().toString(36).slice(2, 14),
|
|
59
|
-
text,
|
|
60
|
-
text_type: 'plain',
|
|
61
|
-
operation: 'query',
|
|
62
|
-
frontend_type: 'unitTson',
|
|
63
|
-
voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
|
|
64
|
-
audio: { audio_type: 'mp3' },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const response = await fetch(HTTP_ENDPOINT, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify(payload),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const data: any = await response.json();
|
|
79
|
-
if (data.code !== 3000) {
|
|
80
|
-
return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
|
|
81
|
-
}
|
|
82
|
-
return { success: true, audioBase64: data.data, format: 'mp3' };
|
|
83
|
-
} catch (e: any) {
|
|
84
|
-
return { success: false, error: `TTS request failed: ${e.message || e}` };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* List available TTS voices.
|
|
90
|
-
*/
|
|
91
|
-
export function listVoices(): VoiceOption[] {
|
|
92
|
-
return [...VOICE_CATALOG];
|
|
93
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
|
|
3
|
+
* Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
|
7
|
+
|
|
8
|
+
export interface VoiceOption {
|
|
9
|
+
key: string;
|
|
10
|
+
name: string;
|
|
11
|
+
desc: string;
|
|
12
|
+
voiceType: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const VOICE_CATALOG: VoiceOption[] = [
|
|
16
|
+
{ key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
|
|
17
|
+
{ key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
|
|
18
|
+
{ key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
|
|
19
|
+
{ key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
|
|
20
|
+
{ key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
|
|
21
|
+
{ key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface TTSOptions {
|
|
25
|
+
text: string;
|
|
26
|
+
voice?: string;
|
|
27
|
+
speed?: number;
|
|
28
|
+
pitch?: number;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TTSResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
audioBase64?: string;
|
|
35
|
+
format?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert text to speech using Volcano Engine TTS API.
|
|
41
|
+
*/
|
|
42
|
+
export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
|
|
43
|
+
const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
|
|
44
|
+
|
|
45
|
+
if (!text || !text.trim()) {
|
|
46
|
+
return { success: false, error: 'Text is required' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const payload = {
|
|
55
|
+
app: { appid: process.env.VOLC_APP_ID || '' },
|
|
56
|
+
user: { uid: 'skyloom' },
|
|
57
|
+
request: {
|
|
58
|
+
reqid: Math.random().toString(36).slice(2, 14),
|
|
59
|
+
text,
|
|
60
|
+
text_type: 'plain',
|
|
61
|
+
operation: 'query',
|
|
62
|
+
frontend_type: 'unitTson',
|
|
63
|
+
voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
|
|
64
|
+
audio: { audio_type: 'mp3' },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(HTTP_ENDPOINT, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data: any = await response.json();
|
|
79
|
+
if (data.code !== 3000) {
|
|
80
|
+
return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
|
|
81
|
+
}
|
|
82
|
+
return { success: true, audioBase64: data.data, format: 'mp3' };
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
return { success: false, error: `TTS request failed: ${e.message || e}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List available TTS voices.
|
|
90
|
+
*/
|
|
91
|
+
export function listVoices(): VoiceOption[] {
|
|
92
|
+
return [...VOICE_CATALOG];
|
|
93
|
+
}
|