@mingxy/ocosay 1.0.0

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.
Files changed (126) hide show
  1. package/README.md +556 -0
  2. package/TECH_PLAN.md +352 -0
  3. package/__mocks__/@opencode-ai/plugin.ts +32 -0
  4. package/dist/config.d.ts +26 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +95 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/core/backends/afplay-backend.d.ts +33 -0
  9. package/dist/core/backends/afplay-backend.d.ts.map +1 -0
  10. package/dist/core/backends/afplay-backend.js +144 -0
  11. package/dist/core/backends/afplay-backend.js.map +1 -0
  12. package/dist/core/backends/aplay-backend.d.ts +33 -0
  13. package/dist/core/backends/aplay-backend.d.ts.map +1 -0
  14. package/dist/core/backends/aplay-backend.js +142 -0
  15. package/dist/core/backends/aplay-backend.js.map +1 -0
  16. package/dist/core/backends/base.d.ts +94 -0
  17. package/dist/core/backends/base.d.ts.map +1 -0
  18. package/dist/core/backends/base.js +6 -0
  19. package/dist/core/backends/base.js.map +1 -0
  20. package/dist/core/backends/index.d.ts +29 -0
  21. package/dist/core/backends/index.d.ts.map +1 -0
  22. package/dist/core/backends/index.js +114 -0
  23. package/dist/core/backends/index.js.map +1 -0
  24. package/dist/core/backends/naudiodon-backend.d.ts +52 -0
  25. package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
  26. package/dist/core/backends/naudiodon-backend.js +123 -0
  27. package/dist/core/backends/naudiodon-backend.js.map +1 -0
  28. package/dist/core/backends/powershell-backend.d.ts +34 -0
  29. package/dist/core/backends/powershell-backend.d.ts.map +1 -0
  30. package/dist/core/backends/powershell-backend.js +154 -0
  31. package/dist/core/backends/powershell-backend.js.map +1 -0
  32. package/dist/core/player.d.ts +97 -0
  33. package/dist/core/player.d.ts.map +1 -0
  34. package/dist/core/player.js +268 -0
  35. package/dist/core/player.js.map +1 -0
  36. package/dist/core/speaker.d.ts +97 -0
  37. package/dist/core/speaker.d.ts.map +1 -0
  38. package/dist/core/speaker.js +218 -0
  39. package/dist/core/speaker.js.map +1 -0
  40. package/dist/core/stream-player.d.ts +107 -0
  41. package/dist/core/stream-player.d.ts.map +1 -0
  42. package/dist/core/stream-player.js +272 -0
  43. package/dist/core/stream-player.js.map +1 -0
  44. package/dist/core/stream-reader.d.ts +86 -0
  45. package/dist/core/stream-reader.d.ts.map +1 -0
  46. package/dist/core/stream-reader.js +172 -0
  47. package/dist/core/stream-reader.js.map +1 -0
  48. package/dist/core/streaming-synthesizer.d.ts +51 -0
  49. package/dist/core/streaming-synthesizer.d.ts.map +1 -0
  50. package/dist/core/streaming-synthesizer.js +103 -0
  51. package/dist/core/streaming-synthesizer.js.map +1 -0
  52. package/dist/core/types.d.ts +141 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +37 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +40 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +179 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/plugin.d.ts +4 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +151 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/providers/base.d.ts +55 -0
  65. package/dist/providers/base.d.ts.map +1 -0
  66. package/dist/providers/base.js +95 -0
  67. package/dist/providers/base.js.map +1 -0
  68. package/dist/providers/minimax.d.ts +84 -0
  69. package/dist/providers/minimax.d.ts.map +1 -0
  70. package/dist/providers/minimax.js +387 -0
  71. package/dist/providers/minimax.js.map +1 -0
  72. package/dist/tools/tts.d.ts +147 -0
  73. package/dist/tools/tts.d.ts.map +1 -0
  74. package/dist/tools/tts.js +232 -0
  75. package/dist/tools/tts.js.map +1 -0
  76. package/jest.config.js +15 -0
  77. package/package.json +49 -0
  78. package/src/config.ts +121 -0
  79. package/src/core/backends/afplay-backend.ts +162 -0
  80. package/src/core/backends/aplay-backend.ts +160 -0
  81. package/src/core/backends/base.ts +117 -0
  82. package/src/core/backends/index.ts +128 -0
  83. package/src/core/backends/naudiodon-backend.ts +164 -0
  84. package/src/core/backends/powershell-backend.ts +173 -0
  85. package/src/core/player.ts +322 -0
  86. package/src/core/speaker.ts +283 -0
  87. package/src/core/stream-player.ts +326 -0
  88. package/src/core/stream-reader.ts +190 -0
  89. package/src/core/streaming-synthesizer.ts +123 -0
  90. package/src/core/types.ts +185 -0
  91. package/src/index.ts +233 -0
  92. package/src/plugin.ts +166 -0
  93. package/src/providers/base.ts +150 -0
  94. package/src/providers/minimax.ts +515 -0
  95. package/src/tools/tts.ts +277 -0
  96. package/src/types/naudiodon.d.ts +19 -0
  97. package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
  98. package/tests/backends.test.ts +831 -0
  99. package/tests/index.test.ts +201 -0
  100. package/tests/integration-test.d.ts +6 -0
  101. package/tests/integration-test.d.ts.map +1 -0
  102. package/tests/integration-test.js +84 -0
  103. package/tests/integration-test.js.map +1 -0
  104. package/tests/integration-test.ts +93 -0
  105. package/tests/p1-fixes.test.ts +160 -0
  106. package/tests/plugin.test.ts +311 -0
  107. package/tests/provider.test.d.ts +2 -0
  108. package/tests/provider.test.d.ts.map +1 -0
  109. package/tests/provider.test.js +69 -0
  110. package/tests/provider.test.js.map +1 -0
  111. package/tests/provider.test.ts +87 -0
  112. package/tests/speaker.test.d.ts +2 -0
  113. package/tests/speaker.test.d.ts.map +1 -0
  114. package/tests/speaker.test.js +63 -0
  115. package/tests/speaker.test.js.map +1 -0
  116. package/tests/speaker.test.ts +232 -0
  117. package/tests/stream-player.test.ts +303 -0
  118. package/tests/stream-reader.test.ts +269 -0
  119. package/tests/streaming-synthesizer.test.ts +225 -0
  120. package/tests/tts-tools.test.ts +270 -0
  121. package/tests/types.test.d.ts +2 -0
  122. package/tests/types.test.d.ts.map +1 -0
  123. package/tests/types.test.js +61 -0
  124. package/tests/types.test.js.map +1 -0
  125. package/tests/types.test.ts +63 -0
  126. package/tsconfig.json +22 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * OpenCode TTS 工具定义
