fix codex skills running

This commit is contained in:
cexll
2025-11-19 14:54:45 +08:00
parent 18c26a252a
commit 4230479ff4
2 changed files with 151 additions and 78 deletions

View File

@@ -23,7 +23,6 @@ DEFAULT_MODEL = 'gpt-5.1-codex'
DEFAULT_WORKDIR = '.'
DEFAULT_TIMEOUT = 7200 # 2 hours in seconds
FORCE_KILL_DELAY = 5
STDIN_THRESHOLD = 800 # Auto-switch to stdin for prompts longer than 800 chars
def log_error(message: str):
@@ -36,6 +35,11 @@ def log_warn(message: str):
sys.stderr.write(f"WARN: {message}\n")
def log_info(message: str):
"""输出信息到 stderr"""
sys.stderr.write(f"INFO: {message}\n")
def resolve_timeout() -> int:
"""解析超时配置(秒)"""
raw = os.environ.get('CODEX_TIMEOUT', '')
@@ -90,33 +94,55 @@ def parse_args():
}
def build_codex_args(params: dict, use_stdin: bool) -> list:
def read_piped_task() -> Optional[str]:
"""
从 stdin 读取任务文本:
- 如果 stdin 是管道(非 tty且存在内容返回读取到的字符串
- 否则返回 None
"""
stdin = sys.stdin
if stdin is None or stdin.isatty():
return None
data = stdin.read()
return data if data else None
def should_stream_via_stdin(task_text: str, piped: bool) -> bool:
"""
判定是否通过 stdin 传递任务:
- 有管道输入
- 文本包含换行
- 文本包含反斜杠
- 文本长度 > 800
"""
if piped:
return True
if '\n' in task_text:
return True
if '\\' in task_text:
return True
if len(task_text) > 800:
return True
return False
def build_codex_args(params: dict, target_arg: str) -> list:
"""
构建 codex CLI 参数
Args:
params: 参数字典
use_stdin: 是否使用 stdin 模式(不在命令行参数中传递 task
target_arg: 最终传递给 codex 的参数('-' 或具体 task 文本
"""
if params['mode'] == 'resume':
if use_stdin:
return [
'codex', 'e',
'--skip-git-repo-check',
'--json',
'resume',
params['session_id'],
'-' # 从 stdin 读取
]
else:
return [
'codex', 'e',
'--skip-git-repo-check',
'--json',
'resume',
params['session_id'],
params['task']
]
return [
'codex', 'e',
'--skip-git-repo-check',
'--json',
'resume',
params['session_id'],
target_arg
]
else:
base_args = [
'codex', 'e',
@@ -124,50 +150,43 @@ def build_codex_args(params: dict, use_stdin: bool) -> list:
'--dangerously-bypass-approvals-and-sandbox',
'--skip-git-repo-check',
'-C', params['workdir'],
'--json'
'--json',
target_arg
]
if use_stdin:
base_args.append('-') # 从 stdin 读取
else:
base_args.append(params['task'])
return base_args
def main():
params = parse_args()
timeout_sec = resolve_timeout()
# **FIX: Auto-detect long inputs and enable stdin mode**
task_length = len(params['task'])
use_stdin = task_length > STDIN_THRESHOLD
if use_stdin:
log_warn(f"Task length ({task_length} chars) exceeds threshold, using stdin mode to avoid shell escaping issues")
codex_args = build_codex_args(params, use_stdin)
def run_codex_process(codex_args, task_text: str, use_stdin: bool, timeout_sec: int):
"""
启动 codex 子进程,处理 stdin / JSON 行输出和错误,成功时返回 (last_agent_message, thread_id)。
失败路径上负责日志和退出码。
"""
thread_id: Optional[str] = None
last_agent_message: Optional[str] = None
process: Optional[subprocess.Popen] = None
try:
# 启动 codex 子进程
# 启动 codex 子进程(文本模式管道)
process = subprocess.Popen(
codex_args,
stdin=subprocess.PIPE if use_stdin else None, # **FIX: Enable stdin**
stdin=subprocess.PIPE if use_stdin else None,
stdout=subprocess.PIPE,
stderr=sys.stderr, # 错误直接透传到 stderr
stderr=sys.stderr,
text=True,
bufsize=1 # 行缓冲
bufsize=1,
)
# **FIX: 如果使用 stdin 模式,写入任务到 stdin**
if use_stdin:
process.stdin.write(params['task'])
# 如果使用 stdin 模式,写入任务到 stdin 并关闭
if use_stdin and process.stdin is not None:
process.stdin.write(task_text)
process.stdin.close()
# 逐行解析 JSON 输出
if process.stdout is None:
log_error('Codex stdout pipe not available')
sys.exit(1)
for line in process.stdout:
line = line.strip()
if not line:
@@ -190,33 +209,26 @@ def main():
except json.JSONDecodeError:
log_warn(f"Failed to parse line: {line}")
# 等待进程结束
# 等待进程结束并检查退出码
returncode = process.wait(timeout=timeout_sec)
if returncode == 0:
if last_agent_message:
# 输出 agent_message
sys.stdout.write(f"{last_agent_message}\n")
# 输出 session_id如果存在
if thread_id:
sys.stdout.write(f"\n---\nSESSION_ID: {thread_id}\n")
sys.exit(0)
else:
log_error('Codex completed without agent_message output')
sys.exit(1)
else:
if returncode != 0:
log_error(f'Codex exited with status {returncode}')
sys.exit(returncode)
if not last_agent_message:
log_error('Codex completed without agent_message output')
sys.exit(1)
return last_agent_message, thread_id
except subprocess.TimeoutExpired:
log_error('Codex execution timeout')
process.kill()
try:
process.wait(timeout=FORCE_KILL_DELAY)
except subprocess.TimeoutExpired:
pass
if process is not None:
process.kill()
try:
process.wait(timeout=FORCE_KILL_DELAY)
except subprocess.TimeoutExpired:
pass
sys.exit(124)
except FileNotFoundError:
@@ -224,13 +236,61 @@ def main():
sys.exit(127)
except KeyboardInterrupt:
process.terminate()
try:
process.wait(timeout=FORCE_KILL_DELAY)
except subprocess.TimeoutExpired:
process.kill()
log_error("Codex interrupted by user")
if process is not None:
process.terminate()
try:
process.wait(timeout=FORCE_KILL_DELAY)
except subprocess.TimeoutExpired:
process.kill()
sys.exit(130)
def main():
params = parse_args()
timeout_sec = resolve_timeout()
piped_task = read_piped_task()
piped = piped_task is not None
task_text = piped_task if piped else params['task']
use_stdin = should_stream_via_stdin(task_text, piped)
if use_stdin:
reasons = []
if piped:
reasons.append('piped input')
if '\n' in task_text:
reasons.append('newline')
if '\\' in task_text:
reasons.append('backslash')
if len(task_text) > 800:
reasons.append('length>800')
if reasons:
log_warn(f"Using stdin mode for task due to: {', '.join(reasons)}")
target_arg = '-' if use_stdin else params['task']
codex_args = build_codex_args(params, target_arg)
log_info('codex running...')
last_agent_message, thread_id = run_codex_process(
codex_args=codex_args,
task_text=task_text,
use_stdin=use_stdin,
timeout_sec=timeout_sec,
)
# 输出 agent_message
sys.stdout.write(f"{last_agent_message}\n")
# 输出 session_id如果存在
if thread_id:
sys.stdout.write(f"\n---\nSESSION_ID: {thread_id}\n")
sys.exit(0)
if __name__ == '__main__':
main()