新增 pc-client:畅联 PC 端工程(Electron+React,按确认效果图改造,B-59)
- 基于 openim-electron-demo 改造:工号+密码登录(自研账号服务)、会话列表/聊天窗/通讯录界面重做
- 文字/语音/文件/图片消息、文件拖拽发送、表情面板、一对一语音通话(LiveKit)
- RTC 令牌对接账号服务带鉴权版 /api/rtc_token(Bearer imToken + {room,identity})
- account-service: CORS Allow-Headers 补 authorization(浏览器/渲染进程跨域调 rtc_token 需要)
- 附本地 mock 账号服务(scripts/mock-account-server.js)与截图联调脚本
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 本地开发用 mock 账号服务(畅联 PC 端联调用)
|
||||
*
|
||||
* ⚠️ 仅供本地开发联调!
|
||||
* - 内置测试环境默认密钥(OpenIM secret / LiveKit key),都是内网测试环境的公开默认值
|
||||
* - 禁止打包进客户端、禁止用于正式环境
|
||||
* - 真实员工账号由管理员在服务器上的账号服务(:10010)开通
|
||||
*
|
||||
* 用法:
|
||||
* 1. node scripts/mock-account-server.js(或 npm run mock:account)
|
||||
* 2. 把 .env 里的 VITE_ACCOUNT_URL 临时指向 http://127.0.0.1:11010
|
||||
* 3. 用 test001 / test123(或 test002 / test123)登录
|
||||
*
|
||||
* 接口形状与真实账号服务完全一致:POST /api/login、POST /api/rtc_token。
|
||||
* 零依赖,Node >= 18(用到全局 fetch)。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = 11010;
|
||||
const OPENIM_API_URL = 'http://192.168.200.11:10002';
|
||||
const OPENIM_SECRET = 'openIM123';
|
||||
const LIVEKIT_API_KEY = 'openimLKkey';
|
||||
const LIVEKIT_API_SECRET = 'lk-secret-9f2c7a41d5e8b063f1a4c795e2d8b3a6';
|
||||
|
||||
// 本地测试账号(密码均为 test123)
|
||||
const USERS = {
|
||||
test001: { password: 'test123', nickname: '测试一号' },
|
||||
test002: { password: 'test123', nickname: '测试二号' },
|
||||
};
|
||||
|
||||
// ---------------- OpenIM 管理接口 ----------------
|
||||
|
||||
async function imApi(route, body, token) {
|
||||
const res = await fetch(OPENIM_API_URL + route, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
operationID: crypto.randomUUID(),
|
||||
...(token ? { token } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getAdminToken() {
|
||||
const r = await imApi('/auth/get_admin_token', {
|
||||
secret: OPENIM_SECRET,
|
||||
userID: 'imAdmin',
|
||||
});
|
||||
if (r.errCode !== 0) throw new Error('OpenIM 管理员令牌获取失败: ' + (r.errMsg || r.errCode));
|
||||
return r.data.token;
|
||||
}
|
||||
|
||||
/** 确保 OpenIM 里存在该用户(不存在则注册,尽力而为) */
|
||||
async function ensureImUser(userID, nickname, admin) {
|
||||
try {
|
||||
await imApi('/user/user_register', {
|
||||
users: [{ userID, nickname, faceURL: '' }],
|
||||
}, admin);
|
||||
} catch { /* 忽略,后面取令牌时再报错 */ }
|
||||
}
|
||||
|
||||
async function getImUserToken(userID, platformID) {
|
||||
const admin = await getAdminToken();
|
||||
await ensureImUser(userID, USERS[userID].nickname, admin);
|
||||
const r = await imApi('/auth/get_user_token', { secret: OPENIM_SECRET, platformID, userID }, admin);
|
||||
if (r.errCode !== 0) throw new Error('OpenIM 用户令牌获取失败: ' + (r.errMsg || r.errCode));
|
||||
return r.data;
|
||||
}
|
||||
|
||||
// ---------------- LiveKit RTC 令牌签发 ----------------
|
||||
|
||||
const b64url = (s) => Buffer.from(s).toString('base64url');
|
||||
|
||||
function signLivekitToken(userID, room) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||
const payload = b64url(JSON.stringify({
|
||||
iss: LIVEKIT_API_KEY,
|
||||
sub: userID,
|
||||
exp: now + 3600,
|
||||
nbf: now - 10,
|
||||
video: { roomJoin: true, room },
|
||||
metadata: '',
|
||||
}));
|
||||
const sig = crypto.createHmac('sha256', LIVEKIT_API_SECRET).update(`${header}.${payload}`).digest('base64url');
|
||||
return `${header}.${payload}.${sig}`;
|
||||
}
|
||||
|
||||
// ---------------- HTTP 服务 ----------------
|
||||
|
||||
function send(res, status, obj) {
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, authorization',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||||
});
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
const ok = (res, data) => send(res, 200, { code: 0, msg: '', data });
|
||||
const fail = (res, msg, httpStatus = 200) => send(res, httpStatus, { code: 1, msg });
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buf = '';
|
||||
req.on('data', (c) => { buf += c; });
|
||||
req.on('end', () => {
|
||||
if (!buf) return resolve({});
|
||||
try { resolve(JSON.parse(buf)); } catch { reject(new Error('请求体不是合法 JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
if (req.method === 'OPTIONS') return send(res, 204, {});
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/login') {
|
||||
const body = await readBody(req);
|
||||
const { staffNo, password, platformID } = body;
|
||||
if (!staffNo || !password) return fail(res, '工号和密码不能为空');
|
||||
const user = USERS[staffNo];
|
||||
if (!user || user.password !== String(password)) return fail(res, '工号或密码错误');
|
||||
const t = await getImUserToken(staffNo, Number.isInteger(platformID) ? platformID : 3);
|
||||
return ok(res, {
|
||||
userID: staffNo,
|
||||
nickname: user.nickname,
|
||||
imToken: t.token,
|
||||
expireTimeSeconds: t.expireTimeSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/rtc_token') {
|
||||
// 与真实账号服务一致:Authorization: Bearer <imToken> + {room, identity},返回 {token}
|
||||
const m = /^Bearer\s+(.+)$/.exec(String(req.headers.authorization || ''));
|
||||
if (!m) return fail(res, '未登录或登录已过期', 401);
|
||||
const body = await readBody(req);
|
||||
const { room, identity } = body;
|
||||
if (!room || !identity) return fail(res, '房间号或用户标识不能为空');
|
||||
return ok(res, { token: signLivekitToken(String(identity), String(room)) });
|
||||
}
|
||||
|
||||
return fail(res, '接口不存在', 404);
|
||||
} catch (e) {
|
||||
return fail(res, '服务内部错误: ' + e.message, 500);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`mock-account-server listening on http://127.0.0.1:${PORT}`);
|
||||
console.log('测试账号:test001 / test123,test002 / test123');
|
||||
});
|
||||
Reference in New Issue
Block a user