fix(response, review): fix P1-6, P1-7, P1-8 from Codex review
- P1-6: Add threadId support to Discord reply (use threadId as target channel) - P1-7: Handle long input (>4000 chars) with temp files - P1-8: Capture tmux output with capture-pane instead of send-keys stdout
This commit is contained in:
parent
415a52055c
commit
ca57901c2d
@ -129,7 +129,10 @@ function buildEmbeds(embeds: ReplyEmbed[]): Record<string, unknown>[] {
|
|||||||
* @param options - オプション
|
* @param options - オプション
|
||||||
*/
|
*/
|
||||||
export async function sendReply(replyData: ReplyData, options: DiscordReplyOptions): Promise<void> {
|
export async function sendReply(replyData: ReplyData, options: DiscordReplyOptions): Promise<void> {
|
||||||
const { api, channelId, messageId } = options;
|
const { api, channelId, messageId, threadId } = options;
|
||||||
|
|
||||||
|
// P1-6修正: スレッド内返信の場合はthreadIdをチャンネルとして使用
|
||||||
|
const targetChannelId = threadId || channelId;
|
||||||
|
|
||||||
// 引用メタデータ構築
|
// 引用メタデータ構築
|
||||||
const quote: QuoteMetadata = {
|
const quote: QuoteMetadata = {
|
||||||
@ -157,10 +160,19 @@ export async function sendReply(replyData: ReplyData, options: DiscordReplyOptio
|
|||||||
// メンション設定
|
// メンション設定
|
||||||
const allowedMentions = buildAllowedMentions(options);
|
const allowedMentions = buildAllowedMentions(options);
|
||||||
|
|
||||||
|
// P1-6修正: message_referenceにthread_idを追加
|
||||||
|
const messageReference: Record<string, string> = {
|
||||||
|
channel_id: channelId,
|
||||||
|
message_id: messageId,
|
||||||
|
};
|
||||||
|
if (threadId) {
|
||||||
|
messageReference.thread_id = threadId;
|
||||||
|
}
|
||||||
|
|
||||||
// ファイル添付がある場合
|
// ファイル添付がある場合
|
||||||
if (options.fileUrls && options.fileUrls.length > 0) {
|
if (options.fileUrls && options.fileUrls.length > 0) {
|
||||||
// 最初のメッセージでテキスト+最初のファイル
|
// 最初のメッセージでテキスト+最初のファイル
|
||||||
await api.rest.post(`/channels/${channelId}/messages`, {
|
await api.rest.post(`/channels/${targetChannelId}/messages`, {
|
||||||
content,
|
content,
|
||||||
allowed_mentions: allowedMentions,
|
allowed_mentions: allowedMentions,
|
||||||
attachments: options.fileUrls.slice(0, 1).map((url, i) => ({
|
attachments: options.fileUrls.slice(0, 1).map((url, i) => ({
|
||||||
@ -168,15 +180,12 @@ export async function sendReply(replyData: ReplyData, options: DiscordReplyOptio
|
|||||||
description: `artifact-${i}`,
|
description: `artifact-${i}`,
|
||||||
url,
|
url,
|
||||||
})),
|
})),
|
||||||
message_reference: {
|
message_reference: messageReference,
|
||||||
channel_id: channelId,
|
|
||||||
message_id: messageId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 追加ファイルを別メッセージで送信
|
// 追加ファイルを別メッセージで送信
|
||||||
for (const fileUrl of options.fileUrls.slice(1)) {
|
for (const fileUrl of options.fileUrls.slice(1)) {
|
||||||
await api.rest.post(`/channels/${channelId}/messages`, {
|
await api.rest.post(`/channels/${targetChannelId}/messages`, {
|
||||||
content: "",
|
content: "",
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
@ -192,25 +201,21 @@ export async function sendReply(replyData: ReplyData, options: DiscordReplyOptio
|
|||||||
|
|
||||||
// Embedがある場合
|
// Embedがある場合
|
||||||
if (options.embeds && options.embeds.length > 0) {
|
if (options.embeds && options.embeds.length > 0) {
|
||||||
await api.rest.post(`/channels/${channelId}/messages`, {
|
await api.rest.post(`/channels/${targetChannelId}/messages`, {
|
||||||
content,
|
content,
|
||||||
allowed_mentions: allowedMentions,
|
allowed_mentions: allowedMentions,
|
||||||
embeds: buildEmbeds(options.embeds),
|
embeds: buildEmbeds(options.embeds),
|
||||||
message_reference: {
|
message_reference: messageReference,
|
||||||
channel_id: channelId,
|
|
||||||
message_id: messageId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// シンプルテキスト返信
|
// シンプルテキスト返信
|
||||||
await api.rest.post(`/channels/${channelId}/messages`, {
|
await api.rest.post(`/channels/${targetChannelId}/messages`, {
|
||||||
content,
|
content,
|
||||||
allowed_mentions: allowedMentions,
|
allowed_mentions: allowedMentions,
|
||||||
message_reference: {
|
message_reference: {
|
||||||
channel_id: channelId,
|
...messageReference,
|
||||||
message_id: messageId,
|
|
||||||
fail_if_not_exists: false,
|
fail_if_not_exists: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import { writeFileSync, unlinkSync } from "fs";
|
||||||
|
import { mkdtempSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
import type {
|
import type {
|
||||||
CodexReview,
|
CodexReview,
|
||||||
ReviewRequest,
|
ReviewRequest,
|
||||||
@ -29,6 +32,9 @@ const DEFAULT_TIMEOUT = 5 * 60 * 1000;
|
|||||||
/** デフォルトtmuxターゲット (MacBook用) */
|
/** デフォルトtmuxターゲット (MacBook用) */
|
||||||
const DEFAULT_TMUX_TARGET = "%2"; // カエデ (CodeGen) のペイン
|
const DEFAULT_TMUX_TARGET = "%2"; // カエデ (CodeGen) のペイン
|
||||||
|
|
||||||
|
/** P1-7修正: 一時ファイルの最大サイズ (4000文字) */
|
||||||
|
const MAX_COMMAND_LENGTH = 4000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* シェルコマンド用に文字列をエスケープ
|
* シェルコマンド用に文字列をエスケープ
|
||||||
* tmux send-keys に安全に渡すためのエスケープ処理
|
* tmux send-keys に安全に渡すためのエスケープ処理
|
||||||
@ -50,6 +56,8 @@ function escapeShellString(str: string): string {
|
|||||||
/**
|
/**
|
||||||
* tmuxコマンドを実行
|
* tmuxコマンドを実行
|
||||||
*
|
*
|
||||||
|
* P1-8修正: capture-paneで出力を取得する
|
||||||
|
*
|
||||||
* @param command - 実行するコマンド
|
* @param command - 実行するコマンド
|
||||||
* @param target - tmuxターゲット (ペインID)
|
* @param target - tmuxターゲット (ペインID)
|
||||||
* @param options - オプション
|
* @param options - オプション
|
||||||
@ -62,21 +70,32 @@ async function execTmux(
|
|||||||
): Promise<TmuxResult> {
|
): Promise<TmuxResult> {
|
||||||
const { timeout = DEFAULT_TIMEOUT, env = {} } = options;
|
const { timeout = DEFAULT_TIMEOUT, env = {} } = options;
|
||||||
|
|
||||||
// tmux send-keys でコマンドを送信 (コマンドインジェクション対策)
|
|
||||||
const escapedCommand = escapeShellString(command);
|
|
||||||
const sendCommand = `tmux send-keys -t ${target} "${escapedCommand}" Enter`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// タイムアウト付きで実行
|
// tmux send-keys でコマンドを送信 (コマンドインジェクション対策)
|
||||||
const { stdout, stderr } = await execAsync(sendCommand, {
|
const escapedCommand = escapeShellString(command);
|
||||||
|
const sendCommand = `tmux send-keys -t ${target} "${escapedCommand}" Enter`;
|
||||||
|
|
||||||
|
// コマンド送信
|
||||||
|
await execAsync(sendCommand, {
|
||||||
timeout,
|
timeout,
|
||||||
env: { ...process.env, ...env },
|
env: { ...process.env, ...env },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// P1-8修正: 実行完了を待ってから出力をキャプチャ
|
||||||
|
// コマンド実行に十分な時間を待つ(デフォルトでtimeoutの80%を待機)
|
||||||
|
const waitTime = Math.min(timeout * 0.8, 30000); // 最大30秒待機
|
||||||
|
await sleep(waitTime);
|
||||||
|
|
||||||
|
// capture-paneでペインの内容を取得
|
||||||
|
const captureCommand = `tmux capture-pane -t ${target} -p -S -`;
|
||||||
|
const { stdout: captured } = await execAsync(captureCommand, {
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
stdout: stdout.trim(),
|
stdout: captured.trim(),
|
||||||
stderr: stderr.trim(),
|
stderr: "",
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -90,9 +109,18 @@ async function execTmux(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定ミリ秒待機する
|
||||||
|
*/
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codexでコードレビューを実行
|
* Codexでコードレビューを実行
|
||||||
*
|
*
|
||||||
|
* P1-7修正: 一時ファイルのクリーンアップ処理を追加
|
||||||
|
*
|
||||||
* @param content - レビュー対象コード
|
* @param content - レビュー対象コード
|
||||||
* @param options - オプション
|
* @param options - オプション
|
||||||
* @returns レビュー結果
|
* @returns レビュー結果
|
||||||
@ -103,11 +131,10 @@ export async function runCodexReview(
|
|||||||
): Promise<ReviewResult> {
|
): Promise<ReviewResult> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
// P1-7修正: buildCodexCommandはcleanup関数を返す場合がある
|
||||||
// Codexに送信するコマンド構築
|
const { command, cleanup } = buildCodexCommand(content, options);
|
||||||
// 既存のスキルやコマンドを使用
|
|
||||||
const command = buildCodexCommand(content, options);
|
|
||||||
|
|
||||||
|
try {
|
||||||
// tmux経由でCodexを実行
|
// tmux経由でCodexを実行
|
||||||
const result = await execTmux(command, options.tmuxTarget, {
|
const result = await execTmux(command, options.tmuxTarget, {
|
||||||
timeout: options.timeout ?? DEFAULT_TIMEOUT,
|
timeout: options.timeout ?? DEFAULT_TIMEOUT,
|
||||||
@ -138,30 +165,77 @@ export async function runCodexReview(
|
|||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
duration,
|
duration,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
// P1-7修正: 一時ファイルをクリーンアップ
|
||||||
|
cleanup?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codexコマンドを構築
|
* Codexコマンドを構築
|
||||||
*
|
*
|
||||||
|
* P1-7修正: 長い入力は一時ファイル経由で処理
|
||||||
|
*
|
||||||
* @param content - レビュー対象コード
|
* @param content - レビュー対象コード
|
||||||
* @param options - オプション
|
* @param options - オプション
|
||||||
* @returns コマンド文字列
|
* @returns コマンド文字列とクリーンアップ関数
|
||||||
*/
|
*/
|
||||||
function buildCodexCommand(content: string, options: ReviewOptions): string {
|
function buildCodexCommand(
|
||||||
// コンテンツを一時ファイルに保存または引数として渡す
|
content: string,
|
||||||
// 長いコードの場合はファイル経由が安全
|
options: ReviewOptions,
|
||||||
|
): { command: string; cleanup?: () => void } {
|
||||||
|
// P1-7修正: 長いコードは一時ファイルに書き出し
|
||||||
|
if (content.length > MAX_COMMAND_LENGTH) {
|
||||||
|
// 一時ディレクトリを作成
|
||||||
|
const tempDir = mkdtempSync("/tmp/codex-review-");
|
||||||
|
const tempFile = join(tempDir, "code.txt");
|
||||||
|
|
||||||
const maxLength = 1000;
|
try {
|
||||||
const useFile = content.length > maxLength;
|
// コンテンツをファイルに書き出し
|
||||||
|
writeFileSync(tempFile, content, "utf-8");
|
||||||
|
|
||||||
if (useFile) {
|
// オプションを付与
|
||||||
// TODO: 一時ファイルを使用する場合
|
const opts: string[] = [];
|
||||||
// 今回は簡易的に引数渡し (コマンドインジェクション対策)
|
if (options.threshold) {
|
||||||
const escapedContent = escapeShellString(content);
|
opts.push(`--threshold ${options.threshold}`);
|
||||||
return `codex review "${escapedContent}"`;
|
}
|
||||||
|
if (options.issuesOnly) {
|
||||||
|
opts.push("--issues-only");
|
||||||
|
}
|
||||||
|
if (options.suggestionsOnly) {
|
||||||
|
opts.push("--suggestions-only");
|
||||||
|
}
|
||||||
|
if (options.verbose) {
|
||||||
|
opts.push("--verbose");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = `codex review ${opts.join(" ")} -f ${escapeShellString(tempFile)}`;
|
||||||
|
|
||||||
|
// クリーンアップ関数を返す
|
||||||
|
return {
|
||||||
|
command: cmd,
|
||||||
|
cleanup: () => {
|
||||||
|
try {
|
||||||
|
unlinkSync(tempFile);
|
||||||
|
} catch {
|
||||||
|
// クリーンアップエラーは無視
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 一時ディレクトリの削除(rmdirは空の場合のみ成功)
|
||||||
|
const { rmdirSync } = require("fs");
|
||||||
|
rmdirSync(tempDir);
|
||||||
|
} catch {
|
||||||
|
// ディレクトリが空でない場合は無視
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// ファイル書き込みエラー時はフォールバック
|
||||||
|
console.warn("[CodexReviewer] Failed to write temp file, using inline content");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 短いコンテンツは引数渡し
|
||||||
// オプションを付与
|
// オプションを付与
|
||||||
const opts: string[] = [];
|
const opts: string[] = [];
|
||||||
if (options.threshold) {
|
if (options.threshold) {
|
||||||
@ -180,7 +254,7 @@ function buildCodexCommand(content: string, options: ReviewOptions): string {
|
|||||||
// コマンドインジェクション対策: contentをエスケープしてクォート
|
// コマンドインジェクション対策: contentをエスケープしてクォート
|
||||||
const escapedContent = escapeShellString(content);
|
const escapedContent = escapeShellString(content);
|
||||||
const cmd = `codex review ${opts.join(" ")} "${escapedContent}"`;
|
const cmd = `codex review ${opts.join(" ")} "${escapedContent}"`;
|
||||||
return cmd;
|
return { command: cmd };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user