feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* project ide app 的生命周期
|
||||
*/
|
||||
import { injectable, inject } from 'inversify';
|
||||
import {
|
||||
type LifecycleContribution,
|
||||
LayoutRestorer,
|
||||
Emitter,
|
||||
} from '@coze-project-ide/framework';
|
||||
|
||||
import { WidgetEventService } from './widget-event-service';
|
||||
import { ProjectInfoService } from './project-info-service';
|
||||
import { OpenURIResourceService } from './open-url-resource-service';
|
||||
|
||||
@injectable()
|
||||
export class AppContribution implements LifecycleContribution {
|
||||
@inject(OpenURIResourceService)
|
||||
private openURIResourceService: OpenURIResourceService;
|
||||
|
||||
@inject(WidgetEventService)
|
||||
private widgetEventService: WidgetEventService;
|
||||
|
||||
@inject(LayoutRestorer)
|
||||
private layoutRestorer: LayoutRestorer;
|
||||
|
||||
@inject(ProjectInfoService)
|
||||
private projectInfoService: ProjectInfoService;
|
||||
|
||||
onStartedEmitter = new Emitter<void>();
|
||||
onStarted = this.onStartedEmitter.event;
|
||||
|
||||
// ide 初始化完成,可执行业务逻辑的时机
|
||||
onStart() {
|
||||
// 更新项目信息
|
||||
this.projectInfoService.init();
|
||||
|
||||
// // 打开 url 上携带的资源
|
||||
this.openURIResourceService.open();
|
||||
this.openURIResourceService.listen();
|
||||
// 订阅变化事件
|
||||
this.widgetEventService.listen();
|
||||
// listen layout store
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
this.layoutRestorer.listen();
|
||||
this.onStartedEmitter.fire();
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
// 销毁所有的订阅
|
||||
this.widgetEventService.dispose();
|
||||
this.openURIResourceService.dispose();
|
||||
this.onStartedEmitter.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 承载 project ide app 业务逻辑的插件
|
||||
*/
|
||||
import { type NavigateFunction } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
bindContributions,
|
||||
definePluginCreator,
|
||||
LifecycleContribution,
|
||||
LayoutRestorer,
|
||||
type PluginCreator,
|
||||
OptionsService,
|
||||
} from '@coze-project-ide/framework';
|
||||
|
||||
import { WidgetEventService } from './widget-event-service';
|
||||
import { ProjectInfoService } from './project-info-service';
|
||||
import { OpenURIResourceService } from './open-url-resource-service';
|
||||
import { LayoutRestoreService } from './layout-restore-service';
|
||||
import { AppContribution } from './app-contribution';
|
||||
|
||||
interface createAppPluginOptions {
|
||||
spaceId: string;
|
||||
projectId: string;
|
||||
version: string;
|
||||
navigate: NavigateFunction;
|
||||
}
|
||||
|
||||
export const createAppPlugin: PluginCreator<createAppPluginOptions> =
|
||||
definePluginCreator({
|
||||
onBind({ bind, rebind }, options) {
|
||||
bind(OptionsService).toConstantValue(options);
|
||||
bind(ProjectInfoService).toSelf().inSingletonScope();
|
||||
|
||||
bind(OpenURIResourceService).toSelf().inSingletonScope();
|
||||
bind(WidgetEventService).toSelf().inSingletonScope();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-expect-error
|
||||
rebind(LayoutRestorer).to(LayoutRestoreService).inSingletonScope();
|
||||
|
||||
bindContributions(bind, AppContribution, [LifecycleContribution]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 业务层数据持久化
|
||||
*/
|
||||
import { isPlainObject } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
ApplicationShell,
|
||||
WidgetManager,
|
||||
ViewRenderer,
|
||||
SIDEBAR_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
URI,
|
||||
type ReactWidget,
|
||||
type ProjectIDEWidget,
|
||||
OptionsService,
|
||||
} from '@coze-project-ide/framework';
|
||||
|
||||
import { saveLayoutData, readLayoutData } from './utils/layout-store';
|
||||
|
||||
/**
|
||||
* 被持久化的 widget 可能是普通的 widget 也可能是 project 特定的 widget
|
||||
*/
|
||||
type LayoutWidget = ReactWidget | ProjectIDEWidget;
|
||||
|
||||
interface LayoutWidgetData {
|
||||
uri: string;
|
||||
title?: string;
|
||||
iconType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是 ProjectIDEWidget
|
||||
*/
|
||||
const isProjectIDEWidget = (w: LayoutWidget): w is ProjectIDEWidget =>
|
||||
!!(w as ProjectIDEWidget).context;
|
||||
|
||||
@injectable()
|
||||
export class LayoutRestoreService {
|
||||
@inject(ApplicationShell)
|
||||
private applicationShell: ApplicationShell;
|
||||
@inject(WidgetManager)
|
||||
protected readonly widgetManager: WidgetManager;
|
||||
@inject(ViewRenderer)
|
||||
protected readonly viewRenderer: ViewRenderer;
|
||||
|
||||
@inject(OptionsService)
|
||||
private optionsService: OptionsService;
|
||||
|
||||
/**
|
||||
* 本地数据是否生效
|
||||
*/
|
||||
private _openFirstWorkflow = false;
|
||||
|
||||
/**
|
||||
* 是否启用持久化,暂时不可配置,若开发过程中出现问题,可以关闭
|
||||
* 此开关只会开关是否在初始化时恢复布局数据
|
||||
*/
|
||||
private enabled = true;
|
||||
// private enabled = false;
|
||||
|
||||
get openFirstWorkflow() {
|
||||
return this._openFirstWorkflow;
|
||||
}
|
||||
|
||||
set openFirstWorkflow(status: boolean) {
|
||||
this._openFirstWorkflow = status;
|
||||
}
|
||||
|
||||
init() {
|
||||
//
|
||||
}
|
||||
storeLayout() {
|
||||
saveLayoutData(
|
||||
this.optionsService.spaceId,
|
||||
this.optionsService.projectId,
|
||||
this.getLayoutData(),
|
||||
);
|
||||
}
|
||||
async restoreLayout() {
|
||||
// 无论是否需要持久化,这一步必须要做
|
||||
await this.addSidebarWidget();
|
||||
if (this.enabled) {
|
||||
const data = await readLayoutData(
|
||||
this.optionsService.spaceId,
|
||||
this.optionsService.projectId,
|
||||
);
|
||||
await this.setLayoutData(data || {});
|
||||
}
|
||||
}
|
||||
storeWidget() {
|
||||
//
|
||||
}
|
||||
restoreWidget() {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当前 ide 的布局数据
|
||||
*/
|
||||
getLayoutData() {
|
||||
const data: Record<string, any> = {};
|
||||
const { primarySidebar, mainPanel } = this.applicationShell;
|
||||
|
||||
/**
|
||||
* primarySidebar 数据
|
||||
* 在当前特化业务下,primarySidebar 只可能打开特定的 widget,所以这里无需存储通用的 widgets 数据
|
||||
*/
|
||||
data.primarySidebar = {
|
||||
isHidden: !!primarySidebar?.isHidden,
|
||||
};
|
||||
|
||||
const mainPanelData = mainPanel.saveLayout();
|
||||
data.mainPanel = this.widgetsStringifyBFS(mainPanelData);
|
||||
|
||||
return data;
|
||||
}
|
||||
async setLayoutData(data) {
|
||||
const { primarySidebar, mainPanel } = data || {};
|
||||
|
||||
/**
|
||||
* primarySidebar 面板初始化
|
||||
* 1. 数据不存在时,说明没有本地数据,需要默认打开
|
||||
* 2. 数据存在,且 hidden 为假,默认打开
|
||||
* 3. 其他情况不打开
|
||||
*/
|
||||
if (!primarySidebar || !primarySidebar.isHidden) {
|
||||
this.applicationShell.primarySidebar.show();
|
||||
} else {
|
||||
this.applicationShell.primarySidebar.hide();
|
||||
}
|
||||
|
||||
if (mainPanel) {
|
||||
const mainPanelData = await this.widgetsParseBFS(mainPanel);
|
||||
// 如果初始化的时候没有 widget 打开,默认打开一个。
|
||||
// widget: tab
|
||||
// children: 分屏
|
||||
const { main } = mainPanelData || {};
|
||||
if (!main?.widgets?.length && !main?.children?.length) {
|
||||
this._openFirstWorkflow = true;
|
||||
}
|
||||
this.applicationShell.mainPanel.restoreLayout(mainPanelData);
|
||||
// FlowDockPanel 需要挂载监听
|
||||
this.applicationShell.mainPanel.initWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载默认的 sidebar widget
|
||||
*/
|
||||
async addSidebarWidget() {
|
||||
const widget = await this.widgetParse({
|
||||
uri: SIDEBAR_URI.toString(),
|
||||
});
|
||||
this.applicationShell.primarySidebar.addWidget(widget);
|
||||
}
|
||||
|
||||
listen() {
|
||||
'{"primarySidebar":{"isHidden":false},"mainPanel":{"main":{"type":"tab-area","widgets":[{"uri":"coze-project:///workflow/7446703015509245996","title":"project_more_version","iconType":"0"},{"uri":"coze-project:///plugin/7446710920656896044","title":"头条新闻forautotest"},{"uri":"coze-project:///workflow/7446703015509377068","title":"wl_pro_to_pro_use_library_plug_462971","iconType":"0"},{"uri":"coze-project:///workflow/7446703015509311532","title":"wl_pro_to_library","iconType":"0"},{"uri":"coze-project:///workflow/7446703015509344300","title":"wl_pro_to_pro_use_library_plug","iconType":"0"}],"currentIndex":1}}}';
|
||||
|
||||
const listener = () => {
|
||||
this.storeLayout();
|
||||
window.removeEventListener('unload', listener);
|
||||
};
|
||||
window.addEventListener('unload', listener);
|
||||
}
|
||||
|
||||
private widgetStringify(widget: LayoutWidget) {
|
||||
if (!widget?.uri) {
|
||||
return;
|
||||
}
|
||||
const data: LayoutWidgetData = {
|
||||
uri: widget.uri.toString(),
|
||||
};
|
||||
|
||||
if (isProjectIDEWidget(widget)) {
|
||||
const sub = widget.context.widget;
|
||||
const title = sub.getTitle();
|
||||
if (title) {
|
||||
data.title = title;
|
||||
}
|
||||
const iconType = sub.getIconType();
|
||||
if (iconType) {
|
||||
data.iconType = iconType;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
private widgetsStringify(widgets: LayoutWidget[]) {
|
||||
return widgets
|
||||
.map(widget => this.widgetStringify(widget))
|
||||
.filter(str => str?.uri && str.uri !== MAIN_PANEL_DEFAULT_URI.toString());
|
||||
}
|
||||
private widgetsStringifyBFS(data: any) {
|
||||
const bfs = next => {
|
||||
if (isPlainObject(next)) {
|
||||
return Object.keys(next).reduce((acc, key) => {
|
||||
if (key === 'widgets' && Array.isArray(next[key])) {
|
||||
acc[key] = this.widgetsStringify(next[key]);
|
||||
} else {
|
||||
acc[key] = bfs(next[key]);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
} else if (Array.isArray(next)) {
|
||||
return next.map(bfs);
|
||||
}
|
||||
return next;
|
||||
};
|
||||
return bfs(data);
|
||||
}
|
||||
private async widgetParse(data) {
|
||||
const uri = new URI(data.uri);
|
||||
const factory = this.widgetManager.getFactoryFromURI(uri);
|
||||
const widget = (await this.widgetManager.getOrCreateWidgetFromURI(
|
||||
uri,
|
||||
factory,
|
||||
)) as LayoutWidget;
|
||||
if (isProjectIDEWidget(widget)) {
|
||||
const sub = widget.context.widget;
|
||||
data.title && sub.setTitle(data.title, 'normal');
|
||||
data.iconType && sub.setIconType(data.iconType);
|
||||
}
|
||||
this.viewRenderer.addReactPortal(widget);
|
||||
|
||||
this.applicationShell.track(widget);
|
||||
return widget;
|
||||
}
|
||||
private async widgetsParseBFS(data) {
|
||||
const bfs = async next => {
|
||||
if (isPlainObject(next)) {
|
||||
return await Object.keys(next).reduce(async (accPromise, key) => {
|
||||
const acc = await accPromise;
|
||||
if (key === 'widgets' && Array.isArray(next[key])) {
|
||||
acc[key] = await Promise.all(
|
||||
next[key].map(w => this.widgetParse(w)),
|
||||
);
|
||||
} else {
|
||||
acc[key] = await bfs(next[key]);
|
||||
}
|
||||
return acc;
|
||||
}, Promise.resolve({}));
|
||||
} else if (Array.isArray(next)) {
|
||||
return await Promise.all(next.map(bfs));
|
||||
}
|
||||
return next;
|
||||
};
|
||||
return await bfs(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* project ide app 初始化时打开 url 上携带的资源
|
||||
*/
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
getURIByPath,
|
||||
getResourceByPathname,
|
||||
getURIPathByPathname,
|
||||
getURIByResource,
|
||||
ProjectIDEServices,
|
||||
ViewService,
|
||||
Disposable,
|
||||
UI_BUILDER_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
DisposableCollection,
|
||||
ApplicationShell,
|
||||
type ReactWidget,
|
||||
} from '@coze-project-ide/framework';
|
||||
|
||||
@injectable()
|
||||
export class OpenURIResourceService {
|
||||
@inject(ProjectIDEServices)
|
||||
private projectIDEServices: ProjectIDEServices;
|
||||
|
||||
@inject(ViewService)
|
||||
private viewService: ViewService;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
private applicationShell: ApplicationShell;
|
||||
|
||||
private disposable = new DisposableCollection();
|
||||
|
||||
/**
|
||||
* 针对 1.直接打开;2.外部系统跳转的场景,请勿在此添加其他副作用逻辑
|
||||
*/
|
||||
open() {
|
||||
const { resourceType } = getResourceByPathname(window.location.pathname);
|
||||
// ui-builder
|
||||
if (resourceType === UI_BUILDER_URI.displayName) {
|
||||
this.openDesign();
|
||||
// 展示默认页
|
||||
this.tryOpenDefault();
|
||||
} else {
|
||||
const path = getURIPathByPathname(window.location.pathname);
|
||||
if (!path || path.startsWith(MAIN_PANEL_DEFAULT_URI.displayName)) {
|
||||
this.tryOpenDefault();
|
||||
// 路由不匹配时需要手动激活 currentWidget
|
||||
if (this.applicationShell.mainPanel.currentTitle?.owner) {
|
||||
this.applicationShell.setCurrentWidget(
|
||||
this.applicationShell.mainPanel?.currentTitle?.owner as ReactWidget,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.projectIDEServices.view.open(getURIByPath(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listen() {
|
||||
const POP_STATE_EVENT_TYPE = 'popstate';
|
||||
window.addEventListener(POP_STATE_EVENT_TYPE, this.syncPopstate);
|
||||
this.disposable.push(
|
||||
Disposable.create(() =>
|
||||
window.removeEventListener(POP_STATE_EVENT_TYPE, this.syncPopstate),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
openDevelop(resourceType: string, resourceId: string, query?: string) {
|
||||
this.projectIDEServices.view.open(
|
||||
getURIByResource(resourceType, resourceId, query),
|
||||
);
|
||||
}
|
||||
|
||||
openDesign() {
|
||||
this.projectIDEServices.view.openPanel('ui-builder');
|
||||
}
|
||||
tryOpenDefault() {
|
||||
if (this.viewService.shell.mainPanel?.tabBars?.()?.next?.()?.done) {
|
||||
this.projectIDEServices.view.openDefault();
|
||||
}
|
||||
}
|
||||
|
||||
syncPopstate = () => {
|
||||
this.open();
|
||||
};
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import { type ProjectFormValues } from '@coze-studio/project-entity-adapter';
|
||||
import {
|
||||
Emitter,
|
||||
type Event,
|
||||
ModalService,
|
||||
OptionsService,
|
||||
ModalType,
|
||||
ErrorService,
|
||||
} from '@coze-project-ide/framework';
|
||||
import {
|
||||
IntelligenceType,
|
||||
type IntelligenceBasicInfo,
|
||||
type IntelligencePublishInfo,
|
||||
type User,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import {
|
||||
BehaviorType,
|
||||
SpaceResourceType,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
import {
|
||||
PlaygroundApi,
|
||||
PluginDevelopApi,
|
||||
intelligenceApi,
|
||||
} from '@coze-arch/bot-api';
|
||||
|
||||
@injectable()
|
||||
export class ProjectInfoService {
|
||||
@inject(OptionsService)
|
||||
private optionsService: OptionsService;
|
||||
|
||||
@inject(ModalService)
|
||||
private modalService: ModalService;
|
||||
|
||||
@inject(ErrorService)
|
||||
private errorService: ErrorService;
|
||||
|
||||
public projectInfo?: {
|
||||
projectInfo?: IntelligenceBasicInfo;
|
||||
publishInfo?: IntelligencePublishInfo;
|
||||
ownerInfo?: User;
|
||||
};
|
||||
|
||||
public initialValue: ProjectFormValues;
|
||||
|
||||
private readonly onProjectInfoUpdatedEmitter = new Emitter<void>();
|
||||
readonly onProjectInfoUpdated: Event<void> =
|
||||
this.onProjectInfoUpdatedEmitter.event;
|
||||
|
||||
init() {
|
||||
this.updateProjectInfo().catch(() => {
|
||||
// project 信息接口报错跳转到兜底页
|
||||
this.errorService.toErrorPage();
|
||||
});
|
||||
if (!IS_OPEN_SOURCE) {
|
||||
this.wakeUpPlugin();
|
||||
}
|
||||
this.initTaskList();
|
||||
this.reportUserBehavior();
|
||||
}
|
||||
|
||||
async initTaskList() {
|
||||
const res = await intelligenceApi.DraftProjectInnerTaskList({
|
||||
project_id: this.optionsService.projectId,
|
||||
});
|
||||
// 和后端确认,默认 task_list 长度为 1.
|
||||
// 如果有长度为 2 没有都住的场景,用户刷新后也可以获取到下一个。
|
||||
const { task_list } = res.data || {};
|
||||
const taskId = task_list?.[0]?.task_id;
|
||||
if (taskId) {
|
||||
// 请求轮询接口获取基础信息
|
||||
const { task_detail } = await PluginDevelopApi.ResourceCopyDetail({
|
||||
task_id: taskId,
|
||||
});
|
||||
this.modalService.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.RESOURCE,
|
||||
scene: task_detail?.scene,
|
||||
resourceName: task_detail?.res_name,
|
||||
});
|
||||
this.modalService.doPolling(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开 project 页面需要上报,后端才能筛选出最近打开
|
||||
*/
|
||||
reportUserBehavior() {
|
||||
PlaygroundApi.ReportUserBehavior({
|
||||
space_id: this.optionsService.spaceId,
|
||||
behavior_type: BehaviorType.Visit,
|
||||
resource_id: this.optionsService.projectId,
|
||||
resource_type: SpaceResourceType.Project,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 单向请求接口
|
||||
* 提前唤醒 ide 插件,无需消费返回值
|
||||
*/
|
||||
wakeUpPlugin() {
|
||||
PluginDevelopApi.WakeupIdePlugin({
|
||||
space_id: this.optionsService.spaceId,
|
||||
project_id: this.optionsService.projectId,
|
||||
dev_id: userStoreService.getUserInfo()?.user_id_str,
|
||||
});
|
||||
}
|
||||
|
||||
async updateProjectInfo() {
|
||||
const res = await intelligenceApi.GetDraftIntelligenceInfo({
|
||||
intelligence_id: this.optionsService.projectId,
|
||||
intelligence_type: IntelligenceType.Project,
|
||||
version: this.optionsService.version || undefined,
|
||||
});
|
||||
this.projectInfo = {
|
||||
projectInfo: res.data?.basic_info,
|
||||
publishInfo: res.data?.publish_info,
|
||||
ownerInfo: res.data?.owner_info,
|
||||
};
|
||||
this.initialValue = {
|
||||
space_id: this.optionsService?.spaceId,
|
||||
project_id: this.optionsService?.projectId,
|
||||
name: this.projectInfo?.projectInfo?.name,
|
||||
description: this.projectInfo?.projectInfo?.description,
|
||||
icon_uri: [
|
||||
{
|
||||
uid: this.projectInfo?.projectInfo?.icon_uri || '',
|
||||
url: this.projectInfo?.projectInfo?.icon_url || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
this.onProjectInfoUpdatedEmitter.fire();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Dexie, type EntityTable } from 'dexie';
|
||||
|
||||
/**
|
||||
* 布局数据
|
||||
*/
|
||||
interface DBLayoutRow {
|
||||
/**
|
||||
* 自增 id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
spaceId: string;
|
||||
/**
|
||||
* 项目 id
|
||||
*/
|
||||
projectId: string;
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
timestamp: number;
|
||||
/**
|
||||
* 数据版本
|
||||
*/
|
||||
version: number;
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
data: string;
|
||||
}
|
||||
|
||||
type DBLayout = Dexie & {
|
||||
layout: EntityTable<DBLayoutRow, 'id'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 持久化储存形式的版本号
|
||||
*/
|
||||
const VERSION = 3;
|
||||
|
||||
/**
|
||||
* 数据库名称
|
||||
*/
|
||||
const DB_NAME = 'CozProjectIDELayoutData';
|
||||
/**
|
||||
* 数据库版本
|
||||
*/
|
||||
const DB_VERSION = 1;
|
||||
/**
|
||||
* 数据有效期
|
||||
*/
|
||||
const DB_EXPIRE = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
let cache: DBLayout | undefined;
|
||||
|
||||
const isExpired = (row: DBLayoutRow) =>
|
||||
row.timestamp < Date.now() - DB_EXPIRE || row.version !== VERSION;
|
||||
|
||||
/**
|
||||
* 获取数据库实例
|
||||
*/
|
||||
const getDB = () => {
|
||||
if (!cache) {
|
||||
cache = new Dexie(DB_NAME) as DBLayout;
|
||||
cache.version(DB_VERSION).stores({
|
||||
layout: '++id, spaceId, projectId, timestamp, data',
|
||||
});
|
||||
}
|
||||
return cache;
|
||||
};
|
||||
|
||||
const setDataDB = async (
|
||||
spaceId: string,
|
||||
projectId: string,
|
||||
row: Omit<DBLayoutRow, 'id' | 'projectId' | 'spaceId'>,
|
||||
) => {
|
||||
const db = getDB();
|
||||
const record = await db.layout.where({ spaceId, projectId }).first();
|
||||
if (record) {
|
||||
await db.layout.update(record.id, {
|
||||
...row,
|
||||
});
|
||||
} else {
|
||||
await db.layout.add({
|
||||
spaceId,
|
||||
projectId,
|
||||
...row,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDataDB = async (spaceId: string, projectId: string) => {
|
||||
const db = getDB();
|
||||
const record = await db.layout.where({ spaceId, projectId }).first();
|
||||
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
if (isExpired(record)) {
|
||||
await db.layout.where({ id: record.id }).delete();
|
||||
return undefined;
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_KEY_PREFIX = 'coz-project-ide-layout-data';
|
||||
|
||||
const setDataLS = (
|
||||
spaceId: string,
|
||||
projectId: string,
|
||||
row: Omit<DBLayoutRow, 'id' | 'projectId' | 'spaceId'>,
|
||||
) => {
|
||||
const key = `${LOCAL_STORAGE_KEY_PREFIX}-${spaceId}-${projectId}`;
|
||||
window.localStorage.setItem(key, JSON.stringify(row));
|
||||
};
|
||||
|
||||
const getDataLS = (spaceId: string, projectId: string) => {
|
||||
const key = `${LOCAL_STORAGE_KEY_PREFIX}-${spaceId}-${projectId}`;
|
||||
const str = window.localStorage.getItem(key);
|
||||
if (!str) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(str);
|
||||
if (isExpired(data)) {
|
||||
window.localStorage.removeItem(key);
|
||||
return undefined;
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const deleteDataLS = (spaceId: string, projectId: string) => {
|
||||
const key = `${LOCAL_STORAGE_KEY_PREFIX}-${spaceId}-${projectId}`;
|
||||
window.localStorage.removeItem(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存布局数据
|
||||
* 注:调用时机为组件销毁或浏览器关闭时,故不可用异步函数
|
||||
*/
|
||||
const saveLayoutData = (spaceId: string, projectId: string, data: any) => {
|
||||
try {
|
||||
// 无论是什么值都需要序列化成字符串
|
||||
const str = JSON.stringify(data);
|
||||
const row = {
|
||||
data: str,
|
||||
timestamp: Number(Date.now()),
|
||||
version: VERSION,
|
||||
};
|
||||
setDataLS(spaceId, projectId, row);
|
||||
setDataDB(spaceId, projectId, row);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取布局数据
|
||||
* 会同时从 indexedDB 和 localStorage 中读取数据,会有以下几种情况:
|
||||
* 1. localStorage 无数据,返回 indexedDB 数据
|
||||
* 2. localStorage 有数据
|
||||
* 2.1. indexedDB 无数据,更新 indexedDB 数据,删除 localStorage 数据,返回 indexedDB 数据
|
||||
* 2.2. indexedDB 有数据,比较时间戳。返回最近的数据,删除 localStorage 数据
|
||||
* 2.2.1. 若 localStorage 数据较新,则更新到 indexedDB 中
|
||||
*/
|
||||
const readLayoutData = async (spaceId: string, projectId: string) => {
|
||||
let str;
|
||||
const recordDB = await getDataDB(spaceId, projectId);
|
||||
const recordLS = getDataLS(spaceId, projectId);
|
||||
if (!recordLS) {
|
||||
str = recordDB?.data;
|
||||
} else if (!recordDB || recordDB.timestamp < recordLS.timestamp) {
|
||||
await setDataDB(spaceId, projectId, recordLS);
|
||||
deleteDataLS(spaceId, projectId);
|
||||
str = recordLS.data;
|
||||
} else {
|
||||
deleteDataLS(spaceId, projectId);
|
||||
str = recordDB.data;
|
||||
}
|
||||
if (!str) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export { saveLayoutData, readLayoutData };
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 监听 widget 事件而需要执行的业务逻辑
|
||||
*/
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
ProjectIDEServices,
|
||||
ViewService,
|
||||
WidgetManager,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
DisposableCollection,
|
||||
getPathnameByURI,
|
||||
type ReactWidget,
|
||||
OptionsService,
|
||||
compareURI,
|
||||
addPreservedSearchParams,
|
||||
} from '@coze-project-ide/framework';
|
||||
|
||||
@injectable()
|
||||
export class WidgetEventService {
|
||||
@inject(ViewService)
|
||||
private viewService: ViewService;
|
||||
|
||||
@inject(WidgetManager)
|
||||
private widgetManager: WidgetManager;
|
||||
|
||||
@inject(ProjectIDEServices)
|
||||
private projectIDEServices: ProjectIDEServices;
|
||||
|
||||
@inject(OptionsService)
|
||||
private optionsService: OptionsService;
|
||||
|
||||
private disposable = new DisposableCollection();
|
||||
|
||||
listen() {
|
||||
// listen current widget change
|
||||
this.disposable.push(
|
||||
this.viewService.shell.onCurrentWidgetChange(widget => {
|
||||
this.toggleDefaultWidget(widget);
|
||||
this.syncURL(widget);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 有 widget 打开时需要关闭默认页
|
||||
* 2. 关闭所有 widget 时需要打开默认页
|
||||
*/
|
||||
toggleDefaultWidget(widget) {
|
||||
if ((widget as ReactWidget)?.uri) {
|
||||
const widgetUri = widget?.uri;
|
||||
if (widgetUri.displayName !== 'default') {
|
||||
// 关闭默认的 widget
|
||||
const defaultWidget = this.widgetManager.getWidgetFromURI(
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
);
|
||||
defaultWidget?.dispose?.();
|
||||
}
|
||||
} else {
|
||||
this.viewService.disableFullScreenMode();
|
||||
this.projectIDEServices.view.openDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步切换资源 tab 时的 url 变化
|
||||
*/
|
||||
syncURL(widget) {
|
||||
if (widget) {
|
||||
const widgetUri = widget?.uri;
|
||||
// 默认页无需同步 url
|
||||
if (compareURI(widgetUri, MAIN_PANEL_DEFAULT_URI)) {
|
||||
return;
|
||||
}
|
||||
if (widgetUri) {
|
||||
const path = getPathnameByURI(widgetUri);
|
||||
if (path) {
|
||||
let url = `/space/${this.optionsService.spaceId}/project-ide/${this.optionsService.projectId}${path}`;
|
||||
if (widgetUri.query) {
|
||||
url += `?${widgetUri.query}`;
|
||||
}
|
||||
this.navigate(url);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.navigate(
|
||||
`/space/${this.optionsService.spaceId}/project-ide/${this.optionsService.projectId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
diffPath(next: string) {
|
||||
const { pathname, search } = window.location;
|
||||
return pathname + search !== next;
|
||||
}
|
||||
|
||||
navigate(url: string) {
|
||||
if (!this.diffPath(url)) {
|
||||
return;
|
||||
}
|
||||
this.optionsService.navigate(addPreservedSearchParams(url));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
17
frontend/packages/project-ide/main/src/plugins/index.ts
Normal file
17
frontend/packages/project-ide/main/src/plugins/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createAppPlugin } from './create-app-plugin';
|
||||
Reference in New Issue
Block a user