@jacksontian/kite-server 0.1.0 → 0.4.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 +105 -0
- package/bin/server.js +191 -0
- package/config.example.yaml +31 -5
- package/deploy/kiteserver.service +24 -0
- package/lib/api.js +209 -0
- package/lib/auth.js +130 -0
- package/lib/broadcast.js +31 -0
- package/lib/cli/args.js +83 -0
- package/lib/cli/deploy.js +94 -0
- package/lib/cli/init.js +208 -0
- package/lib/cli/keygen.js +23 -0
- package/lib/cli/meta.js +56 -0
- package/lib/cli/util.js +44 -0
- package/lib/config.js +280 -0
- package/lib/im.js +70 -0
- package/lib/logger.js +129 -0
- package/lib/pty.js +55 -0
- package/lib/static.js +51 -0
- package/lib/store.js +115 -0
- package/lib/summary.js +29 -0
- package/lib/util.js +37 -0
- package/lib/websocket.js +261 -0
- package/package.json +25 -5
- package/web/index.html +305 -187
- package/web/pc.html +1104 -0
- package/web/vendor/addon-fit.min.js +8 -0
- package/web/vendor/xterm.min.css +8 -0
- package/web/vendor/xterm.min.js +8 -0
- package/config.js +0 -205
- package/server.js +0 -921
package/config.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 云端 server 配置加载与校验。
|
|
3
|
-
*
|
|
4
|
-
* 只认 --config 指定的 YAML 配置文件(不再读取任何环境变量):
|
|
5
|
-
* node server.js --config /path/to/server.yaml
|
|
6
|
-
* 结构与类型校验由 zod schema 完成(CONFIG_SCHEMA):未知配置项、
|
|
7
|
-
* 类型错误、缺必填项一次性报全后 exit 1;dataDir 的可创建/可写校验
|
|
8
|
-
* 是运行时行为,仍在校验通过后单独执行。模板见包内 config.example.yaml。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import os from 'node:os';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import YAML from 'yaml';
|
|
15
|
-
import { z } from 'zod';
|
|
16
|
-
|
|
17
|
-
/** 解析启动参数;--config 必填,缺失/重复/取不到值立即退出 */
|
|
18
|
-
function parseArgs() {
|
|
19
|
-
const args = process.argv.slice(2);
|
|
20
|
-
const values = [];
|
|
21
|
-
for (let i = 0; i < args.length; i++) {
|
|
22
|
-
if (args[i] === '--config' || args[i] === '-c') {
|
|
23
|
-
values.push(args[i + 1]);
|
|
24
|
-
i++;
|
|
25
|
-
} else if (args[i].startsWith('--config=')) {
|
|
26
|
-
values.push(args[i].slice('--config='.length));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (values.length === 0) {
|
|
30
|
-
console.error('[server] 缺少启动参数:node server.js --config <配置文件路径>');
|
|
31
|
-
console.error('[server] 配置模板见包内 config.example.yaml');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
if (values.length > 1) {
|
|
35
|
-
console.error('[server] --config 只能指定一次');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
const file = String(values[0] ?? '').trim();
|
|
39
|
-
if (!file || file.startsWith('-')) {
|
|
40
|
-
console.error('[server] --config 后需要给出配置文件路径');
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
return path.resolve(file);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function fail(errors, tag) {
|
|
47
|
-
console.error(`[${tag}] 配置错误,无法启动:`);
|
|
48
|
-
for (const e of errors) console.error(' - ' + e);
|
|
49
|
-
console.error(`[${tag}] 请检查 --config 指定的 YAML 文件,模板见包内 config.example.yaml`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isPlainObject(v) {
|
|
54
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 可选字符串:缺省/给 null 都按空串处理,统一 trim
|
|
58
|
-
* (zod v4 中 union(z.undefined()) 不能让对象键变可选,须用 .optional()) */
|
|
59
|
-
const optStr = (errMsg) =>
|
|
60
|
-
z.string({ error: errMsg }).trim().optional().nullable().transform((v) => v ?? '');
|
|
61
|
-
|
|
62
|
-
/** 把 zod issues 转成错误行:未知配置项逐条展开并附该层级可用项清单;
|
|
63
|
-
* custom/各叶子的报错文案均已自带字段名,直接透传 */
|
|
64
|
-
function formatIssues(issues, knownByPath) {
|
|
65
|
-
const out = [];
|
|
66
|
-
for (const iss of issues) {
|
|
67
|
-
if (iss.code === 'unrecognized_keys') {
|
|
68
|
-
const known = knownByPath[iss.path.join('.')] || [];
|
|
69
|
-
for (const key of iss.keys) out.push(`未知配置项 "${key}"(可用:${known.join(', ')})`);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
out.push(iss.message);
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** 展开开头的 ~ 为用户主目录(YAML 不会替我们做这件事) */
|
|
78
|
-
function expandHome(p) {
|
|
79
|
-
if (p === '~') return os.homedir();
|
|
80
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
81
|
-
return p;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const SECRET_HINT =
|
|
85
|
-
'(两端共享的签名密钥;生成:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))")';
|
|
86
|
-
|
|
87
|
-
const CONFIG_SCHEMA = z.strictObject({
|
|
88
|
-
apiSecret: z
|
|
89
|
-
.string({
|
|
90
|
-
error: (iss) =>
|
|
91
|
-
iss.input === undefined || iss.input === null
|
|
92
|
-
? `缺少 apiSecret${SECRET_HINT}`
|
|
93
|
-
: 'apiSecret 必须是字符串(建议加引号,避免被 YAML 解析为数字)',
|
|
94
|
-
})
|
|
95
|
-
.trim()
|
|
96
|
-
.min(1, `缺少 apiSecret${SECRET_HINT}`),
|
|
97
|
-
port: z.coerce
|
|
98
|
-
.number({ error: 'port 必须是数字' })
|
|
99
|
-
.int('port 必须是整数')
|
|
100
|
-
.min(1, 'port 超出范围 [1, 65535]')
|
|
101
|
-
.max(65535, 'port 超出范围 [1, 65535]')
|
|
102
|
-
.default(8787),
|
|
103
|
-
// 语义(~ 展开 / 相对启动工作目录 / 缺省 ~/.kite/data / 可写校验)
|
|
104
|
-
// 无法纯声明式表达,schema 只约束类型,解析后命令式处理
|
|
105
|
-
dataDir: optStr('dataDir 必须是字符串'),
|
|
106
|
-
maxTasks: z.coerce.number({ error: 'maxTasks 必须是数字' }).positive('maxTasks 必须是正数').default(200),
|
|
107
|
-
staleRunningSec: z.coerce
|
|
108
|
-
.number({ error: 'staleRunningSec 必须是数字' })
|
|
109
|
-
.positive('staleRunningSec 必须是正数')
|
|
110
|
-
.default(600),
|
|
111
|
-
sigWindowSec: z.coerce
|
|
112
|
-
.number({ error: 'sigWindowSec 必须是数字' })
|
|
113
|
-
.positive('sigWindowSec 必须是正数')
|
|
114
|
-
.default(300),
|
|
115
|
-
ptyIdleSec: z.coerce
|
|
116
|
-
.number({ error: 'ptyIdleSec 必须是数字' })
|
|
117
|
-
.positive('ptyIdleSec 必须是正数')
|
|
118
|
-
.default(90),
|
|
119
|
-
imWebhook: optStr('imWebhook 必须是字符串'),
|
|
120
|
-
imSecret: optStr('imSecret 必须是字符串'),
|
|
121
|
-
imNotifyEvents: z
|
|
122
|
-
.array(
|
|
123
|
-
// 与原实现一致:元素宽容转字符串后归一化小写再比枚举
|
|
124
|
-
z.coerce.string({ error: 'imNotifyEvents 元素必须是字符串' }).trim().toLowerCase().pipe(
|
|
125
|
-
z.enum(['done', 'failed', 'canceled'], {
|
|
126
|
-
error: (iss) => `imNotifyEvents 含非法事件 "${iss.input}"(可用:done|failed|canceled)`,
|
|
127
|
-
}),
|
|
128
|
-
),
|
|
129
|
-
{ error: 'imNotifyEvents 必须是数组(如 [failed, done])' },
|
|
130
|
-
)
|
|
131
|
-
.default(['failed']),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/** 加载并校验配置,返回归一化后的常量对象 */
|
|
135
|
-
export async function loadServerConfig() {
|
|
136
|
-
const tag = 'server';
|
|
137
|
-
const configFile = parseArgs();
|
|
138
|
-
|
|
139
|
-
let raw;
|
|
140
|
-
try {
|
|
141
|
-
raw = await fs.promises.readFile(configFile, 'utf8');
|
|
142
|
-
} catch (err) {
|
|
143
|
-
console.error(`[${tag}] 无法读取配置文件 ${configFile}: ${err.message}`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const doc = YAML.parseDocument(raw);
|
|
148
|
-
if (doc.errors.length) {
|
|
149
|
-
console.error(`[${tag}] 配置文件 YAML 语法错误(${configFile}):`);
|
|
150
|
-
for (const e of doc.errors) console.error(' - ' + e.message);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
const cfg = doc.toJS();
|
|
154
|
-
if (!isPlainObject(cfg)) {
|
|
155
|
-
console.error(`[${tag}] 配置文件内容必须是一个键值映射(${configFile})`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const parsed = CONFIG_SCHEMA.safeParse(cfg);
|
|
160
|
-
if (!parsed.success) {
|
|
161
|
-
fail(formatIssues(parsed.error.issues, { '': Object.keys(CONFIG_SCHEMA.shape) }), tag);
|
|
162
|
-
}
|
|
163
|
-
const v = parsed.data;
|
|
164
|
-
const errors = [];
|
|
165
|
-
|
|
166
|
-
// 未配置时默认主目录下的固定位置,与配置文件位置、启动目录都解耦;
|
|
167
|
-
// 相对路径相对启动工作目录解析(path.resolve 默认行为),建议写绝对路径
|
|
168
|
-
const dataDir = path.resolve(
|
|
169
|
-
expandHome(v.dataDir || path.join(os.homedir(), '.kite', 'data')),
|
|
170
|
-
);
|
|
171
|
-
// 启动前校验可创建、可写:误配在这里一次性报清,
|
|
172
|
-
// 而不是运行中落盘失败才静默降级(只打日志、数据留在内存)
|
|
173
|
-
try {
|
|
174
|
-
await fs.promises.mkdir(dataDir, { recursive: true });
|
|
175
|
-
const probe = path.join(dataDir, '.rw-write-probe');
|
|
176
|
-
await fs.promises.writeFile(probe, '');
|
|
177
|
-
await fs.promises.rm(probe);
|
|
178
|
-
} catch (err) {
|
|
179
|
-
if (err.code === 'EEXIST' || err.code === 'ENOTDIR') {
|
|
180
|
-
errors.push(`dataDir 指向的路径已存在但不是目录:${dataDir}`);
|
|
181
|
-
} else {
|
|
182
|
-
errors.push(`dataDir 无法创建或不可写(${dataDir}):${err.message};请确认运行用户对该目录有读写权限`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (errors.length) fail(errors, tag);
|
|
186
|
-
|
|
187
|
-
// 典型误配提示:配了加签密钥但没配 webhook,secret 会被静默忽略
|
|
188
|
-
if (v.imSecret && !v.imWebhook) {
|
|
189
|
-
console.warn(`[${tag}] 已配置 imSecret 但 imWebhook 为空,加签密钥将被忽略`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
configFile,
|
|
194
|
-
apiSecret: v.apiSecret,
|
|
195
|
-
port: v.port,
|
|
196
|
-
dataDir,
|
|
197
|
-
maxTasks: v.maxTasks,
|
|
198
|
-
staleRunningMs: v.staleRunningSec * 1000,
|
|
199
|
-
sigWindowSec: v.sigWindowSec,
|
|
200
|
-
ptyIdleMs: v.ptyIdleSec * 1000,
|
|
201
|
-
imWebhook: v.imWebhook,
|
|
202
|
-
imSecret: v.imSecret,
|
|
203
|
-
imNotifyEvents: new Set(v.imNotifyEvents),
|
|
204
|
-
};
|
|
205
|
-
}
|