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 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("超级管理员用户,具有绝对权限。").default([]),
44
- root: import_koishi.Schema.string().description("初始工作路径。").default(process.env.HOME),
45
- shell: import_koishi.Schema.string().description("Shell路径。留空则自动检测系统默认Shell。"),
46
- encoding: import_koishi.Schema.string().description("输出内容编码。").default("utf8"),
47
- timeout: import_koishi.Schema.number().description("超时时长。").default(import_koishi.Time.minute),
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("单次发送最大输出长度。").default(1800)
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
- return input.replace(
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 sendCommand(shellSession, command) {
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
- cleanupSession(shellSession, key);
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: 4 }).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) => {
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": "Persistent terminal interface to a shell session over QQ.",
4
- "version": "1.0.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",