├── src ├── share.ts ├── structure.ts ├── static.ts ├── vendor.ts ├── service.ts ├── data.ts ├── index.ts ├── plugin.ts ├── console.ts └── transpiler.ts ├── client ├── api │ ├── toolbox.ts │ ├── workspace.ts │ └── manager.ts ├── blockly │ ├── template │ │ ├── plugin_id.js │ │ ├── logger_initialize.js │ │ ├── key_value_initialize.js │ │ └── index.ts │ ├── template.js.tpl │ ├── listeners │ │ ├── auto-save.ts │ │ └── consumer.ts │ ├── extensions │ │ ├── index.ts │ │ ├── scope.ts │ │ ├── parameter.ts │ │ ├── segment.ts │ │ └── type.ts │ ├── vendor.ts │ ├── blocks │ │ ├── number.ts │ │ ├── logic.ts │ │ ├── debugging.ts │ │ ├── index.ts │ │ ├── message.ts │ │ ├── environment.ts │ │ ├── bot.ts │ │ ├── text.ts │ │ ├── session.ts │ │ ├── parameter.ts │ │ ├── segment.ts │ │ ├── data.ts │ │ ├── typing.ts │ │ └── event.ts │ ├── definer.ts │ ├── binding.ts │ ├── typing │ │ ├── converter.ts │ │ ├── overwritting.ts │ │ └── index.ts │ ├── build.ts │ ├── pack.ts │ ├── fields │ │ └── binding.ts │ ├── blockly.vue │ ├── plugins │ │ ├── scope.ts │ │ └── type.ts │ ├── msg │ │ └── zh.ts │ └── toolbox.xml ├── types.d.ts ├── version.ts ├── utils.ts ├── tsconfig.json ├── icons │ ├── new-file.vue │ ├── window.vue │ ├── activity.vue │ ├── import.vue │ ├── template.ts │ └── typings.ts ├── flow-engine │ ├── nodes │ │ ├── numeric.ts │ │ ├── index.ts │ │ ├── structure.ts │ │ ├── io.ts │ │ ├── string.ts │ │ ├── interface.ts │ │ ├── logical.ts │ │ └── object.ts │ └── data-flow.vue ├── index.ts ├── components │ ├── dialogs │ │ ├── export.vue │ │ ├── import.vue │ │ ├── author.vue │ │ └── text-template.vue │ ├── console │ │ ├── code.vue │ │ └── build.vue │ ├── blockly-tab-group.vue │ ├── blockly-tab-item.vue │ ├── variable.vue │ └── sidebar │ │ └── index.vue ├── index.scss ├── meta.vue └── index.vue ├── docs ├── usage.md ├── develop │ └── index.md ├── reference │ ├── index.md │ └── block │ │ ├── array.md │ │ ├── bot.md │ │ ├── data.md │ │ ├── debug.md │ │ ├── event.md │ │ ├── loop.md │ │ ├── math.md │ │ ├── element.md │ │ ├── logical.md │ │ ├── message.md │ │ ├── session.md │ │ ├── string.md │ │ └── variable.md ├── examples │ ├── image.md │ ├── platform.md │ ├── argument.md │ ├── mention.md │ ├── weather.md │ └── hello-world.md ├── .vitepress │ ├── theme │ │ └── index.ts │ └── config.ts ├── index.md └── starter.md ├── vercel.json ├── media ├── 1x1.gif ├── click.mp3 ├── click.ogg ├── click.wav ├── delete.mp3 ├── delete.ogg ├── delete.wav ├── quote0.png ├── quote1.png ├── handopen.cur ├── pilcrow.png ├── sprites.png ├── disconnect.mp3 ├── disconnect.ogg ├── disconnect.wav ├── handclosed.cur ├── handdelete.cur ├── dropdown-arrow.svg └── sprites.svg ├── tsconfig.json ├── tests ├── blocks.ts └── scope.spec.ts ├── canary.js ├── readme.md ├── .gitignore └── package.json /src/share.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/api/toolbox.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # 用法 2 | -------------------------------------------------------------------------------- /docs/develop/index.md: -------------------------------------------------------------------------------- 1 | # 扩展开发 2 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # 参考 2 | -------------------------------------------------------------------------------- /docs/examples/image.md: -------------------------------------------------------------------------------- 1 | # 发送图片 2 | 3 | -------------------------------------------------------------------------------- /docs/examples/platform.md: -------------------------------------------------------------------------------- 1 | # 平台功能 2 | -------------------------------------------------------------------------------- /docs/reference/block/array.md: -------------------------------------------------------------------------------- 1 | # 列表 2 | -------------------------------------------------------------------------------- /docs/reference/block/bot.md: -------------------------------------------------------------------------------- 1 | # 操作 2 | -------------------------------------------------------------------------------- /docs/reference/block/data.md: -------------------------------------------------------------------------------- 1 | # 数据 2 | -------------------------------------------------------------------------------- /docs/reference/block/debug.md: -------------------------------------------------------------------------------- 1 | # 调试 2 | -------------------------------------------------------------------------------- /docs/reference/block/event.md: -------------------------------------------------------------------------------- 1 | # 事件 2 | -------------------------------------------------------------------------------- /docs/reference/block/loop.md: -------------------------------------------------------------------------------- 1 | # 循环 2 | -------------------------------------------------------------------------------- /docs/reference/block/math.md: -------------------------------------------------------------------------------- 1 | # 数学 2 | -------------------------------------------------------------------------------- /docs/examples/argument.md: -------------------------------------------------------------------------------- 1 | # 指令参数 2 | 3 | -------------------------------------------------------------------------------- /docs/examples/mention.md: -------------------------------------------------------------------------------- 1 | # at 人功能 2 | 3 | -------------------------------------------------------------------------------- /docs/examples/weather.md: -------------------------------------------------------------------------------- 1 | # 查询天气 2 | 3 | -------------------------------------------------------------------------------- /docs/reference/block/element.md: -------------------------------------------------------------------------------- 1 | # 元素 2 | -------------------------------------------------------------------------------- /docs/reference/block/logical.md: -------------------------------------------------------------------------------- 1 | # 逻辑 2 | -------------------------------------------------------------------------------- /docs/reference/block/message.md: -------------------------------------------------------------------------------- 1 | # 消息 2 | -------------------------------------------------------------------------------- /docs/reference/block/session.md: -------------------------------------------------------------------------------- 1 | # 会话 2 | -------------------------------------------------------------------------------- /docs/reference/block/string.md: -------------------------------------------------------------------------------- 1 | # 文本 2 | -------------------------------------------------------------------------------- /docs/reference/block/variable.md: -------------------------------------------------------------------------------- 1 | # 变量 2 | -------------------------------------------------------------------------------- /docs/examples/hello-world.md: -------------------------------------------------------------------------------- 1 | # 你好,世界 2 | 3 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/blockly/template/plugin_id.js: -------------------------------------------------------------------------------- 1 | const plugin_id = "{{plugin_id}}" 2 | -------------------------------------------------------------------------------- /client/blockly/template/logger_initialize.js: -------------------------------------------------------------------------------- 1 | const logger = ctx.logger("{{name}}") 2 | -------------------------------------------------------------------------------- /media/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/1x1.gif -------------------------------------------------------------------------------- /media/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/click.mp3 -------------------------------------------------------------------------------- /media/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/click.ogg -------------------------------------------------------------------------------- /media/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/click.wav -------------------------------------------------------------------------------- /media/delete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/delete.mp3 -------------------------------------------------------------------------------- /media/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/delete.ogg -------------------------------------------------------------------------------- /media/delete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/delete.wav -------------------------------------------------------------------------------- /media/quote0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/quote0.png -------------------------------------------------------------------------------- /media/quote1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/quote1.png -------------------------------------------------------------------------------- /media/handopen.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/handopen.cur -------------------------------------------------------------------------------- /media/pilcrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/pilcrow.png -------------------------------------------------------------------------------- /media/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/sprites.png -------------------------------------------------------------------------------- /client/api/workspace.ts: -------------------------------------------------------------------------------- 1 | import {WorkspaceSvg} from "blockly"; 2 | import {send} from "@koishijs/client"; 3 | -------------------------------------------------------------------------------- /media/disconnect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/disconnect.mp3 -------------------------------------------------------------------------------- /media/disconnect.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/disconnect.ogg -------------------------------------------------------------------------------- /media/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/disconnect.wav -------------------------------------------------------------------------------- /media/handclosed.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/handclosed.cur -------------------------------------------------------------------------------- /media/handdelete.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koishijs/koishi-plugin-blockly/HEAD/media/handdelete.cur -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from '@koishijs/vitepress/client' 2 | 3 | export default defineTheme() 4 | -------------------------------------------------------------------------------- /client/types.d.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from "blockly" 2 | declare module "blockly"{ 3 | interface Workspace{ 4 | meta:any 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/blockly/template.js.tpl: -------------------------------------------------------------------------------- 1 | export const name = "{{name}}" 2 | export const using = {{using}} 3 | export async function apply(ctx){ 4 | {{apply}} 5 | } 6 | -------------------------------------------------------------------------------- /client/blockly/template/key_value_initialize.js: -------------------------------------------------------------------------------- 1 | ctx.database.extend("blockly_key_value",{ 2 | key: "string", 3 | value: "string", 4 | },{ 5 | primary: "key" 6 | }) 7 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # koishi-plugin-blockly 2 | 3 | koishi-plugin-blockly 是一个基于 [Blockly](https://developers.google.com/blockly) 的可视化编程插件。它可以让你在没有任何编程基础的情况下,通过拖拽积木的方式编写 Koishi 插件。 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/version.ts: -------------------------------------------------------------------------------- 1 | import PackageData from '../package.json?raw' 2 | const parsed_data = JSON.parse(PackageData) 3 | export const BLOCKLY_API_VERSION = 1 4 | export const BLOCKLY_VERSION = parsed_data.version ?? '0.0.0' 5 | -------------------------------------------------------------------------------- /client/utils.ts: -------------------------------------------------------------------------------- 1 | export function stringToArrayBuffer (str) { 2 | let array = new Uint8Array(str.length); 3 | for(let i = 0; i < str.length; i++) { 4 | array[i] = str.charCodeAt(i); 5 | } 6 | return array.buffer 7 | } 8 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "types": [ 8 | "@koishijs/client/global" 9 | ] 10 | }, 11 | "include": [ 12 | "." 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/blockly/template/index.ts: -------------------------------------------------------------------------------- 1 | import KeyValueInitialize from './key_value_initialize?raw' 2 | import LoggerInitialize from './logger_initialize?raw' 3 | import PluginId from './plugin_id?raw' 4 | 5 | export const TemplateCodes = { 6 | 'key_value_initialize':KeyValueInitialize, 7 | 'logger_initialize':LoggerInitialize, 8 | 'plugin_id':PluginId 9 | } 10 | -------------------------------------------------------------------------------- /client/icons/new-file.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/structure.ts: -------------------------------------------------------------------------------- 1 | export interface BlocklyDocument{ 2 | id:number 3 | uuid:string 4 | name:string 5 | body:string 6 | code:string 7 | enabled:boolean 8 | edited:boolean 9 | } 10 | 11 | declare module "koishi"{ 12 | interface Tables{ 13 | blockly:BlocklyDocument 14 | } 15 | } 16 | 17 | export interface BlocklyMenuItem{ 18 | id:number 19 | name:string 20 | enabled:boolean 21 | edited:boolean 22 | uuid:string 23 | } 24 | 25 | -------------------------------------------------------------------------------- /client/blockly/listeners/auto-save.ts: -------------------------------------------------------------------------------- 1 | import {Abstract} from "blockly/core/events/events_abstract"; 2 | import {BlockMove} from "blockly/core/events/events_block_move"; 3 | import {BlockCreate} from "blockly/core/events/events_block_create"; 4 | 5 | export function autoSaveListener(this:{autoSave:Function},_event:Abstract){ 6 | if (!(_event.type === 'create' || _event.type === 'move' || _event.type === 'delete' || _event.type === 'change'))return; 7 | this.autoSave(); 8 | } 9 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/numeric.ts: -------------------------------------------------------------------------------- 1 | import {TextInputInterface} from "baklavajs"; 2 | import {defineNode, NodeInterface} from "@baklavajs/core"; 3 | 4 | export const ConstNumber = defineNode({ 5 | type: "数字常量/转为数字", 6 | inputs: { 7 | value: () => new TextInputInterface("输入", "0"), 8 | }, 9 | outputs: { 10 | value: () => new NodeInterface("输出", "0") 11 | } 12 | }) 13 | 14 | export const NumericNodes = [ 15 | ConstNumber 16 | ].map((node) => [node,"数字"]); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "target": "es2020", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "composite": true, 9 | "incremental": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "jsx": "react-jsx", 14 | "jsxImportSource": "@satorijs/element", 15 | }, 16 | "include": [ 17 | "src", 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import {} from 'koishi-plugin-blockly' 2 | import { Context, icons } from '@koishijs/client' 3 | import Page from './index.vue' 4 | import Activity from './icons/activity.vue' 5 | 6 | icons.register('blockly', Activity) 7 | 8 | import './index.scss' 9 | 10 | export default (ctx: Context) => { 11 | ctx.page({ 12 | name: 'Blockly 可视化编程', 13 | path: '/blockly', 14 | icon: 'blockly', 15 | authority: 5, 16 | component: Page, 17 | fields:['blockly'] 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/index.ts: -------------------------------------------------------------------------------- 1 | import {IOBlocks} from "./io"; 2 | import {ObjectNodes} from "./object"; 3 | import {LogicalNodes} from "./logical"; 4 | import {StringNode} from "./string"; 5 | import {NumericNodes} from "./numeric"; 6 | import {Structure} from "./structure"; 7 | import {InterfaceNodes} from "./interface"; 8 | 9 | export const Nodes = [ 10 | ...IOBlocks, 11 | ...ObjectNodes, 12 | ...LogicalNodes, 13 | ...StringNode, 14 | ...NumericNodes, 15 | ...Structure, 16 | ...InterfaceNodes 17 | ] 18 | -------------------------------------------------------------------------------- /media/dropdown-arrow.svg: -------------------------------------------------------------------------------- 1 | dropdown-arrow -------------------------------------------------------------------------------- /client/flow-engine/nodes/structure.ts: -------------------------------------------------------------------------------- 1 | import {defineNode} from "@baklavajs/core"; 2 | import {NodeInterface, TextInputInterface} from "baklavajs"; 3 | 4 | 5 | export const Queue = defineNode({ 6 | type: "队列", 7 | inputs: { 8 | value: () => new NodeInterface("输入值", 0), 9 | length: () => new TextInputInterface("长度", "10").setPort(false) 10 | }, 11 | outputs: { 12 | value: () => new NodeInterface("队列输出", 0), 13 | overflow: () => new NodeInterface("溢出", 0) 14 | } 15 | }) 16 | 17 | export const Structure = [ 18 | Queue 19 | ].map((node) => [node,"数据结构"]); 20 | -------------------------------------------------------------------------------- /src/static.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import {Context} from "koishi"; 4 | 5 | export function registerStaticFileRoute(ctx:Context){ 6 | ctx.router.get(/\/static\/blockly\/([a-z0-9-]+.[a-z0-9]+)/,async function (ctx) { 7 | const resource_path = path.resolve(__dirname,'../media/'+ctx.params[0]) 8 | if(path.relative(path.resolve(__dirname+'/../'),resource_path).startsWith('..')){ 9 | return 10 | } 11 | if(!fs.existsSync(resource_path)){ 12 | return 13 | } 14 | ctx.body = await fs.promises.readFile(resource_path) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /client/blockly/extensions/index.ts: -------------------------------------------------------------------------------- 1 | import {parameterListMutator} from "./parameter"; 2 | import {registerScopeExtensions} from "./scope"; 3 | import * as Blockly from "blockly"; 4 | import {typeMutatorExtension} from "./type"; 5 | import {registerSegmentParserMutator} from "./segment"; 6 | 7 | export function unregisterIfRegistered(name:string){ 8 | if(Blockly.Extensions.isRegistered(name)){ 9 | Blockly.Extensions.unregister(name) 10 | } 11 | } 12 | 13 | export function registerExtensions(){ 14 | parameterListMutator(); 15 | registerScopeExtensions() 16 | typeMutatorExtension() 17 | registerSegmentParserMutator() 18 | } 19 | -------------------------------------------------------------------------------- /client/icons/window.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/components/dialogs/export.vue: -------------------------------------------------------------------------------- 1 | 16 | 21 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/io.ts: -------------------------------------------------------------------------------- 1 | import {defineNode, NodeInterface, TextInputInterface} from "baklavajs"; 2 | export const InputBlock = defineNode({ 3 | type: "输入", 4 | inputs: { 5 | name: () => new TextInputInterface("字段名称", "").setPort(false), 6 | }, 7 | outputs: { 8 | output: () => new NodeInterface("输入值", 0), 9 | }, 10 | calculate() { 11 | return {} 12 | }, 13 | }); 14 | 15 | export const OutputBlock = defineNode({ 16 | type: "输出", 17 | inputs: { 18 | output: () => new NodeInterface("输出值", 0), 19 | }, 20 | calculate() { 21 | return {} 22 | }, 23 | }); 24 | 25 | export const IOBlocks = [ 26 | InputBlock, 27 | OutputBlock 28 | ].map((node) => [node,"输入与输出"]); 29 | -------------------------------------------------------------------------------- /client/components/console/code.vue: -------------------------------------------------------------------------------- 1 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /src/vendor.ts: -------------------------------------------------------------------------------- 1 | import {DataService} from "@koishijs/plugin-console"; 2 | import {Dict} from "koishi"; 3 | 4 | declare module '@koishijs/plugin-console' { 5 | namespace Console{ 6 | interface Services { 7 | 'blockly_vendors': BlocklyVendorDataService 8 | } 9 | } 10 | } 11 | 12 | export interface BlockDefinition { 13 | type: string 14 | definition: any 15 | } 16 | 17 | export interface BlocklyVendor{ 18 | id:string 19 | blocks:BlockDefinition[] 20 | } 21 | 22 | export class BlocklyVendorDataService extends DataService>{ 23 | 24 | constructor(ctx) { 25 | super(ctx,'blockly_vendors'); 26 | } 27 | 28 | async get() { 29 | return this.ctx.blockly.vendors 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/blockly/vendor.ts: -------------------------------------------------------------------------------- 1 | import {store} from '@koishijs/client' 2 | import * as Blockly from "blockly"; 3 | import type {BlocklyVendor} from "koishi-plugin-blockly"; 4 | export function vendorCallback(){ 5 | if(!store['blockly_vendors']) 6 | return [] 7 | const vendors : BlocklyVendor[] = Object.values(store['blockly_vendors']) 8 | 9 | vendors.forEach(t=>{ 10 | Blockly.defineBlocksWithJsonArray(t.blocks.map(t=>t.definition)) 11 | }) 12 | 13 | 14 | console.info(vendors.map(t=>t.blocks).flat(10).map(t=>t.definition).map(t=>t.type).map(t=>({ 15 | 'kind': 'block', 16 | 'type': t 17 | }))) 18 | 19 | return vendors.map(t=>t.blocks).flat(10).map(t=>t.definition).map(t=>t.type).map(t=>({ 20 | 'kind': 'block', 21 | 'type': t 22 | })) 23 | } 24 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/string.ts: -------------------------------------------------------------------------------- 1 | import {NodeInterface} from "@baklavajs/core"; 2 | import {TextInputInterface, defineDynamicNode} from "baklavajs"; 3 | 4 | export const StringTemplateNode = defineDynamicNode({ 5 | type: "字符串模板", 6 | inputs: { 7 | template: () => new TextInputInterface("模板", "").setPort(false), 8 | }, 9 | outputs: { 10 | value: () => new NodeInterface("输出", ""), 11 | }, 12 | onUpdate({template}) { 13 | const matches = template.match(/%.+?%/g) 14 | return { 15 | inputs: matches ? Object.fromEntries(matches.map(t => t.slice(1, -1)).map(t => [t, () => new TextInputInterface(t, "")])) : undefined 16 | } 17 | } 18 | }) 19 | 20 | 21 | export const StringNode = [ 22 | StringTemplateNode 23 | ].map((node) => [node, "字符串"]); 24 | -------------------------------------------------------------------------------- /client/blockly/blocks/number.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const ToNumberBlock = { 4 | "type": "to_number", 5 | "message0": "转换为数字 %1", 6 | "args0": [ 7 | { 8 | "type": "input_value", 9 | "name": "value" 10 | } 11 | ], 12 | "inputsInline": false, 13 | "output": null, 14 | "colour": 230, 15 | "tooltip": "", 16 | "helpUrl": "" 17 | } 18 | 19 | export function toNumberBlockGenerator(block){ 20 | let value = javascriptGenerator.valueToCode(block, 'value', javascriptGenerator.ORDER_ATOMIC) 21 | return [`Number(${value})`, javascriptGenerator.ORDER_ATOMIC]; 22 | } 23 | 24 | export const NumberBlocks = [ 25 | ToNumberBlock 26 | ] 27 | 28 | export const numberBlockGenerators = { 29 | to_number: toNumberBlockGenerator 30 | } 31 | -------------------------------------------------------------------------------- /client/blockly/definer.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from 'blockly' 2 | 3 | import {} from './typing' 4 | export function defineBlockWithJsonCustomFields(block:any){ 5 | Blockly.Blocks[block['type']] = { 6 | init: function() { 7 | this.jsonInit(block); 8 | this.imports = block.imports; 9 | this.template = block.template; 10 | block.args0?.forEach((item)=>{ 11 | if(item.type === 'input_value'){ 12 | if(!item.input_type) 13 | return 14 | const input = this.getInput(item.name) 15 | if(input) 16 | input.input_type = item.input_type 17 | } 18 | }) 19 | if(block.init) 20 | block.init.apply(this) 21 | } 22 | } 23 | } 24 | 25 | export function defineBlocksWithJsonCustomFields(blocks:any[]){ 26 | blocks.forEach(defineBlockWithJsonCustomFields) 27 | } 28 | -------------------------------------------------------------------------------- /client/blockly/blocks/logic.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const SleepBlock = { 4 | "type": "sleep", 5 | "message0": "等待 %1 毫秒", 6 | "args0": [ 7 | { 8 | "type": "input_value", 9 | "name": "milliseconds", 10 | "check": [ 11 | "Number" 12 | ] 13 | } 14 | ], 15 | "previousStatement": null, 16 | "nextStatement": null, 17 | "colour": 230, 18 | "tooltip": "", 19 | "helpUrl": "" 20 | }; 21 | 22 | export function sleepBlockGenerator(block){ 23 | let value_name = javascriptGenerator.valueToCode(block, 'milliseconds', javascriptGenerator.ORDER_ATOMIC); 24 | return `await new Promise(resolve => ctx.setTimeout(resolve, ${value_name}));\n`; 25 | } 26 | 27 | export const LogicalBlocks = [ 28 | SleepBlock 29 | ] 30 | 31 | export const logicalBlocks = { 32 | sleep: sleepBlockGenerator 33 | } 34 | -------------------------------------------------------------------------------- /client/index.scss: -------------------------------------------------------------------------------- 1 | .blocklyToolboxDiv { 2 | background-color: var(--bg2); 3 | } 4 | 5 | .blocklySvg { 6 | background-color: var(--bg3); 7 | } 8 | 9 | .blocklyMainBackground { 10 | stroke: none; 11 | } 12 | 13 | .blocklyTreeSeparator { 14 | border-bottom: 1px solid var(--border); 15 | } 16 | 17 | .blocklyScrollbarHandle { 18 | fill: var(--el-text-color-secondary) !important; 19 | opacity: var(--el-scrollbar-opacity, 0.3) !important; 20 | } 21 | 22 | .blocklyScrollbarHandle:hover { 23 | opacity: var(--el-scrollbar-hover-opacity, 0.5) !important; 24 | } 25 | 26 | .layout-container .layout-left{ 27 | min-width: var(--aside-width); 28 | } 29 | 30 | .hljs{ 31 | background-color: var(--bg2); 32 | overflow-x: unset!important; 33 | color:var(--fg1) 34 | } 35 | 36 | .xterm{ 37 | container-type: size; 38 | } 39 | 40 | .blocklyToolboxDiv{ 41 | min-width: 140px; 42 | } 43 | -------------------------------------------------------------------------------- /client/icons/activity.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/blocks.ts: -------------------------------------------------------------------------------- 1 | // test_scope_provider , test_scope_noop , test_scope_consumer 2 | export const TestBlocks = [ 3 | { 4 | "type": "test_scope_provider", 5 | "message0": "provide %1", 6 | "args0": [ 7 | { 8 | "type": "input_statement", 9 | "name": "NAME" 10 | } 11 | ], 12 | "previousStatement": null, 13 | "nextStatement": null, 14 | }, 15 | { 16 | "type": "test_scope_noop", 17 | "message0": "noop %1", 18 | "args0": [ 19 | { 20 | "type": "input_statement", 21 | "name": "NAME" 22 | } 23 | ], 24 | 25 | "previousStatement": null, 26 | "nextStatement": null, 27 | }, 28 | { 29 | "type": "test_scope_consumer", 30 | "message0": "consume %1", 31 | "args0": [ 32 | { 33 | "type": "input_statement", 34 | "name": "NAME" 35 | } 36 | ], 37 | 38 | "previousStatement": null, 39 | "nextStatement": null, 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /client/icons/import.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /client/components/blockly-tab-group.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 35 | -------------------------------------------------------------------------------- /canary.js: -------------------------------------------------------------------------------- 1 | // Blockly canary publish script. 2 | 'use strict'; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const {execSync} = require('child_process'); 7 | const cosmokit = require('cosmokit') 8 | 9 | async function publish(){ 10 | const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); 11 | const originalPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); 12 | if(!packageJson.name.endsWith('-canary')){ 13 | console.error('This script should only be run on canary packages') 14 | return; 15 | } 16 | const version = packageJson.version; 17 | packageJson.version = version + '-canary.' + cosmokit.Time.template('YYYYMMDD'); 18 | fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)) 19 | try{ 20 | execSync('yarn publish --access public', {stdio: 'inherit'}); 21 | }catch (e){ 22 | console.error('Publish failed, reverting package.json') 23 | } 24 | fs.writeFileSync('package.json', JSON.stringify(originalPackageJson, null, 2)) 25 | } 26 | 27 | setTimeout(publish,0) 28 | -------------------------------------------------------------------------------- /client/blockly/extensions/scope.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from "blockly"; 2 | import {unregisterIfRegistered} from "./index"; 3 | 4 | export const scopes = ['session','argument','segment_item'] 5 | 6 | export function registerScopeExtensions(){ 7 | scopes.forEach(t=>{ 8 | consumerExtension(t) 9 | providerExtension(t) 10 | }) 11 | } 12 | 13 | export function consumerExtension(name:string){ 14 | unregisterIfRegistered(name+'_consumer') 15 | Blockly.Extensions.register(name+'_consumer', function(){ 16 | if(!this['scope'])this['scope'] = {}; 17 | if(!this['scope']['consumes'])this['scope']['consumes'] = []; 18 | if(!this['scope']['consumes'].includes(name)) 19 | this['scope']['consumes'].push(name); 20 | }) 21 | } 22 | 23 | export function providerExtension(name:string){ 24 | unregisterIfRegistered(name+'_provider') 25 | Blockly.Extensions.register(name+'_provider', function(){ 26 | if(!this['scope'])this['scope'] = {}; 27 | if(!this['scope']['provides'])this['scope']['provides'] = []; 28 | if(!this['scope']['provides'].includes(name)) 29 | this['scope']['provides'].push(name); 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import {Service,Dict} from "koishi"; 2 | import {PluginManager} from "./plugin"; 3 | import {BlocklyVendor} from "./vendor"; 4 | 5 | declare module "koishi"{ 6 | interface Context{ 7 | blockly:BlocklyService 8 | } 9 | } 10 | 11 | export class BlocklyService extends Service{ 12 | 13 | manager : PluginManager 14 | 15 | vendors : Dict = {} 16 | 17 | constructor(ctx) { 18 | super(ctx,'blockly'); 19 | this.manager = new PluginManager(ctx) 20 | } 21 | 22 | async reload(restart?:boolean){ 23 | if(restart){ 24 | this.manager.plugins = (await this.ctx.database.get('blockly',{enabled:true},["code","enabled"])) 25 | .filter(t=>t.enabled).map(t=>t.code) 26 | this.manager.restart() 27 | } 28 | if(this.ctx['console.blockly']){ 29 | await this.ctx['console.blockly'].refresh() 30 | } 31 | } 32 | 33 | async registerVendor(vendor:BlocklyVendor){ 34 | this.vendors[vendor.id] = vendor 35 | 36 | if(this.ctx['console.blockly_console']){ 37 | await this.ctx['console.blockly_console'].patch(Object.fromEntries([[vendor.id,vendor]])) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/interface.ts: -------------------------------------------------------------------------------- 1 | import {defineNode} from "@baklavajs/core"; 2 | import {defineDynamicNode, NodeInterface, SelectInterface, TextInputInterface} from "baklavajs"; 3 | 4 | export const HttpNode = defineNode({ 5 | type: "HTTP", 6 | inputs: { 7 | url: () => new TextInputInterface("URL", ""), 8 | method: () => new SelectInterface("方法", 'GET', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).setPort(false), 9 | headers: () => new NodeInterface("头部", 0), 10 | body: () => new NodeInterface("主体", 0) 11 | }, 12 | outputs:{ 13 | code: () => new NodeInterface("状态码", 0), 14 | body: () => new NodeInterface("返回体", 0), 15 | headers: () => new NodeInterface("返回头部", 0), 16 | } 17 | }) 18 | 19 | export const CallBlocklyNode = defineDynamicNode({ 20 | type: "调用Blockly代码", 21 | inputs: { 22 | script: () => new SelectInterface("脚本", "",[]).setPort(false) 23 | }, 24 | outputs:{ 25 | output: () => new NodeInterface("输出", 0) 26 | }, 27 | onUpdate(){ 28 | return {} 29 | } 30 | }) 31 | 32 | export const InterfaceNodes = [ 33 | HttpNode, 34 | //CallBlocklyNode 35 | ].map((node) => [node,"接口"]); 36 | -------------------------------------------------------------------------------- /docs/starter.md: -------------------------------------------------------------------------------- 1 | # 起步 2 | 3 | ## 快速搭建 4 | 5 | ::: tip 6 | 下面的内容你也可以选择观看 [此视频](https://www.bilibili.com/video/BV1nG4y1y7zc)。 7 | ::: 8 | 9 | 为没有使用过 Koishi 的新人提供一份快速搭建指南 (以 Windows 为例): 10 | 11 | 1. [前往官网](https://koishi.chat/manual/starter/windows.html) 下载并安装 Koishi 桌面版 12 | 2. 启动桌面版,你将会看到一个控制台界面 13 | 3. 前往「插件市场」,搜索「blockly」,点击下载 14 | 4. 前往「插件配置」,点击右上角的「启用」按钮 15 | 5. 现在你已经可以使用 Blockly 插件了! 16 | 17 | ::: tip 18 | 除了 Windows 外,我们也为 [macOS](https://koishi.chat/manual/starter/macos.html)、[Linux](https://koishi.chat/manual/starter/linux.html)、[Android](https://koishi.chat/manual/starter/android.html)、[Docker](https://koishi.chat/manual/starter/docker.html) 用户提供了安装包。此外,开发者也可以直接使用 [模板项目](https://koishi.chat/manual/starter/boilerplate.html) 完成搭建。 19 | ::: 20 | 21 | ## 接入聊天平台 22 | 23 | ::: tip 24 | 下面的内容你也可以选择观看 [此视频](https://www.bilibili.com/video/BV1W14y137rt)。 25 | ::: 26 | 27 | 如果想进一步在 QQ 中使用,可继续进行下列操作: 28 | 29 | 1. 准备一个 QQ 号 (等级不要过低,否则可能被风控) 30 | 2. 点击左侧的「插件配置」,选择「onebot」插件,完成以下配置: 31 | - 在「selfId」填写你的 QQ 号 32 | - 在「password」填写你的密码 33 | - 在「protocol」选择 ws-reverse 34 | - 开启「gocqhttp.enable」选项 35 | 3. 点击右上角的「启用」按钮 36 | 4. 现在你可以在 QQ 中使用 Koishi 机器人了! 37 | 38 | 其他平台的接入方式也与之类似,你可以在 [这篇文档](https://koishi.chat/manual/console/adapter.html) 中了解全部官方支持的平台。 39 | -------------------------------------------------------------------------------- /client/icons/template.ts: -------------------------------------------------------------------------------- 1 | export const TextTemplateIcon = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIiBmaWxsPSJ3aGl0ZSI+CiAgICA8cGF0aCBkPSJNMTg0IDE2MEMxOTMuNSAxNjAgMjAyLjEgMTY1LjYgMjA1LjkgMTc0LjNMMjY5LjkgMzE4LjNDMjc1LjMgMzMwLjQgMjY5LjkgMzQ0LjUgMjU3LjcgMzQ5LjlDMjQ1LjYgMzU1LjMgMjMxLjUgMzQ5LjkgMjI2LjEgMzM3LjdMMjIxLjcgMzI4SDE0Ni4zTDE0MS45IDMzNy43QzEzNi41IDM0OS45IDEyMi40IDM1NS4zIDExMC4zIDM0OS45Qzk4LjE0IDM0NC41IDkyLjY5IDMzMC40IDk4LjA3IDMxOC4zTDE2Mi4xIDE3NC4zQzE2NS45IDE2NS42IDE3NC41IDE2MCAxODQgMTYwSDE4NHpNMTY3LjYgMjgwSDIwMC40TDE4NCAyNDMuMUwxNjcuNiAyODB6TTMwNCAxODRDMzA0IDE3MC43IDMxNC43IDE2MCAzMjggMTYwSDM4MEM0MTMuMSAxNjAgNDQwIDE4Ni45IDQ0MCAyMjBDNDQwIDIyOS4yIDQzNy45IDIzNy45IDQzNC4yIDI0NS43QzQ0Ny41IDI1Ni43IDQ1NiAyNzMuNCA0NTYgMjkyQzQ1NiAzMjUuMSA0MjkuMSAzNTIgMzk2IDM1MkgzMjhDMzE0LjcgMzUyIDMwNCAzNDEuMyAzMDQgMzI4VjE4NHpNMzUyIDIwOFYyMzJIMzgwQzM4Ni42IDIzMiAzOTIgMjI2LjYgMzkyIDIyMEMzOTIgMjEzLjQgMzg2LjYgMjA4IDM4MCAyMDhIMzUyek0zNTIgMzA0SDM5NkM0MDIuNiAzMDQgNDA4IDI5OC42IDQwOCAyOTJDNDA4IDI4NS40IDQwMi42IDI4MCAzOTYgMjgwSDM1MlYzMDR6TTAgMTI4QzAgOTIuNjUgMjguNjUgNjQgNjQgNjRINTc2QzYxMS4zIDY0IDY0MCA5Mi42NSA2NDAgMTI4VjM4NEM2NDAgNDE5LjMgNjExLjMgNDQ4IDU3NiA0NDhINjRDMjguNjUgNDQ4IDAgNDE5LjMgMCAzODRWMTI4ek02NCAzODRINTc2VjEyOEg2NFYzODR6Ii8+Cjwvc3ZnPg==` 2 | -------------------------------------------------------------------------------- /client/blockly/blocks/debugging.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | export const LoggingBlock = { 3 | "type": "logging", 4 | "message0": "输出一个 %1 级别的日志 %2", 5 | "args0": [ 6 | { 7 | "type": "field_dropdown", 8 | "name": "level", 9 | "options": [ 10 | [ 11 | "调试", 12 | "debug" 13 | ], 14 | [ 15 | "提示", 16 | "info" 17 | ], 18 | [ 19 | "警告", 20 | "warn" 21 | ], 22 | [ 23 | "成功", 24 | "success" 25 | ], 26 | [ 27 | "错误", 28 | "error" 29 | ] 30 | ] 31 | }, 32 | { 33 | "type": "input_value", 34 | "name": "log" 35 | } 36 | ], 37 | "template":['logger_initialize'], 38 | "previousStatement": null, 39 | "nextStatement": null, 40 | "colour": 230, 41 | "tooltip": "", 42 | "helpUrl": "" 43 | } 44 | const loggingBlockGenerator = function(block) { 45 | var dropdown_level = block.getFieldValue('level'); 46 | var log = javascriptGenerator.valueToCode(block, 'log', javascriptGenerator.ORDER_ATOMIC) 47 | return `logger.${dropdown_level}(${log});\n`; 48 | }; 49 | 50 | export const DebugBlocks = [ 51 | LoggingBlock 52 | ] 53 | 54 | export const debugBlockGenerators = { 55 | 'logging':loggingBlockGenerator 56 | } 57 | -------------------------------------------------------------------------------- /client/components/dialogs/import.vue: -------------------------------------------------------------------------------- 1 | 29 | 43 | -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | import {DataService} from "@koishijs/plugin-console"; 2 | import {Context} from "koishi"; 3 | import {BlocklyMenuItem} from "./index"; 4 | import {v4 as uuidV4} from 'uuid' 5 | 6 | declare module '@koishijs/plugin-console' { 7 | namespace Console{ 8 | interface Services { 9 | blockly: BlocklyProvider 10 | } 11 | } 12 | } 13 | 14 | export class BlocklyProvider extends DataService { 15 | constructor(ctx: Context) { 16 | super(ctx, 'blockly') 17 | } 18 | async get() { 19 | return (await this.ctx.database.get('blockly',{id:{$not:-1}},["id","name","enabled","edited","uuid"])) 20 | } 21 | } 22 | 23 | export async function initializeDatabase(ctx) { 24 | ctx.database.extend('blockly', { 25 | id: 'integer', 26 | name: 'string', 27 | body: 'text', 28 | code: 'text', 29 | enabled: 'boolean', 30 | edited: 'boolean', 31 | uuid: 'string' 32 | }, { 33 | autoInc: true 34 | }) 35 | const blocks = await ctx.database.get('blockly', {id: {$not:-1}}) 36 | const logger = ctx.logger('blockly') 37 | for(const block of blocks){ 38 | if(!block.uuid){ 39 | const uuid = uuidV4() 40 | logger.info(`block ${block.id} has no uuid -> ${uuid}`) 41 | await ctx.database.set('blockly',block.id,{uuid}) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/components/console/build.vue: -------------------------------------------------------------------------------- 1 | 41 | 44 | 45 | -------------------------------------------------------------------------------- /client/components/blockly-tab-item.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /client/blockly/binding.ts: -------------------------------------------------------------------------------- 1 | export class ReactiveValue{ 2 | constructor(public value:T,public identifier:string){ 3 | } 4 | 5 | watcher:Set<(value:T)=>void> = new Set; 6 | 7 | set(value:T){ 8 | this.value = value; 9 | this.watcher.forEach(watcher=>watcher(value)); 10 | } 11 | 12 | addWatcher(watcher:(value:T)=>void){ 13 | this.watcher.add(watcher); 14 | return watcher 15 | } 16 | 17 | removeWatcher(watcher:(value:T)=>void){ 18 | this.watcher.delete(watcher); 19 | } 20 | } 21 | 22 | export class ReactiveBindingSet extends Set{ 23 | 24 | watcher:Set<(bindingSet:ReactiveBindingSet,value:T,type:'add'|'remove')=>void> = new Set 25 | 26 | constructor(){ 27 | super(); 28 | } 29 | 30 | add(value:T){ 31 | this.watcher.forEach(watcher=>watcher(this,value,'add')); 32 | return super.add(value); 33 | } 34 | 35 | delete(value:T){ 36 | this.watcher.forEach(watcher=>watcher(this,value,'remove')); 37 | return super.delete(value); 38 | } 39 | 40 | 41 | addWatcher(watcher:(bindingSet:ReactiveBindingSet,value:T,type:'add'|'remove')=>void){ 42 | this.watcher.add(watcher); 43 | } 44 | 45 | removeWatcher(watcher:(bindingSet:ReactiveBindingSet,value:T,type:'add'|'remove')=>void){ 46 | this.watcher.delete(watcher); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/logical.ts: -------------------------------------------------------------------------------- 1 | import {SelectInterface,defineNode, NodeInterface, TextInputInterface} from "baklavajs"; 2 | 3 | export const LRLogicalExpression = defineNode({ 4 | type: "二元逻辑运算", 5 | inputs: { 6 | left: () => new NodeInterface("A", 0), 7 | right: () => new NodeInterface("B", 0), 8 | operator: () => new SelectInterface("运算符", "A大于B",["A大于B","A小于B","A等于B","A不等于B","A大于等于B","A小于等于B"]), 9 | }, 10 | outputs: { 11 | value: () => new NodeInterface("输出值", 0) 12 | } 13 | }); 14 | 15 | export const SingleLogicalExpression = defineNode({ 16 | type: "单元逻辑运算", 17 | inputs: { 18 | value: () => new NodeInterface("值", 0), 19 | operator: () => new SelectInterface("运算符", "!",["取反"]), 20 | }, 21 | outputs: { 22 | value: () => new NodeInterface("输出值", 0) 23 | } 24 | }) 25 | 26 | export const TernaryLogicalExpression = defineNode({ 27 | type: "三元逻辑运算", 28 | inputs: { 29 | condition: () => new NodeInterface("条件", 0), 30 | trueValue: () => new TextInputInterface("真值", ""), 31 | falseValue: () => new TextInputInterface("假值", ""), 32 | }, 33 | outputs: { 34 | value: () => new NodeInterface("输出值", 0) 35 | } 36 | }) 37 | 38 | export const LogicalNodes = [ 39 | LRLogicalExpression, 40 | SingleLogicalExpression, 41 | TernaryLogicalExpression 42 | ].map((node) => [node,"逻辑"]); 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Context, Schema} from 'koishi' 2 | import { resolve } from 'path' 3 | import {BlocklyService} from "./service"; 4 | import {BlocklyProvider, initializeDatabase} from "./data"; 5 | import {initializeConsoleApiBacked} from "./console"; 6 | import {registerStaticFileRoute} from "./static"; 7 | import {BlocklyVendorDataService} from "./vendor"; 8 | 9 | export const name = 'blockly' 10 | 11 | export interface Config {} 12 | 13 | export const Config: Schema = Schema.object({}) 14 | 15 | export const using = ['database','console'] 16 | 17 | export async function apply(ctx: Context) { 18 | ctx.plugin(BlocklyService) 19 | ctx.plugin(BlocklyProvider) 20 | ctx.using(['blockly'],()=>ctx.plugin(BlocklyVendorDataService)) 21 | 22 | await initializeDatabase(ctx); 23 | 24 | ctx.using(['console','blockly'], (ctx) => { 25 | ctx.console.addEntry({ 26 | dev: resolve(__dirname, '../client/index.ts'), 27 | prod: resolve(__dirname, '../dist'), 28 | }) 29 | ctx.blockly.reload(true) 30 | }) 31 | 32 | initializeConsoleApiBacked(ctx) 33 | 34 | registerStaticFileRoute(ctx) 35 | 36 | } 37 | 38 | export * from './structure' 39 | export * from "./data"; 40 | export * from "./plugin" 41 | export * from "./service" 42 | export * from "./static" 43 | export * from './console' 44 | export * from './transpiler' 45 | export * from './vendor' 46 | -------------------------------------------------------------------------------- /client/flow-engine/data-flow.vue: -------------------------------------------------------------------------------- 1 | 9 | 24 | 25 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import {Context, ForkScope, Logger, segment} from "koishi"; 2 | import vm from "node:vm"; 3 | import {esModuleToCommonJs} from "./transpiler"; 4 | 5 | export class PluginManager{ 6 | plugins:string[] = []; 7 | runningPlugins:ForkScope[] = []; 8 | private logger: Logger; 9 | constructor(protected ctx:Context) { 10 | this.restart() 11 | this.logger = this.ctx.logger("blockly") 12 | } 13 | restart(){ 14 | this.runningPlugins.forEach(t=>t.dispose()) 15 | this.runningPlugins = [] 16 | if(this.plugins.length == 0){ 17 | this.logger?.info("No plugin loaded") 18 | return; 19 | } 20 | this.logger.info("Loading "+this.plugins.length +" plugin(s)") 21 | this.plugins.forEach(p=>{ 22 | let context : any = {} 23 | context.module = { 24 | exports:{} 25 | } 26 | context.require = require; 27 | context.segment = segment; 28 | let plugin = null 29 | try{ 30 | const code = `const {${Object.keys(context).join(',')}} = this;${esModuleToCommonJs(p)}` 31 | const pluginFunction = new Function(code) 32 | pluginFunction.call(context) 33 | plugin = context.module.exports 34 | }catch (e){ 35 | this.ctx.logger("blockly").warn(e); 36 | } 37 | if(plugin && plugin['apply']) 38 | this.runningPlugins.push(this.ctx.plugin(plugin)) 39 | }) 40 | this.logger.info("Loaded "+this.runningPlugins.length +" plugin(s)") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # koishi-plugin-blockly 2 | 3 | [![npm](https://img.shields.io/npm/v/koishi-plugin-blockly)](https://www.npmjs.com/package/koishi-plugin-blockly) 4 | ![Download](https://img.shields.io/npm/dm/koishi-plugin-openchat?style=round) 5 | [![Install Size](https://packagephobia.com/badge?p=koishi-plugin-blockly)](https://packagephobia.com/result?p=koishi-plugin-blockly) 6 | [![Package Quality](https://packagequality.com/shield/koishi-plugin-blockly.svg)](https://packagequality.com/#?package=koishi-plugin-blockly) 7 | [![Koishi](https://badge.koishi.chat/rating/koishi-plugin-blockly?style=round)](https://koishi.chat) 8 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkoishijs%2Fkoishi-plugin-blockly.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkoishijs%2Fkoishi-plugin-blockly?ref=badge_shield) 9 | 10 | 使用 Blockly 在你的 Koishi 里进行可视化编程! 11 | 12 | Use blockly plugin to programming with blocks and no-code needed! 13 | 14 | ## 如何使用? How to use it? 15 | 在 Koishi 市场中搜索"Blockly",安装并启用插件即可。 16 | 17 | 插件的页面在控制台左侧的"Blockly 可视化编程"页面中 18 | 19 | Search "blockly" in the koishi market, install and enable it. 20 | 21 | The page of the plugin is in the "Blockly" page in the left bar of the console. 22 | 23 | ## 文档 Documentations 24 | 暂未完善 25 | 26 | 27 | ## License 28 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkoishijs%2Fkoishi-plugin-blockly.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkoishijs%2Fkoishi-plugin-blockly?ref=badge_large) -------------------------------------------------------------------------------- /client/blockly/blocks/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import {DataBlocks, dataBlockGenerators} from "./data"; 3 | import {ParameterBlocks} from "./parameter"; 4 | import {SessionBlocks, sessionBlockGenerators} from "./session"; 5 | import {botBlockGenerators, BotBlocks} from "./bot"; 6 | import {debugBlockGenerators, DebugBlocks} from "./debugging"; 7 | import {eventBlockGenerators, EventBlocks} from "./event"; 8 | import {environmentBlockGenerators, EnvironmentBlocks} from "./environment"; 9 | import {textBlockGenerators, TextBlocks} from "./text"; 10 | import {logicalBlocks, LogicalBlocks} from "./logic"; 11 | import {messageBlocks, MessageBlocks} from "./message"; 12 | import {segmentBlockGenerators, SegmentBlocks} from "./segment"; 13 | import {numberBlockGenerators, NumberBlocks} from "./number"; 14 | import {TypeBlocks} from "./typing"; 15 | 16 | export const Blocks = [ 17 | ...LogicalBlocks, 18 | ...TextBlocks, 19 | ...EventBlocks, 20 | ...SessionBlocks, 21 | ...MessageBlocks, 22 | ...SegmentBlocks, 23 | ...DataBlocks, 24 | ...BotBlocks, 25 | ...DebugBlocks, 26 | ...EnvironmentBlocks, 27 | ...ParameterBlocks, 28 | ...NumberBlocks, 29 | ...TypeBlocks 30 | ] 31 | 32 | export const BlockGenerators=Object.assign({},...[ 33 | logicalBlocks, 34 | textBlockGenerators, 35 | eventBlockGenerators, 36 | sessionBlockGenerators, 37 | messageBlocks, 38 | segmentBlockGenerators, 39 | dataBlockGenerators, 40 | botBlockGenerators, 41 | debugBlockGenerators, 42 | environmentBlockGenerators, 43 | numberBlockGenerators 44 | ]) 45 | -------------------------------------------------------------------------------- /client/blockly/typing/converter.ts: -------------------------------------------------------------------------------- 1 | import {BlockSvg, WorkspaceSvg} from "blockly"; 2 | import {ArrayType, Type, UnionType} from "./index"; 3 | 4 | export function convertTypeToBlock(workspace:WorkspaceSvg,type:Type):BlockSvg{ 5 | if(!type)return workspace.newBlock('type_any') 6 | switch (type.getTypeName()){ 7 | case 'string': 8 | return workspace.newBlock('type_string') 9 | case 'number': 10 | return workspace.newBlock('type_number') 11 | case 'boolean': 12 | return workspace.newBlock('type_boolean') 13 | case 'array': 14 | const array_block = workspace.newBlock('type_array') 15 | const array_items = convertTypeToBlock(workspace,(type as ArrayType).getPrototype()) 16 | array_items.initSvg() 17 | array_block.getInput('type').connection.connect( 18 | array_items.outputConnection 19 | ) 20 | return array_block 21 | case 'union': 22 | const union_block = workspace.newBlock('type_union') 23 | const union_items = (type as UnionType).getPrototype() 24 | union_block['itemCount_'] = union_items.length 25 | union_block.initSvg() 26 | union_block['updateShape_']() 27 | for(let i=0;i,name="",using=[],apply=""){ 8 | return [...Object.entries(imports)].map(([i,j])=> 9 | `import { ${j.join(', ')} } from "${i}"\n` 10 | ).join("")+ 11 | WrapperTemplate 12 | .replace(/\{\{name}}/g,name.replace(/"/g,"\\\"").replace(/\\/g,"\\\\")) 13 | .replace(/\{\{using}}/g,JSON.stringify(using)) 14 | .replace(/\{\{apply}}/g,apply.split("\n").map(t=>" "+t).join("\n")) 15 | } 16 | export function build(name,plugin_id,workspace:Workspace){ 17 | let currentImportMap = {} 18 | const blocks = workspace.getAllBlocks(false) 19 | blocks.filter(b=>b['imports']).map(b=>b['imports']).forEach(t=>{ 20 | [...Object.entries(t)].forEach(([i,j])=>{ 21 | if(!currentImportMap[i]) currentImportMap[i] = [] 22 | currentImportMap[i] = deduplicate([...currentImportMap[i],...j as any]) 23 | }) 24 | }) 25 | const templates = []; 26 | blocks.filter(b=>b['template']).map(b=>b['template']).forEach(t=>{ 27 | t.forEach(t=>{ 28 | if(!templates.includes(t)) templates.push(t) 29 | }) 30 | }) 31 | 32 | return createWrapper(currentImportMap,name,[],templates.map(t=>TemplateCodes[t]+"\n").map(t=>t.replace('{{name}}',name).replace("{{plugin_id}}",plugin_id)).join("")+javascriptGenerator.workspaceToCode(workspace)) 33 | } 34 | -------------------------------------------------------------------------------- /client/blockly/blocks/message.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const SendSessionMessageBlock = { 4 | "type": "send_session_message", 5 | "message0": "发送消息给事件发送者 %1", 6 | "args0": [ 7 | { 8 | "type": "input_value", 9 | "name": "content", 10 | "check": [ 11 | "Boolean", 12 | "String" 13 | ] 14 | } 15 | ], 16 | "previousStatement": null, 17 | "nextStatement": null, 18 | "extensions":['session_consumer'], 19 | "colour": 230, 20 | "tooltip": "", 21 | "helpUrl": "" 22 | }; 23 | 24 | export function sendSessionMessageBlockGenerator(block){ 25 | let value_name = javascriptGenerator.valueToCode(block, 'content', javascriptGenerator.ORDER_ATOMIC); 26 | return `await session.send(${value_name});\n`; 27 | } 28 | 29 | export const ReturnMessageBlock = { 30 | "type": "return_message", 31 | "message0": "终止后续逻辑并发送消息 %1", 32 | "args0": [ 33 | { 34 | "type": "input_value", 35 | "name": "content", 36 | "check": [ 37 | "Boolean", 38 | "String" 39 | ] 40 | } 41 | ], 42 | "previousStatement": null, 43 | "extensions":['session_consumer'], 44 | "colour": 230, 45 | "tooltip": "", 46 | "helpUrl": "" 47 | }; 48 | 49 | export function returnMessageBlockGenerator(block){ 50 | let value_name = javascriptGenerator.valueToCode(block, 'content', javascriptGenerator.ORDER_ATOMIC); 51 | return `return ${value_name};\n`; 52 | } 53 | 54 | export const MessageBlocks = [ 55 | SendSessionMessageBlock, 56 | ReturnMessageBlock 57 | ] 58 | 59 | export const messageBlocks = { 60 | 'send_session_message':sendSessionMessageBlockGenerator, 61 | 'return_message':returnMessageBlockGenerator 62 | } 63 | -------------------------------------------------------------------------------- /client/blockly/pack.ts: -------------------------------------------------------------------------------- 1 | import {gzip, ungzip} from "pako"; 2 | import {stringToArrayBuffer} from "../utils"; 3 | import type {BlocklyDocument} from "koishi-plugin-blockly"; 4 | import {BLOCKLY_API_VERSION, BLOCKLY_VERSION} from "../version"; 5 | 6 | export function encodeBlocklyExport(name:string,uuid:string,body){ 7 | const exportObject = { 8 | version:BLOCKLY_API_VERSION, 9 | plugin_version:BLOCKLY_VERSION, // @todo:replace here to enable automatic version embedding 10 | vendors:[], 11 | body, 12 | name, 13 | uuid 14 | } 15 | 16 | 17 | const encodedShareObject = encodeURI(JSON.stringify(exportObject)) 18 | 19 | const shareBody = btoa(String.fromCharCode.apply(null, gzip(encodedShareObject))) 20 | 21 | let shareCode = `插件名称: ${name}\n` 22 | shareCode += `导出时间: ${new Date().toLocaleString()}\n` 23 | shareCode += `-=-=-=-=--=-=-=-=- BEGIN KOISHI BLOCKLY BLOCK V1 -=-=--=-=-=--=-=--=-=-=-\n` 24 | shareCode +=`${shareBody.replace(/(.{64})/g, "$1\n")}\n` 25 | shareCode += `-=-=--=-=-=--=-=-=-=- END KOISHI BLOCKLY BLOCK V1 -=-=--=-=-=--=-=--=-=-=-\n` 26 | return shareCode.replace("\n\n","\n") 27 | } 28 | 29 | export function decodeBlocklyExport(content:string):Partial|null{ 30 | 31 | const data_body = content 32 | .match(/[=–-]+\s+BEGIN KOISHI BLOCKLY BLOCK V1\s+[=–-]+\n([\s\S]+)\n[=–-]+\s+END KOISHI BLOCKLY BLOCK V1\s+[=–-]+/)?.[1] 33 | .replace(/[\r\n\t ]/g,'') 34 | 35 | if(!data_body) { 36 | return null 37 | } 38 | 39 | const encodedShareObject = ungzip(stringToArrayBuffer(atob(data_body)), {to: 'string'}) 40 | 41 | if(!encodedShareObject) { 42 | return null 43 | } 44 | 45 | const data = JSON.parse(decodeURI(encodedShareObject)) 46 | 47 | if(!data){ 48 | return null 49 | } 50 | 51 | return data 52 | } 53 | -------------------------------------------------------------------------------- /client/components/variable.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 71 | -------------------------------------------------------------------------------- /client/components/dialogs/author.vue: -------------------------------------------------------------------------------- 1 | 51 | 65 | -------------------------------------------------------------------------------- /client/flow-engine/nodes/object.ts: -------------------------------------------------------------------------------- 1 | import {defineDynamicNode, defineNode, NodeInterface, TextInputInterface} from "baklavajs"; 2 | 3 | export const GetObjectProperty = defineNode({ 4 | type: "获取对象属性", 5 | inputs: { 6 | object: () => new NodeInterface("对象", 0), 7 | property: () => new TextInputInterface("属性","") 8 | }, 9 | outputs: { 10 | value: () => new NodeInterface("值", 0) 11 | }, 12 | calculate(){ 13 | return {} 14 | } 15 | }) 16 | 17 | export const SetObjectProperty = defineNode({ 18 | type: "设置对象属性", 19 | inputs: { 20 | object: () => new NodeInterface("对象", 0), 21 | property: () => new TextInputInterface("属性",""), 22 | value: () => new TextInputInterface("值", ""), 23 | }, 24 | outputs: { 25 | value: () => new NodeInterface("改变后的对象", 0) 26 | }, 27 | calculate(input,ctx){ 28 | return {} 29 | } 30 | }); 31 | 32 | export const CreateObject = defineDynamicNode({ 33 | type: "创建对象", 34 | inputs: { 35 | prototype: () => new TextInputInterface("原型,用逗号分隔", "").setPort(false) 36 | }, 37 | outputs: { 38 | value: () => new NodeInterface("对象", 0) 39 | }, 40 | onUpdate(){ 41 | return { 42 | inputs: Object.fromEntries(this.inputs.prototype.value?.split(",").filter(t=>t).map((name) => [name, () => new NodeInterface(name, 0)])) 43 | } 44 | } 45 | }) 46 | 47 | export const SplitObject = defineDynamicNode({ 48 | type: "分离对象", 49 | inputs: { 50 | value: () => new NodeInterface("对象", 0), 51 | prototype: () => new TextInputInterface("原型,用逗号分隔", "").setPort(false) 52 | }, 53 | onUpdate(){ 54 | return { 55 | outputs: Object.fromEntries(this.inputs.prototype.value?.split(",").filter(t=>t).map((name) => [name, () => new NodeInterface(name, 0)])) 56 | } 57 | } 58 | }) 59 | 60 | export const ObjectNodes = [ 61 | CreateObject, 62 | SplitObject, 63 | GetObjectProperty, 64 | SetObjectProperty 65 | ].map((node) => [node,"对象"]); 66 | -------------------------------------------------------------------------------- /client/blockly/typing/overwritting.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from 'blockly' 2 | import {AnyType, ArrayType, ClassType, NumberType, StringType, Type, unify, UnionType} from "./index"; 3 | import {BlockSvg} from "blockly"; 4 | type BlockInitializer = {init:(...args:any)=>any} 5 | const injectSymbol = Symbol('injected') 6 | 7 | export interface TypeDefinition{ 8 | input?:Record 9 | output?:()=>Type 10 | } 11 | function defineType(target: BlockInitializer,types:TypeDefinition):BlockInitializer{ 12 | if(target[injectSymbol]) 13 | return target 14 | const init = target.init 15 | target[injectSymbol] = true 16 | target.init = function(this:BlockSvg,...args){ 17 | init.apply(this,args) 18 | if(types.input) 19 | Object.entries(types.input).forEach(([name,type])=>{ 20 | const input = this.getInput(name) 21 | if(input) 22 | input.input_type = type 23 | }) 24 | if(types.output) 25 | this.getOutputType = types.output.bind(this) 26 | } 27 | } 28 | 29 | export function initializeType(){ 30 | 31 | defineType(Blockly.Blocks['text'],{ 32 | output:()=>new StringType() 33 | }) 34 | 35 | defineType(Blockly.Blocks['send_session_message'],{ 36 | input:{ 37 | content: new UnionType([new StringType(),new ClassType('Segment')]) 38 | } 39 | }) 40 | 41 | defineType(Blockly.Blocks['segment_at'],{ 42 | input:{ 43 | user: new UnionType([new StringType()]) 44 | }, 45 | output:()=>new ClassType('Segment') 46 | }) 47 | 48 | defineType(Blockly.Blocks['lists_create_with'],{ 49 | output:function(this:BlockSvg){ 50 | return new ArrayType( 51 | unify( 52 | this.inputList.map(t=>t.name) 53 | .filter(t=>t) 54 | .map(t=>this.getInput(t).connection?.targetBlock()) 55 | .filter(t=>t) 56 | .map(t=>t.getOutputType?.()??new AnyType()) 57 | ) 58 | ) 59 | } 60 | }) 61 | 62 | 63 | defineType(Blockly.Blocks['math_number'], { 64 | output: () => new NumberType() 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /client/blockly/listeners/consumer.ts: -------------------------------------------------------------------------------- 1 | import {Abstract} from "blockly/core/events/events_abstract"; 2 | import {BlockMove} from "blockly/core/events/events_block_move"; 3 | import {BlockCreate} from "blockly/core/events/events_block_create"; 4 | import {BlockSvg, WorkspaceSvg} from "blockly"; 5 | import * as Blockly from "blockly"; 6 | 7 | export function disableOrphansAndOrphanConsumersEvent(_event:Abstract){ 8 | if (!(_event.type === 'move' || _event.type === 'create'))return; 9 | const event = _event as BlockMove | BlockCreate 10 | if (!event.workspaceId || !event.blockId) 11 | return; 12 | const eventWorkspace = event.getEventWorkspace_() as WorkspaceSvg; 13 | if(!eventWorkspace) 14 | return; 15 | let block = eventWorkspace.getBlockById(event.blockId); 16 | Blockly.Events.disableOrphans(_event); 17 | disableOrphanConsumers(block); 18 | } 19 | export function disableOrphanConsumers(block:BlockSvg){ 20 | if(!block)return 21 | const children = [].concat(block.getChildren(false)) 22 | if(children) 23 | children.forEach(b=>{ 24 | disableOrphanConsumers(b) 25 | }); 26 | 27 | if(!block || !block['scope'] || !block['scope']['consumes']){ 28 | return; 29 | } 30 | 31 | const consumes : Map = new Map((Array.isArray(block['scope']['consumes']) ? block['scope']['consumes'] : [block['scope']['consumes']]).map(t=>[t,true])); 32 | 33 | block.setWarningText(null) 34 | let current = block 35 | while(current = current.getParent()){ 36 | if(!current['scope'] || !current['scope']['provides'])continue; 37 | const provides = Array.isArray(current['scope']['provides']) ? current['scope']['provides'] : [current['scope']['provides']]; 38 | provides.forEach(provide => { 39 | consumes.delete(provide) 40 | }) 41 | } 42 | 43 | if(consumes.size) { 44 | block.setEnabled(false); 45 | block.setWarningText('The consumer should be placed in the variable scope of the provider:'+Array.from(consumes.keys()).join(',')) 46 | } 47 | 48 | const next = block.getNextBlock(); 49 | if(next)disableOrphanConsumers(next); 50 | } 51 | -------------------------------------------------------------------------------- /media/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Microbundle cache 55 | .rpt2_cache/ 56 | .rts2_cache_cjs/ 57 | .rts2_cache_es/ 58 | .rts2_cache_umd/ 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # Next.js build output 77 | .next 78 | 79 | # Nuxt.js build / generate output 80 | .nuxt 81 | dist 82 | 83 | # Gatsby files 84 | .cache/ 85 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # public 88 | 89 | # vuepress build output 90 | .vuepress/dist 91 | 92 | # Serverless directories 93 | .serverless/ 94 | 95 | # FuseBox cache 96 | .fusebox/ 97 | 98 | # DynamoDB Local files 99 | .dynamodb/ 100 | 101 | # TernJS port file 102 | .tern-port 103 | 104 | lib/ 105 | -------------------------------------------------------------------------------- /client/blockly/extensions/parameter.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from "blockly"; 2 | import {unregisterIfRegistered} from "./index"; 3 | 4 | export function parameterListMutator(){ 5 | unregisterIfRegistered("parameter_list") 6 | Blockly.Extensions.registerMutator('parameter_list', { 7 | decompose: function(workspace) { 8 | let topBlock = workspace.newBlock('parameter_list'); 9 | topBlock.initSvg(); 10 | if(!Array.isArray(this.parameters)){ 11 | this.parameters = []; 12 | } 13 | var connection = topBlock.getInput('parameter_list').connection; 14 | this.parameters.forEach((parameter)=>{ 15 | let itemBlock = workspace.newBlock(parameter.type); 16 | itemBlock.setFieldValue(parameter.name,'name') 17 | itemBlock.setFieldValue(parameter.required?'TRUE':'FALSE','required'); 18 | itemBlock.initSvg() 19 | connection.connect(itemBlock.previousConnection); 20 | connection = itemBlock.nextConnection; 21 | }) 22 | return topBlock; 23 | }, 24 | 25 | compose: function(topBlock) { 26 | const parameter_links = topBlock.getChildren() 27 | console.info(parameter_links) 28 | if(!parameter_links.length || parameter_links.length<=0){ 29 | this.parameters = []; 30 | return; 31 | } 32 | const parameters = []; 33 | let currentBlock = parameter_links[0]; 34 | while(currentBlock){ 35 | console.info(currentBlock,currentBlock.type) 36 | const name = currentBlock.getFieldValue('name') 37 | const required = currentBlock.getFieldValue('required').toLowerCase() === 'true' 38 | const type = currentBlock.type 39 | parameters.push({name,required,type}) 40 | currentBlock = currentBlock.getNextBlock(); 41 | } 42 | this.parameters = parameters; 43 | }, 44 | 45 | saveExtraState: function() { 46 | return { 47 | 'parameters': this.parameters, 48 | }; 49 | }, 50 | 51 | loadExtraState: function(state) { 52 | this.parameters = state['parameters']; 53 | } 54 | }, undefined, ['any_parameter','string_parameter','number_parameter','boolean_parameter','int_parameter']); 55 | } 56 | -------------------------------------------------------------------------------- /client/icons/typings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Base64 - encoded SVG icon for the "typings" block. 3 | */ 4 | export const TypingsIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgUHJvIDYuMy4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlIChDb21tZXJjaWFsIExpY2Vuc2UpIENvcHlyaWdodCAyMDIzIEZvbnRpY29ucywgSW5jLiAtLT48cGF0aCBkPSJNMTE4LjYgODBjLTExLjUgMC0yMS40IDcuOS0yNCAxOS4xTDU3IDI2MC4zYzIwLjUtNi4yIDQ4LjMtMTIuMyA3OC43LTEyLjNjMzIuMyAwIDYxLjggNi45IDgyLjggMTMuNWMxMC42IDMuMyAxOS4zIDYuNyAyNS40IDkuMmMzLjEgMS4zIDUuNSAyLjQgNy4zIDMuMmMuOSAuNCAxLjYgLjcgMi4xIDFsLjYgLjMgLjIgLjEgLjEgMCAwIDAgMCAwczAgMC02LjMgMTIuN2gwbDYuMy0xMi43YzUuOCAyLjkgMTAuNCA3LjMgMTMuNSAxMi43aDQwLjZjMy4xLTUuMyA3LjctOS44IDEzLjUtMTIuN2w2LjMgMTIuN2gwYy02LjMtMTIuNy02LjMtMTIuNy02LjMtMTIuN2wwIDAgMCAwIC4xIDAgLjItLjEgLjYtLjNjLjUtLjIgMS4yLS42IDIuMS0xYzEuOC0uOCA0LjItMS45IDcuMy0zLjJjNi4xLTIuNiAxNC44LTUuOSAyNS40LTkuMmMyMS02LjYgNTAuNC0xMy41IDgyLjgtMTMuNWMzMC40IDAgNTguMiA2LjEgNzguNyAxMi4zTDQ4MS40IDk5LjFjLTIuNi0xMS4yLTEyLjYtMTkuMS0yNC0xOS4xYy0zLjEgMC02LjIgLjYtOS4yIDEuOEw0MTYuOSA5NC4zYy0xMi4zIDQuOS0yNi4zLTEuMS0zMS4yLTEzLjRzMS4xLTI2LjMgMTMuNC0zMS4ybDMxLjMtMTIuNWM4LjYtMy40IDE3LjctNS4yIDI3LTUuMmMzMy44IDAgNjMuMSAyMy4zIDcwLjggNTYuMmw0My45IDE4OGMxLjcgNy4zIDIuOSAxNC43IDMuNSAyMi4xYy4zIDEuOSAuNSAzLjggLjUgNS43djYuN1YzNTJ2MTZjMCA2MS45LTUwLjEgMTEyLTExMiAxMTJINDE5LjdjLTU5LjQgMC0xMDguNS00Ni40LTExMS44LTEwNS44TDMwNi42IDM1MkgyNjkuNGwtMS4yIDIyLjJDMjY0LjkgNDMzLjYgMjE1LjggNDgwIDE1Ni4zIDQ4MEgxMTJDNTAuMSA0ODAgMCA0MjkuOSAwIDM2OFYzNTIgMzEwLjcgMzA0YzAtMS45IC4yLTMuOCAuNS01LjdjLjYtNy40IDEuOC0xNC44IDMuNS0yMi4xbDQzLjktMTg4QzU1LjUgNTUuMyA4NC44IDMyIDExOC42IDMyYzkuMiAwIDE4LjQgMS44IDI3IDUuMmwzMS4zIDEyLjVjMTIuMyA0LjkgMTguMyAxOC45IDEzLjQgMzEuMnMtMTguOSAxOC4zLTMxLjIgMTMuNEwxMjcuOCA4MS44Yy0yLjktMS4yLTYtMS44LTkuMi0xLjh6TTY0IDMyNS40VjM2OGMwIDI2LjUgMjEuNSA0OCA0OCA0OGg0NC4zYzI1LjUgMCA0Ni41LTE5LjkgNDcuOS00NS4zbDIuNS00NS42Yy0yLjMtLjgtNC45LTEuNy03LjUtMi41Yy0xNy4yLTUuNC0zOS45LTEwLjUtNjMuNi0xMC41Yy0yMy43IDAtNDYuMiA1LjEtNjMuMiAxMC41Yy0zLjEgMS01LjkgMS45LTguNSAyLjl6TTUxMiAzNjhWMzI1LjRjLTIuNi0uOS01LjUtMS45LTguNS0yLjljLTE3LTUuNC0zOS41LTEwLjUtNjMuMi0xMC41Yy0yMy43IDAtNDYuNCA1LjEtNjMuNiAxMC41Yy0yLjcgLjgtNS4yIDEuNy03LjUgMi41bDIuNSA0NS42YzEuNCAyNS40IDIyLjUgNDUuMyA0Ny45IDQ1LjNINDY0YzI2LjUgMCA0OC0yMS41IDQ4LTQ4eiIvPjwvc3ZnPgo="; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koishi-plugin-blockly", 3 | "description": "Use blockly to develop a simple koishi plugin", 4 | "version": "0.6.3", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/koishijs/koishi-plugin-blockly.git" 10 | }, 11 | "homepage": "https://blockly.koishi.chat", 12 | "files": [ 13 | "lib", 14 | "dist", 15 | "media" 16 | ], 17 | "license": "AGPL-3.0", 18 | "scripts": { 19 | "docs:dev": "vitepress dev docs --open", 20 | "docs:build": "vitepress build docs", 21 | "docs:serve": "vitepress serve docs", 22 | "test": "yakumo mocha -r esbuild-register -r yml-register --exit" 23 | }, 24 | "keywords": [ 25 | "chatbot", 26 | "koishi", 27 | "plugin", 28 | "blockly", 29 | "lowcode", 30 | "program", 31 | "develop", 32 | "flowgraph", 33 | "dataflow", 34 | "visualization" 35 | ], 36 | "koishi": { 37 | "browser": true, 38 | "public": [ 39 | "dist" 40 | ], 41 | "description": { 42 | "en": "Use blockly to develop a simple koishi plugin", 43 | "zh": "Blockly 可视化编程插件" 44 | } 45 | }, 46 | "peerDependencies": { 47 | "@koishijs/plugin-console": "^5.7.7", 48 | "koishi": "^4.12.8" 49 | }, 50 | "devDependencies": { 51 | "@baklavajs/themes": "^2.0.1-next.0", 52 | "@highlightjs/vue-plugin": "^2.1.2", 53 | "@koishijs/client": "^5.7.6", 54 | "@koishijs/plugin-console": "^5.7.6", 55 | "@koishijs/vitepress": "^1.9.5", 56 | "@mit-app-inventor/blockly-block-lexical-variables": "^0.0.13", 57 | "@types/node": "^18.16.8", 58 | "@types/pako": "^2.0.0", 59 | "@types/semver": "^7.5.0", 60 | "@types/uuid": "^9.0.1", 61 | "baklavajs": "2.0.2-beta.3", 62 | "blockly": "9.2.1", 63 | "chai": "^4.3.7", 64 | "element-plus": "^2.3.4", 65 | "highlight.js": "^11.8.0", 66 | "koishi": "^4.11.6", 67 | "mocha": "^10.2.0", 68 | "pako": "^2.1.0", 69 | "sass": "^1.62.1", 70 | "semver": "^7.5.0", 71 | "typescript": "^4.9.5", 72 | "uuid": "^9.0.0", 73 | "vitepress": "1.0.0-alpha.26", 74 | "xterm": "^5.1.0", 75 | "yakumo-mocha": "^0.3.1" 76 | }, 77 | "dependencies": { 78 | "jsonpath-plus": "^7.2.0", 79 | "uuid": "^9.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client/blockly/blocks/environment.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | export const TimestampBlock = { 3 | "type": "time_stamp", 4 | "message0": "%1 时间戳", 5 | "args0": [ 6 | { 7 | "type": "field_dropdown", 8 | "name": "type", 9 | "options": [ 10 | [ 11 | "13位", 12 | "13" 13 | ], 14 | [ 15 | "11位", 16 | "11" 17 | ] 18 | ] 19 | } 20 | ], 21 | "output": null, 22 | "colour": 230, 23 | "tooltip": "", 24 | "helpUrl": "" 25 | } 26 | 27 | export function timestampBlockGenerator(block) { 28 | let timestamp_type = block.getFieldValue('type'); 29 | return [`Math.round(new Date() ${ timestamp_type === "11" ? " / 1000" : "" })`, javascriptGenerator.ORDER_NONE]; 30 | } 31 | 32 | export const TimeFormatBlock = { 33 | "type": "time_format", 34 | "message0": "日期时间格式 %1 13位时间戳 %2", 35 | "args0": [ 36 | { 37 | "type": "field_input", 38 | "name": "fmt", 39 | "text": "yyyy-MM-dd hh:mm:ss" 40 | }, 41 | { 42 | "type": "input_value", 43 | "name": "date" 44 | } 45 | ], 46 | "output": null, 47 | "imports":{koishi:['Time']}, 48 | "colour": 230, 49 | "tooltip": "", 50 | "helpUrl": "" 51 | } 52 | export function timeFormatBlockGenerator(block) { 53 | var text_date = javascriptGenerator.valueToCode(block, 'date', javascriptGenerator.ORDER_ATOMIC); 54 | var text_fmt = block.getFieldValue('fmt'); 55 | return [`Time.template('${text_fmt}',new Date(${text_date}))`, javascriptGenerator.ORDER_NONE]; 56 | } 57 | 58 | export const PluginIdBlock = { 59 | "type": "plugin_id", 60 | "message0": "当前插件ID", 61 | "output": null, 62 | "colour": 230, 63 | "tooltip": "", 64 | "template":["plugin_id"], 65 | "helpUrl": "" 66 | } 67 | 68 | export function pluginIdBlockGenerator(block) { 69 | return [`plugin_id`, javascriptGenerator.ORDER_NONE]; 70 | } 71 | 72 | export const EnvironmentBlocks = [ 73 | TimestampBlock, 74 | TimeFormatBlock, 75 | PluginIdBlock 76 | ] 77 | 78 | export const environmentBlockGenerators = { 79 | 'time_stamp':timestampBlockGenerator, 80 | 'time_format':timeFormatBlockGenerator, 81 | 'plugin_id':pluginIdBlockGenerator 82 | } 83 | -------------------------------------------------------------------------------- /client/blockly/fields/binding.ts: -------------------------------------------------------------------------------- 1 | import {Field, FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption} from "blockly"; 2 | import {ReactiveBindingSet, ReactiveValue} from "../binding"; 3 | 4 | export class FieldBindingStringDropdown extends FieldDropdown { 5 | 6 | protected override menuGenerator_: MenuGeneratorFunction | null; 7 | 8 | protected trace = Symbol('Dropdown Tracing') 9 | 10 | constructor(protected targets: ReactiveBindingSet>) { 11 | super(Field.SKIP_SETUP) 12 | this.menuGenerator_ = FieldBindingStringDropdown.generateBindingMenu as MenuGeneratorFunction 13 | targets.forEach((target) => { 14 | this.traceReactive(target) 15 | }) 16 | targets.addWatcher((bindingSet, value, type) => { 17 | if (type === 'add') { 18 | this.traceReactive(value) 19 | } else { 20 | this.detachReactive(value) 21 | } 22 | }) 23 | } 24 | 25 | static generateBindingMenu(this: FieldBindingStringDropdown) { 26 | return Array.from(this.targets).map((target) => { 27 | return [target.value, target.identifier] 28 | }) 29 | } 30 | 31 | traceReactive(value: ReactiveValue) { 32 | if (value[this.trace]) return; 33 | value[this.trace] = value.addWatcher((s) => { 34 | if (this.value_ == value.identifier) { 35 | this.value_ = value.identifier 36 | this.doValueUpdate_(value.identifier) 37 | this.forceRerender() 38 | } 39 | }) 40 | } 41 | 42 | detachReactive(value: ReactiveValue) { 43 | if (!value[this.trace]) return; 44 | value.removeWatcher(value[this.trace]); 45 | if (this.value_ == value.identifier) { 46 | this.value_ = undefined 47 | this.forceRerender() 48 | } 49 | delete value[this.trace]; 50 | } 51 | 52 | dispose() { 53 | this.targets.forEach((target) => { 54 | this.detachReactive(target) 55 | }) 56 | super.dispose() 57 | } 58 | 59 | protected getDisplayText_(): string { 60 | return (this.menuGenerator_ as () => any)().find(t => t[1] == this.value_)?.[0] ?? '' 61 | } 62 | 63 | override doClassValidation_(opt_newValue) { 64 | return opt_newValue 65 | } 66 | 67 | getOptions(opt_useCache?: boolean): MenuOption[] { 68 | if(this.menuGenerator_().length==0)return [] 69 | return super.getOptions(opt_useCache); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/scope.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { describe,it } from 'mocha' 3 | import { Workspace } from "blockly"; 4 | import * as Blockly from "blockly"; 5 | 6 | // @ts-ignore 7 | import {TestBlocks} from "./blocks"; 8 | // @ts-ignore 9 | import {registerScope} from '../client/blockly/plugins/scope' 10 | 11 | function assertCallbackExecuted(callback:Function){ 12 | expect(callback).to.be.called 13 | } 14 | 15 | describe('Blockly Scope Manager',()=>{ 16 | Blockly.defineBlocksWithJsonArray(TestBlocks) 17 | const workspace = new Workspace() 18 | registerScope(workspace) 19 | const provider = workspace.newBlock('test_scope_provider') 20 | const noop1 = workspace.newBlock('test_scope_noop') 21 | const noop2 = workspace.newBlock('test_scope_noop') 22 | const consumer = workspace.newBlock('test_scope_consumer') 23 | 24 | provider.getInput('NAME').connection.connect(noop1.previousConnection) 25 | noop1.getInput('NAME').connection.connect(noop2.previousConnection) 26 | noop2.getInput('NAME').connection.connect(consumer.previousConnection) 27 | 28 | 29 | 30 | it("Call the watch function when firstly using",(done)=>{ 31 | provider.block_state.providing.push('test') 32 | let disposable 33 | disposable = consumer.block_state.using('test',()=>{ 34 | disposable() 35 | done() 36 | }) 37 | }) 38 | 39 | it("Call the watch function when disconnect some nodes in the path",(done)=>{ 40 | let counter = 0; 41 | const disposable = consumer.block_state.using('test',()=>{ 42 | counter++; 43 | if(counter==2){ 44 | noop2.getInput('NAME').connection.connect(consumer.previousConnection) 45 | disposable() 46 | done() 47 | } 48 | }) 49 | setTimeout(()=>{ 50 | noop2.getInput('NAME').connection.disconnect() 51 | }) 52 | }) 53 | 54 | it("Call the watch function when reconnect some nodes in the path",(done)=>{ 55 | let counter = 0; 56 | const disposable = consumer.block_state.using('test',()=>{ 57 | counter++; 58 | if(counter==3){ 59 | disposable() 60 | done() 61 | } 62 | }) 63 | setTimeout(()=>{ 64 | noop2.getInput('NAME').connection.disconnect() 65 | setTimeout(()=>{ 66 | noop2.getInput('NAME').connection.connect(consumer.previousConnection) 67 | }) 68 | }) 69 | }) 70 | 71 | }) 72 | -------------------------------------------------------------------------------- /client/blockly/blocks/bot.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const DeleteMessageBlock = { 4 | "type": "delete_message", 5 | "message0": "撤回消息 %1 机器人对象 %2 消息ID %3 频道ID %4", 6 | "args0": [ 7 | { 8 | "type": "input_dummy" 9 | }, 10 | { 11 | "type": "input_value", 12 | "name": "bot" 13 | }, 14 | { 15 | "type": "input_value", 16 | "name": "message_id" 17 | }, 18 | { 19 | "type": "input_value", 20 | "name": "channel_id" 21 | } 22 | ], 23 | "inputsInline": false, 24 | "previousStatement": null, 25 | "nextStatement": null, 26 | "colour": 230, 27 | "tooltip": "", 28 | "helpUrl": "" 29 | } 30 | 31 | export function deleteMessageBlockGenerator(block){ 32 | let bot = javascriptGenerator.valueToCode(block, 'bot', javascriptGenerator.ORDER_ATOMIC) 33 | let message_id = javascriptGenerator.valueToCode(block, 'message_id', javascriptGenerator.ORDER_ATOMIC) 34 | let channel_id = javascriptGenerator.valueToCode(block, 'channel_id', javascriptGenerator.ORDER_ATOMIC) 35 | return `await ${bot}.deleteMessage(${channel_id},${message_id});\n` 36 | } 37 | 38 | export const MuteUserBlock = { 39 | "type": "mute_user", 40 | "message0": "禁言用户 %1 机器人对象 %2 用户ID %3 群组ID %4 禁言时间(毫秒) %5", 41 | "args0": [ 42 | { 43 | "type": "input_dummy" 44 | }, 45 | { 46 | "type": "input_value", 47 | "name": "bot" 48 | }, 49 | { 50 | "type": "input_value", 51 | "name": "user_id" 52 | }, 53 | { 54 | "type": "input_value", 55 | "name": "guild_id" 56 | }, 57 | { 58 | "type": "input_value", 59 | "name": "time" 60 | } 61 | ], 62 | "inputsInline": false, 63 | "previousStatement": null, 64 | "nextStatement": null, 65 | "colour": 230, 66 | "tooltip": "", 67 | "helpUrl": "" 68 | } 69 | 70 | export function muteUserGenerator(block){ 71 | let bot = javascriptGenerator.valueToCode(block, 'bot', javascriptGenerator.ORDER_ATOMIC) 72 | let user_id = javascriptGenerator.valueToCode(block, 'user_id', javascriptGenerator.ORDER_ATOMIC) 73 | let guild_id = javascriptGenerator.valueToCode(block, 'guild_id', javascriptGenerator.ORDER_ATOMIC) 74 | let time = javascriptGenerator.valueToCode(block, 'time', javascriptGenerator.ORDER_ATOMIC) 75 | return `await ${bot}.muteGuildMember(${guild_id},${user_id},${time});\n` 76 | } 77 | 78 | export const BotBlocks = [ 79 | DeleteMessageBlock, 80 | MuteUserBlock 81 | ] 82 | 83 | export const botBlockGenerators = { 84 | 'delete_message':deleteMessageBlockGenerator, 85 | 'mute_user':muteUserGenerator 86 | } 87 | -------------------------------------------------------------------------------- /src/console.ts: -------------------------------------------------------------------------------- 1 | import {Context} from "koishi"; 2 | import {} from "./structure" 3 | import {v4 as uuidV4} from "uuid"; 4 | 5 | declare module '@koishijs/plugin-console' { 6 | interface Events { 7 | 'create-blockly-block'(uuid?:string): Promise 8 | 'save-blockly-block'(id:number, data:{body?:object,code?:string,name?:string}): void 9 | 'load-blockly-block'(id:number): Promise 10 | 'rename-blockly-block'(id:number, name:string): Promise 11 | 'delete-blockly-block'(id:number): Promise 12 | 'set-blockly-block-state'(id:number,enabled:boolean): Promise 13 | } 14 | } 15 | 16 | export function initializeConsoleApiBacked(ctx:Context){ 17 | ctx.console.addListener("load-blockly-block",async (id:number)=>{ 18 | return JSON.parse((await ctx.database.get("blockly",id,['body']))[0].body) 19 | },{authority:5}) 20 | 21 | ctx.console.addListener("save-blockly-block",async (id:number, data)=>{ 22 | const save_object = {} 23 | if(data.body)save_object['body'] = JSON.stringify(data.body) 24 | if(data.code)save_object['code'] = data.code 25 | if(data.name)save_object['name'] = data.name 26 | save_object ['edited'] = !data.code 27 | await ctx.database.set("blockly",id,save_object) 28 | setTimeout(()=>ctx.blockly.reload(!!data.code),0) 29 | //console.info(save_object) 30 | },{authority:5}) 31 | 32 | ctx.console.addListener("rename-blockly-block",async (id:number, name:string)=>{ 33 | await ctx.database.set("blockly",id,{name}) 34 | await ctx.blockly.reload() 35 | },{authority:5}) 36 | 37 | ctx.console.addListener("rename-blockly-block",async (id:number, name:string)=>{ 38 | await ctx.database.set("blockly",id,{name}) 39 | await ctx.blockly.reload() 40 | },{authority:5}) 41 | 42 | ctx.console.addListener("delete-blockly-block",async (id:number)=>{ 43 | await ctx.database.remove("blockly",{id}) 44 | await ctx.blockly.reload() 45 | },{authority:5}) 46 | 47 | ctx.console.addListener("create-blockly-block",async (uuid)=>{ 48 | 49 | if(uuid){ 50 | const blocks = await ctx.database.get("blockly",{uuid},['id']) 51 | if(blocks.length>0)return blocks[0].id 52 | } 53 | 54 | const data = await ctx.database.create('blockly',{ 55 | name:'未命名Koishi代码', 56 | code:'', 57 | body:'{}', 58 | enabled:false, 59 | edited:false, 60 | uuid:uuid??uuidV4() 61 | }) 62 | 63 | await ctx.blockly.reload() 64 | return data.id 65 | },{authority:5}) 66 | 67 | ctx.console.addListener("set-blockly-block-state",async (id, enabled)=>{ 68 | await ctx.database.set("blockly",id,{enabled}) 69 | await ctx.blockly.reload(true) 70 | },{authority:5}) 71 | } 72 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@koishijs/vitepress' 2 | 3 | export default defineConfig({ 4 | lang: 'zh-CN', 5 | title: 'koishi-plugin-blockly', 6 | description: 'Blockly 可视化编程插件', 7 | 8 | head: [ 9 | ['link', { rel: 'icon', href: 'https://koishi.chat/logo.png' }], 10 | ['link', { rel: 'manifest', href: 'https://koishi.chat/manifest.json' }], 11 | ['meta', { name: 'theme-color', content: '#5546a3' }], 12 | ], 13 | 14 | themeConfig: { 15 | nav: [{ 16 | text: '指南', 17 | link: './', 18 | }, { 19 | text: '参考', 20 | link: './reference/', 21 | // }, { 22 | // text: '扩展', 23 | // link: './develop/', 24 | }, { 25 | text: '更多', 26 | items: [ 27 | { text: 'Koishi 官网', link: 'https://koishi.chat' }, 28 | { text: 'Koishi 论坛', link: 'https://forum.koishi.xyz' }, 29 | ], 30 | }], 31 | 32 | sidebar: { 33 | '/develop/': [{ 34 | items: [ 35 | { text: '扩展', link: './develop/' }, 36 | ], 37 | }], 38 | '/reference/': [{ 39 | items: [ 40 | { text: '总览', link: './reference/' }, 41 | ], 42 | }, { 43 | text: '块', 44 | items: [ 45 | { text: '逻辑', link: './reference/block/logical.md' }, 46 | { text: '循环', link: './reference/block/loop.md' }, 47 | { text: '数学', link: './reference/block/math.md' }, 48 | { text: '文本', link: './reference/block/string.md' }, 49 | { text: '列表', link: './reference/block/array.md' }, 50 | { text: '变量', link: './reference/block/variable.md' }, 51 | { text: '事件', link: './reference/block/event.md' }, 52 | { text: '会话', link: './reference/block/session.md' }, 53 | { text: '消息', link: './reference/block/message.md' }, 54 | { text: '元素', link: './reference/block/element.md' }, 55 | { text: '数据', link: './reference/block/data.md' }, 56 | { text: '操作', link: './reference/block/bot.md' }, 57 | { text: '调试', link: './reference/block/debug.md' }, 58 | ], 59 | // }, { 60 | // text: '流', 61 | // items: [], 62 | }], 63 | '/': [{ 64 | text: '指南', 65 | items: [ 66 | { text: '介绍', link: './' }, 67 | { text: '搭建', link: './starter.md' }, 68 | { text: '用法', link: './usage.md' }, 69 | ], 70 | }, { 71 | text: '示例', 72 | items: [ 73 | { text: '你好,世界', link: './examples/hello-world.md' }, 74 | { text: 'at 人功能', link: './examples/mention.md' }, 75 | { text: '查询天气', link: './examples/weather.md' }, 76 | { text: '指令参数', link: './examples/argument.md' }, 77 | { text: '发送图片', link: './examples/image.md' }, 78 | { text: '平台功能', link: './examples/platform.md' }, 79 | ], 80 | }, { 81 | text: '更多', 82 | items: [ 83 | { text: 'Koishi 官网', link: 'https://koishi.chat' }, 84 | { text: 'Koishi 论坛', link: 'https://forum.koishi.xyz' }, 85 | ], 86 | }], 87 | }, 88 | }, 89 | }) 90 | -------------------------------------------------------------------------------- /client/blockly/extensions/segment.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from 'blockly' 2 | import {unregisterIfRegistered} from "./index"; 3 | import {BlockSvg, FieldDropdown} from "blockly"; 4 | export function registerSegmentParserMutator(){ 5 | unregisterIfRegistered('segment_parser') 6 | Blockly.Extensions.registerMutator('segment_parser', { 7 | decompose: function(workspace) { 8 | let topBlock = workspace.newBlock('parse_segment_root'); 9 | topBlock.initSvg(); 10 | let connection = topBlock.getInput('types').connection; 11 | const branches : Blockly.Input[] = this.inputList.filter(t=>t.name.startsWith('branch_type_')) 12 | branches.forEach(s=>{ 13 | const type = this.getFieldValue(s.name+'_selector') 14 | let itemBlock = workspace.newBlock("segment_type"); 15 | itemBlock.setFieldValue(type,'type') 16 | itemBlock.initSvg() 17 | connection.connect(itemBlock.previousConnection); 18 | connection = itemBlock.nextConnection; 19 | }) 20 | return topBlock; 21 | }, 22 | compose: function(topBlock) { 23 | this.inputList.filter(t=>t.name.startsWith('branch_type_') || t.name.startsWith('branch_action_')) 24 | .forEach(t=>this.removeInput(t.name)) 25 | const branches = topBlock.getChildren() 26 | if(!branches.length || branches.length<=0){ 27 | return; 28 | } 29 | let current = branches[0] 30 | let i = 0 31 | while(current){ 32 | this.addBlockInput(current.getFieldValue("type"),i) 33 | current = current.getNextBlock(); 34 | i++; 35 | } 36 | }, 37 | saveExtraState: function() { 38 | const inputs = this.inputList.filter(t=>t.name.startsWith('branch_type_')).map(s=>this.getFieldValue(s.name+'_selector')) 39 | console.info(inputs) 40 | return inputs 41 | }, 42 | 43 | loadExtraState: function(state) { 44 | state?.forEach((t,i)=>this.addBlockInput(t,i)) 45 | console.info(state) 46 | }, 47 | addBlockInput(type,i){ 48 | if(this.inputList.some(t=>t.name == "branch_type_"+i))return; 49 | const select = this.appendDummyInput("branch_type_"+i) 50 | select.appendField("如果消息元素是") 51 | select.appendField(new FieldDropdown([ 52 | ["at消息", "at"], 53 | ["引用回复", "quote"], 54 | ["图片", "image"] 55 | ]),"branch_type_"+i+"_selector") 56 | this.setFieldValue(type,"branch_type_"+i+"_selector") 57 | const action = this.appendStatementInput("branch_action_"+i) 58 | action.appendField("则运行") 59 | } 60 | },undefined,["segment_type"]) 61 | Blockly.defineBlocksWithJsonArray([{ 62 | "type": "segment_type", 63 | "message0": "消息段类型 %1", 64 | "args0": [ 65 | { 66 | "type": "field_dropdown", 67 | "name": "type", 68 | "options": [ 69 | ["at消息", "at"], 70 | ["引用回复", "quote"], 71 | ["图片", "image"] 72 | ] 73 | } 74 | ], 75 | "previousStatement": null, 76 | "nextStatement": null, 77 | "colour": 230, 78 | "tooltip": "", 79 | "helpUrl": "" 80 | },{ 81 | "type": "parse_segment_root", 82 | "message0": "要解析的消息段类型 %1 %2", 83 | "args0": [ 84 | { 85 | "type": "input_dummy" 86 | }, 87 | { 88 | "type": "input_statement", 89 | "name": "types" 90 | } 91 | ], 92 | "colour": 230, 93 | "tooltip": "", 94 | "helpUrl": "" 95 | }]) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /client/blockly/blocks/text.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | import {TextTemplateIcon} from "../../icons/template"; 3 | import {FieldImage} from "blockly"; 4 | import {ElMessageBox} from "element-plus"; 5 | import templates from "rollup-plugin-visualizer/dist/plugin/template-types"; 6 | export const RegularBlock = { 7 | "type": "regular", 8 | "message0": "正则表达式 %1 %2", 9 | "args0": [ 10 | { 11 | "type": "field_input", 12 | "name": "regular", 13 | "text": "/.*/g" 14 | }, 15 | { 16 | "type": "input_value", 17 | "name": "str" 18 | } 19 | ], 20 | "output": null, 21 | "colour": 160, 22 | "tooltip": "", 23 | "helpUrl": "" 24 | } 25 | 26 | export function regularBlockGenerator(block) { 27 | let text_regular = block.getFieldValue('regular') 28 | let value_text = javascriptGenerator.valueToCode(block, 'str', javascriptGenerator.ORDER_ATOMIC) 29 | let reg_exp = text_regular?.match("\/(.*)\/([gmiyusd]*)") 30 | return [`(${value_text}).match(new RegExp("${reg_exp[1]}", "${reg_exp[2]}"))`,javascriptGenerator.ORDER_ATOMIC] 31 | } 32 | 33 | export const TemplateStringBlock = { 34 | "type": "template_string", 35 | "message0": "%1 模板字符串", 36 | "args0": [ 37 | { 38 | "type": "field_image", 39 | "name":"edit_template", 40 | "src": TextTemplateIcon, 41 | "alt": "编辑模板", 42 | "width": 25, 43 | "height": 25 44 | } 45 | ], 46 | "output": null, 47 | "colour": 160, 48 | init(){ 49 | this.loadSlotsWithTemplate = function(){ 50 | this.inputList 51 | .filter((input)=>input.name.startsWith("input_")) 52 | .forEach((input)=>this.removeInput(input.name)) 53 | if(!this.text_template){ 54 | return; 55 | } 56 | this.text_template.variables.forEach((variable)=>{ 57 | this.appendValueInput(`input_${variable}`) 58 | .appendField(variable) 59 | }) 60 | }; 61 | (this.getField("edit_template") as FieldImage).setOnClickHandler(async (field)=> { 62 | // Show the editor , wait for the flow engine merged 63 | const workspace = field.getSourceBlock().workspace 64 | this.text_template = await workspace['topLevel'].openDialog('text-template',this.text_template??{variables:[]}) 65 | console.info(this.text_template) 66 | this.loadSlotsWithTemplate() 67 | }) 68 | this.saveExtraState = function(){ 69 | return { 70 | template:this.text_template 71 | } 72 | } 73 | this.loadExtraState = function(state){ 74 | this.text_template = state.template 75 | this.loadSlotsWithTemplate() 76 | } 77 | } 78 | } 79 | 80 | export function templateStringBlockGenerator(block) { 81 | if(!block.text_template){ 82 | return ["\"\"",javascriptGenerator.ORDER_ATOMIC] 83 | } 84 | const variables = block.text_template.variables 85 | const values = variables.map((variable)=>{ 86 | return javascriptGenerator.valueToCode(block, `input_${variable}`, javascriptGenerator.ORDER_ATOMIC) 87 | }) 88 | const template = ((block.text_template.template as string).replace(/\$\{([^}]+)\}/g,(match,variable)=>{ 89 | return `\${${values[variables.indexOf(variable)]}}` 90 | }) as any).replaceAll("\n","\\n").replaceAll("`","\\`") 91 | return [`\`${template}\``,javascriptGenerator.ORDER_ATOMIC] 92 | } 93 | 94 | export const TextBlocks = [ 95 | TemplateStringBlock, 96 | RegularBlock 97 | ] 98 | 99 | export const textBlockGenerators = { 100 | "regular":regularBlockGenerator, 101 | "template_string":templateStringBlockGenerator 102 | } 103 | -------------------------------------------------------------------------------- /client/api/manager.ts: -------------------------------------------------------------------------------- 1 | import {send, store} from "@koishijs/client"; 2 | import {decodeBlocklyExport, encodeBlocklyExport} from "../blockly/pack"; 3 | import {build, createWrapper} from "../blockly/build"; 4 | import {javascriptGenerator} from "blockly/javascript"; 5 | import * as semver from 'semver' 6 | import {ElMessageBox} from "element-plus"; 7 | import {BLOCKLY_API_VERSION, BLOCKLY_VERSION} from "../version"; 8 | 9 | export const createBlockly = async () => (await send('create-blockly-block')).toString() 10 | 11 | export const saveBlockly = async (current,workspace) => { 12 | if(current==undefined)return; 13 | await send('save-blockly-block',current,{body:workspace.save()}) 14 | } 15 | 16 | export async function buildBlockly(id:number|undefined,name:string,plugin_id:string,workspace,logger){ 17 | if(id==undefined)return 18 | logger.info("正在开始编译.......") 19 | let code 20 | try { 21 | code = build(name,plugin_id,workspace.getWorkspaceSvg()) 22 | }catch (e){ 23 | logger.error("编译时发生错误: "+e.toString()) 24 | console.error(e) 25 | return 26 | } 27 | logger.info("正在上传......") 28 | await send('save-blockly-block',id,{code}) 29 | logger.success("上传成功") 30 | return code 31 | } 32 | 33 | export async function enableBlockly(id:number|undefined){ 34 | if(id==undefined)return; 35 | await send('set-blockly-block-state',id,true) 36 | } 37 | 38 | export async function disableBlockly(id:number|undefined){ 39 | if(id==undefined)return; 40 | await send('set-blockly-block-state',id,false) 41 | } 42 | 43 | export async function renameBlockly(id:number|undefined,name:string){ 44 | if(id==undefined){ 45 | return 46 | } 47 | await send('rename-blockly-block',id,name) 48 | } 49 | 50 | export async function deletePlugin(id:number|undefined){ 51 | if(id==undefined)return 52 | await send('delete-blockly-block',id) 53 | } 54 | 55 | export async function exportPlugin(id:number|undefined,name:string,uuid:string,workspace){ 56 | if(id==undefined)return 57 | return encodeBlocklyExport(name,uuid,workspace.save()) 58 | } 59 | 60 | export async function importPlugin(content,asNewPlugin) { 61 | if (content.length == 0) { 62 | return 63 | } 64 | const documentData = decodeBlocklyExport(content) 65 | console.info(documentData) 66 | if (!documentData) { 67 | return 68 | } 69 | if(documentData.version!=BLOCKLY_API_VERSION) { 70 | ElMessageBox.alert(`无法读取该插件: 该插件的API Major版本(${documentData.version})不兼容当前版本`) 71 | return 72 | } 73 | if(documentData.plugin_version && semver.cmp(documentData.plugin_version,'>',BLOCKLY_VERSION)){ 74 | if(await ElMessageBox.confirm(`该插件是由更高版本(${documentData.plugin_version})的Blockly插件导出的,可能无法正常导入或执行异常。建议升级您的Blockly版本到最新版本`, '提示', { 75 | confirmButtonText: '继续', 76 | cancelButtonText: '取消', 77 | type: 'warning' 78 | }) == 'cancel') 79 | return 80 | } 81 | if(documentData.plugin_version && semver.major(documentData.plugin_version)!=semver.major(BLOCKLY_VERSION)){ 82 | if(await ElMessageBox.confirm(`该插件是由不同主版本(${documentData.plugin_version})的Blockly插件导出的,可能无法正常导入或执行异常。`, '提示', { 83 | confirmButtonText: '继续', 84 | cancelButtonText: '取消', 85 | type: 'warning' 86 | }) == 'cancel') 87 | return 88 | } 89 | if(!asNewPlugin && documentData?.uuid){ 90 | if(store.blockly.filter(t=>t.uuid==documentData.uuid).length>0){ 91 | if(await ElMessageBox.confirm('检测到该插件已存在,是否覆盖?', '提示', { 92 | confirmButtonText: '确定', 93 | cancelButtonText: '取消', 94 | type: 'warning' 95 | }) == 'cancel') 96 | return 97 | } 98 | } 99 | const id = await send('create-blockly-block',asNewPlugin?undefined:documentData.uuid) 100 | await send('save-blockly-block', id, { 101 | body: documentData.body, 102 | name: documentData.name 103 | }) 104 | return id.toString() 105 | } 106 | -------------------------------------------------------------------------------- /client/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 129 | -------------------------------------------------------------------------------- /client/blockly/extensions/type.ts: -------------------------------------------------------------------------------- 1 | import {unregisterIfRegistered} from "./index"; 2 | import * as Blockly from 'blockly' 3 | import {BlockSvg} from "blockly"; 4 | export function typeMutatorExtension(){ 5 | unregisterIfRegistered("union_mutator") 6 | Blockly.Extensions.registerMutator('union_mutator', { 7 | updateShape_: function(){ 8 | // Delete every input which starts with "TYPE_" and less than itemCount_ 9 | this.inputList 10 | .filter(input => input.name.startsWith("TYPE_")) 11 | .filter(input => parseInt(input.name.split("_")[1]) < this.itemCount_) 12 | .forEach(input => this.removeInput(input.name)) 13 | // Add new inputs 14 | for(let i = 0; i < this.itemCount_; i++){ 15 | if(!this.getInput("TYPE_" + i)){ 16 | const input = this.appendValueInput("TYPE_" + i) 17 | input.align = Blockly.ALIGN_RIGHT 18 | if(i == 0){ 19 | input.appendField("类型") 20 | }else{ 21 | input.appendField("或") 22 | } 23 | } 24 | } 25 | }, 26 | decompose: function(workspace){ 27 | const containerBlock = workspace.newBlock('type_union_root'); 28 | containerBlock.initSvg(); 29 | let connection = containerBlock.getInput('types').connection; 30 | for(let i = 0; i < this.itemCount_; i++){ 31 | const itemBlock = workspace.newBlock('type_union_entity'); 32 | itemBlock.initSvg(); 33 | connection.connect(itemBlock.previousConnection); 34 | connection = itemBlock.nextConnection; 35 | } 36 | return containerBlock; 37 | }, 38 | compose: function(topBlock){ 39 | this.itemCount_ = 0; 40 | let itemBlock = topBlock.getInputTargetBlock('types'); 41 | while(itemBlock){ 42 | this.itemCount_++; 43 | itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock(); 44 | } 45 | this.updateShape_(); 46 | }, 47 | saveExtraState: function(workspace){ 48 | return { 49 | 'itemCount': this.itemCount_ 50 | } 51 | }, 52 | loadExtraState: function(state){ 53 | this.itemCount_ = state['itemCount']; 54 | this.updateShape_() 55 | } 56 | },undefined,['type_union_entity']) 57 | 58 | unregisterIfRegistered("object_mutator") 59 | Blockly.Extensions.registerMutator('object_mutator', { 60 | updateShape_: function(){ 61 | // Each property is a value input , and contains a field for the property name 62 | this.properties_?.forEach((property,index)=>{ 63 | if(!this.getInput("PROPERTY_" + property)){ 64 | const input = this.appendValueInput("PROPERTY_" + property) 65 | input.align = Blockly.ALIGN_RIGHT 66 | input.appendField(property) 67 | } 68 | }) 69 | this.inputList.filter(input => input.name.startsWith("PROPERTY_")) 70 | .filter(input => !this.properties_.includes(input.name.split("_")[1])) 71 | .forEach(input => this.removeInput(input.name)) 72 | }, 73 | decompose: function(workspace){ 74 | const containerBlock = workspace.newBlock('type_object_root'); 75 | containerBlock.initSvg(); 76 | let connection = containerBlock.getInput('properties').connection; 77 | this.properties_?.forEach((property)=>{ 78 | const itemBlock = workspace.newBlock('type_object_entity'); 79 | itemBlock.initSvg(); 80 | itemBlock.setFieldValue(property,'name') 81 | connection.connect(itemBlock.previousConnection); 82 | connection = itemBlock.nextConnection; 83 | }) 84 | return containerBlock; 85 | }, 86 | compose: function(topBlock){ 87 | const properties = [] 88 | let itemBlock = topBlock.getInputTargetBlock('properties'); 89 | while(itemBlock){ 90 | properties.push(itemBlock.getFieldValue('name')) 91 | itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock(); 92 | } 93 | this.properties_ = properties; 94 | this.updateShape_() 95 | }, 96 | saveExtraState: function(workspace){ 97 | return { 98 | 'properties': this.properties_ 99 | } 100 | }, 101 | loadExtraState: function(state){ 102 | this.properties_ = state['properties']; 103 | this.updateShape_() 104 | } 105 | },undefined,['type_object_entity']) 106 | } 107 | -------------------------------------------------------------------------------- /client/blockly/blocks/session.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from 'blockly/javascript' 2 | 3 | export const GetArgumentBlock = { 4 | "type": "get_argument", 5 | "message0": "第 %1 个参数", 6 | "args0": [ 7 | { 8 | "type": "field_number", 9 | "name": "id", 10 | "value": 0 11 | } 12 | ], 13 | "output": "String", 14 | "extensions":["argument_consumer"], 15 | "colour": 230, 16 | "tooltip": "", 17 | "helpUrl": "" 18 | } 19 | 20 | export function getArgumentBlockGenerator(block){ 21 | let argument_id = block.getFieldValue('id'); 22 | return [`args[${argument_id}]`,javascriptGenerator.ORDER_NONE] 23 | } 24 | 25 | export const BreakMiddlewareBlock = { 26 | "type": "break_middleware", 27 | "message0": "终止后续逻辑执行", 28 | "previousStatement": null, 29 | "extensions":['session_consumer'], 30 | "colour": 230, 31 | "tooltip": "", 32 | "helpUrl": "" 33 | }; 34 | 35 | export function breakMiddlewareBlockGenerator(){ 36 | return 'return null;\n'; 37 | } 38 | 39 | export const SessionMessageBlock = { 40 | "type": "session_message", 41 | "message0": "发送消息的内容", 42 | "extensions":['session_consumer'], 43 | "output": "String", 44 | "colour": 230, 45 | "tooltip": "", 46 | "helpUrl": "" 47 | } 48 | 49 | export function sessionMessageBlockGenerator(){ 50 | return [`session.content`,javascriptGenerator.ORDER_NONE]; 51 | } 52 | 53 | 54 | export const SessionUserIdBlock = { 55 | "type": "session_user_id", 56 | "message0": "发送消息用户的平台用户ID", 57 | "output": "String", 58 | "extensions":['session_consumer'], 59 | "colour": 230, 60 | "tooltip": "", 61 | "helpUrl": "" 62 | } 63 | 64 | export function sessionUserIdBlockGenerator(){ 65 | return [`session.userId`,javascriptGenerator.ORDER_NONE]; 66 | } 67 | 68 | export const SessionBotBlock = { 69 | "type": "session_bot", 70 | "message0": "收到消息的机器人对象", 71 | "output": "String", 72 | "extensions":['session_consumer'], 73 | "colour": 230, 74 | "tooltip": "", 75 | "helpUrl": "" 76 | } 77 | 78 | export function sessionBotBlockGenerator(){ 79 | return [`session.bot`,javascriptGenerator.ORDER_NONE]; 80 | } 81 | 82 | export const SessionGuildIdBlock = { 83 | "type": "session_guild_id", 84 | "message0": "消息来自的群组编号", 85 | "output": "String", 86 | "extensions":['session_consumer'], 87 | "colour": 230, 88 | "tooltip": "", 89 | "helpUrl": "" 90 | } 91 | 92 | export function sessionChannelIdBlockGenerator(){ 93 | return [`session.channelId`,javascriptGenerator.ORDER_NONE]; 94 | } 95 | 96 | export const SessionChannelIdBlock = { 97 | "type": "session_channel_id", 98 | "message0": "消息来自的频道编号(群号)", 99 | "output": "String", 100 | "extensions":['session_consumer'], 101 | "colour": 230, 102 | "tooltip": "", 103 | "helpUrl": "" 104 | } 105 | 106 | export function sessionGuildIdBlockGenerator(){ 107 | return [`session.guildId`,javascriptGenerator.ORDER_NONE]; 108 | } 109 | 110 | export const SessionMessageIdBlock = { 111 | "type": "session_message_id", 112 | "message0": "消息编号", 113 | "output": "String", 114 | "extensions":['session_consumer'], 115 | "colour": 230, 116 | "tooltip": "", 117 | "helpUrl": "" 118 | } 119 | 120 | export function sessionMessageIdBlockGenerator(){ 121 | return [`session.messageId`,javascriptGenerator.ORDER_NONE]; 122 | } 123 | 124 | export const SessionElementsBlock = { 125 | "type": "session_elements", 126 | "message0": "当前消息的消息元素列表", 127 | "output": "Array", 128 | "extensions":['session_consumer'], 129 | "colour": 230, 130 | "tooltip": "", 131 | "helpUrl": "" 132 | } 133 | 134 | export function sessionElementsBlockGenerator(){ 135 | return [`session.elements`,javascriptGenerator.ORDER_NONE]; 136 | } 137 | 138 | export const SessionBlocks = [ 139 | GetArgumentBlock, 140 | BreakMiddlewareBlock, 141 | SessionMessageBlock, 142 | SessionUserIdBlock, 143 | SessionChannelIdBlock, 144 | SessionGuildIdBlock, 145 | SessionMessageIdBlock, 146 | SessionBotBlock, 147 | SessionElementsBlock, 148 | ] 149 | 150 | export const sessionBlockGenerators = { 151 | 'get_argument':getArgumentBlockGenerator, 152 | 'break_middleware':breakMiddlewareBlockGenerator, 153 | 'session_message':sessionMessageBlockGenerator, 154 | 'session_user_id':sessionUserIdBlockGenerator, 155 | 'session_channel_id':sessionChannelIdBlockGenerator, 156 | 'session_guild_id':sessionGuildIdBlockGenerator, 157 | 'session_message_id':sessionMessageIdBlockGenerator, 158 | 'session_bot':sessionBotBlockGenerator, 159 | 'session_elements':sessionElementsBlockGenerator, 160 | } 161 | -------------------------------------------------------------------------------- /client/blockly/blocks/parameter.ts: -------------------------------------------------------------------------------- 1 | export const ParameterListBlock = { 2 | "type": "parameter_list", 3 | "message0": "参数列表 %1", 4 | "args0": [ 5 | { 6 | "type": "input_statement", 7 | "name": "parameter_list", 8 | "check": "Parameter" 9 | } 10 | ], 11 | "colour": 230, 12 | "tooltip": "", 13 | "helpUrl": "" 14 | }; 15 | 16 | export const AnyParameter = { 17 | "type": "any_parameter", 18 | "message0": "任何类型参数 %1 %2 参数必选 %3", 19 | "args0": [ 20 | { 21 | "type": "field_input", 22 | "name": "name", 23 | "text": "参数名称" 24 | }, 25 | { 26 | "type": "input_dummy" 27 | }, 28 | { 29 | "type": "field_checkbox", 30 | "name": "required", 31 | "checked": true 32 | } 33 | ], 34 | "previousStatement": "Parameter", 35 | "nextStatement": "Parameter", 36 | "colour": 230, 37 | "tooltip": "", 38 | "helpUrl": "" 39 | } 40 | 41 | export const StringParameter = { 42 | "type": "string_parameter", 43 | "message0": "字符串类型参数 %1 %2 参数必选 %3", 44 | "args0": [ 45 | { 46 | "type": "field_input", 47 | "name": "name", 48 | "text": "参数名称" 49 | }, 50 | { 51 | "type": "input_dummy" 52 | }, 53 | { 54 | "type": "field_checkbox", 55 | "name": "required", 56 | "checked": true 57 | } 58 | ], 59 | "previousStatement": "Parameter", 60 | "nextStatement": "Parameter", 61 | "colour": 230, 62 | "tooltip": "", 63 | "helpUrl": "" 64 | } 65 | 66 | export const NumberParameter = { 67 | "type": "number_parameter", 68 | "message0": "数字类型参数 %1 %2 参数必选 %3", 69 | "args0": [ 70 | { 71 | "type": "field_input", 72 | "name": "name", 73 | "text": "参数名称" 74 | }, 75 | { 76 | "type": "input_dummy" 77 | }, 78 | { 79 | "type": "field_checkbox", 80 | "name": "required", 81 | "checked": true 82 | } 83 | ], 84 | "previousStatement": "Parameter", 85 | "nextStatement": "Parameter", 86 | "colour": 230, 87 | "tooltip": "", 88 | "helpUrl": "" 89 | } 90 | 91 | export const IntParameter = { 92 | "type": "int_parameter", 93 | "message0": "整数类型参数 %1 %2 参数必选 %3", 94 | "args0": [ 95 | { 96 | "type": "field_input", 97 | "name": "name", 98 | "text": "参数名称" 99 | }, 100 | { 101 | "type": "input_dummy" 102 | }, 103 | { 104 | "type": "field_checkbox", 105 | "name": "required", 106 | "checked": true 107 | } 108 | ], 109 | "previousStatement": "Parameter", 110 | "nextStatement": "Parameter", 111 | "colour": 230, 112 | "tooltip": "", 113 | "helpUrl": "" 114 | } 115 | 116 | export const BooleanParameter = { 117 | "type": "boolean_parameter", 118 | "message0": "布尔类型参数 %1 %2 参数必选 %3", 119 | "args0": [ 120 | { 121 | "type": "field_input", 122 | "name": "name", 123 | "text": "参数名称" 124 | }, 125 | { 126 | "type": "input_dummy" 127 | }, 128 | { 129 | "type": "field_checkbox", 130 | "name": "required", 131 | "checked": true 132 | } 133 | ], 134 | "previousStatement": "Parameter", 135 | "nextStatement": "Parameter", 136 | "colour": 230, 137 | "tooltip": "", 138 | "helpUrl": "" 139 | } 140 | 141 | export const PosintParameter = { 142 | "type": "posint_parameter", 143 | "message0": "正整数类型参数 %1 %2 参数必选 %3", 144 | "args0": [ 145 | { 146 | "type": "field_input", 147 | "name": "name", 148 | "text": "参数名称" 149 | }, 150 | { 151 | "type": "input_dummy" 152 | }, 153 | { 154 | "type": "field_checkbox", 155 | "name": "required", 156 | "checked": true 157 | } 158 | ], 159 | "previousStatement": "Parameter", 160 | "nextStatement": "Parameter", 161 | "colour": 230, 162 | "tooltip": "", 163 | "helpUrl": "" 164 | } 165 | 166 | export const TextParameter = { 167 | "type": "text_parameter", 168 | "message0": "长文本类型参数 %1 %2 参数必选 %3", 169 | "args0": [ 170 | { 171 | "type": "field_input", 172 | "name": "name", 173 | "text": "参数名称" 174 | }, 175 | { 176 | "type": "input_dummy" 177 | }, 178 | { 179 | "type": "field_checkbox", 180 | "name": "required", 181 | "checked": true 182 | } 183 | ], 184 | "previousStatement": "Parameter", 185 | "nextStatement": "Parameter", 186 | "colour": 230, 187 | "tooltip": "", 188 | "helpUrl": "" 189 | } 190 | 191 | 192 | export const ParameterBlocks = [ 193 | ParameterListBlock, 194 | AnyParameter, 195 | StringParameter, 196 | NumberParameter, 197 | IntParameter, 198 | BooleanParameter, 199 | PosintParameter, 200 | TextParameter 201 | ] 202 | -------------------------------------------------------------------------------- /client/components/dialogs/text-template.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 124 | 125 | 142 | -------------------------------------------------------------------------------- /client/blockly/blockly.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 128 | 129 | 142 | -------------------------------------------------------------------------------- /client/blockly/blocks/segment.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const SegmentAtBlock = { 4 | "type": "segment_at", 5 | "message0": "@ %1", 6 | "args0": [ 7 | { 8 | "type": "input_value", 9 | "name": "user", 10 | "check": "String" 11 | } 12 | ], 13 | "output": "String", 14 | "imports":{koishi:['h']}, 15 | "colour": 230, 16 | "tooltip": "", 17 | "helpUrl": "" 18 | } 19 | 20 | export function segmentAtBlockGenerator(block){ 21 | let user = javascriptGenerator.valueToCode(block, 'user', javascriptGenerator.ORDER_ATOMIC); 22 | return [`h('at',{ id: ${user} })`,javascriptGenerator.ORDER_NONE]; 23 | } 24 | 25 | export const SegmentImageBlock = { 26 | "type": "segment_image", 27 | "message0": "图片 %1", 28 | "args0": [ 29 | { 30 | "type": "input_value", 31 | "name": "image", 32 | "check": "String" 33 | } 34 | ], 35 | "output": "String", 36 | "imports":{koishi:['h']}, 37 | "colour": 230, 38 | "tooltip": "", 39 | "helpUrl": "" 40 | } 41 | 42 | export function segmentImageBlockGenerator(block){ 43 | let image = javascriptGenerator.valueToCode(block, 'image', javascriptGenerator.ORDER_ATOMIC); 44 | return [`h('image',{ url: ${image} })`,javascriptGenerator.ORDER_NONE]; 45 | } 46 | 47 | export const SegmentAudioBlock = { 48 | "type": "segment_audio", 49 | "message0": "语音 %1", 50 | "args0": [ 51 | { 52 | "type": "input_value", 53 | "name": "audio", 54 | "check": "String" 55 | } 56 | ], 57 | "output": "String", 58 | "imports":{koishi:['h']}, 59 | "colour": 230, 60 | "tooltip": "", 61 | "helpUrl": "" 62 | } 63 | 64 | 65 | export function segmentAudioBlockGenerator(block){ 66 | let audio = javascriptGenerator.valueToCode(block, 'audio', javascriptGenerator.ORDER_ATOMIC); 67 | return [`h('audio',{ url: ${audio} })`,javascriptGenerator.ORDER_NONE] 68 | } 69 | 70 | export const SegmentVideoBlock = { 71 | "type": "segment_video", 72 | "message0": "视频 %1", 73 | "args0": [ 74 | { 75 | "type": "input_value", 76 | "name": "video", 77 | "check": "String" 78 | } 79 | ], 80 | "output": "String", 81 | "imports":{koishi:['h']}, 82 | "colour": 230, 83 | "tooltip": "", 84 | "helpUrl": "" 85 | } 86 | 87 | 88 | export function segmentVideoBlockGenerator(block){ 89 | let video = javascriptGenerator.valueToCode(block, 'video', javascriptGenerator.ORDER_ATOMIC); 90 | return [`h('video',{ url: ${video} })`,javascriptGenerator.ORDER_NONE] 91 | } 92 | 93 | export const ParseSegmentListBlock = { 94 | "type": "parse_segment_list", 95 | "message0": "解析消息元素列表 %1 对每个元素按顺序执行:", 96 | "args0": [ 97 | { 98 | "type": "input_value", 99 | "name": "segments" 100 | } 101 | ], 102 | "colour": 230, 103 | "tooltip": "", 104 | "helpUrl": "", 105 | "inputsInline":false, 106 | "mutator": "segment_parser", 107 | "previousStatement": null, 108 | "nextStatement": null, 109 | } 110 | 111 | export function parseSegmentListBlockGenerator(block) { 112 | let segment = javascriptGenerator.valueToCode(block, 'segments', javascriptGenerator.ORDER_ATOMIC); 113 | let branches = block.inputList.filter(field => field.name.startsWith("branch_type_")).map(field => field.name.substring(12)); 114 | let types = Object.fromEntries(branches.map(branch => [branch, block.getFieldValue(`branch_type_${branch}_selector`)])); 115 | let statements = Object.fromEntries(branches.map(branch => [branch, javascriptGenerator.statementToCode(block,`branch_action_${branch}`, javascriptGenerator.ORDER_ATOMIC)])); 116 | return `for(let current_segment of ${segment}){ 117 | switch(segment.type){ 118 | ${Object.entries(types).map(([branch, type]) => `case '${type}': 119 | ${statements[branch]} 120 | break;`).join('\n')} 121 | } 122 | }` 123 | } 124 | 125 | export const currentSegmentBlock = { 126 | "type": "current_segment", 127 | "message0": "当前指向的消息元素", 128 | "output": null, 129 | "colour": 230, 130 | "tooltip": "", 131 | } 132 | 133 | export function currentSegmentBlockGenerator(block) { 134 | return ['current_segment', javascriptGenerator.ORDER_NONE]; 135 | } 136 | 137 | export const SegmentBlocks = [ 138 | SegmentAtBlock, 139 | SegmentImageBlock, 140 | SegmentAudioBlock, 141 | SegmentVideoBlock, 142 | ParseSegmentListBlock, 143 | currentSegmentBlock 144 | ] 145 | 146 | export const segmentBlockGenerators = { 147 | 'segment_at':segmentAtBlockGenerator, 148 | 'segment_image':segmentImageBlockGenerator, 149 | 'segment_audio':segmentAudioBlockGenerator, 150 | 'segment_video':segmentVideoBlockGenerator, 151 | 'parse_segment_list':parseSegmentListBlockGenerator, 152 | 'current_segment':currentSegmentBlockGenerator 153 | } 154 | -------------------------------------------------------------------------------- /client/blockly/plugins/scope.ts: -------------------------------------------------------------------------------- 1 | import {BlockSvg, Workspace, WorkspaceSvg,Block} from "blockly"; 2 | import {Abstract} from "blockly/core/events/events_abstract"; 3 | import {BlockMove} from "blockly/core/events/events_block_move"; 4 | import {BlockCreate} from "blockly/core/events/events_block_create"; 5 | 6 | declare module "blockly"{ 7 | interface Block{ 8 | block_state: BlockStateManager 9 | } 10 | } 11 | 12 | export class BlockStateManager{ 13 | cached_consuming:Map> = new Map 14 | providing:string[] = [] 15 | consuming:string[] = [] 16 | 17 | consume_reload_callbacks:Map = new Map 18 | 19 | constructor(protected block:Block){ 20 | 21 | } 22 | 23 | using(name,reload_callback){ 24 | this.consume_reload_callbacks.set(name,reload_callback) 25 | setTimeout(()=>this.addConsume(name),0); 26 | return ()=>{ 27 | this.consume_reload_callbacks.delete(name) 28 | this.removeConsume(name) 29 | } 30 | } 31 | 32 | removeFromParent(parent:Block){ 33 | parent.block_state.removeChildren(this.block) 34 | } 35 | 36 | removeChildren(children:Block){ 37 | let deleted_consumes = [] 38 | children.block_state.cached_consuming.forEach((consumes,consumeId)=> { 39 | consumes.forEach(fromEntry => { 40 | if(this.removeCachedConsumeInternal(consumeId,fromEntry)) 41 | deleted_consumes.push(consumeId) 42 | }) 43 | }) 44 | deleted_consumes.forEach(consumeId=>{ 45 | this.block.getParent()?.block_state.removeCachedConsume(consumeId,children.id) 46 | }) 47 | } 48 | 49 | removeCachedConsume(consumeId:string,fromEntry:string){ 50 | if(this.removeCachedConsumeInternal(consumeId,fromEntry)) 51 | this.block.getParent()?.block_state.removeCachedConsume(consumeId,fromEntry) 52 | } 53 | 54 | protected removeCachedConsumeInternal(consumeId:string,fromEntry:string):boolean{ 55 | if(this.providing.includes(consumeId)){ 56 | this.block.workspace.getBlockById(fromEntry)?.block_state.notifyConsumeReload(consumeId) 57 | } 58 | if(!this.cached_consuming.has(consumeId)) 59 | return false 60 | const parentConsumes = this.cached_consuming.get(consumeId) 61 | parentConsumes.delete(fromEntry) 62 | if(parentConsumes.size<=0){ 63 | this.cached_consuming.delete(consumeId) 64 | return true 65 | } 66 | return false 67 | } 68 | 69 | addConsume(consumeId:string){ 70 | if(!this.consuming.includes(consumeId)) { 71 | this.consuming.push(consumeId) 72 | this.addCachedConsume(consumeId, this.block.id) 73 | } 74 | } 75 | 76 | removeConsume(consumeId:string){ 77 | if(this.consuming.includes(consumeId)) { 78 | this.consuming.splice(this.consuming.indexOf(consumeId),1) 79 | this.removeCachedConsume(consumeId,this.block.id) 80 | } 81 | } 82 | 83 | addCachedConsume(consumeId:string,fromEntry:string){ 84 | if(this.providing.includes(consumeId)){ 85 | this.block.workspace.getBlockById(fromEntry)?.block_state.notifyConsumeReload(consumeId) 86 | } 87 | if(!this.cached_consuming.has(consumeId)) 88 | this.cached_consuming.set(consumeId,new Set) 89 | this.cached_consuming.get(consumeId).add(fromEntry) 90 | this.block.getParent()?.block_state.addCachedConsume(consumeId,fromEntry) 91 | } 92 | 93 | notifyConsumeReload(consumeId:string){ 94 | if(this.consume_reload_callbacks.has(consumeId)) 95 | this.consume_reload_callbacks.get(consumeId)() 96 | } 97 | 98 | recoverCachedConsume(){ 99 | this.cached_consuming.forEach((fromEntries,consumeId)=>{ 100 | fromEntries.forEach(fromEntry=>{ 101 | this.block.getParent()?.block_state.addCachedConsume(consumeId,fromEntry) 102 | }) 103 | }) 104 | } 105 | } 106 | 107 | export function registerScope(workspace:Workspace){ 108 | workspace.addChangeListener((_event:Abstract)=>{ 109 | if(_event.type=='finished_loading'){ 110 | workspace.getAllBlocks(false).forEach(block=>{ 111 | if(!block.block_state) 112 | block.block_state = new BlockStateManager(block) 113 | }) 114 | return; 115 | } 116 | if (!(_event.type === 'move' || _event.type === 'create'))return; 117 | const event = _event as BlockMove | BlockCreate 118 | const targetBlock = event.getEventWorkspace_().getBlockById(event.blockId) 119 | if(!targetBlock)return 120 | if(!targetBlock.block_state) 121 | targetBlock.block_state = new BlockStateManager(targetBlock) 122 | if(event.type!='move')return; 123 | let new_event = event as BlockMove 124 | if(new_event.oldParentId) 125 | targetBlock.block_state.removeFromParent(targetBlock.workspace.getBlockById(new_event.oldParentId)) 126 | if(new_event.newParentId) 127 | targetBlock.block_state.recoverCachedConsume() 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /client/blockly/blocks/data.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | import {BlockSvg} from "blockly"; 3 | 4 | export const HttpGetBlock = { 5 | "type": "http_get", 6 | "message0": "发送简单HTTP GET请求 %1 网址 %2 返回类型 %3", 7 | "args0": [ 8 | { 9 | "type": "input_dummy" 10 | }, 11 | { 12 | "type": "input_value", 13 | "name": "url" 14 | }, 15 | { 16 | "type": "field_dropdown", 17 | "name": "response_type", 18 | "options": [ 19 | [ 20 | "默认类型", 21 | "" 22 | ], 23 | [ 24 | "JSON对象类型", 25 | "json" 26 | ], 27 | [ 28 | "文本类型", 29 | "text" 30 | ] 31 | ] 32 | } 33 | ], 34 | "inputsInline": false, 35 | "output": null, 36 | "colour": 230, 37 | "tooltip": "", 38 | "helpUrl": "" 39 | } 40 | 41 | export function httpGetBlockGenerator(block:BlockSvg){ 42 | let value_url = javascriptGenerator.valueToCode(block, 'url', javascriptGenerator.ORDER_ATOMIC); 43 | let response_type = block.getFieldValue('response_type'); 44 | return [`await ctx.http.get(${value_url},{responseType:"${response_type}"})`, javascriptGenerator.ORDER_NONE]; 45 | } 46 | 47 | export const JsonPathParseBlock = { 48 | "type": "json_path_parse", 49 | "message0": "解析JSON对象 %1 JSONPath %2", 50 | "args0": [ 51 | { 52 | "type": "input_value", 53 | "name": "value" 54 | }, 55 | { 56 | "type": "field_input", 57 | "name": "path", 58 | "text": "$" 59 | } 60 | ], 61 | "inputsInline": false, 62 | "imports":{'jsonpath-plus':['JSONPath as parseJson']}, 63 | "output": null, 64 | "colour": 230, 65 | "tooltip": "", 66 | "helpUrl": "" 67 | } 68 | 69 | export function jsonPathBlockGenerator(block:BlockSvg){ 70 | let value_value = javascriptGenerator.valueToCode(block, 'value', javascriptGenerator.ORDER_ATOMIC); 71 | let text_path = block.getFieldValue('path'); 72 | return [`await parseJson({path: "${text_path}", json: ${value_value}})`, javascriptGenerator.ORDER_NONE]; 73 | } 74 | 75 | export const KeyValueWriteBlock = { 76 | "type": "key_value_write", 77 | "message0": "写入键值对 作用域ID %1 键 %2 值 %3", 78 | "args0": [ 79 | { 80 | "type": "input_value", 81 | "name": "scope_id" 82 | }, 83 | { 84 | "type": "input_value", 85 | "name": "key" 86 | }, 87 | { 88 | "type": "input_value", 89 | "name": "value", 90 | "align": "RIGHT" 91 | } 92 | ], 93 | "template":["key_value_initialize"], 94 | "inputsInline": false, 95 | "previousStatement": null, 96 | "nextStatement": null, 97 | "colour": 230, 98 | "tooltip": "", 99 | "helpUrl": "" 100 | } 101 | 102 | export function keyValueWriteBlockGenerator(block:BlockSvg){ 103 | let value_key = javascriptGenerator.valueToCode(block, 'key', javascriptGenerator.ORDER_ATOMIC); 104 | let value_value = javascriptGenerator.valueToCode(block, 'value', javascriptGenerator.ORDER_ATOMIC); 105 | let value_scope_id = javascriptGenerator.valueToCode(block, 'scope_id', javascriptGenerator.ORDER_ATOMIC); 106 | if(value_scope_id.length){ 107 | // For backwards compatibility only, remove this in 1.x 108 | value_scope_id += "+\".\"+" 109 | } 110 | return `await ctx.database.upsert('blockly_key_value',[{key:${value_scope_id}${value_key},value:${value_value}}],['key'])\n`; 111 | } 112 | 113 | export const KeyValueReadBlock = { 114 | "type": "key_value_read", 115 | "message0": "读取键值对 作用域ID %1 键 %2", 116 | "args0": [ 117 | { 118 | "type": "input_value", 119 | "name": "scope_id" 120 | }, 121 | { 122 | "type": "input_value", 123 | "name": "key" 124 | } 125 | ], 126 | "template":["key_value_initialize"], 127 | "inputsInline": false, 128 | "output": null, 129 | "colour": 230, 130 | "tooltip": "", 131 | "helpUrl": "" 132 | } 133 | 134 | export function keyValueReadBlockGenerator(block:BlockSvg){ 135 | let value_key = javascriptGenerator.valueToCode(block, 'key', javascriptGenerator.ORDER_ATOMIC); 136 | let value_scope_id = javascriptGenerator.valueToCode(block, 'scope_id', javascriptGenerator.ORDER_ATOMIC); 137 | if(value_scope_id.length){ 138 | // For backwards compatibility only, remove this in 1.x 139 | value_scope_id += "+\".\"+" 140 | } 141 | return [`(await ctx.database.get('blockly_key_value',{key:${value_scope_id}${value_key}}))[0]?.value`, javascriptGenerator.ORDER_NONE]; 142 | } 143 | 144 | 145 | export const DataBlocks = [ 146 | HttpGetBlock, 147 | JsonPathParseBlock, 148 | KeyValueWriteBlock, 149 | KeyValueReadBlock 150 | ] 151 | 152 | export const dataBlockGenerators = { 153 | 'http_get':httpGetBlockGenerator, 154 | 'json_path_parse':jsonPathBlockGenerator, 155 | 'key_value_write':keyValueWriteBlockGenerator, 156 | 'key_value_read':keyValueReadBlockGenerator 157 | } 158 | -------------------------------------------------------------------------------- /src/transpiler.ts: -------------------------------------------------------------------------------- 1 | // Code from node_module rewrite-exports 2 | const RE_COMMENTS = /\/\*[^]*?\*\//g; 3 | const RE_KEYWORD = /(\bdefault\s+)?\b(let|const|class|function(?:\s*\*)?)(\s+)(\*?\s*[$\w][$\w\s\d,.=]+)([^]*?)$/i; 4 | const RE_EXPORT = /(^|\s+)export(?!\w)\s*(\{[^{}]*?\}.*?(?=;\n?|$)|[^]*?(?=[\n;]|$))/gi; 5 | const RE_FROM = /\bfrom\s+(["'])([^"']*)\1/gi; 6 | const RE_DF = /\bdefault(\s+as\s+(\w+))?\b/i; 7 | const RE_AS = /\b(\w+)\s+as\s+(\w+)\b/gi; 8 | 9 | function allVars(chunks) { 10 | if (typeof chunks === 'string') return allVars(chunks.replace(/{|}/g, '').split(/\s*,\s*/).map(x => x.trim())); 11 | return chunks.reduce((memo, text) => memo.concat(text.split(/\s*,\s*/).map(x => x.trim())), []); 12 | } 13 | 14 | function mapVars(tokens) { 15 | return tokens.replace(/[{\s}]+/g, '').split(',').reduce((memo, k) => Object.assign(memo, { [k.split(':')[0]]: k.split(':')[1] }), {}); 16 | } 17 | 18 | function rewriteExportBuilder(ctx, fn?, x?, f?) { 19 | ctx = ctx || 'module.exports'; 20 | fn = fn || 'require'; 21 | x = x || 'Object.assign'; 22 | 23 | return (_, left, tokens) => { 24 | let prefix = `${left}${ctx}`; 25 | 26 | tokens = tokens.replace(RE_COMMENTS, _ => _.replace(/\S/g, ' ')); 27 | const symbols = tokens.match(RE_KEYWORD); 28 | 29 | if (symbols) { 30 | if (symbols[2] === 'let' || symbols[2] === 'const') { 31 | let vars = symbols[4].split('=').filter(Boolean); 32 | let last = ''; 33 | 34 | if (vars.length !== 1) { 35 | last = vars[vars.length - 1]; 36 | vars = vars.slice(0, vars.length - 1); 37 | } 38 | 39 | if (!symbols[4].includes('=') && symbols[4].includes(',')) { 40 | return `${left}${tokens};${symbols[2] === 'let' && f ? f('let', allVars(vars), null, ctx, fn, x) : `${x}(${ctx},{${vars.join(',')}})`}`; 41 | } 42 | 43 | if (vars[0].includes(',')) { 44 | vars = vars[0].split(','); 45 | } 46 | return `${left}${symbols[2]}${symbols[3]}${vars.map(x => `${x}=${ctx}.${x.trim()}`).join('=void 0,')}=${last}${symbols[5]}`; 47 | } 48 | 49 | if (symbols[2] === 'class' || symbols[2].includes('function')) { 50 | prefix = prefix.replace(left, `${left}const ${symbols[4].split(/[({\s]+/)[0].replace('*', '')}=`); 51 | } 52 | 53 | if (!symbols[1]) { 54 | prefix += `.${symbols[4].trim().replace('*', '')}`; 55 | } 56 | } 57 | 58 | const def = tokens.match(RE_DF); 59 | 60 | if (tokens.match(RE_FROM)) { 61 | const vars = tokens.replace(RE_AS, '$2').replace(RE_FROM, '').replace(/\s+/g, ''); 62 | let mod; 63 | 64 | tokens = tokens.replace(RE_FROM, (_, q, src) => `=${fn}("${mod = src}")`); 65 | tokens = tokens.replace(RE_AS, '$1:$2'); 66 | 67 | const req = tokens.split('=').pop().trim(); 68 | 69 | if (vars === '*') { 70 | return `${prefix}=${f ? f('*', req, mod, ctx, fn, x) : req}`; 71 | } 72 | 73 | if (def) { 74 | if (def[2]) { 75 | prefix += `.${def[2]}`; 76 | } 77 | 78 | return `${prefix}=${f ? f('default', req, mod, ctx, fn, x) : req}`; 79 | } 80 | 81 | return `${left}const ${tokens};${f ? f('const', allVars(vars), mod, ctx, fn, x) : `${x}(${ctx},${vars})`}`; 82 | } 83 | 84 | if (def) { 85 | if (symbols || !tokens.match(RE_AS)) { 86 | tokens = tokens.replace(RE_DF, '').trim(); 87 | } else { 88 | tokens = tokens.match(RE_AS)[0].split(' ').shift(); 89 | } 90 | } else { 91 | tokens = tokens.replace(RE_AS, '$2:$1'); 92 | } 93 | 94 | if (!def && tokens.charAt() === '{') { 95 | if (tokens.includes('}')) { 96 | return `${left}${f ? f('object', mapVars(tokens), null, ctx, fn, x) : `${x}(${ctx},${tokens.replace(/\s+/g, '')})`}`; 97 | } 98 | return `${left}${ctx}=${tokens}`; 99 | } 100 | return `${prefix}=${tokens}`; 101 | }; 102 | } 103 | 104 | const rewriteExport = (code, ctx?, fn?, x?, i?) => code.replace(RE_EXPORT, rewriteExportBuilder(ctx, fn, x, i)); 105 | 106 | //Code from rewrite-imports 107 | 108 | function destruct(keys, target) { 109 | var out=[]; 110 | while (keys.length) out.push(keys.shift().trim().replace(/ as /g, ':')); 111 | return 'const { ' + out.join(', ') + ' } = ' + target; 112 | } 113 | 114 | function generate(keys, dep, base) { 115 | if (keys.length && !base) return destruct(keys, dep); 116 | return 'const ' + base + ' = ' + dep + (keys.length ? ';\n' + destruct(keys, base) : ''); 117 | } 118 | 119 | export function rewriteImport(str, fn?) { 120 | fn = fn || 'require'; 121 | return str.replace(/(^|;\s*|\r?\n+)import\s*((?:\*\s*as)?\s*([a-z$_][\w$]*)?\s*,?\s*(?:{([\s\S]*?)})?)?\s*(from)?\s*(['"`][^'"`]+['"`])(?=;?)(?=([^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)/gi, function (raw, ws, _, base, req, fro, dep) { 122 | dep = fn + '(' + dep + ')'; 123 | return (ws||'') + (fro ? generate(req ? req.split(',') : [], dep, base) : dep); 124 | }); 125 | } 126 | 127 | export function esModuleToCommonJs(code){ 128 | return rewriteImport(rewriteExport(code)); 129 | } 130 | -------------------------------------------------------------------------------- /client/blockly/blocks/typing.ts: -------------------------------------------------------------------------------- 1 | import {Block} from "blockly"; 2 | import * as Blockly from "blockly"; 3 | import {deduplicate} from 'cosmokit' 4 | import {FieldBindingStringDropdown} from "../fields/binding"; 5 | import {ReactiveValue} from "../binding"; 6 | 7 | export const TypeRootBlock = { 8 | "type": "type_root", 9 | "message0": "类型 %1", 10 | "args0": [ 11 | { 12 | "type": "input_value", 13 | "name": "type", 14 | "check": "Type", 15 | } 16 | ], 17 | "colour": "#ce4bc9", 18 | "tooltip": "", 19 | "helpUrl": "" 20 | } 21 | 22 | export const TypeDefinitionBlock = { 23 | "type": "type_definition", 24 | "message0": "定义类型 %1 %2", 25 | "args0": [ 26 | { 27 | "type": "field_input", 28 | "name": "name", 29 | "text": "类型名称" 30 | }, 31 | { 32 | "type": "input_value", 33 | "name": "type", 34 | "check": "Type", 35 | } 36 | ], 37 | "colour": "#ce4bc9", 38 | "tooltip": "", 39 | "helpUrl": "", 40 | init(this:Block&Record){ 41 | if(!this.workspace.typings)return 42 | this.type_value = new ReactiveValue(this.getFieldValue('name'),this.id) 43 | this.workspace.typings.add(this.type_value) 44 | this.onchange = ()=>{ 45 | this.type_value.set(this.getFieldValue('name')) 46 | } 47 | const dispose = this.dispose 48 | this.dispose = (h)=>{ 49 | this.workspace.typings.delete(this.type_value) 50 | dispose.bind(this)(h) 51 | } 52 | } 53 | } 54 | 55 | export const TypeAnyBlock = { 56 | "type": "type_any", 57 | "message0": "任意类型", 58 | "output": "Type", 59 | "colour": "#ce4bc9", 60 | "tooltip": "", 61 | } 62 | 63 | export const TypeNeverBlock = { 64 | "type": "type_never", 65 | "message0": "永远不会出现的类型", 66 | "output": "Type", 67 | "colour": "#ce4bc9", 68 | "tooltip": "" 69 | } 70 | 71 | export const TypeStringBlock = { 72 | "type": "type_string", 73 | "message0": "字符串", 74 | "output": "Type", 75 | "colour": "#ce4bc9", 76 | "tooltip": "", 77 | } 78 | 79 | export const TypeNumberBlock = { 80 | "type": "type_number", 81 | "message0": "数字", 82 | "output": "Type", 83 | "colour": "#ce4bc9", 84 | "tooltip": "", 85 | } 86 | 87 | export const TypeBooleanBlock = { 88 | "type": "type_boolean", 89 | "message0": "布尔值", 90 | "output": "Type", 91 | "colour": "#ce4bc9", 92 | "tooltip": "", 93 | } 94 | 95 | export const TypeArrayBlock = { 96 | "type": "type_array", 97 | "message0": "由%1组成的数组", 98 | "args0": [ 99 | { 100 | "type": "input_value", 101 | "name": "type", 102 | "check": "Type" 103 | } 104 | ], 105 | "output": "Type", 106 | "colour": "#ce4bc9", 107 | } 108 | 109 | export const TypeUnionRootBlock = { 110 | "type": "type_union_root", 111 | "message0": "联合类型 %1", 112 | "args0": [ 113 | { 114 | "type": "input_statement", 115 | "name": "types", 116 | "check": "Type" 117 | } 118 | ] 119 | } 120 | 121 | export const TypeUnionEntityBlock = { 122 | "type": "type_union_entity", 123 | "message0": "项目", 124 | "previousStatement": "Type", 125 | "nextStatement": "Type", 126 | "colour": 160 127 | } 128 | 129 | export const TypeUnionBlock = { 130 | "type": "type_union", 131 | "output": "Type", 132 | "message0": "联合类型", 133 | "args0":[], 134 | init(){ 135 | this.updateShape_() 136 | }, 137 | "mutator":"union_mutator", 138 | "colour": "#ce4bc9", 139 | } 140 | 141 | export const TypeObjectRootBlock = { 142 | "type": "type_object_root", 143 | "message0": "对象 %1", 144 | "args0": [ 145 | { 146 | "type": "input_statement", 147 | "name": "properties", 148 | "check": "Type" 149 | }, 150 | ], 151 | "colour": "#ce4bc9", 152 | "tooltip": "", 153 | "helpUrl": "" 154 | } 155 | 156 | export const TypeObjectEntityBlock = { 157 | "type": "type_object_entity", 158 | "message0": "属性 %1", 159 | "args0": [ 160 | { 161 | "type": "field_input", 162 | "name": "name", 163 | "text": "属性名称" 164 | } 165 | ], 166 | "previousStatement": "Type", 167 | "nextStatement": "Type", 168 | "colour": 160 169 | } 170 | 171 | export const TypeObjectBlock = { 172 | "type": "type_object", 173 | "message0": "对象", 174 | "output": "Type", 175 | "colour": "#ce4bc9", 176 | "tooltip": "", 177 | "helpUrl": "", 178 | "mutator":"object_mutator" 179 | } 180 | 181 | export const TypeGetter = { 182 | "type": "type_getter", 183 | "message0": "类型", 184 | "output": "Type", 185 | "colour": "#ce4bc9", 186 | "tooltip": "", 187 | "helpUrl": "", 188 | init(this:Block){ 189 | let field 190 | if(this.workspace.typings) { 191 | field = new FieldBindingStringDropdown(this.workspace.typings) 192 | this.inputList[0].appendField(field, "type") 193 | } 194 | } 195 | } 196 | 197 | export const TypeBlocks = [ 198 | TypeRootBlock, 199 | TypeAnyBlock, 200 | TypeNeverBlock, 201 | TypeStringBlock, 202 | TypeNumberBlock, 203 | TypeBooleanBlock, 204 | TypeArrayBlock, 205 | TypeUnionRootBlock, 206 | TypeUnionEntityBlock, 207 | TypeUnionBlock, 208 | TypeDefinitionBlock, 209 | TypeObjectRootBlock, 210 | TypeObjectEntityBlock, 211 | TypeObjectBlock, 212 | TypeGetter 213 | ] 214 | -------------------------------------------------------------------------------- /client/meta.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 129 | 130 | 146 | -------------------------------------------------------------------------------- /client/blockly/blocks/event.ts: -------------------------------------------------------------------------------- 1 | import {javascriptGenerator} from "blockly/javascript"; 2 | 3 | export const MiddlewareBlock = { 4 | "type": "middleware", 5 | "message0": "当接受到聊天消息 %1 %2", 6 | "args0": [ 7 | { 8 | "type": "input_dummy" 9 | }, 10 | { 11 | "type": "input_statement", 12 | "name": "callback" 13 | } 14 | ], 15 | "extensions":['session_provider'], 16 | "colour": 230, 17 | "tooltip": "", 18 | "helpUrl": "" 19 | } 20 | 21 | export function middlewareBlockGenerator(block){ 22 | let statements_callback = javascriptGenerator.statementToCode(block, 'callback'); 23 | return `ctx.middleware(async (session,next)=>{\n${statements_callback} return next();\n})` 24 | } 25 | 26 | export const OnMessageEvent = { 27 | "type": "on_message_event", 28 | "message0": "当消息 %1 %2 运行 %3", 29 | "args0": [ 30 | { 31 | "type": "field_dropdown", 32 | "name": "event_name", 33 | "options": [ 34 | [ 35 | "被删除(撤回)", 36 | "message-deleted" 37 | ], 38 | [ 39 | "消息被修改", 40 | "message-updated" 41 | ], 42 | [ 43 | "被机器人发送", 44 | "send" 45 | ] 46 | ] 47 | }, 48 | { 49 | "type": "input_dummy" 50 | }, 51 | { 52 | "type": "input_statement", 53 | "name": "listener" 54 | } 55 | ], 56 | "extensions":['session_provider'], 57 | "colour": 230, 58 | "tooltip": "", 59 | "helpUrl": "" 60 | } 61 | 62 | export const OnGuildMemberEvent = { 63 | "type": "on_guild_member_event", 64 | "message0": "当群组成员 %1 %2 运行 %3", 65 | "args0": [ 66 | { 67 | "type": "field_dropdown", 68 | "name": "event_name", 69 | "options": [ 70 | [ 71 | "加入", 72 | "guild-member-added" 73 | ], 74 | [ 75 | "退出/踢出", 76 | "guild-member-deleted" 77 | ], 78 | [ 79 | "申请加入", 80 | "guild-member-request" 81 | ] 82 | ] 83 | }, 84 | { 85 | "type": "input_dummy" 86 | }, 87 | { 88 | "type": "input_statement", 89 | "name": "listener" 90 | } 91 | ], 92 | "extensions":['session_provider'], 93 | "colour": 230, 94 | "tooltip": "", 95 | "helpUrl": "" 96 | } 97 | 98 | export const OnGuildEvent = { 99 | "type": "on_guild_event", 100 | "message0": "当你的机器人 %1 群组 %2 运行 %3", 101 | "args0": [ 102 | { 103 | "type": "field_dropdown", 104 | "name": "event_name", 105 | "options": [ 106 | [ 107 | "加入", 108 | "guild-added" 109 | ], 110 | [ 111 | "退出/被踢出", 112 | "guild-deleted" 113 | ], 114 | [ 115 | "被邀请到一个", 116 | "guild-request" 117 | ] 118 | ] 119 | }, 120 | { 121 | "type": "input_dummy" 122 | }, 123 | { 124 | "type": "input_statement", 125 | "name": "listener" 126 | } 127 | ], 128 | "extensions":['session_provider'], 129 | "colour": 230, 130 | "tooltip": "", 131 | "helpUrl": "" 132 | } 133 | 134 | export function eventBlockGenerator(block){ 135 | var dropdown_event_name = block.getFieldValue('event_name') 136 | var statements_listener = javascriptGenerator.statementToCode(block, 'listener') 137 | return `ctx.on('${dropdown_event_name}',async (session)=>{\n${statements_listener}\n})` 138 | } 139 | 140 | export const PluginApplyBlock = { 141 | "type":"plugin_apply", 142 | "message0":"当启用插件时 %1 执行 %2", 143 | "args0":[ 144 | { 145 | "type":"input_dummy" 146 | }, 147 | { 148 | "type":"input_statement", 149 | "name":"apply" 150 | } 151 | ], 152 | "inputsInline": false, 153 | "colour": 230, 154 | "tooltip": "", 155 | "helpUrl": "" 156 | } 157 | 158 | export function pluginApplyBlockGenerator(block){ 159 | return javascriptGenerator.statementToCode(block, 'apply') 160 | } 161 | 162 | export const CommandBlock = { 163 | "type": "command", 164 | "message0": "创建一个新的指令 %1 %2 调用函数 %3", 165 | "args0": [ 166 | { 167 | "type": "field_input", 168 | "name": "name", 169 | "text": "指令名称" 170 | }, 171 | { 172 | "type": "input_dummy" 173 | }, 174 | { 175 | "type": "input_statement", 176 | "name": "action" 177 | } 178 | ], 179 | "mutator":"parameter_list", 180 | "extensions":['session_provider','argument_provider'], 181 | "colour": 230, 182 | "tooltip": "", 183 | "helpUrl": "" 184 | }; 185 | export function commandBlockGenerator(block){ 186 | let text_name = block.getFieldValue('name'); 187 | let parameters = block.parameters ?? [] 188 | let statements_action = javascriptGenerator.statementToCode(block, 'action'); 189 | console.info(block.workspace.meta) 190 | let configure = block.workspace.meta.commands?.[text_name] ?? {} 191 | let description = configure.description ? `,\`${configure.description.replace('\n','\\n')}\`` : '' 192 | if(configure['description']) delete configure['description'] 193 | let configure_object = configure && Object.keys(configure).length>0 ? `,${JSON.stringify(configure)}` : '' 194 | let command_definition = text_name + ' ' + parameters.map((parameter)=>{ 195 | const {required,name,type} = parameter 196 | 197 | return (required?'<':'[') + name + (type!='any_parameter'?':'+type.split('_')[0]:'') + (required?'>':']') 198 | }).join(' ') 199 | return `ctx.command('${command_definition.trim()}'${description}${configure_object}).action(async ({session},...args)=>{\n${statements_action}\n});\n`; 200 | } 201 | 202 | export const EventBlocks = [ 203 | MiddlewareBlock, 204 | OnMessageEvent, 205 | OnGuildMemberEvent, 206 | OnGuildEvent, 207 | PluginApplyBlock, 208 | CommandBlock 209 | ] 210 | 211 | export const eventBlockGenerators = { 212 | 'middleware':middlewareBlockGenerator, 213 | 'command':commandBlockGenerator, 214 | 'plugin_apply':pluginApplyBlockGenerator, 215 | 'on_message_event':eventBlockGenerator, 216 | 'on_guild_member_event':eventBlockGenerator, 217 | 'on_guild_event':eventBlockGenerator 218 | } 219 | -------------------------------------------------------------------------------- /client/blockly/msg/zh.ts: -------------------------------------------------------------------------------- 1 | import * as Blockly from 'blockly' 2 | Blockly.Msg['LANG_VARIABLES_GLOBAL_DECLARATION_TITLE_INIT'] = 3 | '初始化全局变量'; 4 | Blockly.Msg['LANG_VARIABLES_GLOBAL_DECLARATION_NAME'] = '变量名称'; 5 | Blockly.Msg['LANG_VARIABLES_GLOBAL_DECLARATION_TO'] = '为'; 6 | Blockly.Msg['LANG_VARIABLES_GLOBAL_DECLARATION_COLLAPSED_TEXT'] = '全局变量'; 7 | Blockly.Msg['LANG_VARIABLES_GLOBAL_DECLARATION_TOOLTIP'] = 8 | '创建一个全局变量并赋值'; 9 | Blockly.Msg['LANG_VARIABLES_GLOBAL_PREFIX'] = '[全局变量]'; 10 | Blockly.Msg['LANG_VARIABLES_GET_TITLE_GET'] = '读取变量'; 11 | Blockly.Msg['LANG_VARIABLES_GET_COLLAPSED_TEXT'] = '设置变量'; 12 | Blockly.Msg['LANG_VARIABLES_GET_TOOLTIP'] = 13 | '返回变量的值'; 14 | Blockly.Msg['LANG_VARIABLES_SET_TITLE_SET'] = '设置变量'; 15 | Blockly.Msg['LANG_VARIABLES_SET_TITLE_TO'] = '为'; 16 | Blockly.Msg['LANG_VARIABLES_SET_COLLAPSED_TEXT'] = '设置'; 17 | Blockly.Msg['LANG_VARIABLES_SET_TOOLTIP'] = 18 | '设置变量'; 19 | Blockly.Msg['LANG_VARIABLES_VARIABLE'] = ' 变量'; 20 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_TITLE_INIT'] = '初始化局部变量'; 21 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_DEFAULT_NAME'] = '局部变量1'; 22 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_INPUT_TO'] = '为'; 23 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_IN_DO'] = '作用域'; 24 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_COLLAPSED_TEXT'] = '局部变量'; 25 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_TOOLTIP'] = 26 | 'Allows you to create variables that are only accessible in the do part' + 27 | ' of this block.'; 28 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_TRANSLATED_NAME'] = 29 | 'initialize local in do'; 30 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_EXPRESSION_IN_RETURN'] = 'in'; 31 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_EXPRESSION_COLLAPSED_TEXT'] = 32 | 'local'; 33 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_EXPRESSION_TOOLTIP'] = 34 | 'Allows you to create variables that are only accessible in the return' + 35 | ' part of this block.'; 36 | Blockly.Msg['LANG_VARIABLES_LOCAL_DECLARATION_EXPRESSION_TRANSLATED_NAME'] = 37 | 'initialize local in return'; 38 | Blockly.Msg['LANG_VARIABLES_LOCAL_MUTATOR_CONTAINER_TITLE_LOCAL_NAMES'] = 39 | 'local names'; 40 | Blockly.Msg['LANG_VARIABLES_LOCAL_MUTATOR_CONTAINER_TOOLTIP'] = ''; 41 | Blockly.Msg['LANG_VARIABLES_LOCAL_MUTATOR_ARG_TITLE_NAME'] = 'name'; 42 | Blockly.Msg['LANG_VARIABLES_LOCAL_MUTATOR_ARG_DEFAULT_VARIABLE'] = 'x'; 43 | Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_DEFINE'] = 'to'; 44 | Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_PROCEDURE'] = 'procedure'; 45 | Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_DO'] = 'do'; 46 | Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_COLLAPSED_PREFIX'] = 'to '; 47 | Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_TOOLTIP'] = 48 | 'A procedure that does not return a value.'; 49 | Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_THEN_RETURN'] = 'result'; 50 | Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_DO'] = 'do'; 51 | Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_RETURN'] = 'result'; 52 | Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_TOOLTIP'] = 53 | 'Runs the blocks in \'do\' and returns a statement. Useful if you need' + 54 | ' to run a procedure before returning a value to a variable.'; 55 | Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_COLLAPSED_TEXT'] = 'do/result'; 56 | Blockly.Msg['LANG_PROCEDURES_DEFRETURN_DEFINE'] = 'to'; 57 | Blockly.Msg['LANG_PROCEDURES_DEFRETURN_PROCEDURE'] = 'procedure'; 58 | Blockly.Msg['LANG_PROCEDURES_DEFRETURN_RETURN'] = 'result'; 59 | Blockly.Msg['LANG_PROCEDURES_DEFRETURN_COLLAPSED_PREFIX'] = 'to '; 60 | Blockly.Msg['LANG_PROCEDURES_DEFRETURN_TOOLTIP'] = 61 | 'A procedure returning a result value.'; 62 | Blockly.Msg['LANG_PROCEDURES_DEF_DUPLICATE_WARNING'] = 63 | 'Warning:\nThis procedure has\nduplicate inputs.'; 64 | Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_CALL'] = 'call '; 65 | Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_PROCEDURE'] = 'procedure'; 66 | Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_COLLAPSED_PREFIX'] = 'call '; 67 | Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_TOOLTIP'] = 68 | 'Call a procedure with no return value.'; 69 | Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_TRANSLATED_NAME'] = 'call no return'; 70 | Blockly.Msg['LANG_PROCEDURES_CALLRETURN_COLLAPSED_PREFIX'] = 'call '; 71 | Blockly.Msg['LANG_PROCEDURES_CALLRETURN_TOOLTIP'] = 72 | 'Call a procedure with a return value.'; 73 | Blockly.Msg['LANG_PROCEDURES_CALLRETURN_TRANSLATED_NAME'] = 'call return'; 74 | Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_TITLE'] = 'inputs'; 75 | Blockly.Msg['LANG_PROCEDURES_MUTATORARG_TITLE'] = 'input:'; 76 | Blockly.Msg['LANG_PROCEDURES_HIGHLIGHT_DEF'] = 'Highlight Procedure'; 77 | Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_TOOLTIP'] = ''; 78 | Blockly.Msg['LANG_PROCEDURES_MUTATORARG_TOOLTIP'] = ''; 79 | Blockly.Msg['LANG_CONTROLS_FOR_INPUT_WITH'] = 'count with'; 80 | Blockly.Msg['LANG_CONTROLS_FOR_INPUT_VAR'] = 'x'; 81 | Blockly.Msg['LANG_CONTROLS_FOR_INPUT_FROM'] = 'from'; 82 | Blockly.Msg['LANG_CONTROLS_FOR_INPUT_TO'] = 'to'; 83 | Blockly.Msg['LANG_CONTROLS_FOR_INPUT_DO'] = 'do'; 84 | Blockly.Msg['LANG_CONTROLS_FOR_TOOLTIP'] = 85 | 'Count from a start number to an end number.\nFor each count, set the' + 86 | ' current count number to\nvariable \'%1\', and then do some statements.'; 87 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_ITEM'] = 'for each'; 88 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_VAR'] = 'number'; 89 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_START'] = 'from'; 90 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_END'] = 'to'; 91 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_STEP'] = 'by'; 92 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_DO'] = 'do'; 93 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_COLLAPSED_TEXT'] = 94 | 'for number in range'; 95 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_COLLAPSED_PREFIX'] = 'for'; 96 | Blockly.Msg['LANG_CONTROLS_FORRANGE_INPUT_COLLAPSED_SUFFIX'] = ' in range'; 97 | Blockly.Msg['LANG_CONTROLS_FORRANGE_TOOLTIP'] = 98 | 'Runs the blocks in the \'do\' section for each numeric value in the' + 99 | ' range from start to end, stepping the value each time. Use the given' + 100 | ' variable name to refer to the current value.'; 101 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_ITEM'] = 'for each'; 102 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_VAR'] = 'item'; 103 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_INLIST'] = 'in list'; 104 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_DO'] = 'do'; 105 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_COLLAPSED_TEXT'] = 'for item in list'; 106 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_COLLAPSED_PREFIX'] = 'for '; 107 | Blockly.Msg['LANG_CONTROLS_FOREACH_INPUT_COLLAPSED_SUFFIX'] = ' in list'; 108 | Blockly.Msg['LANG_CONTROLS_FOREACH_TOOLTIP'] = 109 | 'Runs the blocks in the \'do\' section for each item in the list. Use' + 110 | ' the given variable name to refer to the current list item.'; 111 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_INPUT'] = 112 | 'for each %1 with %2 in dictionary %3'; 113 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_INPUT_DO'] = 'do'; 114 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_INPUT_KEY'] = 'key'; 115 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_INPUT_VALUE'] = 'value'; 116 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_TITLE'] = 'for each in dictionary'; 117 | Blockly.Msg['LANG_CONTROLS_FOREACH_DICT_TOOLTIP'] = 118 | 'Runs the blocks in the \'do\' section for each key-value entry in the' + 119 | ' dictionary. Use the given variable names to refer to the key/value of' + 120 | ' the current dictionary item.'; 121 | Blockly.Msg['ERROR_SELECT_VALID_ITEM_FROM_DROPDOWN'] = 122 | 'Select a valid item in the drop down.'; 123 | Blockly.Msg['ERROR_BLOCK_CANNOT_BE_IN_DEFINITION'] = 124 | 'This block cannot be in a definition'; 125 | Blockly.Msg['HORIZONTAL_PARAMETERS'] = 'Arrange Parameters Horizontally'; 126 | Blockly.Msg['VERTICAL_PARAMETERS'] = 'Arrange Parameters Vertically'; 127 | Blockly.Msg['LANG_CONTROLS_DO_THEN_RETURN_INPUT_DO'] = 'do'; 128 | Blockly.Msg['LANG_CONTROLS_DO_THEN_RETURN_INPUT_RETURN'] = 'result'; 129 | Blockly.Msg['LANG_CONTROLS_DO_THEN_RETURN_TOOLTIP'] = 130 | 'Runs the blocks in \'do\' and returns a statement. Useful if you need to' + 131 | ' run a procedure before returning a value to a variable.'; 132 | Blockly.Msg['LANG_CONTROLS_DO_THEN_ RETURN_COLLAPSED_TEXT'] = 'do/result'; 133 | Blockly.Msg['LANG_CONTROLS_DO_THEN_RETURN_TITLE'] = 'do result'; 134 | -------------------------------------------------------------------------------- /client/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 149 | 150 | 170 | 171 | 212 | -------------------------------------------------------------------------------- /client/blockly/typing/index.ts: -------------------------------------------------------------------------------- 1 | import {Connection, ConnectionChecker, IConnectionChecker, INPUT_VALUE, OUTPUT_VALUE, WorkspaceSvg} from 'blockly' 2 | export {initializeType} from './overwritting' 3 | 4 | export type TsTerminalType = string | number | boolean | undefined 5 | 6 | export abstract class Type

