@xpert-ai/plugin-spaces 0.0.0-stage → 0.2.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 +43 -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/content.d.ts +19 -0
- package/dist/content.js +47 -0
- package/dist/documents.d.ts +36 -0
- package/dist/documents.js +158 -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 +68 -0
- package/dist/entities.js +251 -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 +48 -0
- package/dist/knowledge.d.ts +43 -0
- package/dist/knowledge.js +166 -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-provider.d.ts +36 -0
- package/dist/resource-provider.js +144 -0
- package/dist/spaces.d.ts +95 -0
- package/dist/spaces.js +228 -0
- package/dist/subpages.d.ts +24 -0
- package/dist/subpages.js +118 -0
- package/dist/view.d.ts +97 -0
- package/dist/view.js +226 -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
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { SpaceImports } from './imports.js';
|
|
2
|
+
import type { XpertExtensionViewManifest, XpertResolvedViewHostContext, XpertViewQuery, XpertViewActionRequest, XpertRemoteComponentViewSchema, XpertViewFileAccessRequest } from '@xpert-ai/contracts';
|
|
3
|
+
import { type IXpertViewExtensionProvider, type XpertViewFileActionFile } from '@xpert-ai/plugin-sdk';
|
|
4
|
+
import { SpacesService } from './spaces.js';
|
|
5
|
+
import { SpacePages } from './pages.js';
|
|
6
|
+
import { SpaceAi } from './ai.js';
|
|
7
|
+
import { SpaceEditors } from './editors.js';
|
|
8
|
+
import { SpaceFiles } from './files.js';
|
|
9
|
+
import { SpaceKnowledge } from './knowledge.js';
|
|
10
|
+
import { SpaceContent } from './content.js';
|
|
11
|
+
import { SpaceAccess } from './access.js';
|
|
12
|
+
export declare class SpacesView implements IXpertViewExtensionProvider {
|
|
13
|
+
private readonly imports;
|
|
14
|
+
private readonly content;
|
|
15
|
+
private readonly spaces;
|
|
16
|
+
private readonly pages;
|
|
17
|
+
private readonly ai;
|
|
18
|
+
private readonly files;
|
|
19
|
+
private readonly editors;
|
|
20
|
+
private readonly knowledge;
|
|
21
|
+
private readonly access;
|
|
22
|
+
constructor(imports: SpaceImports, content: SpaceContent, spaces: SpacesService, pages: SpacePages, ai: SpaceAi, files: SpaceFiles, editors: SpaceEditors, knowledge: SpaceKnowledge, access: SpaceAccess);
|
|
23
|
+
supports(context: XpertResolvedViewHostContext): boolean;
|
|
24
|
+
getViewManifests(context: XpertResolvedViewHostContext, slot: string): XpertExtensionViewManifest[];
|
|
25
|
+
getRemoteComponentEntry(context: XpertResolvedViewHostContext, viewKey: string, component: XpertRemoteComponentViewSchema['component']): Promise<{
|
|
26
|
+
html: string;
|
|
27
|
+
contentType: "text/html; charset=utf-8";
|
|
28
|
+
}>;
|
|
29
|
+
getViewData(context: XpertResolvedViewHostContext, viewKey: string, query: XpertViewQuery): Promise<{
|
|
30
|
+
items: never[];
|
|
31
|
+
meta: {
|
|
32
|
+
requiresProject: boolean;
|
|
33
|
+
};
|
|
34
|
+
} | {
|
|
35
|
+
items: never[];
|
|
36
|
+
meta: {
|
|
37
|
+
project: {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
};
|
|
41
|
+
assistant: {
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
dataScope: string;
|
|
45
|
+
};
|
|
46
|
+
root: import("./entities.js").Space | import("./entities.js").SpaceItem;
|
|
47
|
+
role: "editor" | "manager" | "owner" | "member";
|
|
48
|
+
canWrite: boolean;
|
|
49
|
+
canManage: boolean;
|
|
50
|
+
items: (import("./entities.js").SpaceItem & {
|
|
51
|
+
role: string;
|
|
52
|
+
})[];
|
|
53
|
+
requiresProject?: undefined;
|
|
54
|
+
};
|
|
55
|
+
}>;
|
|
56
|
+
executeViewAction(context: XpertResolvedViewHostContext, viewKey: string, action: string, request: XpertViewActionRequest): Promise<{
|
|
57
|
+
success: boolean;
|
|
58
|
+
refresh: boolean;
|
|
59
|
+
data: object;
|
|
60
|
+
}>;
|
|
61
|
+
/** Check once per View request, before accessing resources or serving the empty state.
|
|
62
|
+
* Resource services still enforce their own Project and operation ACLs. */
|
|
63
|
+
private actor;
|
|
64
|
+
private runAction;
|
|
65
|
+
executeViewFileAction(context: XpertResolvedViewHostContext, viewKey: string, actionKey: string, request: XpertViewActionRequest, file: XpertViewFileActionFile): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
data: import("./entities.js").SpaceItem;
|
|
68
|
+
refresh?: undefined;
|
|
69
|
+
} | {
|
|
70
|
+
success: boolean;
|
|
71
|
+
refresh: boolean;
|
|
72
|
+
data: {
|
|
73
|
+
item: import("./entities.js").SpaceItem;
|
|
74
|
+
version: import("./entities.js").SpaceFileVersion;
|
|
75
|
+
};
|
|
76
|
+
}>;
|
|
77
|
+
resolveViewFile(context: XpertResolvedViewHostContext, viewKey: string, request: XpertViewFileAccessRequest): Promise<{
|
|
78
|
+
reference: {
|
|
79
|
+
userId: string;
|
|
80
|
+
projectId: string;
|
|
81
|
+
catalog: "projects";
|
|
82
|
+
source: "platform.workspace.files";
|
|
83
|
+
filePath: string;
|
|
84
|
+
scopeId: string;
|
|
85
|
+
workspacePath: string;
|
|
86
|
+
tenantId: string;
|
|
87
|
+
organizationId: string;
|
|
88
|
+
};
|
|
89
|
+
fileName: string;
|
|
90
|
+
mimeType: string;
|
|
91
|
+
} | {
|
|
92
|
+
reference: import("@xpert-ai/plugin-sdk").WorkspacePortableFileReference;
|
|
93
|
+
fileName: string;
|
|
94
|
+
mimeType: string;
|
|
95
|
+
size: number | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
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 { SpaceImports, sourceSelection, importSelection } from './imports.js';
|
|
11
|
+
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { z } from 'zod/v3';
|
|
14
|
+
import { ViewExtensionProvider, renderRemoteReactIframeHtml } from '@xpert-ai/plugin-sdk';
|
|
15
|
+
import { actorSchema, denied, id, PLUGIN, VIEW } from './domain.js';
|
|
16
|
+
import { createItemSchema, editItemSchema, SpacesService } from './spaces.js';
|
|
17
|
+
import { SpacePages } from './pages.js';
|
|
18
|
+
import { SpaceAi } from './ai.js';
|
|
19
|
+
import { SpaceEditors } from './editors.js';
|
|
20
|
+
import { SpaceFiles } from './files.js';
|
|
21
|
+
import { SpaceKnowledge } from './knowledge.js';
|
|
22
|
+
import { SpaceContent } from './content.js';
|
|
23
|
+
import { SpaceAccess } from './access.js';
|
|
24
|
+
const selection = z.object({ itemId: id }).strict();
|
|
25
|
+
const label = (en_US, zh_Hans) => ({ en_US, zh_Hans });
|
|
26
|
+
const actions = ['source.list', 'source.search', 'source.preview', 'source.import', 'document.open', 'knowledge.enable',
|
|
27
|
+
'knowledge.status', 'knowledge.refresh', 'knowledge.search', 'item.create', 'item.edit', 'page.read', 'search',
|
|
28
|
+
'file.list', 'page.export', 'artifact.list', 'artifact.collect', 'artifact.open', 'editor.list', 'editor.create'];
|
|
29
|
+
let SpacesView = class SpacesView {
|
|
30
|
+
imports;
|
|
31
|
+
content;
|
|
32
|
+
spaces;
|
|
33
|
+
pages;
|
|
34
|
+
ai;
|
|
35
|
+
files;
|
|
36
|
+
editors;
|
|
37
|
+
knowledge;
|
|
38
|
+
access;
|
|
39
|
+
constructor(imports, content, spaces, pages, ai, files, editors, knowledge, access) {
|
|
40
|
+
this.imports = imports;
|
|
41
|
+
this.content = content;
|
|
42
|
+
this.spaces = spaces;
|
|
43
|
+
this.pages = pages;
|
|
44
|
+
this.ai = ai;
|
|
45
|
+
this.files = files;
|
|
46
|
+
this.editors = editors;
|
|
47
|
+
this.knowledge = knowledge;
|
|
48
|
+
this.access = access;
|
|
49
|
+
}
|
|
50
|
+
supports(context) {
|
|
51
|
+
return context.hostType === 'agent' && context.capabilities?.features?.includes('spaces') === true;
|
|
52
|
+
}
|
|
53
|
+
getViewManifests(context, slot) {
|
|
54
|
+
if (!this.supports(context) || !['agent.workbench.main', 'agent.workbench.fixed'].includes(slot))
|
|
55
|
+
return [];
|
|
56
|
+
return [{ key: VIEW, title: label('Spaces', '协作空间'), hostType: context.hostType, slot,
|
|
57
|
+
source: { provider: 'spaces.views', plugin: PLUGIN }, refreshable: true,
|
|
58
|
+
activation: { requiredFeatures: ['spaces'] },
|
|
59
|
+
workbench: slot === 'agent.workbench.fixed' ? { fixed: true, menu: { enabled: true, label: label('Spaces', '协作空间'), order: 35 } } : {},
|
|
60
|
+
view: { type: 'remote_component', runtime: 'react', protocolVersion: 1,
|
|
61
|
+
component: { isolation: 'iframe', entry: 'spaces' }, dataSource: { mode: 'platform' } },
|
|
62
|
+
dataSource: { mode: 'platform', cache: { enabled: false }, querySchema: { supportsSelection: true, supportsParameters: true } },
|
|
63
|
+
fileAccess: { purposes: ['download'] },
|
|
64
|
+
actions: [...actions.map((key) => ({ key, label: label(key, key), actionType: 'invoke' })),
|
|
65
|
+
{ key: 'editor.import', label: label('Import document', '导入文档'), actionType: 'invoke', transport: 'file' },
|
|
66
|
+
{ key: 'file.upload', label: label('Upload file', '上传文件'), actionType: 'invoke', transport: 'file' }],
|
|
67
|
+
// Declare both phases: platform resource resolution, then container-local navigation.
|
|
68
|
+
clientCommands: ['assistant.context.set', 'workbench.content.resolve', 'workbench.navigation.open'].map((key) => ({ key, required: key === 'workbench.content.resolve', label: label(key, key) })) }];
|
|
69
|
+
}
|
|
70
|
+
async getRemoteComponentEntry(context, viewKey, component) {
|
|
71
|
+
if (viewKey !== VIEW || component.entry !== 'spaces')
|
|
72
|
+
throw new Error('spaces.view_missing');
|
|
73
|
+
const actor = await this.actor(context, false);
|
|
74
|
+
if (actor.projectId)
|
|
75
|
+
await this.spaces.workspace(actor);
|
|
76
|
+
const [appScript, appCss] = await Promise.all(['app.js', 'app.css'].map((name) => readFile(new URL(`./remote/${name}`, import.meta.url), 'utf8')));
|
|
77
|
+
return { html: renderRemoteReactIframeHtml({ title: 'Spaces', reactUmd: '', reactDomUmd: '', appScript, appCss }), contentType: 'text/html; charset=utf-8' };
|
|
78
|
+
}
|
|
79
|
+
async getViewData(context, viewKey, query) {
|
|
80
|
+
if (viewKey !== VIEW)
|
|
81
|
+
throw new Error('spaces.view_missing');
|
|
82
|
+
const actor = await this.actor(context, false);
|
|
83
|
+
if (!actor.projectId)
|
|
84
|
+
return { items: [], meta: { requiresProject: true } };
|
|
85
|
+
const space = await this.spaces.workspace(actor);
|
|
86
|
+
const resourceId = !query.selectionId || query.selectionId === 'root' ? space.id : id.parse(query.selectionId);
|
|
87
|
+
return { items: [], meta: { ...await this.spaces.tree(actor, resourceId, query.parameters?.trash === true),
|
|
88
|
+
project: { id: space.projectId, title: space.title },
|
|
89
|
+
assistant: { id: context.hostId, title: space.title, dataScope: 'project' } } };
|
|
90
|
+
}
|
|
91
|
+
async executeViewAction(context, viewKey, action, request) {
|
|
92
|
+
try {
|
|
93
|
+
return await this.runAction(context, viewKey, action, request);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof z.ZodError)
|
|
97
|
+
throw new BadRequestException('spaces.invalid_input');
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Check once per View request, before accessing resources or serving the empty state.
|
|
102
|
+
* Resource services still enforce their own Project and operation ACLs. */
|
|
103
|
+
async actor(context, requireProject = true) {
|
|
104
|
+
if (!this.supports(context))
|
|
105
|
+
throw denied();
|
|
106
|
+
const actor = actorSchema.extend({ assistantId: id }).parse({ ...context, assistantId: context.hostId, projectId: context.runtimeScope?.projectId ?? undefined });
|
|
107
|
+
if (requireProject && !actor.projectId)
|
|
108
|
+
throw denied();
|
|
109
|
+
await this.access.assistant(actor);
|
|
110
|
+
return actor;
|
|
111
|
+
}
|
|
112
|
+
async runAction(context, viewKey, action, request) {
|
|
113
|
+
if (viewKey !== VIEW)
|
|
114
|
+
throw new Error('spaces.view_missing');
|
|
115
|
+
if (!actions.includes(action))
|
|
116
|
+
throw new Error('spaces.action_unknown');
|
|
117
|
+
const actor = await this.actor(context), input = request.input;
|
|
118
|
+
await this.spaces.workspace(actor);
|
|
119
|
+
let result;
|
|
120
|
+
switch (action) {
|
|
121
|
+
case 'source.list':
|
|
122
|
+
result = await this.imports.sources(actor);
|
|
123
|
+
break;
|
|
124
|
+
case 'source.search':
|
|
125
|
+
result = await this.imports.search(actor, z.object({ bindingId: id, query: z.string().trim().max(200).default(''), cursor: z.string().max(2000).optional(), pageSize: z.number().int().min(1).max(50).default(25) }).strict().parse(input));
|
|
126
|
+
break;
|
|
127
|
+
case 'source.preview':
|
|
128
|
+
result = await this.imports.preview(actor, sourceSelection.parse(input));
|
|
129
|
+
break;
|
|
130
|
+
case 'source.import':
|
|
131
|
+
result = await this.imports.apply(actor, importSelection.parse(input));
|
|
132
|
+
break;
|
|
133
|
+
case 'document.open':
|
|
134
|
+
result = await this.content.open(actor, selection.parse(input).itemId);
|
|
135
|
+
break;
|
|
136
|
+
case 'knowledge.enable':
|
|
137
|
+
case 'knowledge.status':
|
|
138
|
+
case 'knowledge.refresh': {
|
|
139
|
+
const p = z.object({ spaceId: id }).strict().parse(input);
|
|
140
|
+
result = action === 'knowledge.enable' ? await this.knowledge.enable(actor, p.spaceId)
|
|
141
|
+
: action === 'knowledge.refresh' ? await this.knowledge.refresh(actor, p.spaceId) : await this.knowledge.status(actor, p.spaceId);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'knowledge.search': {
|
|
145
|
+
const p = z.object({ spaceId: id, query: z.string().trim().min(1).max(4000) }).strict().parse(input);
|
|
146
|
+
result = await this.knowledge.search(actor, p.spaceId, p.query);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'editor.list':
|
|
150
|
+
result = await this.editors.list(actor);
|
|
151
|
+
break;
|
|
152
|
+
case 'editor.create': {
|
|
153
|
+
const p = z.object({ spaceId: id, parentId: id.nullable().default(null), title: z.string().trim().min(1).max(200), editorKey: z.string().max(100) }).strict().parse(input);
|
|
154
|
+
result = await this.editors.create(actor, p.spaceId, p.parentId, p.title, p.editorKey);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'item.create':
|
|
158
|
+
result = await this.spaces.createItem(actor, createItemSchema.extend({ kind: z.enum(['folder', 'page']), artifactId: z.never().optional(), artifactVersionId: z.never().optional() }).parse(input));
|
|
159
|
+
break;
|
|
160
|
+
case 'item.edit':
|
|
161
|
+
result = await this.spaces.editItem(actor, editItemSchema.parse(input));
|
|
162
|
+
break;
|
|
163
|
+
case 'page.read':
|
|
164
|
+
result = await this.pages.read(actor, selection.parse(input).itemId);
|
|
165
|
+
break;
|
|
166
|
+
case 'page.export':
|
|
167
|
+
result = await this.files.exportPage(actor, selection.parse(input).itemId);
|
|
168
|
+
break;
|
|
169
|
+
case 'file.list':
|
|
170
|
+
result = await this.files.list(actor, selection.parse(input).itemId);
|
|
171
|
+
break;
|
|
172
|
+
case 'artifact.open':
|
|
173
|
+
result = await this.files.openArtifact(actor, selection.parse(input).itemId);
|
|
174
|
+
break;
|
|
175
|
+
case 'artifact.list':
|
|
176
|
+
result = await this.files.available(actor, z.object({ page: z.number().int().min(1).default(1) }).strict().parse(input).page);
|
|
177
|
+
break;
|
|
178
|
+
case 'artifact.collect': {
|
|
179
|
+
const p = z.object({ spaceId: id, parentId: id.nullable().default(null), artifactId: id, versionId: id }).strict().parse(input);
|
|
180
|
+
result = await this.files.collect(actor, p.spaceId, p.parentId, p.artifactId, p.versionId);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'search': {
|
|
184
|
+
const p = z.object({ resourceId: id, query: z.string().trim().min(1).max(4000) }).strict().parse(input);
|
|
185
|
+
result = await this.ai.search(actor, p.resourceId, p.query);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
default: throw new Error('spaces.action_unknown');
|
|
189
|
+
}
|
|
190
|
+
return { success: true, refresh: false, data: result };
|
|
191
|
+
}
|
|
192
|
+
async executeViewFileAction(context, viewKey, actionKey, request, file) {
|
|
193
|
+
if (viewKey !== VIEW || !['file.upload', 'editor.import'].includes(actionKey))
|
|
194
|
+
throw new Error('spaces.action_unknown');
|
|
195
|
+
const actor = await this.actor(context);
|
|
196
|
+
if (actionKey === 'editor.import') {
|
|
197
|
+
const p = z.object({ spaceId: id, parentId: id.nullable().default(null), editorKey: z.string().max(100) }).strict().parse(request.input);
|
|
198
|
+
return { success: true, data: await this.editors.create(actor, p.spaceId, p.parentId, file.originalname ?? 'Document', p.editorKey, file.buffer) };
|
|
199
|
+
}
|
|
200
|
+
const input = z.object({ spaceId: id, parentId: id.nullable().default(null) }).strict().parse(request.input);
|
|
201
|
+
return { success: true, refresh: false, data: await this.files.upload(actor, input.spaceId, input.parentId, file.originalname ?? 'file', file.mimetype ?? 'application/octet-stream', file.buffer) };
|
|
202
|
+
}
|
|
203
|
+
async resolveViewFile(context, viewKey, request) {
|
|
204
|
+
if (viewKey !== VIEW)
|
|
205
|
+
throw new Error('spaces.view_missing');
|
|
206
|
+
const actor = await this.actor(context);
|
|
207
|
+
if (request.fileKey.startsWith('item:'))
|
|
208
|
+
return this.files.current(actor, id.parse(request.fileKey.slice(5)));
|
|
209
|
+
if (request.fileKey.startsWith('artifact:')) {
|
|
210
|
+
const { version } = await this.files.openArtifact(actor, id.parse(request.fileKey.slice('artifact:'.length)));
|
|
211
|
+
if (!version.workspaceFileRef)
|
|
212
|
+
throw new BadRequestException('spaces.artifact_file_unavailable');
|
|
213
|
+
return { reference: version.workspaceFileRef, fileName: version.fileName ?? 'artifact',
|
|
214
|
+
mimeType: version.mimeType ?? 'application/octet-stream', size: version.size ?? undefined };
|
|
215
|
+
}
|
|
216
|
+
const version = await this.files.get(actor, id.parse(request.fileKey));
|
|
217
|
+
return { reference: version.reference, fileName: version.reference.originalName ?? 'file',
|
|
218
|
+
mimeType: version.reference.mimeType ?? 'application/octet-stream', size: version.reference.size ?? undefined };
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
SpacesView = __decorate([
|
|
222
|
+
Injectable(),
|
|
223
|
+
ViewExtensionProvider('spaces.views'),
|
|
224
|
+
__metadata("design:paramtypes", [SpaceImports, SpaceContent, SpacesService, SpacePages, SpaceAi, SpaceFiles, SpaceEditors, SpaceKnowledge, SpaceAccess])
|
|
225
|
+
], SpacesView);
|
|
226
|
+
export { SpacesView };
|
package/docs/.mintignore
ADDED
package/docs/AGENTS.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Spaces documentation
|
|
2
|
+
|
|
3
|
+
Document the behavior shipped by this plugin and its required platform capabilities. Keep the distinction between authoring workspace, Assistant file workspace, Space, and Knowledgebase explicit. Do not describe a mock model run as a real model acceptance test. Keep credentials and local deployment identities out of documentation. Use concise MDX pages with frontmatter and update `docs.json` when adding a page.
|
package/docs/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mintlify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "架构和验证"
|
|
3
|
+
description: "Project 存储与成员权限、插件资源和协作能力边界"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 平台与插件边界
|
|
7
|
+
|
|
8
|
+
协作空间使用内置 Project Type `platform / space`。Project 管理成员、角色、归档、多 Assistant 绑定和文件工作区;Spaces 插件提供资源树、来源信息、知识库启用入口及 Remote View。
|
|
9
|
+
|
|
10
|
+
Space 必须绑定 Project,`(tenantId, organizationId, projectId)` 唯一索引保证并发初始化只有一个扩展记录。`SpaceItem.spaceId` 仅关联插件内部的 Space 记录,不是文件存储身份。模块按全新功能开发,不提供独立 Space 卷、Assistant 私有 Space 或旧空间迁移入口。
|
|
11
|
+
|
|
12
|
+
平台提供 `platform.project.access`、`platform.project.files`、`platform.content.resources`、`platform.content.documents`、协作、知识库及 Artifact 能力。每次资源访问重新检查 Project 成员权限;通过 Assistant 打开时还检查 Assistant 与项目的绑定以及 `spaces` 能力。具有 Assistant 权限本身不授予项目内容权限。
|
|
13
|
+
|
|
14
|
+
Remote View 使用共享 shadcn/ui 组件、Tailwind 和宿主主题变量。Markdown 由独立 Tiptap 插件编辑;通用审阅与 AI 交互属于 Assistant。当前选择可以传给聊天作为上下文,但不授予权限。
|
|
15
|
+
|
|
16
|
+
## 文件引用与权威状态
|
|
17
|
+
|
|
18
|
+
所有空间文件位于 `/project/<projectId>`。正文和附件使用可见的项目相对路径,版本快照使用同一项目下的 `.space-history/<itemId>/<versionId>/<name>`。平台通用文件协议使用 `catalog: 'projects'`、`projectId` 和 `scopeId: projectId`,不包含 `spaceId`。
|
|
19
|
+
|
|
20
|
+
页面在创建或发现时直接注册 `markdown.tiptap` 平台文档;打开页面只读取已注册文档。没有 Spaces 自有的协作模型、修订/评论/提案表或 `importLegacy` 路径。只读成员发现的新 Markdown 先作为项目文件展示,编辑成员可将其注册为协作文档。
|
|
21
|
+
|
|
22
|
+
Markdown 文件是可交换内容,平台协作文档维护编辑状态和提交一致性;Space 的正文缓存、哈希和同步状态属于派生信息。写入或投影失败不得回退为仅更新数据库正文。来自其他 Assistant 的项目文件也可以在空间发现。
|
|
23
|
+
|
|
24
|
+
Artifact 以 `pluginName + resourceType + resourceId` 标识来源,`authorizationProviderKey: "spaces"` 对已有 `resourceId` 附加资源授权,`workspaceFileRef` 定位 Project 中的文件。创建时通过 `scope: { projectId, userId: null }` 显式使用项目共享归属,保留真实操作人用于授权和审计。Provider 不决定归属或列表可见性;列表对当前用户授权后分页,下载同时检查项目权限、资源权限和链接策略。移除独立 Space 卷不移除资源授权、路径校验和项目文件的写保护。
|
|
25
|
+
|
|
26
|
+
## Runtime 能力边界
|
|
27
|
+
|
|
28
|
+
- 执行身份共用 `RuntimeActor`;组织内内容要求 `OrganizationRuntimeActor`。`AssistantMiddlewareAccess.assertAccess` 使用显式 `scope`:独立 Assistant 检查发布目录访问权,项目内使用检查当前项目成员及 Assistant 绑定,不额外要求目录或创作工作区可见性;两者均检查有效组织成员和 `provider + requiredFeature`,并与平台 View Middleware Runtime 共用解析规则。
|
|
29
|
+
- View 的组件加载、数据、动作、文件传输与工具调用在入口重新检查能力;空项目状态也需独立 Assistant 授权。身份和项目范围来自可信宿主上下文,不可由 iframe 或工具参数指定。资源授权及协作广播保留项目和操作权限,不逐文档检查功能开关;移除中间件不改变已有文件的项目归属。不存在跨调用授权缓存。
|
|
30
|
+
- `ContentResources.authorize` 负责资源授权,`identities` 子接口提供当前组织身份。Provider 按需组合 `references`、`pages`、`publication`、`locations`,没有对应扩展即可省略。平台文档输入的 `resourceId` 与扩展输入的 `sourceResourceId` 明确区分;子页面创建返回 `documentId`。
|
|
31
|
+
- `ContentEditors`(`platform.content.editors`)负责通用编辑器发现、资源解析与带版本校验的文件读写;平台协议不包含表格、单元格或具体格式的修改指令。
|
|
32
|
+
- `ProjectKnowledge.ensureBinding`(`platform.project.knowledge`)按 projectId 建立一个内置知识库,以平台 `platform.project` 资源策略继承项目权限;Space 表只缓存知识库 ID。已有知识库服务仍要求匹配 actor 的请求/任务上下文。
|
|
33
|
+
- `ProjectFiles` 是显式项目身份的门面;与 `WorkspaceFiles` 共用 Volume 文件读写、路径约束和写入锁,提交前重新检查托管文档写保护。CAS 哈希防止版本覆盖。
|
|
34
|
+
|
|
35
|
+
## 权限与验证
|
|
36
|
+
|
|
37
|
+
项目 owner/manager 管理资源,owner/manager/editor 编辑内容,member 只读;归档项目只读。实时会话及后台任务重新校验当前权限。跨项目引用不能通过传入目标项目身份绕过源项目的权限。
|
|
38
|
+
|
|
39
|
+
交付需联合部署配套平台、SDK 和插件。使用最新实体初始化;本次代码收敛不操作已有开发数据库,不提供历史独立 Space 的升级脚本。
|
|
40
|
+
|
|
41
|
+
自动化回归覆盖 Project 文件引用重放、跨项目拒绝、只读和撤权拒绝写入、Assistant 解绑,以及正文和版本快照使用相同项目身份。插件还需通过构建、类型检查、元数据检查和 lifecycle harness。浏览器、双用户协作和真实模型验收需要单独执行,不能以单元测试或构建成功代替。
|
|
42
|
+
|
|
43
|
+
## 编辑器职责边界
|
|
44
|
+
|
|
45
|
+
- 平台:统一资源引用、Project 与 Assistant 绑定授权、编辑器注册及 Workbench 路由。注册区分文本协作模型与文件编辑器;Markdown 继续使用文本/Yjs 协作管线。
|
|
46
|
+
- Spaces:目录、创建和导入文件、资源权限及版本快照。`editor.describe` 描述当前资源位置;`editor.files` 处理不透明字节与版本,不解析表格。保存时检查资源版本、磁盘哈希和当前权限。
|
|
47
|
+
- Office:XLSX 创建、校验、单元格读取与修改、序列化,以及独立的 `office.files__office.file` Remote View。文件编辑模式按版本保存,冲突时阻止覆盖,不自动合并并发修改;与原生 Office 快照/Yjs 模式分别处理。
|
|
48
|
+
|
|
49
|
+
Spaces 通过 `workbench.content.resolve({ resourceId, providerKey: 'spaces' })` 解析资源,将返回的 navigation(包括 contentProvider 参数)原样交给当前 Workbench 打开。Spaces 不包含表格编辑 UI。新文档格式通过注册编辑器和独立视图扩展,不需要向平台增加专用格式 RuntimeCapability。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 实时协同编辑
|
|
3
|
+
description: Markdown 双账号同步、并发合并、光标与真实验收
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 已实现的协作链路
|
|
7
|
+
|
|
8
|
+
同一项目中,具有权限的成员可以通过不同 Assistant 打开同一 Markdown 文档。编辑器使用 Tiptap 与 Yjs;平台管理协作房间、身份、权限、更新日志和文件发布。Spaces 负责内容组织,独立 Markdown 插件负责编辑界面。
|
|
9
|
+
|
|
10
|
+
- 按文档与 epoch 隔离房间;同一文档不因入口 Assistant 不同而产生副本。
|
|
11
|
+
- 本地增量经宿主 `document.update` 提交,平台持久化后通过 WebSocket 向已授权接收者广播。Yjs 合并并发修改,重复增量不会重复追加正文。
|
|
12
|
+
- 在线人数按账号去重;光标和选区按窗口区分,姓名与颜色来自平台身份。状态不写入正文。
|
|
13
|
+
- 断线后通过状态向量补齐缺失更新。未提交增量保留在当前窗口重试;这不等于关闭浏览器后的持久离线编辑。
|
|
14
|
+
- 保存状态区分「编辑已接收」与「已写入项目文件」。只有文件发布完成才显示已保存;外部文件冲突不能被静默覆盖。
|
|
15
|
+
- 历史恢复或 schema 升级切换 epoch,旧窗口不能覆盖新版本。所有会话和广播仍检查当前项目、Assistant 及资源访问权限。
|
|
16
|
+
|
|
17
|
+
## 跨账号广播修复
|
|
18
|
+
|
|
19
|
+
2026-10-23 的真实测试复现了:两个账号能显示在线,但提交编辑会导致另一方断连。进程内广播继承了 HTTP 提交者的 AsyncLocalStorage;文档权限校验把接收者身份与提交者进行比较,误判为身份不匹配。
|
|
20
|
+
|
|
21
|
+
平台 `CollaborationGateway` 在广播投递边界退出提交者请求上下文,再按各接收者已认证的协作会话逐个授权。普通 HTTP 请求的身份一致性校验保留,撤权接收者仍被断开。Provider 必须使用显式授权上下文中的 actor,不能借用广播提交者的身份或权限。
|
|
22
|
+
|
|
23
|
+
回归测试覆盖发送方与接收方不同、HTTP 冒用 actor 被拒绝、已撤权接收者不获得广播。网关与协作服务的 6 项定向测试通过。
|
|
24
|
+
|
|
25
|
+
## 真实验收
|
|
26
|
+
|
|
27
|
+
两个独立测试账号通过正常登录获取身份,分别经两个已发布 Assistant 打开同一项目文档。测试直接连接实际 API、WebSocket、数据库和项目文件,没有替换协作服务或模拟持久化。
|
|
28
|
+
|
|
29
|
+
| 场景 | 验证结果 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 双账号进入 | 相同房间、不同身份,双方均看到两人在线 |
|
|
32
|
+
| 同一位置同时输入 | 两份内容通过远端 WebSocket 广播收敛,甲乙输入均保留,重复提交被去重 |
|
|
33
|
+
| 光标与选区 | 对方收到正确账号与 Yjs 相对位置 |
|
|
34
|
+
| 任务清单与公式 | 不同账号的节点修改同时保留 |
|
|
35
|
+
| 断线与重连 | 离线成员移出在线列表,重连补齐遗漏内容,缓存修改重发后收敛 |
|
|
36
|
+
| 未授权账号 | 获取会话与提交更新均被拒绝 |
|
|
37
|
+
| 文件与重开 | 两端内容、重新获取的文档状态和实际 Markdown 文件一致,SHA-256 与平台记录一致 |
|
|
38
|
+
|
|
39
|
+
这次浏览器自动化连接中断,未完成两个可见浏览器窗口的交互验收;上表为真实协议客户端验收,不能等同于双窗口视觉验收。跨 API 节点和长时间压力测试也不在本次结果中。
|
|
40
|
+
|
|
41
|
+
## 复现方式
|
|
42
|
+
|
|
43
|
+
先启动配套平台与插件,准备私有目录中的正常登录助手 `probe.mjs` 和已有测试项目配置 `editor-acceptance-fixture.json`,再从 Markdown 插件目录执行:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
SPACES_TEST_DIRECTORY=/absolute/path/to/private-test-environment \
|
|
47
|
+
node tests/collaboration.live.mjs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
脚本创建独立验收页面,输出七项结果,并将包含实例标识的回执保存到该私有目录。凭据不写入源码、文档或终端结果。可在同一项目的协作空间中打开标题以「双账号实时协作验收」开头的页面继续人工验证。
|
package/docs/docs.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://mintlify.com/docs.json",
|
|
3
|
+
"theme": "mint",
|
|
4
|
+
"name": "Xpert Spaces",
|
|
5
|
+
"colors": {
|
|
6
|
+
"primary": "#16A34A",
|
|
7
|
+
"light": "#07C983",
|
|
8
|
+
"dark": "#15803D"
|
|
9
|
+
},
|
|
10
|
+
"navigation": {
|
|
11
|
+
"groups": [
|
|
12
|
+
{
|
|
13
|
+
"group": "协作空间",
|
|
14
|
+
"pages": [
|
|
15
|
+
"index",
|
|
16
|
+
"quickstart",
|
|
17
|
+
"architecture",
|
|
18
|
+
"insert-menu-plan",
|
|
19
|
+
"math-subpages",
|
|
20
|
+
"collaboration",
|
|
21
|
+
"page-resources-and-connectors",
|
|
22
|
+
"feishu-document-import"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"contextual": {
|
|
28
|
+
"options": [
|
|
29
|
+
"copy",
|
|
30
|
+
"view",
|
|
31
|
+
"chatgpt",
|
|
32
|
+
"claude",
|
|
33
|
+
"perplexity",
|
|
34
|
+
"mcp",
|
|
35
|
+
"cursor",
|
|
36
|
+
"vscode"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 飞书文档文本副本导入
|
|
3
|
+
description: 通过连接器读取来源,显式复制到项目文件,记录出处与版本
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 本批范围
|
|
7
|
+
|
|
8
|
+
本批落地第二阶段的只读来源与文本副本导入。入口为协作空间“新建 → 从文档平台导入”。飞书新版文档(`docx`)可列出、按关键词搜索、粘贴链接预览,再导入当前项目文件夹。
|
|
9
|
+
|
|
10
|
+
这是**文本副本**:图片、附件、公式、表格结构、排版和评论不随副本保留。对话框在导入前显示实际文本及转换提示,用户确认后才保存。原文不会被修改。富文档转换、知识库目录浏览、远端引用、发布与读回、双向同步仍属后续阶段。
|
|
11
|
+
|
|
12
|
+
## 插件与平台职责
|
|
13
|
+
|
|
14
|
+
| 层 | 职责 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| SDK | 可选 `ConnectorStrategyRuntime.documents` hook;显式 `text/plain` 快照、修订号、内容哈希和提示 |
|
|
17
|
+
| 平台 | `platform.connector.documents` 提供来源目录、搜索、读取;依据当前身份、已发布 Assistant、项目绑定和实时连接状态授权 |
|
|
18
|
+
| 飞书插件 | 在现有 `plugin-lark-cli` 增加独立文档适配模块;调用固定 OpenAPI 域名,保持授权凭据仅在服务端 |
|
|
19
|
+
| Spaces 插件 | 预览、目标文件夹、共享确认、请求幂等、项目 Markdown 文件及来源元数据 |
|
|
20
|
+
| Markdown 插件 | 继续负责 Tiptap/Yjs 编辑;不依赖飞书 SDK,不引入远端协同协议 |
|
|
21
|
+
|
|
22
|
+
平台目录从已授权的 Assistant 工作区与当前项目连接构建。浏览器传入 `bindingId` 不构成授权;平台先从实时目录中选择,再使用已有 scoped credential API 完成身份、项目、连接状态、刷新与审计检查。此交互 API 不扩大 Agent 执行中的 Connector grants。
|
|
23
|
+
|
|
24
|
+
当前连接模型只使用平台已有的 **shared** 连接。操作依据该连接授权账户的可见范围,不等于每位项目成员自己的飞书权限。管理员必须按预期团队范围配置连接;个人连接的独立授权和权限交集需要后续单独交付。未连接、不支持文档 hook 的来源不会被当作可读来源。
|
|
25
|
+
|
|
26
|
+
## 用户流程
|
|
27
|
+
|
|
28
|
+
1. 在当前文件夹选择“从文档平台导入”。
|
|
29
|
+
2. 选择可用来源。空查询列出云盘根目录;关键词搜索新版文档,当前接口最多取前 199 项。文件夹与知识库导航暂未接入,非根目录文档可用关键词或 `/docx/`、`/wiki/` 链接定位。知识库链接会解析到其指向的新版文档;表格、文件等其他节点类型仍不可导入。搜索和云盘根目录不保证列出个人文档库中的知识库节点,未找到时可粘贴链接。
|
|
30
|
+
3. 选择结果或粘贴新版文档链接,查看文本预览、原文版本和目标项目。
|
|
31
|
+
4. 确认将文本副本共享给项目成员。导入后的内容按项目权限长期保存,飞书撤权不会自动删除已经导入的副本。
|
|
32
|
+
5. 导入后在空间打开,进入同一 Workbench 中的 Markdown 编辑器。列表来源显示“外部文本副本”;更多菜单中的“查看导入来源”显示来源平台、文档 ID、版本、时间、转换提示与可用的原文链接。
|
|
33
|
+
|
|
34
|
+
空来源、加载中、失败重试和授权失效均有对应界面。搜索结果分页加载并按文档 ID 去重。控件字号和密度沿用宿主主题。
|
|
35
|
+
|
|
36
|
+
## 一致性与失败恢复
|
|
37
|
+
|
|
38
|
+
- 读取采用“元数据 → 原始文本 → 元数据”,两次修订号或标题不同则要求重新预览。
|
|
39
|
+
- 导入请求带预览修订号和 SHA-256,服务端重新读取;变化后拒绝导入旧预览。
|
|
40
|
+
- 文件名使用标题与请求 UUID;相同请求 ID 只创建同一份文件,重试不会产生重复副本。请求绑定操作者、来源、预览版本和目标。
|
|
41
|
+
- 使用空间锁与现有发现/移动流程串行;先保存导入意图,再进行文件发布。注册编辑器中断可重试;文件被其他主体修改时保留文件并报冲突。
|
|
42
|
+
- 完成的导入请求仅返回既有副本。后续远端变化不会覆盖已经协同编辑的本地内容。
|
|
43
|
+
- 内容保存在项目 `.md` 文件;`plugin_spaces_item.importOrigin` 保存来源元数据、哈希和幂等凭据,不保存正文、token 或签名下载地址。
|
|
44
|
+
- 导入先编码为字面 Markdown 文本,不将远端 HTML 或链接标记直接作为活动内容。单篇可读取文本上限为 100 KB。
|
|
45
|
+
|
|
46
|
+
飞书适配器仅接受 HTTPS 的飞书/Lark `/docx/`、`/wiki/` 地址或文档 token;输入 URL 只用于提取文档 ID,绝不直接抓取。请求使用固定 API origin,禁止重定向,有超时和响应大小限制。错误仅带固定错误标识、HTTP 状态与数字业务码,不转发 Axios headers、凭据或原始错误响应。
|
|
47
|
+
|
|
48
|
+
知识库链接通过飞书当前 `wiki/v2/spaces/node_by_token` 接口解析,使用 `wiki:node:retrieve` 权限。仅接受返回的 `obj_type=docx`,校验节点 ID,并在正文读取后复核映射;元数据保存真实文档 ID,来源链接保留去掉追踪参数的原始知识库 URL。
|
|
49
|
+
|
|
50
|
+
## 部署
|
|
51
|
+
|
|
52
|
+
1. 更新平台 plugin-sdk 与 server-ai,重启 API,使 `platform.connector.documents` 生效。
|
|
53
|
+
2. 构建并更新 Spaces 与 Lark CLI 插件。Lark CLI 在 monorepo 中声明 `workspace:^` 依赖,独立部署使用 `corepack pnpm pack` 生成 `.tgz`,再通过平台归档安装接口上传;直接复制源码目录给 npm 安装会报 `EUNSUPPORTEDPROTOCOL`。发布包会把 workspace 范围转换为已发布版本。
|
|
54
|
+
3. 在未启用开发 schema 同步的环境执行 `migrations/20261023-document-import-origin.sql`。
|
|
55
|
+
4. 为所需 Assistant 工作区或项目配置 shared 飞书连接,并在飞书应用与账户中授予读取/搜索文档所需权限。
|
|
56
|
+
|
|
57
|
+
新 API/SDK 应先于插件升级。仅更新插件会因宿主尚未导出新 capability 而装载失败。
|
|
58
|
+
|
|
59
|
+
## 验收记录
|
|
60
|
+
|
|
61
|
+
### 2026-10-23
|
|
62
|
+
|
|
63
|
+
- Spaces 29 项测试通过,包含并发重复请求、注册中断恢复、源版本变化、撤权、只读/项目外访问、已有文件外部修改保护与字面文本转换。另以实际 Markdown/Tiptap 模型确认 URL、HTML、图片标记与公式样式文本均按文字导入。导入测试使用内存文件与模拟来源,尚不代表真实外部导入。
|
|
64
|
+
- Lark CLI 77 项测试通过,其中新增 5 项覆盖 URL/域名限制、分页与类型过滤、文本快照与修订检查、大小限制、错误脱敏。飞书 HTTP 响应使用模拟数据。
|
|
65
|
+
- 平台相关 3 个套件、105 项测试通过,包含新来源 API 的授权、目录筛选、撤权,以及没有作者工作区的项目 Assistant 连接目录兼容。
|
|
66
|
+
- SDK、两个插件构建和 Spaces 前后端类型检查通过;两个插件生命周期检查通过。Spaces 元数据检查通过;Lark CLI 未声明 package 级 native metadata,静态检查未匹配到条目,其元数据和装载通过生命周期检查确认。
|
|
67
|
+
- 本地 API 已重启加载新能力;Lark CLI 标准发布归档通过真实插件安装接口部署成功。
|
|
68
|
+
- 真实 API 中,项目所有者/编辑者可以查询来源目录,项目外账号被拒绝;伪造连接 ID 的预览被拒绝,畸形输入通过真实 View action 返回 400。当日测试项目没有已授权连接,目录为空。
|
|
69
|
+
- 浏览器已验证 ChatKit Workbench 中的新建菜单、导入对话框、空连接提示与不可提交状态。未将空状态验收记为搜索/预览/导入成功。
|
|
70
|
+
|
|
71
|
+
**当日待补验**:账户授权、远端搜索/读取、预览后的源变化、导入到真实项目文件并进入 Tiptap、源撤权及不同成员可见范围。当日未对飞书原文执行任何写入。
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### 2026-10-24:真实账户、导入与协作闭环
|
|
75
|
+
|
|
76
|
+
- 已由用户完成共享连接授权,真实平台来源目录返回 `active / available`。
|
|
77
|
+
- 经用户授权,在飞书客户端新建了只含验收内容的文档,并看到“已经保存到云端”。该客户端的新文档默认使用 `/wiki/` 地址,因此补充知识库链接解析、节点类型和稳定映射校验。
|
|
78
|
+
- 初次真实飞书节点读取返回 `131006`(资源无权限),原因是客户端身份与连接授权身份不同。适配器将此拒绝映射为 HTTP 403,界面提示核对账号与文档权限。用户随后改用文档创建者账号完成重新授权,连接已恢复 `active`,实际读取成功。没有为解决身份不匹配而扩大文档公开范围或发送邀请通知。
|
|
79
|
+
- 真实搜索请求成功,但精确标题搜索仍未返回个人文档库中的这篇文档。粘贴 `/wiki/` 链接可以解析并读取,直接使用解析后的 `docx` token 读取也得到相同内容哈希。这项结果证明链接读取可用,不代表搜索召回已通过。
|
|
80
|
+
- Lark CLI 81 项测试、Spaces 29 项测试通过;两个插件构建、Spaces 前后端类型检查和两个插件生命周期检查通过。Spaces 静态元数据检查通过,Lark CLI 静态检查未匹配到条目,使用生命周期检查验证装载。Lark CLI 完整 spec 类型检查仍有既有 `lark-bootstrap.connector.spec.ts` 三处旧凭据 fixture 缺少字段;本次文档适配器测试没有类型错误。
|
|
81
|
+
- 更新后的 Lark CLI 发布归档安装成功,Spaces 已刷新;授权完成后重启本地 API,实际加载并使用更新后的插件。
|
|
82
|
+
|
|
83
|
+
以下检查使用真实飞书账户、专用无敏感内容的测试文档、本地运行的 API、数据库和项目文件系统。没有使用模拟来源替代飞书读取。
|
|
84
|
+
|
|
85
|
+
| 验收项 | 实际结果 |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| 浏览器预览与导入 | 在 ChatKit Workbench 内打开导入对话框,粘贴知识库链接,查看真实文本与转换提示,勾选项目共享确认后导入;仅新增一份文档 |
|
|
88
|
+
| 文件与来源 | 文档注册为 `markdown.tiptap`;真实项目 `.md` 文件、来源文档 ID、修订号、内容哈希和原文链接均正确保留 |
|
|
89
|
+
| Workbench 导航 | 点击导入后的条目,在当前 ChatKit Workbench 内打开 Markdown 标签页 |
|
|
90
|
+
| 多成员、多 Assistant | 两个真实平台账号分别经由不同已发布 Assistant,加入同一文档协作房间;在线成员和远端光标可见 |
|
|
91
|
+
| 并发与浏览器输入 | 两个认证客户端在相同位置并发插入,双方收敛;浏览器 Tiptap 输入中文后,两端通过真实 WebSocket 收到更新,无需刷新;重复 update 被识别为幂等 |
|
|
92
|
+
| 断线重连 | 断线后远端在线状态移除;重连取得断线期间的更新,提交缓冲编辑后双方再次收敛 |
|
|
93
|
+
| 持久化 | 两个客户端、重新打开的文档会话、Markdown 读取接口与项目文件正文一致,SHA-256 与登记的文件哈希一致 |
|
|
94
|
+
| 访问边界 | 项目外账号不能预览来源、导入、读取本地文档、创建协作会话或提交编辑;未确认共享的导入返回 400 |
|
|
95
|
+
| 原文独立 | 本地协作前后,飞书原文的版本与哈希保持不变;本地协作不会修改来源元数据 |
|
|
96
|
+
| 来源变化保护 | 随后从飞书客户端手动修改专用测试原文,重新读取确认版本与哈希变化;使用旧预览发起新导入返回 409,不新增文档 |
|
|
97
|
+
| 完成请求重试 | 原文改变后重试原先已完成的导入请求,仍返回已有文档;条目数不变,本地协作后的文件哈希不变,来源仍记录导入时版本 |
|
|
98
|
+
|
|
99
|
+
协作验收组合是**一个真实浏览器 Tiptap 界面 + 两个真实账号的 API/WebSocket 客户端**,不是两个独立浏览器同时操作的测试,也不包含 AI 模型生成验收。客户端使用平台协作 SDK 和实际 Yjs 文档模型,连接真实服务。脚本位于 `apps/markdown-editor/tests/imported-collaboration.live.mjs`;私有 fixture 指向浏览器已导入的文档,不另建替代页面。运行时通过 `SPACES_TEST_DIRECTORY` 指定私有测试目录,凭据通过本机测试登录设施取得;截图、租户标识和完整回执保留在仓库外。
|
|
100
|
+
|
|
101
|
+
真实飞书纯文本 API 没有返回测试原文的公式内容,粗体也降为普通文本,与导入前展示的有损转换边界一致。飞书撤权后的实时拒绝在模拟回归中覆盖,本轮没有撤销用户刚完成的连接,故尚未完成该项真实租户验收。
|
|
102
|
+
|
|
103
|
+
## Remote View 组件规范
|
|
104
|
+
|
|
105
|
+
导入弹窗使用共享 shadcn/Radix 的 `Dialog`、`DialogHeader`、`DialogFooter`,来源选择使用 `Select`,共享确认使用 `Checkbox` 与关联的 `Label`。不使用原生 select/checkbox,也不覆盖组件的标题字号、内边距、遮罩、边框和按钮外观;这些视觉属性沿用共享组件和宿主 theme variables。插件只约束弹窗可用宽度、滚动区域和内容布局。
|
|
106
|
+
|
|
107
|
+
Spaces 与 Markdown 的远程资源只加载共享 shadcn 样式中的一次 Tailwind Preflight,插件样式仅补充 theme/utilities,避免第二次 reset 覆盖组件的默认边框规则。来源选择支持方向键、Enter 与 Escape,确认勾选支持空格键;从新建菜单打开的导入弹窗关闭后,焦点返回新建按钮。
|
|
108
|
+
|
|
109
|
+
## 后续
|
|
110
|
+
|
|
111
|
+
- 继续补充个人文档库搜索/目录导航、不同来源授权范围和真实撤权验收;富文本块转换另行实现,当前导入闭环以文本副本为准。
|
|
112
|
+
- 有损转换必须持续可见;无法表达的块保留原文或原格式引用,不以纯文本覆盖飞书原文。
|
|
113
|
+
- 发布能力单独设计目标选择、差异预览、版本条件写入及读回;远端协同继续由飞书提供。
|
|
114
|
+
- 其他平台以相同文档来源 hook 接入,Spaces 不判断供应商名称来选择业务逻辑。
|
|
115
|
+
|
|
116
|
+
接口依据:[飞书文档搜索](https://open.feishu.cn/document/server-docs/docs/drive-v1/search/document-search)、[获取文档基本信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document/get)、[获取文档纯文本内容](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document/raw_content)、[飞书官方云盘技能](https://github.com/larksuite/cli/blob/main/skills/lark-drive/SKILL.md)。
|
|
117
|
+
|
|
118
|
+
知识库节点解析接口依据:[飞书官方 CLI 节点解析说明](https://github.com/larksuite/cli/blob/main/skills/lark-wiki/references/lark-wiki-node-get.md)。
|