refactor: 移除自签名证书方案,服务只跑 HTTP

麦克风安全上下文改由部署方案解决:本机用 localhost,
对外部署挂 HTTPS 反向代理。删除 certs/、gen-cert.sh、
/cert 路由与 HTTPS server 分支,文档同步更新。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 19:28:59 +08:00
parent 9be38aa44e
commit 5f7f68150e
11 changed files with 41 additions and 177 deletions

3
.gitignore vendored
View File

@@ -1,8 +1,5 @@
# whisper 模型(体积大,用 bootstrap.sh 自动下载)
models/
# 自签证书(每台机器用 gen-cert.sh 生成)
certs/
# 系统文件
.DS_Store

View File

@@ -11,15 +11,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands
```bash
# 新电脑一键部署(幂等:装依赖 → 补模型 → 生成证书 → 启动,见 DEPLOY.md
# 新电脑一键部署(幂等:装依赖 → 补模型 → 启动,见 DEPLOY.md
bash bootstrap.sh
# 启动HTTP:8000 + HTTPS:8443首次自动生成自签证书
# 启动HTTP:8000;麦克风需安全上下文:本机 localhost 或部署时挂 HTTPS 反向代理
bash start.sh # 或 node server.js
# 换电脑 / IP 变了后重新生成自签名证书
bash gen-cert.sh
# 语法检查(无测试框架,改完用这个自查)
node -e "new (require('vm').Script)(require('fs').readFileSync('server.js','utf8'));console.log('server ok')"
node -e "const m=require('fs').readFileSync('index.html','utf8').match(/<script>([\s\S]*?)<\/script>/)[1];new (require('vm').Script)(m);console.log('frontend ok')"
@@ -50,9 +47,9 @@ VOICES=Tingting,Shelley node eval.js # 多音色
## Architecture
### 后端 `server.js`(纯 Node `http`/`https`,无框架无依赖)
- 同一`handler(req,res)` 同时挂在 HTTP(8000) 和 HTTPS(8443) 上。HTTPS 用 `certs/` 里的自签证书,缺证书则只起 HTTP
- 路由:`GET /`index.html`GET /cert`iOS 下载证书)、`GET /health``POST /stt`
### 后端 `server.js`(纯 Node `http`,无框架无依赖)
- `handler(req,res)` 挂在 HTTP(8000)。不做 HTTPS——本机用 `localhost`(安全上下文),对外部署挂 HTTPS 反向代理
- 路由:`GET /`index.html`GET /health``POST /stt`
- `POST /stt?a=&b=` 流水线(`transcribe()`):收原始 body Buffer → 按 `Content-Type` 定扩展名写临时文件 → `ffmpeg` 转 wav前后各补 0.5s 静音,短音节必需)→ `whisper-cli -l zh` 语法约束解码输出 txt → 去标点/空白 → 返回 `{text}`。**前端只拿 `text`,判分在前端做**,后端不碰口诀逻辑。
- **grammar 约束解码是识别准确率的命根子**:带 `a`/`b` 参数时用 `dynGrammar()` 按题生成 grammar前缀锁死为本题因数、得数全开放——只约束前缀不会把答错扶正否则退回静态 `grammar/number.gbnf`;没有 grammar 文件才用 `--prompt` 偏置。grammar 只允许中文数字输出——阿拉伯数字会把「十四/七十四」压扁成 14/74 丢掉「十」。grammar 另含发音兼容 `alias` 规则(牛/流/柳/溜/两/酒):不给别名时方言音会被硬扭到错误数字(如 niú 声学上更近 jiǔ「九」给别名让解码器输出真实音、前端 `DIG` 映射回数字。
@@ -71,6 +68,5 @@ VOICES=Tingting,Shelley node eval.js # 多音色
## 平台约束(重要,别踩坑)
- **iOS 语音必须 HTTPS + 受信任证书**。iOS 上所有浏览器都是 WebKit`http://IP` 下苹果直接禁麦克风、不弹授权;且浏览器自带的 `SpeechRecognition` 在 iOS 基本不可用 —— 这正是走「录音上传后端识别」而非浏览器识别的原因。iOS 首次需下载 `/cert` 并在「设置 → 通用 → 证书信任设置」里开完全信任,详见 `README.md`
- **音频/麦克风/TTS 必须在用户手势内解锁**`ensureAudio()` 在首页点「开始」(`startGame`)时调用一次 `getUserMedia` + `AudioContext.resume()` + 静音 TTS之后才能全自动。
- 证书 SAN 里写死了本机 LAN IPIP 变了必须 `gen-cert.sh` 重新生成。
- **麦克风只在安全上下文可用**(浏览器硬限制):本机开发/运行用 `http://localhost:8000`;对外部署把服务挂在支持 HTTPS 的反向代理后面(项目自身不做 TLS浏览器自带的 `SpeechRecognition` 兼容性差——这正是走「录音上传后端识别」而非浏览器识别的原因。
- **音频/麦克风/TTS 必须在用户手势内解锁**`ensureAudio()` 在首页点「开始」(`startGame`/`enterMatch`)时调用一次 `getUserMedia` + `AudioContext.resume()` + 静音 TTS之后才能全自动。

View File

@@ -11,11 +11,11 @@
|---|---|---|
| **拷贝的文件** | 整个 `calx/` 目录 | 见下方清单。`models/`(模型)建议一起拷,省下载 |
| **要装的软件** | Node.js、`ffmpeg``whisper-cpp` | 前者官网/brew后两者 `brew install` |
| **不用带** | `node_modules`(没有)`certs/`(新机自动生成) | 证书含本机 IP换机必须重新生成 |
| **不用带** | `node_modules`(没有) | 真·零依赖 |
必须一起迁移的文件清单:
```
index.html server.js bootstrap.sh start.sh gen-cert.sh
index.html server.js bootstrap.sh start.sh grammar/
models/ggml-small.bin (465MB必需)
models/ggml-base.bin (141MB可选更快)
README.md DEPLOY.md CLAUDE.md
@@ -32,15 +32,9 @@ cd calx
bash bootstrap.sh
```
脚本会自动:检查 Node → 装 `ffmpeg`/`whisper-cpp` → 补下模型(若缺)→ 生成自签证书 → 启动服务。
脚本会自动:检查 Node → 装 `ffmpeg`/`whisper-cpp` → 补下模型(若缺)→ 启动服务。
可反复运行,已装的会跳过。
启动后终端打印:
```
HTTP : http://localhost:8000 http://<本机IP>:8000
HTTPS: https://<本机IP>:8443
```
---
## 方式 B手动部署逐步
@@ -54,29 +48,23 @@ mkdir -p models && cd models
curl -L -o ggml-small.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin
cd ..
# 3. 生成证书 + 启动
bash gen-cert.sh
# 3. 启动
node server.js
```
---
## 各端怎么访问
## 怎么访问
### 💻 电脑本机
Chrome / Edge / Safari 打开 `http://localhost:8000` —— 麦克风直接可用,点「开始」授权即可。
### 💻 本机运行
Chrome / Edge / Safari 打开 `http://localhost:8000` —— 麦克风直接可用localhost 是浏览器认可的安全上下文),点「开始」授权即可。
### 📱 iPhone / iPad要用语音 → 必须 HTTPS + 信任证书,一次性
> iOS 只有在「HTTPS + 受信任证书」下才允许网页用麦克风,这是苹果硬限制。用 **Safari** 完成
### 🌐 对外部署(其它设备访问
浏览器只在**安全上下文**HTTPS 或 localhost里放行麦克风。把服务挂在支持 HTTPS 的反向代理nginx / Caddy / 云平台均可)后面,其余零改动
1. 手机与电脑连**同一 Wi-Fi**(或你的远程方案能到 `8443` 端口)。
2. Safari 打开 `https://<电脑IP>:8443/cert` → 下载描述文件(提示不受信任,继续)。
3. **设置 → 通用 → VPN 与设备管理** → 安装 `calx-local`
4. **设置 → 通用 → 关于本机 → 证书信任设置** → 打开对 `calx-local` 的**完全信任**。
5. Safari 打开 `https://<电脑IP>:8443/` → 点「开始」→ **允许麦克风** → 全程免手。
### 🤖 安卓 / 其它
Chrome 打开 `https://<电脑IP>:8443/`,首次会警告证书不受信 → 「高级 → 继续前往」→ 允许麦克风。
```
浏览器 ──HTTPS──> 反向代理 ──HTTP──> node server.js (:8000)
```
---
@@ -87,13 +75,10 @@ Chrome 打开 `https://<电脑IP>:8443/`,首次会警告证书不受信 →
WHISPER_MODEL=models/ggml-base.bin node server.js
# 改端口
PORT=8000 HTTPS_PORT=8443 node server.js
# 电脑 IP 变了(换网络) → 证书失效,重新生成,手机重做信任步骤
bash gen-cert.sh
PORT=8000 node server.js
```
前端玩法参数(读题开关 / 限时秒数 / 随机题数 / 错题加权 / 静音检测灵敏度)在 `index.html` 顶部 `CONFIG`,或游戏首页「⚙️ 家长设置」里改常用项。
前端玩法参数(读题开关 / 限时秒数 / 口诀范围 / 随机题数 / 错题加权 / 抢答参数 / 静音检测灵敏度)在 `index.html` 顶部 `CONFIG`,或游戏首页「⚙️ 家长设置」里改常用项。
---
@@ -101,8 +86,7 @@ bash gen-cert.sh
| 现象 | 原因 / 处理 |
|---|---|
| 点「开始」不弹麦克风授权 | 不是 HTTPS或 iOS 证书没「完全信任」→ 重做 iOS 第 2~4 步 |
| iOS 证书装了仍拦麦克风 | 证书 SAN 的 IP 与访问 IP 不一致 → `bash gen-cert.sh` 后重装证书 |
| 点「开始」不弹麦克风授权 | 不是安全上下文:本机用 `http://localhost:8000`;对外部署走 HTTPS 反向代理 |
| 识别没反应 / 不准 | 看服务端日志有无 `[stt] … -> "xxx"``/health``modelExists` 应为 true环境别太吵、吐字清楚 |
| `whisper-cli: command not found` | `brew install whisper-cpp` |
| `ffmpeg: command not found` | `brew install ffmpeg` |
@@ -111,7 +95,6 @@ bash gen-cert.sh
## 验证部署是否成功
```bash
curl -s http://localhost:8000/health # {"ok":true,...,"modelExists":true}
curl -sk https://localhost:8443/health # 同上(自签名用 -k
curl -s http://localhost:8000/health # {"ok":true,...,"modelExists":true}
```
`modelExists:true` + 两个端口都返回 = 部署成功。
`modelExists:true` = 部署成功。

