onebots 1.0.6 → 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.
- package/lib/app.d.ts +28 -10
- package/lib/app.js +42 -676
- package/lib/index.d.ts +3 -3
- package/lib/index.js +20 -3
- package/lib/routes/adapter-api.d.ts +17 -0
- package/lib/routes/adapter-api.js +127 -0
- package/lib/routes/auth.d.ts +15 -0
- package/lib/routes/auth.js +136 -0
- package/lib/routes/config.d.ts +13 -0
- package/lib/routes/config.js +73 -0
- package/lib/routes/public-static.d.ts +12 -0
- package/lib/routes/public-static.js +162 -0
- package/lib/routes/terminal.d.ts +10 -0
- package/lib/routes/terminal.js +164 -0
- package/lib/routes/verification.d.ts +17 -0
- package/lib/routes/verification.js +116 -0
- package/lib/service-manager.js +26 -17
- package/package.json +3 -3
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export { App, createOnebots, defineConfig } from './app.js';
|
|
2
|
+
export { getAppConfigSchema } from './config-schema.js';
|
|
3
|
+
export { AdapterRegistry, ProtocolRegistry, Adapter, Account, Protocol, BaseApp, SqliteDB, Router, CommonEvent, CommonTypes, AccountStatus, type Schema, type RouterContext, type Next, type Dict, type WsServer, ConnectionManager, RetryPresets, yaml, configure, readLine, dateLikeToEventMs, toUnixSeconds, unixSecondsToEventMs, unixMillisToEventMs, buildProxyUrl, maskProxyUrl, createProxyAgent, createHttpsProxyAgent, createSocksProxyAgent, createManagedTokenValidator, initTokenManager, ConfigValidator, } from '@onebots/core';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
// App exports
|
|
2
|
+
export { App, createOnebots, defineConfig } from './app.js';
|
|
3
|
+
export { getAppConfigSchema } from './config-schema.js';
|
|
4
|
+
// Re-export core symbols that adapters and protocols depend on
|
|
5
|
+
// (avoids requiring consumers to add @onebots/core as a direct dependency)
|
|
6
|
+
export {
|
|
7
|
+
// Registry
|
|
8
|
+
AdapterRegistry, ProtocolRegistry,
|
|
9
|
+
// Base classes
|
|
10
|
+
Adapter, Account, Protocol, BaseApp, SqliteDB, Router, AccountStatus,
|
|
11
|
+
// Infrastructure
|
|
12
|
+
ConnectionManager, RetryPresets,
|
|
13
|
+
// Utilities
|
|
14
|
+
yaml, configure, readLine, dateLikeToEventMs, toUnixSeconds, unixSecondsToEventMs, unixMillisToEventMs,
|
|
15
|
+
// Proxy
|
|
16
|
+
buildProxyUrl, maskProxyUrl, createProxyAgent, createHttpsProxyAgent, createSocksProxyAgent,
|
|
17
|
+
// Auth
|
|
18
|
+
createManagedTokenValidator, initTokenManager,
|
|
19
|
+
// Config
|
|
20
|
+
ConfigValidator, } from '@onebots/core';
|
|
4
21
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Router } from "@onebots/core";
|
|
2
|
+
import type { App } from "../app.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register adapter / account management and message-sending routes.
|
|
5
|
+
*
|
|
6
|
+
* Routes in this group:
|
|
7
|
+
* GET /api/adapters — list all adapter infos
|
|
8
|
+
* GET /api/list — list all account infos
|
|
9
|
+
* POST /api/add — add an account
|
|
10
|
+
* POST /api/edit — update an account
|
|
11
|
+
* GET /api/remove — remove an account
|
|
12
|
+
* POST /api/bots/start — set a bot online
|
|
13
|
+
* POST /api/bots/stop — set a bot offline
|
|
14
|
+
* POST /api/send — send a message through a running gateway
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerAdapterRoutes(app: App, router: Router): void;
|
|
17
|
+
//# sourceMappingURL=adapter-api.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register adapter / account management and message-sending routes.
|
|
3
|
+
*
|
|
4
|
+
* Routes in this group:
|
|
5
|
+
* GET /api/adapters — list all adapter infos
|
|
6
|
+
* GET /api/list — list all account infos
|
|
7
|
+
* POST /api/add — add an account
|
|
8
|
+
* POST /api/edit — update an account
|
|
9
|
+
* GET /api/remove — remove an account
|
|
10
|
+
* POST /api/bots/start — set a bot online
|
|
11
|
+
* POST /api/bots/stop — set a bot offline
|
|
12
|
+
* POST /api/send — send a message through a running gateway
|
|
13
|
+
*/
|
|
14
|
+
export function registerAdapterRoutes(app, router) {
|
|
15
|
+
router.get("/api/adapters", (ctx) => {
|
|
16
|
+
ctx.body = [...app.adapters.values()].map(adapter => adapter.info);
|
|
17
|
+
});
|
|
18
|
+
router.get("/api/list", (ctx) => {
|
|
19
|
+
ctx.body = app.accounts.map(bot => bot.info);
|
|
20
|
+
});
|
|
21
|
+
router.post("/api/add", (ctx) => {
|
|
22
|
+
const config = ctx.request.body;
|
|
23
|
+
try {
|
|
24
|
+
app.addAccount(config);
|
|
25
|
+
ctx.body = { success: true, message: '添加成功' };
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
ctx.status = 500;
|
|
29
|
+
ctx.body = { success: false, message: e.message };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
router.post("/api/edit", async (ctx) => {
|
|
33
|
+
const config = ctx.request.body;
|
|
34
|
+
try {
|
|
35
|
+
await app.updateAccount(config);
|
|
36
|
+
ctx.body = { success: true, message: '修改成功' };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
ctx.status = 500;
|
|
40
|
+
ctx.body = { success: false, message: e.message };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
router.get("/api/remove", async (ctx) => {
|
|
44
|
+
const { uin, platform, force } = ctx.request.query;
|
|
45
|
+
try {
|
|
46
|
+
await app.removeAccount(String(platform), String(uin), Boolean(force));
|
|
47
|
+
ctx.body = { success: true, message: '移除成功' };
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
ctx.status = 500;
|
|
51
|
+
ctx.body = { success: false, message: e.message };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
router.post("/api/bots/start", async (ctx) => {
|
|
55
|
+
const { platform, uin } = ctx.request.body;
|
|
56
|
+
try {
|
|
57
|
+
const adapter = app.adapters.get(platform);
|
|
58
|
+
await adapter?.setOnline(uin);
|
|
59
|
+
ctx.body = { success: true, data: adapter?.getAccount(uin)?.info };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
ctx.status = 500;
|
|
63
|
+
ctx.body = { success: false, message: e.message };
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
router.post("/api/bots/stop", async (ctx) => {
|
|
67
|
+
const { platform, uin } = ctx.request.body;
|
|
68
|
+
try {
|
|
69
|
+
const adapter = app.adapters.get(platform);
|
|
70
|
+
await adapter?.setOffline(uin);
|
|
71
|
+
ctx.body = { success: true, data: adapter?.getAccount(uin)?.info };
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
ctx.status = 500;
|
|
75
|
+
ctx.body = { success: false, message: e.message };
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// CLI send:通过已运行网关发信
|
|
79
|
+
router.post("/api/send", async (ctx) => {
|
|
80
|
+
try {
|
|
81
|
+
const body = ctx.request.body || {};
|
|
82
|
+
const channel = String(body.channel ?? "");
|
|
83
|
+
const target_id = String(body.target_id ?? "");
|
|
84
|
+
const target_type = String(body.target_type ?? "private");
|
|
85
|
+
const message = String(body.message ?? "");
|
|
86
|
+
if (!channel || !target_id) {
|
|
87
|
+
ctx.status = 400;
|
|
88
|
+
ctx.body = { success: false, message: "缺少 channel 或 target_id" };
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const parts = channel.split(".");
|
|
92
|
+
const platform = parts[0];
|
|
93
|
+
const account_id = parts.slice(1).join(".") || parts[1];
|
|
94
|
+
if (!platform || !account_id) {
|
|
95
|
+
ctx.status = 400;
|
|
96
|
+
ctx.body = { success: false, message: "channel 格式应为 platform.account_id" };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const adapter = app.adapters.get(platform);
|
|
100
|
+
if (!adapter) {
|
|
101
|
+
ctx.status = 404;
|
|
102
|
+
ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const account = adapter.getAccount(account_id);
|
|
106
|
+
if (!account) {
|
|
107
|
+
ctx.status = 404;
|
|
108
|
+
ctx.body = { success: false, message: `账号 ${channel} 不存在` };
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const segments = [{ type: "text", data: { text: message } }];
|
|
112
|
+
const scene_id = adapter.createId(target_id);
|
|
113
|
+
const result = await adapter.sendMessage(account_id, {
|
|
114
|
+
scene_type: target_type,
|
|
115
|
+
scene_id,
|
|
116
|
+
message: segments,
|
|
117
|
+
});
|
|
118
|
+
ctx.body = { success: true, message_id: result?.message_id ?? null };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
const err = e;
|
|
122
|
+
ctx.status = 500;
|
|
123
|
+
ctx.body = { success: false, message: err?.message ?? "发送失败" };
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=adapter-api.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { App } from "../app.js";
|
|
2
|
+
import type { Router } from "@onebots/core";
|
|
3
|
+
/**
|
|
4
|
+
* Register authentication-related routes.
|
|
5
|
+
*
|
|
6
|
+
* Computes auth state from app.config rather than relying on start() closure variables.
|
|
7
|
+
* The middleware registered here protects all /api/* routes except login and refresh.
|
|
8
|
+
*
|
|
9
|
+
* A note on the access token: it can come from config.access_token or the environment
|
|
10
|
+
* variable ONEBOTS_ACCESS_TOKEN. The access token path skips the per-session token
|
|
11
|
+
* manager and is validated by direct comparison. The username/password path generates
|
|
12
|
+
* managed tokens via app.tokenManager.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerAuthRoutes(app: App, router: Router): void;
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createManagedTokenValidator, BaseApp } from "@onebots/core";
|
|
2
|
+
/**
|
|
3
|
+
* Register authentication-related routes.
|
|
4
|
+
*
|
|
5
|
+
* Computes auth state from app.config rather than relying on start() closure variables.
|
|
6
|
+
* The middleware registered here protects all /api/* routes except login and refresh.
|
|
7
|
+
*
|
|
8
|
+
* A note on the access token: it can come from config.access_token or the environment
|
|
9
|
+
* variable ONEBOTS_ACCESS_TOKEN. The access token path skips the per-session token
|
|
10
|
+
* manager and is validated by direct comparison. The username/password path generates
|
|
11
|
+
* managed tokens via app.tokenManager.
|
|
12
|
+
*/
|
|
13
|
+
export function registerAuthRoutes(app, router) {
|
|
14
|
+
/* ── auth state derived from app.config ───────────────────── */
|
|
15
|
+
const expectedUsername = app.config.username ?? BaseApp.defaultConfig.username;
|
|
16
|
+
const expectedPassword = app.config.password ?? BaseApp.defaultConfig.password;
|
|
17
|
+
const expectedAccessToken = (app.config.access_token?.trim()
|
|
18
|
+
|| process.env.ONEBOTS_ACCESS_TOKEN?.trim()
|
|
19
|
+
|| undefined);
|
|
20
|
+
/** Extract a Bearer token or ?access_token from an http.IncomingMessage (used by WebSocket auth). */
|
|
21
|
+
const getTokenFromRequest = (request) => {
|
|
22
|
+
const authHeader = request.headers.authorization;
|
|
23
|
+
if (authHeader && typeof authHeader === 'string') {
|
|
24
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
25
|
+
return match ? match[1] : authHeader;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(request.url || '/', 'http://localhost');
|
|
29
|
+
return url.searchParams.get('access_token') || undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Extract a Bearer token or ?access_token from a Koa / RouterContext. */
|
|
36
|
+
const getTokenFromKoa = (ctx) => {
|
|
37
|
+
const authHeader = ctx.request.headers.authorization;
|
|
38
|
+
if (authHeader && typeof authHeader === 'string') {
|
|
39
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
40
|
+
return match ? match[1] : authHeader;
|
|
41
|
+
}
|
|
42
|
+
return ctx.request.query?.access_token || undefined;
|
|
43
|
+
};
|
|
44
|
+
const authValidator = createManagedTokenValidator(app.tokenManager, {
|
|
45
|
+
tokenName: 'access_token',
|
|
46
|
+
errorMessage: 'Unauthorized',
|
|
47
|
+
});
|
|
48
|
+
/* ── routes ────────────────────────────────────────────────── */
|
|
49
|
+
router.post("/api/auth/login", (ctx) => {
|
|
50
|
+
const body = ctx.request.body;
|
|
51
|
+
// 鉴权码登录
|
|
52
|
+
if (body.access_token != null && body.access_token !== '') {
|
|
53
|
+
if (expectedAccessToken && body.access_token === expectedAccessToken) {
|
|
54
|
+
ctx.body = {
|
|
55
|
+
success: true,
|
|
56
|
+
token: body.access_token,
|
|
57
|
+
expiresAt: null,
|
|
58
|
+
refreshToken: null,
|
|
59
|
+
isDefaultCredentials: false,
|
|
60
|
+
};
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.status = 401;
|
|
64
|
+
ctx.body = { success: false, message: "鉴权码错误" };
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!body.username || !body.password || body.username !== expectedUsername || body.password !== expectedPassword) {
|
|
68
|
+
ctx.status = 401;
|
|
69
|
+
ctx.body = { success: false, message: "用户名或密码错误" };
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tokenInfo = app.tokenManager.generateToken({ username: body.username });
|
|
73
|
+
ctx.body = {
|
|
74
|
+
success: true,
|
|
75
|
+
token: tokenInfo.token,
|
|
76
|
+
expiresAt: tokenInfo.expiresAt,
|
|
77
|
+
refreshToken: tokenInfo.refreshToken,
|
|
78
|
+
isDefaultCredentials: app.credentialsWereAutoGenerated,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
router.post("/api/auth/refresh", (ctx) => {
|
|
82
|
+
const { refreshToken } = ctx.request.body;
|
|
83
|
+
if (!refreshToken) {
|
|
84
|
+
ctx.status = 400;
|
|
85
|
+
ctx.body = { success: false, message: "缺少 refreshToken" };
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const tokenInfo = app.tokenManager.refreshToken(refreshToken);
|
|
89
|
+
if (!tokenInfo) {
|
|
90
|
+
ctx.status = 401;
|
|
91
|
+
ctx.body = { success: false, message: "refreshToken 无效或已过期" };
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ctx.body = {
|
|
95
|
+
success: true,
|
|
96
|
+
token: tokenInfo.token,
|
|
97
|
+
expiresAt: tokenInfo.expiresAt,
|
|
98
|
+
refreshToken: tokenInfo.refreshToken,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* API auth middleware — protects all /api/* paths EXCEPT login & refresh.
|
|
103
|
+
* Access-token paths (config.access_token / env ONEBOTS_ACCESS_TOKEN) short-circuit
|
|
104
|
+
* the per-session token-manager check. All other paths go through authValidator
|
|
105
|
+
* which validates the Bearer token issued by the login endpoint.
|
|
106
|
+
*/
|
|
107
|
+
router.use('/api', async (ctx, next) => {
|
|
108
|
+
if (ctx.path === '/api/auth/login' || ctx.path === '/api/auth/refresh')
|
|
109
|
+
return next();
|
|
110
|
+
const token = getTokenFromKoa(ctx);
|
|
111
|
+
if (expectedAccessToken && token === expectedAccessToken) {
|
|
112
|
+
ctx.state.token = token;
|
|
113
|
+
ctx.state.tokenInfo = { metadata: { username: 'token' }, expiresAt: null };
|
|
114
|
+
return next();
|
|
115
|
+
}
|
|
116
|
+
return authValidator(ctx, next);
|
|
117
|
+
});
|
|
118
|
+
router.post("/api/auth/logout", (ctx) => {
|
|
119
|
+
const token = ctx.state.token;
|
|
120
|
+
if (token && token !== expectedAccessToken)
|
|
121
|
+
app.tokenManager.revokeToken(token);
|
|
122
|
+
ctx.body = { success: true };
|
|
123
|
+
});
|
|
124
|
+
router.get("/api/auth/me", (ctx) => {
|
|
125
|
+
const tokenInfo = ctx.state.tokenInfo;
|
|
126
|
+
ctx.body = {
|
|
127
|
+
success: true,
|
|
128
|
+
data: {
|
|
129
|
+
username: tokenInfo?.metadata?.username ?? expectedUsername,
|
|
130
|
+
expiresAt: tokenInfo?.expiresAt ?? null,
|
|
131
|
+
isDefaultCredentials: app.credentialsWereAutoGenerated,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Router } from "@onebots/core";
|
|
2
|
+
import type { App } from "../app.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register configuration and system management routes.
|
|
5
|
+
*
|
|
6
|
+
* - GET/POST /api/config — read / write the config YAML file
|
|
7
|
+
* - GET /api/config/schema — JSON schema for the config
|
|
8
|
+
* - GET /api/system — app info
|
|
9
|
+
* - POST /api/system/restart — graceful shutdown
|
|
10
|
+
* - POST /api/system/backup-to-hf — push config + data dir to Hugging Face
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerConfigRoutes(app: App, router: Router): void;
|
|
13
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BaseApp } from "@onebots/core";
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { getAppConfigSchema } from "../config-schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register configuration and system management routes.
|
|
6
|
+
*
|
|
7
|
+
* - GET/POST /api/config — read / write the config YAML file
|
|
8
|
+
* - GET /api/config/schema — JSON schema for the config
|
|
9
|
+
* - GET /api/system — app info
|
|
10
|
+
* - POST /api/system/restart — graceful shutdown
|
|
11
|
+
* - POST /api/system/backup-to-hf — push config + data dir to Hugging Face
|
|
12
|
+
*/
|
|
13
|
+
export function registerConfigRoutes(app, router) {
|
|
14
|
+
router.get("/api/config", (ctx) => {
|
|
15
|
+
ctx.body = readFileSync(BaseApp.configPath, "utf8");
|
|
16
|
+
});
|
|
17
|
+
router.get("/api/config/schema", (ctx) => {
|
|
18
|
+
ctx.body = getAppConfigSchema();
|
|
19
|
+
});
|
|
20
|
+
router.post("/api/config", async (ctx) => {
|
|
21
|
+
try {
|
|
22
|
+
const configContent = ctx.request.body;
|
|
23
|
+
writeFileSync(BaseApp.configPath, configContent, "utf8");
|
|
24
|
+
app.credentialsWereAutoGenerated = false;
|
|
25
|
+
const backupResult = await app.backupDataToHf(configContent);
|
|
26
|
+
if (!backupResult.success && backupResult.message) {
|
|
27
|
+
app.logger?.warn?.(backupResult.message);
|
|
28
|
+
}
|
|
29
|
+
ctx.body = { success: true, message: "配置已保存" };
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
ctx.status = 500;
|
|
33
|
+
ctx.body = { success: false, message: e.message };
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
router.get("/api/system", (ctx) => {
|
|
37
|
+
ctx.body = {
|
|
38
|
+
...app.info,
|
|
39
|
+
isDefaultCredentials: app.credentialsWereAutoGenerated,
|
|
40
|
+
configDir: BaseApp.configDir,
|
|
41
|
+
configPath: BaseApp.configPath,
|
|
42
|
+
dataDir: BaseApp.dataDir,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
/** 手动将 data 与配置备份到 HF 仓库(与保存配置时的备份逻辑一致) */
|
|
46
|
+
router.post("/api/system/backup-to-hf", async (ctx) => {
|
|
47
|
+
try {
|
|
48
|
+
const configContent = readFileSync(BaseApp.configPath, "utf8");
|
|
49
|
+
const result = await app.backupDataToHf(configContent);
|
|
50
|
+
if (result.success) {
|
|
51
|
+
ctx.body = { success: true, message: "已备份到仓库" };
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
ctx.status = 400;
|
|
55
|
+
ctx.body = { success: false, message: result.message ?? "备份失败" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
ctx.status = 500;
|
|
60
|
+
ctx.body = { success: false, message: e.message };
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
/** 重启服务:进程退出后由 Docker 的 restart 策略自动拉起容器;非 Docker 需手动重新启动 */
|
|
64
|
+
router.post("/api/system/restart", (ctx) => {
|
|
65
|
+
ctx.body = { success: true, message: "服务即将重启" };
|
|
66
|
+
setImmediate(() => {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}, 1500);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Router } from "@onebots/core";
|
|
2
|
+
import type { App } from "../app.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register public-static file management routes.
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* GET /api/public-static/files — list files in public_static_dir
|
|
8
|
+
* POST /api/public-static/upload — upload a file
|
|
9
|
+
* DELETE /api/public-static/:filename — delete a file
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerPublicStaticRoutes(app: App, router: Router): void;
|
|
12
|
+
//# sourceMappingURL=public-static.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/** 站点静态根目录下的文件名:禁止路径分隔与控制字符,仅使用 basename */
|
|
4
|
+
function sanitizePublicStaticBasename(original) {
|
|
5
|
+
if (original == null || String(original).trim() === '')
|
|
6
|
+
return null;
|
|
7
|
+
let raw = String(original).trim();
|
|
8
|
+
try {
|
|
9
|
+
raw = decodeURIComponent(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (/[\\/]/.test(raw) || raw.includes('..'))
|
|
15
|
+
return null;
|
|
16
|
+
if (/[\x00-\x1f]/.test(raw))
|
|
17
|
+
return null;
|
|
18
|
+
const base = path.basename(raw);
|
|
19
|
+
if (!base || base !== raw || base === '.' || base === '..')
|
|
20
|
+
return null;
|
|
21
|
+
if (base.length > 255)
|
|
22
|
+
return null;
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
function pickPublicStaticUpload(files) {
|
|
26
|
+
if (!files || typeof files !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const raw = files.file ?? files.upload;
|
|
29
|
+
if (!raw)
|
|
30
|
+
return null;
|
|
31
|
+
const file = Array.isArray(raw) ? raw[0] : raw;
|
|
32
|
+
if (!file || typeof file !== 'object')
|
|
33
|
+
return null;
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register public-static file management routes.
|
|
38
|
+
*
|
|
39
|
+
* Routes:
|
|
40
|
+
* GET /api/public-static/files — list files in public_static_dir
|
|
41
|
+
* POST /api/public-static/upload — upload a file
|
|
42
|
+
* DELETE /api/public-static/:filename — delete a file
|
|
43
|
+
*/
|
|
44
|
+
export function registerPublicStaticRoutes(app, router) {
|
|
45
|
+
router.get("/api/public-static/files", (ctx) => {
|
|
46
|
+
const root = app.getPublicStaticRoot();
|
|
47
|
+
if (!root) {
|
|
48
|
+
ctx.status = 400;
|
|
49
|
+
ctx.body = {
|
|
50
|
+
success: false,
|
|
51
|
+
message: '请先在基础配置中设置 public_static_dir 并保存配置',
|
|
52
|
+
};
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const names = fs
|
|
57
|
+
.readdirSync(root, { withFileTypes: true })
|
|
58
|
+
.filter((d) => d.isFile())
|
|
59
|
+
.map((d) => d.name)
|
|
60
|
+
.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
ctx.body = { success: true, files: names, root };
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
ctx.status = 500;
|
|
65
|
+
ctx.body = { success: false, message: e.message };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
router.post("/api/public-static/upload", async (ctx) => {
|
|
69
|
+
const root = app.getPublicStaticRoot();
|
|
70
|
+
if (!root) {
|
|
71
|
+
ctx.status = 400;
|
|
72
|
+
ctx.body = {
|
|
73
|
+
success: false,
|
|
74
|
+
message: '请先在基础配置中设置 public_static_dir 并保存配置',
|
|
75
|
+
};
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const file = pickPublicStaticUpload(ctx.request.files);
|
|
79
|
+
if (!file?.filepath) {
|
|
80
|
+
ctx.status = 400;
|
|
81
|
+
ctx.body = { success: false, message: '缺少上传文件(字段名 file)' };
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const safeName = sanitizePublicStaticBasename(file.originalFilename ?? file.newFilename);
|
|
85
|
+
if (!safeName) {
|
|
86
|
+
try {
|
|
87
|
+
fs.unlinkSync(file.filepath);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* 忽略临时文件清理失败 */
|
|
91
|
+
}
|
|
92
|
+
ctx.status = 400;
|
|
93
|
+
ctx.body = { success: false, message: '非法或无法识别的文件名' };
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const dest = path.join(root, safeName);
|
|
97
|
+
const tmpPath = file.filepath;
|
|
98
|
+
try {
|
|
99
|
+
fs.copyFileSync(tmpPath, dest);
|
|
100
|
+
ctx.body = { success: true, message: '上传成功', filename: safeName };
|
|
101
|
+
const hf = await app.backupDataDirToHfAfterStaticChange();
|
|
102
|
+
if (hf.attempted) {
|
|
103
|
+
ctx.body.hf_backup = hf;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
ctx.status = 500;
|
|
108
|
+
ctx.body = { success: false, message: e.message };
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(tmpPath);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* 忽略 */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
router.delete("/api/public-static/:filename", async (ctx) => {
|
|
120
|
+
const root = app.getPublicStaticRoot();
|
|
121
|
+
if (!root) {
|
|
122
|
+
ctx.status = 400;
|
|
123
|
+
ctx.body = {
|
|
124
|
+
success: false,
|
|
125
|
+
message: '请先在基础配置中设置 public_static_dir 并保存配置',
|
|
126
|
+
};
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const safeName = sanitizePublicStaticBasename(ctx.params.filename ?? '');
|
|
130
|
+
if (!safeName) {
|
|
131
|
+
ctx.status = 400;
|
|
132
|
+
ctx.body = { success: false, message: '非法文件名' };
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const resolvedRoot = path.resolve(root);
|
|
136
|
+
const target = path.join(root, safeName);
|
|
137
|
+
const rel = path.relative(resolvedRoot, path.resolve(target));
|
|
138
|
+
if (rel.startsWith('..') || path.isAbsolute(rel) || rel === '') {
|
|
139
|
+
ctx.status = 400;
|
|
140
|
+
ctx.body = { success: false, message: '路径非法' };
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
145
|
+
ctx.status = 404;
|
|
146
|
+
ctx.body = { success: false, message: '文件不存在' };
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
fs.unlinkSync(target);
|
|
150
|
+
ctx.body = { success: true, message: '已删除' };
|
|
151
|
+
const hf = await app.backupDataDirToHfAfterStaticChange();
|
|
152
|
+
if (hf.attempted) {
|
|
153
|
+
ctx.body.hf_backup = hf;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
ctx.status = 500;
|
|
158
|
+
ctx.body = { success: false, message: e.message };
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=public-static.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Router } from "@onebots/core";
|
|
2
|
+
import type { App } from "../app.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register terminal and log-streaming endpoints.
|
|
5
|
+
*
|
|
6
|
+
* WS /api/terminal — interactive PTY terminal via WebSocket
|
|
7
|
+
* SSE /api/logs — real-time log stream (stdout / stderr interception)
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerTerminalRoutes(app: App, router: Router): void;
|
|
10
|
+
//# sourceMappingURL=terminal.d.ts.map
|