3
+ * 用于 OpenCode Plugin 注册
4
+ */
5
+ import { speak, stop, pause, resume, listVoices, getDefaultSpeaker } from '../core/speaker';
6
+ import { TTSError, TTSErrorCode } from '../core/types';
7
+ import { isStreamEnabled, isAutoReadEnabled, getStreamStatus, getStreamReader, getStreamingSynthesizer, getStreamPlayer } from '../index';
8
+ /**
9
+ * OpenCode TTS 工具定义
10
+ * 用于 OpenCode Plugin 注册
11
+ */
12
+ export const ttsTools = [
13
+ {
14
+ name: 'tts_speak',
15
+ description: '将文本转换为语音并播放',
16
+ input: {
17
+ type: 'object',
18
+ properties: {
19
+ text: {
20
+ type: 'string',
21
+ description: '要转换的文本内容'
22
+ },
23
+ provider: {
24
+ type: 'string',
25
+ description: 'TTS 提供商名称',
26
+ default: 'minimax'
27
+ },
28
+ voice: {
29
+ type: 'string',
30
+ description: '音色 ID'
31
+ },
32
+ model: {
33
+ type: 'string',
34
+ enum: ['sync', 'async', 'stream'],
35
+ description: '合成模式',
36
+ default: 'stream'
37
+ },
38
+ speed: {
39
+ type: 'number',
40
+ description: '语速 0.5-2.0'
41
+ },
42
+ volume: {
43
+ type: 'number',
44
+ description: '音量 0-100'
45
+ },
46
+ pitch: {
47
+ type: 'number',
48
+ description: '音调 0.5-2.0'
49
+ }
50
+ },
51
+ required: ['text']
52
+ }
53
+ },
54
+ {
55
+ name: 'tts_stop',
56
+ description: '停止当前 TTS 播放'
57
+ },
58
+ {
59
+ name: 'tts_pause',
60
+ description: '暂停当前 TTS 播放'
61
+ },
62
+ {
63
+ name: 'tts_resume',
64
+ description: '恢复暂停的 TTS 播放'
65
+ },
66
+ {
67
+ name: 'tts_list_voices',
68
+ description: '列出可用的音色',
69
+ input: {
70
+ type: 'object',
71
+ properties: {
72
+ provider: {
73
+ type: 'string',
74
+ description: 'TTS 提供商名称',
75
+ default: 'minimax'
76
+ }
77
+ }
78
+ }
79
+ },
80
+ {
81
+ name: 'tts_list_providers',
82
+ description: '列出所有已注册的 TTS 提供商'
83
+ },
84
+ {
85
+ name: 'tts_status',
86
+ description: '获取当前 TTS 播放状态',
87
+ output: {
88
+ type: 'object',
89
+ properties: {
90
+ isPlaying: { type: 'boolean' },
91
+ isPaused: { type: 'boolean' }
92
+ }
93
+ }
94
+ },
95
+ {
96
+ name: 'tts_stream_speak',
97
+ description: '启动流式朗读(豆包模式),订阅AI回复并边生成边朗读',
98
+ input: {
99
+ type: 'object',
100
+ properties: {
101
+ text: {
102
+ type: 'string',
103
+ description: '初始文本(可选)'
104
+ },
105
+ voice: {
106
+ type: 'string',
107
+ description: '音色ID'
108
+ },
109
+ model: {
110
+ type: 'string',
111
+ enum: ['sync', 'async', 'stream'],
112
+ default: 'stream'
113
+ }
114
+ }
115
+ }
116
+ },
117
+ {
118
+ name: 'tts_stream_stop',
119
+ description: '停止当前流式朗读'
120
+ },
121
+ {
122
+ name: 'tts_stream_status',
123
+ description: '获取当前流式朗读状态',
124
+ output: {
125
+ type: 'object',
126
+ properties: {
127
+ isActive: { type: 'boolean' },
128
+ bytesWritten: { type: 'number' },
129
+ state: { type: 'string' }
130
+ }
131
+ }
132
+ }
133
+ ];
134
+ /**
135
+ * 工具执行处理器
136
+ */
137
+ export async function handleToolCall(toolName, args) {
138
+ try {
139
+ switch (toolName) {
140
+ case 'tts_speak':
141
+ await speak(args?.text, {
142
+ provider: args?.provider,
143
+ voice: args?.voice,
144
+ model: args?.model,
145
+ speed: args?.speed,
146
+ volume: args?.volume,
147
+ pitch: args?.pitch
148
+ });
149
+ return { success: true, message: 'Speech completed' };
150
+ case 'tts_stop':
151
+ await stop();
152
+ return { success: true, message: 'Stopped' };
153
+ case 'tts_pause':
154
+ pause();
155
+ return { success: true, message: 'Paused' };
156
+ case 'tts_resume':
157
+ resume();
158
+ return { success: true, message: 'Resumed' };
159
+ case 'tts_list_voices':
160
+ const voices = await listVoices(args?.provider);
161
+ return { success: true, voices };
162
+ case 'tts_list_providers':
163
+ const speaker = getDefaultSpeaker();
164
+ const providers = speaker.getProviders();
165
+ return { success: true, providers };
166
+ case 'tts_status':
167
+ const s = getDefaultSpeaker();
168
+ return {
169
+ success: true,
170
+ isPlaying: s.isPlaying(),
171
+ isPaused: s.isPausedState()
172
+ };
173
+ case 'tts_stream_speak':
174
+ if (!isAutoReadEnabled()) {
175
+ throw new TTSError('Stream mode is not enabled. autoRead must be enabled in configuration to use tts_stream_speak.', TTSErrorCode.UNKNOWN, 'tts_stream');
176
+ }
177
+ if (!isStreamEnabled()) {
178
+ throw new TTSError('Stream components not initialized. Please initialize with autoRead enabled.', TTSErrorCode.UNKNOWN, 'tts_stream');
179
+ }
180
+ const streamReader = getStreamReader();
181
+ const synthesizer = getStreamingSynthesizer();
182
+ if (streamReader && synthesizer) {
183
+ streamReader.start();
184
+ if (args?.text) {
185
+ synthesizer.synthesize(args.text);
186
+ }
187
+ return { success: true, message: 'Stream speak started' };
188
+ }
189
+ throw new TTSError('Stream components not available', TTSErrorCode.UNKNOWN, 'tts_stream');
190
+ case 'tts_stream_stop':
191
+ if (!isStreamEnabled()) {
192
+ throw new TTSError('Stream mode is not enabled. Please enable autoRead in configuration.', TTSErrorCode.UNKNOWN, 'tts_stream');
193
+ }
194
+ const player = getStreamPlayer();
195
+ if (player) {
196
+ player.stop();
197
+ return { success: true, message: 'Stream stopped' };
198
+ }
199
+ throw new TTSError('Stream player not available', TTSErrorCode.UNKNOWN, 'tts_stream');
200
+ case 'tts_stream_status':
201
+ if (!isStreamEnabled()) {
202
+ return {
203
+ success: true,
204
+ isActive: false,
205
+ bytesWritten: 0,
206
+ state: 'not_initialized'
207
+ };
208
+ }
209
+ return {
210
+ success: true,
211
+ ...getStreamStatus()
212
+ };
213
+ default:
214
+ throw new TTSError(`Unknown tool: ${toolName}`, TTSErrorCode.UNKNOWN, 'tools');
215
+ }
216
+ }
217
+ catch (error) {
218
+ if (error instanceof TTSError) {
219
+ return {
220
+ success: false,
221
+ error: error.message,
222
+ code: error.code,
223
+ provider: error.provider
224
+ };
225
+ }
226
+ return {
227
+ success: false,
228
+ error: String(error)
229
+ };
230
+ }
231
+ }
232
+ //# sourceMappingURL=tts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tts.js","sourceRoot":"","sources":["../../src/tools/tts.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,15 @@
1
+ export default {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/tests', '<rootDir>/src'],
5
+ testMatch: ['**/*.test.ts'],
6
+ moduleFileExtensions: ['ts', 'js', 'json'],
7
+ collectCoverageFrom: ['src/**/*.ts'],
8
+ coverageDirectory: 'coverage',
9
+ transform: {
10
+ '^.+\\.tsx?$': ['ts-jest', {
11
+ useESM: true
12
+ }]
13
+ },
14
+ extensionsToTreatAsEsm: ['.ts']
15
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@mingxy/ocosay",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode TTS 播放插件 - 支持豆包模式边接收边朗读",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./plugin": {
14
+ "import": "./dist/plugin.js",
15
+ "types": "./dist/plugin.d.ts"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "watch": "tsc --watch",
21
+ "test": "jest",
22
+ "lint": "eslint src --ext .ts"
23
+ },
24
+ "keywords": [
25
+ "tts",
26
+ "text-to-speech",
27
+ "opencode",
28
+ "plugin"
29
+ ],
30
+ "author": "",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@opencode-ai/plugin": "^1.3.15",
34
+ "axios": "^1.6.0",
35
+ "ws": "^8.14.0",
36
+ "zod": "^4.3.6"
37
+ },
38
+ "devDependencies": {
39
+ "@types/jest": "^29.5.14",
40
+ "@types/node": "^20.0.0",
41
+ "@types/ws": "^8.5.0",
42
+ "jest": "^29.7.0",
43
+ "ts-jest": "^29.1.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }
package/src/config.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Configuration Management
3
+ * 配置管理模块
4
+ */
5
+
6
+ import { GlobalConfig, ProviderConfig, OcosayConfig } from './core/types'
7
+ import { TTSError, TTSErrorCode } from './core/types'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+
11
+ const DEFAULT_CONFIG: OcosayConfig = {
12
+ enabled: true,
13
+ autoPlay: false,
14
+ autoRead: false, // 豆包模式开关
15
+ streamMode: true, // 流式朗读模式
16
+ streamBufferSize: 30, // 缓冲字符数
17
+ streamBufferTimeout: 2000, // 缓冲区超时(ms)
18
+ provider: 'minimax',
19
+ ttsModel: 'speech-02-turbo',
20
+ baseURL: 'https://api.minimax.io',
21
+ speed: 1.0,
22
+ volume: 80,
23
+ pitch: 1.0
24
+ }
25
+
26
+ class ConfigManager {
27
+ private config: OcosayConfig
28
+ private configPath: string
29
+ private providers: Record<string, ProviderConfig> = {}
30
+
31
+ constructor(configPath?: string) {
32
+ this.configPath = configPath || './config.json'
33
+ const loaded = this.loadConfig()
34
+ this.config = loaded
35
+ this.providers = (loaded as Record<string, unknown>).providers as Record<string, ProviderConfig> || {}
36
+ }
37
+
38
+ private loadConfig(): OcosayConfig {
39
+ try {
40
+ if (fs.existsSync(this.configPath)) {
41
+ const content = fs.readFileSync(this.configPath, 'utf-8')
42
+ const parsed = JSON.parse(content)
43
+ this.providers = parsed.providers || {}
44
+ const { providers, ...config } = parsed
45
+ return { ...DEFAULT_CONFIG, ...config }
46
+ }
47
+ } catch (error) {
48
+ console.warn('Failed to load config, using defaults')
49
+ }
50
+ return { ...DEFAULT_CONFIG }
51
+ }
52
+
53
+ saveConfig(): void {
54
+ try {
55
+ const dir = path.dirname(this.configPath)
56
+ if (!fs.existsSync(dir)) {
57
+ fs.mkdirSync(dir, { recursive: true })
58
+ }
59
+ const configWithProviders = { ...this.config, providers: this.providers }
60
+ fs.writeFileSync(this.configPath, JSON.stringify(configWithProviders, null, 2))
61
+ } catch (error) {
62
+ console.error('Failed to save config:', error)
63
+ }
64
+ }
65
+
66
+ getGlobal(): OcosayConfig {
67
+ return this.config
68
+ }
69
+
70
+ getProviderConfig(providerName: string): ProviderConfig | undefined {
71
+ return this.providers[providerName]
72
+ }
73
+
74
+ setProviderConfig(providerName: string, config: ProviderConfig): void {
75
+ this.providers[providerName] = { enabled: true, ...config }
76
+ this.saveConfig()
77
+ }
78
+
79
+ getDefaultProvider(): string {
80
+ return this.config.provider || 'minimax'
81
+ }
82
+
83
+ setDefaultProvider(provider: string): void {
84
+ this.config.provider = provider
85
+ this.saveConfig()
86
+ }
87
+
88
+ validateApiKey(provider: string, apiKey: string): void {
89
+ if (!apiKey) {
90
+ throw new TTSError(
91
+ `API key is required for provider "${provider}"`,
92
+ TTSErrorCode.AUTH,
93
+ provider
94
+ )
95
+ }
96
+ }
97
+
98
+ validateStreamConfig(): void {
99
+ if ((this.config.streamBufferSize ?? 30) < 5) {
100
+ throw new TTSError(
101
+ 'streamBufferSize must be at least 5',
102
+ TTSErrorCode.INVALID_PARAMS,
103
+ 'config'
104
+ )
105
+ }
106
+ if ((this.config.streamBufferTimeout ?? 2000) < 500) {
107
+ throw new TTSError(
108
+ 'streamBufferTimeout must be at least 500ms',
109
+ TTSErrorCode.INVALID_PARAMS,
110
+ 'config'
111
+ )
112
+ }
113
+ }
114
+ }
115
+
116
+ // 单例导出
117
+ export const configManager = new ConfigManager()
118
+
119
+ export { ConfigManager, DEFAULT_CONFIG }
120
+ export type { OcosayConfig, GlobalConfig, ProviderConfig }
121
+ export default configManager
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Afplay Backend - macOS 平台音频播放后端
3
+ * 使用系统内置的 afplay 命令
4
+ */
5
+
6
+ import { execFile, ChildProcess } from 'child_process'
7
+ import { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
8
+ import { tmpdir } from 'os'
9
+ import { join } from 'path'
10
+ import { writeFileSync, unlinkSync, existsSync } from 'fs'
11
+
12
+ // 白名单:只允许特定路径格式(禁止 - 防止命令注入)
13
+ const SAFE_PATH_REGEX = /^[\w\/\.]+$/
14
+
15
+ /**
16
+ * AfplayBackend - macOS 原生音频播放后端
17
+ * 不支持真正的流式播放,需要先将数据写入临时文件
18
+ */
19
+ export class AfplayBackend implements AudioBackend {
20
+ readonly name = 'afplay'
21
+ readonly supportsStreaming = false
22
+
23
+ private process?: ChildProcess
24
+ private tempFile?: string
25
+ private events?: AudioBackendEvents
26
+ private _started = false
27
+ private _paused = false
28
+ private _stopped = false
29
+ // P0-4: 缓冲所有chunk,等end()时一次性写入文件
30
+ private chunks: Buffer[] = []
31
+ private hasEnded = false
32
+
33
+ constructor(options: BackendOptions = {}) {
34
+ this.events = options.events
35
+ }
36
+
37
+ start(filePath: string): void {
38
+ if (this._started) return
39
+
40
+ if (!SAFE_PATH_REGEX.test(filePath)) {
41
+ throw new Error(`Invalid file path: ${filePath}`)
42
+ }
43
+
44
+ this.tempFile = filePath
45
+ this._started = true
46
+ this._stopped = false
47
+
48
+ this.events?.onStart?.()
49
+
50
+ // 启动播放进程
51
+ this.process = execFile('afplay', [filePath], (error) => {
52
+ if (this._stopped) return
53
+
54
+ if (error) {
55
+ this.handleError(error)
56
+ return
57
+ }
58
+
59
+ // 播放正常结束
60
+ this._started = false
61
+ this.events?.onEnd?.()
62
+ })
63
+
64
+ this.process.on('error', (error) => {
65
+ this.handleError(error)
66
+ })
67
+ }
68
+
69
+ write(chunk: Buffer): void {
70
+ if (this._stopped) return
71
+ // P0-4: 缓冲所有chunk,等end()时一次性写入
72
+ this.chunks.push(chunk)
73
+ }
74
+
75
+ end(): void {
76
+ if (this._stopped || this.hasEnded) return
77
+ this.hasEnded = true
78
+
79
+ if (this.chunks.length === 0) return
80
+
81
+ // P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
82
+ this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`)
83
+ writeFileSync(this.tempFile, Buffer.concat(this.chunks))
84
+ this.chunks = []
85
+ this.start(this.tempFile)
86
+ }
87
+
88
+ pause(): void {
89
+ if (!this._started || this._paused || this._stopped) return
90
+
91
+ if (this.process) {
92
+ try {
93
+ this.process.kill('SIGSTOP')
94
+ this._paused = true
95
+ this.events?.onPause?.()
96
+ } catch (e) {
97
+ // SIGSTOP 可能失败
98
+ }
99
+ }
100
+ }
101
+
102
+ resume(): void {
103
+ if (!this._paused || this._stopped) return
104
+
105
+ if (this.process) {
106
+ try {
107
+ this.process.kill('SIGCONT')
108
+ this._paused = false
109
+ this.events?.onResume?.()
110
+ } catch (e) {
111
+ // SIGCONT 可能失败
112
+ }
113
+ }
114
+ }
115
+
116
+ stop(): void {
117
+ this._stopped = true
118
+ this._started = false
119
+ this._paused = false
120
+
121
+ if (this.process) {
122
+ try {
123
+ this.process.kill('SIGTERM')
124
+ } catch (e) {
125
+ // 忽略错误
126
+ }
127
+ this.process = undefined
128
+ }
129
+
130
+ // 清理临时文件
131
+ this.cleanup()
132
+ this.chunks = []
133
+ this.hasEnded = false
134
+
135
+ this.events?.onStop?.()
136
+ }
137
+
138
+ setVolume(_volume: number): void {
139
+ // afplay 不支持命令行设置音量
140
+ }
141
+
142
+ destroy(): void {
143
+ this.stop()
144
+ }
145
+
146
+ private cleanup(): void {
147
+ if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
148
+ try {
149
+ if (existsSync(this.tempFile)) {
150
+ unlinkSync(this.tempFile)
151
+ }
152
+ } catch (e) {
153
+ // 忽略清理错误
154
+ }
155
+ this.tempFile = undefined
156
+ }
157
+ }
158
+
159
+ private handleError(error: Error): void {
160
+ this.events?.onError?.(error)
161
+ }
162
+ }