View File

@@ -1,7 +1,7 @@
# 🦉 乘法口诀大闯关
小学生乘法口诀语音背诵小程序:**自动读题 → 小朋友开口说答案 → 离线语音识别判分**,全程免手。
支持顺序闯关 / 随机挑战(错题动态加权,练熟自动减少),限时作答、正/负反馈动效音效。
支持顺序闯关 / 随机挑战(错题动态加权,练熟自动减少)/ 双人抢答,限时作答、正/负反馈动效音效。
语音识别用 **whisper.cpp 本地离线模型**,不联网、不花钱、不上传任何数据到云端。
@@ -14,14 +14,14 @@ calx/
├── index.html # 前端(单页面)
├── server.js # 后端(零 npm 依赖,纯 Node
├── start.sh # 一键启动
├── gen-cert.sh # 生成自签名 HTTPS 证书
├── models/
│ ├── ggml-small.bin # 中文识别模型默认较准465MB
── ggml-base.bin # 备用模型较快141MB
└── certs/ # HTTPS 证书(换电脑/换 IP 后重新生成
├── bootstrap.sh # 新机一键部署
├── grammar/ # whisper 解码语法约束
└── models/
── ggml-small.bin # 中文识别模型默认较准465MB
└── ggml-base.bin # 备用模型较快141MB
```
`certs/` `node_modules` 不用拷(没有 node_modules`models/` 必须一起拷,或到新机器重新下载(见下)。
没有 `node_modules``models/` 建议一起拷,或到新机器重新下载(见下)。
## 二、新电脑需要的运行环境
@@ -39,46 +39,28 @@ curl -L -o ggml-base.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/m
cd ..
```
## 三、启动
## 三、启动与访问
```bash
bash start.sh
# 或: node server.js
```
启动后终端会打印:
```
HTTP : http://localhost:8000 http://<你的IP>:8000
HTTPS: https://<你的IP>:8443
```
- **本机运行**:浏览器打开 `http://localhost:8000`麦克风直接可用localhost 是浏览器认可的安全上下文)。
- **对外部署**:浏览器只在安全上下文里放行麦克风,把服务挂在支持 **HTTPS 的反向代理**(如 nginx / Caddy / 云平台)后面即可,其余零改动。
- **电脑本机浏览器**Chrome/Edge/Safari直接开 `http://localhost:8000`,麦克风可用。
- **iPhone / iPad**:必须用 HTTPS见下一节。
## 四、参数配置
## 四、iPhone / iPad 用语音(一次性设置)
iOS 只有在「HTTPS + 受信任证书」下才允许网页用麦克风。步骤(用 **Safari**
1. 手机和电脑连**同一 Wi-Fi**(或你自己的远程方式能到 `8443` 端口)。
2. Safari 打开 `https://<电脑IP>:8443/cert` → 提示下载描述文件 → 允许。
3. **设置 → 通用 → VPN 与设备管理** → 安装刚下载的 `calx-local` 描述文件。
4. **设置 → 通用 → 关于本机 → 证书信任设置** → 打开对 `calx-local` 的**完全信任**。
5. Safari 打开 `https://<电脑IP>:8443/` → 点“开始” → **允许麦克风** → 之后听题、答题全程免手。
> 换电脑或电脑 IP 变了:重新 `bash gen-cert.sh` 生成证书,手机重做第 2~4 步。
## 五、参数配置
- 前端:`index.html` 顶部 `CONFIG`(读题开关、限时秒数、随机题数、错题加权、静音检测灵敏度等),或首页「⚙️ 家长设置」里直接改常用项。
- 前端:`index.html` 顶部 `CONFIG`(读题开关、限时秒数、口诀范围、随机题数、错题加权、抢答参数、静音检测灵敏度等),或首页「⚙️ 家长设置」里直接改常用项。
- 后端模型:默认 `ggml-small.bin`。想更快可切 base
```bash
WHISPER_MODEL=models/ggml-base.bin node server.js
```
- 端口:`PORT=8000 HTTPS_PORT=8443`(环境变量可改)。
- 端口:环境变量 `PORT`(默认 8000)。
## 、常见问题
## 、常见问题
- **点“开始”没弹麦克风授权**多半不是 HTTPS 或证书没「完全信任」→ 重做第四节
- **点“开始”没弹麦克风授权**不是安全上下文——本机请用 `http://localhost:8000`,对外部署须走 HTTPS 反向代理
- **识别不准 / 没反应**:确认终端有 `[stt] ... -> "xxx"` 日志;模型加载 `✅`;小朋友吐字清楚、环境别太吵。
- **iOS 上没声音读题**iOS 要求首次交互才解锁语音,点过“开始”后即正常。
- **完全不想用语音**:点游戏里「⌨️ 用手写」,或环境不支持时会自动切数字键盘。

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# ============================================================
# 乘法口诀 —— 新电脑一键部署脚本macOS
# 作用:检查/安装依赖(ffmpeg + whisper-cpp) → 补齐模型 → 生成证书 → 启动服务
# 作用:检查/安装依赖(ffmpeg + whisper-cpp) → 补齐模型 → 启动服务
# 幂等:已装的会跳过,可反复运行
# ============================================================
set -e
@@ -41,14 +41,6 @@ else
echo "✅ 模型 ggml-small.bin 已存在"
fi
# 5) 自签名证书iOS 语音必需)
if [ ! -f certs/cert.pem ]; then
echo "🔐 生成自签名 HTTPS 证书…"
bash gen-cert.sh
else
echo "✅ 证书已存在IP 变了请手动跑 bash gen-cert.sh 重生成)"
fi
echo ""
echo "==== 部署完成,启动服务 ===="
exec node server.js

Binary file not shown.

View File

@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDLTCCAhWgAwIBAgIUU3wQsQyuE+9iWOTikINHiQzObmcwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAwwKY2FseC1sb2NhbDAeFw0yNjA3MTgwNzE2MjVaFw0yODEw
MjAwNzE2MjVaMBUxEzARBgNVBAMMCmNhbHgtbG9jYWwwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCrpEVDdf0hl75+SvVvtvV+Qfa/0o3675VaHOe1QBUE
el4max9hDCptebApt52uHu15rvm8ZO0wJOX/0MJHYDCl3C9clFtA/QRpMbPv7A+J
5zVeBVSAU/VWPBQYmaS62Klm5FFchwNBvPXmv07iJ3aaOBqu4h/5a+tcAEACvmwg
mubF3jBVOLsBDi8rlLO2wVaQ98Xyq+vsAQnbgq8nsrznw7VVI05+TEXLMus3KAnU
1aS0GnMDVzRUOqsOlBn7zNiVUcxHJD6Rt9Q0WaVJ+cOUixIyPyI9W5lYSIXZAcsN
4jt+CZf+r4/iAfVWINzp9aUHgubOhvqlNHGU9BPKgTmrAgMBAAGjdTBzMB0GA1Ud
DgQWBBSHKrKKHSaZaJTKnqsqkuwdNQMpRTAfBgNVHSMEGDAWgBSHKrKKHSaZaJTK
nqsqkuwdNQMpRTAPBgNVHRMBAf8EBTADAQH/MCAGA1UdEQQZMBeHBMCoCbmHBH8A
AAGCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAmFTaG80LZK1fkIt6tYde
JyY7+dM/dLOEd5i/0ZB8b/oybmJ5xszqd4obQwHO/X8UdQOazjhtZM1DEmp2BM99
w+pPHB6X7or+GXsvh41LoOdbzXMVeCQbjZYbMWJ+Z0PfV55jF55Rsyd0RgQwdEYW
vQxTfB1ka2+6BhrSm/cJMDpZynDZsLBMIAl+N/r2aviD4mJ1axkHqoBrUDP3Uz2O
tgMoGOKgc3HH6jDFFi99ITo4WBtmPGeprFIn8czVkmZCsyz57bC77nbL2IGC5pbr
xe7SV3pgY4KM0kuV5GnjzzjORXCQDbEgIVvt2pZ8mAs4KA2dXtHLefMfQT8dIhYv
xw==
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrpEVDdf0hl75+
SvVvtvV+Qfa/0o3675VaHOe1QBUEel4max9hDCptebApt52uHu15rvm8ZO0wJOX/
0MJHYDCl3C9clFtA/QRpMbPv7A+J5zVeBVSAU/VWPBQYmaS62Klm5FFchwNBvPXm
v07iJ3aaOBqu4h/5a+tcAEACvmwgmubF3jBVOLsBDi8rlLO2wVaQ98Xyq+vsAQnb
gq8nsrznw7VVI05+TEXLMus3KAnU1aS0GnMDVzRUOqsOlBn7zNiVUcxHJD6Rt9Q0
WaVJ+cOUixIyPyI9W5lYSIXZAcsN4jt+CZf+r4/iAfVWINzp9aUHgubOhvqlNHGU
9BPKgTmrAgMBAAECggEAHaT+jXs4X+/sZ7WclFHeLpNIAbNk8D0m52xsnIJX3vEf
h7Uis0k9nrDQuzPKWQNgco7E9UPdL16AFuHAXEGBxXO0/mFlLWFFvMFn2X1E5cV4
EygKgyt9CQrRUbpfmCHJiErQvpFW/6iaM+NAxZCZTGN6QFAvw41fFgG5DIBE/pBZ
NALrPxiWORTY1wnqktEZYa97QKv4NRPCDHnOwS7R5ILps79a0qv6IiKQA8ztC4EI
AOA7J9i2G6SZz7wbCfc+8Ndw3Z0a7Y7OMrfuPxHvYlJoZS70o3Et1TFeuKo0FDuV
6NdAnQu4UBRMU1i1ggqLFbsM4StJOzI2jeIZmsjFAQKBgQDydUme1jKcxLGPZT2T
nvcIPyJm4FjUNNbv8VqW/V6YtFlKt0rEvoGDKQbV0pkcf4MkXPcmDCJbB411g2MR
AhThsKVWL0bpYUdB6SX3xEPkKc4r/cHbqXVPSl38Dgqw4qHHaH02qlXBy+RNuwMv
uOKvk68pH30EXzzl9RCZhpLLgQKBgQC1Om+J61EtGkEIid5QonvvIViYfgVcfgMy
igQD7GnUSb4vNbd1mNPm4sAKHxSp+Hp59uupmjtLr/5ksTZa6ibbXdiX53cXqAsp
vzNidP4T+QCkoB1t958QCKRQnd50pijFOUeAa8BuoBCSsYF86Q+WBpt6pHkdXe5H
NNnU8FqLKwKBgAn4ZsGgX05xDDgwWpric5gkOH7whNadIaWWUDpbSvOOnwlihjGW
/+5r1nsegOtaFwN8yzEGWLdTlS6a64z8f+1T/UiRAyJ/vJVqSARrTrtoOLE4aytk
SY2sKjc+QJRLjYmMs3F3CbXBc676nMPun2vG7/IJ+HMgk3mFXVxi21CBAoGAbLmw
LRhGTT7RAWRlQg8ZN2/UDTid0s2u8WUn34q4Oyo6tWwOsriLeJtVBz16UjbGk+F1
pPzo0hG2F4bRy11rDnSQ+yFcq46fk94n38u8mxLqNdnxjgXfIeltWwg7a9Q75Ybl
xL/gZP4Ws0tXGi9gsGsqYDvIhbvStlmYzk2cpJ0CgYAoxb239sleOrqvglo+wogt
JK3K74i358BITd1NsB+XsuphQYaYuLc17KLoiGIqyBv6gwfetcn9U+2vmXF1vnwy
dLqwhi0ryf0xxrNxOpow1814UK4kuN0uHwhE4Xj+UmdcpjkMyud+jP7qqGbzvfF1
ZD4ohLo4O1/U7yLoR3bO5Q==
-----END PRIVATE KEY-----

View File

@@ -1,12 +0,0 @@
#!/bin/bash
# 生成/更新自签名 HTTPS 证书IP 变了就重新跑一次)
cd "$(dirname "$0")"
mkdir -p certs
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo 127.0.0.1)
echo "为本机 IP = $IP 生成自签名证书..."
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout certs/key.pem -out certs/cert.pem -days 825 \
-subj "/CN=calx-local" \
-addext "subjectAltName=IP:${IP},IP:127.0.0.1,DNS:localhost"
openssl x509 -in certs/cert.pem -outform der -out certs/calx-cert.cer
echo "完成。iOS 请访问 https://${IP}:8443/cert 下载并信任证书。"

