create-pardx-scaffold 0.1.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.
- package/README.md +45 -0
- package/cli.js +104 -0
- package/package.json +15 -0
- package/template/.cursorrules +479 -0
- package/template/.prettierignore +29 -0
- package/template/.prettierrc +11 -0
- package/template/.vscode/extensions.json +8 -0
- package/template/.vscode/settings.json +122 -0
- package/template/CLAUDE.md +762 -0
- package/template/README.md +125 -0
- package/template/apps/api/.env.example +11 -0
- package/template/apps/api/.eslintrc.js +222 -0
- package/template/apps/api/config.local.yaml +397 -0
- package/template/apps/api/libs/domain/auth/package.json +11 -0
- package/template/apps/api/libs/domain/auth/src/README.md +189 -0
- package/template/apps/api/libs/domain/auth/src/auth-validation.service.ts +37 -0
- package/template/apps/api/libs/domain/auth/src/auth.guard.ts +173 -0
- package/template/apps/api/libs/domain/auth/src/auth.module.ts +23 -0
- package/template/apps/api/libs/domain/auth/src/auth.service.ts +198 -0
- package/template/apps/api/libs/domain/auth/src/auth.ts +66 -0
- package/template/apps/api/libs/domain/auth/src/decorators/presets.decorator.ts +50 -0
- package/template/apps/api/libs/domain/auth/src/decorators/rbac.decorator.ts +67 -0
- package/template/apps/api/libs/domain/auth/src/decorators/resource-owner.decorator.ts +67 -0
- package/template/apps/api/libs/domain/auth/src/dto/auth.dto.ts +10 -0
- package/template/apps/api/libs/domain/auth/src/guards/streaming-asr-session.guard.ts +179 -0
- package/template/apps/api/libs/domain/auth/src/index.ts +12 -0
- package/template/apps/api/libs/domain/auth/src/types/auth.interface.ts +52 -0
- package/template/apps/api/libs/domain/auth/tsconfig.lib.json +9 -0
- package/template/apps/api/libs/domain/db/package.json +11 -0
- package/template/apps/api/libs/domain/db/src/index.ts +14 -0
- package/template/apps/api/libs/domain/db/src/modules/country-code/country-code.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/country-code/country-code.service.ts +140 -0
- package/template/apps/api/libs/domain/db/src/modules/country-code/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/discord-auth/discord-auth.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/discord-auth/discord-auth.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/discord-auth/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/email-auth/email-auth.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/email-auth/email-auth.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/email-auth/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/file-source/file-source.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/file-source/file-source.service.ts +109 -0
- package/template/apps/api/libs/domain/db/src/modules/file-source/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/google-auth/google-auth.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/google-auth/google-auth.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/google-auth/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/message/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/message/message.module.ts +10 -0
- package/template/apps/api/libs/domain/db/src/modules/message/message.service.ts +314 -0
- package/template/apps/api/libs/domain/db/src/modules/mobile-auth/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/mobile-auth/mobile-auth.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/mobile-auth/mobile-auth.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/risk-detection-record/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/risk-detection-record/risk-detection-record.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/risk-detection-record/risk-detection-record.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/system-task-queue/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/system-task-queue/system-task-queue.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/system-task-queue/system-task-queue.service.ts +101 -0
- package/template/apps/api/libs/domain/db/src/modules/user-info/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/user-info/user-info.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/user-info/user-info.service.ts +139 -0
- package/template/apps/api/libs/domain/db/src/modules/wechat-auth/index.ts +2 -0
- package/template/apps/api/libs/domain/db/src/modules/wechat-auth/wechat-auth.module.ts +12 -0
- package/template/apps/api/libs/domain/db/src/modules/wechat-auth/wechat-auth.service.ts +101 -0
- package/template/apps/api/libs/domain/db/tsconfig.lib.json +9 -0
- package/template/apps/api/libs/infra/clients/internal/ai/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/ai/risk-detection.client.ts +301 -0
- package/template/apps/api/libs/infra/clients/internal/ai/risk-detection.module.ts +22 -0
- package/template/apps/api/libs/infra/clients/internal/crypt/crypt-client.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/clients/internal/crypt/crypt.client.ts +37 -0
- package/template/apps/api/libs/infra/clients/internal/crypt/crypt.module.ts +10 -0
- package/template/apps/api/libs/infra/clients/internal/crypt/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/email/dto/email.dto.ts +75 -0
- package/template/apps/api/libs/infra/clients/internal/email/index.ts +11 -0
- package/template/apps/api/libs/infra/clients/internal/email/sendcloud.client.ts +400 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/README.md +255 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/dto/file-cdn.dto.ts +96 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/file-cdn-client.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/file-cdn.client.ts +620 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/file-cdn.module.ts +19 -0
- package/template/apps/api/libs/infra/clients/internal/file-cdn/index.ts +40 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/config/file.config.ts +14 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/dto/file.dto.ts +127 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-gcs.client.ts +154 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-qiniu.client.ts +729 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-s3.client.ts +1097 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-storage.interface.ts +114 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-tos.client.ts +767 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/file-us3.client.ts +176 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/index.ts +16 -0
- package/template/apps/api/libs/infra/clients/internal/ocr/dto/ocr.dto.ts +61 -0
- package/template/apps/api/libs/infra/clients/internal/ocr/index.ts +3 -0
- package/template/apps/api/libs/infra/clients/internal/ocr/ocr.client.ts +123 -0
- package/template/apps/api/libs/infra/clients/internal/ocr/ocr.module.ts +15 -0
- package/template/apps/api/libs/infra/clients/internal/openai/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/openai/openai.client.ts +135 -0
- package/template/apps/api/libs/infra/clients/internal/openai/openai.module.ts +17 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/README.md +508 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/index.ts +44 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/openspeech.client.ts +441 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/openspeech.factory.ts +450 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/openspeech.module.ts +56 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/providers/aliyun.provider.ts +308 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/providers/base.provider.ts +114 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/providers/index.ts +10 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/providers/volcengine-streaming.provider.ts +1689 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/providers/volcengine.provider.ts +387 -0
- package/template/apps/api/libs/infra/clients/internal/openspeech/types.ts +467 -0
- package/template/apps/api/libs/infra/clients/internal/sms/dto/sms.dto.ts +97 -0
- package/template/apps/api/libs/infra/clients/internal/sms/index.ts +15 -0
- package/template/apps/api/libs/infra/clients/internal/sms/sms-aliyun.client.ts +52 -0
- package/template/apps/api/libs/infra/clients/internal/sms/sms-http.client.ts +111 -0
- package/template/apps/api/libs/infra/clients/internal/sms/sms-tencent.client.ts +54 -0
- package/template/apps/api/libs/infra/clients/internal/sms/sms-volcengine.client.ts +165 -0
- package/template/apps/api/libs/infra/clients/internal/sms/sms-zxjc.client.ts +47 -0
- package/template/apps/api/libs/infra/clients/internal/sse/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/sse/sse-client.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/clients/internal/sse/sse.client.ts +360 -0
- package/template/apps/api/libs/infra/clients/internal/sse/sse.module.ts +17 -0
- package/template/apps/api/libs/infra/clients/internal/third-party-sse/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/third-party-sse/third-party-sse.client.ts +51 -0
- package/template/apps/api/libs/infra/clients/internal/third-party-sse/third-party-sse.module.ts +10 -0
- package/template/apps/api/libs/infra/clients/internal/third-party-sse/third-party-sse.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/clients/internal/verify/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/verify/verify.client.ts +42 -0
- package/template/apps/api/libs/infra/clients/internal/verify/verify.module.ts +10 -0
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/dto/tts.dto.ts +64 -0
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/index.ts +3 -0
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/volcengine-tts.client.ts +846 -0
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/volcengine-tts.module.ts +21 -0
- package/template/apps/api/libs/infra/clients/internal/wechat/index.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/wechat/wechat-client.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/clients/internal/wechat/wechat.client.ts +4 -0
- package/template/apps/api/libs/infra/clients/internal/wechat/wechat.module.ts +8 -0
- package/template/apps/api/libs/infra/clients/plugin/decorators/inject-client.decorator.ts +60 -0
- package/template/apps/api/libs/infra/clients/plugin/index.ts +21 -0
- package/template/apps/api/libs/infra/clients/plugin/interceptors/http-logging.interceptor.ts +130 -0
- package/template/apps/api/libs/infra/clients/plugin/interfaces/client.interface.ts +86 -0
- package/template/apps/api/libs/infra/clients/plugin/utils/retry.util.ts +157 -0
- package/template/apps/api/libs/infra/common/adapters/base.adapter.ts +94 -0
- package/template/apps/api/libs/infra/common/adapters/index.ts +38 -0
- package/template/apps/api/libs/infra/common/config/README.md +254 -0
- package/template/apps/api/libs/infra/common/config/agentx.config.ts +91 -0
- package/template/apps/api/libs/infra/common/config/configuration.ts +289 -0
- package/template/apps/api/libs/infra/common/config/constant/config.constants.ts +92 -0
- package/template/apps/api/libs/infra/common/config/dto/config.dto.ts +282 -0
- package/template/apps/api/libs/infra/common/config/package.json +11 -0
- package/template/apps/api/libs/infra/common/config/validation/env.validation.ts +161 -0
- package/template/apps/api/libs/infra/common/config/validation/index.ts +188 -0
- package/template/apps/api/libs/infra/common/config/validation/keys.validation.ts +564 -0
- package/template/apps/api/libs/infra/common/config/validation/yaml.validation.ts +582 -0
- package/template/apps/api/libs/infra/common/decorators/app-version/app-version.controller.ts +135 -0
- package/template/apps/api/libs/infra/common/decorators/app-version/app-version.interceptor.ts +36 -0
- package/template/apps/api/libs/infra/common/decorators/app-version/app-version.module.ts +27 -0
- package/template/apps/api/libs/infra/common/decorators/app-version/app-version.service.ts +252 -0
- package/template/apps/api/libs/infra/common/decorators/app-version/index.ts +13 -0
- package/template/apps/api/libs/infra/common/decorators/cache/cache.decorator.ts +437 -0
- package/template/apps/api/libs/infra/common/decorators/cache/cache.interceptor.ts +268 -0
- package/template/apps/api/libs/infra/common/decorators/cache/cache.module.ts +24 -0
- package/template/apps/api/libs/infra/common/decorators/cache/index.ts +33 -0
- package/template/apps/api/libs/infra/common/decorators/event/event.decorator.ts +229 -0
- package/template/apps/api/libs/infra/common/decorators/event/event.interceptor.ts +155 -0
- package/template/apps/api/libs/infra/common/decorators/event/event.module.ts +47 -0
- package/template/apps/api/libs/infra/common/decorators/event/handlers/cache-event.handler.ts +159 -0
- package/template/apps/api/libs/infra/common/decorators/event/index.ts +29 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/feature-flag.decorator.ts +221 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/feature-flag.interceptor.ts +150 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/feature-flag.module.ts +27 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/feature-flag.service.spec.ts +330 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/feature-flag.service.ts +423 -0
- package/template/apps/api/libs/infra/common/decorators/feature-flag/index.ts +28 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/dto/rate-limit.dto.ts +201 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/index.ts +54 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/rate-limit.decorator.ts +216 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/rate-limit.exception.ts +74 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/rate-limit.module.ts +37 -0
- package/template/apps/api/libs/infra/common/decorators/rate-limit/rate-limit.service.ts +430 -0
- package/template/apps/api/libs/infra/common/decorators/response.decorator.ts +67 -0
- package/template/apps/api/libs/infra/common/decorators/skip-version-check.decorator.ts +27 -0
- package/template/apps/api/libs/infra/common/decorators/transaction/index.ts +12 -0
- package/template/apps/api/libs/infra/common/decorators/transaction/transactional.decorator.ts +677 -0
- package/template/apps/api/libs/infra/common/decorators/ts-rest-controller.decorator.ts +63 -0
- package/template/apps/api/libs/infra/common/decorators/validation.decorator.ts +120 -0
- package/template/apps/api/libs/infra/common/decorators/version/index.ts +24 -0
- package/template/apps/api/libs/infra/common/decorators/version/version.decorator.ts +168 -0
- package/template/apps/api/libs/infra/common/decorators/version/version.interceptor.ts +97 -0
- package/template/apps/api/libs/infra/common/decorators/version/version.module.ts +21 -0
- package/template/apps/api/libs/infra/common/enums/action.enum.ts +7 -0
- package/template/apps/api/libs/infra/common/enums/error-codes.ts +71 -0
- package/template/apps/api/libs/infra/common/enums/role.enum.ts +4 -0
- package/template/apps/api/libs/infra/common/filter/exception/api.exception.ts +168 -0
- package/template/apps/api/libs/infra/common/filter/exception/exception.ts +47 -0
- package/template/apps/api/libs/infra/common/filter/exception/http.exception.ts +126 -0
- package/template/apps/api/libs/infra/common/guards/index.ts +1 -0
- package/template/apps/api/libs/infra/common/guards/version.guard.ts +312 -0
- package/template/apps/api/libs/infra/common/interceptor/mask/index.ts +1 -0
- package/template/apps/api/libs/infra/common/interceptor/mask/mask.interceptor.ts +242 -0
- package/template/apps/api/libs/infra/common/interceptor/rate-limit/no-rate-limit.interceptor.ts +14 -0
- package/template/apps/api/libs/infra/common/interceptor/rate-limit/rate-limit.interceptor.ts +230 -0
- package/template/apps/api/libs/infra/common/interceptor/transform/transform.interceptor.spec.ts +7 -0
- package/template/apps/api/libs/infra/common/interceptor/transform/transform.interceptor.ts +75 -0
- package/template/apps/api/libs/infra/common/interceptor/version/index.ts +1 -0
- package/template/apps/api/libs/infra/common/interceptor/version/version-header.interceptor.ts +62 -0
- package/template/apps/api/libs/infra/common/middleware/request.middleware.ts +109 -0
- package/template/apps/api/libs/infra/common/package.json +11 -0
- package/template/apps/api/libs/infra/common/pipes/transform-root.pipe.ts +12 -0
- package/template/apps/api/libs/infra/common/ts-rest/index.ts +26 -0
- package/template/apps/api/libs/infra/common/ts-rest/response.helper.ts +233 -0
- package/template/apps/api/libs/infra/i18n/en/errors.json +77 -0
- package/template/apps/api/libs/infra/i18n/en/events.json +1 -0
- package/template/apps/api/libs/infra/i18n/package.json +11 -0
- package/template/apps/api/libs/infra/i18n/zh-CN/errors.json +77 -0
- package/template/apps/api/libs/infra/i18n/zh-CN/events.json +1 -0
- package/template/apps/api/libs/infra/jwt/dto/jwt.dto.ts +1 -0
- package/template/apps/api/libs/infra/jwt/jwt.module.ts +26 -0
- package/template/apps/api/libs/infra/jwt/package.json +11 -0
- package/template/apps/api/libs/infra/prisma/db-metrics/package.json +11 -0
- package/template/apps/api/libs/infra/prisma/db-metrics/src/db-metrics.module.ts +141 -0
- package/template/apps/api/libs/infra/prisma/db-metrics/src/db-metrics.service.ts +456 -0
- package/template/apps/api/libs/infra/prisma/db-metrics/src/index.ts +2 -0
- package/template/apps/api/libs/infra/prisma/db-metrics/tsconfig.lib.json +9 -0
- package/template/apps/api/libs/infra/prisma/middleware/soft-delete.middleware.ts +179 -0
- package/template/apps/api/libs/infra/prisma/package.json +11 -0
- package/template/apps/api/libs/infra/prisma/prisma/index.ts +3 -0
- package/template/apps/api/libs/infra/prisma/prisma/prisma.module.ts +12 -0
- package/template/apps/api/libs/infra/prisma/prisma/prisma.service.ts +18 -0
- package/template/apps/api/libs/infra/prisma/prisma/types.ts +6 -0
- package/template/apps/api/libs/infra/prisma/prisma-read/prisma-read.module.ts +11 -0
- package/template/apps/api/libs/infra/prisma/prisma-read/prisma-read.service.ts +280 -0
- package/template/apps/api/libs/infra/prisma/prisma-write/prisma-write.module.ts +11 -0
- package/template/apps/api/libs/infra/prisma/prisma-write/prisma-write.service.ts +278 -0
- package/template/apps/api/libs/infra/prisma/prometheus/index.ts +1 -0
- package/template/apps/api/libs/infra/prisma/prometheus/prometheus.module.ts +231 -0
- package/template/apps/api/libs/infra/rabbitmq/package.json +11 -0
- package/template/apps/api/libs/infra/rabbitmq/src/dto/rabbitmq.dto.ts +13 -0
- package/template/apps/api/libs/infra/rabbitmq/src/index.ts +5 -0
- package/template/apps/api/libs/infra/rabbitmq/src/rabbitmq-events.module.ts +132 -0
- package/template/apps/api/libs/infra/rabbitmq/src/rabbitmq-events.service.ts +199 -0
- package/template/apps/api/libs/infra/rabbitmq/src/rabbitmq.module.ts +101 -0
- package/template/apps/api/libs/infra/rabbitmq/src/rabbitmq.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/rabbitmq/src/rabbitmq.service.ts +543 -0
- package/template/apps/api/libs/infra/rabbitmq/tsconfig.lib.json +9 -0
- package/template/apps/api/libs/infra/redis/dto/redis.dto.ts +3 -0
- package/template/apps/api/libs/infra/redis/package.json +11 -0
- package/template/apps/api/libs/infra/redis/src/index.ts +2 -0
- package/template/apps/api/libs/infra/redis/src/redis.module.ts +63 -0
- package/template/apps/api/libs/infra/redis/src/redis.service.spec.ts +18 -0
- package/template/apps/api/libs/infra/redis/src/redis.service.ts +730 -0
- package/template/apps/api/libs/infra/redis/tsconfig.lib.json +9 -0
- package/template/apps/api/libs/infra/shared-db/index.ts +14 -0
- package/template/apps/api/libs/infra/shared-db/transaction-context.ts +51 -0
- package/template/apps/api/libs/infra/shared-db/transaction.module.ts +15 -0
- package/template/apps/api/libs/infra/shared-db/transaction.perf.spec.ts +226 -0
- package/template/apps/api/libs/infra/shared-db/transactional-service.base.ts +102 -0
- package/template/apps/api/libs/infra/shared-db/unit-of-work.service.ts +142 -0
- package/template/apps/api/libs/infra/shared-services/email/dto/email.dto.ts +87 -0
- package/template/apps/api/libs/infra/shared-services/email/email.module.ts +27 -0
- package/template/apps/api/libs/infra/shared-services/email/email.service.ts +258 -0
- package/template/apps/api/libs/infra/shared-services/email/index.ts +5 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/README.md +376 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +306 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.factory.ts +347 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +62 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.service.ts +849 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/index.ts +57 -0
- package/template/apps/api/libs/infra/shared-services/file-storage/types.ts +210 -0
- package/template/apps/api/libs/infra/shared-services/ip-info/index.ts +2 -0
- package/template/apps/api/libs/infra/shared-services/ip-info/ip-info.module.ts +18 -0
- package/template/apps/api/libs/infra/shared-services/ip-info/ip-info.service.ts +118 -0
- package/template/apps/api/libs/infra/shared-services/sms/index.ts +11 -0
- package/template/apps/api/libs/infra/shared-services/sms/sms.factory.ts +367 -0
- package/template/apps/api/libs/infra/shared-services/sms/sms.module.ts +27 -0
- package/template/apps/api/libs/infra/shared-services/sms/sms.service.ts +315 -0
- package/template/apps/api/libs/infra/shared-services/sms/types.ts +297 -0
- package/template/apps/api/libs/infra/shared-services/streaming-asr/index.ts +50 -0
- package/template/apps/api/libs/infra/shared-services/streaming-asr/streaming-asr.module.ts +47 -0
- package/template/apps/api/libs/infra/shared-services/streaming-asr/streaming-asr.service.ts +1336 -0
- package/template/apps/api/libs/infra/shared-services/streaming-asr/types.ts +208 -0
- package/template/apps/api/libs/infra/shared-services/system-health/index.ts +3 -0
- package/template/apps/api/libs/infra/shared-services/system-health/system-health.controller.ts +61 -0
- package/template/apps/api/libs/infra/shared-services/system-health/system-health.module.ts +16 -0
- package/template/apps/api/libs/infra/shared-services/system-health/system-health.service.ts +69 -0
- package/template/apps/api/libs/infra/shared-services/uploader/index.ts +2 -0
- package/template/apps/api/libs/infra/shared-services/uploader/uploader.module.ts +11 -0
- package/template/apps/api/libs/infra/shared-services/uploader/uploader.service.ts +265 -0
- package/template/apps/api/libs/infra/utils/array-buffer.util.ts +8 -0
- package/template/apps/api/libs/infra/utils/array.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/bcrypt.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/bigint.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/bytes.convert.util.ts +13 -0
- package/template/apps/api/libs/infra/utils/crypto.util.ts +206 -0
- package/template/apps/api/libs/infra/utils/download.ts +21 -0
- package/template/apps/api/libs/infra/utils/enviroment.util.ts +130 -0
- package/template/apps/api/libs/infra/utils/ffmpeg.util.ts +29 -0
- package/template/apps/api/libs/infra/utils/file.util.ts +448 -0
- package/template/apps/api/libs/infra/utils/folder.util.ts +11 -0
- package/template/apps/api/libs/infra/utils/frame.util.ts +24 -0
- package/template/apps/api/libs/infra/utils/http-client.ts +133 -0
- package/template/apps/api/libs/infra/utils/ip.util.ts +22 -0
- package/template/apps/api/libs/infra/utils/json.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/load-env.util.ts +53 -0
- package/template/apps/api/libs/infra/utils/logger.util.ts +121 -0
- package/template/apps/api/libs/infra/utils/object.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/package.json +11 -0
- package/template/apps/api/libs/infra/utils/prisma-error.util.ts +397 -0
- package/template/apps/api/libs/infra/utils/response.ts +23 -0
- package/template/apps/api/libs/infra/utils/serialize.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/string.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/timer.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/urlencode.util.ts +3 -0
- package/template/apps/api/libs/infra/utils/validate.util.ts +3 -0
- package/template/apps/api/nest-cli.json +25 -0
- package/template/apps/api/package.json +174 -0
- package/template/apps/api/prisma/schema.prisma +352 -0
- package/template/apps/api/prisma/seed.ts +30 -0
- package/template/apps/api/scripts/generate-db-crud.js +344 -0
- package/template/apps/api/scripts/insert-country-codes.ts +325 -0
- package/template/apps/api/scripts/link-prisma.js +44 -0
- package/template/apps/api/scripts/validate-api-versions.ts +273 -0
- package/template/apps/api/src/app.module.ts +208 -0
- package/template/apps/api/src/main.ts +298 -0
- package/template/apps/api/src/modules/health/health.controller.ts +13 -0
- package/template/apps/api/src/modules/health/health.module.ts +7 -0
- package/template/apps/api/tsconfig.build.json +4 -0
- package/template/apps/api/tsconfig.json +123 -0
- package/template/apps/web/.env.example +5 -0
- package/template/apps/web/app/globals.css +27 -0
- package/template/apps/web/app/layout.tsx +19 -0
- package/template/apps/web/app/page.tsx +42 -0
- package/template/apps/web/hooks/useAspectRatioSize.ts +187 -0
- package/template/apps/web/hooks/useDebouncedValue.ts +25 -0
- package/template/apps/web/hooks/useErrorHandler.ts +113 -0
- package/template/apps/web/hooks/useHotkeys.ts +251 -0
- package/template/apps/web/hooks/useI18nToast.ts +240 -0
- package/template/apps/web/hooks/useI18nValidation.ts +262 -0
- package/template/apps/web/hooks/useNotificationSSE.ts +270 -0
- package/template/apps/web/hooks/useOperationFeedback.ts +108 -0
- package/template/apps/web/hooks/usePerformanceMonitor.ts +105 -0
- package/template/apps/web/hooks/usePermissions.ts +17 -0
- package/template/apps/web/hooks/useTask.ts +489 -0
- package/template/apps/web/hooks/useVersionCheck.ts +329 -0
- package/template/apps/web/i18n/config.ts +50 -0
- package/template/apps/web/i18n/index.ts +30 -0
- package/template/apps/web/i18n/navigation.ts +26 -0
- package/template/apps/web/i18n/request.ts +50 -0
- package/template/apps/web/i18n/routing.ts +21 -0
- package/template/apps/web/i18n/types.ts +57 -0
- package/template/apps/web/lib/actions/auth.ts +81 -0
- package/template/apps/web/lib/actions/chat.ts +129 -0
- package/template/apps/web/lib/actions/common.ts +13 -0
- package/template/apps/web/lib/actions/task.ts +20 -0
- package/template/apps/web/lib/agent/chat-client.ts +42 -0
- package/template/apps/web/lib/agent/prompts.ts +43 -0
- package/template/apps/web/lib/analytics/components/PageTracker.tsx +137 -0
- package/template/apps/web/lib/analytics/hooks/usePageTracking.ts +137 -0
- package/template/apps/web/lib/analytics/index.ts +180 -0
- package/template/apps/web/lib/api/agents.ts +7 -0
- package/template/apps/web/lib/api/agno-chat.ts +263 -0
- package/template/apps/web/lib/api/auth-server.ts +244 -0
- package/template/apps/web/lib/api/avatar-upload.ts +96 -0
- package/template/apps/web/lib/api/cache-config.ts +236 -0
- package/template/apps/web/lib/api/client.ts +649 -0
- package/template/apps/web/lib/api/contracts/client.ts +336 -0
- package/template/apps/web/lib/api/contracts/hooks/index.ts +25 -0
- package/template/apps/web/lib/api/contracts/hooks/notification.ts +180 -0
- package/template/apps/web/lib/api/contracts/hooks/setting.ts +33 -0
- package/template/apps/web/lib/api/contracts/index.ts +18 -0
- package/template/apps/web/lib/api/contracts/server-client.ts +145 -0
- package/template/apps/web/lib/api/hooks/use-python-task.ts +154 -0
- package/template/apps/web/lib/api/queries/analytics.ts +51 -0
- package/template/apps/web/lib/api/queries/message.ts +75 -0
- package/template/apps/web/lib/api.ts +179 -0
- package/template/apps/web/lib/aspect-ratio.ts +10 -0
- package/template/apps/web/lib/audio-buffer-queue.ts +273 -0
- package/template/apps/web/lib/config.ts +163 -0
- package/template/apps/web/lib/data/industry.json +369 -0
- package/template/apps/web/lib/data/region.json +501 -0
- package/template/apps/web/lib/errors/error-handler.ts +194 -0
- package/template/apps/web/lib/errors/index.ts +16 -0
- package/template/apps/web/lib/errors/streaming-asr-errors.ts +434 -0
- package/template/apps/web/lib/form/index.ts +23 -0
- package/template/apps/web/lib/form/use-form.ts +143 -0
- package/template/apps/web/lib/icons-usage.md +99 -0
- package/template/apps/web/lib/icons.tsx +395 -0
- package/template/apps/web/lib/performance/monitor.ts +225 -0
- package/template/apps/web/lib/requests.ts +177 -0
- package/template/apps/web/lib/storage/index.ts +158 -0
- package/template/apps/web/lib/upload/api.ts +260 -0
- package/template/apps/web/lib/upload/batch-uploader.ts +286 -0
- package/template/apps/web/lib/upload/errors.ts +44 -0
- package/template/apps/web/lib/upload/folder-utils.ts +295 -0
- package/template/apps/web/lib/upload/uploader.ts +439 -0
- package/template/apps/web/lib/utils/reconnect.ts +223 -0
- package/template/apps/web/lib/utils/transcript-export.ts +321 -0
- package/template/apps/web/lib/version-mismatch.ts +147 -0
- package/template/apps/web/lib/version.ts +60 -0
- package/template/apps/web/next-env.d.ts +6 -0
- package/template/apps/web/next.config.ts +97 -0
- package/template/apps/web/package.json +89 -0
- package/template/apps/web/providers/app-provider.tsx +45 -0
- package/template/apps/web/providers/index.tsx +45 -0
- package/template/apps/web/providers/query-provider.tsx +181 -0
- package/template/apps/web/providers/theme-provider.tsx +26 -0
- package/template/apps/web/tsconfig.json +30 -0
- package/template/package.json +91 -0
- package/template/packages/config/eslint/base.js +32 -0
- package/template/packages/config/eslint/next.js +134 -0
- package/template/packages/config/eslint/react-internal.js +41 -0
- package/template/packages/config/eslint.config.mjs +26 -0
- package/template/packages/config/eslint.nestjs.config.mjs +62 -0
- package/template/packages/config/index.ts +2 -0
- package/template/packages/config/package.json +44 -0
- package/template/packages/config/postcss.config.mjs +8 -0
- package/template/packages/config/prettier.config.mjs +14 -0
- package/template/packages/config/tsconfig.json +19 -0
- package/template/packages/config/typescript/base.json +20 -0
- package/template/packages/config/typescript/nextjs.json +12 -0
- package/template/packages/config/typescript/react-library.json +8 -0
- package/template/packages/constants/README.md +111 -0
- package/template/packages/constants/package.json +25 -0
- package/template/packages/constants/src/index.ts +243 -0
- package/template/packages/constants/tsconfig.build.json +13 -0
- package/template/packages/constants/tsconfig.json +12 -0
- package/template/packages/contracts/ERROR-MIGRATION.md +179 -0
- package/template/packages/contracts/README.md +203 -0
- package/template/packages/contracts/jest.config.js +11 -0
- package/template/packages/contracts/package.json +60 -0
- package/template/packages/contracts/src/api/analytics.contract.ts +45 -0
- package/template/packages/contracts/src/api/download.contract.ts +66 -0
- package/template/packages/contracts/src/api/index.ts +12 -0
- package/template/packages/contracts/src/api/message.contract.ts +70 -0
- package/template/packages/contracts/src/api/risk-words.contract.ts +44 -0
- package/template/packages/contracts/src/api/setting.contract.ts +127 -0
- package/template/packages/contracts/src/api/sign.contract.ts +269 -0
- package/template/packages/contracts/src/api/sms.contract.ts +95 -0
- package/template/packages/contracts/src/api/system.contract.ts +52 -0
- package/template/packages/contracts/src/api/task.contract.ts +58 -0
- package/template/packages/contracts/src/api/uploader.contract.ts +93 -0
- package/template/packages/contracts/src/api/user.contract.ts +60 -0
- package/template/packages/contracts/src/api/webhook.contract.ts +73 -0
- package/template/packages/contracts/src/base.ts +319 -0
- package/template/packages/contracts/src/errors/codes.ts +55 -0
- package/template/packages/contracts/src/errors/domains/common.errors.ts +212 -0
- package/template/packages/contracts/src/errors/domains/index.ts +7 -0
- package/template/packages/contracts/src/errors/domains/user.errors.ts +51 -0
- package/template/packages/contracts/src/errors/error-response.ts +145 -0
- package/template/packages/contracts/src/errors/index.ts +16 -0
- package/template/packages/contracts/src/errors/messages.ts +240 -0
- package/template/packages/contracts/src/index.ts +16 -0
- package/template/packages/contracts/src/schemas/analytics.schema.ts +81 -0
- package/template/packages/contracts/src/schemas/download.schema.ts +59 -0
- package/template/packages/contracts/src/schemas/index.ts +18 -0
- package/template/packages/contracts/src/schemas/message.schema.ts +83 -0
- package/template/packages/contracts/src/schemas/risk-words.schema.ts +25 -0
- package/template/packages/contracts/src/schemas/setting.schema.ts +84 -0
- package/template/packages/contracts/src/schemas/sign.schema.ts +171 -0
- package/template/packages/contracts/src/schemas/sms.schema.ts +53 -0
- package/template/packages/contracts/src/schemas/sse.schema.ts +30 -0
- package/template/packages/contracts/src/schemas/system.schema.ts +26 -0
- package/template/packages/contracts/src/schemas/tag.schema.ts +65 -0
- package/template/packages/contracts/src/schemas/task.schema.ts +47 -0
- package/template/packages/contracts/src/schemas/uploader.schema.ts +121 -0
- package/template/packages/contracts/src/schemas/user.schema.ts +75 -0
- package/template/packages/contracts/src/schemas/webhook.schema.ts +72 -0
- package/template/packages/contracts/tsconfig.build.json +20 -0
- package/template/packages/contracts/tsconfig.json +12 -0
- package/template/packages/types/README.md +143 -0
- package/template/packages/types/ai.ts +30 -0
- package/template/packages/types/auth.ts +99 -0
- package/template/packages/types/common.ts +13 -0
- package/template/packages/types/creative.ts +68 -0
- package/template/packages/types/image-factory.ts +122 -0
- package/template/packages/types/index.ts +8 -0
- package/template/packages/types/package.json +21 -0
- package/template/packages/types/task.ts +27 -0
- package/template/packages/types/tsconfig.json +11 -0
- package/template/packages/ui/README.md +30 -0
- package/template/packages/ui/components.json +22 -0
- package/template/packages/ui/eslint.config.js +4 -0
- package/template/packages/ui/package.json +58 -0
- package/template/packages/ui/postcss.config.mjs +6 -0
- package/template/packages/ui/src/components/accordion.tsx +66 -0
- package/template/packages/ui/src/components/alert.tsx +61 -0
- package/template/packages/ui/src/components/avatar.tsx +57 -0
- package/template/packages/ui/src/components/badge.tsx +38 -0
- package/template/packages/ui/src/components/button.tsx +60 -0
- package/template/packages/ui/src/components/calendar.tsx +71 -0
- package/template/packages/ui/src/components/card.tsx +92 -0
- package/template/packages/ui/src/components/carousel.tsx +241 -0
- package/template/packages/ui/src/components/checkbox.tsx +32 -0
- package/template/packages/ui/src/components/command.tsx +184 -0
- package/template/packages/ui/src/components/dialog.tsx +134 -0
- package/template/packages/ui/src/components/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/components/empty.tsx +104 -0
- package/template/packages/ui/src/components/field.tsx +248 -0
- package/template/packages/ui/src/components/form.tsx +172 -0
- package/template/packages/ui/src/components/input-group.tsx +170 -0
- package/template/packages/ui/src/components/input.tsx +21 -0
- package/template/packages/ui/src/components/item.tsx +193 -0
- package/template/packages/ui/src/components/label.tsx +24 -0
- package/template/packages/ui/src/components/password-strength.tsx +248 -0
- package/template/packages/ui/src/components/popover.tsx +48 -0
- package/template/packages/ui/src/components/progress.tsx +35 -0
- package/template/packages/ui/src/components/scroll-area.tsx +48 -0
- package/template/packages/ui/src/components/select.tsx +190 -0
- package/template/packages/ui/src/components/separator.tsx +28 -0
- package/template/packages/ui/src/components/sheet.tsx +139 -0
- package/template/packages/ui/src/components/sidebar.tsx +729 -0
- package/template/packages/ui/src/components/skeleton.tsx +13 -0
- package/template/packages/ui/src/components/slider.tsx +87 -0
- package/template/packages/ui/src/components/sonner.tsx +40 -0
- package/template/packages/ui/src/components/switch.tsx +31 -0
- package/template/packages/ui/src/components/tabs.tsx +66 -0
- package/template/packages/ui/src/components/textarea.tsx +18 -0
- package/template/packages/ui/src/components/tooltip.tsx +61 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +21 -0
- package/template/packages/ui/src/index.ts +38 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/styles/globals.css +134 -0
- package/template/packages/ui/tsconfig.json +11 -0
- package/template/packages/ui/tsconfig.lint.json +8 -0
- package/template/packages/utils/README.md +173 -0
- package/template/packages/utils/array.util.ts +335 -0
- package/template/packages/utils/bcrypt.util.ts +10 -0
- package/template/packages/utils/bigint.util.ts +111 -0
- package/template/packages/utils/cn.ts +6 -0
- package/template/packages/utils/encrypt.ts +104 -0
- package/template/packages/utils/fetch.ts +170 -0
- package/template/packages/utils/file.ts +275 -0
- package/template/packages/utils/headers.ts +116 -0
- package/template/packages/utils/index.ts +22 -0
- package/template/packages/utils/jest.config.js +28 -0
- package/template/packages/utils/json.util.ts +9 -0
- package/template/packages/utils/mask.util.ts +348 -0
- package/template/packages/utils/object.util.ts +149 -0
- package/template/packages/utils/package.json +112 -0
- package/template/packages/utils/serialize.util.ts +17 -0
- package/template/packages/utils/string.util.ts +159 -0
- package/template/packages/utils/timer.util.ts +210 -0
- package/template/packages/utils/tsconfig.build.json +17 -0
- package/template/packages/utils/tsconfig.json +13 -0
- package/template/packages/utils/urlencode.util.ts +18 -0
- package/template/packages/utils/validate.util.ts +25 -0
- package/template/packages/validators/README.md +149 -0
- package/template/packages/validators/jest.config.js +20 -0
- package/template/packages/validators/package.json +32 -0
- package/template/packages/validators/src/index.ts +178 -0
- package/template/packages/validators/tsconfig.build.json +19 -0
- package/template/packages/validators/tsconfig.json +12 -0
- package/template/pnpm-lock.yaml +21574 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/scripts/generate-i18n-errors.ts +371 -0
- package/template/scripts/generate-prisma-enums.js +170 -0
- package/template/scripts/generate-prisma-enums.ts +172 -0
- package/template/scripts/init-project.js +232 -0
- package/template/turbo.json +55 -0
|
@@ -0,0 +1,1689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 火山引擎流式语音识别 Provider
|
|
3
|
+
*
|
|
4
|
+
* 本文件实现了火山引擎大模型流式语音识别功能,支持实时音频流转写。
|
|
5
|
+
* 火山引擎流式语音识别文档:https://www.volcengine.com/docs/6561/1354869
|
|
6
|
+
*
|
|
7
|
+
* 支持的模式:
|
|
8
|
+
* - 双向流式模式(优化版本):wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
|
|
9
|
+
* 推荐使用,只有结果变化时才返回新数据包,性能更优
|
|
10
|
+
*
|
|
11
|
+
* @module openspeech/providers/volcengine-streaming
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Logger } from 'winston';
|
|
15
|
+
import WebSocket from 'ws';
|
|
16
|
+
import * as zlib from 'zlib';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
19
|
+
import {
|
|
20
|
+
VolcengineSaucConfig,
|
|
21
|
+
IStreamingAsrProvider,
|
|
22
|
+
StreamingConnectParams,
|
|
23
|
+
StreamingAsrCallbacks,
|
|
24
|
+
StreamingAsrResult,
|
|
25
|
+
StreamingAsrStatus,
|
|
26
|
+
StreamingUtterance,
|
|
27
|
+
StreamingWord,
|
|
28
|
+
} from '../types';
|
|
29
|
+
|
|
30
|
+
const gzipAsync = promisify(zlib.gzip);
|
|
31
|
+
const gunzipAsync = promisify(zlib.gunzip);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 消息类型常量
|
|
35
|
+
* @description 定义 WebSocket 二进制协议中的消息类型
|
|
36
|
+
*/
|
|
37
|
+
const MESSAGE_TYPE = {
|
|
38
|
+
/** 端上发送包含请求参数的 full client request */
|
|
39
|
+
FULL_CLIENT_REQUEST: 0b0001,
|
|
40
|
+
/** 端上发送包含音频数据的 audio only request */
|
|
41
|
+
AUDIO_ONLY_CLIENT_REQUEST: 0b0010,
|
|
42
|
+
/** 服务端下发包含识别结果的 full server response */
|
|
43
|
+
FULL_SERVER_RESPONSE: 0b1001,
|
|
44
|
+
/** 服务端处理错误时下发的消息类型 */
|
|
45
|
+
ERROR_RESPONSE: 0b1111,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 消息类型特定标志
|
|
50
|
+
*/
|
|
51
|
+
const MESSAGE_FLAGS = {
|
|
52
|
+
/** header后4个字节不为sequence number */
|
|
53
|
+
NO_SEQUENCE: 0b0000,
|
|
54
|
+
/** header后4个字节为sequence number且为正 */
|
|
55
|
+
POSITIVE_SEQUENCE: 0b0001,
|
|
56
|
+
/** header后4个字节不为sequence number,仅指示此为最后一包(负包) */
|
|
57
|
+
LAST_PACKET_NO_SEQ: 0b0010,
|
|
58
|
+
/** header后4个字节为sequence number且需要为负数(最后一包/负包) */
|
|
59
|
+
LAST_PACKET_WITH_SEQ: 0b0011,
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 序列化方法常量
|
|
64
|
+
*/
|
|
65
|
+
const SERIALIZATION = {
|
|
66
|
+
/** 无序列化 */
|
|
67
|
+
NONE: 0b0000,
|
|
68
|
+
/** JSON 格式 */
|
|
69
|
+
JSON: 0b0001,
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 压缩方法常量
|
|
74
|
+
*/
|
|
75
|
+
const COMPRESSION = {
|
|
76
|
+
/** 无压缩 */
|
|
77
|
+
NONE: 0b0000,
|
|
78
|
+
/** Gzip 压缩 */
|
|
79
|
+
GZIP: 0b0001,
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 火山引擎 ASR 错误码
|
|
84
|
+
* @see https://www.volcengine.com/docs/6561/1354869
|
|
85
|
+
*/
|
|
86
|
+
const VOLCENGINE_ERROR_CODES = {
|
|
87
|
+
/** 成功 */
|
|
88
|
+
SUCCESS: 20000000,
|
|
89
|
+
/** 请求参数无效(缺失必需字段/字段值无效/重复请求) */
|
|
90
|
+
INVALID_PARAMS: 45000001,
|
|
91
|
+
/** 空音频 */
|
|
92
|
+
EMPTY_AUDIO: 45000002,
|
|
93
|
+
/** 等包超时 */
|
|
94
|
+
PACKET_TIMEOUT: 45000081,
|
|
95
|
+
/** 音频格式不正确 */
|
|
96
|
+
INVALID_AUDIO_FORMAT: 45000151,
|
|
97
|
+
/** 服务器繁忙(服务过载) */
|
|
98
|
+
SERVER_BUSY: 55000031,
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 错误码到中文描述的映射
|
|
103
|
+
*/
|
|
104
|
+
const ERROR_CODE_MESSAGES: Record<number, string> = {
|
|
105
|
+
[VOLCENGINE_ERROR_CODES.SUCCESS]: '成功',
|
|
106
|
+
[VOLCENGINE_ERROR_CODES.INVALID_PARAMS]:
|
|
107
|
+
'请求参数无效:缺失必需字段、字段值无效或重复请求',
|
|
108
|
+
[VOLCENGINE_ERROR_CODES.EMPTY_AUDIO]: '空音频:未收到有效的音频数据',
|
|
109
|
+
[VOLCENGINE_ERROR_CODES.PACKET_TIMEOUT]: '等包超时:音频发送间隔过长',
|
|
110
|
+
[VOLCENGINE_ERROR_CODES.INVALID_AUDIO_FORMAT]:
|
|
111
|
+
'音频格式不正确:请检查音频编码格式',
|
|
112
|
+
[VOLCENGINE_ERROR_CODES.SERVER_BUSY]: '服务器繁忙:服务过载,请稍后重试',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 推荐的音频包大小(毫秒)
|
|
117
|
+
* 文档建议 100-200ms,双向流式模式推荐 200ms
|
|
118
|
+
*/
|
|
119
|
+
const RECOMMENDED_AUDIO_PACKET_MS = {
|
|
120
|
+
MIN: 100,
|
|
121
|
+
MAX: 200,
|
|
122
|
+
OPTIMAL: 200,
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 重连配置接口
|
|
127
|
+
*/
|
|
128
|
+
interface ReconnectConfig {
|
|
129
|
+
/** 最大重试次数 */
|
|
130
|
+
maxRetries: number;
|
|
131
|
+
/** 初始重试延迟(毫秒) */
|
|
132
|
+
initialDelay: number;
|
|
133
|
+
/** 最大重试延迟(毫秒) */
|
|
134
|
+
maxDelay: number;
|
|
135
|
+
/** 延迟增长因子 */
|
|
136
|
+
backoffMultiplier: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 默认重连配置
|
|
141
|
+
*/
|
|
142
|
+
const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
|
|
143
|
+
maxRetries: 3,
|
|
144
|
+
initialDelay: 1000,
|
|
145
|
+
maxDelay: 10000,
|
|
146
|
+
backoffMultiplier: 2,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 心跳配置
|
|
151
|
+
*/
|
|
152
|
+
const HEARTBEAT_CONFIG = {
|
|
153
|
+
/** 心跳间隔(毫秒) */
|
|
154
|
+
interval: 25000,
|
|
155
|
+
/** 心跳超时(毫秒) */
|
|
156
|
+
timeout: 10000,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 连接信息接口
|
|
161
|
+
*/
|
|
162
|
+
interface ConnectionInfo {
|
|
163
|
+
ws: WebSocket;
|
|
164
|
+
status: StreamingAsrStatus;
|
|
165
|
+
callbacks: StreamingAsrCallbacks;
|
|
166
|
+
sessionId: string;
|
|
167
|
+
sequence: number;
|
|
168
|
+
transcript: string;
|
|
169
|
+
utterances: StreamingUtterance[];
|
|
170
|
+
/** 连接参数(用于重连) */
|
|
171
|
+
connectParams: StreamingConnectParams;
|
|
172
|
+
/** 重试次数 */
|
|
173
|
+
retryCount: number;
|
|
174
|
+
/** 心跳定时器 */
|
|
175
|
+
heartbeatTimer?: NodeJS.Timeout;
|
|
176
|
+
/** 心跳超时定时器 */
|
|
177
|
+
heartbeatTimeoutTimer?: NodeJS.Timeout;
|
|
178
|
+
/** 上次活动时间 */
|
|
179
|
+
lastActivityTime: number;
|
|
180
|
+
/** 是否正在重连 */
|
|
181
|
+
isReconnecting: boolean;
|
|
182
|
+
/** 待发送的音频缓冲(重连时使用) */
|
|
183
|
+
pendingAudioBuffer: Buffer[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 火山引擎流式语音识别 Provider
|
|
188
|
+
*
|
|
189
|
+
* @description 封装了火山引擎大模型流式语音识别的核心功能:
|
|
190
|
+
* - WebSocket 连接管理
|
|
191
|
+
* - 二进制协议编解码
|
|
192
|
+
* - 音频数据分包发送
|
|
193
|
+
* - 实时识别结果解析
|
|
194
|
+
*
|
|
195
|
+
* @class VolcengineStreamingAsrProvider
|
|
196
|
+
* @implements {IStreamingAsrProvider}
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const provider = new VolcengineStreamingAsrProvider(logger, config);
|
|
201
|
+
*
|
|
202
|
+
* // 建立连接
|
|
203
|
+
* const connectionId = await provider.connect(
|
|
204
|
+
* { sessionId: 'session-123', audioFormat: 'pcm' },
|
|
205
|
+
* {
|
|
206
|
+
* onResult: (result) => console.log('识别结果:', result.text),
|
|
207
|
+
* onConnected: () => console.log('已连接'),
|
|
208
|
+
* onError: (error) => console.error('错误:', error),
|
|
209
|
+
* }
|
|
210
|
+
* );
|
|
211
|
+
*
|
|
212
|
+
* // 发送音频数据
|
|
213
|
+
* await provider.sendAudio(connectionId, audioBuffer);
|
|
214
|
+
*
|
|
215
|
+
* // 发送最后一帧
|
|
216
|
+
* await provider.sendAudio(connectionId, lastAudioBuffer, true);
|
|
217
|
+
*
|
|
218
|
+
* // 关闭连接
|
|
219
|
+
* await provider.disconnect(connectionId);
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export class VolcengineStreamingAsrProvider implements IStreamingAsrProvider {
|
|
223
|
+
/**
|
|
224
|
+
* 云服务商标识
|
|
225
|
+
*/
|
|
226
|
+
readonly vendor = 'volcengine-streaming';
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 连接池
|
|
230
|
+
* @description 存储所有活跃的 WebSocket 连接
|
|
231
|
+
*/
|
|
232
|
+
private readonly connections: Map<string, ConnectionInfo> = new Map();
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 重连配置
|
|
236
|
+
*/
|
|
237
|
+
private readonly reconnectConfig: ReconnectConfig;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 构造函数
|
|
241
|
+
*
|
|
242
|
+
* @param {Logger} logger - Winston 日志记录器
|
|
243
|
+
* @param {VolcengineSaucConfig} config - 火山引擎流式语音识别配置
|
|
244
|
+
* @param {Partial<ReconnectConfig>} reconnectConfig - 重连配置(可选)
|
|
245
|
+
*/
|
|
246
|
+
constructor(
|
|
247
|
+
private readonly logger: Logger,
|
|
248
|
+
private readonly config: VolcengineSaucConfig,
|
|
249
|
+
reconnectConfig?: Partial<ReconnectConfig>,
|
|
250
|
+
) {
|
|
251
|
+
if (!config) {
|
|
252
|
+
throw new Error('Volcengine Streaming ASR config is required');
|
|
253
|
+
}
|
|
254
|
+
this.reconnectConfig = {
|
|
255
|
+
...DEFAULT_RECONNECT_CONFIG,
|
|
256
|
+
...reconnectConfig,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 记录信息日志
|
|
262
|
+
*/
|
|
263
|
+
private logInfo(message: string, meta?: Record<string, unknown>): void {
|
|
264
|
+
this.logger.info(`[volcengine-streaming] ${message}`, meta);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 记录错误日志
|
|
269
|
+
*/
|
|
270
|
+
private logError(message: string, meta?: Record<string, unknown>): void {
|
|
271
|
+
this.logger.error(`[volcengine-streaming] ${message}`, meta);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 记录警告日志
|
|
276
|
+
*/
|
|
277
|
+
private logWarn(message: string, meta?: Record<string, unknown>): void {
|
|
278
|
+
this.logger.warn(`[volcengine-streaming] ${message}`, meta);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 记录调试日志
|
|
283
|
+
*/
|
|
284
|
+
private logDebug(message: string, meta?: Record<string, unknown>): void {
|
|
285
|
+
this.logger.debug(`[volcengine-streaming] ${message}`, meta);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 构建 WebSocket 连接认证头
|
|
290
|
+
*
|
|
291
|
+
* @param connectId - 连接唯一标识
|
|
292
|
+
* @returns HTTP 请求头
|
|
293
|
+
*/
|
|
294
|
+
private buildHeaders(connectId: string): Record<string, string> {
|
|
295
|
+
const headers: Record<string, string> = {
|
|
296
|
+
'X-Api-App-Key': this.config.appId!,
|
|
297
|
+
'X-Api-Access-Key': this.config.appAccessToken!,
|
|
298
|
+
'X-Api-Connect-Id': connectId,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// 可选的资源 ID
|
|
302
|
+
if (this.config.resourceId) {
|
|
303
|
+
headers['X-Api-Resource-Id'] = this.config.resourceId;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return headers;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 构建消息头(4 bytes)
|
|
311
|
+
*
|
|
312
|
+
* @description 按照火山引擎 WebSocket 二进制协议构建消息头
|
|
313
|
+
*
|
|
314
|
+
* 协议格式:
|
|
315
|
+
* - Byte 0: Version (4 bits) + Header Size (4 bits)
|
|
316
|
+
* - Byte 1: Message Type (4 bits) + Message Type Specific Flags (4 bits)
|
|
317
|
+
* - Byte 2: Serialization Method (4 bits) + Compression (4 bits)
|
|
318
|
+
* - Byte 3: Reserved
|
|
319
|
+
*
|
|
320
|
+
* @param messageType - 消息类型
|
|
321
|
+
* @param specificFlags - 消息类型特定标志
|
|
322
|
+
* @param serialization - 序列化方法
|
|
323
|
+
* @param compression - 压缩方法
|
|
324
|
+
* @returns 4 字节的消息头 Buffer
|
|
325
|
+
*/
|
|
326
|
+
private buildMessageHeader(
|
|
327
|
+
messageType: number,
|
|
328
|
+
specificFlags: number = MESSAGE_FLAGS.NO_SEQUENCE,
|
|
329
|
+
serialization: number = SERIALIZATION.JSON,
|
|
330
|
+
compression: number = COMPRESSION.GZIP,
|
|
331
|
+
): Buffer {
|
|
332
|
+
const header = Buffer.alloc(4);
|
|
333
|
+
// Byte 0: Version (0b0001) + Header Size (0b0001 = 4 bytes)
|
|
334
|
+
header[0] = (0b0001 << 4) | 0b0001;
|
|
335
|
+
// Byte 1: Message Type + Specific Flags
|
|
336
|
+
header[1] = (messageType << 4) | specificFlags;
|
|
337
|
+
// Byte 2: Serialization + Compression
|
|
338
|
+
header[2] = (serialization << 4) | compression;
|
|
339
|
+
// Byte 3: Reserved
|
|
340
|
+
header[3] = 0x00;
|
|
341
|
+
return header;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 构建完整客户端请求消息(初始请求)
|
|
346
|
+
*
|
|
347
|
+
* @param data - 请求参数(JSON 对象)
|
|
348
|
+
* @returns 完整的二进制消息
|
|
349
|
+
*/
|
|
350
|
+
private async buildFullClientRequest(data: object): Promise<Buffer> {
|
|
351
|
+
const header = this.buildMessageHeader(
|
|
352
|
+
MESSAGE_TYPE.FULL_CLIENT_REQUEST,
|
|
353
|
+
MESSAGE_FLAGS.NO_SEQUENCE,
|
|
354
|
+
SERIALIZATION.JSON,
|
|
355
|
+
COMPRESSION.GZIP,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const jsonData = JSON.stringify(data);
|
|
359
|
+
const compressed = await gzipAsync(Buffer.from(jsonData, 'utf-8'));
|
|
360
|
+
|
|
361
|
+
// Payload size (4 bytes, big-endian)
|
|
362
|
+
const payloadSize = Buffer.alloc(4);
|
|
363
|
+
payloadSize.writeUInt32BE(compressed.length, 0);
|
|
364
|
+
|
|
365
|
+
return Buffer.concat([header, payloadSize, compressed]);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 构建音频数据请求消息
|
|
370
|
+
*
|
|
371
|
+
* @param audioData - 音频数据
|
|
372
|
+
* @param isLast - 是否为最后一帧
|
|
373
|
+
* @returns 完整的二进制消息
|
|
374
|
+
*/
|
|
375
|
+
private async buildAudioOnlyRequest(
|
|
376
|
+
audioData: Buffer,
|
|
377
|
+
isLast: boolean = false,
|
|
378
|
+
): Promise<Buffer> {
|
|
379
|
+
const specificFlags = isLast
|
|
380
|
+
? MESSAGE_FLAGS.LAST_PACKET_NO_SEQ
|
|
381
|
+
: MESSAGE_FLAGS.NO_SEQUENCE;
|
|
382
|
+
|
|
383
|
+
const header = this.buildMessageHeader(
|
|
384
|
+
MESSAGE_TYPE.AUDIO_ONLY_CLIENT_REQUEST,
|
|
385
|
+
specificFlags,
|
|
386
|
+
SERIALIZATION.NONE, // 音频数据不序列化
|
|
387
|
+
COMPRESSION.GZIP,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const compressed = await gzipAsync(audioData);
|
|
391
|
+
|
|
392
|
+
// Payload size (4 bytes, big-endian)
|
|
393
|
+
const payloadSize = Buffer.alloc(4);
|
|
394
|
+
payloadSize.writeUInt32BE(compressed.length, 0);
|
|
395
|
+
|
|
396
|
+
return Buffer.concat([header, payloadSize, compressed]);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 构建初始请求参数对象
|
|
401
|
+
*
|
|
402
|
+
* @description 根据连接参数构建符合火山引擎 API 规范的初始请求
|
|
403
|
+
* @param options - 连接参数选项
|
|
404
|
+
* @returns 初始请求对象
|
|
405
|
+
*/
|
|
406
|
+
private buildInitRequest(options: {
|
|
407
|
+
audioFormat: string;
|
|
408
|
+
sampleRate: number;
|
|
409
|
+
channels: number;
|
|
410
|
+
enableSpeakerInfo: boolean;
|
|
411
|
+
language?: string;
|
|
412
|
+
enableItn: boolean;
|
|
413
|
+
enablePunc: boolean;
|
|
414
|
+
enableDdc: boolean;
|
|
415
|
+
enableNonstream: boolean;
|
|
416
|
+
showUtterances: boolean;
|
|
417
|
+
showSpeechRate: boolean;
|
|
418
|
+
showVolume: boolean;
|
|
419
|
+
enableLid: boolean;
|
|
420
|
+
enableEmotionDetection: boolean;
|
|
421
|
+
enableGenderDetection: boolean;
|
|
422
|
+
resultType: string;
|
|
423
|
+
enableAccelerateText: boolean;
|
|
424
|
+
accelerateScore?: number;
|
|
425
|
+
vadSegmentDuration?: number;
|
|
426
|
+
endWindowSize?: number;
|
|
427
|
+
forceToSpeechTime?: number;
|
|
428
|
+
sensitiveWordsFilter?: string;
|
|
429
|
+
corpus?: {
|
|
430
|
+
boostingTableName?: string;
|
|
431
|
+
boostingTableId?: string;
|
|
432
|
+
correctTableName?: string;
|
|
433
|
+
correctTableId?: string;
|
|
434
|
+
hotwords?: Array<{ word: string; factor: number }>;
|
|
435
|
+
};
|
|
436
|
+
}): Record<string, unknown> {
|
|
437
|
+
const {
|
|
438
|
+
audioFormat,
|
|
439
|
+
sampleRate,
|
|
440
|
+
channels,
|
|
441
|
+
enableSpeakerInfo,
|
|
442
|
+
language,
|
|
443
|
+
enableItn,
|
|
444
|
+
enablePunc,
|
|
445
|
+
enableDdc,
|
|
446
|
+
enableNonstream,
|
|
447
|
+
showUtterances,
|
|
448
|
+
showSpeechRate,
|
|
449
|
+
showVolume,
|
|
450
|
+
enableLid,
|
|
451
|
+
enableEmotionDetection,
|
|
452
|
+
enableGenderDetection,
|
|
453
|
+
resultType,
|
|
454
|
+
enableAccelerateText,
|
|
455
|
+
accelerateScore,
|
|
456
|
+
vadSegmentDuration,
|
|
457
|
+
endWindowSize,
|
|
458
|
+
forceToSpeechTime,
|
|
459
|
+
sensitiveWordsFilter,
|
|
460
|
+
corpus,
|
|
461
|
+
} = options;
|
|
462
|
+
|
|
463
|
+
// 构建 audio 配置
|
|
464
|
+
const audio: Record<string, unknown> = {
|
|
465
|
+
format: audioFormat,
|
|
466
|
+
rate: sampleRate,
|
|
467
|
+
bits: 16,
|
|
468
|
+
channel: channels,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// 如果指定了语言,添加 language 字段
|
|
472
|
+
if (language) {
|
|
473
|
+
audio.language = language;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 构建 request 配置
|
|
477
|
+
const request: Record<string, unknown> = {
|
|
478
|
+
model_name: 'bigmodel',
|
|
479
|
+
enable_itn: enableItn,
|
|
480
|
+
enable_punc: enablePunc,
|
|
481
|
+
enable_ddc: enableDdc,
|
|
482
|
+
enable_nonstream: enableNonstream,
|
|
483
|
+
show_utterances: showUtterances,
|
|
484
|
+
show_speech_rate: showSpeechRate,
|
|
485
|
+
show_volume: showVolume,
|
|
486
|
+
enable_lid: enableLid,
|
|
487
|
+
enable_emotion_detection: enableEmotionDetection,
|
|
488
|
+
enable_gender_detection: enableGenderDetection,
|
|
489
|
+
enable_speaker_info: enableSpeakerInfo,
|
|
490
|
+
result_type: resultType,
|
|
491
|
+
enable_accelerate_text: enableAccelerateText,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// 可选参数
|
|
495
|
+
if (accelerateScore !== undefined) {
|
|
496
|
+
request.accelerate_score = accelerateScore;
|
|
497
|
+
}
|
|
498
|
+
if (vadSegmentDuration !== undefined) {
|
|
499
|
+
request.vad_segment_duration = vadSegmentDuration;
|
|
500
|
+
}
|
|
501
|
+
if (endWindowSize !== undefined) {
|
|
502
|
+
request.end_window_size = endWindowSize;
|
|
503
|
+
}
|
|
504
|
+
if (forceToSpeechTime !== undefined) {
|
|
505
|
+
request.force_to_speech_time = forceToSpeechTime;
|
|
506
|
+
}
|
|
507
|
+
if (sensitiveWordsFilter) {
|
|
508
|
+
request.sensitive_words_filter = sensitiveWordsFilter;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 构建 corpus 配置(热词/干预词)
|
|
512
|
+
if (corpus) {
|
|
513
|
+
const corpusConfig: Record<string, unknown> = {};
|
|
514
|
+
|
|
515
|
+
if (corpus.boostingTableName) {
|
|
516
|
+
corpusConfig.boosting_table_name = corpus.boostingTableName;
|
|
517
|
+
}
|
|
518
|
+
if (corpus.boostingTableId) {
|
|
519
|
+
corpusConfig.boosting_table_id = corpus.boostingTableId;
|
|
520
|
+
}
|
|
521
|
+
if (corpus.correctTableName) {
|
|
522
|
+
corpusConfig.correct_table_name = corpus.correctTableName;
|
|
523
|
+
}
|
|
524
|
+
if (corpus.correctTableId) {
|
|
525
|
+
corpusConfig.correct_table_id = corpus.correctTableId;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 热词直传
|
|
529
|
+
if (corpus.hotwords && corpus.hotwords.length > 0) {
|
|
530
|
+
corpusConfig.context = JSON.stringify({
|
|
531
|
+
hotwords: corpus.hotwords,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (Object.keys(corpusConfig).length > 0) {
|
|
536
|
+
request.corpus = corpusConfig;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
user: {
|
|
542
|
+
uid: this.config.uid,
|
|
543
|
+
},
|
|
544
|
+
audio,
|
|
545
|
+
request,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* 解析服务器响应
|
|
551
|
+
*
|
|
552
|
+
* @param data - 原始二进制响应数据
|
|
553
|
+
* @returns 解析后的识别结果
|
|
554
|
+
*/
|
|
555
|
+
private async parseServerResponse(data: Buffer): Promise<StreamingAsrResult> {
|
|
556
|
+
if (data.length < 4) {
|
|
557
|
+
throw new Error('Invalid response: too short');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 解析消息头
|
|
561
|
+
const messageType = (data[1] >> 4) & 0x0f;
|
|
562
|
+
const specificFlags = data[1] & 0x0f;
|
|
563
|
+
const compression = data[2] & 0x0f;
|
|
564
|
+
|
|
565
|
+
// 处理错误响应
|
|
566
|
+
if (messageType === MESSAGE_TYPE.ERROR_RESPONSE) {
|
|
567
|
+
if (data.length < 12) {
|
|
568
|
+
throw new Error('Invalid error response: too short');
|
|
569
|
+
}
|
|
570
|
+
const errorCode = data.readUInt32BE(4);
|
|
571
|
+
const errorSize = data.readUInt32BE(8);
|
|
572
|
+
|
|
573
|
+
// 验证 errorSize 的合理性(不超过 1MB)
|
|
574
|
+
if (errorSize > 1024 * 1024) {
|
|
575
|
+
this.logError('Invalid error size in error response', {
|
|
576
|
+
errorSize,
|
|
577
|
+
dataLength: data.length,
|
|
578
|
+
firstBytes: data.slice(0, 16).toString('hex'),
|
|
579
|
+
});
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Invalid error response: errorSize too large (${errorSize})`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (data.length < 12 + errorSize) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Incomplete error response: expected ${12 + errorSize} bytes, got ${data.length}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const rawErrorMessage = data.slice(12, 12 + errorSize).toString('utf-8');
|
|
592
|
+
|
|
593
|
+
// 使用友好的错误描述
|
|
594
|
+
const friendlyMessage =
|
|
595
|
+
ERROR_CODE_MESSAGES[errorCode] || `未知错误 (${errorCode})`;
|
|
596
|
+
|
|
597
|
+
this.logError('Volcengine ASR error response received', {
|
|
598
|
+
errorCode,
|
|
599
|
+
rawErrorMessage,
|
|
600
|
+
friendlyMessage,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
text: '',
|
|
605
|
+
isFinal: true,
|
|
606
|
+
error: `火山引擎 ASR 错误 [${errorCode}]: ${friendlyMessage}。原始信息: ${rawErrorMessage}`,
|
|
607
|
+
errorCode,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 验证消息类型
|
|
612
|
+
if (messageType !== MESSAGE_TYPE.FULL_SERVER_RESPONSE) {
|
|
613
|
+
this.logWarn('Unexpected message type', {
|
|
614
|
+
messageType,
|
|
615
|
+
expected: MESSAGE_TYPE.FULL_SERVER_RESPONSE,
|
|
616
|
+
dataLength: data.length,
|
|
617
|
+
firstBytes: data.slice(0, 16).toString('hex'),
|
|
618
|
+
});
|
|
619
|
+
throw new Error(`Unexpected message type: ${messageType}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 根据 specificFlags 判断是否有 sequence 字段
|
|
623
|
+
// 0b0001 (POSITIVE_SEQUENCE) 和 0b0011 (LAST_PACKET_WITH_SEQ) 包含 sequence
|
|
624
|
+
// 0b0000 (NO_SEQUENCE) 和 0b0010 (LAST_PACKET_NO_SEQ) 不包含 sequence
|
|
625
|
+
const hasSequence =
|
|
626
|
+
specificFlags === MESSAGE_FLAGS.POSITIVE_SEQUENCE ||
|
|
627
|
+
specificFlags === MESSAGE_FLAGS.LAST_PACKET_WITH_SEQ;
|
|
628
|
+
|
|
629
|
+
// 从 header 之后开始解析(header 固定 4 bytes)
|
|
630
|
+
let offset = 4;
|
|
631
|
+
|
|
632
|
+
// 解析序列号(如果存在)
|
|
633
|
+
let sequence: number | undefined;
|
|
634
|
+
if (hasSequence) {
|
|
635
|
+
// 验证数据长度:至少需要 8 字节(header 4 + sequence 4)
|
|
636
|
+
if (data.length < offset + 4) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Invalid response: expected at least ${offset + 4} bytes for sequence, got ${data.length}`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
// 使用 readInt32BE 读取有符号整数(LAST_PACKET_WITH_SEQ 时 sequence 为负数)
|
|
642
|
+
sequence = data.readInt32BE(offset);
|
|
643
|
+
offset += 4;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 解析 payload size(紧跟在 header 或 sequence 之后)
|
|
647
|
+
if (data.length < offset + 4) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Invalid response: expected at least ${offset + 4} bytes for payload size, got ${data.length}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const payloadSize = data.readUInt32BE(offset);
|
|
653
|
+
offset += 4;
|
|
654
|
+
|
|
655
|
+
// 验证 payloadSize 的合理性(不超过 10MB,防止读取错误位置导致异常大的值)
|
|
656
|
+
const MAX_PAYLOAD_SIZE = 10 * 1024 * 1024; // 10MB
|
|
657
|
+
if (payloadSize > MAX_PAYLOAD_SIZE) {
|
|
658
|
+
this.logError('Invalid payload size', {
|
|
659
|
+
payloadSize,
|
|
660
|
+
dataLength: data.length,
|
|
661
|
+
sequence,
|
|
662
|
+
messageType,
|
|
663
|
+
specificFlags,
|
|
664
|
+
hasSequence,
|
|
665
|
+
firstBytes: data.slice(0, 16).toString('hex'),
|
|
666
|
+
});
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Invalid payload size: ${payloadSize} bytes (max: ${MAX_PAYLOAD_SIZE}). ` +
|
|
669
|
+
`This may indicate a parsing error or corrupted message.`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// 验证数据完整性:确保有足够的数据来读取 payload
|
|
674
|
+
const expectedLength = offset + payloadSize;
|
|
675
|
+
if (data.length < expectedLength) {
|
|
676
|
+
this.logWarn('Incomplete message detected', {
|
|
677
|
+
expectedLength,
|
|
678
|
+
actualLength: data.length,
|
|
679
|
+
payloadSize,
|
|
680
|
+
sequence,
|
|
681
|
+
specificFlags,
|
|
682
|
+
hasSequence,
|
|
683
|
+
firstBytes: data.slice(0, 16).toString('hex'),
|
|
684
|
+
});
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Incomplete message: expected ${expectedLength} bytes, got ${data.length}. ` +
|
|
687
|
+
`This may indicate a fragmented WebSocket message.`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const payload = data.slice(offset, offset + payloadSize);
|
|
692
|
+
|
|
693
|
+
// 解压 payload
|
|
694
|
+
let jsonBuffer: Buffer;
|
|
695
|
+
try {
|
|
696
|
+
if (compression === COMPRESSION.GZIP) {
|
|
697
|
+
jsonBuffer = await gunzipAsync(payload);
|
|
698
|
+
} else {
|
|
699
|
+
jsonBuffer = payload;
|
|
700
|
+
}
|
|
701
|
+
} catch (decompressError) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`Failed to decompress payload: ${(decompressError as Error).message}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 安全地解析 JSON
|
|
708
|
+
let result: any;
|
|
709
|
+
try {
|
|
710
|
+
const jsonString = jsonBuffer.toString('utf-8');
|
|
711
|
+
result = JSON.parse(jsonString);
|
|
712
|
+
} catch (parseError) {
|
|
713
|
+
// 提供更详细的错误信息,包括部分 JSON 内容
|
|
714
|
+
const partialJson = jsonBuffer.toString('utf-8').substring(0, 100);
|
|
715
|
+
throw new Error(
|
|
716
|
+
`Failed to parse JSON: ${(parseError as Error).message}. ` +
|
|
717
|
+
`Partial content: ${partialJson}...`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 提取音频时长信息
|
|
722
|
+
const audioDuration = result.audio_info?.duration;
|
|
723
|
+
|
|
724
|
+
// 提取识别结果
|
|
725
|
+
const text = result.result?.text || result.result?.[0]?.text || '';
|
|
726
|
+
const rawUtterances =
|
|
727
|
+
result.result?.utterances || result.result?.[0]?.utterances || [];
|
|
728
|
+
|
|
729
|
+
// 格式化 utterances(说话人分离数据)
|
|
730
|
+
const utterances: StreamingUtterance[] = rawUtterances.map(
|
|
731
|
+
(u: any, index: number) => {
|
|
732
|
+
// 解析分词信息
|
|
733
|
+
const rawWords = u.words || [];
|
|
734
|
+
const words: StreamingWord[] = rawWords.map((w: any) => ({
|
|
735
|
+
text: w.text || '',
|
|
736
|
+
startTime: w.start_time ?? w.startTime ?? 0,
|
|
737
|
+
endTime: w.end_time ?? w.endTime ?? 0,
|
|
738
|
+
blankDuration: w.blank_duration ?? w.blankDuration,
|
|
739
|
+
}));
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
speakerId:
|
|
743
|
+
u.additions?.speaker ||
|
|
744
|
+
u.speaker_id ||
|
|
745
|
+
u.speakerId ||
|
|
746
|
+
`speaker_${index}`,
|
|
747
|
+
text: u.text || u.content || '',
|
|
748
|
+
startTime: u.start_time ?? u.startTime ?? 0,
|
|
749
|
+
endTime: u.end_time ?? u.endTime ?? 0,
|
|
750
|
+
definite: u.definite ?? true,
|
|
751
|
+
speechRate: u.additions?.speech_rate ?? u.speech_rate ?? undefined,
|
|
752
|
+
volume: u.additions?.volume ?? u.volume ?? undefined,
|
|
753
|
+
emotion: u.additions?.emotion ?? u.emotion ?? undefined,
|
|
754
|
+
gender: u.additions?.gender ?? u.gender ?? undefined,
|
|
755
|
+
language: u.additions?.language ?? u.language ?? undefined,
|
|
756
|
+
words: words.length > 0 ? words : undefined,
|
|
757
|
+
};
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// 判断是否为最终结果
|
|
762
|
+
// specificFlags: 0b0010 (LAST_PACKET_NO_SEQ) 或 0b0011 (LAST_PACKET_WITH_SEQ) 表示最后一包
|
|
763
|
+
const isFinal =
|
|
764
|
+
specificFlags === MESSAGE_FLAGS.LAST_PACKET_NO_SEQ ||
|
|
765
|
+
specificFlags === MESSAGE_FLAGS.LAST_PACKET_WITH_SEQ;
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
text,
|
|
769
|
+
isFinal,
|
|
770
|
+
utterances,
|
|
771
|
+
sequence: sequence ?? 0, // 如果没有 sequence,返回 0
|
|
772
|
+
audioDuration,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 建立流式识别连接
|
|
778
|
+
*
|
|
779
|
+
* @description 建立 WebSocket 连接并发送初始配置请求
|
|
780
|
+
*
|
|
781
|
+
* @param params - 连接参数
|
|
782
|
+
* @param callbacks - 事件回调
|
|
783
|
+
* @returns 连接 ID
|
|
784
|
+
*/
|
|
785
|
+
async connect(
|
|
786
|
+
params: StreamingConnectParams,
|
|
787
|
+
callbacks: StreamingAsrCallbacks,
|
|
788
|
+
): Promise<string> {
|
|
789
|
+
const {
|
|
790
|
+
sessionId,
|
|
791
|
+
audioFormat = 'pcm',
|
|
792
|
+
sampleRate = 16000,
|
|
793
|
+
channels = 1,
|
|
794
|
+
enableSpeakerInfo = true,
|
|
795
|
+
// 新增参数
|
|
796
|
+
language,
|
|
797
|
+
enableItn = true,
|
|
798
|
+
enablePunc = true,
|
|
799
|
+
enableDdc = true,
|
|
800
|
+
enableNonstream = true,
|
|
801
|
+
showUtterances = true,
|
|
802
|
+
showSpeechRate = true,
|
|
803
|
+
showVolume = true,
|
|
804
|
+
enableLid = true,
|
|
805
|
+
enableEmotionDetection = true,
|
|
806
|
+
enableGenderDetection = true,
|
|
807
|
+
resultType = 'full',
|
|
808
|
+
enableAccelerateText = false,
|
|
809
|
+
accelerateScore,
|
|
810
|
+
vadSegmentDuration,
|
|
811
|
+
endWindowSize = 800,
|
|
812
|
+
forceToSpeechTime,
|
|
813
|
+
sensitiveWordsFilter,
|
|
814
|
+
corpus,
|
|
815
|
+
} = params;
|
|
816
|
+
|
|
817
|
+
// 生成连接 ID
|
|
818
|
+
const connectionId = uuidv4();
|
|
819
|
+
|
|
820
|
+
// 验证配置
|
|
821
|
+
if (!this.config.appId || !this.config.appAccessToken) {
|
|
822
|
+
throw new Error('Volcengine config missing appId or appAccessToken');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!this.config.uid) {
|
|
826
|
+
throw new Error('Volcengine config missing uid');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// 使用配置中的流式端点或默认端点
|
|
830
|
+
const endpoint = this.config.endpoint;
|
|
831
|
+
|
|
832
|
+
// 构建认证头
|
|
833
|
+
const headers = this.buildHeaders(connectionId);
|
|
834
|
+
|
|
835
|
+
this.logInfo('Creating WebSocket connection', {
|
|
836
|
+
sessionId,
|
|
837
|
+
connectionId,
|
|
838
|
+
endpoint,
|
|
839
|
+
audioFormat,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
return new Promise((resolve, reject) => {
|
|
843
|
+
// 创建 WebSocket 连接
|
|
844
|
+
const ws = new WebSocket(endpoint, { headers });
|
|
845
|
+
|
|
846
|
+
// 初始化连接信息
|
|
847
|
+
const connectionInfo: ConnectionInfo = {
|
|
848
|
+
ws,
|
|
849
|
+
status: 'connecting',
|
|
850
|
+
callbacks,
|
|
851
|
+
sessionId,
|
|
852
|
+
sequence: 0,
|
|
853
|
+
transcript: '',
|
|
854
|
+
utterances: [],
|
|
855
|
+
connectParams: params,
|
|
856
|
+
retryCount: 0,
|
|
857
|
+
lastActivityTime: Date.now(),
|
|
858
|
+
isReconnecting: false,
|
|
859
|
+
pendingAudioBuffer: [],
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
this.connections.set(connectionId, connectionInfo);
|
|
863
|
+
|
|
864
|
+
// 连接建立事件
|
|
865
|
+
ws.on('open', async () => {
|
|
866
|
+
try {
|
|
867
|
+
this.logInfo('WebSocket connection opened', {
|
|
868
|
+
connectionId,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// 发送初始请求(Full client request)
|
|
872
|
+
const initRequest = this.buildInitRequest({
|
|
873
|
+
audioFormat,
|
|
874
|
+
sampleRate,
|
|
875
|
+
channels,
|
|
876
|
+
enableSpeakerInfo,
|
|
877
|
+
language,
|
|
878
|
+
enableItn,
|
|
879
|
+
enablePunc,
|
|
880
|
+
enableDdc,
|
|
881
|
+
enableNonstream,
|
|
882
|
+
showUtterances,
|
|
883
|
+
showSpeechRate,
|
|
884
|
+
showVolume,
|
|
885
|
+
enableLid,
|
|
886
|
+
enableEmotionDetection,
|
|
887
|
+
enableGenderDetection,
|
|
888
|
+
resultType,
|
|
889
|
+
enableAccelerateText,
|
|
890
|
+
accelerateScore,
|
|
891
|
+
vadSegmentDuration,
|
|
892
|
+
endWindowSize,
|
|
893
|
+
forceToSpeechTime,
|
|
894
|
+
sensitiveWordsFilter,
|
|
895
|
+
corpus,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const message = await this.buildFullClientRequest(initRequest);
|
|
899
|
+
ws.send(message);
|
|
900
|
+
|
|
901
|
+
// 更新状态
|
|
902
|
+
connectionInfo.status = 'connected';
|
|
903
|
+
connectionInfo.retryCount = 0; // 重置重试计数
|
|
904
|
+
connectionInfo.lastActivityTime = Date.now();
|
|
905
|
+
|
|
906
|
+
// 启动心跳机制
|
|
907
|
+
this.startHeartbeat(connectionId);
|
|
908
|
+
|
|
909
|
+
callbacks.onConnected?.();
|
|
910
|
+
|
|
911
|
+
resolve(connectionId);
|
|
912
|
+
} catch (error) {
|
|
913
|
+
this.logError('Failed to send init request', {
|
|
914
|
+
connectionId,
|
|
915
|
+
error: (error as Error).message,
|
|
916
|
+
});
|
|
917
|
+
connectionInfo.status = 'error';
|
|
918
|
+
reject(error);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// 消息接收事件
|
|
923
|
+
ws.on('message', async (data: Buffer) => {
|
|
924
|
+
try {
|
|
925
|
+
// 更新最后活动时间
|
|
926
|
+
connectionInfo.lastActivityTime = Date.now();
|
|
927
|
+
|
|
928
|
+
// 重置心跳超时
|
|
929
|
+
this.resetHeartbeatTimeout(connectionId);
|
|
930
|
+
|
|
931
|
+
// 记录接收到的消息详细信息(用于调试协议解析问题)
|
|
932
|
+
if (data.length > 0) {
|
|
933
|
+
const messageType = data.length >= 2 ? (data[1] >> 4) & 0x0f : -1;
|
|
934
|
+
const specificFlags = data.length >= 2 ? data[1] & 0x0f : -1;
|
|
935
|
+
const compression = data.length >= 3 ? data[2] & 0x0f : -1;
|
|
936
|
+
|
|
937
|
+
this.logDebug('Received WebSocket message', {
|
|
938
|
+
connectionId,
|
|
939
|
+
messageSize: data.length,
|
|
940
|
+
messageType,
|
|
941
|
+
specificFlags,
|
|
942
|
+
compression,
|
|
943
|
+
firstBytes: data
|
|
944
|
+
.slice(0, Math.min(20, data.length))
|
|
945
|
+
.toString('hex'),
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const result = await this.parseServerResponse(data);
|
|
950
|
+
|
|
951
|
+
// 更新累积结果
|
|
952
|
+
if (result.text) {
|
|
953
|
+
connectionInfo.transcript = result.text;
|
|
954
|
+
}
|
|
955
|
+
if (result.utterances && result.utterances.length > 0) {
|
|
956
|
+
connectionInfo.utterances = result.utterances;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 触发回调
|
|
960
|
+
callbacks.onResult?.(result);
|
|
961
|
+
|
|
962
|
+
// 如果是最终结果,更新状态
|
|
963
|
+
if (result.isFinal) {
|
|
964
|
+
connectionInfo.status = 'completed';
|
|
965
|
+
// 完成后停止心跳
|
|
966
|
+
this.stopHeartbeat(connectionId);
|
|
967
|
+
} else {
|
|
968
|
+
connectionInfo.status = 'streaming';
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
this.logError('Failed to parse server response', {
|
|
972
|
+
connectionId,
|
|
973
|
+
error: (error as Error).message,
|
|
974
|
+
});
|
|
975
|
+
callbacks.onError?.(error as Error);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// 错误事件
|
|
980
|
+
ws.on('error', (error) => {
|
|
981
|
+
this.logError('WebSocket error', {
|
|
982
|
+
connectionId,
|
|
983
|
+
error: error.message,
|
|
984
|
+
});
|
|
985
|
+
connectionInfo.status = 'error';
|
|
986
|
+
callbacks.onError?.(error);
|
|
987
|
+
reject(error);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// 关闭事件
|
|
991
|
+
ws.on('close', async (code, reason) => {
|
|
992
|
+
this.logInfo('WebSocket connection closed', {
|
|
993
|
+
connectionId,
|
|
994
|
+
code,
|
|
995
|
+
reason: reason.toString(),
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// 停止心跳
|
|
999
|
+
this.stopHeartbeat(connectionId);
|
|
1000
|
+
|
|
1001
|
+
// 如果是异常关闭且未完成,尝试重连
|
|
1002
|
+
if (
|
|
1003
|
+
connectionInfo.status !== 'completed' &&
|
|
1004
|
+
connectionInfo.status !== 'disconnected' &&
|
|
1005
|
+
!connectionInfo.isReconnecting &&
|
|
1006
|
+
code !== 1000 // 正常关闭不重连
|
|
1007
|
+
) {
|
|
1008
|
+
await this.attemptReconnect(connectionId);
|
|
1009
|
+
} else if (connectionInfo.status !== 'completed') {
|
|
1010
|
+
connectionInfo.status = 'disconnected';
|
|
1011
|
+
callbacks.onDisconnected?.();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// 连接超时处理
|
|
1016
|
+
const timeout = setTimeout(() => {
|
|
1017
|
+
if (connectionInfo.status === 'connecting') {
|
|
1018
|
+
this.logError('WebSocket connection timeout', {
|
|
1019
|
+
connectionId,
|
|
1020
|
+
});
|
|
1021
|
+
ws.close();
|
|
1022
|
+
connectionInfo.status = 'error';
|
|
1023
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1024
|
+
}
|
|
1025
|
+
}, 30000); // 30 秒超时
|
|
1026
|
+
|
|
1027
|
+
// 连接成功后清除超时
|
|
1028
|
+
ws.on('open', () => {
|
|
1029
|
+
clearTimeout(timeout);
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* 计算音频时长(毫秒)
|
|
1036
|
+
*
|
|
1037
|
+
* @description 根据音频数据大小和采样参数计算时长
|
|
1038
|
+
* @param audioBytes - 音频数据字节数
|
|
1039
|
+
* @param sampleRate - 采样率(默认 16000)
|
|
1040
|
+
* @param channels - 声道数(默认 1)
|
|
1041
|
+
* @param bitsPerSample - 采样位深(默认 16)
|
|
1042
|
+
* @returns 音频时长(毫秒)
|
|
1043
|
+
*/
|
|
1044
|
+
private calculateAudioDurationMs(
|
|
1045
|
+
audioBytes: number,
|
|
1046
|
+
sampleRate: number = 16000,
|
|
1047
|
+
channels: number = 1,
|
|
1048
|
+
bitsPerSample: number = 16,
|
|
1049
|
+
): number {
|
|
1050
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
1051
|
+
const bytesPerSecond = sampleRate * channels * bytesPerSample;
|
|
1052
|
+
return (audioBytes / bytesPerSecond) * 1000;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* 发送音频数据
|
|
1057
|
+
*
|
|
1058
|
+
* @description 将音频数据分包发送到火山引擎服务
|
|
1059
|
+
* 文档建议单包音频大小 100-200ms,双向流式模式推荐 200ms
|
|
1060
|
+
*
|
|
1061
|
+
* @param connectionId - 连接 ID
|
|
1062
|
+
* @param audioData - 音频数据(Buffer)
|
|
1063
|
+
* @param isLast - 是否为最后一帧
|
|
1064
|
+
*/
|
|
1065
|
+
async sendAudio(
|
|
1066
|
+
connectionId: string,
|
|
1067
|
+
audioData: Buffer,
|
|
1068
|
+
isLast: boolean = false,
|
|
1069
|
+
): Promise<void> {
|
|
1070
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1071
|
+
|
|
1072
|
+
if (!connectionInfo) {
|
|
1073
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// 计算音频包时长并记录(仅对非空包进行检查)
|
|
1077
|
+
if (audioData.length > 0) {
|
|
1078
|
+
const { connectParams } = connectionInfo;
|
|
1079
|
+
const audioDurationMs = this.calculateAudioDurationMs(
|
|
1080
|
+
audioData.length,
|
|
1081
|
+
connectParams.sampleRate || 16000,
|
|
1082
|
+
connectParams.channels || 1,
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// 如果音频包时长超出推荐范围,记录调试日志
|
|
1086
|
+
if (
|
|
1087
|
+
audioDurationMs < RECOMMENDED_AUDIO_PACKET_MS.MIN ||
|
|
1088
|
+
audioDurationMs > RECOMMENDED_AUDIO_PACKET_MS.MAX * 2
|
|
1089
|
+
) {
|
|
1090
|
+
this.logDebug('Audio packet size outside recommended range', {
|
|
1091
|
+
connectionId,
|
|
1092
|
+
audioBytes: audioData.length,
|
|
1093
|
+
audioDurationMs: Math.round(audioDurationMs),
|
|
1094
|
+
recommendedMs: `${RECOMMENDED_AUDIO_PACKET_MS.MIN}-${RECOMMENDED_AUDIO_PACKET_MS.MAX}`,
|
|
1095
|
+
optimalMs: RECOMMENDED_AUDIO_PACKET_MS.OPTIMAL,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// 如果正在重连,将音频数据缓冲
|
|
1101
|
+
if (connectionInfo.isReconnecting) {
|
|
1102
|
+
this.bufferAudioDuringReconnect(connectionId, audioData);
|
|
1103
|
+
this.logInfo('Audio buffered during reconnect', {
|
|
1104
|
+
connectionId,
|
|
1105
|
+
bufferSize: connectionInfo.pendingAudioBuffer.length,
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Allow resuming from 'completed' or 'disconnected' status
|
|
1111
|
+
// This handles the case where:
|
|
1112
|
+
// 1. The ASR provider marked the connection as completed due to receiving isFinal=true
|
|
1113
|
+
// 2. The WebSocket connection was disconnected (e.g., page refresh, network issue)
|
|
1114
|
+
// In both cases, we try to reactivate the connection
|
|
1115
|
+
if (
|
|
1116
|
+
connectionInfo.status !== 'connected' &&
|
|
1117
|
+
connectionInfo.status !== 'streaming' &&
|
|
1118
|
+
connectionInfo.status !== 'completed' &&
|
|
1119
|
+
connectionInfo.status !== 'disconnected'
|
|
1120
|
+
) {
|
|
1121
|
+
throw new Error(`Invalid connection status: ${connectionInfo.status}`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// If status is 'completed' or 'disconnected', try to reactivate
|
|
1125
|
+
if (
|
|
1126
|
+
connectionInfo.status === 'completed' ||
|
|
1127
|
+
connectionInfo.status === 'disconnected'
|
|
1128
|
+
) {
|
|
1129
|
+
this.logInfo(`Reactivating ${connectionInfo.status} connection`, {
|
|
1130
|
+
connectionId,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// Check if WebSocket is still open
|
|
1134
|
+
if (connectionInfo.ws.readyState !== WebSocket.OPEN) {
|
|
1135
|
+
// WebSocket is closed, need to reconnect
|
|
1136
|
+
this.logWarn('WebSocket closed, attempting reconnect', {
|
|
1137
|
+
connectionId,
|
|
1138
|
+
readyState: connectionInfo.ws.readyState,
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// Try to reconnect
|
|
1142
|
+
try {
|
|
1143
|
+
await this.attemptReconnect(connectionId);
|
|
1144
|
+
// After reconnect, check if the connection is now active
|
|
1145
|
+
// Note: attemptReconnect updates connectionInfo in the Map
|
|
1146
|
+
const updatedInfo = this.connections.get(connectionId);
|
|
1147
|
+
if (
|
|
1148
|
+
!updatedInfo ||
|
|
1149
|
+
(updatedInfo.status !== 'connected' &&
|
|
1150
|
+
updatedInfo.status !== 'streaming')
|
|
1151
|
+
) {
|
|
1152
|
+
if (!isLast) {
|
|
1153
|
+
this.bufferAudioDuringReconnect(connectionId, audioData);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
throw new Error('Failed to reconnect WebSocket');
|
|
1157
|
+
}
|
|
1158
|
+
// Reconnect succeeded, continue with the updated connection
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
// Reconnect failed
|
|
1161
|
+
if (!isLast) {
|
|
1162
|
+
this.bufferAudioDuringReconnect(connectionId, audioData);
|
|
1163
|
+
this.logError('Reconnect failed, audio buffered', {
|
|
1164
|
+
connectionId,
|
|
1165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1166
|
+
});
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
// WebSocket is still open, just update status
|
|
1173
|
+
connectionInfo.status = 'streaming';
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const { ws } = connectionInfo;
|
|
1178
|
+
|
|
1179
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1180
|
+
// WebSocket 未打开,尝试缓冲数据
|
|
1181
|
+
if (!isLast) {
|
|
1182
|
+
this.bufferAudioDuringReconnect(connectionId, audioData);
|
|
1183
|
+
this.logWarn('WebSocket not open, audio buffered', {
|
|
1184
|
+
connectionId,
|
|
1185
|
+
readyState: ws.readyState,
|
|
1186
|
+
});
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
throw new Error('WebSocket is not open');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// 构建并发送音频数据包
|
|
1193
|
+
const message = await this.buildAudioOnlyRequest(audioData, isLast);
|
|
1194
|
+
ws.send(message);
|
|
1195
|
+
|
|
1196
|
+
// 更新状态和最后活动时间
|
|
1197
|
+
connectionInfo.status = 'streaming';
|
|
1198
|
+
connectionInfo.lastActivityTime = Date.now();
|
|
1199
|
+
|
|
1200
|
+
if (isLast) {
|
|
1201
|
+
this.logInfo('Sent last audio packet', { connectionId });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* 关闭连接
|
|
1207
|
+
*
|
|
1208
|
+
* @param connectionId - 连接 ID
|
|
1209
|
+
*/
|
|
1210
|
+
async disconnect(connectionId: string): Promise<void> {
|
|
1211
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1212
|
+
|
|
1213
|
+
if (!connectionInfo) {
|
|
1214
|
+
this.logWarn('Connection not found for disconnect', {
|
|
1215
|
+
connectionId,
|
|
1216
|
+
});
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// 停止心跳
|
|
1221
|
+
this.stopHeartbeat(connectionId);
|
|
1222
|
+
|
|
1223
|
+
// 标记为已断开,防止重连
|
|
1224
|
+
connectionInfo.status = 'disconnected';
|
|
1225
|
+
connectionInfo.isReconnecting = false;
|
|
1226
|
+
|
|
1227
|
+
const { ws } = connectionInfo;
|
|
1228
|
+
|
|
1229
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1230
|
+
ws.close(1000, 'Normal closure'); // 使用正常关闭码
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
this.connections.delete(connectionId);
|
|
1234
|
+
this.logInfo('Connection disconnected', { connectionId });
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* 获取连接状态
|
|
1239
|
+
*
|
|
1240
|
+
* @param connectionId - 连接 ID
|
|
1241
|
+
* @returns 连接状态
|
|
1242
|
+
*/
|
|
1243
|
+
getConnectionStatus(connectionId: string): StreamingAsrStatus {
|
|
1244
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1245
|
+
return connectionInfo?.status || 'disconnected';
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* 获取累积的转写结果
|
|
1250
|
+
*
|
|
1251
|
+
* @param connectionId - 连接 ID
|
|
1252
|
+
* @returns 转写结果
|
|
1253
|
+
*/
|
|
1254
|
+
getTranscript(connectionId: string): {
|
|
1255
|
+
transcript: string;
|
|
1256
|
+
utterances: StreamingUtterance[];
|
|
1257
|
+
} | null {
|
|
1258
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1259
|
+
if (!connectionInfo) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
transcript: connectionInfo.transcript,
|
|
1264
|
+
utterances: connectionInfo.utterances,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* 获取活跃连接数
|
|
1270
|
+
*
|
|
1271
|
+
* @returns 活跃连接数量
|
|
1272
|
+
*/
|
|
1273
|
+
getActiveConnectionCount(): number {
|
|
1274
|
+
return this.connections.size;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* 清理所有连接
|
|
1279
|
+
*
|
|
1280
|
+
* @description 关闭所有活跃的 WebSocket 连接
|
|
1281
|
+
*/
|
|
1282
|
+
async cleanupAllConnections(): Promise<void> {
|
|
1283
|
+
const connectionIds = Array.from(this.connections.keys());
|
|
1284
|
+
for (const connectionId of connectionIds) {
|
|
1285
|
+
await this.disconnect(connectionId);
|
|
1286
|
+
}
|
|
1287
|
+
this.logInfo('All connections cleaned up');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// =========================================================================
|
|
1291
|
+
// 心跳机制相关方法
|
|
1292
|
+
// =========================================================================
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* 启动心跳机制
|
|
1296
|
+
*
|
|
1297
|
+
* @description 定期发送心跳包以保持连接活跃
|
|
1298
|
+
* @param connectionId - 连接 ID
|
|
1299
|
+
*/
|
|
1300
|
+
private startHeartbeat(connectionId: string): void {
|
|
1301
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1302
|
+
if (!connectionInfo) return;
|
|
1303
|
+
|
|
1304
|
+
// 清除已有的心跳定时器
|
|
1305
|
+
this.stopHeartbeat(connectionId);
|
|
1306
|
+
|
|
1307
|
+
// 设置新的心跳定时器
|
|
1308
|
+
connectionInfo.heartbeatTimer = setInterval(() => {
|
|
1309
|
+
this.sendHeartbeat(connectionId);
|
|
1310
|
+
}, HEARTBEAT_CONFIG.interval);
|
|
1311
|
+
|
|
1312
|
+
this.logInfo('Heartbeat started', { connectionId });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* 停止心跳机制
|
|
1317
|
+
*
|
|
1318
|
+
* @param connectionId - 连接 ID
|
|
1319
|
+
*/
|
|
1320
|
+
private stopHeartbeat(connectionId: string): void {
|
|
1321
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1322
|
+
if (!connectionInfo) return;
|
|
1323
|
+
|
|
1324
|
+
if (connectionInfo.heartbeatTimer) {
|
|
1325
|
+
clearInterval(connectionInfo.heartbeatTimer);
|
|
1326
|
+
connectionInfo.heartbeatTimer = undefined;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (connectionInfo.heartbeatTimeoutTimer) {
|
|
1330
|
+
clearTimeout(connectionInfo.heartbeatTimeoutTimer);
|
|
1331
|
+
connectionInfo.heartbeatTimeoutTimer = undefined;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* 发送心跳包
|
|
1337
|
+
*
|
|
1338
|
+
* @description 发送一个空的音频包作为心跳
|
|
1339
|
+
* @param connectionId - 连接 ID
|
|
1340
|
+
*/
|
|
1341
|
+
private async sendHeartbeat(connectionId: string): Promise<void> {
|
|
1342
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1343
|
+
if (!connectionInfo) return;
|
|
1344
|
+
|
|
1345
|
+
const { ws, status } = connectionInfo;
|
|
1346
|
+
|
|
1347
|
+
if (status !== 'connected' && status !== 'streaming') {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1352
|
+
this.logWarn('Cannot send heartbeat: WebSocket not open', {
|
|
1353
|
+
connectionId,
|
|
1354
|
+
readyState: ws.readyState,
|
|
1355
|
+
});
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
try {
|
|
1360
|
+
// 发送空音频包作为心跳
|
|
1361
|
+
const emptyAudio = Buffer.alloc(0);
|
|
1362
|
+
const message = await this.buildAudioOnlyRequest(emptyAudio, false);
|
|
1363
|
+
ws.send(message);
|
|
1364
|
+
|
|
1365
|
+
// 设置心跳超时检测
|
|
1366
|
+
connectionInfo.heartbeatTimeoutTimer = setTimeout(() => {
|
|
1367
|
+
this.handleHeartbeatTimeout(connectionId);
|
|
1368
|
+
}, HEARTBEAT_CONFIG.timeout);
|
|
1369
|
+
|
|
1370
|
+
this.logInfo('Heartbeat sent', { connectionId });
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
this.logError('Failed to send heartbeat', {
|
|
1373
|
+
connectionId,
|
|
1374
|
+
error: (error as Error).message,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* 重置心跳超时
|
|
1381
|
+
*
|
|
1382
|
+
* @description 收到服务器响应时重置超时检测
|
|
1383
|
+
* @param connectionId - 连接 ID
|
|
1384
|
+
*/
|
|
1385
|
+
private resetHeartbeatTimeout(connectionId: string): void {
|
|
1386
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1387
|
+
if (!connectionInfo) return;
|
|
1388
|
+
|
|
1389
|
+
if (connectionInfo.heartbeatTimeoutTimer) {
|
|
1390
|
+
clearTimeout(connectionInfo.heartbeatTimeoutTimer);
|
|
1391
|
+
connectionInfo.heartbeatTimeoutTimer = undefined;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* 处理心跳超时
|
|
1397
|
+
*
|
|
1398
|
+
* @description 心跳超时表示连接可能已断开,触发重连
|
|
1399
|
+
* @param connectionId - 连接 ID
|
|
1400
|
+
*/
|
|
1401
|
+
private handleHeartbeatTimeout(connectionId: string): void {
|
|
1402
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1403
|
+
if (!connectionInfo) return;
|
|
1404
|
+
|
|
1405
|
+
this.logWarn('Heartbeat timeout, connection may be dead', {
|
|
1406
|
+
connectionId,
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// 标记状态为错误
|
|
1410
|
+
connectionInfo.status = 'error';
|
|
1411
|
+
|
|
1412
|
+
// 关闭当前连接并触发重连
|
|
1413
|
+
const { ws } = connectionInfo;
|
|
1414
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1415
|
+
ws.close(4000, 'Heartbeat timeout');
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// =========================================================================
|
|
1420
|
+
// 重连机制相关方法
|
|
1421
|
+
// =========================================================================
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* 尝试重新连接
|
|
1425
|
+
*
|
|
1426
|
+
* @description 使用指数退避策略进行重连
|
|
1427
|
+
* @param connectionId - 连接 ID
|
|
1428
|
+
*/
|
|
1429
|
+
private async attemptReconnect(connectionId: string): Promise<void> {
|
|
1430
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1431
|
+
if (!connectionInfo) return;
|
|
1432
|
+
|
|
1433
|
+
// 检查重试次数
|
|
1434
|
+
if (connectionInfo.retryCount >= this.reconnectConfig.maxRetries) {
|
|
1435
|
+
this.logError('Max reconnect attempts reached', {
|
|
1436
|
+
connectionId,
|
|
1437
|
+
retryCount: connectionInfo.retryCount,
|
|
1438
|
+
});
|
|
1439
|
+
connectionInfo.status = 'disconnected';
|
|
1440
|
+
connectionInfo.callbacks.onError?.(
|
|
1441
|
+
new Error('Max reconnect attempts reached'),
|
|
1442
|
+
);
|
|
1443
|
+
connectionInfo.callbacks.onDisconnected?.();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
connectionInfo.isReconnecting = true;
|
|
1448
|
+
connectionInfo.retryCount++;
|
|
1449
|
+
|
|
1450
|
+
// 计算延迟时间(指数退避)
|
|
1451
|
+
const delay = Math.min(
|
|
1452
|
+
this.reconnectConfig.initialDelay *
|
|
1453
|
+
Math.pow(
|
|
1454
|
+
this.reconnectConfig.backoffMultiplier,
|
|
1455
|
+
connectionInfo.retryCount - 1,
|
|
1456
|
+
),
|
|
1457
|
+
this.reconnectConfig.maxDelay,
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
this.logInfo('Attempting reconnect', {
|
|
1461
|
+
connectionId,
|
|
1462
|
+
retryCount: connectionInfo.retryCount,
|
|
1463
|
+
delayMs: delay,
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// 等待延迟后重连
|
|
1467
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1468
|
+
|
|
1469
|
+
// 检查是否仍需要重连
|
|
1470
|
+
if (
|
|
1471
|
+
connectionInfo.status === 'completed' ||
|
|
1472
|
+
connectionInfo.status === 'disconnected'
|
|
1473
|
+
) {
|
|
1474
|
+
this.logInfo('Reconnect cancelled: status changed', {
|
|
1475
|
+
connectionId,
|
|
1476
|
+
status: connectionInfo.status,
|
|
1477
|
+
});
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
try {
|
|
1482
|
+
// 创建新的 WebSocket 连接
|
|
1483
|
+
await this.reconnect(connectionId);
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
this.logError('Reconnect failed', {
|
|
1486
|
+
connectionId,
|
|
1487
|
+
error: (error as Error).message,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
// 递归重试
|
|
1491
|
+
await this.attemptReconnect(connectionId);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* 执行重连
|
|
1497
|
+
*
|
|
1498
|
+
* @description 创建新的 WebSocket 连接并恢复状态
|
|
1499
|
+
* @param connectionId - 连接 ID
|
|
1500
|
+
*/
|
|
1501
|
+
private async reconnect(connectionId: string): Promise<void> {
|
|
1502
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1503
|
+
if (!connectionInfo) {
|
|
1504
|
+
throw new Error('Connection info not found');
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const { connectParams, callbacks } = connectionInfo;
|
|
1508
|
+
|
|
1509
|
+
// 使用配置中的流式端点或默认端点
|
|
1510
|
+
const endpoint = this.config.endpoint;
|
|
1511
|
+
const headers = this.buildHeaders(connectionId);
|
|
1512
|
+
|
|
1513
|
+
return new Promise((resolve, reject) => {
|
|
1514
|
+
const ws = new WebSocket(endpoint, { headers });
|
|
1515
|
+
|
|
1516
|
+
// 连接建立事件
|
|
1517
|
+
ws.on('open', async () => {
|
|
1518
|
+
try {
|
|
1519
|
+
this.logInfo('Reconnected successfully', { connectionId });
|
|
1520
|
+
|
|
1521
|
+
// 发送初始请求(使用保存的连接参数)
|
|
1522
|
+
const initRequest = this.buildInitRequest({
|
|
1523
|
+
audioFormat: connectParams.audioFormat || 'pcm',
|
|
1524
|
+
sampleRate: connectParams.sampleRate || 16000,
|
|
1525
|
+
channels: connectParams.channels || 1,
|
|
1526
|
+
enableSpeakerInfo: connectParams.enableSpeakerInfo !== false,
|
|
1527
|
+
language: connectParams.language,
|
|
1528
|
+
enableItn: connectParams.enableItn ?? true,
|
|
1529
|
+
enablePunc: connectParams.enablePunc ?? true,
|
|
1530
|
+
enableDdc: connectParams.enableDdc ?? true,
|
|
1531
|
+
enableNonstream: connectParams.enableNonstream ?? true,
|
|
1532
|
+
showUtterances: connectParams.showUtterances ?? true,
|
|
1533
|
+
showSpeechRate: connectParams.showSpeechRate ?? true,
|
|
1534
|
+
showVolume: connectParams.showVolume ?? true,
|
|
1535
|
+
enableLid: connectParams.enableLid ?? true,
|
|
1536
|
+
enableEmotionDetection:
|
|
1537
|
+
connectParams.enableEmotionDetection ?? true,
|
|
1538
|
+
enableGenderDetection: connectParams.enableGenderDetection ?? true,
|
|
1539
|
+
resultType: connectParams.resultType || 'full',
|
|
1540
|
+
enableAccelerateText: connectParams.enableAccelerateText ?? false,
|
|
1541
|
+
accelerateScore: connectParams.accelerateScore,
|
|
1542
|
+
vadSegmentDuration: connectParams.vadSegmentDuration,
|
|
1543
|
+
endWindowSize: connectParams.endWindowSize ?? 800,
|
|
1544
|
+
forceToSpeechTime: connectParams.forceToSpeechTime,
|
|
1545
|
+
sensitiveWordsFilter: connectParams.sensitiveWordsFilter,
|
|
1546
|
+
corpus: connectParams.corpus,
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
const message = await this.buildFullClientRequest(initRequest);
|
|
1550
|
+
ws.send(message);
|
|
1551
|
+
|
|
1552
|
+
// 更新连接信息
|
|
1553
|
+
connectionInfo.ws = ws;
|
|
1554
|
+
connectionInfo.status = 'connected';
|
|
1555
|
+
connectionInfo.isReconnecting = false;
|
|
1556
|
+
connectionInfo.lastActivityTime = Date.now();
|
|
1557
|
+
|
|
1558
|
+
// 重新启动心跳
|
|
1559
|
+
this.startHeartbeat(connectionId);
|
|
1560
|
+
|
|
1561
|
+
// 发送缓冲的音频数据
|
|
1562
|
+
if (connectionInfo.pendingAudioBuffer.length > 0) {
|
|
1563
|
+
this.logInfo('Sending buffered audio data', {
|
|
1564
|
+
connectionId,
|
|
1565
|
+
bufferCount: connectionInfo.pendingAudioBuffer.length,
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
for (const buffer of connectionInfo.pendingAudioBuffer) {
|
|
1569
|
+
await this.sendAudio(connectionId, buffer, false);
|
|
1570
|
+
}
|
|
1571
|
+
connectionInfo.pendingAudioBuffer = [];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
callbacks.onConnected?.();
|
|
1575
|
+
resolve();
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
this.logError('Failed to initialize reconnected session', {
|
|
1578
|
+
connectionId,
|
|
1579
|
+
error: (error as Error).message,
|
|
1580
|
+
});
|
|
1581
|
+
reject(error);
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// 消息接收事件
|
|
1586
|
+
ws.on('message', async (data: Buffer) => {
|
|
1587
|
+
try {
|
|
1588
|
+
connectionInfo.lastActivityTime = Date.now();
|
|
1589
|
+
this.resetHeartbeatTimeout(connectionId);
|
|
1590
|
+
|
|
1591
|
+
const result = await this.parseServerResponse(data);
|
|
1592
|
+
|
|
1593
|
+
if (result.text) {
|
|
1594
|
+
connectionInfo.transcript = result.text;
|
|
1595
|
+
}
|
|
1596
|
+
if (result.utterances && result.utterances.length > 0) {
|
|
1597
|
+
connectionInfo.utterances = result.utterances;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
callbacks.onResult?.(result);
|
|
1601
|
+
|
|
1602
|
+
if (result.isFinal) {
|
|
1603
|
+
connectionInfo.status = 'completed';
|
|
1604
|
+
this.stopHeartbeat(connectionId);
|
|
1605
|
+
} else {
|
|
1606
|
+
connectionInfo.status = 'streaming';
|
|
1607
|
+
}
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
this.logError('Failed to parse server response', {
|
|
1610
|
+
connectionId,
|
|
1611
|
+
error: (error as Error).message,
|
|
1612
|
+
});
|
|
1613
|
+
callbacks.onError?.(error as Error);
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// 错误事件
|
|
1618
|
+
ws.on('error', (error) => {
|
|
1619
|
+
this.logError('Reconnect WebSocket error', {
|
|
1620
|
+
connectionId,
|
|
1621
|
+
error: error.message,
|
|
1622
|
+
});
|
|
1623
|
+
reject(error);
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// 关闭事件
|
|
1627
|
+
ws.on('close', async (code, reason) => {
|
|
1628
|
+
this.logInfo('Reconnected WebSocket closed', {
|
|
1629
|
+
connectionId,
|
|
1630
|
+
code,
|
|
1631
|
+
reason: reason.toString(),
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
this.stopHeartbeat(connectionId);
|
|
1635
|
+
|
|
1636
|
+
if (
|
|
1637
|
+
connectionInfo.status !== 'completed' &&
|
|
1638
|
+
connectionInfo.status !== 'disconnected' &&
|
|
1639
|
+
code !== 1000
|
|
1640
|
+
) {
|
|
1641
|
+
await this.attemptReconnect(connectionId);
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// 连接超时
|
|
1646
|
+
const timeout = setTimeout(() => {
|
|
1647
|
+
this.logError('Reconnect timeout', { connectionId });
|
|
1648
|
+
ws.close();
|
|
1649
|
+
reject(new Error('Reconnect timeout'));
|
|
1650
|
+
}, 15000);
|
|
1651
|
+
|
|
1652
|
+
ws.on('open', () => {
|
|
1653
|
+
clearTimeout(timeout);
|
|
1654
|
+
});
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* 缓冲音频数据(用于重连期间)
|
|
1660
|
+
*
|
|
1661
|
+
* @description 在重连期间缓冲音频数据,重连成功后发送
|
|
1662
|
+
* @param connectionId - 连接 ID
|
|
1663
|
+
* @param audioData - 音频数据
|
|
1664
|
+
*/
|
|
1665
|
+
bufferAudioDuringReconnect(connectionId: string, audioData: Buffer): void {
|
|
1666
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1667
|
+
if (!connectionInfo) return;
|
|
1668
|
+
|
|
1669
|
+
// 限制缓冲区大小(最多 100 个包)
|
|
1670
|
+
if (connectionInfo.pendingAudioBuffer.length < 100) {
|
|
1671
|
+
connectionInfo.pendingAudioBuffer.push(audioData);
|
|
1672
|
+
} else {
|
|
1673
|
+
this.logWarn('Audio buffer full during reconnect', {
|
|
1674
|
+
connectionId,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* 检查连接是否正在重连
|
|
1681
|
+
*
|
|
1682
|
+
* @param connectionId - 连接 ID
|
|
1683
|
+
* @returns 是否正在重连
|
|
1684
|
+
*/
|
|
1685
|
+
isReconnecting(connectionId: string): boolean {
|
|
1686
|
+
const connectionInfo = this.connections.get(connectionId);
|
|
1687
|
+
return connectionInfo?.isReconnecting ?? false;
|
|
1688
|
+
}
|
|
1689
|
+
}
|