{ 7 | prototype:P = undefined as any 8 | 9 | constructor(...data:P extends never?[]:[P]) { 10 | if(data.length) 11 | this.prototype = data[0] 12 | } 13 | 14 | getPrototype(): P { 15 | return this.prototype; 16 | } 17 | 18 | abstract getTypeName():string 19 | abstract canWriteTo(target:Type):boolean 20 | abstract equalsTo(target:Type):boolean 21 | canWriteBy?(target:Type):boolean 22 | } 23 | 24 | export class ConstTerminalType extends Type{ 25 | 26 | getTypeName(): string { 27 | return 'const' 28 | } 29 | 30 | canWriteTo(target: Type) { 31 | return this.equalsTo(target) || target.getTypeName() == typeof this.getPrototype() || target.canWriteBy?.(this) 32 | } 33 | 34 | equalsTo(target: Type) { 35 | return target.getTypeName() == this.getTypeName() && target.getPrototype() == this.getPrototype() 36 | } 37 | } 38 | 39 | export abstract class TerminalType extends Type{ 40 | 41 | canWriteTo(target: Type) : boolean { 42 | return this.equalsTo(target) || target.canWriteBy?.(this) 43 | } 44 | 45 | abstract getTypeName(): string 46 | 47 | equalsTo(target: Type) { 48 | return target.getTypeName() == this.getTypeName() 49 | } 50 | 51 | } 52 | 53 | export class StringType extends TerminalType{ 54 | getTypeName(): string { 55 | return "string"; 56 | } 57 | } 58 | 59 | export class NumberType extends TerminalType{ 60 | getTypeName(): string { 61 | return "number"; 62 | } 63 | } 64 | 65 | export class BooleanType extends TerminalType{ 66 | getTypeName(): string { 67 | return "boolean"; 68 | } 69 | } 70 | 71 | export class UndefinedType extends TerminalType{ 72 | getTypeName(): string { 73 | return "undefined"; 74 | } 75 | } 76 | 77 | export abstract class ComplexType

