@jacksontian/kite-server 0.4.0 → 0.5.0
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.
- package/README.md +11 -11
- package/bin/server.js +8 -8
- package/config.example.yaml +5 -5
- package/lib/api.js +121 -81
- package/lib/auth.js +1 -1
- package/lib/broadcast.js +3 -3
- package/lib/config.js +2 -2
- package/lib/im.js +23 -23
- package/lib/logger.js +1 -1
- package/lib/pty.js +16 -16
- package/lib/static.js +5 -10
- package/lib/store.js +31 -21
- package/lib/summary.js +4 -2
- package/lib/util.js +5 -5
- package/lib/websocket.js +41 -35
- package/package.json +2 -2
- package/web/app.js +1107 -0
- package/web/favicon.svg +18 -0
- package/web/index.html +143 -986
- package/web/style.css +486 -0
- package/web/pc.html +0 -1104
package/lib/static.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 静态文件(Web
|
|
2
|
+
* 静态文件(Web 控制台:单页应用,index.html 一份,手机/PC 自适应)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -13,16 +13,10 @@ const MIME = {
|
|
|
13
13
|
'.png': 'image/png',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
// 移动端 UA 特征词;命中走手机版,其余(桌面浏览器)走 PC 版。
|
|
17
|
-
// iPadOS 13+ 默认伪装 Mac UA,会落到 PC 版——需要时可显式访问 /index.html
|
|
18
|
-
const MOBILE_UA = /android|iphone|ipod|ipad|mobile|harmonyos/i;
|
|
19
|
-
|
|
20
16
|
export function createStaticServer(WEB_DIR) {
|
|
21
17
|
return function serveStatic(req, res, pathname) {
|
|
22
18
|
let rel = pathname.replace(/^\/+/, '');
|
|
23
|
-
if (!rel)
|
|
24
|
-
rel = MOBILE_UA.test(req.headers['user-agent'] || '') ? 'index.html' : 'pc.html';
|
|
25
|
-
}
|
|
19
|
+
if (!rel) rel = 'index.html';
|
|
26
20
|
// resolve 归一化 ../ 后按「目录 + 分隔符」前缀判断,避免同级前缀目录(web-xxx)绕过
|
|
27
21
|
const filePath = path.resolve(WEB_DIR, rel);
|
|
28
22
|
if (filePath !== WEB_DIR && !filePath.startsWith(WEB_DIR + path.sep)) {
|
|
@@ -38,8 +32,9 @@ export function createStaticServer(WEB_DIR) {
|
|
|
38
32
|
'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream',
|
|
39
33
|
'Cache-Control': 'no-store',
|
|
40
34
|
// 纵深防御:页面资源一律限同源,阻断潜在注入脚本把密钥外传(connect-src
|
|
41
|
-
// 的 'self' 按 CSP3 含同源 ws/wss
|
|
42
|
-
//
|
|
35
|
+
// 的 'self' 按 CSP3 含同源 ws/wss,实时通道不受影响);应用逻辑在外部
|
|
36
|
+
// app.js('self' 放行),head 仅剩主题初始化内联脚本,故 script-src 还需
|
|
37
|
+
// 'unsafe-inline';style-src 同理(行内样式操作)
|
|
43
38
|
'Content-Security-Policy':
|
|
44
39
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " +
|
|
45
40
|
"img-src 'self' data:; connect-src 'self'",
|
package/lib/store.js
CHANGED
|
@@ -1,43 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 持久化:单文件 JSON,异步落盘(脏标记 + 节流 + 串行化,全程无同步 IO);
|
|
3
|
-
*
|
|
3
|
+
* 以及卡死会话回收(sweepStaleRunning)。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
9
9
|
export function createStore(ctx) {
|
|
10
|
-
const DB_FILE = path.join(ctx.cfg.dataDir, '
|
|
11
|
-
const
|
|
10
|
+
const DB_FILE = path.join(ctx.cfg.dataDir, 'sessions.json');
|
|
11
|
+
const MAX_SESSIONS = ctx.cfg.maxSessions;
|
|
12
12
|
const STALE_RUNNING_MS = ctx.cfg.staleRunningMs;
|
|
13
13
|
|
|
14
|
-
const state = {
|
|
14
|
+
const state = { sessions: [], heartbeats: {} };
|
|
15
15
|
|
|
16
16
|
async function loadState() {
|
|
17
17
|
let raw;
|
|
18
18
|
try {
|
|
19
19
|
raw = await fs.promises.readFile(DB_FILE, 'utf8');
|
|
20
20
|
} catch {
|
|
21
|
-
|
|
21
|
+
// 旧版本数据文件叫 tasks.json:新文件不存在时迁移读取并改名,避免丢历史会话
|
|
22
|
+
const legacy = path.join(ctx.cfg.dataDir, 'tasks.json');
|
|
23
|
+
try {
|
|
24
|
+
raw = await fs.promises.readFile(legacy, 'utf8');
|
|
25
|
+
await fs.promises.rename(legacy, DB_FILE);
|
|
26
|
+
console.log('[server] 已将历史数据文件 tasks.json 迁移为 sessions.json');
|
|
27
|
+
} catch {
|
|
28
|
+
return; // 首次启动,文件尚不存在,从空状态开始
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
try {
|
|
24
32
|
const parsed = JSON.parse(raw);
|
|
25
|
-
|
|
33
|
+
// 旧文件数组键为 tasks,新文件为 sessions,两者都兼容(迁移改名后下次落盘即刷新)
|
|
34
|
+
state.sessions = Array.isArray(parsed.sessions) ? parsed.sessions
|
|
35
|
+
: Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
26
36
|
state.heartbeats = parsed.heartbeats || {};
|
|
27
37
|
} catch {
|
|
28
38
|
// 文件损坏:先备份,避免被后续写盘静默覆盖丢失历史数据
|
|
29
|
-
console.warn('[server]
|
|
39
|
+
console.warn('[server] sessions.json 已损坏,备份后从空状态启动');
|
|
30
40
|
await fs.promises.rename(DB_FILE, `${DB_FILE}.corrupt-${Date.now()}`).catch(() => {});
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
// 只保留最近
|
|
35
|
-
function
|
|
36
|
-
const finished = state.
|
|
37
|
-
if (finished.length >
|
|
44
|
+
// 只保留最近 MAX_SESSIONS 个终态会话,进行中的不受影响,防止无限增长
|
|
45
|
+
function pruneFinishedSessions() {
|
|
46
|
+
const finished = state.sessions.filter((t) => ['done', 'failed', 'canceled'].includes(t.status));
|
|
47
|
+
if (finished.length > MAX_SESSIONS) {
|
|
38
48
|
const sorted = finished.sort((a, b) => b.finishedAt - a.finishedAt);
|
|
39
|
-
const cutoff = sorted[
|
|
40
|
-
state.
|
|
49
|
+
const cutoff = sorted[MAX_SESSIONS - 1].finishedAt;
|
|
50
|
+
state.sessions = state.sessions.filter(
|
|
41
51
|
(t) => !['done', 'failed', 'canceled'].includes(t.status) || t.finishedAt >= cutoff,
|
|
42
52
|
);
|
|
43
53
|
}
|
|
@@ -45,7 +55,7 @@ export function createStore(ctx) {
|
|
|
45
55
|
|
|
46
56
|
// 落盘策略:脏标记 + 300ms 节流合并高频写(心跳/日志上报),串行链防交叠,
|
|
47
57
|
// tmp + rename 原子替换。崩溃最多丢约 300ms 的变更,
|
|
48
|
-
// 卡住的 running
|
|
58
|
+
// 卡住的 running 会话由心跳超时回收兜底,可接受。
|
|
49
59
|
let savePending = false;
|
|
50
60
|
let saveTimer = null;
|
|
51
61
|
let savingChain = Promise.resolve();
|
|
@@ -53,11 +63,11 @@ export function createStore(ctx) {
|
|
|
53
63
|
async function flushSave() {
|
|
54
64
|
if (!savePending) return;
|
|
55
65
|
savePending = false; // 先清脏标记再序列化快照;写失败会重新标脏重试
|
|
56
|
-
|
|
66
|
+
pruneFinishedSessions();
|
|
57
67
|
try {
|
|
58
68
|
const data = JSON.stringify(state, null, 2);
|
|
59
69
|
const tmp = DB_FILE + '.tmp';
|
|
60
|
-
// 0600:
|
|
70
|
+
// 0600:sessions.json 含全部命令/日志/结果,不允许同机其他用户读取
|
|
61
71
|
await fs.promises.writeFile(tmp, data, { mode: 0o600 });
|
|
62
72
|
await fs.promises.chmod(tmp, 0o600); // writeFile 的 mode 只在创建时生效,历史文件需显式收紧
|
|
63
73
|
await fs.promises.rename(tmp, DB_FILE);
|
|
@@ -83,7 +93,7 @@ export function createStore(ctx) {
|
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
// Worker 崩溃/掉线后,回收卡死在 running
|
|
96
|
+
// Worker 崩溃/掉线后,回收卡死在 running 的会话。
|
|
87
97
|
// pty 交互会话例外:时长不可预期,由 ptyIdleSec 空闲回收(见 sweepPtyIdle)
|
|
88
98
|
function sweepStaleRunning() {
|
|
89
99
|
const now = Date.now();
|
|
@@ -96,16 +106,16 @@ export function createStore(ctx) {
|
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
if (hbPruned) scheduleSave();
|
|
99
|
-
for (const t of state.
|
|
109
|
+
for (const t of state.sessions) {
|
|
100
110
|
if (t.status !== 'running' || t.type === 'pty') continue;
|
|
101
111
|
const hb = t.workerId ? state.heartbeats[t.workerId] : null;
|
|
102
112
|
const workerAlive = hb && now - hb.at < 90 * 1000;
|
|
103
113
|
if (!workerAlive && t.startedAt && now - t.startedAt > STALE_RUNNING_MS) {
|
|
104
114
|
t.status = 'failed';
|
|
105
115
|
t.finishedAt = now;
|
|
106
|
-
t.result = { error: 'Worker
|
|
107
|
-
t.logs.push('[server] Worker
|
|
108
|
-
ctx.broadcast({ type: '
|
|
116
|
+
t.result = { error: 'Worker 离线超时,会话被服务端回收' };
|
|
117
|
+
t.logs.push('[server] Worker 心跳超时,会话被标记为失败');
|
|
118
|
+
ctx.broadcast({ type: 'session.updated', session: ctx.sessionSummary(t), result: t.result });
|
|
109
119
|
ctx.notifyIm(t, 'failed');
|
|
110
120
|
}
|
|
111
121
|
}
|
package/lib/summary.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 会话/Worker 摘要视图:WS 推送与 API 列表共用的只读投影。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function sessionSummary(t) {
|
|
6
6
|
return {
|
|
7
7
|
id: t.id,
|
|
8
8
|
type: t.type,
|
|
9
9
|
title: t.title,
|
|
10
|
+
project: (t.payload && t.payload.project) || null,
|
|
10
11
|
status: t.status,
|
|
11
12
|
createdAt: t.createdAt,
|
|
12
13
|
startedAt: t.startedAt,
|
|
13
14
|
finishedAt: t.finishedAt,
|
|
15
|
+
openedAt: t.openedAt || null,
|
|
14
16
|
workerId: t.workerId,
|
|
15
17
|
targetWorker: t.targetWorker || null,
|
|
16
18
|
logCount: t.logs.length,
|
package/lib/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 服务端通用小工具:常量时间比较、JSON
|
|
2
|
+
* 服务端通用小工具:常量时间比较、JSON 响应、请求体解析、会话公开视图。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
@@ -30,8 +30,8 @@ export function parseBody(rawBody) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function
|
|
34
|
-
// 浅拷贝切断与内部状态的引用别名,防止响应侧改动波及 state.
|
|
35
|
-
//
|
|
36
|
-
return { ...
|
|
33
|
+
export function publicSession(session) {
|
|
34
|
+
// 浅拷贝切断与内部状态的引用别名,防止响应侧改动波及 state.sessions;
|
|
35
|
+
// 会话现有字段(含 logs/result/payload)均为控制台可见,无需裁剪
|
|
36
|
+
return { ...session };
|
|
37
37
|
}
|
package/lib/websocket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket 连接管理:/ws 控制台实时广播 + pty 终端输入上行;
|
|
3
|
-
* /worker/ws Worker
|
|
3
|
+
* /worker/ws Worker 会话通知与 pty 会话 IO 中继。
|
|
4
4
|
* 鉴权走连接后的第一条消息(Token 不进 URL,不落日志)。
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -57,7 +57,7 @@ export function createWebSockets(ctx) {
|
|
|
57
57
|
ws.send(
|
|
58
58
|
JSON.stringify({
|
|
59
59
|
type: 'hello',
|
|
60
|
-
|
|
60
|
+
sessions: [...ctx.state.sessions].sort((a, b) => b.createdAt - a.createdAt).slice(0, 50).map(ctx.sessionSummary),
|
|
61
61
|
workers: workersSnapshot(ctx.state.heartbeats),
|
|
62
62
|
}),
|
|
63
63
|
);
|
|
@@ -71,32 +71,32 @@ export function createWebSockets(ctx) {
|
|
|
71
71
|
} catch {
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
if (!msg || typeof msg.
|
|
75
|
-
const
|
|
74
|
+
if (!msg || typeof msg.sessionId !== 'string') return;
|
|
75
|
+
const sessionId = msg.sessionId.slice(0, 64);
|
|
76
76
|
|
|
77
77
|
if (msg.type === 'pty.attach') {
|
|
78
78
|
// 打开终端:先补回看缓冲,再告知 Worker 侧会话是否仍活;
|
|
79
79
|
// 不活时附带 reason,控制台据此区分等待指引:
|
|
80
|
-
// not_claimed
|
|
80
|
+
// not_claimed 会话还在 pending,等 Worker 上线领取
|
|
81
81
|
// worker_offline 会话在册但 Worker 通道断开,重连后自动重新挂载
|
|
82
82
|
// starting 已领取但 session.started 未到(或已退出待 finish 上报)
|
|
83
|
-
// ended
|
|
84
|
-
const s = ctx.ptySessions.get(
|
|
83
|
+
// ended 会话已终态或不存在,会话已结束/被回收
|
|
84
|
+
const s = ctx.ptySessions.get(sessionId);
|
|
85
85
|
const live = Boolean(s && s.ws && s.ws.readyState === s.ws.OPEN);
|
|
86
86
|
let reason;
|
|
87
87
|
if (!live) {
|
|
88
88
|
if (s) {
|
|
89
89
|
reason = 'worker_offline';
|
|
90
90
|
} else {
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
93
|
-
else if (
|
|
91
|
+
const session = ctx.state.sessions.find((t) => t.id === sessionId);
|
|
92
|
+
if (!session || ['done', 'failed', 'canceled'].includes(session.status)) reason = 'ended';
|
|
93
|
+
else if (session.status === 'pending') reason = 'not_claimed';
|
|
94
94
|
else reason = 'starting';
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
ctx.wsSendJson(ws, {
|
|
98
98
|
type: 'pty.attach',
|
|
99
|
-
id:
|
|
99
|
+
id: sessionId,
|
|
100
100
|
tail: s ? s.tail : '',
|
|
101
101
|
live,
|
|
102
102
|
reason,
|
|
@@ -104,26 +104,26 @@ export function createWebSockets(ctx) {
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
if (msg.type === 'session.input') {
|
|
107
|
-
const s = ctx.ptySessions.get(
|
|
108
|
-
if (!s) return ctx.wsSendJson(ws, { type: 'pty.exited', id:
|
|
107
|
+
const s = ctx.ptySessions.get(sessionId);
|
|
108
|
+
if (!s) return ctx.wsSendJson(ws, { type: 'pty.exited', id: sessionId, reason: '会话已结束' });
|
|
109
109
|
s.lastActiveAt = Date.now();
|
|
110
|
-
ctx.wsSendJson(s.ws, { type: 'session.input',
|
|
110
|
+
ctx.wsSendJson(s.ws, { type: 'session.input', sessionId, data: String(msg.data || '').slice(0, 65536) });
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
if (msg.type === 'session.resize') {
|
|
114
|
-
const s = ctx.ptySessions.get(
|
|
114
|
+
const s = ctx.ptySessions.get(sessionId);
|
|
115
115
|
if (!s) return;
|
|
116
116
|
ctx.wsSendJson(s.ws, {
|
|
117
117
|
type: 'session.resize',
|
|
118
|
-
|
|
118
|
+
sessionId,
|
|
119
119
|
cols: Number(msg.cols) || 80,
|
|
120
120
|
rows: Number(msg.rows) || 24,
|
|
121
121
|
});
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
if (msg.type === 'session.close') {
|
|
125
|
-
const s = ctx.ptySessions.get(
|
|
126
|
-
if (s) ctx.wsSendJson(s.ws, { type: 'session.close',
|
|
125
|
+
const s = ctx.ptySessions.get(sessionId);
|
|
126
|
+
if (s) ctx.wsSendJson(s.ws, { type: 'session.close', sessionId });
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
|
|
@@ -137,7 +137,7 @@ export function createWebSockets(ctx) {
|
|
|
137
137
|
ws.on('error', () => {});
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
// --- Worker 通道 /worker/ws:鉴权方式同 /ws;鉴权后下发
|
|
140
|
+
// --- Worker 通道 /worker/ws:鉴权方式同 /ws;鉴权后下发 session.available,
|
|
141
141
|
// 并承载 pty 会话上行消息(session.started/output/exited)---
|
|
142
142
|
const workerWss = new WebSocketServer(WS_OPTS);
|
|
143
143
|
|
|
@@ -161,7 +161,7 @@ export function createWebSockets(ctx) {
|
|
|
161
161
|
ws.close(4002, 'bad auth message');
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
-
// Worker 通道只认 Worker 侧密钥:控制台密钥泄露不能冒充 Worker
|
|
164
|
+
// Worker 通道只认 Worker 侧密钥:控制台密钥泄露不能冒充 Worker 领会话。
|
|
165
165
|
// 逐一尝试 WORKER_AUTH 条目;命中 workerSecrets 绑定密钥时身份以绑定
|
|
166
166
|
// 为准(自报 workerId 不一致即拒绝),共享密钥为兼容路径(身份自报)
|
|
167
167
|
let result = { ok: false, reason: '缺少签名头' };
|
|
@@ -197,9 +197,9 @@ export function createWebSockets(ctx) {
|
|
|
197
197
|
ws.workerId = boundId || claimedId;
|
|
198
198
|
ctx.wsWorkers.add(ws);
|
|
199
199
|
console.log(`[server] Worker ${ws.workerId} 通道已连接(来源 ${remote})`);
|
|
200
|
-
//
|
|
201
|
-
if (ctx.state.
|
|
202
|
-
ws.send(JSON.stringify({ type: '
|
|
200
|
+
// 断线窗口内可能已有积压会话,连接建立即补一次通知(只推指名本 Worker 的)
|
|
201
|
+
if (ctx.state.sessions.some((t) => t.status === 'pending' && t.targetWorker === ws.workerId)) {
|
|
202
|
+
ws.send(JSON.stringify({ type: 'session.available' }));
|
|
203
203
|
}
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
@@ -211,34 +211,40 @@ export function createWebSockets(ctx) {
|
|
|
211
211
|
} catch {
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
|
-
if (!msg || typeof msg.
|
|
215
|
-
const
|
|
216
|
-
const
|
|
214
|
+
if (!msg || typeof msg.sessionId !== 'string') return;
|
|
215
|
+
const sessionId = msg.sessionId.slice(0, 64);
|
|
216
|
+
const session = ctx.state.sessions.find((t) => t.id === sessionId);
|
|
217
217
|
|
|
218
218
|
if (msg.type === 'session.started') {
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
// 无记录(已被控制台关闭删除)或已终态:拒绝重挂并通知 Worker 回收残留 PTY,
|
|
220
|
+
// 避免孤儿进程常驻
|
|
221
|
+
if (!session || ['done', 'failed', 'canceled'].includes(session.status)) {
|
|
222
|
+
ctx.wsSendJson(ws, { type: 'session.close', sessionId });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// 会话注册(含 Worker 通道重连后的重新挂载);只认该 Worker 自己领取的会话
|
|
226
|
+
if (session.type !== 'pty' || session.workerId !== ws.workerId) return;
|
|
227
|
+
const prev = ctx.ptySessions.get(sessionId);
|
|
228
|
+
ctx.ptySessions.set(sessionId, {
|
|
223
229
|
ws,
|
|
224
230
|
tail: prev ? prev.tail : '',
|
|
225
231
|
lastActiveAt: Date.now(),
|
|
226
232
|
});
|
|
227
|
-
ctx.broadcast({ type: 'pty.started', id:
|
|
233
|
+
ctx.broadcast({ type: 'pty.started', id: sessionId, pid: Number(msg.pid) || 0 });
|
|
228
234
|
return;
|
|
229
235
|
}
|
|
230
236
|
if (msg.type === 'session.output') {
|
|
231
|
-
const s = ctx.ptySessions.get(
|
|
237
|
+
const s = ctx.ptySessions.get(sessionId);
|
|
232
238
|
if (!s) return;
|
|
233
239
|
const data = String(msg.data || '');
|
|
234
240
|
s.lastActiveAt = Date.now();
|
|
235
241
|
s.tail = (s.tail + data).slice(-PTY_TAIL_BYTES); // 回看缓冲,重连可重放
|
|
236
|
-
ctx.broadcast({ type: 'pty.data', id:
|
|
242
|
+
ctx.broadcast({ type: 'pty.data', id: sessionId, data });
|
|
237
243
|
return;
|
|
238
244
|
}
|
|
239
245
|
if (msg.type === 'session.exited') {
|
|
240
|
-
//
|
|
241
|
-
ctx.closePtySession(
|
|
246
|
+
// 只清会话映射;会话终态由 Worker 随后的 HTTP finish 上报负责
|
|
247
|
+
ctx.closePtySession(sessionId, {});
|
|
242
248
|
}
|
|
243
249
|
});
|
|
244
250
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacksontian/kite-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "kite 云端信箱 API:HMAC
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "kite 云端信箱 API:HMAC 签名鉴权、会话队列、WebSocket 实时推送、手机端 Web 控制台",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|