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,121 @@
/*
* 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 { Disposable, DisposableCollection, Emitter } from '@flowgram-adapter/common';
import { type URI } from '../common';
export interface HistoryState {
/**
* uri.toString()
*/
uri?: string;
/**
* 在栈中的索引
* react-router 使用「idx」使用「fIdx」防止串数据
*/
fIdx?: number;
}
const POP_STATE_EVENT_TYPE = 'popstate';
const uriToUrl = (uri: URI) => {
let url = uri.path.toString();
const { query } = uri;
if (query) {
url += `?${query}`;
}
const hash = uri.fragment;
if (hash) {
url += '#hash';
}
return url;
};
export class BrowserHistory implements Disposable {
private window = document.defaultView || window;
private history = this.window.history;
private onChangeEmitter = new Emitter<HistoryState>();
onChange = this.onChangeEmitter.event;
private disposable = new DisposableCollection(this.onChangeEmitter);
private get state() {
return this.history.state;
}
private get idx() {
return this.state?.fIdx || null;
}
private listener = (e: any) => {
this.onChangeEmitter.fire((e.state || {}) as HistoryState);
};
init() {
/**
* 初始化的时候 index 必然为 null
* 如果不是,说明有别的框架或者在直接使用 history.pushState 或者 history.replaceState 污染
*/
if (this.idx === null) {
this.history.replaceState({ ...this.state, fIdx: -1, uri: '' }, '');
}
this.window.addEventListener(POP_STATE_EVENT_TYPE, this.listener);
this.disposable.push(
Disposable.create(() =>
this.window.removeEventListener(POP_STATE_EVENT_TYPE, this.listener),
),
);
}
push(to: URI, idx = this.idx + 1) {
const state = {
uri: to.toString(),
fIdx: idx,
};
const url = uriToUrl(to);
try {
this.history.pushState(state, '', url);
} catch {
/**
* history 还是有可能因为 state 或者浏览器等原因挂掉的
* 降级成直接跳转
*/
this.window.location.assign(url);
}
}
replace(to: URI, idx = this.idx) {
const state = {
uri: toString(),
fIdx: idx,
};
const url = uriToUrl(to);
this.history.replaceState(state, '', url);
}
go(n: number) {
return this.history.go(n);
}
dispose() {
this.disposable.dispose();
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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 { bindContributions } from '@flowgram-adapter/common';
import { ShortcutsContribution } from '../shortcut/shortcuts-service';
import { definePluginCreator, LifecycleContribution } from '../common';
import { CommandContribution } from '../command';
import { NavigationService } from './navigation-service';
import { NavigationHistory } from './navigation-history';
import { NavigationContribution } from './navigation-contribution';
export interface NavigationPluginOptions {
uriScheme?: string;
}
export const createNavigationPlugin =
definePluginCreator<NavigationPluginOptions>({
onBind: ({ bind }) => {
bind(NavigationHistory).toSelf().inSingletonScope();
bind(NavigationService).toSelf().inSingletonScope();
bindContributions(bind, NavigationContribution, [
LifecycleContribution,
CommandContribution,
ShortcutsContribution,
]);
},
onInit(ctx, opts) {
if (opts.uriScheme) {
ctx.container.get(NavigationService).setScheme(opts.uriScheme);
}
},
});

View File

@@ -0,0 +1,22 @@
/*
* 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 { NavigationService } from './navigation-service';
export { NavigationHistory } from './navigation-history';
export {
createNavigationPlugin,
type NavigationPluginOptions,
} from './create-navigation-plugin';

View File

@@ -0,0 +1,110 @@
/*
* 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 { DisposableCollection, addEventListener } from '@flowgram-adapter/common';
import {
type ShortcutsContribution,
type ShortcutsRegistry,
} from '../shortcut/shortcuts-service';
import { type LifecycleContribution } from '../common';
import { type CommandContribution, type CommandRegistry } from '../command';
import { NavigationService } from './navigation-service';
@injectable()
class NavigationContribution
implements LifecycleContribution, CommandContribution, ShortcutsContribution
{
@inject(NavigationService)
protected readonly navigationService: NavigationService;
private readonly _toDispose = new DisposableCollection();
onLayoutInit() {
// this.registerMouseNavigationListener();
}
onStart() {
this.navigationService.init();
}
onDispose(): void {
this.navigationService.dispose();
this._toDispose.dispose();
}
registerCommands(registry: CommandRegistry) {
registry.registerCommand(
{
id: 'navigation.forward',
label: 'Forward',
},
{
execute: () => {
this.navigationService.forward();
},
isEnabled: () => this.navigationService.canGoForward(),
},
);
registry.registerCommand(
{
id: 'navigation.back',
label: 'Backward',
},
{
execute: () => {
this.navigationService.back();
},
isEnabled: () => this.navigationService.canGoBack(),
},
);
}
registerShortcuts(registry: ShortcutsRegistry) {
registry.registerHandlers(
{
keybinding: 'control shift -',
commandId: 'navigation.forward',
preventDefault: true,
},
{
keybinding: 'control -',
commandId: 'navigation.back',
preventDefault: true,
},
);
}
registerMouseNavigationListener() {
this._toDispose.push(
addEventListener(document.body, 'mousedown', (e: MouseEvent) => {
switch (e.button) {
case 3:
this.navigationService.back();
break;
case 4:
this.navigationService.forward();
break;
default:
break;
}
}),
);
}
}
export { NavigationContribution };

View File

@@ -0,0 +1,154 @@
/*
* 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 { injectable } from 'inversify';
import {
isNumber,
Emitter,
logger,
DisposableCollection,
} from '@flowgram-adapter/common';
import { type URI } from '../common';
import { BrowserHistory, type HistoryState } from './browser-history';
/** location 结构 */
interface Location {
/** 唯一标识,一般来说是 uri */
uri: URI;
/** 预留字段 */
}
@injectable()
class NavigationHistory {
private stack: Location[] = [];
private idx = -1;
private history = new BrowserHistory();
private onChangeEmitter = new Emitter<Location>();
private onPopstateEmitter = new Emitter<Location>();
onDidHistoryChange = this.onChangeEmitter.event;
onPopstate = this.onPopstateEmitter.event;
private disposable = new DisposableCollection(
this.history,
this.onChangeEmitter,
this.onPopstateEmitter,
);
get location() {
return this.stack[this.idx];
}
init() {
this.history.init();
this.disposable.push(this.history.onChange(this.listener));
}
dispose() {
this.disposable.dispose();
}
pushOrReplace(location: Location, replace = false) {
// 如果处于回退状态,则清除之后所有的历史
if (this.stack.length > this.idx + 1) {
this.stack = this.stack.slice(0, this.idx + 1);
}
return replace ? this.replace(location) : this.push(location);
}
push(location: Location) {
logger.log('navigation history push');
if (this.similar(location, this.location)) {
logger.log('location is similar');
return;
}
this.stack.push(location);
this.idx = this.stack.length - 1;
this.history.push(location.uri, this.idx);
this.onChangeEmitter.fire(location);
}
replace(location: Location) {
logger.log('navigation history replace');
this.stack.splice(this.idx + 1, 0, location);
this.idx = this.stack.length - 1;
this.history.replace(location.uri, this.idx);
this.onChangeEmitter.fire(location);
}
private go(delta: number) {
const next = this.idx + delta;
const nextLocation = this.stack[next];
// 越界按照无效处理
if (next >= this.stack.length || next < 0 || !nextLocation) {
return;
}
this.idx = next;
window.history.go(delta);
this.onChangeEmitter.fire(nextLocation);
return this.stack[this.idx];
}
back() {
return this.go(-1);
}
canGoBack() {
return this.idx >= 1;
}
forward() {
return this.go(1);
}
canGoForward() {
return this.idx >= 0 && this.idx !== this.stack.length - 1;
}
private listener = (state: HistoryState) => {
/** 无法正确识别 state 时不做任何处理 */
if (!state || !isNumber(state.fIdx) || !state.uri) {
return;
}
const { fIdx: idx } = state;
/** 索引越界 */
if (idx >= this.stack.length || idx < 0) {
return;
}
this.idx = idx;
this.onChangeEmitter.fire(this.location);
this.onPopstateEmitter.fire(this.location);
};
private similar(
left: Location | undefined,
right: Location | undefined,
): boolean {
if (left === undefined || right === undefined) {
return left === right;
}
return left.uri.toString() === right.uri.toString();
}
}
export { NavigationHistory, type Location };

View File

@@ -0,0 +1,95 @@
/*
* 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 { DisposableCollection } from '@flowgram-adapter/common';
import { URI, OpenerService } from '../common';
import { NavigationHistory } from './navigation-history';
@injectable()
class NavigationService {
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(NavigationHistory)
protected readonly history: NavigationHistory;
private disposable = new DisposableCollection();
scheme = 'flowide';
init() {
this.history.init();
this.disposable.pushAll([
this.history,
this.history.onPopstate(e => {
this.openerService.open(e.uri);
}),
]);
}
public async goto(uri: URI | string, replace = false, options?: any) {
let gotoUri: URI;
if (typeof uri === 'string') {
gotoUri = new URI(`${this.scheme}://${uri}`);
} else {
gotoUri = uri;
}
this.history.pushOrReplace({ uri: gotoUri }, replace);
await this.openerService.open(gotoUri, options);
}
public async back() {
const location = this.history.back();
if (location) {
await this.openerService.open(location.uri);
}
}
public async forward() {
const location = this.history.forward();
if (location) {
await this.openerService.open(location.uri);
}
}
public canGoBack() {
return this.history.canGoBack();
}
public canGoForward() {
return this.history.canGoForward();
}
get uri() {
return this.history.location?.uri;
}
get onDidHistoryChange() {
return this.history.onDidHistoryChange;
}
setScheme(scheme: string) {
this.scheme = scheme;
}
dispose() {
this.disposable.dispose();
}
}
export { NavigationService };