@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,83 @@
|
|
|
1
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
import { SpaceAccess } from './access.js';
|
|
4
|
+
import { SpaceDocuments } from './documents.js';
|
|
5
|
+
import { SpaceArtifacts } from './artifacts.js';
|
|
6
|
+
import { type SpaceActor } from './domain.js';
|
|
7
|
+
export declare const sourceSelection: z.ZodObject<{
|
|
8
|
+
bindingId: z.ZodString;
|
|
9
|
+
locator: z.ZodString;
|
|
10
|
+
}, "strict", z.ZodTypeAny, {
|
|
11
|
+
bindingId: string;
|
|
12
|
+
locator: string;
|
|
13
|
+
}, {
|
|
14
|
+
bindingId: string;
|
|
15
|
+
locator: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const importSelection: z.ZodObject<{
|
|
18
|
+
bindingId: z.ZodString;
|
|
19
|
+
locator: z.ZodString;
|
|
20
|
+
} & {
|
|
21
|
+
spaceId: z.ZodString;
|
|
22
|
+
parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
23
|
+
requestId: z.ZodString;
|
|
24
|
+
expectedRevision: z.ZodString;
|
|
25
|
+
expectedHash: z.ZodString;
|
|
26
|
+
confirmed: z.ZodLiteral<true>;
|
|
27
|
+
}, "strict", z.ZodTypeAny, {
|
|
28
|
+
spaceId: string;
|
|
29
|
+
parentId: string | null;
|
|
30
|
+
expectedHash: string;
|
|
31
|
+
expectedRevision: string;
|
|
32
|
+
requestId: string;
|
|
33
|
+
bindingId: string;
|
|
34
|
+
locator: string;
|
|
35
|
+
confirmed: true;
|
|
36
|
+
}, {
|
|
37
|
+
spaceId: string;
|
|
38
|
+
expectedHash: string;
|
|
39
|
+
expectedRevision: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
bindingId: string;
|
|
42
|
+
locator: string;
|
|
43
|
+
confirmed: true;
|
|
44
|
+
parentId?: string | null | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
/** Encode remote plain text as literal Markdown paragraphs, never executable HTML. Encode colons to prevent autolinks from consuming Markdown escapes. */
|
|
47
|
+
export declare const literalMarkdown: (text: string) => string;
|
|
48
|
+
export declare class SpaceImports {
|
|
49
|
+
private readonly access;
|
|
50
|
+
private readonly documents;
|
|
51
|
+
private readonly artifacts;
|
|
52
|
+
private readonly runtime;
|
|
53
|
+
constructor(access: SpaceAccess, documents: SpaceDocuments, artifacts: SpaceArtifacts, runtime: RuntimeCapabilityRegistry);
|
|
54
|
+
private context;
|
|
55
|
+
private api;
|
|
56
|
+
sources(actor: SpaceActor): Promise<{
|
|
57
|
+
items: import("@xpert-ai/plugin-sdk").ConnectorDocumentConnection[];
|
|
58
|
+
}>;
|
|
59
|
+
search(actor: SpaceActor, input: {
|
|
60
|
+
bindingId: string;
|
|
61
|
+
query: string;
|
|
62
|
+
cursor?: string;
|
|
63
|
+
pageSize: number;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
items: import("@xpert-ai/plugin-sdk").ConnectorDocumentEntry[];
|
|
66
|
+
nextCursor: string | null;
|
|
67
|
+
}>;
|
|
68
|
+
preview(actor: SpaceActor, input: z.infer<typeof sourceSelection>): Promise<{
|
|
69
|
+
provider: string;
|
|
70
|
+
revision: string;
|
|
71
|
+
contentHash: string;
|
|
72
|
+
format: "text/plain";
|
|
73
|
+
content: string;
|
|
74
|
+
warnings: string[];
|
|
75
|
+
id: string;
|
|
76
|
+
title: string;
|
|
77
|
+
url: string | null;
|
|
78
|
+
type: "document";
|
|
79
|
+
}>;
|
|
80
|
+
apply(actor: SpaceActor, input: z.infer<typeof importSelection>): Promise<{
|
|
81
|
+
resourceId: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
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 { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common';
|
|
14
|
+
import { ConnectorDocumentsRuntimeCapability, XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
|
|
15
|
+
import { z } from 'zod/v3';
|
|
16
|
+
import { SpaceAccess } from './access.js';
|
|
17
|
+
import { SpaceDocuments, contentHash, safeName } from './documents.js';
|
|
18
|
+
import { SpaceArtifacts } from './artifacts.js';
|
|
19
|
+
import { Space } from './entities.js';
|
|
20
|
+
import { denied, id, scoped } from './domain.js';
|
|
21
|
+
export const sourceSelection = z.object({ bindingId: id, locator: z.string().trim().min(1).max(2000) }).strict();
|
|
22
|
+
export const importSelection = sourceSelection.extend({ spaceId: id, parentId: id.nullable().default(null), requestId: id,
|
|
23
|
+
expectedRevision: z.string().min(1).max(200), expectedHash: z.string().regex(/^[a-f0-9]{64}$/u), confirmed: z.literal(true) }).strict();
|
|
24
|
+
const missing = (error) => error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
|
25
|
+
/** Encode remote plain text as literal Markdown paragraphs, never executable HTML. Encode colons to prevent autolinks from consuming Markdown escapes. */
|
|
26
|
+
export const literalMarkdown = (text) => text.replace(/\r\n?/gu, '\n').replace(/\u0000/gu, '')
|
|
27
|
+
.replace(/[&:\\`*_{}\[\]()#+.!<>|$~-]/gu, (char) => char === '&' || char === ':' ? `&#${char.codePointAt(0)};` : `\\${char}`).split('\n').join('\n\n');
|
|
28
|
+
let SpaceImports = class SpaceImports {
|
|
29
|
+
access;
|
|
30
|
+
documents;
|
|
31
|
+
artifacts;
|
|
32
|
+
runtime;
|
|
33
|
+
constructor(access, documents, artifacts, runtime) {
|
|
34
|
+
this.access = access;
|
|
35
|
+
this.documents = documents;
|
|
36
|
+
this.artifacts = artifacts;
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
}
|
|
39
|
+
context(actor) {
|
|
40
|
+
if (!actor.assistantId || !actor.projectId)
|
|
41
|
+
throw denied();
|
|
42
|
+
return { actor, assistantId: actor.assistantId, projectId: actor.projectId };
|
|
43
|
+
}
|
|
44
|
+
api() { return this.runtime.require(ConnectorDocumentsRuntimeCapability); }
|
|
45
|
+
async sources(actor) {
|
|
46
|
+
await this.access.project(actor, actor.projectId, 'write');
|
|
47
|
+
return this.api().sources(this.context(actor));
|
|
48
|
+
}
|
|
49
|
+
async search(actor, input) {
|
|
50
|
+
await this.access.project(actor, actor.projectId, 'write');
|
|
51
|
+
return this.api().search({ ...this.context(actor), ...input });
|
|
52
|
+
}
|
|
53
|
+
async preview(actor, input) {
|
|
54
|
+
const sources = await this.sources(actor), source = sources.items.find((entry) => entry.bindingId === input.bindingId && entry.available);
|
|
55
|
+
if (!source)
|
|
56
|
+
throw denied();
|
|
57
|
+
const snapshot = await this.api().read({ ...this.context(actor), ...input });
|
|
58
|
+
if (snapshot.format !== 'text/plain')
|
|
59
|
+
throw new BadRequestException('spaces.import_format_unsupported');
|
|
60
|
+
return { ...snapshot, provider: source.provider };
|
|
61
|
+
}
|
|
62
|
+
async apply(actor, input) {
|
|
63
|
+
if (!input.confirmed)
|
|
64
|
+
throw new BadRequestException('spaces.import_confirmation_required');
|
|
65
|
+
const context = this.context(actor);
|
|
66
|
+
const target = async () => {
|
|
67
|
+
const result = await this.access.resource(actor, input.parentId ?? input.spaceId, 'write');
|
|
68
|
+
if (result.space.id !== input.spaceId || result.space.projectId !== context.projectId || (result.item && result.item.kind !== 'folder'))
|
|
69
|
+
throw denied();
|
|
70
|
+
return result;
|
|
71
|
+
};
|
|
72
|
+
const { space } = await target();
|
|
73
|
+
// A stable request binds all preview/target fields, but stores no credentials or raw pasted URL.
|
|
74
|
+
const requestHash = contentHash(Buffer.from(JSON.stringify({ ...input, confirmed: undefined, requestId: undefined })));
|
|
75
|
+
const existing = await this.access.items.findOneBy({ id: input.requestId });
|
|
76
|
+
const check = (item) => {
|
|
77
|
+
if (item.tenantId !== actor.tenantId || item.organizationId !== actor.organizationId || item.spaceId !== input.spaceId ||
|
|
78
|
+
item.createdById !== actor.userId || item.deleted || item.importOrigin?.requestHash !== requestHash)
|
|
79
|
+
throw new ConflictException('spaces.import_request_conflict');
|
|
80
|
+
};
|
|
81
|
+
if (existing) {
|
|
82
|
+
check(existing);
|
|
83
|
+
// A completed copy is a project resource: remote revocation never deletes it or prevents retry receipts.
|
|
84
|
+
await this.access.resource(actor, existing.id, 'write');
|
|
85
|
+
if (existing.editorKey === 'markdown.tiptap')
|
|
86
|
+
return { resourceId: existing.id };
|
|
87
|
+
}
|
|
88
|
+
const snapshot = await this.preview(actor, input);
|
|
89
|
+
if (snapshot.revision !== input.expectedRevision || snapshot.contentHash !== input.expectedHash)
|
|
90
|
+
throw new ConflictException('spaces.import_source_changed');
|
|
91
|
+
const text = `# ${literalMarkdown(snapshot.title)}\n\n${literalMarkdown(snapshot.content)}\n`;
|
|
92
|
+
await this.documents.content().validateImport({ ...context, modelKey: 'markdown.tiptap', markdown: text });
|
|
93
|
+
const buffer = Buffer.from(text), hash = contentHash(buffer);
|
|
94
|
+
// Serialize with discovery/moves. Persist the intent outside the lock transaction so an interrupted
|
|
95
|
+
// file/register step resumes the same ID and CAS-protected file rather than creating another copy.
|
|
96
|
+
return this.access.items.manager.transaction(async (manager) => {
|
|
97
|
+
await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: space.id }, lock: { mode: 'pessimistic_write' } });
|
|
98
|
+
const destination = await target();
|
|
99
|
+
let item = await this.access.items.findOneBy({ id: input.requestId });
|
|
100
|
+
if (item) {
|
|
101
|
+
check(item);
|
|
102
|
+
if (item.editorKey === 'markdown.tiptap')
|
|
103
|
+
return { resourceId: item.id };
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const prefix = destination.item?.filePath ? `${destination.item.filePath}/` : '';
|
|
107
|
+
item = await this.access.items.save(this.access.items.create({ ...scoped(actor), id: input.requestId, spaceId: space.id,
|
|
108
|
+
parentId: input.parentId, createdById: actor.userId, kind: 'page', title: snapshot.title,
|
|
109
|
+
filePath: `${prefix}${safeName(snapshot.title).slice(0, 100)}--${input.requestId}.md`, syncStatus: 'pending',
|
|
110
|
+
importOrigin: { provider: snapshot.provider, documentId: snapshot.id, url: snapshot.url, revision: snapshot.revision,
|
|
111
|
+
contentHash: snapshot.contentHash, format: snapshot.format, importedAt: new Date().toISOString(),
|
|
112
|
+
warnings: snapshot.warnings, bindingId: input.bindingId, requestHash } }));
|
|
113
|
+
}
|
|
114
|
+
const fileScope = { actor, projectId: context.projectId, path: item.filePath };
|
|
115
|
+
const disk = await this.documents.api().read(fileScope).catch((error) => { if (missing(error))
|
|
116
|
+
return null; throw error; });
|
|
117
|
+
if (disk && disk.sha256 !== (item.fileHash ?? hash))
|
|
118
|
+
throw new ConflictException('spaces.file_conflict');
|
|
119
|
+
if (!disk) {
|
|
120
|
+
if (item.fileHash)
|
|
121
|
+
throw new ConflictException('spaces.file_conflict');
|
|
122
|
+
await this.documents.api().write({ ...fileScope, buffer, expectedHash: null });
|
|
123
|
+
}
|
|
124
|
+
await this.access.items.update(item.id, { fileHash: disk?.sha256 ?? hash, syncStatus: 'ready' });
|
|
125
|
+
await this.documents.content().register({ ...context, resourceId: item.id, path: item.filePath, title: item.title,
|
|
126
|
+
modelKey: 'markdown.tiptap', source: { providerKey: 'spaces', resourceId: item.id } });
|
|
127
|
+
if (!item.artifactId)
|
|
128
|
+
await this.artifacts.initializePage(actor, item.id, item.title);
|
|
129
|
+
await this.access.items.update(item.id, { editorKey: 'markdown.tiptap' });
|
|
130
|
+
return { resourceId: item.id };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
SpaceImports = __decorate([
|
|
135
|
+
Injectable(),
|
|
136
|
+
__param(3, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
137
|
+
__metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceArtifacts, Object])
|
|
138
|
+
], SpaceImports);
|
|
139
|
+
export { SpaceImports };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import { SpaceItemCatalog } from './item-catalog.js';
|
|
8
|
+
import { SpaceCatalog } from './catalog.js';
|
|
9
|
+
import { SpaceContentKnowledge } from './content-knowledge.js';
|
|
10
|
+
import { SpaceImports } from './imports.js';
|
|
11
|
+
import 'reflect-metadata';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
15
|
+
import { XpertServerPlugin } from '@xpert-ai/plugin-sdk';
|
|
16
|
+
import { entities } from './entities.js';
|
|
17
|
+
import { SpaceAccess } from './access.js';
|
|
18
|
+
import { SpacesResourceProvider } from './resource-provider.js';
|
|
19
|
+
import { SpacesService } from './spaces.js';
|
|
20
|
+
import { SpacePages } from './pages.js';
|
|
21
|
+
import { SpaceAi } from './ai.js';
|
|
22
|
+
import { SpacesView } from './view.js';
|
|
23
|
+
import { SpaceEditors } from './editors.js';
|
|
24
|
+
import { SpacesMiddleware } from './middleware.js';
|
|
25
|
+
import { SpaceArtifacts } from './artifacts.js';
|
|
26
|
+
import { SpaceFiles } from './files.js';
|
|
27
|
+
import { SpaceDocuments } from './documents.js';
|
|
28
|
+
import { SpaceKnowledge } from './knowledge.js';
|
|
29
|
+
import { SpaceContent } from './content.js';
|
|
30
|
+
import { SpaceReferences } from './references.js';
|
|
31
|
+
import { SpaceSubpages } from './subpages.js';
|
|
32
|
+
import { SpacesCapabilityProvider } from './capability.js';
|
|
33
|
+
let SpacesPlugin = class SpacesPlugin {
|
|
34
|
+
};
|
|
35
|
+
SpacesPlugin = __decorate([
|
|
36
|
+
XpertServerPlugin({
|
|
37
|
+
imports: [TypeOrmModule.forFeature(entities)], entities,
|
|
38
|
+
providers: [SpaceItemCatalog, SpaceContentKnowledge, SpaceCatalog, SpacesCapabilityProvider, SpaceImports, SpaceReferences, SpaceSubpages, SpaceContent, SpaceAccess, SpaceKnowledge, SpaceDocuments, SpacesResourceProvider, SpacesService, SpacePages, SpaceAi, SpaceFiles, SpaceEditors, SpaceArtifacts, SpacesMiddleware, SpacesView]
|
|
39
|
+
})
|
|
40
|
+
], SpacesPlugin);
|
|
41
|
+
export { SpacesPlugin };
|
|
42
|
+
const plugin = {
|
|
43
|
+
meta: {
|
|
44
|
+
name: manifest.name, version: manifest.version, author: 'XpertAI', level: manifest.xpert.plugin.level, artifactNamespace: manifest.xpert.plugin.artifactNamespace,
|
|
45
|
+
displayName: 'Spaces', description: 'Assistant collaboration capability for shared AI-maintained documents and artifacts',
|
|
46
|
+
category: 'middleware', targetApps: ['xpert'],
|
|
47
|
+
targetAppMeta: { xpert: { types: ['business-app', 'workbench-view', 'assistant-tool'], capabilities: ['spaces'], runtime: { middlewareProviders: ['spaces'], viewProviders: ['spaces.views'] } } }
|
|
48
|
+
},
|
|
49
|
+
register() { return { module: SpacesPlugin, global: true }; }
|
|
50
|
+
};
|
|
51
|
+
export default plugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContentCatalogProvider } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
import { SpaceAccess } from './access.js';
|
|
3
|
+
import { SpaceContentKnowledge } from './content-knowledge.js';
|
|
4
|
+
/** Exposes native SpaceItems to the platform content catalog without copying content.
|
|
5
|
+
* Pages/files retain their identities, file paths and editing protocols. Creation
|
|
6
|
+
* uses the page/upload workflows; discovery rebuilds only derived catalog entries. */
|
|
7
|
+
export declare class SpaceItemCatalog {
|
|
8
|
+
private readonly access;
|
|
9
|
+
private readonly knowledge;
|
|
10
|
+
constructor(access: SpaceAccess, knowledge: SpaceContentKnowledge);
|
|
11
|
+
readonly catalog: ContentCatalogProvider;
|
|
12
|
+
private describe;
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
import { Injectable } from '@nestjs/common';
|
|
11
|
+
import { In } from 'typeorm';
|
|
12
|
+
import { SpaceAccess } from './access.js';
|
|
13
|
+
import { scoped, denied } from './domain.js';
|
|
14
|
+
import { SpaceContentKnowledge } from './content-knowledge.js';
|
|
15
|
+
const models = {
|
|
16
|
+
page: { typeId: 'xpert.markdown', modelId: 'markdown.tiptap', schemaVersion: '1', editProtocol: { id: 'platform.text-document', version: '1' } },
|
|
17
|
+
file: { typeId: 'xpert.file', modelId: 'platform.file', schemaVersion: '1', editProtocol: { id: 'platform.file.cas', version: '1' } }
|
|
18
|
+
};
|
|
19
|
+
/** Exposes native SpaceItems to the platform content catalog without copying content.
|
|
20
|
+
* Pages/files retain their identities, file paths and editing protocols. Creation
|
|
21
|
+
* uses the page/upload workflows; discovery rebuilds only derived catalog entries. */
|
|
22
|
+
let SpaceItemCatalog = class SpaceItemCatalog {
|
|
23
|
+
access;
|
|
24
|
+
knowledge;
|
|
25
|
+
constructor(access, knowledge) {
|
|
26
|
+
this.access = access;
|
|
27
|
+
this.knowledge = knowledge;
|
|
28
|
+
}
|
|
29
|
+
catalog = {
|
|
30
|
+
types: [
|
|
31
|
+
{ ...models.page, creatable: false, title: { en_US: 'Pages', zh_Hans: '页面' }, implementationKey: 'spaces.page', implementationTitle: { en_US: 'Markdown', zh_Hans: 'Markdown' }, defaultEditorKey: 'markdown.tiptap' },
|
|
32
|
+
{ ...models.file, creatable: false, title: { en_US: 'Files', zh_Hans: '文件' }, implementationKey: 'spaces.file', implementationTitle: { en_US: 'File', zh_Hans: '文件' }, defaultEditorKey: 'platform.file' }
|
|
33
|
+
],
|
|
34
|
+
discover: async (input) => {
|
|
35
|
+
const space = await this.access.spaces.findOneBy({ ...scoped(input.actor), projectId: input.projectId, deleted: false });
|
|
36
|
+
if (!space)
|
|
37
|
+
return { resourceIds: [] };
|
|
38
|
+
await this.access.project({ ...input.actor, assistantId: input.assistantId }, input.projectId);
|
|
39
|
+
const offset = Number(input.cursor ?? 0);
|
|
40
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
41
|
+
throw denied();
|
|
42
|
+
const items = await this.access.items.find({ where: { ...scoped(input.actor), spaceId: space.id, kind: In(['page', 'file', 'artifact']), deleted: false }, order: { id: 'ASC' }, skip: offset, take: 100 });
|
|
43
|
+
return { resourceIds: items.filter(item => item.filePath).map(item => item.id), ...(items.length === 100 ? { nextCursor: String(offset + 100) } : {}) };
|
|
44
|
+
},
|
|
45
|
+
describe: input => this.describe(input),
|
|
46
|
+
onResourceReconciled: async (input) => {
|
|
47
|
+
if (input.item.resourceRef.providerKey === 'spaces')
|
|
48
|
+
return;
|
|
49
|
+
const space = await this.access.spaces.findOneBy({ ...scoped(input.actor), projectId: input.projectId, deleted: false });
|
|
50
|
+
if (!space?.knowledgebaseId)
|
|
51
|
+
return;
|
|
52
|
+
const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
|
|
53
|
+
await this.access.project(actor, input.projectId, 'write');
|
|
54
|
+
await this.knowledge.refreshItem(actor, space, input.item);
|
|
55
|
+
},
|
|
56
|
+
create: async () => { throw denied(); }
|
|
57
|
+
};
|
|
58
|
+
async describe(input) {
|
|
59
|
+
const { item, space } = await this.access.resource({ ...input.actor, projectId: input.projectId, assistantId: input.assistantId }, input.sourceResourceId, 'read');
|
|
60
|
+
if (!item?.filePath || item.kind === 'folder')
|
|
61
|
+
throw denied();
|
|
62
|
+
return { ...(item.kind === 'page' ? models.page : models.file), projectId: space.projectId, title: item.title,
|
|
63
|
+
revision: `${item.version}:${item.projectedSequence}:${item.fileHash ?? ''}`, updatedAt: item.updatedAt.toISOString(),
|
|
64
|
+
deleted: item.deleted, source: { kind: 'file', path: item.filePath }, defaultEditorKey: item.editorKey ?? 'platform.file', allowedActions: [] };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
SpaceItemCatalog = __decorate([
|
|
68
|
+
Injectable(),
|
|
69
|
+
__metadata("design:paramtypes", [SpaceAccess, SpaceContentKnowledge])
|
|
70
|
+
], SpaceItemCatalog);
|
|
71
|
+
export { SpaceItemCatalog };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
import { SpaceContentKnowledge } from './content-knowledge.js';
|
|
3
|
+
import { SpaceAccess } from './access.js';
|
|
4
|
+
import { type SpaceActor } from './domain.js';
|
|
5
|
+
import { Space, SpaceItem } from './entities.js';
|
|
6
|
+
export declare class SpaceKnowledge {
|
|
7
|
+
private readonly access;
|
|
8
|
+
private readonly runtime;
|
|
9
|
+
private readonly content;
|
|
10
|
+
constructor(access: SpaceAccess, runtime: RuntimeCapabilityRegistry, content: SpaceContentKnowledge);
|
|
11
|
+
enable(actor: SpaceActor, spaceId: string): Promise<{
|
|
12
|
+
knowledgebaseId: string;
|
|
13
|
+
}>;
|
|
14
|
+
sync(actor: SpaceActor, space: Space, item: SpaceItem): Promise<void>;
|
|
15
|
+
refresh(actor: SpaceActor, spaceId: string): Promise<{
|
|
16
|
+
knowledgebaseId: string | null;
|
|
17
|
+
mode: string;
|
|
18
|
+
items: ({
|
|
19
|
+
itemId: string;
|
|
20
|
+
title: string;
|
|
21
|
+
status: string;
|
|
22
|
+
error: string | null;
|
|
23
|
+
document: null;
|
|
24
|
+
} | {
|
|
25
|
+
itemId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
status: "pending" | "ready" | "failed" | "processing";
|
|
28
|
+
error: string | null;
|
|
29
|
+
document: import("@xpert-ai/plugin-sdk").KnowledgebaseDocumentRecord | null;
|
|
30
|
+
})[];
|
|
31
|
+
}>;
|
|
32
|
+
status(actor: SpaceActor, spaceId: string): Promise<{
|
|
33
|
+
knowledgebaseId: string | null;
|
|
34
|
+
mode: string;
|
|
35
|
+
items: ({
|
|
36
|
+
itemId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
status: string;
|
|
39
|
+
error: string | null;
|
|
40
|
+
document: null;
|
|
41
|
+
} | {
|
|
42
|
+
itemId: string;
|
|
43
|
+
title: string;
|
|
44
|
+
status: "pending" | "ready" | "failed" | "processing";
|
|
45
|
+
error: string | null;
|
|
46
|
+
document: import("@xpert-ai/plugin-sdk").KnowledgebaseDocumentRecord | null;
|
|
47
|
+
})[];
|
|
48
|
+
}>;
|
|
49
|
+
search(actor: SpaceActor, spaceId: string, query: string): Promise<{
|
|
50
|
+
itemId: string;
|
|
51
|
+
title: string;
|
|
52
|
+
text: string;
|
|
53
|
+
filePath: string;
|
|
54
|
+
hash: string;
|
|
55
|
+
}[]>;
|
|
56
|
+
verify(actor: SpaceActor, itemId: string, hash: string, contentId?: string): Promise<boolean>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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 { ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
|
14
|
+
import { ProjectKnowledgeRuntimeCapability, KnowledgebaseDocumentsRuntimeCapability, KnowledgebaseRuntimeCapability, ProjectFilesRuntimeCapability, XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
|
|
15
|
+
import { z } from 'zod/v3';
|
|
16
|
+
import { KBDocumentStatusEnum } from '@xpert-ai/contracts';
|
|
17
|
+
import { SpaceContentKnowledge } from './content-knowledge.js';
|
|
18
|
+
import { SpaceAccess } from './access.js';
|
|
19
|
+
import { denied, scoped } from './domain.js';
|
|
20
|
+
const metadata = z.object({ spaceItemId: z.string().uuid(), spaceFileHash: z.string() }).passthrough();
|
|
21
|
+
const documentMetadata = z.object({ documentId: z.string().uuid() }).passthrough();
|
|
22
|
+
let SpaceKnowledge = class SpaceKnowledge {
|
|
23
|
+
access;
|
|
24
|
+
runtime;
|
|
25
|
+
content;
|
|
26
|
+
constructor(access, runtime, content) {
|
|
27
|
+
this.access = access;
|
|
28
|
+
this.runtime = runtime;
|
|
29
|
+
this.content = content;
|
|
30
|
+
}
|
|
31
|
+
async enable(actor, spaceId) {
|
|
32
|
+
const { space } = await this.access.resource(actor, spaceId, 'manage');
|
|
33
|
+
if (!space.projectId)
|
|
34
|
+
throw denied();
|
|
35
|
+
const result = await this.runtime.require(ProjectKnowledgeRuntimeCapability).ensureBinding({
|
|
36
|
+
actor, title: space.title, projectId: space.projectId
|
|
37
|
+
});
|
|
38
|
+
await this.access.spaces.update(space.id, { knowledgebaseId: result.knowledgebaseId });
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
async sync(actor, space, item) {
|
|
42
|
+
if (!space.knowledgebaseId || !space.projectId || item.kind === 'folder' || !item.filePath)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
item = await this.access.items.findOneByOrFail({ ...scoped(actor), id: item.id, spaceId: space.id });
|
|
46
|
+
if (!item.filePath || item.kind === 'folder')
|
|
47
|
+
return;
|
|
48
|
+
const documents = this.runtime.require(KnowledgebaseDocumentsRuntimeCapability);
|
|
49
|
+
await this.access.project(actor, space.projectId, 'write');
|
|
50
|
+
if (item.deleted) {
|
|
51
|
+
if (item.knowledgeDocumentId)
|
|
52
|
+
await documents.deleteDocuments({ knowledgebaseId: space.knowledgebaseId, documentIds: [item.knowledgeDocumentId] });
|
|
53
|
+
await this.access.items.update(item.id, { knowledgeDocumentId: null, indexedHash: null, indexStatus: 'pending', indexError: null });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const source = await this.runtime.require(ProjectFilesRuntimeCapability).read({ actor, projectId: space.projectId, path: item.filePath });
|
|
57
|
+
// External changes must be reconciled before indexing a collaborative page.
|
|
58
|
+
// A superseded sync must never replace the index pointer of a newer edit.
|
|
59
|
+
if (item.fileHash !== source.sha256)
|
|
60
|
+
return;
|
|
61
|
+
if (item.indexedHash === source.sha256 && item.knowledgeDocumentId) {
|
|
62
|
+
const current = await documents.getDocumentStatus({ knowledgebaseId: space.knowledgebaseId, documentIds: [item.knowledgeDocumentId] });
|
|
63
|
+
if (current.documents[0]?.status === KBDocumentStatusEnum.ERROR)
|
|
64
|
+
await documents.startProcessing({ knowledgebaseId: space.knowledgebaseId, documentIds: [item.knowledgeDocumentId] });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const file = await documents.uploadFile({ knowledgebaseId: space.knowledgebaseId,
|
|
68
|
+
path: `${item.id}/${source.sha256}`, file: { buffer: source.buffer, originalname: item.filePath.split('/').pop() } });
|
|
69
|
+
const result = await documents.createDocuments({
|
|
70
|
+
knowledgebaseId: space.knowledgebaseId, idempotencyKey: `space:${item.id}:${source.sha256}`,
|
|
71
|
+
documents: [{ name: item.title, filePath: file.filePath, fileUrl: file.fileUrl, mimeType: file.mimeType?.split(';')[0], size: file.size,
|
|
72
|
+
metadata: { spaceItemId: item.id, spaceFileHash: source.sha256, projectId: space.projectId, projectPath: item.filePath } }],
|
|
73
|
+
process: true
|
|
74
|
+
});
|
|
75
|
+
const created = result.documents[0];
|
|
76
|
+
if (!created)
|
|
77
|
+
throw new Error('spaces.index_document_missing');
|
|
78
|
+
const updated = await this.access.items.update({ id: item.id, deleted: false, fileHash: source.sha256 }, { knowledgeDocumentId: created.id, indexedHash: source.sha256, indexStatus: 'processing', indexError: null });
|
|
79
|
+
if (!updated.affected)
|
|
80
|
+
return;
|
|
81
|
+
if (item.knowledgeDocumentId && item.knowledgeDocumentId !== created.id)
|
|
82
|
+
await documents.deleteDocuments({ knowledgebaseId: space.knowledgebaseId, documentIds: [item.knowledgeDocumentId] });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (item.fileHash)
|
|
86
|
+
await this.access.items.update({ id: item.id, fileHash: item.fileHash, deleted: item.deleted }, { indexStatus: 'failed', indexError: error instanceof Error ? error.message.slice(0, 1000) : 'spaces.index_failed' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async refresh(actor, spaceId) {
|
|
90
|
+
const { space } = await this.access.resource(actor, spaceId, 'write');
|
|
91
|
+
const items = await this.access.items.findBy({ ...scoped(actor), spaceId });
|
|
92
|
+
for (const item of items)
|
|
93
|
+
await this.sync(actor, space, item);
|
|
94
|
+
await this.content.refresh(actor, space);
|
|
95
|
+
return this.status(actor, spaceId);
|
|
96
|
+
}
|
|
97
|
+
async status(actor, spaceId) {
|
|
98
|
+
const { space } = await this.access.resource(actor, spaceId, 'read');
|
|
99
|
+
const items = await this.access.items.findBy({ ...scoped(actor), spaceId });
|
|
100
|
+
const documents = space.knowledgebaseId && items.some((item) => item.knowledgeDocumentId)
|
|
101
|
+
? await this.runtime.require(KnowledgebaseDocumentsRuntimeCapability).getDocumentStatus({
|
|
102
|
+
knowledgebaseId: space.knowledgebaseId, documentIds: items.flatMap((item) => item.knowledgeDocumentId ? [item.knowledgeDocumentId] : [])
|
|
103
|
+
}) : { documents: [] };
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const document = documents.documents.find((doc) => doc.id === item.knowledgeDocumentId);
|
|
106
|
+
if (!document)
|
|
107
|
+
continue;
|
|
108
|
+
const status = document.status === KBDocumentStatusEnum.FINISH ? 'ready' : document.status === KBDocumentStatusEnum.ERROR ? 'failed' : 'processing';
|
|
109
|
+
const error = status === 'failed' ? document.processMsg ?? 'spaces.index_failed' : null;
|
|
110
|
+
if (item.indexStatus !== status || item.indexError !== error)
|
|
111
|
+
await this.access.items.update(item.id, { indexStatus: status, indexError: error });
|
|
112
|
+
item.indexStatus = status;
|
|
113
|
+
item.indexError = error;
|
|
114
|
+
}
|
|
115
|
+
return { knowledgebaseId: space.knowledgebaseId, mode: 'keyword', items: [...await this.content.status(actor, space), ...items.filter((item) => item.kind !== 'folder' && !item.deleted).map((item) => ({
|
|
116
|
+
itemId: item.id, title: item.title, status: item.indexStatus, error: item.indexError,
|
|
117
|
+
document: documents.documents.find((doc) => doc.id === item.knowledgeDocumentId) ?? null
|
|
118
|
+
}))] };
|
|
119
|
+
}
|
|
120
|
+
async search(actor, spaceId, query) {
|
|
121
|
+
const { space } = await this.access.resource(actor, spaceId, 'read');
|
|
122
|
+
if (!space.knowledgebaseId)
|
|
123
|
+
return [];
|
|
124
|
+
const result = await this.runtime.require(KnowledgebaseRuntimeCapability).search({
|
|
125
|
+
knowledgebaseIds: [space.knowledgebaseId], query, k: 20, source: 'spaces', retrieval: { mode: 'keyword' }
|
|
126
|
+
});
|
|
127
|
+
const matches = [];
|
|
128
|
+
for (const document of result.documents) {
|
|
129
|
+
const data = metadata.safeParse(document.metadata);
|
|
130
|
+
const source = documentMetadata.safeParse(document.metadata);
|
|
131
|
+
// Platform parsers retain documentId on chunks; custom import metadata lives on the document.
|
|
132
|
+
const indexedItem = source.success ? await this.access.items.findOneBy({
|
|
133
|
+
...scoped(actor), spaceId, knowledgeDocumentId: source.data.documentId, deleted: false
|
|
134
|
+
}) : null;
|
|
135
|
+
const itemId = data.success ? data.data.spaceItemId : indexedItem?.id;
|
|
136
|
+
const hash = data.success ? data.data.spaceFileHash : indexedItem?.indexedHash;
|
|
137
|
+
if (!itemId || !hash)
|
|
138
|
+
continue;
|
|
139
|
+
let item;
|
|
140
|
+
try {
|
|
141
|
+
({ item } = await this.access.resource(actor, itemId, 'read'));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof ForbiddenException)
|
|
145
|
+
continue;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (!item?.filePath || item.deleted || item.spaceId !== spaceId || !space.projectId)
|
|
149
|
+
continue;
|
|
150
|
+
const file = await this.runtime.require(ProjectFilesRuntimeCapability).read({ actor, projectId: space.projectId, path: item.filePath });
|
|
151
|
+
if (file.sha256 !== hash)
|
|
152
|
+
continue;
|
|
153
|
+
matches.push({ itemId: item.id, title: item.title, text: document.pageContent, filePath: item.filePath, hash });
|
|
154
|
+
}
|
|
155
|
+
return [...matches, ...await this.content.matches(actor, space, result.documents)];
|
|
156
|
+
}
|
|
157
|
+
async verify(actor, itemId, hash, contentId) {
|
|
158
|
+
if (contentId)
|
|
159
|
+
return this.content.verify(actor, contentId, hash);
|
|
160
|
+
const { item, space } = await this.access.resource(actor, itemId, 'read');
|
|
161
|
+
if (!item?.filePath || !space.projectId)
|
|
162
|
+
return false;
|
|
163
|
+
const file = await this.runtime.require(ProjectFilesRuntimeCapability).read({ actor, projectId: space.projectId, path: item.filePath });
|
|
164
|
+
return file.sha256 === hash;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
SpaceKnowledge = __decorate([
|
|
168
|
+
Injectable(),
|
|
169
|
+
__param(1, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
170
|
+
__metadata("design:paramtypes", [SpaceAccess, Object, SpaceContentKnowledge])
|
|
171
|
+
], SpaceKnowledge);
|
|
172
|
+
export { SpaceKnowledge };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type IAgentMiddlewareStrategy, type IAgentMiddlewareContext, type AgentMiddleware } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
import type { TAgentMiddlewareMeta } from '@xpert-ai/contracts';
|
|
3
|
+
import { SpaceContent } from './content.js';
|
|
4
|
+
import { SpaceAccess } from './access.js';
|
|
5
|
+
import { SpacePages } from './pages.js';
|
|
6
|
+
import { SpaceAi } from './ai.js';
|
|
7
|
+
import { SpacesService } from './spaces.js';
|
|
8
|
+
export declare class SpacesMiddleware implements IAgentMiddlewareStrategy<Record<string, never>> {
|
|
9
|
+
private readonly content;
|
|
10
|
+
private readonly spaces;
|
|
11
|
+
private readonly pages;
|
|
12
|
+
private readonly ai;
|
|
13
|
+
private readonly access;
|
|
14
|
+
readonly meta: TAgentMiddlewareMeta;
|
|
15
|
+
constructor(content: SpaceContent, spaces: SpacesService, pages: SpacePages, ai: SpaceAi, access: SpaceAccess);
|
|
16
|
+
createMiddleware(_options: Record<string, never>, context: IAgentMiddlewareContext): AgentMiddleware;
|
|
17
|
+
}
|