This commit is contained in:
@@ -41,6 +41,7 @@ const TOOL_DOC_ALIASES: Record<string, string> = {
|
||||
xuserposts: 'xsearch',
|
||||
xpostdetail: 'xsearch',
|
||||
xfetchcardmedia: 'xsearch',
|
||||
xtimeline: 'xsearch',
|
||||
// youtube.ts をまとめる
|
||||
searchyoutube: 'getyoutubetranscript',
|
||||
// maps.ts をまとめる
|
||||
|
||||
@@ -9,6 +9,7 @@ export const RAW_SAVE_TOOLS = new Set([
|
||||
'XSearch',
|
||||
'XUserPosts',
|
||||
'XPostDetail',
|
||||
'XTimeline',
|
||||
'BrowseWeb',
|
||||
'GetYouTubeTranscript',
|
||||
'SearchYouTube',
|
||||
|
||||
@@ -126,7 +126,7 @@ function mkStubSubsystem() {
|
||||
setWindow: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
const client = { end: vi.fn() };
|
||||
const client = { end: vi.fn(), on: vi.fn() };
|
||||
const openShellChannel = vi.fn().mockResolvedValue({
|
||||
channel,
|
||||
client,
|
||||
|
||||
@@ -246,6 +246,7 @@ async function ensureSessionInternal(
|
||||
|
||||
// Open the channel. On failure clear the PEM and bail.
|
||||
let channel: import('ssh2').ClientChannel;
|
||||
let client: import('ssh2').Client;
|
||||
let hostFingerprint: string;
|
||||
try {
|
||||
const shellResult = await sub.openShellChannel({
|
||||
@@ -266,6 +267,7 @@ async function ensureSessionInternal(
|
||||
timeoutMs: sub.config.callTimeoutSeconds * 1000,
|
||||
});
|
||||
channel = shellResult.channel;
|
||||
client = shellResult.client;
|
||||
hostFingerprint = shellResult.hostFingerprint;
|
||||
} catch (e) {
|
||||
clearBuffer(pemBuf);
|
||||
@@ -292,8 +294,10 @@ async function ensureSessionInternal(
|
||||
return err(`SshConsoleEnsure: failed to open shell channel: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
// Build the session and register it. From here on the channel + PEM
|
||||
// belong to the session; we don't clear them on the happy path.
|
||||
// Build the session and register it. From here on the channel + client
|
||||
// + PEM belong to the session; we don't clear them on the happy path.
|
||||
// The session ends the client (and thus releases the PEM-bound
|
||||
// connection) when it closes.
|
||||
const session = new ConsoleSession({
|
||||
localTaskId,
|
||||
connectionId,
|
||||
@@ -303,6 +307,7 @@ async function ensureSessionInternal(
|
||||
rows,
|
||||
scrollbackCap: sub.config.console.scrollbackBytes,
|
||||
channel,
|
||||
client,
|
||||
auditRepo: sub.auditRepo,
|
||||
});
|
||||
sub.sessionRegistry.register(session);
|
||||
|
||||
@@ -129,11 +129,30 @@ const XFETCHCARDMEDIA_DEF: ToolDef = {
|
||||
},
|
||||
};
|
||||
|
||||
const XTIMELINE_DEF: ToolDef = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'XTimeline',
|
||||
description: 'ログイン中アカウントのホームタイムライン(For You / フォロー中)を取得する(twitter-cli 経由、認証 Cookie 設定が必要)。詳細は ReadToolDoc({ name: "XTimeline" })。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tab: { type: 'string', enum: ['for_you', 'following'], description: 'for_you (デフォルト) / following' },
|
||||
limit: { type: 'number', description: '件数 (デフォルト: 20, 最大: 50)' },
|
||||
full_text: { type: 'boolean', description: '長文の省略を避ける' },
|
||||
compact: { type: 'boolean', description: 'token 節約向けの compact 出力' },
|
||||
output_path: { type: 'string', description: '任意: output/x/ 配下に保存する相対パス' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TOOL_DEFS: Record<string, ToolDef> = {
|
||||
XSearch: XSEARCH_DEF,
|
||||
XUserPosts: XUSERPOSTS_DEF,
|
||||
XPostDetail: XPOSTDETAIL_DEF,
|
||||
XFetchCardMedia: XFETCHCARDMEDIA_DEF,
|
||||
XTimeline: XTIMELINE_DEF,
|
||||
};
|
||||
|
||||
type XHistoryRecord = {
|
||||
@@ -608,6 +627,36 @@ async function executeXUserPosts(input: Record<string, unknown>, ctx: ToolContex
|
||||
return runTwitterCli('XUserPosts', args, ctx, outputPath);
|
||||
}
|
||||
|
||||
async function executeXTimeline(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
|
||||
const tab = String(input['tab'] ?? 'for_you');
|
||||
const following = tab === 'following';
|
||||
const args = ['feed', '--max', String(clampLimit(input['limit'], 20)), '--yaml'];
|
||||
if (following) args.push('-t', 'following');
|
||||
maybePushFlag(args, input['full_text'], '--full-text');
|
||||
maybePushFlag(args, input['compact'], '--compact');
|
||||
let outputPath: string | null;
|
||||
try {
|
||||
outputPath = resolveOptionalOutputPath(ctx, input['output_path']);
|
||||
} catch (err) {
|
||||
return { output: `XTimeline error: ${(err as Error).message}`, isError: true };
|
||||
}
|
||||
const result = await runTwitterCli('XTimeline', args, ctx, outputPath);
|
||||
if (result.isError) return result;
|
||||
|
||||
const posts = parseXPostsFromYaml(result.output);
|
||||
if (posts.length > 0) {
|
||||
const refId = `xposts-${Date.now()}`;
|
||||
const structuredBlocks: StructuredBlock[] = [{
|
||||
refId,
|
||||
type: 'x_posts',
|
||||
title: following ? 'X タイムライン(フォロー中)' : 'X タイムライン(For You)',
|
||||
data: { query: following ? 'following' : 'for_you', posts },
|
||||
}];
|
||||
return { output: `${result.output}\n\n[[embed:${refId}]]`, isError: false, structuredBlocks };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function executeXPostDetail(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
|
||||
const tweet = String(input['tweet'] ?? '').trim();
|
||||
if (!tweet) return { output: 'XPostDetail error: tweet is required', isError: true };
|
||||
@@ -862,6 +911,8 @@ export async function executeTool(
|
||||
return executeXSearch(input, ctx);
|
||||
case 'XUserPosts':
|
||||
return executeXUserPosts(input, ctx);
|
||||
case 'XTimeline':
|
||||
return executeXTimeline(input, ctx);
|
||||
case 'XPostDetail':
|
||||
return executeXPostDetail(input, ctx);
|
||||
case 'XFetchCardMedia':
|
||||
|
||||
Reference in New Issue
Block a user