View File

@@ -8,7 +8,6 @@
* 模型:./models/ggml-small.bin默认较准或 ggml-base.bin较快用 WHISPER_MODEL 切换)
*/
const http = require('http');
const https = require('https');
const fs = require('fs');
const os = require('os');
const path = require('path');
@@ -17,7 +16,6 @@ const crypto = require('crypto');
const ROOT = __dirname;
const PORT = process.env.PORT || 8000;
const HTTPS_PORT = process.env.HTTPS_PORT || 8443;
const THREADS = String(Math.min(8, os.cpus().length || 4));
const WHISPER = process.env.WHISPER_BIN || 'whisper-cli';
const FFMPEG = process.env.FFMPEG_BIN || 'ffmpeg';
@@ -126,15 +124,6 @@ function handler(req, res) {
});
return;
}
// ---- iOS 下载证书(浏览器打开会提示安装描述文件) ----
if (req.method === 'GET' && url.pathname === '/cert') {
fs.readFile(path.join(ROOT, 'certs', 'calx-cert.cer'), (e, data) => {
if (e) { res.writeHead(404); res.end('cert not found'); return; }
res.writeHead(200, { 'Content-Type': 'application/x-x509-ca-cert', 'Content-Disposition': 'attachment; filename="calx-cert.cer"' });
res.end(data);
});
return;
}
if (req.method === 'GET' && url.pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, model: path.basename(MODEL), modelExists: fs.existsSync(MODEL) }));
@@ -170,18 +159,5 @@ http.createServer(handler).listen(PORT, '0.0.0.0', () => {
console.log(`\n乘法口诀服务已启动`);
console.log(` HTTP : http://localhost:${PORT} http://${IP}:${PORT}`);
console.log(`识别引擎: ${WHISPER} 模型: ${path.basename(MODEL)} ${fs.existsSync(MODEL) ? '✅' : '❌ 模型不存在'} 线程: ${THREADS} 语法约束: ${HAS_GRAMMAR ? '✅' : '❌(用prompt)'}`);
console.log(`提示: 浏览器麦克风需要安全上下文——本机用 http://localhost:${PORT};对外部署请挂在支持 HTTPS 的反向代理后面`);
});
// ---- HTTPSiOS 用麦克风必需)----
try {
const key = path.join(ROOT, 'certs', 'key.pem');
const cert = path.join(ROOT, 'certs', 'cert.pem');
if (fs.existsSync(key) && fs.existsSync(cert)) {
https.createServer({ key: fs.readFileSync(key), cert: fs.readFileSync(cert) }, handler)
.listen(HTTPS_PORT, '0.0.0.0', () => {
console.log(` HTTPS: https://${IP}:${HTTPS_PORT} (iOS 用这个,先访问 https://${IP}:${HTTPS_PORT}/cert 安装并信任证书)`);
});
} else {
console.log(' HTTPS: 未启用(缺少 certs/key.pem 或 certs/cert.pem');
}
} catch (e) { console.log(' HTTPS 启动失败:', e.message); }

View File

@@ -1,6 +1,4 @@
#!/bin/bash
# 一键启动乘法口诀服务
cd "$(dirname "$0")"
# 没有证书就先生成
[ -f certs/cert.pem ] || bash gen-cert.sh
exec node server.js