@lark-apaas/client-toolkit 1.1.24 → 1.1.25-alpha.1

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.
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { ConfigProvider } from "antd";
4
4
  import { MiaodaInspector } from "@lark-apaas/miaoda-inspector";
5
5
  import IframeBridge from "./IframeBridge.js";
@@ -15,6 +15,7 @@ import { useAppInfo } from "../../hooks/index.js";
15
15
  import { TrackKey } from "../../types/tea.js";
16
16
  import safety from "./safety.js";
17
17
  import { getAppId } from "../../utils/getAppId.js";
18
+ import { ServerLogPoller } from "../../server-log/index.js";
18
19
  registerDayjsPlugins();
19
20
  initAxiosConfig();
20
21
  const isMiaodaPreview = window.IS_MIAODA_PREVIEW;
@@ -30,6 +31,7 @@ const readCssVarColor = (varName, fallback)=>{
30
31
  const App = (props)=>{
31
32
  const { themeMeta = {} } = props;
32
33
  useAppInfo();
34
+ const serverLogPollerRef = useRef(null);
33
35
  const { rem } = findValueByPixel(themeMetaOptions.themeRadius, themeMeta.borderRadius) || {
34
36
  rem: '0.625'
35
37
  };
@@ -41,6 +43,30 @@ const App = (props)=>{
41
43
  borderRadius: radiusToken
42
44
  }
43
45
  };
46
+ useEffect(()=>{
47
+ if ('production' !== process.env.NODE_ENV && window.parent !== window) {
48
+ try {
49
+ const backendUrl = window.location.origin;
50
+ serverLogPollerRef.current = new ServerLogPoller({
51
+ serverUrl: backendUrl,
52
+ apiPath: '/dev/logs/server-logs',
53
+ pollInterval: 2000,
54
+ limit: 100,
55
+ debug: true
56
+ });
57
+ serverLogPollerRef.current.start();
58
+ console.log('[AppContainer] Server log poller started');
59
+ } catch (error) {
60
+ console.error('[AppContainer] Failed to start server log poller:', error);
61
+ }
62
+ return ()=>{
63
+ if (serverLogPollerRef.current) {
64
+ serverLogPollerRef.current.stop();
65
+ console.log('[AppContainer] Server log poller stopped');
66
+ }
67
+ };
68
+ }
69
+ }, []);
44
70
  useEffect(()=>{
45
71
  if (isMiaodaPreview) fetch(`${location.origin}/ai/api/feida_preview/csrf`).then(()=>{
46
72
  setTimeout(()=>{
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Server Log 模块
3
+ *
4
+ * 通过 HTTP 轮询方式获取服务端日志并转发给父窗口
5
+ */
6
+ export { ServerLogPoller } from './poller';
7
+ export type { ServerLogPollerOptions } from './poller';
8
+ export type { ServerLog, ServerLogLevel, ServerLogSource, ServerLogMeta, ServerLogPostMessage, ClientToServerMessage, ServerToClientMessage, } from './types';
9
+ export { ServerLogPoller as default } from './poller';
@@ -0,0 +1,2 @@
1
+ import { ServerLogPoller } from "./poller.js";
2
+ export { ServerLogPoller, ServerLogPoller as default };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Server Log Poller (HTTP 轮询版本)
3
+ *
4
+ * 职责:
5
+ * 1. 定期轮询后端 HTTP API (/dev/logs/server-logs)
6
+ * 2. 获取新的服务端日志
7
+ * 3. 将接收到的日志通过 postMessage 转发给父窗口 (miaoda)
8
+ * 4. 管理连接状态并通知父窗口
9
+ *
10
+ * 与 ServerLogForwarder 的区别:
11
+ * - 使用 HTTP 轮询代替 WebSocket
12
+ * - 无需 socket.io-client 依赖
13
+ * - 更简单、更稳定、更易调试
14
+ */
15
+ export interface ServerLogPollerOptions {
16
+ /**
17
+ * 后端服务器 URL
18
+ * @example 'http://localhost:3000'
19
+ */
20
+ serverUrl: string;
21
+ /**
22
+ * API 路径
23
+ * @default '/dev/logs/server-logs'
24
+ */
25
+ apiPath?: string;
26
+ /**
27
+ * 轮询间隔(毫秒)
28
+ * @default 2000
29
+ */
30
+ pollInterval?: number;
31
+ /**
32
+ * 每次获取的日志数量
33
+ * @default 100
34
+ */
35
+ limit?: number;
36
+ /**
37
+ * 是否启用调试日志
38
+ * @default false
39
+ */
40
+ debug?: boolean;
41
+ }
42
+ type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
43
+ export declare class ServerLogPoller {
44
+ private intervalId;
45
+ private status;
46
+ private lastTimestamp;
47
+ private options;
48
+ private isPolling;
49
+ constructor(options: ServerLogPollerOptions);
50
+ /**
51
+ * 启动轮询器
52
+ */
53
+ start(): void;
54
+ /**
55
+ * 停止轮询器
56
+ */
57
+ stop(): void;
58
+ /**
59
+ * 获取当前连接状态
60
+ */
61
+ getStatus(): ConnectionStatus;
62
+ /**
63
+ * 执行一次轮询
64
+ */
65
+ private poll;
66
+ /**
67
+ * 转发日志到父窗口
68
+ */
69
+ private forwardLog;
70
+ /**
71
+ * 更新连接状态并通知父窗口
72
+ */
73
+ private updateStatus;
74
+ /**
75
+ * 发送消息到父窗口
76
+ */
77
+ private postToParent;
78
+ /**
79
+ * 调试日志
80
+ */
81
+ private log;
82
+ /**
83
+ * 错误日志
84
+ */
85
+ private error;
86
+ }
87
+ export {};
@@ -0,0 +1,133 @@
1
+ class ServerLogPoller {
2
+ intervalId = null;
3
+ status = 'disconnected';
4
+ lastTimestamp = 0;
5
+ options;
6
+ isPolling = false;
7
+ constructor(options){
8
+ this.options = {
9
+ serverUrl: options.serverUrl,
10
+ apiPath: (process.env.CLIENT_BASE_PATH || '') + (options.apiPath || '/dev/logs/server-logs'),
11
+ pollInterval: options.pollInterval || 2000,
12
+ limit: options.limit || 100,
13
+ debug: options.debug ?? false
14
+ };
15
+ }
16
+ start() {
17
+ if (this.isPolling) return void this.log('Poller already started');
18
+ this.isPolling = true;
19
+ this.updateStatus('connecting');
20
+ this.log('Starting server log poller...', {
21
+ serverUrl: this.options.serverUrl,
22
+ apiPath: this.options.apiPath,
23
+ pollInterval: this.options.pollInterval
24
+ });
25
+ this.poll();
26
+ this.intervalId = window.setInterval(()=>{
27
+ this.poll();
28
+ }, this.options.pollInterval);
29
+ }
30
+ stop() {
31
+ if (!this.isPolling) return void this.log('Poller not running');
32
+ this.log('Stopping server log poller...');
33
+ if (null !== this.intervalId) {
34
+ window.clearInterval(this.intervalId);
35
+ this.intervalId = null;
36
+ }
37
+ this.isPolling = false;
38
+ this.updateStatus('disconnected');
39
+ }
40
+ getStatus() {
41
+ return this.status;
42
+ }
43
+ async poll() {
44
+ try {
45
+ const url = `${this.options.serverUrl}${this.options.apiPath}?limit=${this.options.limit}&sources=server,trace,server-std,client-std`;
46
+ this.log('Polling...', {
47
+ url
48
+ });
49
+ const response = await fetch(url, {
50
+ method: 'GET',
51
+ headers: {
52
+ Accept: 'application/json'
53
+ },
54
+ signal: AbortSignal.timeout(5000)
55
+ });
56
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
57
+ const data = await response.json();
58
+ this.log('Received logs', {
59
+ count: data.logs.length,
60
+ total: data.total,
61
+ hasMore: data.hasMore
62
+ });
63
+ const newLogs = data.logs.filter((log)=>log.timestamp > this.lastTimestamp);
64
+ if (newLogs.length > 0) {
65
+ this.lastTimestamp = Math.max(...newLogs.map((log)=>log.timestamp));
66
+ newLogs.sort((a, b)=>a.timestamp - b.timestamp);
67
+ this.log(`Forwarding ${newLogs.length} new logs`);
68
+ newLogs.forEach((log)=>{
69
+ this.forwardLog(log);
70
+ });
71
+ }
72
+ if ('connected' !== this.status) {
73
+ this.updateStatus('connected');
74
+ if (0 === this.lastTimestamp && data.logs.length > 0) this.lastTimestamp = Math.max(...data.logs.map((log)=>log.timestamp));
75
+ }
76
+ } catch (error) {
77
+ this.error('Poll failed', error);
78
+ if ('error' !== this.status) {
79
+ this.updateStatus('error');
80
+ this.postToParent({
81
+ type: 'SERVER_LOG_CONNECTION',
82
+ status: 'error',
83
+ error: error instanceof Error ? error.message : String(error)
84
+ });
85
+ }
86
+ }
87
+ }
88
+ forwardLog(log) {
89
+ try {
90
+ this.log('Forwarding log to parent window', {
91
+ type: 'SERVER_LOG',
92
+ logId: log.id,
93
+ level: log.level,
94
+ tags: log.tags
95
+ });
96
+ this.postToParent({
97
+ type: 'SERVER_LOG',
98
+ payload: JSON.stringify(log)
99
+ });
100
+ this.log('Log forwarded successfully');
101
+ } catch (e) {
102
+ this.error('Failed to forward log', e);
103
+ }
104
+ }
105
+ updateStatus(status) {
106
+ const previousStatus = this.status;
107
+ this.status = status;
108
+ if (previousStatus !== status) {
109
+ this.log(`Status changed: ${previousStatus} → ${status}`);
110
+ this.postToParent({
111
+ type: 'SERVER_LOG_CONNECTION',
112
+ status
113
+ });
114
+ }
115
+ }
116
+ postToParent(message) {
117
+ if (window.parent === window) return;
118
+ try {
119
+ window.parent.postMessage(message, '*');
120
+ } catch (e) {
121
+ this.error('postMessage error', e);
122
+ }
123
+ }
124
+ log(message, data) {
125
+ if (this.options.debug) if (data) console.log(`[ServerLogPoller] ${message}`, data);
126
+ else console.log(`[ServerLogPoller] ${message}`);
127
+ }
128
+ error(message, error) {
129
+ if (error) console.error(`[ServerLogPoller] ${message}`, error);
130
+ else console.error(`[ServerLogPoller] ${message}`);
131
+ }
132
+ }
133
+ export { ServerLogPoller };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Server Log Stream 类型定义
3
+ *
4
+ * 这些类型与 @lark-apaas/nestjs-logger 保持一致
5
+ * 用于 iframe 端的日志转发功能
6
+ */
7
+ /**
8
+ * 后端日志级别
9
+ * 与 NestJS LogLevel 对齐,但增加了 fatal
10
+ */
11
+ export type ServerLogLevel = 'fatal' | 'error' | 'warn' | 'log' | 'debug' | 'verbose';
12
+ /**
13
+ * 日志来源
14
+ */
15
+ export type ServerLogSource = 'server' | 'trace' | 'server-std' | 'client-std' | 'browser';
16
+ /**
17
+ * 日志元数据
18
+ */
19
+ export interface ServerLogMeta {
20
+ pid?: number;
21
+ hostname?: string;
22
+ service?: string;
23
+ path?: string;
24
+ method?: string;
25
+ statusCode?: number;
26
+ durationMs?: number;
27
+ ip?: string;
28
+ requestBody?: any;
29
+ responseBody?: any;
30
+ [key: string]: unknown;
31
+ }
32
+ /**
33
+ * 后端日志对象
34
+ * 用于实时推送给前端展示
35
+ */
36
+ export interface ServerLog {
37
+ /**
38
+ * 唯一标识符(UUID)
39
+ * 用于前端去重和引用
40
+ */
41
+ id: string;
42
+ /**
43
+ * 日志级别
44
+ */
45
+ level: ServerLogLevel;
46
+ /**
47
+ * 时间戳(毫秒)
48
+ */
49
+ timestamp: number;
50
+ /**
51
+ * 日志消息
52
+ */
53
+ message: string;
54
+ /**
55
+ * 日志上下文(如 Controller 名称、Service 名称)
56
+ */
57
+ context?: string;
58
+ /**
59
+ * 请求追踪 ID
60
+ * 用于关联同一个请求的所有日志
61
+ */
62
+ traceId?: string;
63
+ /**
64
+ * 用户 ID
65
+ */
66
+ userId?: string;
67
+ /**
68
+ * 应用 ID
69
+ */
70
+ appId?: string;
71
+ /**
72
+ * 租户 ID
73
+ */
74
+ tenantId?: string;
75
+ /**
76
+ * 错误堆栈(仅 ERROR/FATAL 级别)
77
+ */
78
+ stack?: string;
79
+ /**
80
+ * 额外的元数据
81
+ */
82
+ meta?: ServerLogMeta;
83
+ /**
84
+ * 自定义标签
85
+ * 如 ['server', 'bootstrap'] 或 ['trace', 'http']
86
+ */
87
+ tags?: string[];
88
+ }
89
+ /**
90
+ * WebSocket 消息类型(客户端 → 服务端)
91
+ */
92
+ export type ClientToServerMessage = {
93
+ type: 'SUBSCRIBE';
94
+ payload: {
95
+ levels?: ServerLogLevel[];
96
+ tags?: string[];
97
+ sources?: ServerLogSource[];
98
+ };
99
+ } | {
100
+ type: 'UNSUBSCRIBE';
101
+ } | {
102
+ type: 'GET_HISTORY';
103
+ payload: {
104
+ limit?: number;
105
+ offset?: number;
106
+ levels?: ServerLogLevel[];
107
+ sources?: ServerLogSource[];
108
+ };
109
+ } | {
110
+ type: 'CLEAR_LOGS';
111
+ };
112
+ /**
113
+ * WebSocket 消息类型(服务端 → 客户端)
114
+ */
115
+ export type ServerToClientMessage = {
116
+ type: 'CONNECTED';
117
+ payload: {
118
+ clientId: string;
119
+ timestamp: number;
120
+ };
121
+ } | {
122
+ type: 'LOG';
123
+ payload: ServerLog;
124
+ } | {
125
+ type: 'LOGS_BATCH';
126
+ payload: ServerLog[];
127
+ } | {
128
+ type: 'HISTORY';
129
+ payload: {
130
+ logs: ServerLog[];
131
+ total: number;
132
+ hasMore: boolean;
133
+ };
134
+ } | {
135
+ type: 'LOGS_CLEARED';
136
+ } | {
137
+ type: 'SUBSCRIBED';
138
+ payload: {
139
+ levels?: ServerLogLevel[];
140
+ tags?: string[];
141
+ sources?: ServerLogSource[];
142
+ };
143
+ } | {
144
+ type: 'UNSUBSCRIBED';
145
+ } | {
146
+ type: 'ERROR';
147
+ payload: {
148
+ message: string;
149
+ code?: string;
150
+ };
151
+ };
152
+ /**
153
+ * PostMessage 类型(iframe → parent)
154
+ *
155
+ * iframe 端通过 postMessage 将接收到的 WebSocket 消息转发给父窗口
156
+ */
157
+ export type ServerLogPostMessage = {
158
+ type: 'SERVER_LOG';
159
+ payload: string;
160
+ } | {
161
+ type: 'SERVER_LOG_CONNECTION';
162
+ status: 'connected' | 'disconnected' | 'error' | 'connecting';
163
+ error?: string;
164
+ } | {
165
+ type: 'SERVER_LOG_CLEARED';
166
+ };
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/client-toolkit",
3
- "version": "1.1.24",
3
+ "version": "1.1.25-alpha.1",
4
4
  "types": "./lib/index.d.ts",
5
5
  "main": "./lib/index.js",
6
6
  "files": [
@@ -124,6 +124,8 @@
124
124
  "@tailwindcss/postcss": "^4.1.0",
125
125
  "@testing-library/jest-dom": "^6.6.4",
126
126
  "@testing-library/react": "^16.3.0",
127
+ "@types/blueimp-md5": "^2.18.2",
128
+ "@types/crypto-js": "^4.2.2",
127
129
  "@types/lodash": "^4.17.20",
128
130
  "@types/node": "^22.10.2",
129
131
  "@types/react": "^18.3.23",