Compare commits

..
Author SHA1 Message Date
1d393f4f1e fix(mobile): 让华为 AGConnect 在 AGP 9 下能识别真实配置
放入 agconnect-services.json 后原插件坐标无法解析,且 1.9.1 不兼容 AGP 9。
映射到 agcp 1.9.5.301,Gradle 可按包名校验;该文件继续忽略不入库。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-31 00:56:12 +08:00
a71f6a0ce1 fix(deploy): 公网 overlay 用 !override 覆盖内部端口绑定
Compose 合并后内部端口只绑 127.0.0.1,避免与基础 0.0.0.0 叠加导致起不来。
顺带给 JPUSH_PUSH_INTENT 加引号,并拒绝空 SERVER_IP。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-30 21:55:33 +08:00
编码工程师andmultica-agent f1fe4a3928 feat(mobile,deploy): 接入安卓极光与华为通道并准备公网部署配置
客户端默认指向 jxd.jinniu.ink 的 HTTPS/WSS;登录后把 OpenIM userID 设为极光 alias。
华为 agconnect-services.json 不伪造、不入库。Master Secret 只从服务器文件注入。

Co-authored-by: multica-agent <github@multica.ai>
2026-08-30 21:32:58 +08:00
f35e1f03d5 chore(mobile): 将安卓/苹果应用标识统一为 cn.solsum.jinxunda
后续极光与厂商推送将使用该包名;旧测试版需卸载重装。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-30 21:26:17 +08:00
leefer 2f8a76da3f Merge pull request 'docs: add project handover handbook' (#1) from agent/agent/1f4a1630f24c into main
Reviewed-on: http://192.168.200.36:3200/leefer/tongxunruanjian/pulls/1
2026-08-23 16:55:28 +08:00
编码工程师andmultica-agent 6cd369f548 docs: add project handover handbook
Co-authored-by: multica-agent <github@multica.ai>
2026-08-23 14:28:07 +08:00
编码工程师andmultica-agent fa8584a73f chore(scripts): 修正 fix-livekit-ports.sh 回退指令为从备份恢复
Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 00:32:09 +08:00
编码工程师andmultica-agent 5cc2685f03 fix(livekit): 统一信令/媒体端口为 17880/17881/17882,修复接听后双方网络错误
根因:LiveKit 的 config/livekit.yaml 媒体端口(7881/7882)与宿主实际映射(17881/17882)不一致,
信令走 17880 能通,但媒体被宣告到 7881/7882 连不上。

- config/livekit.yaml: port/tcp_port/udp_port -> 17880/17881/17882
- docker-compose.yaml: 容器侧端口与宿主侧一致(1788x),不再重映射
- .env.example: 默认端口改为 1788x 并注明三处需一致
- scripts/selftest.sh: LK_PORT 默认值改为 17880
- scripts/fix-livekit-ports.sh: 一键备份+对齐 .env+仅重建 livekit 的修复脚本
- README: 更新端口表、客户端接入参数与排障说明

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 00:31:08 +08:00
46 changed files with 969 additions and 80 deletions
+17 -4
View File
@@ -65,10 +65,12 @@ LOG_LEVEL=3
LIVEKIT_API_KEY=openimLKkey
# secret 必须 ≥32 字符,否则 LiveKit 启动报 secret is too short
LIVEKIT_API_SECRET=lk-secret-9f2c7a41d5e8b063f1a4c795e2d8b3a6
# 三个都是宿主机映射端口;7880/7881/7882 被占用时改成 17880/17881/17882 之类即可
LIVEKIT_PORT=7880
LIVEKIT_RTC_TCP_PORT=7881
LIVEKIT_RTC_UDP_PORT=7882
# 三个端口必须与 config/livekit.yaml 的 port/tcp_port/udp_port 完全一致,
# 且与 docker-compose.yaml 的“宿主侧=容器侧”保持一致(默认 17880/17881/17882)。
# 改端口时三个文件要一起改,否则语音通话会出现“能接听但双方网络错误”。
LIVEKIT_PORT=17880
LIVEKIT_RTC_TCP_PORT=17881
LIVEKIT_RTC_UDP_PORT=17882
# ===== 公司账号服务(工号+密码登录,自研)=====
# 宿主端口;管理页 http://<SERVER_IP>:10010/
@@ -82,3 +84,14 @@ ALERTMANAGER_PORT=19093
GRAFANA_PORT=13000
NODE_EXPORTER_PORT=19100
GRAFANA_URL="http://127.0.0.1:13000/"
# ===== 极光离线推送(OpenIM push=====
# AppKey 可以进配置;Master Secret 禁止写在本文件、Git、日志或客户端。
# 公网部署时由 scripts/load-jpush-secret.sh 从
# /root/.config/jinxunda/jpush_master_secret 注入 JPUSH_MASTER_SECRET。
OPENIM_PUSH_ENABLE=
JPUSH_APPKEY=95fc2352648a7d4568ac8d72
JPUSH_PUSH_URL=https://api.jpush.cn/v3/push
# 必须加引号:值含分号。compose 读 .env 无妨;不要用 shell source/. 加载未加引号的副本。
JPUSH_PUSH_INTENT="intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=cn.solsum.jinxunda/cn.solsum.jinxunda.MainActivity;end"
+2
View File
@@ -6,3 +6,5 @@
/components/
.selftest/
data/
**/agconnect-services.json
+10 -8
View File
@@ -33,9 +33,9 @@
| 10002 | TCP/HTTP | OpenIM REST API |
| 10005 | TCP/HTTP | MinIO(图片/语音/文件下载) |
| 10010 | TCP/HTTP | 公司账号服务(员工登录接口 + 管理页) |
| 7880 | TCP/WS | LiveKit 信令(`.env``LIVEKIT_PORT` 可改) |
| 7882 | UDP | LiveKit 语音媒体(`LIVEKIT_RTC_UDP_PORT` 可改) |
| 7881 | TCP | LiveKit 媒体备用通道(`LIVEKIT_RTC_TCP_PORT` 可改) |
| 17880 | TCP/WS | LiveKit 信令(`.env``LIVEKIT_PORT` 可改,但必须与 `config/livekit.yaml` 一致 |
| 17882 | UDP | LiveKit 语音媒体(`LIVEKIT_RTC_UDP_PORT` 可改,同上 |
| 17881 | TCP | LiveKit 媒体备用通道(`LIVEKIT_RTC_TCP_PORT` 可改,同上 |
| 10004 | TCP/HTTP | MinIO 控制台(运维用,可不开放) |
| 12379/12380 | TCP | Etcd(仅容器间用,建议不对外) |
@@ -77,16 +77,18 @@ docker compose up -d # 再启动
## 客户端接入参数(自测通过后)
- OpenIM API: `http://<服务器IP>:10002`
- OpenIM WebSocket: `ws://<服务器IP>:10001`
- LiveKit: `ws://<服务器IP>:7880`7880 为默认信令端口,可用 `.env``LIVEKIT_PORT` 改),API Key/Secret 见 `.env``LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`
- OpenIM API: `http://<服务器IP>:10002`(公网测试见 `deploy/public/README.md``https://jxd.jinniu.ink`
- OpenIM WebSocket: `ws://<服务器IP>:10001`(公网:`wss://jxd.jinniu.ink/msg_gateway`
- LiveKit: `ws://<服务器IP>:17880`17880 为默认信令端口;改端口时必须 `.env``LIVEKIT_PORT``config/livekit.yaml``port``docker-compose.yaml` 的容器侧端口三者一起改),API Key/Secret 见 `.env``LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`
- 测试账号:`test001` / `test002`(由自测脚本注册);语音通话时两名用户以各自 identity 进同一房间即可
公网测试部署(不改本机内网步骤):见 `deploy/public/README.md`。施工员不执行该部署。
## 常见问题
- **文件/语音能发但打不开**`MINIO_EXTERNAL_ADDRESS` 没写成客户端能访问的 IP。改 `.env``docker compose up -d` 重建 openim-server。
- **启动报缺 `ETCD_USERNAME` / `KAFKA_USERNAME` 等警告**:未启用对应组件认证,官方说明可忽略。
- **openim-server 一直 unhealthy**`docker exec -it openim-server mage check` 看哪项依赖没通;首次启动需等 30-60 秒。
- **livekit 启动报 `bind ... 7880/7881: address already in use`**:服务器上这些端口被别的程序占了(`ss -tlnp | grep 788` 查占用者)。不要动别人的服务,把 `.env` `LIVEKIT_PORT` / `LIVEKIT_RTC_TCP_PORT` / `LIVEKIT_RTC_UDP_PORT` 改成空闲端口(如 17880/17881/17882),再 `docker compose up -d --force-recreate livekit`之后客户端和自测脚本都用新端口(自测脚本会自动读 `.env`
- **livekit 启动报 `bind ... 7880/7881: address already in use`**:服务器上这些端口被别的程序占了(`ss -tlnp | grep 788` 查占用者)。不要动别人的服务,把端口统一改成空闲端口(如 17880/17881/17882)——注意**必须三处一起改**:`.env` `LIVEKIT_PORT` / `LIVEKIT_RTC_TCP_PORT` / `LIVEKIT_RTC_UDP_PORT``config/livekit.yaml``port` / `tcp_port` / `udp_port``docker-compose.yaml` 里 livekit 的容器侧端口(保持“宿主侧=容器侧”),然后 `docker compose up -d --force-recreate livekit`只改其中一两处会导致“能邀请、能接听,但接听后双方网络错误”
- **livekit 日志报 `secret is too short`**`LIVEKIT_API_SECRET` 必须 ≥32 字符,换个长密钥后重建 livekit 容器。
- **语音通话连不上**:确认服务器 UDP 7882 放行(内网防火墙/安全组);UDP 不通时会走 TCP 7881 兜底。
- **语音通话连不上(接听后双方网络错误)**:先确认 `.env` / `config/livekit.yaml` / `docker-compose.yaml` 三处端口一致;再确认服务器防火墙放通 `LIVEKIT_PORT`(TCP)、`LIVEKIT_RTC_TCP_PORT`(TCP)、`LIVEKIT_RTC_UDP_PORT`(UDP)(默认 17880/17881/17882)。UDP 不通时会走 TCP 兜底。
+14
View File
@@ -0,0 +1,14 @@
# LiveKit 公网测试配置(jxd.jinniu.ink
# 仅由 deploy/public/docker-compose.public.yaml 挂载;内网仍用 livekit.yaml。
#
# 媒体候选地址走公网探测;信令经 Nginx 443 反代,UDP/TCP 媒体端口仍需对公网开放。
port: 17880
rtc:
tcp_port: 17881
udp_port: 17882
use_external_ip: true
room:
departure_timeout: 300
empty_timeout: 300
logging:
level: info
+10 -3
View File
@@ -1,10 +1,17 @@
# LiveKit 服务端配置(内网部署)
# API Key/Secret 通过环境变量 LIVEKIT_KEYS 注入(见 docker-compose.yaml
# 服务器对内 IP 通过启动参数 --node-ip 注入(deploy.sh 写入 .env 的 SERVER_IP
port: 7880
#
# 重要:这里的 port / tcp_port / udp_port 必须与 docker-compose.yaml 里
# 容器侧端口、以及 .env 的 LIVEKIT_PORT / LIVEKIT_RTC_TCP_PORT /
# LIVEKIT_RTC_UDP_PORT 三者完全一致(默认 17880/17881/17882)。
# LiveKit 用 --node-ip + use_external_ip:false 向客户端宣告媒体地址时,
# 只会宣告这里的端口,不会感知 Docker 宿主侧的重映射;三者不一致会导致
# “能邀请、能接听,但接听后双方网络错误”(信令通、媒体不通)。
port: 17880
rtc:
tcp_port: 7881
udp_port: 7882
tcp_port: 17881
udp_port: 17882
# 内网部署:不做公网 IP 探测,candidate 直接使用 --node-ip
use_external_ip: false
room:
+17
View File
@@ -0,0 +1,17 @@
# 复制到仓库根目录的 .env 后按机填写。禁止把 Master Secret 写进本文件或 Git。
# 公网测试域名已解析到测试机;客户端只认域名,不认 IP。
# 本机公网 IP(LiveKit 媒体候选用)。在服务器上用 `curl -4 -s ifconfig.me` 查看,不要写进客户端。
SERVER_IP=
MINIO_EXTERNAL_ADDRESS="https://jxd.jinniu.ink/files"
OPENIM_PUSH_ENABLE=jpush
JPUSH_APPKEY=95fc2352648a7d4568ac8d72
JPUSH_PUSH_URL=https://api.jpush.cn/v3/push
# 必须加引号:值含分号。compose 读 .env 无妨;不要用 shell source/. 加载未加引号的副本。
JPUSH_PUSH_INTENT="intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=cn.solsum.jinxunda/cn.solsum.jinxunda.MainActivity;end"
# Master Secret 由 scripts/load-jpush-secret.sh 从
# /root/.config/jinxunda/jpush_master_secret 注入为环境变量 JPUSH_MASTER_SECRET。
# JPUSH_MASTER_SECRET_FILE=/root/.config/jinxunda/jpush_master_secret
+66
View File
@@ -0,0 +1,66 @@
# 公网测试部署说明(jxd.jinniu.ink
施工员本卡**不执行**本节命令、不 SSH 改测试机。由总工审核通过后再部署。
目标机:已解析到 `jxd.jinniu.ink` 的香港测试服务器。数据目录仍是仓库下的 `components/``data/`,回滚不删数据。
## 对外暴露与对内绑定
| 入口 | 协议 | 后端 | 说明 |
| --- | --- | --- | --- |
| `https://jxd.jinniu.ink/` | HTTPS | OpenIM API `:10002` | 客户端 `apiAddr` |
| `wss://jxd.jinniu.ink/msg_gateway` | WSS | OpenIM 网关 `:10001` | 客户端 `wsAddr` |
| `https://jxd.jinniu.ink/account/` | HTTPS | 账号服务 `:10010` | 客户端 `authApiBase` |
| `https://jxd.jinniu.ink/files/` | HTTPS | MinIO `:10005` | `MINIO_EXTERNAL_ADDRESS` |
| `wss://jxd.jinniu.ink/livekit/` | WSS | LiveKit 信令 `:17880` | 客户端 `livekitUrl` |
| `:17881/tcp` `:17882/udp` | 媒体 | LiveKit RTC | 必须对公网开放,不能只走 443 |
内部端口(10001/10002/10005/10010/17880/etcd)在 overlay 里绑 `127.0.0.1`。防火墙建议只放行 80/tcp、443/tcp、17881/tcp、17882/udp。
## 证书
1. 先把 `deploy/public/nginx.bootstrap.conf` 挂到 edge(只听 80)。
2. `certbot certonly --webroot -w deploy/public/acme -d jxd.jinniu.ink`
3. 确认 `/etc/letsencrypt/live/jxd.jinniu.ink/``fullchain.pem``privkey.pem`
4. 改回 `nginx.conf` 后重载 edge。
私钥不得复制进 Git。
## 极光 Master Secret
文件在测试机 `/root/.config/jinxunda/jpush_master_secret`(权限 600)。部署前:
```bash
source ./scripts/load-jpush-secret.sh # 只导出环境变量,不打印内容
```
不要 `cat` 该文件,不要写进 `.env` 或 compose 明文。
## 启动(总工)
```bash
cd /path/to/tongxunruanjian
cp .env.example .env # 若还没有
# 按 deploy/public/.env.public.example 写入 SERVER_IP、MINIO_EXTERNAL_ADDRESS、JPUSH_*(不含 Master Secret
source ./scripts/load-jpush-secret.sh
./scripts/deploy-public.sh
```
`SERVER_IP` 填本机公网 IP(LiveKit 媒体候选),不要写进手机/电脑客户端代码。
## 回滚
```bash
./scripts/rollback-public.sh
```
效果:停掉 nginx edge,按原来的 `docker-compose.yaml` 拉起;`components/``data/account` 不动。
## 客户端地址(已写进代码默认值)
- 手机:`mobile/lib/config.dart`,默认 `jxd.jinniu.ink` + TLS。内网回切:`--dart-define=SERVER_HOST=192.168.200.11 --dart-define=USE_TLS=false`
- 电脑:改 `pc-client/.env``VITE_*``https://` / `wss://` 路径,见该目录 `.env.example`
## 华为通道缺口
`mobile/android/app/README-agconnect.md`。缺 `agconnect-services.json` 时华为真机厂商通道不通,其余施工已完成。
View File
+59
View File
@@ -0,0 +1,59 @@
# 公网测试 overlay。用法(在仓库根目录,由总工执行,本卡不跑):
# ./scripts/load-jpush-secret.sh
# docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml up -d
# 不要用 shell source/. 加载 .envJPUSH_PUSH_INTENT 含分号,未加引号时会被拆成命令。
# compose 会自行读取仓库根目录 .env 做变量替换;Master Secret 只经 load-jpush-secret.sh 注入。
#
# 原则:内部端口只绑 127.0.0.1;对外只开 80/443 + LiveKit 媒体 17881/tcp、17882/udp。
# ports 使用 !override,避免与基础 compose 的 0.0.0.0 绑定叠加。
# 回滚:去掉本 overlay,改回 docker compose -f docker-compose.yaml up -d,数据目录 components/ 与 data/ 不动。
services:
openim-server:
ports: !override
- "127.0.0.1:${OPENIM_MSG_GATEWAY_PORT}:10001"
- "127.0.0.1:${OPENIM_API_PORT}:10002"
environment:
- IMENV_OPENIM_PUSH_ENABLE=${OPENIM_PUSH_ENABLE:-jpush}
- IMENV_OPENIM_PUSH_JPUSH_APPKEY=${JPUSH_APPKEY}
- IMENV_OPENIM_PUSH_JPUSH_MASTERSECRET=${JPUSH_MASTER_SECRET}
- IMENV_OPENIM_PUSH_JPUSH_PUSHURL=${JPUSH_PUSH_URL:-https://api.jpush.cn/v3/push}
- IMENV_OPENIM_PUSH_JPUSH_PUSHINTENT=${JPUSH_PUSH_INTENT}
minio:
ports: !override
- "127.0.0.1:${MINIO_PORT}:9000"
- "127.0.0.1:${MINIO_CONSOLE_PORT}:9090"
etcd:
ports: !override
- "127.0.0.1:12379:2379"
- "127.0.0.1:12380:2380"
livekit:
command: --config /etc/livekit.yaml --node-ip ${SERVER_IP}
ports: !override
- "127.0.0.1:${LIVEKIT_PORT:-17880}:17880"
- "${LIVEKIT_RTC_TCP_PORT:-17881}:17881"
- "${LIVEKIT_RTC_UDP_PORT:-17882}:17882/udp"
volumes:
- ./config/livekit.public.yaml:/etc/livekit.yaml
account-server:
ports: !override
- "127.0.0.1:${ACCOUNT_PORT:-10010}:10010"
edge:
image: nginx:1.27-alpine
container_name: jxd-edge
restart: always
network_mode: host
volumes:
- ./deploy/public/nginx.conf:/etc/nginx/nginx.conf:ro
- ./deploy/public/acme:/var/www/acme:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- openim-server
- account-server
- minio
- livekit
+30
View File
@@ -0,0 +1,30 @@
# 仅 80 端口:申请 Let's Encrypt 证书用。证书拿到后换成 nginx.conf。
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server_tokens off;
server {
listen 80;
listen [::]:80;
server_name jxd.jinniu.ink;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 200 'jxd.jinniu.ink bootstrap\n';
add_header Content-Type text/plain;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
# 公网测试:jxd.jinniu.ink 终止 TLS,反代到本机回环上的 OpenIM / 账号 / 对象存储 / LiveKit。
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server_tokens off;
client_max_body_size 100m;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream openim_api {
server 127.0.0.1:10002;
}
upstream openim_ws {
server 127.0.0.1:10001;
}
upstream account_api {
server 127.0.0.1:10010;
}
upstream minio_s3 {
server 127.0.0.1:10005;
}
upstream livekit_signal {
server 127.0.0.1:17880;
}
server {
listen 80;
listen [::]:80;
server_name jxd.jinniu.ink;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jxd.jinniu.ink;
ssl_certificate /etc/letsencrypt/live/jxd.jinniu.ink/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jxd.jinniu.ink/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# OpenIM 消息长连接
location /msg_gateway {
proxy_pass http://openim_ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# 公司账号服务(登录、管理页、rtc_token)
location /account/ {
proxy_pass http://account_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# MinIO 对象(MINIO_EXTERNAL_ADDRESS=https://jxd.jinniu.ink/files
location /files/ {
proxy_pass http://minio_s3/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_request_buffering off;
}
# LiveKit 信令(客户端 wss://jxd.jinniu.ink/livekit → 容器 /rtc 等)
location /livekit/ {
proxy_pass http://livekit_signal/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# OpenIM REST APIcatch-all,须放最后)
location / {
proxy_pass http://openim_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
+13 -4
View File
@@ -358,6 +358,13 @@ services:
- IMENV_LOG_ISSTDOUT=${LOG_IS_STDOUT}
- IMENV_LOG_REMAINLOGLEVEL=${LOG_LEVEL}
- IMENV_OPENIM_API_PROMETHEUS_GRAFANAURL=${GRAFANA_URL}
# 离线推送(极光)。未启用时保持空,OpenIM 走 dummy pusher。
# Master Secret 只从环境变量注入,禁止写进仓库。
- IMENV_OPENIM_PUSH_ENABLE=${OPENIM_PUSH_ENABLE:-}
- IMENV_OPENIM_PUSH_JPUSH_APPKEY=${JPUSH_APPKEY:-}
- IMENV_OPENIM_PUSH_JPUSH_MASTERSECRET=${JPUSH_MASTER_SECRET:-}
- IMENV_OPENIM_PUSH_JPUSH_PUSHURL=${JPUSH_PUSH_URL:-https://api.jpush.cn/v3/push}
- IMENV_OPENIM_PUSH_JPUSH_PUSHINTENT=${JPUSH_PUSH_INTENT:-}
restart: always
depends_on:
- mongo
@@ -370,7 +377,9 @@ services:
# LiveKit:语音/视频通话的 SFUApache 2.0,自部署。
# 客户端通过 ws://<SERVER_IP>:<LIVEKIT_PORT> 连接,凭 LIVEKIT_API_KEY/SECRET 签发的 token 进房。
# 三个宿主端口都可在 .env 调整(7880/7881/7882 被其他服务占用时改映射即可,容器内端口不变)。
# 三个端口必须与 config/livekit.yaml 里容器监听的端口完全一致(默认 17880/17881/17882),
# 即“宿主侧 = 容器侧”,不要在宿主侧做重映射;LiveKit 向客户端宣告媒体地址时只认
# config/livekit.yaml 里的端口,重映射会导致信令通、媒体不通。
livekit:
image: ${LIVEKIT_IMAGE}
container_name: livekit
@@ -379,9 +388,9 @@ services:
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
ports:
- "${LIVEKIT_PORT:-7880}:7880" # HTTP / WebSocket 信令
- "${LIVEKIT_RTC_TCP_PORT:-7881}:7881" # RTC over TCPUDP 不通时的备用通道)
- "${LIVEKIT_RTC_UDP_PORT:-7882}:7882/udp" # RTC 媒体(UDP 复用端口)
- "${LIVEKIT_PORT:-17880}:17880" # HTTP / WebSocket 信令
- "${LIVEKIT_RTC_TCP_PORT:-17881}:17881" # RTC over TCPUDP 不通时的备用通道)
- "${LIVEKIT_RTC_UDP_PORT:-17882}:17882/udp" # RTC 媒体(UDP 复用端口)
volumes:
- ./config/livekit.yaml:/etc/livekit.yaml
networks:
+27
View File
@@ -0,0 +1,27 @@
# 畅联项目手册
这是项目的交接首页。新接手的人先按下面顺序阅读,不要只凭旧文档或聊天记录判断项目状态。
1. 先读 [项目需求](项目需求.md):弄清楚项目要做什么、不能碰什么。
2. 再读 [最新进度](最新进度.md):确认代码已经做到哪里、哪些结论已经被验证。
3. 最后读 [任务清单](任务清单.md):从“正在做”和“还没安排”里选择下一件事。
## 仓库里有什么
- `mobile/`:手机端,使用 Flutter,含 Android 和 iPhone 的工程文件。
- `pc-client/`:电脑端,使用 Electron,Windows 是当前优先支持的平台。
- `account-service/`:公司账号服务,负责工号密码登录、管理员导入和管理名单。
- `docker-compose.yaml``config/``scripts/`:OpenIM、语音服务和内网环境的配置及维护脚本。
- `deploy/public/`:香港测试机 `jxd.jinniu.ink` 的公网反代、证书和回滚说明(由总工部署)。
- `docs/`:本手册和以前留下的验收、回归、使用说明。
## 更新规矩
以后每完成、新增、取消或发现一个任务,负责这件事的智能体必须在同一次提交中顺手更新本目录:
- 改需求或边界时,更新 `项目需求.md`
- 做完功能、修复问题、完成构建或验证时,更新 `最新进度.md`
- 任务状态变化时,更新 `任务清单.md` 的三栏,并写明依据。
- 旧文档不能删除;如果它和当前事实不一致,在旧文档开头加“已过时,仅留档备查”提示,再在本目录写新版。
手册只记录已经确认的事实。拿不准的内容要写成“待确认”,不要当成已完成。
+2
View File
@@ -1,3 +1,5 @@
> ⚠️ 本文档已过时,仅留档备查,请勿删除。当前请先看 `docs/README.md`、`docs/项目需求.md`、`docs/最新进度.md` 和 `docs/任务清单.md`。
# 畅联使用说明(一页纸)
## 这是什么
+2
View File
@@ -1,3 +1,5 @@
> ⚠️ 本文档已过时,仅留档备查,请勿删除。当前请先看 `docs/README.md`、`docs/项目需求.md`、`docs/最新进度.md` 和 `docs/任务清单.md`。
# 第4步 · 试用前部署与验收清单(B-61)
> 本文件记录"50 人试用"前必须在服务器上手动完成的事项、验收清单与手机正式签名流程。
+2
View File
@@ -1,3 +1,5 @@
> ⚠️ 本文档已过时,仅留档备查,请勿删除。当前请先看 `docs/README.md`、`docs/项目需求.md`、`docs/最新进度.md` 和 `docs/任务清单.md`。
# 阶段 4 跨端回归记录(2026-08-16
> 基线:`main` @ `a007e8f`(含 B-52 三项修复:发消息根因 `d6ef6eb`、同事页/五态 `b50ee24`+`c73e19c`、PC 通讯录入口 `032882d`,以及本轮版本号提升 `7eab7d5`、AUTH_HOST 联调开关 `a007e8f`)。
+29
View File
@@ -0,0 +1,29 @@
# 任务清单
更新时间:2026-08-30。任务状态以当前仓库和项目约束为准;没有证据的事项不写成“已完成”。
## 正在做
无。华为配置注入与 Gradle 识别已提交,等待审核。
## 已做完
- 建立手机端 Flutter 工程和 Windows 优先的 Electron 电脑端工程。
- 接入公司工号加密码登录,以及管理员导入、管理员工账号的账号服务。
- 完成聊天、通讯录、同事申请、语音消息、文件/图片消息和一对一语音通话的相关代码接入。
- 完成一轮手机与电脑的跨端回归记录;其中已通过的项目见旧档 `phase4-regression-2026-08-16.md`
- 将 LiveKit 的信令/媒体端口统一为 `17880``17881``17882`,并提供可重复执行的修复脚本。
- 建立本目录四份交接手册,并为三份过时旧说明加上明确的留档提示。
- 手机端应用标识统一为 `cn.solsum.jinxunda`
- 安卓极光 SDK 初始化、通知权限、alias 回传、点击处理,以及 OpenIM 离线推送所需的服务端配置入口(Master Secret 从服务器文件注入)。
- 公网反向代理 / HTTPS / WSS / 防火墙与回滚说明(配置进仓库;香港测试环境已由总工部署)。
- 公网 overlay 用 `!override` 覆盖内部端口绑定;部署脚本校验 `SERVER_IP` 非空。
- 构建机本地放入华为 `agconnect-services.json`(不入库);AGConnect 插件映射/版本已按 AGP 9 补齐,Gradle 能识别包名 `cn.solsum.jinxunda`
## 还没安排
- 在可编译 LiveKit 的环境打出安卓包,并用真机验证华为厂商通道到达。
- 组织真实手机和 Windows 电脑的回归:重点确认文件传输、一对一语音通话和杀进程后的离线通知。
- 明确手机正式签名的负责人和安全保管方式;不能把签名密钥提交进仓库。
- 获得负责人同意后,再导入真实员工名单并验证账号管理流程。
- 不接入小米、OPPO、vivo、荣耀、魅族、FCM 或苹果推送(本阶段明确不做)。
+35
View File
@@ -0,0 +1,35 @@
# 最新进度
更新时间:2026-08-30。以下内容依据当前仓库代码整理。
## 现在做到哪里
项目已经具备可继续测试的手机端、Windows 电脑端、账号服务,香港测试环境已由总工部署;本轮补齐了安卓华为厂商通道的客户端构建配置:
- 手机端在 `mobile/`,使用 OpenIM Flutter SDK `3.8.3+hotfix.12`,版本 `1.0.7+8`
- 安卓/苹果应用标识为 `cn.solsum.jinxunda`
- 手机端已接入极光官方 Flutter 插件(AppKey 在客户端配置),登录后把 OpenIM userID 设为极光 alias。
- 华为 AGConnect 插件在放入本地 `agconnect-services.json` 后启用(agcp `1.9.5.301`,适配 AGP 9);该文件含密钥,仓库继续忽略,不入库。
- 手机默认服务地址为 `jxd.jinniu.ink` 的 HTTPS/WSS;内网可用 `--dart-define=SERVER_HOST=... --dart-define=USE_TLS=false` 覆盖。
- 公网部署 overlay、Nginx、证书步骤、Master Secret 注入脚本和回滚脚本在 `deploy/public/``scripts/deploy-public.sh`;由总工部署,施工员不改测试机。
- overlay 的内部端口用 Compose `!override` 覆盖基础 `0.0.0.0` 绑定,只留 `127.0.0.1`LiveKit `17881/tcp``17882/udp` 仍对公网开放。部署脚本会拒绝空 `SERVER_IP`
## 已有验证记录
- `docs/phase4-regression-2026-08-16.md` 记录过手机与电脑之间的文字、同事申请、图片、语音消息等跨端回归结果。
- 该记录中的文件传输和一对一语音通话,曾因模拟器环境无法闭环,仍需要用真实设备再次确认。
- 旧记录使用的是更早的提交、安装包版本和端口状态;因此已保留为档案并在开头标明过时,不能当作当前验收结论。
- 放入与 `cn.solsum.jinxunda` 匹配的真实华为配置后,Gradle `:app:processDebugAGCPlugin``:app:processReleaseAGCPlugin` 通过;debug/release/profile 均识别该配置文件。未因配置缺失或包名不一致失败。
- `flutter pub get` 已写入 `jpush_flutter 3.5.6` / `jpush_flutter_android 1.0.2``mobile/pubspec.lock`
- `flutter analyze` 仅有 2 条既有 OpenIM `group` 弃用 info`flutter test``config_test` 通过。
- 整包 `flutter build apk` 未出包:锁定的 `livekit_client 2.5.0+hotfix.3` 与 Flutter 3.47 / Dart 3.13 的空安全检查不兼容。此项不在华为配置本卡范围,未改 LiveKit 版本。
## 当前停在哪
华为配置已在构建机本地注入并被 Gradle 识别。整包 APK 与真机华为通道到达,仍待后续在可编译 LiveKit 的环境或真机上完成。本轮未改测试机、未回退公网端口与部署安全修复。
## 已知风险和待验证事实
- 真机上的文件传输、一对一语音通话、杀进程后的离线推送(尤其华为通道)仍需实际设备验证。
- 整包 APK 受 `livekit_client 2.5.0+hotfix.3` 与当前 Flutter 空安全检查限制,需另卡决定是否升级 LiveKit。
- 电脑端使用较旧的 Electron;公网接入后仍需评估 HTTPS、登录限流和跨域策略。
+31
View File
@@ -0,0 +1,31 @@
# 项目需求
## 项目是什么
“畅联”是集团内部使用的通讯 App,也就是给员工聊天、找同事和语音通话的工具。项目以开源 OpenIM 为基础改造,保留 OpenIM 原有标识,不换品牌、不另起一套产品标识。
## 要解决什么
员工需要在公司内部使用同一个账号,在手机和电脑上沟通,不依赖个人社交软件。管理员可以把员工名单一次导入,员工用工号和密码登录。
## 必须具备的功能
1. 聊天:能收发工作消息;当前代码还包含图片、文件和语音消息的相关能力。
2. 通讯录:能查找同事、添加同事和管理联系人。
3. 一对一语音通话:通过 LiveKit(负责实时语音连接的服务)建立通话。
4. 账号登录:员工用“工号 + 密码”登录,不能用个人注册替代。
5. 管理员导入:管理员能导入员工名单并管理账号。
## 支持范围
- 手机端:Android 和 iPhone 工程都在仓库中;当前手机端版本号为 `1.0.7+8`。应用标识 `cn.solsum.jinxunda`
- 电脑端:Windows 优先;当前电脑端显示版本为 `v1.0.2`
- 服务端:以 Docker Compose(把一组服务一起启动的工具)编排 OpenIM、账号服务和语音服务。公网测试域名为 `jxd.jinniu.ink`HTTPS/WSS),部署配置在 `deploy/public/`,实际部署由总工执行。
## 当前边界
- 只做集团内部通讯,不改造成公开社交产品。
- 保留 OpenIM 的现有标识,禁止替换品牌。
- 公网第一阶段:安卓极光 + 华为厂商通道(`agconnect-services.json` 只放构建机、不入库,见 `mobile/android/app/README-agconnect.md`);不接小米/OPPO/vivo/荣耀/魅族/FCM/苹果推送。
- 不把员工名单、密码、管理员口令、极光 Master Secret、华为 Secret、私钥或服务器私密配置写入仓库和文档。
- 本仓库的日常任务如无明确授权,不做发布、部署或改动生产环境。
+3
View File
@@ -44,6 +44,9 @@ app.*.map.json
/android/app/profile
/android/app/release
# 华为 AppGallery Connect 客户端配置(含 client_secret,禁止入库)
/android/app/agconnect-services.json
# 本机路径与构建缓存(不入库)
**/android/local.properties
**/android/.gradle/
+20 -8
View File
@@ -16,20 +16,32 @@ iOS 需要一台 Mac 搭配 Xcode 与开发者证书,另按苹果流程打包
已声明的平台权限(勿删):
- `android/app/src/main/AndroidManifest.xml``INTERNET``RECORD_AUDIO`(语音消息/通话)、`MODIFY_AUDIO_SETTINGS`、存储/相册读取(发文件、发图片);并开启了 `usesCleartextTraffic`(内网服务器暂用 http/ws 明文,开通外网改 HTTPS 后应移除)
- `android/app/src/main/AndroidManifest.xml``INTERNET``RECORD_AUDIO`(语音消息/通话)、`MODIFY_AUDIO_SETTINGS`、存储/相册读取(发文件、发图片)`POST_NOTIFICATIONS`(极光离线通知)。公网默认 HTTPS;内网联调 `--dart-define=USE_TLS=false` 时仍允许明文,故暂保留 `usesCleartextTraffic`
- `ios/Runner/Info.plist``NSMicrophoneUsageDescription`(语音消息与通话)、`NSPhotoLibraryUsageDescription`(发图片)、`NSAppTransportSecurity/NSAllowsArbitraryLoads`(同上,内网明文)。
- `android/build.gradle.kts` / `settings.gradle.kts` 把阿里云 Maven 镜像放在最前(本网络访问 maven.google.com 不稳定)。
- `android/build.gradle.kts` / `settings.gradle.kts` 把阿里云 Maven 镜像放在最前(本网络访问 maven.google.com 不稳定),并增加华为 Maven(厂商通道)
## 修改服务器地址
所有服务器地址集中在 `lib/config.dart`,改 `serverHost`(内网 IP/域名)和对应端口常量即可:
所有服务器地址集中在 `lib/config.dart`。默认公网:`jxd.jinniu.ink` + HTTPS/WSS。
| 常量 | 用途 | 默认值 |
内网联调不要改文件,打包时覆盖:
```bash
flutter build apk --release \
--dart-define=SERVER_HOST=192.168.200.11 \
--dart-define=USE_TLS=false
```
仅覆盖登录服务:`--dart-define=AUTH_HOST=127.0.0.1`
| 常量 | 用途 | 公网默认 |
| --- | --- | --- |
| `apiAddr` | OpenIM API | `http://192.168.200.11:10002` |
| `wsAddr` | OpenIM WebSocket | `ws://192.168.200.11:10001` |
| `livekitUrl` | LiveKit 语音通话 | `ws://192.168.200.11:17880`(注意已重映射,非默认 7880 |
| `authApiBase` | 公司账号登录后端 | `http://192.168.200.11:10010` |
| `apiAddr` | OpenIM API | `https://jxd.jinniu.ink` |
| `wsAddr` | OpenIM WebSocket | `wss://jxd.jinniu.ink/msg_gateway` |
| `livekitUrl` | LiveKit 语音通话 | `wss://jxd.jinniu.ink/livekit` |
| `authApiBase` | 公司账号登录后端 | `https://jxd.jinniu.ink/account` |
华为厂商通道需要本地放置 `android/app/agconnect-services.json`(与包名 `cn.solsum.jinxunda` 对应),取得方式见 `android/app/README-agconnect.md`。不要把该文件提交进 Git。
## 登录接口契约(后端:任务 B-60 account-service,已上线)
+5
View File
@@ -7,6 +7,11 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- android/**
- ios/**
include: package:flutter_lints/flutter.yaml
linter:
+20
View File
@@ -0,0 +1,20 @@
# 华为厂商通道配置文件
华为通道客户端依赖已接入。构建机需要一份本地文件:
`mobile/android/app/agconnect-services.json`
## 从哪里取得
1. 打开华为 [AppGallery Connect](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html)
2. 选择与包名 `cn.solsum.jinxunda` 对应的应用
3. 项目设置 → 应用 → 下载 `agconnect-services.json`
4. 把文件放到本目录(与 `build.gradle.kts` 同级)
5. **不要提交进 Git**(文件含华为 client_secret)。仓库已忽略该路径。
没有这份文件时:
- AGConnect 插件不会启用,安卓包仍可走极光自有通道构建
- **华为真机的厂商通道无法接通**,杀进程后的到达率会明显下降
放入真实配置后,`:app` 会启用 AGConnectagcp `1.9.5.301`,适配 AGP 9)。Gradle 会按包名 `cn.solsum.jinxunda` 校验该文件;校验失败则构建失败。
+31 -6
View File
@@ -1,11 +1,35 @@
buildscript {
repositories {
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/central")
maven("https://developer.huawei.com/repo/")
google()
mavenCentral()
}
// agcp 1.9.x 用 apply() 时会检查 buildscript classpath 里有没有 AGP
// 仅靠 plugins DSL 会报「com.android.tools.build:gradle is no set」。
dependencies {
classpath("com.android.tools.build:gradle:9.0.1")
classpath("com.huawei.agconnect:agcp:1.9.5.301")
}
}
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// 华为 agconnect-services.json 到位后才启用 AGConnect 插件,避免缺文件时整包编不过。
// 文件从华为 AppGallery Connect 下载,放到本目录,不要提交进 Git。
// jpush_flutter_android 只在自己的 library 模块 apply 该插件,不会处理本 app 的配置文件,
// 所以这里仍要在 :app 上 apply,并不是重复。
if (file("agconnect-services.json").exists()) {
apply(plugin = "com.huawei.agconnect")
}
android {
namespace = "cn.shengshentech.changlian"
namespace = "cn.solsum.jinxunda"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -15,14 +39,15 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "cn.shengshentech.changlian"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
applicationId = "cn.solsum.jinxunda"
// 极光厂商通道插件要求 minSdk 24
minSdk = 24
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
manifestPlaceholders["JPUSH_PKGNAME"] = "cn.solsum.jinxunda"
manifestPlaceholders["JPUSH_APPKEY"] = "95fc2352648a7d4568ac8d72"
manifestPlaceholders["JPUSH_CHANNEL"] = "developer-default"
}
// 压缩打包 .so(默认未压缩存储,OpenIM/WebRTC 的 native 库让包多占近一倍体积);
@@ -4,6 +4,9 @@
<!-- OpenIM 插件注册网络状态监听必须要这个权限;插件自身 manifest 漏了声明,
缺它 release 包会 SecurityException 导致插件注册失败、SDK 全部调用无响应 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Android 13+ 通知栏权限(极光离线推送) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- 语音消息录音与语音通话 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
@@ -33,6 +36,10 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- 关闭 Flutter 自动深链接,避免厂商通道点击拉起白屏 -->
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -1,5 +0,0 @@
package cn.shengshentech.changlian
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,9 @@
package cn.solsum.jinxunda
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
// Flutter 3.29+ 默认把启动 Intent 当深链接解析。华为等厂商通道点击拉起时
// intent 带 n_extra,路由表没有对应页会白屏。本应用不用官方深链接。
override fun shouldHandleDeeplinking(): Boolean = false
}
+1
View File
@@ -4,6 +4,7 @@ allprojects {
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/public")
maven("https://developer.huawei.com/repo/")
google()
mavenCentral()
}
+7
View File
@@ -0,0 +1,7 @@
[versions]
agp = "9.0.1"
agcp = "1.9.5.301"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
huawei-agconnect = { id = "com.huawei.agconnect", version.ref = "agcp" }
+13
View File
@@ -15,16 +15,29 @@ pluginManagement {
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/gradle-plugin")
maven("https://developer.huawei.com/repo/")
google()
mavenCentral()
gradlePluginPortal()
}
// 华为插件发布在 Maven 的 agcp 构件上,没有 Plugin Portal 的
// com.huawei.agconnect.gradle.plugin 坐标。plugins DSL 必须映射过去,
// 否则放入 agconnect-services.json 后启用插件会解析失败。
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.huawei.agconnect") {
useModule("com.huawei.agconnect:agcp:1.9.5.301")
}
}
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
id("com.huawei.agconnect") version "1.9.5.301" apply false
}
include(":app")
+6 -6
View File
@@ -382,7 +382,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -398,7 +398,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -415,7 +415,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -430,7 +430,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -561,7 +561,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -583,7 +583,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = cn.shengshentech.changlian;
PRODUCT_BUNDLE_IDENTIFIER = cn.solsum.jinxunda;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
+37 -14
View File
@@ -1,14 +1,24 @@
/// 服务器地址集中配置。
///
/// 服务器换了地址只需要改这一个文件:
/// 1. 改下面的 [serverHost] 为新的服务器内网 IP(或域名);
/// 2. 如果端口也被重新映射,改对应端口常量即可。
/// 默认指向公网测试域名 `jxd.jinniu.ink`HTTPS / WSS)。
/// 内网联调不要改本文件,打包时覆盖:
/// `--dart-define=SERVER_HOST=192.168.200.11 --dart-define=USE_TLS=false`
/// 仅覆盖登录服务:`--dart-define=AUTH_HOST=127.0.0.1`
library;
/// 服务器内网地址(不带协议、不带端口)
const String serverHost = '192.168.200.11';
/// 公网测试域名(不带协议、不带端口)
const String defaultPublicHost = 'jxd.jinniu.ink';
/// OpenIM API 端口(服务端 docker 映射,默认 10002
/// 服务器地址(不带协议、不带端口
const String serverHost = String.fromEnvironment(
'SERVER_HOST',
defaultValue: defaultPublicHost,
);
/// 是否走 HTTPS/WSS(公网默认 true;内网联调传 false)
const bool useTls = bool.fromEnvironment('USE_TLS', defaultValue: true);
/// OpenIM API 端口(服务端 docker 映射,默认 10002TLS 时由反向代理走 443
const int apiPort = 10002;
/// OpenIM WebSocket 端口(默认 10001
@@ -20,17 +30,30 @@ const int livekitPort = 17880;
/// 公司账号登录后端端口(B-60 account-serverdocker 默认映射 10010
const int authApiPort = 10010;
/// OpenIM API 地址,例:http://192.168.200.11:10002
const String apiAddr = 'http://$serverHost:$apiPort';
const String _authHostOverride = String.fromEnvironment('AUTH_HOST', defaultValue: '');
/// OpenIM 消息长连接地址,例:ws://192.168.200.11:10001
const String wsAddr = 'ws://$serverHost:$wsPort';
/// OpenIM API 地址
String get apiAddr => useTls ? 'https://$serverHost' : 'http://$serverHost:$apiPort';
/// LiveKit 连接地址,例:ws://192.168.200.11:17880
const String livekitUrl = 'ws://$serverHost:$livekitPort';
/// OpenIM 消息长连接地址
String get wsAddr => useTls ? 'wss://$serverHost/msg_gateway' : 'ws://$serverHost:$wsPort';
/// 公司账号登录后端地址,例:http://192.168.200.11:10010
/// LiveKit 连接地址
String get livekitUrl => useTls ? 'wss://$serverHost/livekit' : 'ws://$serverHost:$livekitPort';
/// 公司账号登录后端地址
///
/// 仅本地联调用:`--dart-define=AUTH_HOST=127.0.0.1` 可覆盖登录服务地址(OpenIM/LiveKit
/// 仍走 [serverHost]),正式打包不带该参数则与 [serverHost] 一致。
const String authApiBase = 'http://${String.fromEnvironment('AUTH_HOST', defaultValue: serverHost)}:$authApiPort';
String get authApiBase {
if (_authHostOverride.isNotEmpty) {
return 'http://$_authHostOverride:$authApiPort';
}
return useTls ? 'https://$serverHost/account' : 'http://$serverHost:$authApiPort';
}
/// 极光 AppKey(可进客户端;Master Secret 禁止出现在客户端或仓库)
const String jpushAppKey = '95fc2352648a7d4568ac8d72';
/// 登录页自检:探测消息长连接入口(TLS 走反向代理路径,明文走宿主端口)
String get wsProbeUrl => useTls ? 'https://$serverHost/msg_gateway' : 'http://$serverHost:$wsPort/';
+4
View File
@@ -7,10 +7,12 @@ import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
import 'services/call_service.dart';
import 'services/im_service.dart';
import 'services/push_service.dart';
import 'theme.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
PushService.instance.init();
runApp(const ChanglianApp());
}
@@ -27,6 +29,7 @@ class _ChanglianAppState extends State<ChanglianApp> {
super.initState();
// 被踢下线 / token 失效:清凭证、回登录页
IMService.instance.onForceLogout = () async {
await PushService.instance.unbindUser();
await LoginScreen.clearCredential();
navigatorKey.currentState?.pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginScreen()),
@@ -90,6 +93,7 @@ class _SplashGateState extends State<SplashGate> {
await im.init().timeout(const Duration(seconds: 20));
await im.login(userID: credential.$1, token: credential.$2).timeout(const Duration(seconds: 20));
CallService.instance.start();
await PushService.instance.bindUser(credential.$1);
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
return;
+5 -2
View File
@@ -6,6 +6,7 @@ import '../config.dart';
import '../services/auth_api.dart';
import '../services/call_service.dart';
import '../services/im_service.dart';
import '../services/push_service.dart';
import '../theme.dart';
import 'home_screen.dart';
@@ -92,7 +93,9 @@ class _LoginScreenState extends State<LoginScreen> {
);
// 3. 启动通话信令监听
CallService.instance.start();
// 4. 存凭证用于下次自动登录
// 4. 极光 alias = OpenIM userID,供服务端离线推送
await PushService.instance.bindUser(result.userID);
// 5. 存凭证用于下次自动登录
await LoginScreen.saveCredential(result.userID, result.token, result.nickname);
if (!mounted) return;
Navigator.of(context).pushReplacement(
@@ -150,7 +153,7 @@ class _LoginScreenState extends State<LoginScreen> {
final results = await Future.wait([
probe('账号服务', '$authApiBase/api/health'),
probe('消息接口', '$apiAddr/'),
probe('消息长连接', 'http://$serverHost:$wsPort/'),
probe('消息长连接', wsProbeUrl),
]);
return '自检:${results.join('')}';
}
+2
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import '../services/im_service.dart';
import '../services/push_service.dart';
import '../theme.dart';
import '../widgets/avatar.dart';
import 'login_screen.dart';
@@ -166,6 +167,7 @@ class _MineScreenState extends State<MineScreen> {
onPressed: () async {
Navigator.of(ctx).pop();
await LoginScreen.clearCredential();
await PushService.instance.unbindUser();
await IMService.instance.logout();
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
@@ -50,7 +50,12 @@ class MessageSendService {
message: message,
userID: isGroup ? null : peer,
groupID: isGroup ? groupID : null,
offlinePushInfo: OfflinePushInfo(),
offlinePushInfo: OfflinePushInfo(
title: '畅联',
desc: '你收到一条新消息',
iOSBadgeCount: true,
iOSPushSound: '+1',
),
);
return sent;
} catch (e) {
+103
View File
@@ -0,0 +1,103 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jpush_flutter/jpush_flutter.dart';
import 'package:jpush_flutter/jpush_interface.dart';
import 'package:permission_handler/permission_handler.dart';
import '../config.dart';
import '../nav.dart';
import '../screens/home_screen.dart';
import 'im_service.dart';
/// 极光推送封装:初始化、通知权限、Registration ID、把 OpenIM userID 设为 alias。
///
/// OpenIM 服务端 JPUSH 离线推送按 alias=userID 下发,因此登录后必须 setAlias。
/// 本阶段只接安卓;iOS 不调用 setup。
class PushService {
PushService._();
static final PushService instance = PushService._();
final JPushFlutterInterface _jpush = JPush.newJPush();
bool _ready = false;
String? _boundAlias;
Future<void> init() async {
if (_ready) return;
if (kIsWeb || !Platform.isAndroid) return;
try {
_jpush.addEventHandler(
onReceiveNotification: (message) async {},
onOpenNotification: (message) async {
_onNotificationClick();
},
onReceiveMessage: (message) async {},
onReceiveNotificationAuthorization: (message) async {},
onConnected: (message) async {},
onCommandResult: (message) async {},
);
_jpush.setAuth(enable: true);
_jpush.setup(
appKey: jpushAppKey,
channel: 'developer-default',
production: true,
debug: false,
);
_jpush.applyPushAuthority(
const NotificationSettingsIOS(sound: true, alert: true, badge: true),
);
await _requestAndroidNotificationPermission();
_ready = true;
await _jpush.getRegistrationID();
} catch (_) {
// 推送失败不能挡住登录和聊天
}
}
Future<void> bindUser(String userID) async {
if (userID.isEmpty) return;
await init();
if (!_ready) return;
try {
await _jpush.setAlias(userID);
_boundAlias = userID;
} catch (_) {
// alias 失败时下次登录再试
}
}
Future<void> unbindUser() async {
if (!_ready) return;
try {
await _jpush.deleteAlias();
} catch (_) {
// 本地照常清理
}
_boundAlias = null;
}
String? get boundAlias => _boundAlias;
Future<void> _requestAndroidNotificationPermission() async {
try {
final status = await Permission.notification.status;
if (!status.isGranted) {
await Permission.notification.request();
}
} catch (_) {
// 权限申请失败不阻断
}
}
void _onNotificationClick() {
final nav = navigatorKey.currentState;
if (nav == null) return;
if (!IMService.instance.loggedIn) return;
nav.pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const HomeScreen()),
(route) => false,
);
}
}
+26 -10
View File
@@ -369,10 +369,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.20.3"
jni:
dependency: transitive
description:
@@ -397,6 +397,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jpush_flutter:
dependency: "direct main"
description:
name: jpush_flutter
sha256: "5dcfcd553787db0c92a9b2f0898a6dbc053530c6636ed488416cd9edf12f08d3"
url: "https://pub.dev"
source: hosted
version: "3.5.6"
jpush_flutter_android:
dependency: "direct main"
description:
name: jpush_flutter_android
sha256: d2bae547aefb2ff5acf28badaf115bb358e9244b6acef84625d8db0228bbc0b2
url: "https://pub.dev"
source: hosted
version: "1.0.2"
js:
dependency: transitive
description:
@@ -489,10 +505,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
@@ -505,10 +521,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
mime:
dependency: transitive
description:
@@ -902,10 +918,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.12"
typed_data:
dependency: transitive
description:
@@ -926,10 +942,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.4.2"
vm_service:
dependency: transitive
description:
+11 -1
View File
@@ -1,7 +1,7 @@
name: changlian
description: 畅联 —— 公司内部通讯 App(手机端)
publish_to: "none"
version: 1.0.6+7
version: 1.0.7+8
environment:
sdk: ">=3.6.0 <4.0.0"
@@ -37,6 +37,16 @@ dependencies:
open_filex: 4.7.0
# 通话房间号
uuid: 4.5.2
# 极光推送官方 Flutter 插件(MIT);厂商通道另见 jpush_flutter_android
jpush_flutter: 3.5.6
jpush_flutter_android: 1.0.2
# 极光安卓厂商通道:本阶段只开华为。AppKey 可进客户端;其余厂商密钥不要填。
jpush_android:
app_key: 95fc2352648a7d4568ac8d72
channel: developer-default
huawei:
enable: true
dev_dependencies:
flutter_test:
+16
View File
@@ -0,0 +1,16 @@
import 'package:changlian/config.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('默认公网地址走 HTTPS/WSS 且不含测试 IP', () {
expect(serverHost, 'jxd.jinniu.ink');
expect(useTls, isTrue);
expect(apiAddr, 'https://jxd.jinniu.ink');
expect(wsAddr, 'wss://jxd.jinniu.ink/msg_gateway');
expect(livekitUrl, 'wss://jxd.jinniu.ink/livekit');
expect(authApiBase, 'https://jxd.jinniu.ink/account');
expect(apiAddr, isNot(contains('154.37')));
expect(wsAddr, isNot(contains('154.37')));
expect(jpushAppKey, '95fc2352648a7d4568ac8d72');
});
}
+7 -7
View File
@@ -1,7 +1,7 @@
# 网测试环境(192.168.200.11,见仓库根目录 README 端口表)
VITE_WS_URL=ws://192.168.200.11:10001
VITE_API_URL=http://192.168.200.11:10002
# 自研公司账号服务(登录 + 通话令牌)。本地联调可建 .env.local 覆盖为 mockhttp://127.0.0.1:11010
VITE_ACCOUNT_URL=http://192.168.200.11:10010
# LiveKit 语音通话(宿主端口已改映射为 17880
VITE_LIVEKIT_URL=ws://192.168.200.11:17880
# 网测试环境(jxd.jinniu.ink)。内网联调请建 .env.local 覆盖为 http/ws + 内网 IP。
VITE_WS_URL=wss://jxd.jinniu.ink/msg_gateway
VITE_API_URL=https://jxd.jinniu.ink
# 自研公司账号服务(登录 + 通话令牌)。本地联调可覆盖为 mockhttp://127.0.0.1:11010
VITE_ACCOUNT_URL=https://jxd.jinniu.ink/account
# LiveKit 语音通话信令(媒体走服务器 17881/17882,不经 443
VITE_LIVEKIT_URL=wss://jxd.jinniu.ink/livekit
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 香港测试机公网部署入口(jxd.jinniu.ink)。由总工执行;施工员本卡不跑本脚本,也不 SSH 改现网。
# 用法: ./scripts/deploy-public.sh
# 回滚: ./scripts/rollback-public.sh
set -euo pipefail
cd "$(dirname "$0")/.."
if [ ! -f .env ]; then
echo "没有 .env。先从 .env.example 复制,再按 deploy/public/.env.public.example 补公网项。" >&2
exit 1
fi
# LiveKit --node-ip 依赖 SERVER_IP;空值时信令通、媒体宣告错误。
# 不 source .envJPUSH_PUSH_INTENT 含分号);只抽取这一项。
if [ -z "${SERVER_IP:-}" ]; then
SERVER_IP="$(awk -F= '/^SERVER_IP=/{v=$2} END{print v}' .env | tr -d '[:space:]"')"
fi
if [ -z "${SERVER_IP}" ]; then
echo "SERVER_IP 为空,拒绝启动。请在 .env 填写本机公网 IP(LiveKit 媒体候选用)。" >&2
exit 1
fi
# 注入 Master Secret(失败则停,避免空密钥启动)
# shellcheck disable=SC1091
source ./scripts/load-jpush-secret.sh
if [ -z "${JPUSH_MASTER_SECRET:-}" ]; then
echo "Master Secret 未注入,拒绝启动" >&2
exit 1
fi
mkdir -p deploy/public/acme components/redis/config components/mnt/data components/mongodb components/etcd components/kafka
touch components/redis/config/redis.conf
docker compose \
-f docker-compose.yaml \
-f deploy/public/docker-compose.public.yaml \
pull
docker compose \
-f docker-compose.yaml \
-f deploy/public/docker-compose.public.yaml \
up -d
echo "已提交 compose 启动。检查: docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml ps"
echo "证书未就绪时先用 nginx.bootstrap.conf 申请证书,见 deploy/public/README.md"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# 修复语音通话“门号不一致”(信令通、媒体不通)问题。
# 根因:LiveKit 的 config/livekit.yaml 里媒体端口(7881/7882)与宿主实际映射(17881/17882)不一致,
# 导致 LiveKit 向客户端宣告的媒体地址连不上。
# 修复:统一 .env / config/livekit.yaml / docker-compose.yaml 三处端口为 17880/17881/17882
# 只重启 livekit 容器。
#
# 用法(在服务器仓库目录里执行,且已 git pull 到含本修复的版本):
# ./scripts/fix-livekit-ports.sh
#
# 该脚本幂等,可重复执行;会先备份再改动,失败可回退。
set -euo pipefail
cd "$(dirname "$0")/.."
TS="$(date +%Y%m%d-%H%M%S)"
BK="backup-livekit-ports-$TS"
mkdir -p "$BK"
echo "==> 备份当前配置到 $BK/"
for f in .env config/livekit.yaml docker-compose.yaml; do
[ -f "$f" ] && cp -p "$f" "$BK/$(basename "$f")"
done
[ -f "$BK/.env" ] || echo "(未找到 .env,跳过备份)"
# 1. 校验仓库文件已含修复(config/livekit.yaml 端口应为 17880/17881/17882
grep -q '^port: 17880' config/livekit.yaml || { echo "✗ config/livekit.yaml 未包含 port: 17880,请先 git pull 更新仓库后再执行本脚本"; exit 1; }
grep -q 'tcp_port: 17881' config/livekit.yaml || { echo "✗ config/livekit.yaml 未包含 tcp_port: 17881"; exit 1; }
grep -q 'udp_port: 17882' config/livekit.yaml || { echo "✗ config/livekit.yaml 未包含 udp_port: 17882"; exit 1; }
# 2. 对齐 .env 三个端口
if [ ! -f .env ]; then
cp .env.example .env
echo "==> 未找到 .env,已从 .env.example 生成"
fi
sed -i 's|^LIVEKIT_PORT=.*|LIVEKIT_PORT=17880|' .env
sed -i 's|^LIVEKIT_RTC_TCP_PORT=.*|LIVEKIT_RTC_TCP_PORT=17881|' .env
sed -i 's|^LIVEKIT_RTC_UDP_PORT=.*|LIVEKIT_RTC_UDP_PORT=17882|' .env
# 若这三行原本不存在,则追加
grep -q '^LIVEKIT_PORT=' .env || echo 'LIVEKIT_PORT=17880' >> .env
grep -q '^LIVEKIT_RTC_TCP_PORT=' .env || echo 'LIVEKIT_RTC_TCP_PORT=17881' >> .env
grep -q '^LIVEKIT_RTC_UDP_PORT=' .env || echo 'LIVEKIT_RTC_UDP_PORT=17882' >> .env
echo "==> .env 三个端口已设为 17880/17881/17882"
# 3. 只重建 livekit 容器(不动其它服务)
docker compose up -d --force-recreate livekit
# 4. 校验
echo "==> 等待 livekit 就绪..."
sleep 3
docker compose ps livekit
echo
echo "==> 请确认防火墙放通 17880/TCP、17881/TCP、17882/UDP(默认已在本机监听):"
ss -tlnp 2>/dev/null | grep -E ':(17880|17881)' || true
ss -ulnp 2>/dev/null | grep ':17882' || true
echo
echo "==> 完成。回退方法(用刚才的备份恢复旧配置):"
echo " cp $BK/livekit.yaml config/livekit.yaml 2>/dev/null"
echo " cp $BK/docker-compose.yaml docker-compose.yaml 2>/dev/null"
echo " cp $BK/.env .env 2>/dev/null"
echo " docker compose up -d --force-recreate livekit"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# 从服务器安全文件注入极光 Master Secret 到环境变量。
# 不打印文件内容、不写入仓库。供总工部署时 source,本卡不执行。
set -euo pipefail
SECRET_FILE="${JPUSH_MASTER_SECRET_FILE:-/root/.config/jinxunda/jpush_master_secret}"
if [ ! -f "$SECRET_FILE" ]; then
echo "缺少极光 Master Secret 文件(已配置路径,不打印内容)" >&2
exit 1
fi
perm=$(stat -c '%a' "$SECRET_FILE" 2>/dev/null || stat -f '%OLp' "$SECRET_FILE")
if [ "$perm" != "600" ] && [ "$perm" != "400" ]; then
echo "Master Secret 文件权限应为 600 或 400(当前 $perm" >&2
exit 1
fi
len=$(wc -c < "$SECRET_FILE" | tr -d ' ')
if [ "$len" -lt 16 ] || [ "$len" -gt 64 ]; then
echo "Master Secret 文件长度异常(不打印内容)" >&2
exit 1
fi
JPUSH_MASTER_SECRET="$(tr -d '\n\r' < "$SECRET_FILE")"
export JPUSH_MASTER_SECRET
# 有意不 echo 该变量
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# 公网 overlay 回滚到内网 compose:停掉 edge,按原 docker-compose.yaml 拉起。
# 不删除 components/ 与 data/,不改现网数据文件。
set -euo pipefail
cd "$(dirname "$0")/.."
docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml stop edge || true
docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml rm -f edge || true
docker compose -f docker-compose.yaml up -d
echo "已回到内网 compose。数据目录未动。检查: docker compose ps"
+1 -1
View File
@@ -18,7 +18,7 @@ MINIO_SK="$(env_get MINIO_SECRET_ACCESS_KEY)"; MINIO_SK="${MINIO_SK:-openIM123}"
MINIO_PORT="$(env_get MINIO_PORT)"; MINIO_PORT="${MINIO_PORT:-10005}"
LK_KEY="$(env_get LIVEKIT_API_KEY)"
LK_SECRET="$(env_get LIVEKIT_API_SECRET)"
LK_PORT="$(env_get LIVEKIT_PORT)"; LK_PORT="${LK_PORT:-7880}"
LK_PORT="$(env_get LIVEKIT_PORT)"; LK_PORT="${LK_PORT:-17880}"
LK_HTTP="http://${IP}:${LK_PORT}"
LK_WS="ws://${IP}:${LK_PORT}"
LK_CLI_IMAGE="livekit/livekit-cli:v2.18.2"