Claude CLI 在 node-pty 环境中执行时会遇到 TTY 交互模式问题,主要表现为:
- Raw mode 不支持错误
- 交互式输入等待导致超时
- 权限问题(不能以 root 用户运行)
import { spawn } from 'child_process';
const child = spawn('sh', ['-c', `echo "${prompt}" | claude --dangerously-skip-permissions`], {
env: { ...process.env }
});- 优点:简单可靠,不需要 PTY
- 性能:6-9秒响应时间
- 成功率:100%
import pty from 'node-pty';
const outputFile = `/tmp/claude_output_${Date.now()}.txt`;
const command = `echo "${prompt}" | claude --dangerously-skip-permissions > ${outputFile} 2>&1 && cat ${outputFile} && rm ${outputFile}`;
const ptyProcess = pty.spawn('sh', ['-c', command], {
name: 'xterm-256color',
cols: 80,
rows: 30,
cwd: process.cwd(),
env: process.env
});- 优点:在 PTY 环境中可用,适合 Web Terminal
- 性能:7-8秒响应时间
- 成功率:100%
- 直接 Spawn Claude:超时,等待交互输入
- PTY 直接执行:Raw mode 错误
- PTY 非交互模式:仍然触发 Raw mode 错误
- 必须:以非 root 用户运行(UID != 0)
- Docker 中使用
USER node或类似配置 - 测试环境:UID=1000, GID=1000
export ANTHROPIC_API_KEY="your-api-key"
export ANTHROPIC_BASE_URL="http://your-api-endpoint/api/"# 安装 Claude CLI
RUN npm install -g @anthropic-ai/claude-code
# 切换到非 root 用户
USER node
# 设置环境变量
ENV ANTHROPIC_API_KEY="your-key" \
ANTHROPIC_BASE_URL="your-url"// services/claudeService.js
export class ClaudeService {
async executePrompt(prompt) {
// 优先使用 spawn + echo pipe
return new Promise((resolve, reject) => {
const child = spawn('sh', ['-c', `echo "${prompt}" | claude --dangerously-skip-permissions`], {
env: process.env,
timeout: 30000
});
let output = '';
child.stdout.on('data', (data) => {
output += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve(output.trim());
} else {
reject(new Error(`Claude CLI exited with code ${code}`));
}
});
});
}
}对于需要在 Web Terminal 中执行的场景,使用文件重定向方法:
// 在 PTY session 中执行
const executeInPty = async (ptyProcess, prompt) => {
const outputFile = `/tmp/claude_${Date.now()}.txt`;
const command = `echo "${prompt}" | claude --dangerously-skip-permissions > ${outputFile} 2>&1 && cat ${outputFile} && rm ${outputFile}`;
ptyProcess.write(command + '\n');
// 处理输出...
};- 缓存机制:对相同 prompt 实现缓存
- 并发控制:限制同时执行的 Claude 进程数
- 超时处理:设置合理的超时时间(建议 30秒)
- 错误重试:实现指数退避重试机制
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
| "Raw mode is not supported" | TTY 交互模式问题 | 使用 echo pipe 或文件重定向 |
| "cannot be used with root/sudo" | root 权限运行 | 切换到普通用户 |
| 命令超时 | 等待交互输入 | 使用 echo 管道提供输入 |
| "command not found" | Claude CLI 未安装 | 安装 @anthropic-ai/claude-code |
- 测试环境检查
# 检查用户
whoami
id
# 检查 Claude CLI
which claude
claude --version
# 测试简单命令
echo "1+1=" | claude --dangerously-skip-permissions- 日志记录
- 记录命令执行时间
- 保存错误输出
- 监控进程状态
- 错误处理:始终实现超时和错误处理
- 资源清理:确保临时文件被删除
- 安全性:验证和清理用户输入
- 监控:记录执行指标和错误率
- 文档:保持环境配置文档更新
// terminal-backend/src/services/claudeExecutorDirect.js
import { spawn } from 'child_process'
class ClaudeExecutorDirectService {
async executePrompt(prompt, timeout = 30000, purpose = 'general') {
const startTime = Date.now()
return new Promise((resolve) => {
let output = ''
let errorOutput = ''
let processExited = false
// 关键:使用 echo pipe 避免 TTY 交互
const escapedPrompt = prompt
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`')
const command = `echo "${escapedPrompt}" | claude --dangerously-skip-permissions`
const child = spawn('sh', ['-c', command], {
env: {
...process.env,
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL
},
stdio: ['pipe', 'pipe', 'pipe'],
cwd: process.cwd()
})
child.stdout.on('data', (data) => {
output += data.toString()
})
child.stderr.on('data', (data) => {
errorOutput += data.toString()
})
child.on('exit', (code) => {
processExited = true
if (code === 0 && output) {
resolve({
success: true,
output: output.trim(),
executionTime: Date.now() - startTime
})
} else {
resolve({
success: false,
output: '',
executionTime: Date.now() - startTime,
error: errorOutput || `Process exited with code ${code}`
})
}
})
setTimeout(() => {
if (!processExited) {
child.kill('SIGTERM')
resolve({
success: false,
output: output,
executionTime: timeout,
error: 'Execution timeout'
})
}
}, timeout)
})
}
}
export default new ClaudeExecutorDirectService()// terminal-backend/src/routes/generate.js
router.post('/cc', async (req, res) => {
const { prompt, timeout = 30000 } = req.body
if (!prompt) {
return res.status(400).json({
code: 400,
success: false,
message: 'Prompt is required'
})
}
const result = await claudeExecutorDirect.executePrompt(
prompt,
Math.min(timeout, 600000),
'cc_api'
)
res.json({
code: result.success ? 200 : 500,
success: result.success,
output: result.output,
executionTime: result.executionTime,
message: result.error
})
})| 测试用例 | 请求内容 | 响应时间 | 结果 |
|---|---|---|---|
| 简单计算 | 1+1= |
7.7秒 | 2 |
| 知识问答 | What is the capital of France? |
9.3秒 | Paris |
| 中文理解 | 人工智能的定义是什么? |
10.2秒 | 完整定义 |
| 代码生成 | Write a Python quicksort function |
12.5秒 | 完整代码 |
- 字符转义处理:处理双引号、美元符号、反引号等特殊字符
- 环境变量传递:确保 API 密钥正确传递
- 超时机制:防止进程挂起,最大超时 600 秒
- 错误处理:完整的错误捕获和日志记录