企业码解析+登录跑通真实后端:Tinode连接适配层、类型化API客户端、会话store
lib/tinode/connection.ts:单例Tinode连接+自己的发布订阅层——SDK的onConnect/onDisconnect/onLogin
是单槽位属性(赋值会互相覆盖),不是真正的多监听器事件源,所以这里统一接管这几个槽位,自己
扇出给任意数量的React订阅方(lib/tinode/hooks.ts的useSyncExternalStore)。installTenantHiPatch
照搬旧应用tinode-web.jsx的实现搬过来——服务端{hi}包的tenant字段SDK本身没有API,只能在原始
sendText层面注入,这是后端协议的真实约束,不是旧代码库特有的写法。
lib/api/client.ts:REST客户端按旧应用src/lib/im-api.js的request函数+完整REASON_MESSAGES错误码
翻译表搬过来(含请求头X-MeetingIM-APIKey等几个月联调换来的正确写法),这次是重新用TypeScript
写、不是复制文件,但端点行为对齐。
实机联调过程中发现并修正了一处凭记忆猜错的地方:tenants/resolve真实响应结构是
{endpoints:{api,file,websocket}, tenant:{code,name}},不是我最初假设的顶层{code,name}——
已用真实请求验证修正。用企业码default解析成功(显示"Default Tenant")+ 一次真实错误密码
登录尝试(服务端真实401,错误提示正确显示"账号或密码错误")两步都对着真实后端验证通过,
不是只测了正确路径。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8bcd9f3a5b
commit
8cdd84152d
@@ -0,0 +1,107 @@
|
||||
// Singleton Tinode connection + a small pub-sub layer wrapping it for React.
|
||||
//
|
||||
// The SDK's onConnect/onDisconnect/onLogin etc. are plain single-slot properties (`tinode.onConnect
|
||||
// = fn`), not a real multi-listener event emitter -- assigning one overwrites any previous
|
||||
// assignment. If more than one React component tried to register its own callback directly (e.g.
|
||||
// each calling useSyncExternalStore with its own subscribe function touching these slots), they'd
|
||||
// silently stomp on each other. This module claims all the SDK's callback slots exactly once and
|
||||
// fans them out to any number of subscribers via its own Set, which is what hooks.ts's
|
||||
// useSyncExternalStore-based hooks actually subscribe to.
|
||||
import { Tinode } from 'tinode-sdk';
|
||||
|
||||
import { API_KEY, APP_NAME, LOGGING_ENABLED } from '@/lib/config';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
|
||||
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'authenticated';
|
||||
|
||||
let tinode: Tinode | null = null;
|
||||
let status: ConnectionStatus = 'disconnected';
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function setStatus(next: ConnectionStatus) {
|
||||
if (status === next) return;
|
||||
status = next;
|
||||
listeners.forEach((cb) => cb());
|
||||
}
|
||||
|
||||
/**
|
||||
* The server extends the stock {hi} packet with a `tenant` field (server/datamodel.go) that the
|
||||
* vendored JS SDK has no API for. `#initPacket`/the connection's packet builder is private, so the
|
||||
* only usable hook is wrapping the connection's raw sendText and injecting the field into the
|
||||
* already-serialized JSON. Ported as-is from the old app's tnSetup/installTenantHiPatch -- this is
|
||||
* a real backend protocol extension, not an old-codebase-specific workaround.
|
||||
*/
|
||||
function installTenantHiPatch(t: Tinode) {
|
||||
const conn = t._connection;
|
||||
if (!conn || conn.__tenantPatched) return;
|
||||
const original = conn.sendText.bind(conn);
|
||||
conn.sendText = (msg: unknown) => {
|
||||
if (typeof msg === 'string' && msg.indexOf('"hi":') >= 0) {
|
||||
try {
|
||||
const parsed = JSON.parse(msg);
|
||||
if (parsed.hi) {
|
||||
const tenant = useSessionStore.getState().tenant;
|
||||
if (tenant?.code) {
|
||||
parsed.hi.tenant = tenant.code;
|
||||
msg = JSON.stringify(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON -- send as is.
|
||||
}
|
||||
}
|
||||
return original(msg as string);
|
||||
};
|
||||
conn.__tenantPatched = true;
|
||||
}
|
||||
|
||||
function createTinode(): Tinode {
|
||||
const tenant = useSessionStore.getState().tenant;
|
||||
const host = tenant?.host || 'web.118im.com';
|
||||
const secure = tenant?.secure ?? true;
|
||||
const t = new Tinode({ appName: APP_NAME, host, apiKey: API_KEY, transport: 'ws', secure, persist: false });
|
||||
t.setHumanLanguage('zh');
|
||||
t.enableLogging(LOGGING_ENABLED, true);
|
||||
installTenantHiPatch(t);
|
||||
|
||||
t.onConnect = () => setStatus('connected');
|
||||
t.onDisconnect = () => setStatus('disconnected');
|
||||
t.onLogin = (code: number) => {
|
||||
if (code === 200) setStatus('authenticated');
|
||||
};
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Lazily creates the singleton connection. Must only be called once a tenant is resolved. */
|
||||
export function getTinode(): Tinode {
|
||||
if (!tinode) {
|
||||
tinode = createTinode();
|
||||
}
|
||||
return tinode;
|
||||
}
|
||||
|
||||
/** Tears down the singleton so the next getTinode() rebuilds against a freshly-resolved tenant. */
|
||||
export function resetTinode() {
|
||||
if (tinode?.isConnected()) {
|
||||
tinode.disconnect();
|
||||
}
|
||||
tinode = null;
|
||||
setStatus('disconnected');
|
||||
}
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
const t = getTinode();
|
||||
if (t.isConnected()) return;
|
||||
setStatus('connecting');
|
||||
await t.connect();
|
||||
}
|
||||
|
||||
export function getConnectionStatus(): ConnectionStatus {
|
||||
return status;
|
||||
}
|
||||
|
||||
export function subscribeConnectionStatus(callback: () => void): () => void {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import { getConnectionStatus, getTinode, subscribeConnectionStatus } from './connection';
|
||||
|
||||
/** The current WS connection status, reactive via useSyncExternalStore. */
|
||||
export function useConnectionStatus() {
|
||||
return useSyncExternalStore(subscribeConnectionStatus, getConnectionStatus);
|
||||
}
|
||||
|
||||
/** The singleton Tinode client instance (stable reference, not itself reactive). */
|
||||
export function useTinode() {
|
||||
return getTinode();
|
||||
}
|
||||
Vendored
+7
-2
@@ -19,6 +19,10 @@ declare module 'tinode-sdk' {
|
||||
|
||||
export type AuthLevel = 'anon' | 'auth' | 'root';
|
||||
|
||||
export interface LoginCtrl {
|
||||
ctrl: { code: number; text?: string; params?: Record<string, unknown> };
|
||||
}
|
||||
|
||||
export class AccessMode {
|
||||
mode: string;
|
||||
given: string;
|
||||
@@ -101,8 +105,9 @@ declare module 'tinode-sdk' {
|
||||
isAuthenticated(): boolean;
|
||||
getAuthenticatedUserID(): string | undefined;
|
||||
|
||||
login(scheme: string, secret: string, cred?: unknown): Promise<{ ctrl: { code: number; params?: Record<string, unknown> } }>;
|
||||
loginToken(token: string, cred?: unknown): Promise<{ ctrl: { code: number } }>;
|
||||
login(scheme: string, secret: string, cred?: unknown): Promise<LoginCtrl>;
|
||||
loginBasic(username: string, password: string, cred?: unknown): Promise<LoginCtrl>;
|
||||
loginToken(token: string, cred?: unknown): Promise<LoginCtrl>;
|
||||
getAuthToken(): { token: string; expires: Date } | null;
|
||||
|
||||
getMeTopic(): MeTopic;
|
||||
|
||||
Reference in New Issue
Block a user