koishi-plugin-terminal 1.0.0 → 1.0.2
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/index.d.ts +2 -1
- package/lib/index.js +60 -18
- package/package.json +4 -3
package/lib/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import * as pty from "node-pty";
|
|
|
3
3
|
export declare const name = "terminal";
|
|
4
4
|
export interface Config {
|
|
5
5
|
admin?: Array<string>;
|
|
6
|
+
auth?: number;
|
|
6
7
|
root?: string;
|
|
7
8
|
shell?: string;
|
|
8
|
-
encoding?: string;
|
|
9
9
|
timeout?: number;
|
|
10
10
|
cols?: number;
|
|
11
11
|
rows?: number;
|
|
@@ -15,6 +15,7 @@ export interface ShellSession {
|
|
|
15
15
|
terminal: pty.IPty;
|
|
16
16
|
buffer: string;
|
|
17
17
|
timer?: NodeJS.Timeout;
|
|
18
|
+
timeoutTimer?: NodeJS.Timeout;
|
|
18
19
|
disposables: Array<{
|
|
19
20
|
dispose(): void;
|
|
20
21
|
}>;
|
package/lib/index.js
CHANGED
|
@@ -40,14 +40,14 @@ var pty = __toESM(require("node-pty"));
|
|
|
40
40
|
var import_node_timers = require("node:timers");
|
|
41
41
|
var name = "terminal";
|
|
42
42
|
var Config = import_koishi.Schema.object({
|
|
43
|
-
admin: import_koishi.Schema.array(String).description("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
timeout: import_koishi.Schema.number().description("
|
|
43
|
+
admin: import_koishi.Schema.array(String).description("超级管理员用户名单").default([]),
|
|
44
|
+
auth: import_koishi.Schema.number().description("使用本插件所需的最低权限,此外,用户也需要在超级管理员名单中。").min(1).max(4).step(1).default(4),
|
|
45
|
+
root: import_koishi.Schema.string().description("初始工作路径").default(process.env.HOME),
|
|
46
|
+
shell: import_koishi.Schema.string().description("Shell路径,留空则自动检测系统默认Shell"),
|
|
47
|
+
timeout: import_koishi.Schema.number().description("超时时长").default(import_koishi.Time.minute),
|
|
48
48
|
cols: import_koishi.Schema.number().description("终端列数").default(80),
|
|
49
49
|
rows: import_koishi.Schema.number().description("终端行数").default(24),
|
|
50
|
-
maxOutputLength: import_koishi.Schema.number().description("
|
|
50
|
+
maxOutputLength: import_koishi.Schema.number().description("单次发送最大输出长度").default(16384)
|
|
51
51
|
});
|
|
52
52
|
function resolveShell(shell) {
|
|
53
53
|
if (shell) return shell;
|
|
@@ -63,23 +63,55 @@ function resolveShell(shell) {
|
|
|
63
63
|
}
|
|
64
64
|
__name(resolveShell, "resolveShell");
|
|
65
65
|
function stripAnsi(input) {
|
|
66
|
-
|
|
66
|
+
const text = input.replace(
|
|
67
67
|
// eslint-disable-next-line no-control-regex
|
|
68
68
|
/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g,
|
|
69
69
|
""
|
|
70
70
|
);
|
|
71
|
+
return text.replace(/\r\n/g, "\n").split("\n").map((line) => line.split("\r").at(-1)).join("\n");
|
|
71
72
|
}
|
|
72
73
|
__name(stripAnsi, "stripAnsi");
|
|
73
74
|
function getKey(session) {
|
|
74
75
|
return `${session.platform}:${session.userId}`;
|
|
75
76
|
}
|
|
76
77
|
__name(getKey, "getKey");
|
|
78
|
+
function isInteractiveCommand(command) {
|
|
79
|
+
const trimmed = command.trim();
|
|
80
|
+
if (!trimmed) return false;
|
|
81
|
+
const [name2, ...args] = trimmed.split(/\s+/);
|
|
82
|
+
if (/^(vi|vim|nvim|nano|emacs)$/.test(name2)) return true;
|
|
83
|
+
if (/^(less|more|man)$/.test(name2)) return true;
|
|
84
|
+
if (/^(top|htop|btop|watch)$/.test(name2)) return true;
|
|
85
|
+
if (/^(tmux|screen)$/.test(name2)) return true;
|
|
86
|
+
if (/^(ssh|sftp|ftp|telnet)$/.test(name2)) return true;
|
|
87
|
+
if (/^(mysql|psql|sqlite3|redis-cli|mongosh)$/.test(name2)) return true;
|
|
88
|
+
if (/^(node|python|python3|ipython|ruby|irb|php|lua|R)$/.test(name2) && !args.length) return true;
|
|
89
|
+
if (name2 === "tail" && args.includes("-f")) return true;
|
|
90
|
+
if (name2 === "docker" && args.includes("exec") && args.some((arg) => arg.includes("it"))) return true;
|
|
91
|
+
if (name2 === "kubectl" && args.includes("exec") && args.some((arg) => arg.includes("it"))) return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
__name(isInteractiveCommand, "isInteractiveCommand");
|
|
77
95
|
var map = /* @__PURE__ */ new Map();
|
|
78
96
|
function apply(ctx, config) {
|
|
79
97
|
const allowedUsers = config.admin;
|
|
80
|
-
function
|
|
98
|
+
function refreshTimeout(shellSession, key, session) {
|
|
99
|
+
if (!config.timeout) return;
|
|
100
|
+
if (shellSession.timeoutTimer) (0, import_node_timers.clearTimeout)(shellSession.timeoutTimer);
|
|
101
|
+
shellSession.timeoutTimer = setTimeout(async () => {
|
|
102
|
+
if (map.get(key) !== shellSession) return;
|
|
103
|
+
cleanupSession(shellSession, key, true);
|
|
104
|
+
await session.send("Shell session timed out.");
|
|
105
|
+
}, config.timeout);
|
|
106
|
+
}
|
|
107
|
+
__name(refreshTimeout, "refreshTimeout");
|
|
108
|
+
function sendCommand(shellSession, key, session, command) {
|
|
109
|
+
refreshTimeout(shellSession, key, session);
|
|
110
|
+
if (isInteractiveCommand(command)) {
|
|
111
|
+
shellSession.terminal.write(`echo "Interactive command is not supported in chat terminal. Use a non-interactive form, or run shell -t to restart."\r`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
81
114
|
shellSession.terminal.write(command + "\r");
|
|
82
|
-
shellSession.terminal.write("pwd\r");
|
|
83
115
|
}
|
|
84
116
|
__name(sendCommand, "sendCommand");
|
|
85
117
|
function initSession(session, key) {
|
|
@@ -101,7 +133,7 @@ function apply(ctx, config) {
|
|
|
101
133
|
shellSession.buffer = "";
|
|
102
134
|
if (!output) return;
|
|
103
135
|
const text = output.length > config.maxOutputLength ? output.slice(0, config.maxOutputLength - 1) + "\n ...Truncated output" : output;
|
|
104
|
-
await session.send(text
|
|
136
|
+
await session.send(text);
|
|
105
137
|
}, "flush");
|
|
106
138
|
const dataDisposable = terminal.onData((data) => {
|
|
107
139
|
shellSession.buffer += data;
|
|
@@ -109,22 +141,31 @@ function apply(ctx, config) {
|
|
|
109
141
|
shellSession.timer = setTimeout(flush, 300);
|
|
110
142
|
});
|
|
111
143
|
const exitDisposable = terminal.onExit(async () => {
|
|
112
|
-
|
|
144
|
+
flush();
|
|
145
|
+
cleanupSession(shellSession, key, false);
|
|
113
146
|
await session.send("Shell exited.");
|
|
114
147
|
});
|
|
115
148
|
shellSession.disposables.push(dataDisposable, exitDisposable);
|
|
116
149
|
map.set(key, shellSession);
|
|
150
|
+
refreshTimeout(shellSession, key, session);
|
|
117
151
|
return shellSession;
|
|
118
152
|
}
|
|
119
153
|
__name(initSession, "initSession");
|
|
120
|
-
function cleanupSession(shellSession, key) {
|
|
154
|
+
function cleanupSession(shellSession, key, kill = true) {
|
|
121
155
|
map.delete(key);
|
|
122
|
-
shellSession.disposables.forEach((d) => d.dispose());
|
|
123
|
-
shellSession.terminal.kill();
|
|
124
156
|
if (shellSession.timer) (0, import_node_timers.clearTimeout)(shellSession.timer);
|
|
157
|
+
if (shellSession.timeoutTimer) (0, import_node_timers.clearTimeout)(shellSession.timeoutTimer);
|
|
158
|
+
shellSession.disposables.forEach((d) => d.dispose());
|
|
159
|
+
shellSession.disposables.length = 0;
|
|
160
|
+
if (kill) {
|
|
161
|
+
try {
|
|
162
|
+
shellSession.terminal.kill();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
125
166
|
}
|
|
126
167
|
__name(cleanupSession, "cleanupSession");
|
|
127
|
-
ctx.command("shell [command:text]", "Start a persistent shell session", { authority:
|
|
168
|
+
ctx.command("shell [command:text]", "Start a persistent shell session", { authority: config.auth }).option("terminate", "-t Terminate current shell session").usage("After start up, regular user messages will be sent to shell process.").example("shell echo Operating System: Three Easy Pieces > qljj.txt").action(async ({ session, options }, command) => {
|
|
128
169
|
if (!allowedUsers.includes(session.userId)) {
|
|
129
170
|
return "Unauthorized user.";
|
|
130
171
|
}
|
|
@@ -132,7 +173,7 @@ function apply(ctx, config) {
|
|
|
132
173
|
if (options.terminate) {
|
|
133
174
|
const current2 = map.get(key);
|
|
134
175
|
if (!current2) return "There doesn't exist running shell session.";
|
|
135
|
-
cleanupSession(current2, key);
|
|
176
|
+
cleanupSession(current2, key, true);
|
|
136
177
|
return "Shell session terminated.";
|
|
137
178
|
}
|
|
138
179
|
let current = map.get(key);
|
|
@@ -141,7 +182,7 @@ function apply(ctx, config) {
|
|
|
141
182
|
if (!command) return "Shell session started. Send regular messages as commands. Send shell -t to terminate.";
|
|
142
183
|
}
|
|
143
184
|
if (!command) return "Shell session is running.";
|
|
144
|
-
sendCommand(current, command);
|
|
185
|
+
sendCommand(current, key, session, command);
|
|
145
186
|
});
|
|
146
187
|
ctx.middleware(async (session, next) => {
|
|
147
188
|
const key = getKey(session);
|
|
@@ -152,12 +193,13 @@ function apply(ctx, config) {
|
|
|
152
193
|
return next();
|
|
153
194
|
}
|
|
154
195
|
if (!content) return;
|
|
155
|
-
sendCommand(current, content);
|
|
196
|
+
sendCommand(current, key, session, content);
|
|
156
197
|
}, true);
|
|
157
198
|
ctx.on("dispose", () => {
|
|
158
199
|
for (const current of map.values()) {
|
|
159
200
|
current.disposables.forEach((d) => d.dispose());
|
|
160
201
|
if (current.timer) (0, import_node_timers.clearTimeout)(current.timer);
|
|
202
|
+
if (current.timeoutTimer) (0, import_node_timers.clearTimeout)(current.timeoutTimer);
|
|
161
203
|
current.terminal.kill();
|
|
162
204
|
}
|
|
163
205
|
map.clear();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-terminal",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "1.0.
|
|
3
|
+
"description": "通过 QQ 运行持久的 Shell 终端",
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"terminal",
|
|
19
19
|
"cmd",
|
|
20
20
|
"bash",
|
|
21
|
-
"zsh"
|
|
21
|
+
"zsh",
|
|
22
|
+
"command"
|
|
22
23
|
],
|
|
23
24
|
"repository": {
|
|
24
25
|
"type": "git",
|