feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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]);
},
});

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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 };

View File

@@ -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();
}
}

View 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';