fix: route AgentSkillService to per-tenant schema to match SDK engine
Previously AgentSkillService wrote skills to public.agent_skills (TypeORM
entity with tenantId column filter), while ClaudeAgentSdkEngine read from
it0_t_{tenantId}.skills (per-tenant schema). The two tables were never
connected, so any skill added via the CRUD API was invisible to the agent.
This fix:
- Rewrites AgentSkillService to use DataSource + raw SQL against the
per-tenant schema it0_t_{tenantId}.skills
- Maps API fields: script→content, enabled→is_active
- Removes AgentSkillRepository and AgentSkill entity from module (no longer needed)
- CRUD API response shape is unchanged (fields mapped back to script/enabled)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f5d9b1f04f
commit
ce4e7840ec
|
|
@ -23,7 +23,6 @@ import { SessionRepository } from './infrastructure/repositories/session.reposit
|
||||||
import { TaskRepository } from './infrastructure/repositories/task.repository';
|
import { TaskRepository } from './infrastructure/repositories/task.repository';
|
||||||
import { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-agent-config.repository';
|
import { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-agent-config.repository';
|
||||||
import { AgentConfigRepository } from './infrastructure/repositories/agent-config.repository';
|
import { AgentConfigRepository } from './infrastructure/repositories/agent-config.repository';
|
||||||
import { AgentSkillRepository } from './infrastructure/repositories/agent-skill.repository';
|
|
||||||
import { HookScriptRepository } from './infrastructure/repositories/hook-script.repository';
|
import { HookScriptRepository } from './infrastructure/repositories/hook-script.repository';
|
||||||
import { TenantAgentConfigService } from './infrastructure/services/tenant-agent-config.service';
|
import { TenantAgentConfigService } from './infrastructure/services/tenant-agent-config.service';
|
||||||
import { AgentConfigService } from './infrastructure/services/agent-config.service';
|
import { AgentConfigService } from './infrastructure/services/agent-config.service';
|
||||||
|
|
@ -35,7 +34,6 @@ import { CommandRecord } from './domain/entities/command-record.entity';
|
||||||
import { StandingOrderRef } from './domain/entities/standing-order.entity';
|
import { StandingOrderRef } from './domain/entities/standing-order.entity';
|
||||||
import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity';
|
import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity';
|
||||||
import { AgentConfig } from './domain/entities/agent-config.entity';
|
import { AgentConfig } from './domain/entities/agent-config.entity';
|
||||||
import { AgentSkill } from './domain/entities/agent-skill.entity';
|
|
||||||
import { HookScript } from './domain/entities/hook-script.entity';
|
import { HookScript } from './domain/entities/hook-script.entity';
|
||||||
import { ConversationMessage } from './domain/entities/conversation-message.entity';
|
import { ConversationMessage } from './domain/entities/conversation-message.entity';
|
||||||
import { MessageRepository } from './infrastructure/repositories/message.repository';
|
import { MessageRepository } from './infrastructure/repositories/message.repository';
|
||||||
|
|
@ -47,7 +45,7 @@ import { ConversationContextService } from './domain/services/conversation-conte
|
||||||
DatabaseModule.forRoot(),
|
DatabaseModule.forRoot(),
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
AgentSession, AgentTask, CommandRecord, StandingOrderRef,
|
AgentSession, AgentTask, CommandRecord, StandingOrderRef,
|
||||||
TenantAgentConfig, AgentConfig, AgentSkill, HookScript,
|
TenantAgentConfig, AgentConfig, HookScript,
|
||||||
ConversationMessage,
|
ConversationMessage,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
@ -72,7 +70,6 @@ import { ConversationContextService } from './domain/services/conversation-conte
|
||||||
MessageRepository,
|
MessageRepository,
|
||||||
TenantAgentConfigRepository,
|
TenantAgentConfigRepository,
|
||||||
AgentConfigRepository,
|
AgentConfigRepository,
|
||||||
AgentSkillRepository,
|
|
||||||
HookScriptRepository,
|
HookScriptRepository,
|
||||||
TenantAgentConfigService,
|
TenantAgentConfigService,
|
||||||
AgentConfigService,
|
AgentConfigService,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* Service for managing per-tenant agent skills (CRUD).
|
* Service for managing per-tenant agent skills (CRUD).
|
||||||
|
*
|
||||||
|
* Skills are stored in the per-tenant schema table `it0_t_{tenantId}.skills`
|
||||||
|
* so the Claude Agent SDK engine can read them directly during task execution.
|
||||||
|
*
|
||||||
|
* Field mapping between API and DB:
|
||||||
|
* API `script` → DB `content`
|
||||||
|
* API `enabled` → DB `is_active`
|
||||||
*/
|
*/
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { AgentSkillRepository } from '../repositories/agent-skill.repository';
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
import { AgentSkill } from '../../domain/entities/agent-skill.entity';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
export interface CreateSkillDto {
|
export interface CreateSkillDto {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -23,43 +30,112 @@ export interface UpdateSkillDto {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkillRecord {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
script: string; // mapped from DB `content`
|
||||||
|
tags: string[];
|
||||||
|
enabled: boolean; // mapped from DB `is_active`
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AgentSkillService {
|
export class AgentSkillService {
|
||||||
constructor(private readonly repo: AgentSkillRepository) {}
|
private readonly logger = new Logger(AgentSkillService.name);
|
||||||
|
|
||||||
async findAllByTenant(tenantId: string): Promise<{ data: AgentSkill[]; total: number }> {
|
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
||||||
const skills = await this.repo.findByTenantId(tenantId);
|
|
||||||
return { data: skills, total: skills.length };
|
private schema(tenantId: string): string {
|
||||||
|
return `it0_t_${tenantId.replace(/[^a-zA-Z0-9_]/g, '')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(tenantId: string, dto: CreateSkillDto): Promise<AgentSkill> {
|
private toRecord(row: any): SkillRecord {
|
||||||
const skill = new AgentSkill();
|
return {
|
||||||
skill.tenantId = tenantId;
|
id: row.id,
|
||||||
skill.name = dto.name;
|
tenant_id: row.tenant_id,
|
||||||
skill.description = dto.description ?? '';
|
name: row.name,
|
||||||
skill.category = dto.category ?? 'custom';
|
description: row.description ?? '',
|
||||||
skill.script = dto.script ?? '';
|
category: row.category ?? 'custom',
|
||||||
skill.tags = dto.tags ?? [];
|
script: row.content ?? '',
|
||||||
skill.enabled = dto.enabled ?? true;
|
tags: row.tags ?? [],
|
||||||
return this.repo.save(skill);
|
enabled: row.is_active ?? true,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise<AgentSkill | null> {
|
async findAllByTenant(tenantId: string): Promise<{ data: SkillRecord[]; total: number }> {
|
||||||
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
const schema = this.schema(tenantId);
|
||||||
if (!skill) return null;
|
const rows = await this.dataSource.query(
|
||||||
if (dto.name !== undefined) skill.name = dto.name;
|
`SELECT * FROM "${schema}".skills ORDER BY created_at DESC`,
|
||||||
if (dto.description !== undefined) skill.description = dto.description;
|
);
|
||||||
if (dto.category !== undefined) skill.category = dto.category;
|
const data = rows.map((r: any) => this.toRecord(r));
|
||||||
if (dto.script !== undefined) skill.script = dto.script;
|
return { data, total: data.length };
|
||||||
if (dto.tags !== undefined) skill.tags = dto.tags;
|
}
|
||||||
if (dto.enabled !== undefined) skill.enabled = dto.enabled;
|
|
||||||
return this.repo.save(skill);
|
async create(tenantId: string, dto: CreateSkillDto): Promise<SkillRecord> {
|
||||||
|
const schema = this.schema(tenantId);
|
||||||
|
const rows = await this.dataSource.query(
|
||||||
|
`INSERT INTO "${schema}".skills
|
||||||
|
(tenant_id, name, description, category, content, tags, is_active)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
tenantId,
|
||||||
|
dto.name,
|
||||||
|
dto.description ?? '',
|
||||||
|
dto.category ?? 'custom',
|
||||||
|
dto.script ?? '',
|
||||||
|
dto.tags ?? [],
|
||||||
|
dto.enabled ?? true,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return this.toRecord(rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise<SkillRecord | null> {
|
||||||
|
const schema = this.schema(tenantId);
|
||||||
|
const existing = await this.dataSource.query(
|
||||||
|
`SELECT * FROM "${schema}".skills WHERE id = $1`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
if (!existing.length) return null;
|
||||||
|
|
||||||
|
const row = existing[0];
|
||||||
|
const rows = await this.dataSource.query(
|
||||||
|
`UPDATE "${schema}".skills SET
|
||||||
|
name = $1,
|
||||||
|
description = $2,
|
||||||
|
category = $3,
|
||||||
|
content = $4,
|
||||||
|
tags = $5,
|
||||||
|
is_active = $6,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $7
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
dto.name ?? row.name,
|
||||||
|
dto.description ?? row.description,
|
||||||
|
dto.category ?? row.category,
|
||||||
|
dto.script ?? row.content,
|
||||||
|
dto.tags ?? row.tags,
|
||||||
|
dto.enabled ?? row.is_active,
|
||||||
|
id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return this.toRecord(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, tenantId: string): Promise<boolean> {
|
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||||
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
const schema = this.schema(tenantId);
|
||||||
if (!skill) return false;
|
const result = await this.dataSource.query(
|
||||||
await this.repo.deleteById(id);
|
`DELETE FROM "${schema}".skills WHERE id = $1 RETURNING id`,
|
||||||
return true;
|
[id],
|
||||||
|
);
|
||||||
|
return result.length > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue