feat(ai): anonymous AI assistant on public shares
Lets an unauthenticated viewer of a published share ask an AI scoped strictly to that share's page tree. The authenticated agent is untouched; the security boundary is the tool scope (no identity), and nothing is persisted. Server: - workspace toggle settings.ai.publicShareAssistant (default off) + optional settings.ai.provider.publicShareChatModel (cheap model id; reuses the chat driver/baseUrl/key). getChatModel(workspaceId, override) substitutes only the model id, falling back to chatModel. - POST /api/shares/ai/stream (@Public, SSE). Guardrail funnel, each failing before streaming: toggle off -> 404; share missing/wrong-workspace/sharing off -> 404; pageId not in share tree -> 404; provider unconfigured -> 503; per-IP (5/min) and per-workspace (300/h, IP-independent) rate limits -> 429. Uniform 404s never confirm a private page's existence. - forShare read-only in-process toolset: searchSharePages (existing shareId FTS branch, no spaceId/userId), getSharePage (getShareForPage gate + share.id check, content via the public sanitizer), listSharePages. No write/ comment/history/cross-space/external-MCP tools. - Locked share system prompt + immutable safety block; stepCountIs(5). - /shares/page-info exposes an aiAssistant flag (gated behind isSharingAllowed). Client: an ephemeral, text-only Ask-AI widget on the public shared page, shown only when the flag is set; useChat -> /api/shares/ai/stream, credentials omit. Admin toggle + model field in Settings -> AI. Also adds a jest moduleNameMapper for src/-rooted imports (fixes pre-existing unresolvable specs; additive). Implements docs/public-share-assistant-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
AUDIT_SERVICE,
|
||||
IAuditService,
|
||||
} from '../../integrations/audit/audit.service';
|
||||
import { AiSettingsService } from '../../integrations/ai/ai-settings.service';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('shares')
|
||||
@@ -46,6 +47,7 @@ export class ShareController {
|
||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||
private readonly pageAccessService: PageAccessService,
|
||||
private readonly licenseCheckService: LicenseCheckService,
|
||||
private readonly aiSettings: AiSettingsService,
|
||||
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
|
||||
) {}
|
||||
|
||||
@@ -79,8 +81,15 @@ export class ShareController {
|
||||
throw new NotFoundException('Shared page not found');
|
||||
}
|
||||
|
||||
// Surface whether the anonymous public-share AI assistant is enabled, so the
|
||||
// client only renders the "Ask AI" widget when the workspace allows it.
|
||||
const aiAssistant = await this.aiSettings.isPublicShareAssistantEnabled(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return {
|
||||
...shareData,
|
||||
aiAssistant,
|
||||
features: this.licenseCheckService.resolveFeatures(
|
||||
workspace.licenseKey,
|
||||
workspace.plan,
|
||||
|
||||
Reference in New Issue
Block a user