feat: retro Claude ekip konsolunu kur
This commit is contained in:
234
server/sessionManager.js
Normal file
234
server/sessionManager.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { buildBootstrapPrompt } from "./bootstrapPrompt.js";
|
||||
import { LogService } from "./logService.js";
|
||||
import { PtyService } from "./ptyService.js";
|
||||
import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js";
|
||||
import { findMentionedMember } from "./teamConfig.js";
|
||||
|
||||
function cleanChunk(value) {
|
||||
return stripAnsi(value).replace(/\r/g, "");
|
||||
}
|
||||
|
||||
function isLikelyFollowUp(prompt) {
|
||||
const normalized = String(prompt ?? "").trim();
|
||||
const wordCount = normalized.split(/\s+/).filter(Boolean).length;
|
||||
|
||||
return [
|
||||
wordCount <= 8,
|
||||
/^(evet|hayir|tamam|olur|olsun|sade|sekersiz|şekersiz|detaylandir|detaylandır|devam|peki|neden|nasil|nasıl)\b/i.test(normalized)
|
||||
].some(Boolean);
|
||||
}
|
||||
|
||||
function buildRoutedPrompt(prompt, lastDirectedMember = null) {
|
||||
const explicitTarget = findMentionedMember(prompt);
|
||||
const targetMember = explicitTarget ?? (lastDirectedMember && isLikelyFollowUp(prompt) ? lastDirectedMember : null);
|
||||
|
||||
if (!targetMember) {
|
||||
return {
|
||||
targetMember: null,
|
||||
routedPrompt: [
|
||||
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
|
||||
"Bu mesaj genel bir gorev veya genel konusmadir.",
|
||||
"Cevabi once Mazlum baslatsin.",
|
||||
"Konusan herkes ad etiketi kullansin.",
|
||||
"[KULLANICI MESAJI]",
|
||||
prompt
|
||||
].join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
targetMember,
|
||||
routedPrompt: [
|
||||
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
|
||||
`Bu mesaj ${targetMember.name} icindir.`,
|
||||
`Yalnizca ${targetMember.name} cevap versin.`,
|
||||
`Cevap \`${targetMember.name}:\` ile baslasin.`,
|
||||
"[KULLANICI MESAJI]",
|
||||
prompt
|
||||
].join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
export class SessionManager {
|
||||
constructor({ io, config }) {
|
||||
this.io = io;
|
||||
this.config = config;
|
||||
this.logService = new LogService(config.watchLogLimit, config.logToConsole ? console : null);
|
||||
this.ptyService = null;
|
||||
this.chatOutput = "";
|
||||
this.lastDirectedMember = null;
|
||||
this.state = {
|
||||
status: "idle",
|
||||
startedAt: null,
|
||||
teamActivated: false,
|
||||
lastError: null,
|
||||
runtime: getPublicRuntimeConfig(config)
|
||||
};
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
...this.state,
|
||||
runtime: getPublicRuntimeConfig(this.config)
|
||||
};
|
||||
}
|
||||
|
||||
getLogSnapshot() {
|
||||
return this.logService.snapshot();
|
||||
}
|
||||
|
||||
getChatSnapshot() {
|
||||
return this.chatOutput;
|
||||
}
|
||||
|
||||
emitState() {
|
||||
this.io.emit("session:state", this.getState());
|
||||
}
|
||||
|
||||
emitLog(entry) {
|
||||
this.io.emit("log:entry", entry);
|
||||
}
|
||||
|
||||
emitChat(chunk) {
|
||||
this.io.emit("chat:chunk", { chunk });
|
||||
}
|
||||
|
||||
addLog(type, message, meta = {}) {
|
||||
const entry = this.logService.push(type, message, meta);
|
||||
this.emitLog(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
setState(patch) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...patch
|
||||
};
|
||||
this.emitState();
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.ptyService?.isRunning()) {
|
||||
throw new Error("Session is already running");
|
||||
}
|
||||
|
||||
this.chatOutput = "";
|
||||
this.lastDirectedMember = null;
|
||||
this.logService.clear();
|
||||
this.io.emit("chat:reset");
|
||||
|
||||
this.ptyService = new PtyService({
|
||||
cwd: this.config.workspaceDir,
|
||||
env: getClaudeEnv(this.config)
|
||||
});
|
||||
|
||||
this.setState({
|
||||
status: "starting",
|
||||
startedAt: new Date().toISOString(),
|
||||
teamActivated: false,
|
||||
lastError: null
|
||||
});
|
||||
|
||||
this.addLog("lifecycle", `Starting Claude session in ${this.config.workspaceDir}`);
|
||||
|
||||
try {
|
||||
await this.ptyService.start({
|
||||
command: this.config.claudeBin,
|
||||
args: this.config.claudeArgs,
|
||||
onData: (chunk) => this.handlePtyData(chunk),
|
||||
onExit: (event) => this.handlePtyExit(event)
|
||||
});
|
||||
this.setState({ status: "running" });
|
||||
this.addLog("lifecycle", `Claude process started with binary ${this.config.claudeBin}`);
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
status: "error",
|
||||
lastError: error.message
|
||||
});
|
||||
this.addLog("error", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.ptyService?.isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addLog("lifecycle", "Stopping Claude session");
|
||||
await this.ptyService.stop();
|
||||
this.lastDirectedMember = null;
|
||||
this.setState({
|
||||
status: "stopped",
|
||||
teamActivated: false
|
||||
});
|
||||
}
|
||||
|
||||
async sendPrompt(prompt) {
|
||||
if (!this.ptyService?.isRunning()) {
|
||||
throw new Error("Session is not running");
|
||||
}
|
||||
|
||||
const { routedPrompt, targetMember } = buildRoutedPrompt(prompt, this.lastDirectedMember);
|
||||
const input = `${routedPrompt}\r`;
|
||||
this.lastDirectedMember = targetMember ?? null;
|
||||
this.addLog("input", prompt);
|
||||
await this.ptyService.write(input);
|
||||
}
|
||||
|
||||
async sendRawPrompt(prompt, meta = {}) {
|
||||
if (!this.ptyService?.isRunning()) {
|
||||
throw new Error("Session is not running");
|
||||
}
|
||||
|
||||
const input = `${prompt}\r`;
|
||||
this.addLog("input", meta.label ?? prompt, meta);
|
||||
await this.ptyService.write(input);
|
||||
}
|
||||
|
||||
async activateTeam() {
|
||||
const prompt = buildBootstrapPrompt();
|
||||
this.lastDirectedMember = null;
|
||||
await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" });
|
||||
this.setState({ teamActivated: true });
|
||||
this.addLog("system", "Team activation prompt sent");
|
||||
}
|
||||
|
||||
resize({ cols, rows }) {
|
||||
this.ptyService?.resize(cols, rows);
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
this.logService.clear();
|
||||
this.io.emit("log:snapshot", []);
|
||||
this.addLog("system", "Watch log cleared");
|
||||
}
|
||||
|
||||
handlePtyData(chunk) {
|
||||
const clean = cleanChunk(chunk);
|
||||
if (!clean) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatOutput += clean;
|
||||
if (this.chatOutput.length > this.config.chatChunkLimit * 20) {
|
||||
this.chatOutput = this.chatOutput.slice(-this.config.chatChunkLimit * 20);
|
||||
}
|
||||
|
||||
this.emitChat(clean);
|
||||
this.addLog("output", clean);
|
||||
}
|
||||
|
||||
handlePtyExit(event) {
|
||||
const exitCode = event?.exitCode ?? 0;
|
||||
const signal = event?.signal ?? 0;
|
||||
const detail = event?.error ? `, error=${event.error}` : "";
|
||||
this.addLog("lifecycle", `Claude session exited (code=${exitCode}, signal=${signal}${detail})`);
|
||||
this.setState({
|
||||
status: "stopped",
|
||||
teamActivated: false
|
||||
});
|
||||
this.ptyService = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user