feat(agent): inject userId into system prompt + fix agent-instance nullable columns

- SystemPromptBuilder: add userId/userEmail to context, expose internal API curl commands for OpenClaw creation
- agent.controller.ts: extract userId from JWT, build system prompt via SystemPromptBuilder so iAgent knows current user
- agent.module.ts: register SystemPromptBuilder as provider
- agent-instance.entity.ts: make serverHost/sshUser nullable (pool mode doesn't set these upfront)
- DB: ALTER TABLE agent_instances DROP NOT NULL on server_host/ssh_user

Now iAgent can create 小龙虾 instances autonomously when user asks in natural language.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-08 03:05:15 -07:00
parent 49e48d7b3e
commit b87cebf465
4 changed files with 37 additions and 18 deletions

View File

@ -52,6 +52,7 @@ import { AgentInstance } from './domain/entities/agent-instance.entity';
import { AgentInstanceRepository } from './infrastructure/repositories/agent-instance.repository';
import { AgentInstanceDeployService } from './infrastructure/services/agent-instance-deploy.service';
import { AgentInstanceController } from './interfaces/rest/controllers/agent-instance.controller';
import { SystemPromptBuilder } from './infrastructure/engines/claude-code-cli/system-prompt-builder';
@Module({
imports: [
@ -99,6 +100,7 @@ import { AgentInstanceController } from './interfaces/rest/controllers/agent-ins
OpenAISttService,
AgentInstanceRepository,
AgentInstanceDeployService,
SystemPromptBuilder,
],
})
export class AgentModule {}

View File

@ -17,14 +17,14 @@ export class AgentInstance {
@Column({ type: 'uuid', name: 'pool_server_id', nullable: true })
poolServerId?: string;
@Column({ type: 'varchar', length: 255, name: 'server_host' })
serverHost!: string;
@Column({ type: 'varchar', length: 255, name: 'server_host', nullable: true })
serverHost?: string;
@Column({ type: 'int', name: 'ssh_port', default: 22 })
sshPort!: number;
@Column({ type: 'varchar', length: 100, name: 'ssh_user' })
sshUser!: string;
@Column({ type: 'varchar', length: 100, name: 'ssh_user', nullable: true })
sshUser?: string;
@Column({ type: 'varchar', length: 150, name: 'container_name', unique: true })
containerName!: string;

View File

@ -5,6 +5,8 @@ import * as path from 'path';
export interface SystemPromptContext {
tenantId: string;
userId?: string;
userEmail?: string;
serverContext?: string;
skills?: string[];
riskBoundary?: string;
@ -33,21 +35,24 @@ export class SystemPromptBuilder {
'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' +
'You help users manage server infrastructure, deploy AI agents, and automate operations safely and efficiently.\n\n' +
'## OpenClaw Agent Deployment\n' +
'You can deploy real OpenClaw AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' +
'To deploy an OpenClaw instance, call the IT0 API:\n' +
' POST /api/v1/agent/instances\n' +
' Body: { "name": "<user-given name>", "userId": "<user id>", "usePool": true }\n' +
'The instance will be deployed on a pool server automatically. After creation, the user can connect\n' +
'their OpenClaw to Telegram, WhatsApp, or other channels via the channel configuration.\n' +
'To list existing instances: GET /api/v1/agent/instances\n' +
'To stop an instance: PUT /api/v1/agent/instances/:id/stop\n' +
'To remove an instance: DELETE /api/v1/agent/instances/:id\n\n' +
'When a user says they want to create an OpenClaw, a personal AI agent, or an autonomous agent,\n' +
'confirm their desired name, then call the deployment API and report the result.',
'You can deploy real OpenClaw (小龙虾) AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' +
'Use the internal API (no auth required, direct service call):\n' +
' Create: curl -s -X POST http://localhost:3002/api/v1/agent/instances -H "Content-Type: application/json" -d \'{"name":"<name>","userId":"<userId>","usePool":true}\'\n' +
' List: curl -s http://localhost:3002/api/v1/agent/instances\n' +
' Stop: curl -s -X PUT http://localhost:3002/api/v1/agent/instances/<id>/stop\n' +
' Delete: curl -s -X DELETE http://localhost:3002/api/v1/agent/instances/<id>\n\n' +
'When a user asks to create a OpenClaw / 小龙虾 / personal AI agent / autonomous agent:\n' +
'1. Ask for a name if not given\n' +
'2. Use the Current User ID from this prompt as userId\n' +
'3. Call the create API with Bash and report the result (id, status, containerName)',
);
// Tenant context
// Tenant + user context
parts.push(`\nTenant: ${context.tenantId}`);
if (context.userId) {
parts.push(`Current User ID: ${context.userId}${context.userEmail ? ` (${context.userEmail})` : ''}`);
parts.push(`When creating OpenClaw instances for the current user, use userId: "${context.userId}"`);
}
// Server context (available servers, environment, etc.)
if (context.serverContext) {

View File

@ -1,4 +1,4 @@
import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile } from '@nestjs/common';
import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile, Req } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { TenantId, EventPatterns } from '@it0/common';
@ -17,6 +17,7 @@ import { TaskStatus } from '../../../domain/value-objects/task-status.vo';
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
import { AgentEnginePort, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
import { ClaudeAgentSdkEngine } from '../../../infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine';
import { SystemPromptBuilder } from '../../../infrastructure/engines/claude-code-cli/system-prompt-builder';
import * as crypto from 'crypto';
@Controller('api/v1/agent')
@ -34,11 +35,13 @@ export class AgentController {
private readonly contextService: ConversationContextService,
private readonly eventPublisher: EventPublisherService,
private readonly sttService: OpenAISttService,
private readonly systemPromptBuilder: SystemPromptBuilder,
) {}
@Post('tasks')
async executeTask(
@TenantId() tenantId: string,
@Req() req: any,
@Body() body: {
prompt: string;
sessionId?: string;
@ -136,10 +139,19 @@ export class AgentController {
this.logger.log(`[Task ${task.id}] Resuming SDK session: ${resumeSessionId}`);
}
// Build system prompt with user context so iAgent knows who it's serving
const userId: string | undefined = req.user?.sub ?? req.user?.userId;
const userEmail: string | undefined = req.user?.email;
const systemPrompt = body.systemPrompt || this.systemPromptBuilder.build({
tenantId,
userId,
userEmail,
});
// Fire-and-forget: run the task stream
this.runTaskStream(engine, session, task, {
prompt: body.prompt,
systemPrompt: body.systemPrompt || '',
systemPrompt,
allowedTools: body.allowedTools || [],
maxTurns: body.maxTurns || 10,
conversationHistory: historyForEngine.length > 0 ? historyForEngine : undefined,