mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 00:48:07 +00:00
433 lines
14 KiB
TypeScript
433 lines
14 KiB
TypeScript
import { In, IsNull, Not } from 'typeorm';
|
|
import { RequestHandler } from 'express';
|
|
import AppDataSource from '../../data-source';
|
|
import { Agent, Conversation, Message } from '../../entities';
|
|
import { MessageRole } from '../../@types/message';
|
|
import { List, Get, Create, Update, Destroy, AgentJson } from '../../@types/agent';
|
|
import * as ocService from '../../services/openclawService';
|
|
|
|
function toSlug(name: string): string {
|
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'agent';
|
|
}
|
|
|
|
const list: List = async (req, res, next) => {
|
|
try {
|
|
const { page = 0, limit = 40, sortField = 'createdAt', sortType = 'desc' } = req.query;
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
|
|
const qb = agentRepo.createQueryBuilder('agent');
|
|
|
|
if (req.query.search) {
|
|
const search = req.query.search as string;
|
|
if (!Number.isNaN(Number(search))) {
|
|
qb.andWhere('agent._id = :id', { id: Number(search) });
|
|
} else {
|
|
qb.andWhere('agent.name LIKE :s', { s: `%${search}%` });
|
|
}
|
|
}
|
|
|
|
const total = await qb.getCount();
|
|
const items = await qb
|
|
.skip(Number(page) * Number(limit))
|
|
.take(Number(limit))
|
|
.orderBy(`agent.${sortField as string}`, (sortType as string).toUpperCase() === 'ASC' ? 'ASC' : 'DESC')
|
|
.getMany();
|
|
|
|
const ocIds = items.map((a) => a.openclawAgentId);
|
|
const models = ocService.getAgentModelsForOpenclawIds(ocIds);
|
|
const itemsWithModel: AgentJson[] = items.map((a) => ({
|
|
_id: a._id,
|
|
name: a.name,
|
|
openclawAgentId: a.openclawAgentId,
|
|
createdBy: a.createdBy,
|
|
createdAt: a.createdAt,
|
|
updatedAt: a.updatedAt,
|
|
model: models[a.openclawAgentId] ?? null,
|
|
}));
|
|
|
|
return res.json({ total, items: itemsWithModel });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const get: Get = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent) return res.json(null);
|
|
const model = ocService.getAgentModel(agent.openclawAgentId);
|
|
const body: AgentJson = {
|
|
_id: agent._id,
|
|
name: agent.name,
|
|
openclawAgentId: agent.openclawAgentId,
|
|
createdBy: agent.createdBy,
|
|
createdAt: agent.createdAt,
|
|
updatedAt: agent.updatedAt,
|
|
model,
|
|
};
|
|
return res.json(body);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const create: Create = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const { interactive, ...body } = req.body;
|
|
const openclawAgentId = body.openclawAgentId?.trim() || toSlug(body.name || '');
|
|
const agent = agentRepo.create({
|
|
...body,
|
|
openclawAgentId,
|
|
createdBy: req.user!._id,
|
|
createdAt: new Date(),
|
|
});
|
|
const saved = await agentRepo.save(agent);
|
|
|
|
if (!interactive) {
|
|
ocService.registerAgent(openclawAgentId);
|
|
}
|
|
|
|
return res.json(saved);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const update: Update = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const id = Number(req.params.id);
|
|
|
|
await agentRepo.update(id, { name: req.body.name, updatedAt: new Date() });
|
|
const agent = await agentRepo.findOneBy({ _id: id });
|
|
if (!agent) return res.status(404).json(null);
|
|
|
|
if (agent.openclawAgentId) {
|
|
ocService.setAgentIdentity(agent.openclawAgentId, req.body.name || '');
|
|
}
|
|
|
|
return res.json(agent);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const destroy: Destroy = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const convRepo = AppDataSource.getRepository(Conversation);
|
|
const id = Number(req.params.id);
|
|
|
|
const agent = await agentRepo.findOneBy({ _id: id });
|
|
await agentRepo.softDelete(id);
|
|
await convRepo.createQueryBuilder()
|
|
.update(Conversation)
|
|
.set({ deletedAt: new Date() })
|
|
.where('agentId = :id', { id })
|
|
.execute();
|
|
|
|
if (agent?.openclawAgentId) {
|
|
ocService.removeAgent(agent.openclawAgentId);
|
|
}
|
|
|
|
return res.json(null);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const sync: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const convRepo = AppDataSource.getRepository(Conversation);
|
|
const msgRepo = AppDataSource.getRepository(Message);
|
|
|
|
// Phase 1: Sync agents (filesystem scan, no CLI)
|
|
const openclawAgents = ocService.listAgents();
|
|
let syncedAgents = 0;
|
|
|
|
if (openclawAgents.length) {
|
|
const existingAgents = await agentRepo.find({
|
|
where: { openclawAgentId: In(openclawAgents.map((a) => a.agentId)) },
|
|
});
|
|
const existingAgentIds = new Set(existingAgents.map((a) => a.openclawAgentId));
|
|
|
|
const newAgents = openclawAgents.filter((a) => !existingAgentIds.has(a.agentId));
|
|
if (newAgents.length) {
|
|
await agentRepo.save(
|
|
newAgents.map((a) => agentRepo.create({
|
|
name: a.name,
|
|
openclawAgentId: a.agentId,
|
|
createdBy: req.user!._id,
|
|
createdAt: a.createdAt,
|
|
})),
|
|
);
|
|
syncedAgents = newAgents.length;
|
|
}
|
|
}
|
|
|
|
// Phase 2: Sync conversations (skip JSONL parsing for first messages)
|
|
const allAgents = await agentRepo.find();
|
|
|
|
const convResults = await Promise.allSettled(allAgents.map(async (agent) => {
|
|
const sessions = ocService.listSessions(agent.openclawAgentId, true);
|
|
if (!sessions.length) return 0;
|
|
|
|
const existingConvs = await convRepo.find({
|
|
where: {
|
|
agentId: agent._id,
|
|
sessionKey: In(sessions.map((s) => s.sessionKey)),
|
|
},
|
|
});
|
|
const existingByKey = new Map(existingConvs.map((c) => [c.sessionKey, c]));
|
|
|
|
const newSessions = sessions.filter((s) => !existingByKey.get(s.sessionKey));
|
|
if (newSessions.length) {
|
|
await convRepo.save(
|
|
newSessions.map((s) => convRepo.create({
|
|
agentId: agent._id,
|
|
sessionKey: s.sessionKey,
|
|
title: s.label || null,
|
|
createdBy: req.user!._id,
|
|
createdAt: s.updatedAt ? new Date(s.updatedAt) : new Date(),
|
|
})),
|
|
);
|
|
}
|
|
|
|
const titleUpdates = sessions
|
|
.map((s) => ({ session: s, existing: existingByKey.get(s.sessionKey) }))
|
|
.filter(({ session, existing }) => existing && !existing.title && session.label);
|
|
await Promise.all(
|
|
titleUpdates.map(({ session, existing }) => convRepo.update(existing!._id, { title: session.label })),
|
|
);
|
|
|
|
return newSessions.length;
|
|
}));
|
|
const syncedConversations = convResults
|
|
.reduce((sum, r) => sum + (r.status === 'fulfilled' ? r.value : 0), 0);
|
|
|
|
// Phase 3: Sync messages (only for conversations missing messages)
|
|
const allConversations = await convRepo.find({
|
|
where: { sessionKey: Not(IsNull()) },
|
|
});
|
|
|
|
const agentMap = new Map(allAgents.map((a) => [a._id, a]));
|
|
|
|
// Get message counts per conversation in one query
|
|
const msgCounts: { conversationId: number; cnt: string }[] = await msgRepo
|
|
.createQueryBuilder('m')
|
|
.select('m.conversationId', 'conversationId')
|
|
.addSelect('COUNT(*)', 'cnt')
|
|
.where('m.conversationId IN (:...ids)', {
|
|
ids: allConversations.map((c) => c._id),
|
|
})
|
|
.groupBy('m.conversationId')
|
|
.getRawMany();
|
|
const countMap = new Map(msgCounts.map((r) => [r.conversationId, Number(r.cnt)]));
|
|
|
|
const msgResults = await Promise.allSettled(allConversations.map(async (conv) => {
|
|
const agent = agentMap.get(conv.agentId);
|
|
if (!agent) return 0;
|
|
|
|
const openclawMessages = ocService.getSessionMessages(agent.openclawAgentId, conv.sessionKey!);
|
|
const validMessages = openclawMessages.filter((m) => m.externalId);
|
|
if (!validMessages.length) return 0;
|
|
|
|
const dbCount = countMap.get(conv._id) || 0;
|
|
if (dbCount >= validMessages.length) return 0;
|
|
|
|
const existingIds = new Set(
|
|
(await msgRepo.find({
|
|
where: { conversationId: conv._id },
|
|
select: ['externalId'],
|
|
})).map((m) => m.externalId),
|
|
);
|
|
|
|
const toInsert = validMessages.filter((m) => !existingIds.has(m.externalId));
|
|
if (!toInsert.length) return 0;
|
|
|
|
await msgRepo.save(
|
|
toInsert.map((m) => msgRepo.create({
|
|
conversationId: conv._id,
|
|
externalId: m.externalId,
|
|
text: m.text,
|
|
thinking: m.thinking || null,
|
|
files: [],
|
|
role: m.role as MessageRole,
|
|
createdBy: req.user!._id,
|
|
createdAt: m.timestamp ? new Date(m.timestamp) : new Date(),
|
|
})),
|
|
);
|
|
return toInsert.length;
|
|
}));
|
|
const syncedMessages = msgResults
|
|
.reduce((sum, r) => sum + (r.status === 'fulfilled' ? r.value : 0), 0);
|
|
|
|
return res.json({ syncedAgents, syncedConversations, syncedMessages });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const workspaceMeta: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
return res.json(ocService.getWorkspaceMeta(agent.openclawAgentId));
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const getWorkspaceFile: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
return res.json(ocService.getWorkspaceFile(agent.openclawAgentId, req.params.filename));
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const getSessionSettings: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const convRepo = AppDataSource.getRepository(Conversation);
|
|
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const conv = await convRepo.findOneBy({ _id: Number(req.params.conversationId) });
|
|
if (!conv?.sessionKey) {
|
|
return res.json({ ok: true, settings: {} });
|
|
}
|
|
const settings = ocService.getSessionSettings(agent.openclawAgentId, conv.sessionKey);
|
|
return res.json({ ok: true, settings });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const patchSessionSettings: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const convRepo = AppDataSource.getRepository(Conversation);
|
|
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const conv = await convRepo.findOneBy({ _id: Number(req.params.conversationId) });
|
|
const sessionKey = conv?.sessionKey || String(conv?._id);
|
|
const result = await ocService.patchSessionSettings(agent.openclawAgentId, sessionKey, req.body);
|
|
if (!result.ok) {
|
|
return res.status(500).json(result);
|
|
}
|
|
return res.json(result);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const putWorkspaceFile: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const { content } = req.body as { content: string };
|
|
const result = ocService.putWorkspaceFile(agent.openclawAgentId, req.params.filename, content);
|
|
return res.json(result);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const serveWorkspaceUpload: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const fp = ocService.getWorkspaceUploadPath(agent.openclawAgentId, req.params.filename);
|
|
if (!fp) return res.status(404).json({ error: 'File not found' });
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
return res.sendFile(fp);
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const listModels: RequestHandler = async (_req, res, next) => {
|
|
try {
|
|
const models = ocService.listModels(true);
|
|
return res.json({ ok: true, models });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const getAgentModel: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const model = ocService.getAgentModel(agent.openclawAgentId);
|
|
return res.json({ ok: true, model });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
const patchAgentModel: RequestHandler = async (req, res, next) => {
|
|
try {
|
|
const agentRepo = AppDataSource.getRepository(Agent);
|
|
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
|
if (!agent?.openclawAgentId) {
|
|
return res.status(404).json({ error: 'Agent not found' });
|
|
}
|
|
const { model } = req.body as { model: string };
|
|
if (!model) {
|
|
return res.status(400).json({ error: 'model is required' });
|
|
}
|
|
const ok = ocService.setAgentModel(agent.openclawAgentId, model);
|
|
if (!ok) {
|
|
return res.status(500).json({ error: 'Failed to set model' });
|
|
}
|
|
return res.json({ ok: true, model });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
export {
|
|
list,
|
|
get,
|
|
create,
|
|
update,
|
|
destroy,
|
|
sync,
|
|
workspaceMeta,
|
|
getWorkspaceFile,
|
|
putWorkspaceFile,
|
|
getSessionSettings,
|
|
patchSessionSettings,
|
|
serveWorkspaceUpload,
|
|
listModels,
|
|
getAgentModel,
|
|
patchAgentModel,
|
|
};
|