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 { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-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 { TenantAgentConfigService } from './infrastructure/services/tenant-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 { TenantAgentConfig } from './domain/entities/tenant-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 { ConversationMessage } from './domain/entities/conversation-message.entity';
|
||||
import { MessageRepository } from './infrastructure/repositories/message.repository';
|
||||
|
|
@ -47,7 +45,7 @@ import { ConversationContextService } from './domain/services/conversation-conte
|
|||
DatabaseModule.forRoot(),
|
||||
TypeOrmModule.forFeature([
|
||||
AgentSession, AgentTask, CommandRecord, StandingOrderRef,
|
||||
TenantAgentConfig, AgentConfig, AgentSkill, HookScript,
|
||||
TenantAgentConfig, AgentConfig, HookScript,
|
||||
ConversationMessage,
|
||||
]),
|
||||
],
|
||||
|
|
@ -72,7 +70,6 @@ import { ConversationContextService } from './domain/services/conversation-conte
|
|||
MessageRepository,
|
||||
TenantAgentConfigRepository,
|
||||
AgentConfigRepository,
|
||||
AgentSkillRepository,
|
||||
HookScriptRepository,
|
||||
TenantAgentConfigService,
|
||||
AgentConfigService,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
/**
|
||||
* 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 { AgentSkillRepository } from '../repositories/agent-skill.repository';
|
||||
import { AgentSkill } from '../../domain/entities/agent-skill.entity';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
export interface CreateSkillDto {
|
||||
name: string;
|
||||
|
|
@ -23,43 +30,112 @@ export interface UpdateSkillDto {
|
|||
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()
|
||||
export class AgentSkillService {
|
||||
constructor(private readonly repo: AgentSkillRepository) {}
|
||||
private readonly logger = new Logger(AgentSkillService.name);
|
||||
|
||||
async findAllByTenant(tenantId: string): Promise<{ data: AgentSkill[]; total: number }> {
|
||||
const skills = await this.repo.findByTenantId(tenantId);
|
||||
return { data: skills, total: skills.length };
|
||||
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
||||
|
||||
private schema(tenantId: string): string {
|
||||
return `it0_t_${tenantId.replace(/[^a-zA-Z0-9_]/g, '')}`;
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateSkillDto): Promise<AgentSkill> {
|
||||
const skill = new AgentSkill();
|
||||
skill.tenantId = tenantId;
|
||||
skill.name = dto.name;
|
||||
skill.description = dto.description ?? '';
|
||||
skill.category = dto.category ?? 'custom';
|
||||
skill.script = dto.script ?? '';
|
||||
skill.tags = dto.tags ?? [];
|
||||
skill.enabled = dto.enabled ?? true;
|
||||
return this.repo.save(skill);
|
||||
private toRecord(row: any): SkillRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
tenant_id: row.tenant_id,
|
||||
name: row.name,
|
||||
description: row.description ?? '',
|
||||
category: row.category ?? 'custom',
|
||||
script: row.content ?? '',
|
||||
tags: row.tags ?? [],
|
||||
enabled: row.is_active ?? true,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise<AgentSkill | null> {
|
||||
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||
if (!skill) return null;
|
||||
if (dto.name !== undefined) skill.name = dto.name;
|
||||
if (dto.description !== undefined) skill.description = dto.description;
|
||||
if (dto.category !== undefined) skill.category = dto.category;
|
||||
if (dto.script !== undefined) skill.script = dto.script;
|
||||
if (dto.tags !== undefined) skill.tags = dto.tags;
|
||||
if (dto.enabled !== undefined) skill.enabled = dto.enabled;
|
||||
return this.repo.save(skill);
|
||||
async findAllByTenant(tenantId: string): Promise<{ data: SkillRecord[]; total: number }> {
|
||||
const schema = this.schema(tenantId);
|
||||
const rows = await this.dataSource.query(
|
||||
`SELECT * FROM "${schema}".skills ORDER BY created_at DESC`,
|
||||
);
|
||||
const data = rows.map((r: any) => this.toRecord(r));
|
||||
return { data, total: data.length };
|
||||
}
|
||||
|
||||
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> {
|
||||
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||
if (!skill) return false;
|
||||
await this.repo.deleteById(id);
|
||||
return true;
|
||||
const schema = this.schema(tenantId);
|
||||
const result = await this.dataSource.query(
|
||||
`DELETE FROM "${schema}".skills WHERE id = $1 RETURNING id`,
|
||||
[id],
|
||||
);
|
||||
return result.length > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue