mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-08-02 23:29:47 +08:00
feat: implement session management for image generation skills and add session handling functions
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
writeGeminiCookieMapToDisk,
|
||||
} from './cookie-store.js';
|
||||
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js';
|
||||
import { readSession, writeSession, listSessions } from './session-store.js';
|
||||
|
||||
function printUsage(exitCode = 0): never {
|
||||
const cookiePath = resolveGeminiWebCookiePath();
|
||||
@@ -21,6 +22,10 @@ function printUsage(exitCode = 0): never {
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||
|
||||
Multi-turn conversation (agent generates unique sessionId):
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "Remember 42" --sessionId abc123
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "What number?" --sessionId abc123
|
||||
|
||||
Options:
|
||||
-p, --prompt <text> Prompt text
|
||||
--promptfiles <files...> Read prompt from one or more files (concatenated in order)
|
||||
@@ -28,6 +33,8 @@ Options:
|
||||
--json Output JSON
|
||||
--image [path] Generate an image and save it (default: ./generated.png)
|
||||
--reference <files...> Reference images for vision input
|
||||
--sessionId <id> Session ID for multi-turn conversation (agent should generate unique ID)
|
||||
--list-sessions List saved sessions (max 100, sorted by update time)
|
||||
--login Only refresh cookies, then exit
|
||||
--cookie-path <path> Cookie file path (default: ${cookiePath})
|
||||
--profile-dir <path> Chrome profile dir (default: ${profileDir})
|
||||
@@ -77,6 +84,8 @@ function parseArgs(argv: string[]): {
|
||||
cookiePath?: string;
|
||||
profileDir?: string;
|
||||
referenceImages?: string[];
|
||||
sessionId?: string;
|
||||
listSessions?: boolean;
|
||||
} {
|
||||
const out: ReturnType<typeof parseArgs> = {};
|
||||
const positional: string[] = [];
|
||||
@@ -172,6 +181,19 @@ function parseArgs(argv: string[]): {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === '--sessionId' || arg === '--session-id') {
|
||||
out.sessionId = argv[i + 1] ?? '';
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('--sessionId=') || arg.startsWith('--session-id=')) {
|
||||
out.sessionId = arg.split('=')[1] ?? '';
|
||||
continue;
|
||||
}
|
||||
if (arg === '--list-sessions') {
|
||||
out.listSessions = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('-')) {
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
@@ -194,6 +216,8 @@ function parseArgs(argv: string[]): {
|
||||
if (out.profileDir === '') delete out.profileDir;
|
||||
if (out.promptFiles?.length === 0) delete out.promptFiles;
|
||||
if (out.referenceImages?.length === 0) delete out.referenceImages;
|
||||
if (out.sessionId != null) out.sessionId = out.sessionId.trim();
|
||||
if (out.sessionId === '') delete out.sessionId;
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -267,6 +291,18 @@ async function main(): Promise<void> {
|
||||
const cookiePath = args.cookiePath ?? resolveGeminiWebCookiePath();
|
||||
const profileDir = args.profileDir ?? resolveGeminiWebChromeProfileDir();
|
||||
|
||||
if (args.listSessions) {
|
||||
const sessions = await listSessions();
|
||||
if (sessions.length === 0) {
|
||||
console.log('No saved sessions.');
|
||||
} else {
|
||||
for (const { id, updatedAt } of sessions) {
|
||||
console.log(`${id}\t${updatedAt}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.loginOnly) {
|
||||
await ensureGeminiCookieMap({ cookiePath, profileDir });
|
||||
return;
|
||||
@@ -277,6 +313,9 @@ async function main(): Promise<void> {
|
||||
const prompt = promptFromArgs || (await readPromptFromStdin());
|
||||
if (!prompt) printUsage(1);
|
||||
|
||||
const sessionData = args.sessionId ? await readSession(args.sessionId) : null;
|
||||
const chatMetadata = sessionData?.metadata ?? null;
|
||||
|
||||
let cookieMap = await ensureGeminiCookieMap({ cookiePath, profileDir });
|
||||
|
||||
const desiredModel = resolveModel(args.model || 'gemini-3-pro');
|
||||
@@ -297,10 +336,14 @@ async function main(): Promise<void> {
|
||||
files: referenceImages,
|
||||
model: desiredModel,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (args.sessionId && out.metadata) {
|
||||
await writeSession(args.sessionId, out.metadata, prompt, out.text ?? '', out.errorMessage);
|
||||
}
|
||||
|
||||
let imageSaved = false;
|
||||
let imageCount = 0;
|
||||
if (imagePath) {
|
||||
@@ -313,13 +356,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
imagePath ? { ...out, imageSaved, imageCount, imagePath } : out,
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
const jsonOut = { ...out, ...(imagePath && { imageSaved, imageCount, imagePath }), ...(args.sessionId && { sessionId: args.sessionId }) };
|
||||
process.stdout.write(`${JSON.stringify(jsonOut, null, 2)}\n`);
|
||||
if (out.errorMessage) process.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -356,10 +394,14 @@ async function main(): Promise<void> {
|
||||
files: referenceImages,
|
||||
model: desiredModel,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (args.sessionId && out.metadata) {
|
||||
await writeSession(args.sessionId, out.metadata, prompt, out.text ?? '', out.errorMessage);
|
||||
}
|
||||
|
||||
let imageSaved = false;
|
||||
let imageCount = 0;
|
||||
if (imagePath) {
|
||||
@@ -372,13 +414,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
imagePath ? { ...out, imageSaved, imageCount, imagePath } : out,
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
const jsonOut = { ...out, ...(imagePath && { imageSaved, imageCount, imagePath }), ...(args.sessionId && { sessionId: args.sessionId }) };
|
||||
process.stdout.write(`${JSON.stringify(jsonOut, null, 2)}\n`);
|
||||
if (out.errorMessage) process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,3 +34,12 @@ export function resolveGeminiWebChromeProfileDir(): string {
|
||||
if (override) return path.resolve(override);
|
||||
return path.join(resolveGeminiWebDataDir(), PROFILE_DIR_NAME);
|
||||
}
|
||||
|
||||
export function resolveGeminiWebSessionsDir(): string {
|
||||
return path.join(resolveGeminiWebDataDir(), 'sessions');
|
||||
}
|
||||
|
||||
export function resolveGeminiWebSessionPath(name: string): string {
|
||||
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(resolveGeminiWebSessionsDir(), `${sanitized}.json`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { mkdir, readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { resolveGeminiWebSessionsDir, resolveGeminiWebSessionPath } from './paths.js';
|
||||
|
||||
export interface SessionMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
metadata: unknown;
|
||||
messages: SessionMessage[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SessionListItem {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export async function readSession(id: string): Promise<SessionData | null> {
|
||||
const sessionPath = resolveGeminiWebSessionPath(id);
|
||||
try {
|
||||
const content = await readFile(sessionPath, 'utf8');
|
||||
return JSON.parse(content) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeSession(
|
||||
id: string,
|
||||
metadata: unknown,
|
||||
userMessage: string,
|
||||
assistantMessage: string,
|
||||
error?: string,
|
||||
): Promise<void> {
|
||||
const sessionPath = resolveGeminiWebSessionPath(id);
|
||||
const sessionsDir = resolveGeminiWebSessionsDir();
|
||||
await mkdir(sessionsDir, { recursive: true });
|
||||
|
||||
const existing = await readSession(id);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const newMessages: SessionMessage[] = [
|
||||
{ role: 'user', content: userMessage, timestamp: now },
|
||||
{ role: 'assistant', content: assistantMessage, timestamp: now, ...(error && { error }) },
|
||||
];
|
||||
|
||||
const data: SessionData = {
|
||||
id,
|
||||
metadata,
|
||||
messages: [...(existing?.messages ?? []), ...newMessages],
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await writeFile(sessionPath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export async function listSessions(limit = 100): Promise<SessionListItem[]> {
|
||||
const sessionsDir = resolveGeminiWebSessionsDir();
|
||||
try {
|
||||
const files = await readdir(sessionsDir);
|
||||
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
||||
|
||||
const items: { id: string; updatedAt: string; mtime: number }[] = [];
|
||||
for (const file of jsonFiles) {
|
||||
const filePath = path.join(sessionsDir, file);
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
items.push({
|
||||
id: file.slice(0, -5),
|
||||
updatedAt: stats.mtime.toISOString(),
|
||||
mtime: stats.mtime.getTime(),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => b.mtime - a.mtime);
|
||||
return items.slice(0, limit).map(({ id, updatedAt }) => ({ id, updatedAt }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user