@huyooo/ai-chat-core 0.2.19 → 0.2.21

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,32 @@
1
+ /**
2
+ * 测试用:根据 SSE 数据行创建 Response,用于 mock fetch
3
+ */
4
+ export function createSSEResponse(sseDataLines: string[], options?: { ok?: boolean; status?: number }): Response {
5
+ const ok = options?.ok ?? true
6
+ const status = options?.status ?? 200
7
+ const body = new ReadableStream<Uint8Array>({
8
+ start(controller) {
9
+ const encoder = new TextEncoder()
10
+ for (const line of sseDataLines) {
11
+ controller.enqueue(encoder.encode(`data: ${line}\n`))
12
+ }
13
+ controller.close()
14
+ },
15
+ })
16
+ return new Response(body, {
17
+ status,
18
+ ok,
19
+ headers: { 'Content-Type': 'text/event-stream' },
20
+ })
21
+ }
22
+
23
+ /**
24
+ * 收集异步生成器的所有值
25
+ */
26
+ export async function collectAsync<T>(gen: AsyncGenerator<T>): Promise<T[]> {
27
+ const out: T[] = []
28
+ for await (const x of gen) {
29
+ out.push(x)
30
+ }
31
+ return out
32
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 工具执行器
3
+ */
4
+
5
+ import type { ToolExecutor } from './types';
6
+
7
+ /** 危险命令黑名单 */
8
+ const DANGEROUS_COMMANDS = [
9
+ 'rm -rf /', 'rm -rf ~', 'format', 'mkfs', 'dd if=',
10
+ 'shutdown', 'reboot', 'sudo rm', 'sudo format'
11
+ ];
12
+
13
+ function isDangerousCommand(command: string): boolean {
14
+ const lowerCommand = command.toLowerCase().trim();
15
+ return DANGEROUS_COMMANDS.some(dangerous =>
16
+ lowerCommand.includes(dangerous.toLowerCase())
17
+ );
18
+ }
19
+
20
+ /**
21
+ * 创建默认工具执行器(Node.js 环境)
22
+ *
23
+ * 用于执行 shell 命令,包含基本安全检查
24
+ * 支持通过 AbortSignal 取消命令执行
25
+ */
26
+ export function createDefaultToolExecutor(defaultCwd: string = process.cwd()): ToolExecutor {
27
+ return {
28
+ async executeCommand(command: string, cwd?: string, signal?: AbortSignal, hooks?: { onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void }) {
29
+ // 检查是否已取消
30
+ if (signal?.aborted) {
31
+ return { success: false, error: '操作已取消' };
32
+ }
33
+
34
+ if (isDangerousCommand(command)) {
35
+ return { success: false, error: '该命令被安全策略阻止' };
36
+ }
37
+
38
+ try {
39
+ const { spawn } = await import('child_process');
40
+
41
+ return new Promise((resolve) => {
42
+ let stdout = '';
43
+ let stderr = '';
44
+ let killed = false;
45
+ let killTimer: NodeJS.Timeout | null = null;
46
+
47
+ const child = spawn('sh', ['-c', command], {
48
+ cwd: cwd || defaultCwd,
49
+ env: process.env,
50
+ // 关键:让 sh 及其子进程处于同一进程组,便于取消/超时时“一锅端”
51
+ // macOS/Linux: 可用负 PID kill 进程组;Windows 会忽略 detached 的语义
52
+ detached: process.platform !== 'win32',
53
+ });
54
+
55
+ const killProcessTree = (signalName: NodeJS.Signals) => {
56
+ if (killed) return;
57
+ killed = true;
58
+ try {
59
+ if (process.platform !== 'win32' && typeof child.pid === 'number') {
60
+ // 负 PID => 发送到该进程组(避免 sh 死了但子进程继续跑)
61
+ process.kill(-child.pid, signalName);
62
+ } else {
63
+ child.kill(signalName);
64
+ }
65
+ } catch {
66
+ try {
67
+ child.kill(signalName);
68
+ } catch {
69
+ // ignore
70
+ }
71
+ }
72
+
73
+ // 兜底:若 SIGTERM 不生效,短延迟后再 SIGKILL
74
+ if (killTimer) clearTimeout(killTimer);
75
+ killTimer = setTimeout(() => {
76
+ try {
77
+ if (process.platform !== 'win32' && typeof child.pid === 'number') {
78
+ process.kill(-child.pid, 'SIGKILL');
79
+ } else {
80
+ child.kill('SIGKILL');
81
+ }
82
+ } catch {
83
+ // ignore
84
+ }
85
+ }, 1500);
86
+ };
87
+
88
+ // 设置超时
89
+ const timeout = setTimeout(() => {
90
+ if (killed) return;
91
+ killProcessTree('SIGTERM');
92
+ resolve({ success: false, error: '命令执行超时(30秒)' });
93
+ }, 30000);
94
+
95
+ // 监听取消信号
96
+ const abortHandler = () => {
97
+ if (killed) return;
98
+ killProcessTree('SIGTERM');
99
+ clearTimeout(timeout);
100
+ resolve({ success: false, error: '操作已取消' });
101
+ };
102
+ signal?.addEventListener('abort', abortHandler);
103
+
104
+ child.stdout?.on('data', (data) => {
105
+ const chunk = data.toString();
106
+ stdout += chunk;
107
+ hooks?.onStdout?.(chunk);
108
+ });
109
+
110
+ child.stderr?.on('data', (data) => {
111
+ const chunk = data.toString();
112
+ stderr += chunk;
113
+ hooks?.onStderr?.(chunk);
114
+ });
115
+
116
+ child.on('close', (code) => {
117
+ clearTimeout(timeout);
118
+ signal?.removeEventListener('abort', abortHandler);
119
+ if (killTimer) {
120
+ clearTimeout(killTimer);
121
+ killTimer = null;
122
+ }
123
+
124
+ if (killed) return;
125
+
126
+ const output = stdout.trim();
127
+ const errOutput = stderr.trim();
128
+
129
+ if (code === 0) {
130
+ resolve({ success: true, output: output || errOutput || '执行成功' });
131
+ } else {
132
+ // 非零退出码但有输出时,也算成功(某些命令会这样)
133
+ if (output) {
134
+ const warning = errOutput ? `\n[警告: ${errOutput}]` : '';
135
+ resolve({ success: true, output: output + warning });
136
+ } else if (errOutput) {
137
+ resolve({ success: false, error: errOutput });
138
+ } else {
139
+ resolve({ success: false, error: `命令退出码: ${code}` });
140
+ }
141
+ }
142
+ });
143
+
144
+ child.on('error', (err) => {
145
+ clearTimeout(timeout);
146
+ signal?.removeEventListener('abort', abortHandler);
147
+ if (killTimer) {
148
+ clearTimeout(killTimer);
149
+ killTimer = null;
150
+ }
151
+
152
+ if (!killed) {
153
+ resolve({ success: false, error: err.message });
154
+ }
155
+ });
156
+ });
157
+ } catch (error) {
158
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
159
+ }
160
+ }
161
+ };
162
+ }