@xpert-ai/plugin-spaces 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/dist/access.d.ts +38 -0
- package/dist/access.js +96 -0
- package/dist/ai.d.ts +53 -0
- package/dist/ai.js +143 -0
- package/dist/artifacts.d.ts +9 -0
- package/dist/artifacts.js +40 -0
- package/dist/capability.d.ts +22 -0
- package/dist/capability.js +63 -0
- package/dist/catalog.d.ts +196 -0
- package/dist/catalog.js +140 -0
- package/dist/content-backup.d.ts +7 -0
- package/dist/content-backup.js +35 -0
- package/dist/content-coverage.d.ts +9 -0
- package/dist/content-coverage.js +12 -0
- package/dist/content-knowledge.d.ts +40 -0
- package/dist/content-knowledge.js +168 -0
- package/dist/content.d.ts +19 -0
- package/dist/content.js +47 -0
- package/dist/documents.d.ts +38 -0
- package/dist/documents.js +177 -0
- package/dist/domain.d.ts +95 -0
- package/dist/domain.js +23 -0
- package/dist/editors.d.ts +16 -0
- package/dist/editors.js +63 -0
- package/dist/entities.d.ts +91 -0
- package/dist/entities.js +318 -0
- package/dist/files.d.ts +79 -0
- package/dist/files.js +228 -0
- package/dist/imports.d.ts +83 -0
- package/dist/imports.js +139 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +51 -0
- package/dist/item-catalog.d.ts +13 -0
- package/dist/item-catalog.js +71 -0
- package/dist/knowledge.d.ts +57 -0
- package/dist/knowledge.js +172 -0
- package/dist/middleware.d.ts +17 -0
- package/dist/middleware.js +82 -0
- package/dist/pages.d.ts +38 -0
- package/dist/pages.js +41 -0
- package/dist/proposal-id.d.ts +3 -0
- package/dist/proposal-id.js +11 -0
- package/dist/references.d.ts +22 -0
- package/dist/references.js +119 -0
- package/dist/remote/app.css +4 -0
- package/dist/remote/app.js +167 -0
- package/dist/resource-folders.d.ts +7 -0
- package/dist/resource-folders.js +21 -0
- package/dist/resource-provider.d.ts +39 -0
- package/dist/resource-provider.js +160 -0
- package/dist/spaces.d.ts +95 -0
- package/dist/spaces.js +233 -0
- package/dist/subpages.d.ts +24 -0
- package/dist/subpages.js +118 -0
- package/dist/view.d.ts +99 -0
- package/dist/view.js +259 -0
- package/docs/.mintignore +7 -0
- package/docs/AGENTS.md +3 -0
- package/docs/LICENSE +21 -0
- package/docs/README.md +3 -0
- package/docs/architecture.mdx +49 -0
- package/docs/collaboration.mdx +50 -0
- package/docs/docs.json +39 -0
- package/docs/feishu-document-import.mdx +118 -0
- package/docs/index.mdx +12 -0
- package/docs/insert-menu-plan.mdx +74 -0
- package/docs/math-subpages.mdx +62 -0
- package/docs/page-resources-and-connectors.mdx +103 -0
- package/docs/quickstart.mdx +17 -0
- package/migrations/20261023-document-import-origin.sql +4 -0
- package/migrations/20261023-subpages.sql +6 -0
- package/package.json +79 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Repository } from 'typeorm';
|
|
2
|
+
import { SpaceResourcePlacement } from './entities.js';
|
|
3
|
+
import { SpaceAccess } from './access.js';
|
|
4
|
+
import { SpacesService } from './spaces.js';
|
|
5
|
+
import { z } from 'zod/v3';
|
|
6
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
7
|
+
import { type SpaceActor } from './domain.js';
|
|
8
|
+
export declare const catalogQuery: z.ZodObject<{
|
|
9
|
+
parentId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
typeId: z.ZodOptional<z.ZodString>;
|
|
11
|
+
search: z.ZodOptional<z.ZodString>;
|
|
12
|
+
deleted: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
}, "strict", z.ZodTypeAny, {
|
|
15
|
+
offset: number;
|
|
16
|
+
search?: string | undefined;
|
|
17
|
+
deleted?: boolean | undefined;
|
|
18
|
+
parentId?: string | undefined;
|
|
19
|
+
typeId?: string | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
search?: string | undefined;
|
|
22
|
+
deleted?: boolean | undefined;
|
|
23
|
+
parentId?: string | undefined;
|
|
24
|
+
offset?: number | undefined;
|
|
25
|
+
typeId?: string | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export declare const catalogCreate: z.ZodObject<{
|
|
28
|
+
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
29
|
+
providerKey: z.ZodString;
|
|
30
|
+
implementationKey: z.ZodString;
|
|
31
|
+
templateId: z.ZodOptional<z.ZodString>;
|
|
32
|
+
requestId: z.ZodString;
|
|
33
|
+
title: z.ZodString;
|
|
34
|
+
}, "strict", z.ZodTypeAny, {
|
|
35
|
+
title: string;
|
|
36
|
+
providerKey: string;
|
|
37
|
+
implementationKey: string;
|
|
38
|
+
requestId: string;
|
|
39
|
+
parentId?: string | null | undefined;
|
|
40
|
+
templateId?: string | undefined;
|
|
41
|
+
}, {
|
|
42
|
+
title: string;
|
|
43
|
+
providerKey: string;
|
|
44
|
+
implementationKey: string;
|
|
45
|
+
requestId: string;
|
|
46
|
+
parentId?: string | null | undefined;
|
|
47
|
+
templateId?: string | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
export declare const catalogLifecycle: z.ZodObject<{
|
|
50
|
+
contentId: z.ZodString;
|
|
51
|
+
expectedRevision: z.ZodString;
|
|
52
|
+
action: z.ZodEnum<["trash", "restore"]>;
|
|
53
|
+
}, "strict", z.ZodTypeAny, {
|
|
54
|
+
contentId: string;
|
|
55
|
+
expectedRevision: string;
|
|
56
|
+
action: "trash" | "restore";
|
|
57
|
+
}, {
|
|
58
|
+
contentId: string;
|
|
59
|
+
expectedRevision: string;
|
|
60
|
+
action: "trash" | "restore";
|
|
61
|
+
}>;
|
|
62
|
+
export declare const catalogMove: z.ZodObject<{
|
|
63
|
+
contentId: z.ZodString;
|
|
64
|
+
parentId: z.ZodNullable<z.ZodString>;
|
|
65
|
+
expectedVersion: z.ZodNumber;
|
|
66
|
+
}, "strict", z.ZodTypeAny, {
|
|
67
|
+
parentId: string | null;
|
|
68
|
+
contentId: string;
|
|
69
|
+
expectedVersion: number;
|
|
70
|
+
}, {
|
|
71
|
+
parentId: string | null;
|
|
72
|
+
contentId: string;
|
|
73
|
+
expectedVersion: number;
|
|
74
|
+
}>;
|
|
75
|
+
export declare const catalogRename: z.ZodObject<{
|
|
76
|
+
contentId: z.ZodString;
|
|
77
|
+
expectedRevision: z.ZodString;
|
|
78
|
+
title: z.ZodString;
|
|
79
|
+
}, "strict", z.ZodTypeAny, {
|
|
80
|
+
title: string;
|
|
81
|
+
contentId: string;
|
|
82
|
+
expectedRevision: string;
|
|
83
|
+
}, {
|
|
84
|
+
title: string;
|
|
85
|
+
contentId: string;
|
|
86
|
+
expectedRevision: string;
|
|
87
|
+
}>;
|
|
88
|
+
export declare const catalogDuplicate: z.ZodObject<{
|
|
89
|
+
contentId: z.ZodString;
|
|
90
|
+
expectedRevision: z.ZodString;
|
|
91
|
+
title: z.ZodString;
|
|
92
|
+
} & {
|
|
93
|
+
requestId: z.ZodString;
|
|
94
|
+
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
95
|
+
}, "strict", z.ZodTypeAny, {
|
|
96
|
+
title: string;
|
|
97
|
+
contentId: string;
|
|
98
|
+
expectedRevision: string;
|
|
99
|
+
requestId: string;
|
|
100
|
+
parentId?: string | null | undefined;
|
|
101
|
+
}, {
|
|
102
|
+
title: string;
|
|
103
|
+
contentId: string;
|
|
104
|
+
expectedRevision: string;
|
|
105
|
+
requestId: string;
|
|
106
|
+
parentId?: string | null | undefined;
|
|
107
|
+
}>;
|
|
108
|
+
/** Spaces owns discovery UI only. Resource creation, state and permissions belong to providers. */
|
|
109
|
+
export declare class SpaceCatalog {
|
|
110
|
+
private readonly runtime;
|
|
111
|
+
private readonly placements;
|
|
112
|
+
private readonly spaces;
|
|
113
|
+
private readonly access;
|
|
114
|
+
constructor(runtime: RuntimeCapabilityRegistry, placements: Repository<SpaceResourcePlacement>, spaces: SpacesService, access: SpaceAccess);
|
|
115
|
+
private scope;
|
|
116
|
+
private get api();
|
|
117
|
+
backup(actor: SpaceActor, contentId: string): Promise<{
|
|
118
|
+
fileName: string;
|
|
119
|
+
content: string;
|
|
120
|
+
}>;
|
|
121
|
+
preferences(actor: SpaceActor, contentId: string): Promise<import("@xpert-ai/plugin-sdk").ContentEditorPreferenceState[]>;
|
|
122
|
+
setPreference(actor: SpaceActor, input: {
|
|
123
|
+
contentId: string;
|
|
124
|
+
scope: 'resource' | 'project' | 'organization';
|
|
125
|
+
editorKey: string | null;
|
|
126
|
+
expectedRevision: number;
|
|
127
|
+
}): Promise<import("@xpert-ai/plugin-sdk").ContentEditorPreferenceState>;
|
|
128
|
+
types(actor: SpaceActor): Promise<(import("@xpert-ai/contracts").ContentTypeImplementation & {
|
|
129
|
+
providerKey: string;
|
|
130
|
+
})[]>;
|
|
131
|
+
list(actor: SpaceActor, input: z.infer<typeof catalogQuery>): Promise<{
|
|
132
|
+
items: {
|
|
133
|
+
parentId: string | null;
|
|
134
|
+
placementVersion: number;
|
|
135
|
+
compatibleEditors: {
|
|
136
|
+
key: string;
|
|
137
|
+
title: string;
|
|
138
|
+
}[];
|
|
139
|
+
availability?: "available" | "provider-unavailable";
|
|
140
|
+
contentId: string;
|
|
141
|
+
resourceRef: {
|
|
142
|
+
providerKey: string;
|
|
143
|
+
resourceId: string;
|
|
144
|
+
};
|
|
145
|
+
canWrite: boolean;
|
|
146
|
+
bindingRevision: number;
|
|
147
|
+
offlineAccess?: "project";
|
|
148
|
+
sourceSync?: {
|
|
149
|
+
status: "synced" | "pending" | "failed";
|
|
150
|
+
revision?: string;
|
|
151
|
+
};
|
|
152
|
+
preview?: {
|
|
153
|
+
kind: "outline";
|
|
154
|
+
revision: string;
|
|
155
|
+
lines: string[];
|
|
156
|
+
};
|
|
157
|
+
projectId: string;
|
|
158
|
+
title: string;
|
|
159
|
+
revision: string;
|
|
160
|
+
updatedAt: string;
|
|
161
|
+
deleted: boolean;
|
|
162
|
+
source: import("@xpert-ai/contracts").ContentSourceLocation;
|
|
163
|
+
containedPaths?: Array<{
|
|
164
|
+
path: string;
|
|
165
|
+
recursive: boolean;
|
|
166
|
+
}>;
|
|
167
|
+
defaultEditorKey: string;
|
|
168
|
+
allowedActions: import("@xpert-ai/contracts").ContentAction[];
|
|
169
|
+
typeId: string;
|
|
170
|
+
modelId: string;
|
|
171
|
+
schemaVersion: string;
|
|
172
|
+
editProtocol: {
|
|
173
|
+
id: string;
|
|
174
|
+
version: string;
|
|
175
|
+
};
|
|
176
|
+
}[];
|
|
177
|
+
hasMore: boolean;
|
|
178
|
+
}>;
|
|
179
|
+
lifecycle(actor: SpaceActor, input: z.infer<typeof catalogLifecycle>): Promise<import("@xpert-ai/contracts").ContentCatalogItem>;
|
|
180
|
+
rename(actor: SpaceActor, input: z.infer<typeof catalogRename>): Promise<import("@xpert-ai/contracts").ContentCatalogItem>;
|
|
181
|
+
duplicate(actor: SpaceActor, input: z.infer<typeof catalogDuplicate>): Promise<import("@xpert-ai/contracts").ContentCatalogItem>;
|
|
182
|
+
create(actor: SpaceActor, input: z.infer<typeof catalogCreate>): Promise<import("@xpert-ai/contracts").ContentCatalogItem>;
|
|
183
|
+
private place;
|
|
184
|
+
private parent;
|
|
185
|
+
move(actor: SpaceActor, input: z.infer<typeof catalogMove>): Promise<{
|
|
186
|
+
parentId: string | null;
|
|
187
|
+
version: number;
|
|
188
|
+
spaceId: string;
|
|
189
|
+
contentId: string;
|
|
190
|
+
tenantId: string;
|
|
191
|
+
organizationId: string;
|
|
192
|
+
id?: string | undefined;
|
|
193
|
+
createdAt?: Date | undefined;
|
|
194
|
+
updatedAt?: Date | undefined;
|
|
195
|
+
} & SpaceResourcePlacement>;
|
|
196
|
+
}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { contentBackup } from './content-backup.js';
|
|
14
|
+
import { ConflictException, Inject, Injectable } from '@nestjs/common';
|
|
15
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
16
|
+
import { Repository } from 'typeorm';
|
|
17
|
+
import { Space, SpaceResourcePlacement } from './entities.js';
|
|
18
|
+
import { SpaceAccess } from './access.js';
|
|
19
|
+
import { SpacesService } from './spaces.js';
|
|
20
|
+
import { scoped } from './domain.js';
|
|
21
|
+
import { z } from 'zod/v3';
|
|
22
|
+
import { ContentResourcesRuntimeCapability, ContentEditorsRuntimeCapability, supportsContentModel, XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
|
|
23
|
+
import { denied } from './domain.js';
|
|
24
|
+
export const catalogQuery = z.object({ parentId: z.string().uuid().optional(), typeId: z.string().min(1).max(200).optional(), search: z.string().max(200).optional(), deleted: z.boolean().optional(), offset: z.number().int().min(0).default(0) }).strict();
|
|
25
|
+
export const catalogCreate = z.object({ parentId: z.string().uuid().nullable().optional(), providerKey: z.string().min(1).max(200), implementationKey: z.string().min(1).max(200), templateId: z.string().min(1).max(200).optional(), requestId: z.string().uuid(), title: z.string().trim().min(1).max(200) }).strict();
|
|
26
|
+
export const catalogLifecycle = z.object({ contentId: z.string().uuid(), expectedRevision: z.string().min(1), action: z.enum(['trash', 'restore']) }).strict();
|
|
27
|
+
export const catalogMove = z.object({ contentId: z.string().uuid(), parentId: z.string().uuid().nullable(), expectedVersion: z.number().int().nonnegative() }).strict();
|
|
28
|
+
export const catalogRename = z.object({ contentId: z.string().uuid(), expectedRevision: z.string().min(1), title: z.string().trim().min(1).max(200) }).strict();
|
|
29
|
+
export const catalogDuplicate = catalogRename.extend({ requestId: z.string().uuid(), parentId: z.string().uuid().nullable().optional() });
|
|
30
|
+
/** Spaces owns discovery UI only. Resource creation, state and permissions belong to providers. */
|
|
31
|
+
let SpaceCatalog = class SpaceCatalog {
|
|
32
|
+
runtime;
|
|
33
|
+
placements;
|
|
34
|
+
spaces;
|
|
35
|
+
access;
|
|
36
|
+
constructor(runtime, placements, spaces, access) {
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
this.placements = placements;
|
|
39
|
+
this.spaces = spaces;
|
|
40
|
+
this.access = access;
|
|
41
|
+
}
|
|
42
|
+
scope(actor) {
|
|
43
|
+
if (!actor.projectId)
|
|
44
|
+
throw denied();
|
|
45
|
+
return { actor, projectId: actor.projectId, assistantId: actor.assistantId };
|
|
46
|
+
}
|
|
47
|
+
get api() { return this.runtime.require(ContentResourcesRuntimeCapability).catalog; }
|
|
48
|
+
backup(actor, contentId) { return contentBackup(this.runtime, this.scope(actor), contentId); }
|
|
49
|
+
preferences(actor, contentId) { return this.runtime.require(ContentEditorsRuntimeCapability).preferences({ ...this.scope(actor), contentId }); }
|
|
50
|
+
setPreference(actor, input) {
|
|
51
|
+
return this.runtime.require(ContentEditorsRuntimeCapability).setPreference({ ...this.scope(actor), ...input });
|
|
52
|
+
}
|
|
53
|
+
async types(actor) { return (await this.api.types(this.scope(actor))).filter(type => type.creatable !== false); }
|
|
54
|
+
async list(actor, input) {
|
|
55
|
+
const scope = this.scope(actor);
|
|
56
|
+
const space = await this.spaces.workspace(actor);
|
|
57
|
+
if (input.parentId)
|
|
58
|
+
await this.parent(actor, space.id, input.parentId);
|
|
59
|
+
const placements = await this.placements.findBy({ ...scoped(actor), spaceId: space.id });
|
|
60
|
+
const page = await this.api.list({ ...scope, ...input, limit: 50, excludedProviderKeys: ['spaces'],
|
|
61
|
+
...(input.parentId ? { contentIds: placements.filter(p => p.parentId === input.parentId).map(p => p.contentId) } : {}) });
|
|
62
|
+
const editors = await this.runtime.require(ContentEditorsRuntimeCapability).list(scope);
|
|
63
|
+
// Choices are derived from the same compatibility predicate as the platform resolver.
|
|
64
|
+
// Opening still reauthorizes and resolves; this list is not an access grant.
|
|
65
|
+
return { ...page, items: page.items.map(item => ({ ...item,
|
|
66
|
+
parentId: placements.find(p => p.contentId === item.contentId)?.parentId ?? null,
|
|
67
|
+
placementVersion: placements.find(p => p.contentId === item.contentId)?.version ?? 0,
|
|
68
|
+
compatibleEditors: editors.filter(editor => editor.kind === 'resource' && supportsContentModel(editor, item, 'view'))
|
|
69
|
+
.map(editor => ({ key: editor.key, title: editor.title })).sort((a, b) => a.key.localeCompare(b.key))
|
|
70
|
+
})) };
|
|
71
|
+
}
|
|
72
|
+
lifecycle(actor, input) {
|
|
73
|
+
return this.api.mutate({ ...this.scope(actor), contentId: input.contentId, expectedRevision: input.expectedRevision, change: { action: input.action } });
|
|
74
|
+
}
|
|
75
|
+
rename(actor, input) {
|
|
76
|
+
return this.api.mutate({ ...this.scope(actor), contentId: input.contentId, expectedRevision: input.expectedRevision, change: { action: 'rename', title: input.title } });
|
|
77
|
+
}
|
|
78
|
+
async duplicate(actor, input) {
|
|
79
|
+
const space = await this.spaces.workspace(actor);
|
|
80
|
+
await this.parent(actor, space.id, input.parentId ?? null, true);
|
|
81
|
+
const item = await this.api.mutate({ ...this.scope(actor), contentId: input.contentId, expectedRevision: input.expectedRevision,
|
|
82
|
+
change: { action: 'duplicate', requestId: input.requestId, title: input.title } });
|
|
83
|
+
await this.place(actor, space.id, item.contentId, input.parentId ?? null);
|
|
84
|
+
return item;
|
|
85
|
+
}
|
|
86
|
+
async create(actor, input) {
|
|
87
|
+
const space = await this.spaces.workspace(actor);
|
|
88
|
+
await this.parent(actor, space.id, input.parentId ?? null, true);
|
|
89
|
+
const item = await this.api.create({ ...this.scope(actor), ...input });
|
|
90
|
+
await this.place(actor, space.id, item.contentId, input.parentId ?? null);
|
|
91
|
+
return item;
|
|
92
|
+
}
|
|
93
|
+
async place(actor, spaceId, contentId, parentId) {
|
|
94
|
+
// Idempotent creation/copy does not reset a resource that someone has already moved.
|
|
95
|
+
await this.placements.manager.transaction(async (manager) => {
|
|
96
|
+
await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: spaceId }, lock: { mode: 'pessimistic_write' } });
|
|
97
|
+
await this.parent(actor, spaceId, parentId, true);
|
|
98
|
+
await manager.getRepository(SpaceResourcePlacement).createQueryBuilder().insert().values({ ...scoped(actor), spaceId,
|
|
99
|
+
contentId, parentId }).orIgnore().execute();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async parent(actor, spaceId, parentId, write = false) {
|
|
103
|
+
const parent = await this.access.resource(actor, parentId ?? spaceId, write ? 'write' : 'read');
|
|
104
|
+
if (parent.space.id !== spaceId || (parent.item && parent.item.kind !== 'folder'))
|
|
105
|
+
throw denied();
|
|
106
|
+
if (write) {
|
|
107
|
+
let cursor = parent.item;
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
while (cursor) {
|
|
110
|
+
if (cursor.syncStatus === 'pending' || cursor.deleted || seen.has(cursor.id))
|
|
111
|
+
throw new ConflictException('spaces.version_conflict');
|
|
112
|
+
seen.add(cursor.id);
|
|
113
|
+
cursor = cursor.parentId ? await this.access.items.findOneByOrFail({ ...scoped(actor), id: cursor.parentId, spaceId }) : null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async move(actor, input) {
|
|
118
|
+
const space = await this.spaces.workspace(actor);
|
|
119
|
+
const item = await this.api.get({ ...this.scope(actor), contentId: input.contentId });
|
|
120
|
+
if (!item.canWrite)
|
|
121
|
+
throw denied();
|
|
122
|
+
return this.placements.manager.transaction(async (manager) => {
|
|
123
|
+
await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: space.id }, lock: { mode: 'pessimistic_write' } });
|
|
124
|
+
await this.parent(actor, space.id, input.parentId, true);
|
|
125
|
+
const repo = manager.getRepository(SpaceResourcePlacement), where = { ...scoped(actor), spaceId: space.id, contentId: input.contentId };
|
|
126
|
+
const current = await repo.findOneBy(where);
|
|
127
|
+
if ((current?.version ?? 0) !== input.expectedVersion)
|
|
128
|
+
throw new ConflictException('spaces.version_conflict');
|
|
129
|
+
return repo.save({ ...current, ...where, parentId: input.parentId, version: (current?.version ?? 0) + 1 });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
SpaceCatalog = __decorate([
|
|
134
|
+
Injectable(),
|
|
135
|
+
__param(0, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
136
|
+
__param(1, InjectRepository(SpaceResourcePlacement)),
|
|
137
|
+
__metadata("design:paramtypes", [Object, Repository,
|
|
138
|
+
SpacesService, SpaceAccess])
|
|
139
|
+
], SpaceCatalog);
|
|
140
|
+
export { SpaceCatalog };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ContentDocumentScope, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
/** Offline access is an explicit provider delegation. Project membership is checked
|
|
3
|
+
* again by both the catalog and file APIs; an arbitrary cached path is never sufficient. */
|
|
4
|
+
export declare function contentBackup(runtime: RuntimeCapabilityRegistry, scope: ContentDocumentScope, contentId: string): Promise<{
|
|
5
|
+
fileName: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import { ContentResourcesRuntimeCapability, ProjectFilesRuntimeCapability } from '@xpert-ai/plugin-sdk';
|
|
3
|
+
/** Offline access is an explicit provider delegation. Project membership is checked
|
|
4
|
+
* again by both the catalog and file APIs; an arbitrary cached path is never sufficient. */
|
|
5
|
+
export async function contentBackup(runtime, scope, contentId) {
|
|
6
|
+
const item = await runtime.require(ContentResourcesRuntimeCapability).catalog.get({ ...scope, contentId });
|
|
7
|
+
if (item.offlineAccess !== 'project' || item.availability !== 'provider-unavailable')
|
|
8
|
+
throw new BadRequestException('spaces.editor_unavailable');
|
|
9
|
+
const files = runtime.require(ProjectFilesRuntimeCapability);
|
|
10
|
+
let paths;
|
|
11
|
+
if (item.source.kind === 'file')
|
|
12
|
+
paths = [item.source.path];
|
|
13
|
+
else if (item.source.kind === 'bundle') {
|
|
14
|
+
const root = item.source.rootPath;
|
|
15
|
+
paths = (await files.list({ actor: scope.actor, projectId: scope.projectId, path: root, depth: 16 })).filter(file => file.kind === 'file').map(file => file.path);
|
|
16
|
+
if (paths.some(path => !path.startsWith(`${root}/`)))
|
|
17
|
+
throw new BadRequestException('spaces.invalid_path');
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
throw new BadRequestException('spaces.editor_unavailable');
|
|
21
|
+
if (paths.length > 1000)
|
|
22
|
+
throw new BadRequestException('spaces.file_too_large');
|
|
23
|
+
const output = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (const path of paths.sort()) {
|
|
26
|
+
const file = await files.read({ actor: scope.actor, projectId: scope.projectId, path });
|
|
27
|
+
total += file.buffer.length;
|
|
28
|
+
if (total > 20 * 1024 * 1024)
|
|
29
|
+
throw new BadRequestException('spaces.file_too_large');
|
|
30
|
+
output.push({ path, sha256: file.sha256, contentBase64: file.buffer.toString('base64') });
|
|
31
|
+
}
|
|
32
|
+
return { fileName: `${contentId}.source-backup.json`, content: JSON.stringify({ format: 'xpert.content.source-backup', schemaVersion: 1,
|
|
33
|
+
contentId, resourceRef: item.resourceRef, model: { typeId: item.typeId, modelId: item.modelId, schemaVersion: item.schemaVersion },
|
|
34
|
+
recordedRevision: item.revision, source: item.source, files: output }) };
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentCatalogItem } from '@xpert-ai/contracts';
|
|
2
|
+
export type ContentPathCoverage = {
|
|
3
|
+
path: string;
|
|
4
|
+
recursive: boolean;
|
|
5
|
+
};
|
|
6
|
+
/** The native provider declares coverage. Never infer ownership from type names,
|
|
7
|
+
* file extensions or plugin-specific folder conventions in the generic library. */
|
|
8
|
+
export declare function contentCoverage(items: ContentCatalogItem[]): ContentPathCoverage[];
|
|
9
|
+
export declare function coveredByContent(path: string, coverage: ContentPathCoverage[]): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The native provider declares coverage. Never infer ownership from type names,
|
|
2
|
+
* file extensions or plugin-specific folder conventions in the generic library. */
|
|
3
|
+
export function contentCoverage(items) {
|
|
4
|
+
return items.flatMap(item => [
|
|
5
|
+
...(item.containedPaths ?? []),
|
|
6
|
+
...(item.source.kind === 'bundle' ? [{ path: item.source.rootPath, recursive: true }] :
|
|
7
|
+
item.source.kind === 'file' ? [{ path: item.source.path, recursive: false }] : [])
|
|
8
|
+
]);
|
|
9
|
+
}
|
|
10
|
+
export function coveredByContent(path, coverage) {
|
|
11
|
+
return coverage.some(entry => path === entry.path || entry.recursive && path.startsWith(`${entry.path}/`));
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Repository } from 'typeorm';
|
|
2
|
+
import { type ContentCatalogItem } from '@xpert-ai/contracts';
|
|
3
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
4
|
+
import { SpaceContentIndex, Space } from './entities.js';
|
|
5
|
+
import { type SpaceActor } from './domain.js';
|
|
6
|
+
/** Every result rechecks the live ACL, binding and source revision before returning
|
|
7
|
+
* cached text, a title or an anchor. Interrupted projections remain retryable. */
|
|
8
|
+
export declare class SpaceContentKnowledge {
|
|
9
|
+
private readonly rows;
|
|
10
|
+
private readonly runtime;
|
|
11
|
+
constructor(rows: Repository<SpaceContentIndex>, runtime: RuntimeCapabilityRegistry);
|
|
12
|
+
private get catalog();
|
|
13
|
+
private scope;
|
|
14
|
+
private all;
|
|
15
|
+
refresh(actor: SpaceActor, space: Space): Promise<void>;
|
|
16
|
+
refreshItem(actor: SpaceActor, space: Space, item: ContentCatalogItem): Promise<void>;
|
|
17
|
+
status(actor: SpaceActor, space: Space): Promise<{
|
|
18
|
+
itemId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
status: string;
|
|
21
|
+
error: string | null;
|
|
22
|
+
document: null;
|
|
23
|
+
}[]>;
|
|
24
|
+
matches(actor: SpaceActor, space: Space, hits: Array<{
|
|
25
|
+
metadata?: unknown;
|
|
26
|
+
pageContent: string;
|
|
27
|
+
}>): Promise<{
|
|
28
|
+
itemId: string;
|
|
29
|
+
contentId: string;
|
|
30
|
+
title: string;
|
|
31
|
+
text: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
hash: string;
|
|
34
|
+
anchor: {
|
|
35
|
+
kind: string;
|
|
36
|
+
id: string;
|
|
37
|
+
};
|
|
38
|
+
}[]>;
|
|
39
|
+
verify(actor: SpaceActor, contentId: string, hash: string): Promise<boolean>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Injectable, Inject, ForbiddenException, NotFoundException } from '@nestjs/common';
|
|
14
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
15
|
+
import { Repository } from 'typeorm';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { KBDocumentStatusEnum } from '@xpert-ai/contracts';
|
|
18
|
+
import { ContentResourcesRuntimeCapability, KnowledgebaseDocumentsRuntimeCapability, XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
|
|
19
|
+
import { z } from 'zod/v3';
|
|
20
|
+
import { SpaceContentIndex } from './entities.js';
|
|
21
|
+
import { scoped } from './domain.js';
|
|
22
|
+
const documentMetadata = z.object({ documentId: z.string().uuid() }).passthrough();
|
|
23
|
+
/** Every result rechecks the live ACL, binding and source revision before returning
|
|
24
|
+
* cached text, a title or an anchor. Interrupted projections remain retryable. */
|
|
25
|
+
let SpaceContentKnowledge = class SpaceContentKnowledge {
|
|
26
|
+
rows;
|
|
27
|
+
runtime;
|
|
28
|
+
constructor(rows, runtime) {
|
|
29
|
+
this.rows = rows;
|
|
30
|
+
this.runtime = runtime;
|
|
31
|
+
}
|
|
32
|
+
get catalog() { return this.runtime.require(ContentResourcesRuntimeCapability).catalog; }
|
|
33
|
+
scope(actor, space) { return { actor, projectId: space.projectId, assistantId: actor.assistantId }; }
|
|
34
|
+
async all(actor, space, deleted = false) {
|
|
35
|
+
const items = [];
|
|
36
|
+
for (let offset = 0;; offset += 100) {
|
|
37
|
+
const page = await this.catalog.list({ ...this.scope(actor, space), offset, limit: 100, deleted, excludedProviderKeys: ['spaces'] });
|
|
38
|
+
items.push(...page.items);
|
|
39
|
+
if (!page.hasMore)
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async refresh(actor, space) {
|
|
44
|
+
if (!space.knowledgebaseId)
|
|
45
|
+
return;
|
|
46
|
+
for (const item of [...await this.all(actor, space), ...await this.all(actor, space, true)])
|
|
47
|
+
await this.refreshItem(actor, space, item);
|
|
48
|
+
}
|
|
49
|
+
async refreshItem(actor, space, item) {
|
|
50
|
+
if (!space.knowledgebaseId || item.resourceRef.providerKey === 'spaces')
|
|
51
|
+
return;
|
|
52
|
+
const documents = this.runtime.require(KnowledgebaseDocumentsRuntimeCapability);
|
|
53
|
+
const where = { ...scoped(actor), spaceId: space.id, contentId: item.contentId };
|
|
54
|
+
await this.rows.createQueryBuilder().insert().values({ ...where }).orIgnore().execute();
|
|
55
|
+
// Serialize only this projection; the durable pending row survives an interruption.
|
|
56
|
+
await this.rows.manager.transaction(async (manager) => {
|
|
57
|
+
const repo = manager.getRepository(SpaceContentIndex);
|
|
58
|
+
const row = await repo.findOneOrFail({ where, lock: { mode: 'pessimistic_write' } });
|
|
59
|
+
try {
|
|
60
|
+
if (item.deleted) {
|
|
61
|
+
if (row.documents.length)
|
|
62
|
+
await documents.deleteDocuments({ knowledgebaseId: space.knowledgebaseId, documentIds: row.documents.map(doc => doc.id) });
|
|
63
|
+
await repo.update(row.id, { documents: [], revision: item.revision, bindingRevision: item.bindingRevision, status: 'ready' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (row.revision === item.revision && row.bindingRevision === item.bindingRevision && row.documents.length) {
|
|
67
|
+
const status = await documents.getDocumentStatus({ knowledgebaseId: space.knowledgebaseId, documentIds: row.documents.map(doc => doc.id) });
|
|
68
|
+
const failed = status.documents.filter(doc => doc.status === KBDocumentStatusEnum.ERROR).map(doc => doc.id);
|
|
69
|
+
if (failed.length)
|
|
70
|
+
await documents.startProcessing({ knowledgebaseId: space.knowledgebaseId, documentIds: failed });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const projection = await this.catalog.projectText({ ...this.scope(actor, space), contentId: item.contentId, expectedRevision: item.revision });
|
|
74
|
+
if (!projection)
|
|
75
|
+
return;
|
|
76
|
+
const next = [];
|
|
77
|
+
for (const section of projection.sections.filter(section => section.text.trim())) {
|
|
78
|
+
const hash = createHash('sha256').update(JSON.stringify([item.bindingRevision, projection.revision, section])).digest('hex');
|
|
79
|
+
const file = await documents.uploadFile({ knowledgebaseId: space.knowledgebaseId, path: `content/${item.contentId}/${hash}`,
|
|
80
|
+
file: { buffer: Buffer.from(section.text), originalname: `${hash}.txt` } });
|
|
81
|
+
const created = await documents.createDocuments({ knowledgebaseId: space.knowledgebaseId, idempotencyKey: `content:${item.contentId}:${hash}`,
|
|
82
|
+
documents: [{ name: item.title, filePath: file.filePath, fileUrl: file.fileUrl, mimeType: 'text/plain', size: file.size,
|
|
83
|
+
metadata: { contentId: item.contentId, sourceRevision: item.revision, bindingRevision: item.bindingRevision, anchor: section.anchor } }], process: true });
|
|
84
|
+
if (!created.documents[0])
|
|
85
|
+
throw new Error('spaces.index_document_missing');
|
|
86
|
+
next.push({ id: created.documents[0].id, anchor: section.anchor });
|
|
87
|
+
}
|
|
88
|
+
const current = await this.catalog.get({ ...this.scope(actor, space), contentId: item.contentId });
|
|
89
|
+
if (current.deleted || current.revision !== item.revision || current.bindingRevision !== item.bindingRevision) {
|
|
90
|
+
if (next.length)
|
|
91
|
+
await documents.deleteDocuments({ knowledgebaseId: space.knowledgebaseId, documentIds: next.map(doc => doc.id) });
|
|
92
|
+
await repo.update(row.id, { status: 'pending' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await repo.update(row.id, { documents: next, revision: item.revision, bindingRevision: item.bindingRevision, status: next.length ? 'processing' : 'ready' });
|
|
96
|
+
const obsolete = row.documents.filter(doc => !next.some(candidate => candidate.id === doc.id));
|
|
97
|
+
if (obsolete.length)
|
|
98
|
+
await documents.deleteDocuments({ knowledgebaseId: space.knowledgebaseId, documentIds: obsolete.map(doc => doc.id) });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
await repo.update(row.id, { status: 'failed' });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async status(actor, space) {
|
|
106
|
+
const result = [];
|
|
107
|
+
for (const row of await this.rows.findBy({ ...scoped(actor), spaceId: space.id })) {
|
|
108
|
+
let item;
|
|
109
|
+
try {
|
|
110
|
+
item = await this.catalog.get({ ...this.scope(actor, space), contentId: row.contentId });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof ForbiddenException || error instanceof NotFoundException)
|
|
114
|
+
continue;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (item.deleted || item.availability === 'provider-unavailable')
|
|
118
|
+
continue;
|
|
119
|
+
let status = row.revision === item.revision && row.bindingRevision === item.bindingRevision ? row.status : 'pending';
|
|
120
|
+
if (status === 'processing' && space.knowledgebaseId) {
|
|
121
|
+
const processing = await this.runtime.require(KnowledgebaseDocumentsRuntimeCapability).getDocumentStatus({ knowledgebaseId: space.knowledgebaseId, documentIds: row.documents.map(doc => doc.id) });
|
|
122
|
+
status = processing.documents.some(doc => doc.status === KBDocumentStatusEnum.ERROR) ? 'failed'
|
|
123
|
+
: processing.documents.length === row.documents.length && processing.documents.every(doc => doc.status === KBDocumentStatusEnum.FINISH) ? 'ready' : 'processing';
|
|
124
|
+
}
|
|
125
|
+
result.push({ itemId: row.contentId, title: item.title, status, error: status === 'failed' ? 'spaces.index_failed' : null, document: null });
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
async matches(actor, space, hits) {
|
|
130
|
+
const rows = await this.rows.findBy({ ...scoped(actor), spaceId: space.id });
|
|
131
|
+
const result = [];
|
|
132
|
+
for (const hit of hits) {
|
|
133
|
+
const meta = documentMetadata.safeParse(hit.metadata);
|
|
134
|
+
if (!meta.success)
|
|
135
|
+
continue;
|
|
136
|
+
const row = rows.find(row => row.documents.some(doc => doc.id === meta.data.documentId));
|
|
137
|
+
if (!row)
|
|
138
|
+
continue;
|
|
139
|
+
let item;
|
|
140
|
+
try {
|
|
141
|
+
item = await this.catalog.get({ ...this.scope(actor, space), contentId: row.contentId });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof ForbiddenException || error instanceof NotFoundException)
|
|
145
|
+
continue;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (item.deleted || item.availability === 'provider-unavailable' || item.revision !== row.revision || item.bindingRevision !== row.bindingRevision)
|
|
149
|
+
continue;
|
|
150
|
+
result.push({ itemId: item.contentId, contentId: item.contentId, title: item.title, text: hit.pageContent, filePath: '',
|
|
151
|
+
hash: `${item.bindingRevision}:${item.revision}`, anchor: row.documents.find(doc => doc.id === meta.data.documentId).anchor });
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
async verify(actor, contentId, hash) {
|
|
156
|
+
if (!actor.projectId)
|
|
157
|
+
return false;
|
|
158
|
+
const item = await this.catalog.get({ actor, projectId: actor.projectId, assistantId: actor.assistantId, contentId });
|
|
159
|
+
return !item.deleted && `${item.bindingRevision}:${item.revision}` === hash;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
SpaceContentKnowledge = __decorate([
|
|
163
|
+
Injectable(),
|
|
164
|
+
__param(0, InjectRepository(SpaceContentIndex)),
|
|
165
|
+
__param(1, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
166
|
+
__metadata("design:paramtypes", [Repository, Object])
|
|
167
|
+
], SpaceContentKnowledge);
|
|
168
|
+
export { SpaceContentKnowledge };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ContentDocumentProposal } from '@xpert-ai/contracts';
|
|
2
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
3
|
+
import { SpaceAccess } from './access.js';
|
|
4
|
+
import { type SpaceActor, type PagePatch } from './domain.js';
|
|
5
|
+
/** Spaces owns navigation metadata. The platform exclusively owns document state, sessions and review. */
|
|
6
|
+
export declare class SpaceContent {
|
|
7
|
+
private readonly access;
|
|
8
|
+
private readonly runtime;
|
|
9
|
+
constructor(access: SpaceAccess, runtime: RuntimeCapabilityRegistry);
|
|
10
|
+
api(): import("@xpert-ai/plugin-sdk").ContentDocumentsApi;
|
|
11
|
+
scope(actor: SpaceActor, resourceId: string): {
|
|
12
|
+
actor: SpaceActor;
|
|
13
|
+
projectId: string;
|
|
14
|
+
assistantId: string | undefined;
|
|
15
|
+
resourceId: string;
|
|
16
|
+
};
|
|
17
|
+
open(actor: SpaceActor, resourceId: string): Promise<import("@xpert-ai/plugin-sdk").ContentDocumentState>;
|
|
18
|
+
propose(actor: SpaceActor, patch: PagePatch): Promise<ContentDocumentProposal>;
|
|
19
|
+
}
|