onebots 1.0.7 → 1.1.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.
@@ -0,0 +1,164 @@
1
+ import * as pty from "@karinjs/node-pty";
2
+ import { existsSync, readFileSync } from "fs";
3
+ /** SSE 心跳间隔(毫秒) */
4
+ const SSE_HEARTBEAT_INTERVAL_MS = 30000;
5
+ /** 终端重启延迟(毫秒) */
6
+ const TERMINAL_RESTART_DELAY_MS = 500;
7
+ /**
8
+ * Register terminal and log-streaming endpoints.
9
+ *
10
+ * WS /api/terminal — interactive PTY terminal via WebSocket
11
+ * SSE /api/logs — real-time log stream (stdout / stderr interception)
12
+ */
13
+ export function registerTerminalRoutes(app, router) {
14
+ const expectedAccessToken = (app.config.access_token?.trim()
15
+ || process.env.ONEBOTS_ACCESS_TOKEN?.trim()
16
+ || undefined);
17
+ /** Extract a Bearer token or ?access_token from an http.IncomingMessage. */
18
+ const getTokenFromRequest = (request) => {
19
+ const authHeader = request.headers.authorization;
20
+ if (authHeader && typeof authHeader === 'string') {
21
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
22
+ return match ? match[1] : authHeader;
23
+ }
24
+ try {
25
+ const url = new URL(request.url || '/', 'http://localhost');
26
+ return url.searchParams.get('access_token') || undefined;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ };
32
+ /* ── PTY 终端 WebSocket ────────────────────────────────────── */
33
+ const terminalWs = router.ws("/api/terminal");
34
+ terminalWs.on("connection", (client, request) => {
35
+ const token = getTokenFromRequest(request);
36
+ const valid = !!token && (expectedAccessToken
37
+ ? token === expectedAccessToken
38
+ : app.tokenManager.validateToken(token).valid);
39
+ if (!valid) {
40
+ client.close(1008, "Unauthorized");
41
+ return;
42
+ }
43
+ // 创建 PTY 终端实例(如果不存在)
44
+ if (!app.ptyTerminal) {
45
+ const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
46
+ app.ptyTerminal = pty.spawn(shell, [], {
47
+ name: "xterm-color",
48
+ cols: 80,
49
+ rows: 30,
50
+ cwd: process.env.HOME,
51
+ env: process.env,
52
+ });
53
+ // 监听 PTY 输出
54
+ app.ptyTerminal.onData((data) => {
55
+ // 广播到所有连接的客户端
56
+ app.terminalClients.forEach((c) => {
57
+ try {
58
+ c.send(JSON.stringify({ type: 'output', data }));
59
+ }
60
+ catch (e) {
61
+ app.terminalClients.delete(c);
62
+ }
63
+ });
64
+ });
65
+ // 监听 PTY 退出
66
+ app.ptyTerminal.onExit(() => {
67
+ app.ptyTerminal = null;
68
+ app.terminalClients.forEach((c) => {
69
+ try {
70
+ c.send(JSON.stringify({ type: 'exit' }));
71
+ }
72
+ catch {
73
+ // 客户端可能已断开,将在后续连接清理中移除
74
+ }
75
+ });
76
+ app.terminalClients.clear();
77
+ });
78
+ }
79
+ // 添加到客户端列表
80
+ app.terminalClients.add(client);
81
+ // 监听客户端消息(用户输入)
82
+ client.on("message", (msg) => {
83
+ try {
84
+ const payload = JSON.parse(msg.toString());
85
+ if (payload.type === 'input' && app.ptyTerminal) {
86
+ app.ptyTerminal.write(payload.data);
87
+ }
88
+ else if (payload.type === 'resize' && app.ptyTerminal) {
89
+ app.ptyTerminal.resize(payload.cols, payload.rows);
90
+ }
91
+ else if (payload.type === 'restart') {
92
+ // 通知所有客户端
93
+ app.terminalClients.forEach((c) => {
94
+ try {
95
+ c.send(JSON.stringify({ type: 'output', data: '\r\n\x1b[33m[服务即将重启]\x1b[0m' }));
96
+ }
97
+ catch {
98
+ // 客户端可能已断开,忽略
99
+ }
100
+ });
101
+ setTimeout(() => process.exit(100), TERMINAL_RESTART_DELAY_MS);
102
+ }
103
+ }
104
+ catch (e) {
105
+ app.logger.error('终端消息处理失败:', e);
106
+ }
107
+ });
108
+ // 监听客户端断开
109
+ client.on("close", () => {
110
+ app.terminalClients.delete(client);
111
+ // 如果没有客户端了,关闭 PTY
112
+ if (app.terminalClients.size === 0 && app.ptyTerminal) {
113
+ app.ptyTerminal.kill();
114
+ app.ptyTerminal = null;
115
+ }
116
+ });
117
+ });
118
+ /* ── 日志流 SSE ───────────────────────────────────────────── */
119
+ router.get("/api/logs", (ctx) => {
120
+ ctx.request.socket.setTimeout(0);
121
+ ctx.req.socket.setNoDelay(true);
122
+ ctx.req.socket.setKeepAlive(true);
123
+ ctx.set({
124
+ 'Content-Type': 'text/event-stream',
125
+ 'Cache-Control': 'no-cache',
126
+ 'Connection': 'keep-alive',
127
+ 'Access-Control-Allow-Origin': '*',
128
+ 'Access-Control-Allow-Headers': 'Content-Type',
129
+ });
130
+ ctx.status = 200;
131
+ ctx.respond = false;
132
+ app.logClients.add(ctx.res);
133
+ // 发送缓存日志到客户端
134
+ try {
135
+ if (existsSync(app.logCacheFile)) {
136
+ const cachedLogs = readFileSync(app.logCacheFile, 'utf-8');
137
+ if (cachedLogs) {
138
+ // 将历史日志的 \n 也替换为 \r\n
139
+ const terminalLogs = cachedLogs.replace(/\n/g, '\r\n');
140
+ ctx.res.write(`data: ${JSON.stringify({ message: terminalLogs })}\n\n`);
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ app.logger.error('读取日志缓存失败:', error);
146
+ }
147
+ // 定时发送心跳
148
+ const heartbeat = setInterval(() => {
149
+ try {
150
+ ctx.res.write(': heartbeat\n\n');
151
+ }
152
+ catch (error) {
153
+ clearInterval(heartbeat);
154
+ app.logClients.delete(ctx.res);
155
+ }
156
+ }, SSE_HEARTBEAT_INTERVAL_MS);
157
+ // 监听连接关闭
158
+ ctx.req.on('close', () => {
159
+ clearInterval(heartbeat);
160
+ app.logClients.delete(ctx.res);
161
+ });
162
+ });
163
+ }
164
+ //# sourceMappingURL=terminal.js.map
@@ -0,0 +1,17 @@
1
+ import type { Router } from "@onebots/core";
2
+ import type { App } from "../app.js";
3
+ /**
4
+ * Register verification-related routes for adapter login flows (device lock, SMS, etc.).
5
+ *
6
+ * Routes:
7
+ * GET /api/verification/stream — SSE endpoint; pushes verification events to the web UI
8
+ * GET /api/verification/pending — returns pending verification requests
9
+ * POST /api/verification/request-sms — ask the adapter to send an SMS code
10
+ * POST /api/verification/submit — submit a completed verification (slider, SMS, …)
11
+ *
12
+ * Verification-event broadcasting is wired by the App class lifecycle hooks
13
+ * (onAdapterCreated / the adapter subscription loop) — this module only
14
+ * handles the HTTP / SSE endpoints.
15
+ */
16
+ export declare function registerVerificationRoutes(app: App, router: Router): void;
17
+ //# sourceMappingURL=verification.d.ts.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Register verification-related routes for adapter login flows (device lock, SMS, etc.).
3
+ *
4
+ * Routes:
5
+ * GET /api/verification/stream — SSE endpoint; pushes verification events to the web UI
6
+ * GET /api/verification/pending — returns pending verification requests
7
+ * POST /api/verification/request-sms — ask the adapter to send an SMS code
8
+ * POST /api/verification/submit — submit a completed verification (slider, SMS, …)
9
+ *
10
+ * Verification-event broadcasting is wired by the App class lifecycle hooks
11
+ * (onAdapterCreated / the adapter subscription loop) — this module only
12
+ * handles the HTTP / SSE endpoints.
13
+ */
14
+ export function registerVerificationRoutes(app, router) {
15
+ // 验证流 SSE 端点(登录验证事件推送到 Web)
16
+ router.get("/api/verification/stream", (ctx) => {
17
+ ctx.request.socket.setTimeout(0);
18
+ ctx.req.socket.setNoDelay(true);
19
+ ctx.req.socket.setKeepAlive(true);
20
+ ctx.set({
21
+ 'Content-Type': 'text/event-stream',
22
+ 'Cache-Control': 'no-cache',
23
+ 'Connection': 'keep-alive',
24
+ 'Access-Control-Allow-Origin': '*',
25
+ 'Access-Control-Allow-Headers': 'Content-Type',
26
+ });
27
+ ctx.status = 200;
28
+ ctx.respond = false;
29
+ app.verificationClients.add(ctx.res);
30
+ const heartbeat = setInterval(() => {
31
+ try {
32
+ ctx.res.write(': heartbeat\n\n');
33
+ }
34
+ catch (error) {
35
+ clearInterval(heartbeat);
36
+ app.verificationClients.delete(ctx.res);
37
+ }
38
+ }, 30000);
39
+ ctx.req.on('close', () => {
40
+ clearInterval(heartbeat);
41
+ app.verificationClients.delete(ctx.res);
42
+ });
43
+ });
44
+ // 待处理验证列表(Web 打开页面时拉取,避免离线期间错过验证)
45
+ router.get("/api/verification/pending", (ctx) => {
46
+ ctx.body = app.getPendingVerificationList();
47
+ });
48
+ // 请求发送短信验证码(设备锁带手机号时,用户选短信验证前调用)
49
+ router.post("/api/verification/request-sms", async (ctx) => {
50
+ try {
51
+ const body = ctx.request.body || {};
52
+ const platform = String(body.platform ?? '');
53
+ const account_id = String(body.account_id ?? '');
54
+ if (!platform || !account_id) {
55
+ ctx.status = 400;
56
+ ctx.body = { success: false, message: '缺少 platform 或 account_id' };
57
+ return;
58
+ }
59
+ const adapter = app.adapters.get(platform);
60
+ if (!adapter) {
61
+ ctx.status = 404;
62
+ ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
63
+ return;
64
+ }
65
+ const requestSms = adapter.requestSmsCode;
66
+ if (typeof requestSms !== 'function') {
67
+ ctx.status = 501;
68
+ ctx.body = { success: false, message: `适配器 ${platform} 不支持请求短信验证码` };
69
+ return;
70
+ }
71
+ await Promise.resolve(requestSms.call(adapter, account_id));
72
+ ctx.body = { success: true };
73
+ }
74
+ catch (e) {
75
+ const err = e;
76
+ ctx.status = 500;
77
+ ctx.body = { success: false, message: err?.message ?? '请求失败' };
78
+ }
79
+ });
80
+ // 验证提交接口(Web 完成滑块/短信等后提交)
81
+ router.post("/api/verification/submit", async (ctx) => {
82
+ try {
83
+ const body = ctx.request.body || {};
84
+ const platform = String(body.platform ?? '');
85
+ const account_id = String(body.account_id ?? '');
86
+ const type = String(body.type ?? '');
87
+ const data = body.data && typeof body.data === 'object' ? body.data : {};
88
+ if (!platform || !account_id || !type) {
89
+ ctx.status = 400;
90
+ ctx.body = { success: false, message: '缺少 platform、account_id 或 type' };
91
+ return;
92
+ }
93
+ const adapter = app.adapters.get(platform);
94
+ if (!adapter) {
95
+ ctx.status = 404;
96
+ ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
97
+ return;
98
+ }
99
+ const submit = adapter.submitVerification;
100
+ if (typeof submit !== 'function') {
101
+ ctx.status = 501;
102
+ ctx.body = { success: false, message: `适配器 ${platform} 不支持 Web 验证提交` };
103
+ return;
104
+ }
105
+ await Promise.resolve(submit.call(adapter, account_id, type, data));
106
+ app.pendingVerifications.delete(`${platform}:${account_id}:${type}`);
107
+ ctx.body = { success: true };
108
+ }
109
+ catch (e) {
110
+ const err = e;
111
+ ctx.status = 500;
112
+ ctx.body = { success: false, message: err?.message ?? '提交失败' };
113
+ }
114
+ });
115
+ }
116
+ //# sourceMappingURL=verification.js.map
@@ -3,8 +3,16 @@
3
3
  */
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
- import { execSync } from "child_process";
6
+ import * as os from "os";
7
+ import { execFileSync } from "child_process";
7
8
  const SERVICE_NAME = "onebots-gateway";
9
+ function getHomeDir() {
10
+ const home = process.env.HOME || os.homedir();
11
+ if (!home) {
12
+ throw new Error("无法确定用户主目录(HOME 环境变量未设置且 os.homedir() 返回空)");
13
+ }
14
+ return home;
15
+ }
8
16
  function getConfigDir(configPath) {
9
17
  return path.dirname(path.resolve(process.cwd(), configPath));
10
18
  }
@@ -15,7 +23,7 @@ export async function serviceInstall(configPath) {
15
23
  const binPath = process.argv[1];
16
24
  const startCmd = `"${nodePath}" "${binPath}" gateway start -c "${resolvedConfig}"`;
17
25
  if (process.platform === "darwin") {
18
- const plistPath = path.join(process.env.HOME, "Library", "LaunchAgents", `com.onebots.${SERVICE_NAME}.plist`);
26
+ const plistPath = path.join(getHomeDir(), "Library", "LaunchAgents", `com.onebots.${SERVICE_NAME}.plist`);
19
27
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
20
28
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
21
29
  <plist version="1.0">
@@ -52,7 +60,7 @@ export async function serviceInstall(configPath) {
52
60
  return;
53
61
  }
54
62
  if (process.platform === "linux") {
55
- const unitDir = path.join(process.env.HOME, ".config", "systemd", "user");
63
+ const unitDir = path.join(getHomeDir(), ".config", "systemd", "user");
56
64
  if (!fs.existsSync(unitDir))
57
65
  fs.mkdirSync(unitDir, { recursive: true });
58
66
  const unitPath = path.join(unitDir, `${SERVICE_NAME}.service`);
@@ -73,7 +81,7 @@ WantedBy=default.target
73
81
  fs.writeFileSync(unitPath, unit, "utf8");
74
82
  console.log("已安装 systemd 用户服务:", unitPath);
75
83
  try {
76
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
84
+ execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
77
85
  console.log("启用: systemctl --user enable --now " + SERVICE_NAME);
78
86
  }
79
87
  catch {
@@ -97,12 +105,12 @@ WantedBy=default.target
97
105
  }
98
106
  export async function serviceUninstall() {
99
107
  if (process.platform === "darwin") {
100
- const plistPath = path.join(process.env.HOME, "Library", "LaunchAgents", `com.onebots.${SERVICE_NAME}.plist`);
108
+ const plistPath = path.join(getHomeDir(), "Library", "LaunchAgents", `com.onebots.${SERVICE_NAME}.plist`);
101
109
  try {
102
- execSync(`launchctl unload "${plistPath}"`, { stdio: "inherit" });
110
+ execFileSync("launchctl", ["unload", plistPath], { stdio: "inherit" });
103
111
  }
104
112
  catch {
105
- // ignore
113
+ // launchctl unload 失败(服务可能未加载),忽略
106
114
  }
107
115
  if (fs.existsSync(plistPath)) {
108
116
  fs.unlinkSync(plistPath);
@@ -111,22 +119,22 @@ export async function serviceUninstall() {
111
119
  return;
112
120
  }
113
121
  if (process.platform === "linux") {
114
- const unitPath = path.join(process.env.HOME, ".config", "systemd", "user", `${SERVICE_NAME}.service`);
122
+ const unitPath = path.join(getHomeDir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
115
123
  try {
116
- execSync("systemctl --user disable " + SERVICE_NAME, { stdio: "inherit" });
124
+ execFileSync("systemctl", ["--user", "disable", SERVICE_NAME], { stdio: "inherit" });
117
125
  }
118
126
  catch {
119
- // ignore
127
+ // systemctl disable 失败(服务可能未启用),忽略
120
128
  }
121
129
  if (fs.existsSync(unitPath)) {
122
130
  fs.unlinkSync(unitPath);
123
131
  console.log("已卸载 systemd 服务");
124
132
  }
125
133
  try {
126
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
134
+ execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
127
135
  }
128
136
  catch {
129
- // ignore
137
+ // daemon-reload 失败,忽略
130
138
  }
131
139
  return;
132
140
  }
@@ -141,8 +149,9 @@ export async function serviceUninstall() {
141
149
  export async function serviceStatus() {
142
150
  if (process.platform === "darwin") {
143
151
  try {
144
- const out = execSync("launchctl list | grep onebots", { encoding: "utf8" });
145
- console.log(out || "未找到 onebots 服务");
152
+ const out = execFileSync("launchctl", ["list"], { encoding: "utf8" });
153
+ const lines = out.split("\n").filter(line => line.includes("onebots"));
154
+ console.log(lines.length > 0 ? lines.join("\n") : "未找到 onebots 服务");
146
155
  }
147
156
  catch {
148
157
  console.log("未找到 onebots 服务或未加载");
@@ -151,10 +160,10 @@ export async function serviceStatus() {
151
160
  }
152
161
  if (process.platform === "linux") {
153
162
  try {
154
- execSync("systemctl --user status " + SERVICE_NAME, { stdio: "inherit" });
163
+ execFileSync("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
155
164
  }
156
- catch (e) {
157
- const code = e?.status;
165
+ catch (error) {
166
+ const code = error?.status;
158
167
  if (code !== 0)
159
168
  console.log("服务未运行或未安装");
160
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onebots",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "OneBots 整合适配器和协议,提供HTTP/WebSocket服务",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -42,7 +42,7 @@
42
42
  "@karinjs/node-pty": "^1.1.3",
43
43
  "commander": "^14.0.3",
44
44
  "koa-static": "^5.0.0",
45
- "@onebots/core": "1.0.6",
45
+ "@onebots/core": "1.1.0",
46
46
  "@onebots/web": "1.0.7"
47
47
  },
48
48
  "repository": {