@zhin.js/plugin-code-runner 1.0.0 → 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/README.md +28 -0
- package/agent/skills/code-runner.md +55 -0
- package/agent/tools/run_code.ts +17 -0
- package/commands/run/[language].js +16 -0
- package/commands/run/[language].ts +15 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -108
- package/lib/index.js.map +1 -1
- package/lib/run-code.d.ts +9 -0
- package/lib/run-code.d.ts.map +1 -0
- package/lib/run-code.js +72 -0
- package/lib/run-code.js.map +1 -0
- package/package.json +45 -7
- package/plugin.js +8 -0
- package/schema.json +6 -0
- package/src/index.ts +1 -133
- package/src/run-code.ts +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @zhin.js/plugin-code-runner
|
|
2
|
+
|
|
3
|
+
Zhin.js plugin for code-runner functionality.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @zhin.js/plugin-code-runner
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Add the plugin to your `zhin.config.yml`:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
plugins:
|
|
17
|
+
- @zhin.js/plugin-code-runner
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or via CLI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
zhin install @zhin.js/plugin-code-runner
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Configure in `zhin.config.yml` under the plugin's key. Refer to the plugin source for available options.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-runner
|
|
3
|
+
description: >-
|
|
4
|
+
在线代码执行能力。当用户要求运行代码片段、验证算法、测试函数输出、
|
|
5
|
+
或想看某段代码的执行结果时使用。支持 Python、JavaScript、TypeScript、
|
|
6
|
+
Go、Rust、Java、C/C++、Ruby、PHP 等语言。即使用户只是说「帮我跑一下这段代码」
|
|
7
|
+
或贴了一段代码想看结果,也应触发。
|
|
8
|
+
keywords:
|
|
9
|
+
- code
|
|
10
|
+
- run
|
|
11
|
+
- 运行
|
|
12
|
+
- 代码
|
|
13
|
+
- execute
|
|
14
|
+
- sandbox
|
|
15
|
+
- python
|
|
16
|
+
- javascript
|
|
17
|
+
- 编程
|
|
18
|
+
- 执行
|
|
19
|
+
- 测试
|
|
20
|
+
tags:
|
|
21
|
+
- code-runner
|
|
22
|
+
- development
|
|
23
|
+
- sandbox
|
|
24
|
+
tools:
|
|
25
|
+
- run_code
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# 在线代码执行技能
|
|
29
|
+
|
|
30
|
+
通过 glot.io API 在隔离沙箱中执行代码,不影响本地环境。
|
|
31
|
+
|
|
32
|
+
## 使用方式
|
|
33
|
+
|
|
34
|
+
调用 `run_code` 工具,传入 `language`(语言标识)和 `code`(代码内容)。
|
|
35
|
+
|
|
36
|
+
**支持语言:** python, javascript, typescript, go, rust, java, c, cpp, ruby, php
|
|
37
|
+
|
|
38
|
+
**Example:**
|
|
39
|
+
```
|
|
40
|
+
用户: 帮我算一下 1 到 100 的和
|
|
41
|
+
→ run_code(language="python", code="print(sum(range(1, 101)))")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 限制
|
|
45
|
+
|
|
46
|
+
- 代码长度上限 **10000 字符**
|
|
47
|
+
- 执行超时 **30 秒**
|
|
48
|
+
- 无法访问网络和文件系统(沙箱隔离)
|
|
49
|
+
- 不支持交互式输入(stdin)
|
|
50
|
+
|
|
51
|
+
## 易错点
|
|
52
|
+
|
|
53
|
+
1. **language 参数必须是上面列出的标识符**,不接受中文或全名(如用 `python` 不用 `Python3`)。
|
|
54
|
+
2. **C 和 C++ 是不同的 language 值**:`c` 和 `cpp`。
|
|
55
|
+
3. **代码需要有输出语句**(print/console.log/fmt.Println 等),否则返回为空。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { formatResult, runCode } from '../../src/run-code.js';
|
|
4
|
+
|
|
5
|
+
export default defineAgentTool<{ language: string; code: string }>({
|
|
6
|
+
description: '在沙箱中运行代码片段,返回 stdout/stderr/error',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
language: z.string().min(1),
|
|
9
|
+
code: z.string().min(1),
|
|
10
|
+
}),
|
|
11
|
+
keywords: ['运行代码', '执行代码', 'run code', 'execute', '代码'],
|
|
12
|
+
tags: ['code', 'run', 'execute', 'sandbox'],
|
|
13
|
+
async execute({ language, code }) {
|
|
14
|
+
const result = await runCode(language, code);
|
|
15
|
+
return formatResult(result);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { formatResult, runCode } from "../../lib/run-code.js";
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '在沙箱中运行代码片段(/code-runner run <语言> <代码>)',
|
|
6
|
+
params: { language: { type: 'string' } },
|
|
7
|
+
async execute({ params, args }) {
|
|
8
|
+
const language = String(params.language ?? '');
|
|
9
|
+
// code 原来是第二动态段,约定式命令只支持单动态文件参数,改从 args 取
|
|
10
|
+
const code = args.join(' ').trim();
|
|
11
|
+
if (!code)
|
|
12
|
+
return '请提供要运行的代码,例如:/code-runner run javascript alert(1+1)';
|
|
13
|
+
const result = await runCode(language, code);
|
|
14
|
+
return formatResult(result);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import { formatResult, runCode } from '../../src/run-code.js';
|
|
3
|
+
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '在沙箱中运行代码片段(/code-runner run <语言> <代码>)',
|
|
6
|
+
params: { language: { type: 'string' } },
|
|
7
|
+
async execute({ params, args }) {
|
|
8
|
+
const language = String(params.language ?? '');
|
|
9
|
+
// code 原来是第二动态段,约定式命令只支持单动态文件参数,改从 args 取
|
|
10
|
+
const code = args.join(' ').trim();
|
|
11
|
+
if (!code) return '请提供要运行的代码,例如:/code-runner run javascript alert(1+1)';
|
|
12
|
+
const result = await runCode(language, code);
|
|
13
|
+
return formatResult(result);
|
|
14
|
+
},
|
|
15
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { formatResult, runCode } from './run-code.js';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -1,109 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* @zhin.js/plugin-code-runner
|
|
3
|
-
*
|
|
4
|
-
* 沙箱代码执行插件 —— 通过 glot.io API 安全运行代码片段
|
|
5
|
-
*
|
|
6
|
-
* 功能:
|
|
7
|
-
* - AI 工具 run_code:AI 可调用执行代码
|
|
8
|
-
* - 命令「运行 <language> <code>」:用户直接运行代码
|
|
9
|
-
*
|
|
10
|
-
* 安全:
|
|
11
|
-
* - 代码长度上限 10000 字符
|
|
12
|
-
* - 语言白名单校验
|
|
13
|
-
* - 不执行本地代码,全部通过 glot.io 远程沙箱
|
|
14
|
-
*/
|
|
15
|
-
import { formatCompact, MessageCommand, usePlugin, ZhinTool } from 'zhin.js';
|
|
16
|
-
const { addCommand, addTool, logger } = usePlugin();
|
|
17
|
-
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
|
18
|
-
const MAX_CODE_LENGTH = 10000;
|
|
19
|
-
const SUPPORTED_LANGUAGES = {
|
|
20
|
-
python: 'main.py',
|
|
21
|
-
javascript: 'main.js',
|
|
22
|
-
typescript: 'main.ts',
|
|
23
|
-
go: 'main.go',
|
|
24
|
-
rust: 'main.rs',
|
|
25
|
-
java: 'Main.java',
|
|
26
|
-
c: 'main.c',
|
|
27
|
-
cpp: 'main.cpp',
|
|
28
|
-
ruby: 'main.rb',
|
|
29
|
-
php: 'main.php',
|
|
30
|
-
};
|
|
31
|
-
const GLOT_API_BASE = 'https://glot.io/api/run';
|
|
32
|
-
async function runCode(language, code) {
|
|
33
|
-
const lang = language.toLowerCase().trim();
|
|
34
|
-
if (!SUPPORTED_LANGUAGES[lang]) {
|
|
35
|
-
return {
|
|
36
|
-
stdout: '',
|
|
37
|
-
stderr: '',
|
|
38
|
-
error: `不支持的语言: ${lang}。支持: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (code.length > MAX_CODE_LENGTH) {
|
|
42
|
-
return {
|
|
43
|
-
stdout: '',
|
|
44
|
-
stderr: '',
|
|
45
|
-
error: `代码长度超过上限 ${MAX_CODE_LENGTH} 字符(当前 ${code.length})`,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const fileName = SUPPORTED_LANGUAGES[lang];
|
|
49
|
-
const url = `${GLOT_API_BASE}/${lang}/latest`;
|
|
50
|
-
try {
|
|
51
|
-
const response = await fetch(url, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: { 'Content-Type': 'application/json' },
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
files: [{ name: fileName, content: code }],
|
|
56
|
-
}),
|
|
57
|
-
signal: AbortSignal.timeout(30_000),
|
|
58
|
-
});
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
return {
|
|
61
|
-
stdout: '',
|
|
62
|
-
stderr: '',
|
|
63
|
-
error: `API 请求失败: ${response.status} ${response.statusText}`,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const data = (await response.json());
|
|
67
|
-
return {
|
|
68
|
-
stdout: data.stdout || '',
|
|
69
|
-
stderr: data.stderr || '',
|
|
70
|
-
error: data.error || '',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
catch (e) {
|
|
74
|
-
const msg = e?.name === 'TimeoutError' ? '请求超时(30s)' : e?.message || '未知错误';
|
|
75
|
-
return { stdout: '', stderr: '', error: msg };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function formatResult(result) {
|
|
79
|
-
const parts = [];
|
|
80
|
-
if (result.stdout)
|
|
81
|
-
parts.push(`[stdout]\n${result.stdout}`);
|
|
82
|
-
if (result.stderr)
|
|
83
|
-
parts.push(`[stderr]\n${result.stderr}`);
|
|
84
|
-
if (result.error)
|
|
85
|
-
parts.push(`[error]\n${result.error}`);
|
|
86
|
-
return parts.length ? parts.join('\n') : '(无输出)';
|
|
87
|
-
}
|
|
88
|
-
// ─── AI Tool: run_code ───────────────────────────────────────────────────────
|
|
89
|
-
addTool(new ZhinTool('run_code')
|
|
90
|
-
.desc('在沙箱中运行代码片段,返回 stdout/stderr/error')
|
|
91
|
-
.keyword('运行代码', '执行代码', 'run code', 'execute', '代码')
|
|
92
|
-
.tag('code', 'run', 'execute', 'sandbox')
|
|
93
|
-
.param('language', { type: 'string', description: '编程语言(python/javascript/typescript/go/rust/java/c/cpp/ruby/php)' }, true)
|
|
94
|
-
.param('code', { type: 'string', description: '要执行的代码' }, true)
|
|
95
|
-
.execute(async (args) => {
|
|
96
|
-
const result = await runCode(args.language, args.code);
|
|
97
|
-
return formatResult(result);
|
|
98
|
-
})
|
|
99
|
-
.toTool());
|
|
100
|
-
// ─── 命令: 运行 ─────────────────────────────────────────────────────────────
|
|
101
|
-
addCommand(new MessageCommand('运行 <language:text> <code:text>')
|
|
102
|
-
.desc('在沙箱中运行代码片段')
|
|
103
|
-
.action(async (_message, result) => {
|
|
104
|
-
const { language, code } = result.params;
|
|
105
|
-
logger.debug(formatCompact({ op: 'run', language, len: code.length }));
|
|
106
|
-
const res = await runCode(language, code);
|
|
107
|
-
return formatResult(res);
|
|
108
|
-
}));
|
|
1
|
+
export { formatResult, runCode } from './run-code.js';
|
|
109
2
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface RunResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
error: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runCode(language: string, code: string): Promise<RunResult>;
|
|
7
|
+
export declare function formatResult(result: RunResult): string;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=run-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-code.d.ts","sourceRoot":"","sources":["../src/run-code.ts"],"names":[],"mappings":"AAiBA,UAAU,SAAS;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAmDhF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAMtD"}
|
package/lib/run-code.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const MAX_CODE_LENGTH = 10000;
|
|
2
|
+
const SUPPORTED_LANGUAGES = {
|
|
3
|
+
python: 'main.py',
|
|
4
|
+
javascript: 'main.js',
|
|
5
|
+
typescript: 'main.ts',
|
|
6
|
+
go: 'main.go',
|
|
7
|
+
rust: 'main.rs',
|
|
8
|
+
java: 'Main.java',
|
|
9
|
+
c: 'main.c',
|
|
10
|
+
cpp: 'main.cpp',
|
|
11
|
+
ruby: 'main.rb',
|
|
12
|
+
php: 'main.php',
|
|
13
|
+
};
|
|
14
|
+
const GLOT_API_BASE = 'https://glot.io/api/run';
|
|
15
|
+
export async function runCode(language, code) {
|
|
16
|
+
const lang = language.toLowerCase().trim();
|
|
17
|
+
if (!SUPPORTED_LANGUAGES[lang]) {
|
|
18
|
+
return {
|
|
19
|
+
stdout: '',
|
|
20
|
+
stderr: '',
|
|
21
|
+
error: `不支持的语言: ${lang}。支持: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (code.length > MAX_CODE_LENGTH) {
|
|
25
|
+
return {
|
|
26
|
+
stdout: '',
|
|
27
|
+
stderr: '',
|
|
28
|
+
error: `代码长度超过上限 ${MAX_CODE_LENGTH} 字符(当前 ${code.length})`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const fileName = SUPPORTED_LANGUAGES[lang];
|
|
32
|
+
const url = `${GLOT_API_BASE}/${lang}/latest`;
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
files: [{ name: fileName, content: code }],
|
|
39
|
+
}),
|
|
40
|
+
signal: AbortSignal.timeout(30_000),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
return {
|
|
44
|
+
stdout: '',
|
|
45
|
+
stderr: '',
|
|
46
|
+
error: `API 请求失败: ${response.status} ${response.statusText}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const data = (await response.json());
|
|
50
|
+
return {
|
|
51
|
+
stdout: data.stdout || '',
|
|
52
|
+
stderr: data.stderr || '',
|
|
53
|
+
error: data.error || '',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const err = e;
|
|
58
|
+
const msg = err.name === 'TimeoutError' ? '请求超时(30s)' : err.message || '未知错误';
|
|
59
|
+
return { stdout: '', stderr: '', error: msg };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function formatResult(result) {
|
|
63
|
+
const parts = [];
|
|
64
|
+
if (result.stdout)
|
|
65
|
+
parts.push(`[stdout]\n${result.stdout}`);
|
|
66
|
+
if (result.stderr)
|
|
67
|
+
parts.push(`[stderr]\n${result.stderr}`);
|
|
68
|
+
if (result.error)
|
|
69
|
+
parts.push(`[error]\n${result.error}`);
|
|
70
|
+
return parts.length ? parts.join('\n') : '(无输出)';
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=run-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-code.js","sourceRoot":"","sources":["../src/run-code.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-code-runner",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Sandboxed code execution plugin for Zhin.js
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Sandboxed code execution plugin for Zhin.js (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
"plugin.js",
|
|
18
|
+
"schema.json",
|
|
19
|
+
"commands",
|
|
17
20
|
"src",
|
|
18
21
|
"lib",
|
|
22
|
+
"agent",
|
|
19
23
|
"README.md"
|
|
20
24
|
],
|
|
21
25
|
"keywords": [
|
|
@@ -33,13 +37,29 @@
|
|
|
33
37
|
"url": "https://github.com/lc-cn"
|
|
34
38
|
},
|
|
35
39
|
"license": "MIT",
|
|
36
|
-
"dependencies": {},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"typescript": "^6.0.3",
|
|
39
|
-
"
|
|
42
|
+
"vitest": "^4.1.10",
|
|
43
|
+
"zod": "^4.4.3",
|
|
44
|
+
"@zhin.js/agent": "1.1.23",
|
|
45
|
+
"zhin.js": "1.1.0"
|
|
40
46
|
},
|
|
41
47
|
"peerDependencies": {
|
|
42
|
-
"
|
|
48
|
+
"zod": "^4.0.0",
|
|
49
|
+
"@zhin.js/agent": "^1.1.23",
|
|
50
|
+
"@zhin.js/command": "^1.1.0",
|
|
51
|
+
"zhin.js": "^1.1.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@zhin.js/agent": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@zhin.js/command": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"zod": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
43
63
|
},
|
|
44
64
|
"repository": {
|
|
45
65
|
"type": "git",
|
|
@@ -50,8 +70,26 @@
|
|
|
50
70
|
"access": "public",
|
|
51
71
|
"registry": "https://registry.npmjs.org"
|
|
52
72
|
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
75
|
+
},
|
|
76
|
+
"zhin": {
|
|
77
|
+
"protocol": 1,
|
|
78
|
+
"type": "plugin",
|
|
79
|
+
"entry": "./plugin.js",
|
|
80
|
+
"engine": "^1.0.0",
|
|
81
|
+
"runtime": "trusted",
|
|
82
|
+
"features": [
|
|
83
|
+
{
|
|
84
|
+
"package": "@zhin.js/command",
|
|
85
|
+
"api": "^1.0.0"
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"plugins": []
|
|
89
|
+
},
|
|
53
90
|
"scripts": {
|
|
54
|
-
"build": "tsc
|
|
55
|
-
"clean": "rimraf lib"
|
|
91
|
+
"build": "tsc",
|
|
92
|
+
"clean": "rimraf lib",
|
|
93
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/utils/code-runner/tests"
|
|
56
94
|
}
|
|
57
95
|
}
|
package/plugin.js
ADDED
package/schema.json
ADDED
package/src/index.ts
CHANGED
|
@@ -1,133 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @zhin.js/plugin-code-runner
|
|
3
|
-
*
|
|
4
|
-
* 沙箱代码执行插件 —— 通过 glot.io API 安全运行代码片段
|
|
5
|
-
*
|
|
6
|
-
* 功能:
|
|
7
|
-
* - AI 工具 run_code:AI 可调用执行代码
|
|
8
|
-
* - 命令「运行 <language> <code>」:用户直接运行代码
|
|
9
|
-
*
|
|
10
|
-
* 安全:
|
|
11
|
-
* - 代码长度上限 10000 字符
|
|
12
|
-
* - 语言白名单校验
|
|
13
|
-
* - 不执行本地代码,全部通过 glot.io 远程沙箱
|
|
14
|
-
*/
|
|
15
|
-
import { formatCompact, MessageCommand, usePlugin, ZhinTool } from 'zhin.js'
|
|
16
|
-
|
|
17
|
-
const { addCommand, addTool, logger } = usePlugin()
|
|
18
|
-
|
|
19
|
-
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const MAX_CODE_LENGTH = 10000
|
|
22
|
-
|
|
23
|
-
const SUPPORTED_LANGUAGES: Record<string, string> = {
|
|
24
|
-
python: 'main.py',
|
|
25
|
-
javascript: 'main.js',
|
|
26
|
-
typescript: 'main.ts',
|
|
27
|
-
go: 'main.go',
|
|
28
|
-
rust: 'main.rs',
|
|
29
|
-
java: 'Main.java',
|
|
30
|
-
c: 'main.c',
|
|
31
|
-
cpp: 'main.cpp',
|
|
32
|
-
ruby: 'main.rb',
|
|
33
|
-
php: 'main.php',
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const GLOT_API_BASE = 'https://glot.io/api/run'
|
|
37
|
-
|
|
38
|
-
// ─── 核心执行函数 ────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
interface RunResult {
|
|
41
|
-
stdout: string
|
|
42
|
-
stderr: string
|
|
43
|
-
error: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function runCode(language: string, code: string): Promise<RunResult> {
|
|
47
|
-
const lang = language.toLowerCase().trim()
|
|
48
|
-
|
|
49
|
-
if (!SUPPORTED_LANGUAGES[lang]) {
|
|
50
|
-
return {
|
|
51
|
-
stdout: '',
|
|
52
|
-
stderr: '',
|
|
53
|
-
error: `不支持的语言: ${lang}。支持: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`,
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (code.length > MAX_CODE_LENGTH) {
|
|
58
|
-
return {
|
|
59
|
-
stdout: '',
|
|
60
|
-
stderr: '',
|
|
61
|
-
error: `代码长度超过上限 ${MAX_CODE_LENGTH} 字符(当前 ${code.length})`,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const fileName = SUPPORTED_LANGUAGES[lang]
|
|
66
|
-
const url = `${GLOT_API_BASE}/${lang}/latest`
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const response = await fetch(url, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
files: [{ name: fileName, content: code }],
|
|
74
|
-
}),
|
|
75
|
-
signal: AbortSignal.timeout(30_000),
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
return {
|
|
80
|
-
stdout: '',
|
|
81
|
-
stderr: '',
|
|
82
|
-
error: `API 请求失败: ${response.status} ${response.statusText}`,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const data = (await response.json()) as RunResult
|
|
87
|
-
return {
|
|
88
|
-
stdout: data.stdout || '',
|
|
89
|
-
stderr: data.stderr || '',
|
|
90
|
-
error: data.error || '',
|
|
91
|
-
}
|
|
92
|
-
} catch (e: any) {
|
|
93
|
-
const msg = e?.name === 'TimeoutError' ? '请求超时(30s)' : e?.message || '未知错误'
|
|
94
|
-
return { stdout: '', stderr: '', error: msg }
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function formatResult(result: RunResult): string {
|
|
99
|
-
const parts: string[] = []
|
|
100
|
-
if (result.stdout) parts.push(`[stdout]\n${result.stdout}`)
|
|
101
|
-
if (result.stderr) parts.push(`[stderr]\n${result.stderr}`)
|
|
102
|
-
if (result.error) parts.push(`[error]\n${result.error}`)
|
|
103
|
-
return parts.length ? parts.join('\n') : '(无输出)'
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─── AI Tool: run_code ───────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
addTool(
|
|
109
|
-
new ZhinTool('run_code')
|
|
110
|
-
.desc('在沙箱中运行代码片段,返回 stdout/stderr/error')
|
|
111
|
-
.keyword('运行代码', '执行代码', 'run code', 'execute', '代码')
|
|
112
|
-
.tag('code', 'run', 'execute', 'sandbox')
|
|
113
|
-
.param('language', { type: 'string', description: '编程语言(python/javascript/typescript/go/rust/java/c/cpp/ruby/php)' }, true)
|
|
114
|
-
.param('code', { type: 'string', description: '要执行的代码' }, true)
|
|
115
|
-
.execute(async (args) => {
|
|
116
|
-
const result = await runCode(args.language, args.code)
|
|
117
|
-
return formatResult(result)
|
|
118
|
-
})
|
|
119
|
-
.toTool(),
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
// ─── 命令: 运行 ─────────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
addCommand(
|
|
125
|
-
new MessageCommand('运行 <language:text> <code:text>')
|
|
126
|
-
.desc('在沙箱中运行代码片段')
|
|
127
|
-
.action(async (_message, result) => {
|
|
128
|
-
const { language, code } = result.params as { language: string; code: string }
|
|
129
|
-
logger.debug(formatCompact( { op: 'run', language, len: code.length }))
|
|
130
|
-
const res = await runCode(language, code)
|
|
131
|
-
return formatResult(res)
|
|
132
|
-
}),
|
|
133
|
-
)
|
|
1
|
+
export { formatResult, runCode } from './run-code.js';
|
package/src/run-code.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const MAX_CODE_LENGTH = 10000;
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_LANGUAGES: Record<string, string> = {
|
|
4
|
+
python: 'main.py',
|
|
5
|
+
javascript: 'main.js',
|
|
6
|
+
typescript: 'main.ts',
|
|
7
|
+
go: 'main.go',
|
|
8
|
+
rust: 'main.rs',
|
|
9
|
+
java: 'Main.java',
|
|
10
|
+
c: 'main.c',
|
|
11
|
+
cpp: 'main.cpp',
|
|
12
|
+
ruby: 'main.rb',
|
|
13
|
+
php: 'main.php',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const GLOT_API_BASE = 'https://glot.io/api/run';
|
|
17
|
+
|
|
18
|
+
interface RunResult {
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
error: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runCode(language: string, code: string): Promise<RunResult> {
|
|
25
|
+
const lang = language.toLowerCase().trim();
|
|
26
|
+
|
|
27
|
+
if (!SUPPORTED_LANGUAGES[lang]) {
|
|
28
|
+
return {
|
|
29
|
+
stdout: '',
|
|
30
|
+
stderr: '',
|
|
31
|
+
error: `不支持的语言: ${lang}。支持: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (code.length > MAX_CODE_LENGTH) {
|
|
36
|
+
return {
|
|
37
|
+
stdout: '',
|
|
38
|
+
stderr: '',
|
|
39
|
+
error: `代码长度超过上限 ${MAX_CODE_LENGTH} 字符(当前 ${code.length})`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fileName = SUPPORTED_LANGUAGES[lang];
|
|
44
|
+
const url = `${GLOT_API_BASE}/${lang}/latest`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
files: [{ name: fileName, content: code }],
|
|
52
|
+
}),
|
|
53
|
+
signal: AbortSignal.timeout(30_000),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
return {
|
|
58
|
+
stdout: '',
|
|
59
|
+
stderr: '',
|
|
60
|
+
error: `API 请求失败: ${response.status} ${response.statusText}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = (await response.json()) as RunResult;
|
|
65
|
+
return {
|
|
66
|
+
stdout: data.stdout || '',
|
|
67
|
+
stderr: data.stderr || '',
|
|
68
|
+
error: data.error || '',
|
|
69
|
+
};
|
|
70
|
+
} catch (e: unknown) {
|
|
71
|
+
const err = e as Error;
|
|
72
|
+
const msg = err.name === 'TimeoutError' ? '请求超时(30s)' : err.message || '未知错误';
|
|
73
|
+
return { stdout: '', stderr: '', error: msg };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatResult(result: RunResult): string {
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
if (result.stdout) parts.push(`[stdout]\n${result.stdout}`);
|
|
80
|
+
if (result.stderr) parts.push(`[stderr]\n${result.stderr}`);
|
|
81
|
+
if (result.error) parts.push(`[error]\n${result.error}`);
|
|
82
|
+
return parts.length ? parts.join('\n') : '(无输出)';
|
|
83
|
+
}
|