extends Type

{ 78 | 79 | } 80 | 81 | export class TupleType extends ComplexType{ 82 | 83 | getTypeName(): string { 84 | return "tuple"; 85 | } 86 | 87 | canWriteTo(target: Type) : boolean { 88 | if(this.equalsTo(target)) 89 | return true 90 | if(target.canWriteBy?.(this)) 91 | return true 92 | if(!['tuple','array'].includes(target.getTypeName())) 93 | return false 94 | if(target.getTypeName() == 'array'){ 95 | const target_prototype = (target as ArrayType).getPrototype() 96 | for(let i=0;i).getPrototype() 103 | if(target_prototype.length!=this.prototype.length) 104 | return false 105 | for(let i=0;i).getPrototype() 116 | if(target_prototype.length!=this.prototype.length) 117 | return false 118 | for(let i=0;i extends ComplexType{ 128 | getTypeName(): string { 129 | return "array"; 130 | } 131 | 132 | canWriteTo(target: Type) { 133 | return this.equalsTo(target) || target.canWriteBy?.(this) 134 | } 135 | 136 | equalsTo(target: Type): boolean { 137 | return target.getTypeName() == this.getTypeName() && this.getPrototype().equalsTo(target.getPrototype()) 138 | } 139 | } 140 | 141 | export class ObjectType

> extends ComplexType

{ 142 | getTypeName(): string { 143 | return "object"; 144 | } 145 | 146 | canWriteTo(target: Type) { 147 | if(this.equalsTo(target) || target.canWriteBy?.(this)) 148 | return true 149 | if(target.getTypeName() != 'object') 150 | return false 151 | const target_prototype = (target as ObjectType>).getPrototype() 152 | for(const key in this.prototype){ 153 | if(!target_prototype[key] || !target_prototype[key].canWriteTo(this.prototype[key])) 154 | return false 155 | } 156 | return true 157 | } 158 | 159 | canWriteBy(target: Type) : boolean { 160 | return this.equalsTo(target) || target.canWriteTo(this) 161 | } 162 | 163 | equalsTo(target: Type): boolean { 164 | if(target.getTypeName()!=this.getTypeName()) 165 | return false 166 | const target_prototype = (target as ObjectType>).getPrototype() 167 | if(Object.keys(target_prototype).length!=Object.keys(this.prototype).length) 168 | return false 169 | for(const key in this.prototype){ 170 | if(!target_prototype[key] || !target_prototype[key].equalsTo(this.prototype[key])) 171 | return false 172 | } 173 | return true 174 | } 175 | } 176 | 177 | export class UnionType extends ComplexType{ 178 | getTypeName(): string { 179 | return "union"; 180 | } 181 | 182 | canWriteTo(target: Type) { 183 | for(const type of this.prototype){ 184 | if(!type.canWriteTo(target)){ 185 | return false 186 | } 187 | } 188 | return true 189 | } 190 | 191 | equalsTo(target: Type) { 192 | if(target.getTypeName()!=this.getTypeName()) 193 | return false 194 | const target_prototype = (target as UnionType).getPrototype() 195 | if(target_prototype.length!=this.prototype.length) 196 | return false 197 | for(const type of this.prototype){ 198 | let found = target_prototype.findIndex(t=>t.equalsTo(type)) 199 | if(found==-1) 200 | return false 201 | target_prototype.splice(found,1) 202 | } 203 | return true 204 | } 205 | 206 | canWriteBy(target:Type){ 207 | return this.prototype.some((type)=>{ 208 | return target.canWriteTo(type) 209 | }) 210 | } 211 | } 212 | 213 | export class ClassType extends Type{ 214 | getTypeName(): string { 215 | return "class"; 216 | } 217 | 218 | canWriteTo(target: Type) { 219 | return this.equalsTo(target) || target.canWriteBy?.(this) 220 | } 221 | 222 | equalsTo(target: Type) { 223 | return target.getTypeName() == this.getTypeName() && this.prototype == target.getPrototype() 224 | } 225 | } 226 | 227 | export class AnyType extends Type{ 228 | getTypeName(): string { 229 | return "any"; 230 | } 231 | canWriteTo(target: Type): boolean { 232 | return true 233 | } 234 | canWriteBy(target: Type): boolean { 235 | return true 236 | } 237 | equalsTo(target: Type): boolean { 238 | return target.getTypeName() == this.getTypeName() 239 | } 240 | } 241 | 242 | export class NeverType extends Type{ 243 | getTypeName(): string { 244 | return "never"; 245 | } 246 | canWriteTo(target: Type): boolean { 247 | return this.equalsTo(target) 248 | } 249 | canWriteBy(target: Type): boolean { 250 | return this.equalsTo(target) 251 | } 252 | equalsTo(target: Type): boolean { 253 | return target.getTypeName() == this.getTypeName() 254 | } 255 | } 256 | 257 | export function unify(types:T):T[0]|UnionType{ 258 | if(!Array.isArray(types) || types.length <=1) 259 | return types[0] 260 | return new UnionType(types as Type[]) 261 | } 262 | 263 | declare module "blockly"{ 264 | interface Block{ 265 | getOutputType():Type 266 | } 267 | interface Input{ 268 | input_type:Type 269 | } 270 | } 271 | 272 | export class TypedConnectionChecker extends ConnectionChecker{ 273 | protected workspace : WorkspaceSvg 274 | constructor(workspace:WorkspaceSvg) { 275 | super(); 276 | this.workspace = workspace 277 | } 278 | doTypeChecks(a: Connection, b: Connection): boolean { 279 | if(!(a.type == 1 || b.type == 1)) 280 | return super.doTypeChecks(a,b) 281 | const input = a.type == 1 ? a : b 282 | const output = a.type == 1 ? b : a 283 | const output_type = output.getSourceBlock().getOutputType?.() 284 | const input_type = input.getParentInput().input_type 285 | if(!output_type || !input_type) 286 | return super.doTypeChecks(a,b) 287 | return super.doTypeChecks(a,b) && output_type.canWriteTo(input_type) 288 | } 289 | } 290 | 291 | export function registerTypeExtension(workspace:WorkspaceSvg){ 292 | workspace.addChangeListener(()=>{ 293 | 294 | }) 295 | } 296 | -------------------------------------------------------------------------------- /client/blockly/plugins/type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Block, 3 | BlocklyOptions, 4 | BlockSvg, 5 | Bubble, 6 | FieldImage, 7 | Icon, 8 | Mutator, 9 | Options, 10 | Workspace, 11 | WorkspaceSvg 12 | } from "blockly"; 13 | import * as Blockly from "blockly"; 14 | import {ReactiveBindingSet, ReactiveValue} from "../binding"; 15 | import {} from '../typing/' 16 | import {convertTypeToBlock} from "../typing/converter"; 17 | const {dom,Svg,Coordinate} = Blockly.utils 18 | 19 | declare module "blockly"{ 20 | interface Workspace{ 21 | typings: ReactiveBindingSet> 22 | } 23 | interface Block{ 24 | type_manager: TypeManager 25 | getTypeOutput(inputs:any[]):any 26 | } 27 | } 28 | 29 | function disableAllBlock(root_block:Block){ 30 | root_block.setMovable(false) 31 | root_block.setEditable(false) 32 | root_block.setDeletable(false) 33 | root_block.getChildren(false).forEach(disableAllBlock) 34 | } 35 | 36 | function renderAllBlock(root_block:BlockSvg){ 37 | root_block.render() 38 | root_block.getChildren(false).forEach(renderAllBlock) 39 | } 40 | 41 | export class TypeMutator extends Icon{ 42 | 43 | /** Width of workspace. */ 44 | private workspaceWidth = 0; 45 | 46 | /** Height of workspace. */ 47 | private workspaceHeight = 0; 48 | private svgDialog: SVGGElement; 49 | 50 | protected drawIcon_(group: Element) { 51 | // Square with rounded corners. 52 | dom.createSvgElement( 53 | Svg.RECT, { 54 | 'class': 'blocklyIconShape', 55 | 'rx': '4', 56 | 'ry': '4', 57 | 'height': '16', 58 | 'width': '16', 59 | }, 60 | group); 61 | const g = dom.createSvgElement(Svg.G,{ 62 | transform:"scale(0.024,0.024)translate(45,60)", 63 | },group) 64 | // Gear teeth. 65 | dom.createSvgElement( 66 | Svg.PATH, { 67 | 'class': 'blocklyIconSymbol', 68 | 'd': 'M118.6 80c-11.5 0-21.4 7.9-24 19.1L57 260.3c20.5-6.2 48.3-12.3 78.7-12.3c32.3 0 61.8 6.9 82.8 13.5c10.6 3.3 19.3 6.7 25.4 9.2c3.1 1.3 5.5 2.4 7.3 3.2c.9 .4 1.6 .7 2.1 1l.6 .3 .2 .1 .1 0 0 0 0 0s0 0-6.3 12.7h0l6.3-12.7c5.8 2.9 10.4 7.3 13.5 12.7h40.6c3.1-5.3 7.7-9.8 13.5-12.7l6.3 12.7h0c-6.3-12.7-6.3-12.7-6.3-12.7l0 0 0 0 .1 0 .2-.1 .6-.3c.5-.2 1.2-.6 2.1-1c1.8-.8 4.2-1.9 7.3-3.2c6.1-2.6 14.8-5.9 25.4-9.2c21-6.6 50.4-13.5 82.8-13.5c30.4 0 58.2 6.1 78.7 12.3L481.4 99.1c-2.6-11.2-12.6-19.1-24-19.1c-3.1 0-6.2 .6-9.2 1.8L416.9 94.3c-12.3 4.9-26.3-1.1-31.2-13.4s1.1-26.3 13.4-31.2l31.3-12.5c8.6-3.4 17.7-5.2 27-5.2c33.8 0 63.1 23.3 70.8 56.2l43.9 188c1.7 7.3 2.9 14.7 3.5 22.1c.3 1.9 .5 3.8 .5 5.7v6.7V352v16c0 61.9-50.1 112-112 112H419.7c-59.4 0-108.5-46.4-111.8-105.8L306.6 352H269.4l-1.2 22.2C264.9 433.6 215.8 480 156.3 480H112C50.1 480 0 429.9 0 368V352 310.7 304c0-1.9 .2-3.8 .5-5.7c.6-7.4 1.8-14.8 3.5-22.1l43.9-188C55.5 55.3 84.8 32 118.6 32c9.2 0 18.4 1.8 27 5.2l31.3 12.5c12.3 4.9 18.3 18.9 13.4 31.2s-18.9 18.3-31.2 13.4L127.8 81.8c-2.9-1.2-6-1.8-9.2-1.8zM64 325.4V368c0 26.5 21.5 48 48 48h44.3c25.5 0 46.5-19.9 47.9-45.3l2.5-45.6c-2.3-.8-4.9-1.7-7.5-2.5c-17.2-5.4-39.9-10.5-63.6-10.5c-23.7 0-46.2 5.1-63.2 10.5c-3.1 1-5.9 1.9-8.5 2.9zM512 368V325.4c-2.6-.9-5.5-1.9-8.5-2.9c-17-5.4-39.5-10.5-63.2-10.5c-23.7 0-46.4 5.1-63.6 10.5c-2.7 .8-5.2 1.7-7.5 2.5l2.5 45.6c1.4 25.4 22.5 45.3 47.9 45.3H464c26.5 0 48-21.5 48-48z', 69 | }, 70 | g); 71 | } 72 | display_workspace_:WorkspaceSvg 73 | createDisplay():SVGGElement{ 74 | this.svgDialog = dom.createSvgElement( 75 | Svg.SVG, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH}); 76 | 77 | const block = this.getBlock(); 78 | const workspaceOptions = new Options(({ 79 | 'disable': true, 80 | 'media': block.workspace.options.pathToMedia, 81 | 'rtl': block.RTL, 82 | 'horizontalLayout': false, 83 | 'renderer': block.workspace.options.renderer, 84 | 'rendererOverrides': block.workspace.options.rendererOverrides, 85 | } as BlocklyOptions)); 86 | 87 | this.display_workspace_ = new WorkspaceSvg(workspaceOptions) 88 | 89 | const background = this.display_workspace_.createDom('blocklyMutatorBackground') 90 | 91 | this.svgDialog.appendChild(background); 92 | 93 | return this.svgDialog; 94 | 95 | } 96 | setVisible(_visible: boolean) { 97 | console.info("BP0") 98 | if(_visible == this.isVisible())return 99 | if(_visible){ 100 | const block = this.getBlock(); 101 | console.info(this.iconXY_) 102 | this.bubble_ = new Bubble( 103 | block.workspace, this.createDisplay(), block.pathObject.svgPath, 104 | (this.iconXY_), null, null); 105 | const ws = this.display_workspace_!; 106 | this.bubble_.setSvgId(block.id); 107 | this.bubble_.registerMoveEvent(this.onBubbleMove.bind(this)); 108 | const tree = ws.options.languageTree; 109 | const flyout = ws.getFlyout(); 110 | this.resizeBubble(); 111 | this.applyColour(); 112 | const root_block = this.display_workspace_.newBlock('type_root') 113 | root_block.initSvg() 114 | root_block.moveTo(new Coordinate(10,10)) 115 | this.display_workspace_.addTopBlock(root_block) 116 | if(this.getBlock().getOutputType){ 117 | const topType = convertTypeToBlock(this.display_workspace_,this.getBlock().getOutputType()) 118 | if(topType){ 119 | topType.initSvg() 120 | root_block.getInput('type').connection.connect(topType.outputConnection) 121 | topType.render() 122 | } 123 | }else{ 124 | const topType = this.display_workspace_.newBlock('type_any') 125 | topType.initSvg() 126 | root_block.getInput('type').connection.connect(topType.outputConnection) 127 | topType.render() 128 | } 129 | renderAllBlock(root_block) 130 | disableAllBlock(root_block) 131 | this.display_workspace_.scrollCenter() 132 | this.resizeBubble() 133 | }else{ 134 | this.display_workspace_.getTopBlocks(false) 135 | .forEach((b)=> { 136 | this.display_workspace_.removeTopBlock(b) 137 | }) 138 | this.svgDialog = null; 139 | this.display_workspace_.dispose() 140 | this.display_workspace_ = null 141 | this.bubble_.dispose(); 142 | this.bubble_ = null; 143 | this.workspaceWidth = 0; 144 | this.workspaceHeight = 0; 145 | /*if (this.sourceListener) { 146 | block.workspace.removeChangeListener(this.sourceListener); 147 | this.sourceListener = null; 148 | }*/ 149 | } 150 | } 151 | protected resizeBubble() { 152 | if (!this.display_workspace_) { 153 | return; 154 | } 155 | const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH; 156 | const canvas = this.display_workspace_.getCanvas(); 157 | const workspaceSize = canvas.getBBox(); 158 | let width = workspaceSize.width + workspaceSize.x; 159 | let height = workspaceSize.height + doubleBorderWidth * 3; 160 | const flyout = this.display_workspace_.getFlyout(); 161 | if (flyout) { 162 | const flyoutScrollMetrics = 163 | flyout.getWorkspace().getMetricsManager().getScrollMetrics(); 164 | height = Math.max(height, flyoutScrollMetrics.height + 20); 165 | width += flyout.getWidth(); 166 | } 167 | 168 | const isRtl = this.getBlock().RTL; 169 | if (isRtl) { 170 | width = -workspaceSize.x; 171 | } 172 | width += doubleBorderWidth * 3; 173 | if (Math.abs(this.workspaceWidth - width) > doubleBorderWidth || 174 | Math.abs(this.workspaceHeight - height) > doubleBorderWidth) { 175 | this.workspaceWidth = width; 176 | this.workspaceHeight = height; 177 | this.bubble_!.setBubbleSize( 178 | width + doubleBorderWidth, height + doubleBorderWidth); 179 | this.svgDialog!.setAttribute('width', `${width}`); 180 | this.svgDialog!.setAttribute('height', `${height}`); 181 | this.display_workspace_.setCachedParentSvgSize(width, height); 182 | } 183 | if (isRtl) { 184 | canvas.setAttribute('transform', `translate(${this.workspaceWidth}, 0)`); 185 | } 186 | this.display_workspace_.resize(); 187 | } 188 | onBubbleMove(){ 189 | this.display_workspace_?.recordDragTargets(); 190 | } 191 | } 192 | 193 | export class TypeManager{ 194 | constructor(protected block:Block) { 195 | const output = block 196 | } 197 | } 198 | 199 | export function registerTypeManager(workspace:Workspace){ 200 | workspace.typings = new ReactiveBindingSet>() 201 | Object.keys(Blockly.Blocks).forEach(k=>{ 202 | const _init = Blockly.Blocks[k].init 203 | if(k.startsWith('type_'))return 204 | Blockly.Blocks[k].init = function(this:BlockSvg){ 205 | _init.call(this) 206 | this.type_manager = new TypeManager(this) 207 | if(!this.outputConnection)return 208 | const mutator = new TypeMutator(this) 209 | //mutator.setVisible(true) 210 | const getIcon_ = this.getIcons.bind(this) 211 | this.getIcons = ()=>{ 212 | return [...getIcon_(),mutator] 213 | } 214 | } 215 | }) 216 | } 217 | -------------------------------------------------------------------------------- /client/blockly/toolbox.xml: -------------------------------------------------------------------------------- 1 | 485 | --------------------------------------------------------------------------------