2023 是 AI 大爆发的一年,这一年我在我的生产力工具中(一个叫 lowcode 的 vscode 插件)接入了 ChatGPT API,插件也进行了重构,日常搬砖也因为 ChatGPT 的引入发生了很大的变化。

在介绍 ChatGPT 是如何与 lowcode 插件结合之前,先说说 lowcode 插件的发展历史,毕竟从 2020 年第一个版本发布到现在也迭代 3 年多了。

介绍

轮子的产生

一开始写这么个插件的目的为了拉取 YAPI 接口文档信息生成前端 API 请求方法,如下

  1. export interface IFetchUserListResult {
  2. code: number;
  3. msg: string;
  4. result: {
  5. rows: {
  6. name: string;
  7. age: number;
  8. mobile: string;
  9. address: string;
  10. tags: string[];
  11. id: number;
  12. }[];
  13. total: number;
  14. };
  15. }
  16. export interface IFetchUserListParams {
  17. name?: string;
  18. page: number;
  19. size: number;
  20. }
  21. /**
  22. * 用户列表
  23. * http://yapi.smart-xwork.cn/project/129987/interface/api/1796953
  24. * @author 划水摸鱼糊屎工程师
  25. *
  26. * @param {IFetchUserListParams} params
  27. * @returns
  28. */
  29. export function fetchUserList(params: IFetchUserListParams) {
  30. return request<IFetchUserListResult>(`${env.API_HOST}/api/user/page`, {
  31. method: 'GET',
  32. params,
  33. });
  34. }

之后增加了根据 JSON 生成 API 请求、根据 JSON 生成 TS 类型等功能。

物料的概念

再之后就是引入了物料的概念:代码片段和区块。上面说的根据 YAPI 接口信息生成 API 请求方法、根据 JSON 生成 API 请求、根据 JSON 生成 TS 类型都属于代码片段,只在当前激活的文件里生成代码。而区块就是在多个文件里生成代码(或者说创建多个文件)。

代码片段

代码片段可以通过右键菜单、输入提示、可视化界面进行使用,区块只能通过可视化界面使用。

右键菜单

输入提示

输入提示类似 vscode 自带的代码片段功能,同时兼容 vscode 代码片段的语法

可视化界面

代码片段可视化的功能目前我也很少用到(可以不用,但不能没有)

区块

前面也说了,区块是为了在多个文件里生成代码(或者创建多个文件)。比如写一个 react 组件的时候,可能包含 js 文件和 css 文件。

不同区块的 Schema 表单不一样,产生不一样的模板数据,就可以达到模板数据 + 模板生成代码的目的。

内部细节

插件读取项目根目录下的 materials/blocks 作为区块,读取 materials/snippets 作为代码片段。

目前已经支持配置任意目录,在所有项目中共享物料

代码片段和区块目录内的内容如下

主要是 src 目录内的内容存在差异,代码片段的 src 目录内必须是 template.ejs 文件,区块的 src 目录内可以是任意内容。生成代码的时候,使用 ejs 模板引擎编译 ejs 文件。代码片段是将编译以后的内容插入到编辑器光标所在的位置,区块是将编译后的文件拷贝到指定的目录里(非 ejs 文件直接拷贝)。

model.json

默认模板数据

preview.json

物料相关配置

  1. {
  2. "title": "",
  3. "description": "",
  4. "img": [
  5. "https://gitee.com/img-host/img-host/raw/master/2020/11/05/1604587962875.jpg"
  6. ],
  7. "category": [],
  8. "notShowInCommand": true,
  9. "notShowInSnippetsList": true,
  10. "notShowInintellisense": true,
  11. "showInRunSnippetScript": true,
  12. "schema": "amis",
  13. "scripts": []
  14. }

schema.json

可视化界面 Schema 表单配置,支持 form-render、formily、amis。

script/index.js

模板编译周期钩子函数

  1. module.exports = {
  2. beforeCompile: context => {
  3. console.log(context)
  4. },
  5. afterCompile: context => {
  6. console.log(context)
  7. },
  8. complete: context => {
  9. console.log(context)
  10. },
  11. }

重构之后会利用这个文件做更多有趣的事

缺陷

右键菜单使用代码片段的适用范围有限

  1. export const generateCode = (context: vscode.ExtensionContext) => {
  2. context.subscriptions.push(
  3. vscode.commands.registerTextEditorCommand(
  4. 'lowcode.generateCode',
  5. async () => {
  6. const rawClipboardText = getClipboardText();
  7. let clipboardText = rawClipboardText.trim();
  8. clipboardText = JSON.stringify(jsonParse(clipboardText));
  9. const validYapiId = isYapiId(clipboardText);
  10. const validJson = jsonIsValid(clipboardText);
  11. const valid = validJson || validYapiId;
  12. if (valid) {
  13. if (validYapiId) {
  14. await genCodeByYapi(clipboardText, rawClipboardText);
  15. } else {
  16. await genCodeByJson(clipboardText, rawClipboardText);
  17. }
  18. return;
  19. }
  20. try {
  21. await genCodeByTypescript(rawClipboardText, rawClipboardText);
  22. } catch {
  23. window.showErrorMessage('请复制Yapi接口ID或JSON字符串或TS类型');
  24. }
  25. },
  26. ),
  27. );
  28. };

代码里写死了逻辑,只能处理 json 、ts 类型、YAPI 接口。

与 ChatGPT 的结合

引入 ChatGPT 的最初目的是为了翻译区块 Schema 表单对应的模板数据里的指定字段。

翻译物料模板数据指定字段

点击 Ask ChatGPT 就会打开 ChatGPT 的 WebView 界面,并自动发送预设的 Prompt。

预设的 Prompt 就是放在 viewPrompt.ejs 中

内容如下:

  1. <%- model %> 将这段 json 中,filters 字段中的 key 字段翻译为英文,使用驼峰语法,label、placeholder
  2. 保留中文。columns 字段中的 key、dataIndex 字段翻译为英文,使用驼峰语法,title 字段保留中文。
  3. 返回翻译后的 markdown 语法的代码块

这种方式和 ChatGPT 交流会有各种玄学问题,比如翻译的字段不对或者所有的字段都翻译了,也可能是我写的 Prompt 有问题,这个功能也几乎不用了,后面会介绍另一种方式。

代码片段当作 Prompt 管理工具

看过几个 vscode 里 ChatGPT 的插件,大都是写死几个菜单,比如解释一段代码的意思、重构一段代码、给代码添加单元测试,说实话,有点 low low 的。

我是加了两个菜单:Ask ChatGPT、Ask ChatGPT With Template

Ask ChatGPT

逻辑很简单,直接把当前选中的代码或者剪贴板里的内容原封不动的发给 ChatGPT。

  1. vscode.commands.registerCommand('lowcode.askChatGPT', () => {
  2. showChatGPTView({
  3. task: {
  4. task: 'askChatGPT',
  5. data: getSelectedText() || getClipboardText(),
  6. },
  7. });
  8. }),

其实这个菜单完全可以去掉,用 Ask ChatGPT With Template 也能实现

Ask ChatGPT With Template

顾名思义就是根据不同的场景使用不同的 Prompt 模版去问 ChatGPT。

只需要在代码片段的目录下添加 commandPrompt.ejs 文件即可

内容可能如下

  1. 下面我让你来充当翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。
  2. 请翻译下面的内容:“<%- rawSelectedText || rawClipboardText %>”

重构、优化

之前提到过右键菜单使用代码片段的适用范围有限,只能处理 json 、ts 类型、YAPI 接口。接入 ChatGPT 后,又加了两个菜单。如果之后要加什么新功能还得接着加菜单,那就太 low 了。

虽然引入了 ChatGPT,但是 ChatGPT 的交互页面是独立的,ChatGPT 返回结果后还需要手动复制,我这种懒人是无法接受的。

webview 界面调用 nodejs 脚本

可视化界面配置表单还是挺费时的,而且原来的 Ask ChatGPT 的功能也比较玄学。加了一个“执行脚本”的按钮,可以实现调用物料目录下 src/index.js 文件内指定方法。

在使用 ChatGPT 进行翻译的时候,使用了 TypeChat(关于 TypeChat 可以看 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话),但是并不需要在插件内部引入 TypeChat。如下

  1. export async function handleAskChatGPT() {
  2. const { lowcodeContext } = context;
  3. const schema = fs.readFileSync(
  4. path.join(lowcodeContext!.materialPath, 'config/schema.ts'),
  5. 'utf8',
  6. );
  7. const typeName = 'PageConfig';
  8. const res = await translate<PageConfig>({
  9. schema,
  10. typeName,
  11. request: JSON.stringify(lowcodeContext!.model as PageConfig),
  12. completePrompt:
  13. `你是一个根据以下 TypeScript 类型定义将用户请求转换为 "${typeName}" 类型的 JSON 对象的服务,并且按照字段的注释进行处理:\n` +
  14. `\`\`\`\n${schema}\`\`\`\n` +
  15. `以下是用户请求:\n` +
  16. `"""\n${JSON.stringify(lowcodeContext!.model as PageConfig)}\n"""\n` +
  17. `The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n`,
  18. createChatCompletion: lowcodeContext!.createChatCompletion,
  19. showWebview: true,
  20. extendValidate: (jsonObject) => ({ success: true, data: jsonObject }),
  21. });
  22. lowcodeContext!.outputChannel.appendLine(JSON.stringify(res, null, 2));
  23. if (res.success) {
  24. return { ...res.data };
  25. }
  26. return lowcodeContext!.model;
  27. }

脚本方法执行完后将模版数据(model)返回,省去手动复制。

可以像写业务代码一样,根据自己需要添加各种处理方法,尝试各种新的技术。

如果 schema 表单用的是 amis,还可以在 schema 中配置执行脚本,比如:

  1. {
  2. "type": "button",
  3. "label": "插入// lowcode-model-import-api",
  4. "onEvent": {
  5. "click": {
  6. "actions": [{
  7. "actionType": "runScript",
  8. "args": {
  9. "method": "insertPlaceholder",
  10. "params": "// lowcode-model-import-api"
  11. }
  12. }]
  13. }
  14. }
  15. }

点击按钮的时候,会调用 insertPlaceholder 方法,参数为 // lowcode-model-import-api

Run Snippet Script

添加了 Run Snippet Script 菜单

选择对应的模版(代码片段)后,会执行模版目录下 src/index.js 的 onSelect 方法,方法里可以写任何逻辑。

只需要将代码片段目录下的 config/preview.json 文件里的showInRunSnippetScript 设置为 true,代码片段就会出现在菜单中。

  1. {
  2. "title": "",
  3. "description": "",
  4. "img": [
  5. "https://gitee.com/img-host/img-host/raw/master/2020/11/05/1604587962875.jpg"
  6. ],
  7. "category": [],
  8. "notShowInCommand": false,
  9. "notShowInSnippetsList": true,
  10. "notShowInintellisense": true,
  11. "showInRunSnippetScript": true,
  12. "schema": "amis",
  13. "scripts": []
  14. }

这个功能的加入,可以做很多有趣的事情,如下:

axios-request-api

把插件内部根据 YAPI 接口文档信息生成前端 API 请求方法的代码挪到了外面,并且加了个有意思的功能,让 ChatGPT 生成请求方法的名称,部分代码如下:

  1. const res = await fetchApiDetailInfo(domain, yapiId, token);
  2. if (!res.data.data) {
  3. throw res.data.errmsg;
  4. }
  5. funcName = await context.lowcodeContext!.createChatCompletion({
  6. messages: [
  7. {
  8. role: 'system',
  9. content: `你是一个代码专家,按照用户传给你的 api 接口地址,和接口请求方法,根据接口地址里的信息推测出一个生动形象的方法名称,驼峰格式,返回方法名称`,
  10. },
  11. {
  12. role: 'user',
  13. content: `api 地址:${res.data.data.query_path},${res.data.data.method} 方法,作用是${res.data.data.title}`,
  14. },
  15. ],
  16. });
  17. typeName = `I${funcName.charAt(0).toUpperCase() + funcName.slice(1)}Result`;

完整代码:https://github.com/lowcode-scaffold/lowcode-materials/tree/master/materials/snippets/axios-request-api-外挂脚本/script

OCR

使用百度 OCR 识别图片文字

因为 nodejs 没法读取剪贴板里的图片,只能打开一个 webview 去读取,核心代码如下:

  1. import { window, Range, env } from 'vscode';
  2. import { generalBasic } from '../../../../../share/BaiduOCR/index';
  3. import { context } from './context';
  4. export async function bootstrap() {
  5. const { lowcodeContext } = context;
  6. const clipboardImage = await lowcodeContext?.getClipboardImage();
  7. const ocrRes = await generalBasic({ image: clipboardImage! });
  8. const words = ocrRes.words_result.map((s) => s.words).join(',');
  9. env.clipboard.writeText(words).then(() => {
  10. window.showInformationMessage('内容已经复制到剪贴板');
  11. });
  12. window.activeTextEditor?.edit((editBuilder) => {
  13. // editBuilder.replace(activeTextEditor.selection, content);
  14. if (window.activeTextEditor?.selection.isEmpty) {
  15. editBuilder.insert(window.activeTextEditor.selection.start, words);
  16. } else {
  17. editBuilder.replace(
  18. new Range(
  19. window.activeTextEditor!.selection.start,
  20. window.activeTextEditor!.selection.end,
  21. ),
  22. words,
  23. );
  24. }
  25. });
  26. }

启动一个 nestjs 服务

  1. import { Controller, Get } from '@nestjs/common';
  2. import { AppService } from './app.service';
  3. @Controller()
  4. export class AppController {
  5. constructor(private readonly appService: AppService) {}
  6. @Get()
  7. getMaterialPath() {
  8. return this.appService.getMaterialPath();
  9. }
  10. }
  1. import { Injectable } from '@nestjs/common';
  2. import { context } from './context';
  3. @Injectable()
  4. export class AppService {
  5. getMaterialPath() {
  6. return context.lowcodeContext?.materialPath;
  7. }
  8. }

完整代码:https://github.com/lowcode-scaffold/lowcode-materials/tree/master/materials/snippets/start nest api server/script

生成 value-label 格式 JSON

使用了TypeChat,ChatGPT 返回的结果有提问,最终重试之后正确了。

完整代码:https://github.com/lowcode-scaffold/lowcode-materials/tree/master/materials/snippets/生成 value-label 格式 JSON/script

翻译成驼峰格式

代码:

  1. import { env, window, Range } from 'vscode';
  2. import { context } from './context';
  3. export async function bootstrap() {
  4. const clipboardText = await env.clipboard.readText();
  5. const { selection, document } = window.activeTextEditor!;
  6. const selectText = document.getText(selection).trim();
  7. let content = await context.lowcodeContext!.createChatCompletion({
  8. messages: [
  9. {
  10. role: 'system',
  11. content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
  12. },
  13. {
  14. role: 'user',
  15. content: selectText || clipboardText,
  16. },
  17. ],
  18. });
  19. content = content.charAt(0).toLowerCase() + content.slice(1);
  20. window.activeTextEditor?.edit((editBuilder) => {
  21. if (window.activeTextEditor?.selection.isEmpty) {
  22. editBuilder.insert(window.activeTextEditor.selection.start, content);
  23. } else {
  24. editBuilder.replace(
  25. new Range(
  26. window.activeTextEditor!.selection.start,
  27. window.activeTextEditor!.selection.end,
  28. ),
  29. content,
  30. );
  31. }
  32. });
  33. }

当前目录翻译成英文

代码:

  1. import * as path from 'path';
  2. import * as vscode from 'vscode';
  3. import * as fs from 'fs-extra';
  4. import { context } from './context';
  5. export async function bootstrap() {
  6. const { lowcodeContext } = context;
  7. const explorerSelectedPath = path
  8. .join(lowcodeContext?.explorerSelectedPath || '')
  9. .replace(/\\/g, '/');
  10. const explorerSelectedPathArr = explorerSelectedPath.split('/');
  11. const name = explorerSelectedPathArr.pop();
  12. vscode.window.withProgress(
  13. {
  14. location: vscode.ProgressLocation.Notification,
  15. },
  16. async (progress) => {
  17. progress.report({
  18. message: `loading`,
  19. });
  20. let content = await context.lowcodeContext!.createChatCompletion({
  21. messages: [
  22. {
  23. role: 'system',
  24. content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
  25. },
  26. {
  27. role: 'user',
  28. content: name || '',
  29. },
  30. ],
  31. });
  32. content = content.charAt(0).toLowerCase() + content.slice(1);
  33. fs.renameSync(
  34. path.join(lowcodeContext?.explorerSelectedPath || ''),
  35. path.join(explorerSelectedPathArr.join('/'), content),
  36. );
  37. },
  38. );
  39. }

快速创建区块

代码:

  1. import * as path from 'path';
  2. import { window } from 'vscode';
  3. import * as fs from 'fs-extra';
  4. import { context } from './context';
  5. import { renderEjsTemplates } from '../../../../../share/utils/ejs';
  6. export async function bootstrap() {
  7. const { lowcodeContext } = context;
  8. const result = await window.showQuickPick(
  9. [
  10. 'uniapp/vue3-mvp',
  11. 'uniapp/vue3-mvp emit',
  12. 'uniapp/vue3-mvp props',
  13. 'uniapp/vue3-mvp props emit',
  14. ].map((s) => s),
  15. { placeHolder: '请选择模板' },
  16. );
  17. if (!result) {
  18. return;
  19. }
  20. const tempWorkPath = path.join(
  21. lowcodeContext?.env.rootPath || '',
  22. '.lowcode',
  23. );
  24. fs.copySync(path.join(lowcodeContext?.materialPath || ''), tempWorkPath);
  25. await renderEjsTemplates(
  26. {
  27. createBlockPath: path
  28. .join(lowcodeContext?.explorerSelectedPath || '')
  29. .replace(/\\/g, '/'),
  30. },
  31. path.join(tempWorkPath, 'src'),
  32. );
  33. fs.copySync(
  34. path.join(tempWorkPath, 'src', result),
  35. path.join(lowcodeContext?.explorerSelectedPath || ''),
  36. );
  37. fs.removeSync(tempWorkPath);
  38. }

打开 WebView

右边 WebView 是一个独立的工程,部署在 vercel 上,主要为了学一下 UnoCSS,后续可能会把 screenshot-to-code 抄过来

完整代码:https://github.com/lowcode-scaffold/lowcode-materials/tree/master/materials/snippets/打开webview/script

WebView 项目代码(Vue):lowcode-webview-vue

WebView 项目代码(React):lowcode-webview-react-vite

无限可能

上面列举了我常用的一些功能,以及正在尝试的东西,可以看出 lowcode 插件的自由度已经很高了,后续如果出现了什么好玩的技术可以立即接入玩一下。

遗憾

2023 对图片相关的 AI 研究的比较少,也想不到有什么使用场景。

2024 研究一下 Design to Code + AI 的落地。

源码

插件源码:https://github.com/lowcoding/lowcode-vscode

物料源码:https://github.com/lowcode-scaffold/lowcode-materials

我的2023年度关键词:ChatGPT、生产力工具的更多相关文章

  1. 爬取朋友圈,Get年度关键词

    人生苦短,我用Python && C#. 1.引言 最近初学Python,写爬虫上瘾.爬了豆瓣练手,又爬了公司的论坛生成词云分析年度关键词.最近琢磨着2017又仅剩两月了,我的年度关键 ...

  2. Windows生产力工具推荐

    相信大部分同学还是Windows用户,作为一个长期Windows/MacOS双系统长期用户,Windows在用的好,工作效率也很高,下面就推荐几款Windows下面的生产力工具. utools 用过M ...

  3. 强大生产力工具Alfred

    今天要给大家介绍的工具是Alfred,一款Mac下的高效生产力产品.它能做什么呢?简单的说就是:让你能够通过打几个字,就可以完成原本需要一顿操作的事情.举一个简单的栗子:如果我们要在Google搜索一 ...

  4. RayLink远程控制软件:叮~你收到一份年度关键词报告

    叮~~~ 今天是12月31日,2022年的最后一天.今天过后,明天就是2023年啦!R君提前恭祝大家新年快乐,温情满满的跨年之际,RayLink感恩2022遇见大家,2023还请大家多多关照~ 202 ...

  5. 打造程序员的高效生产力工具-mac篇

    打造程序员的高效生产力工具-mac篇 1   概述 古语有云:“工欲善其事,必先利其器” [1] ,作为一个程序员,他最重要的生产资源是脑力知识,最重要的生产工具是什么?电脑. 在进行重要的脑力成果输 ...

  6. 生产力工具:shell 与 Bash 脚本

    生产力工具:shell 与 Bash 脚本 作者:吴甜甜 个人博客网站: wutiantian.github.io 注意:本文只是我个人总结的学习笔记,不适合0基础人士观看. 参考内容: 王顶老师 l ...

  7. 将WSL2作为生产力工具

    适用于 Linux 的 Windows 子系统 (WSL) 是 Windows 10新增的功能,使用它可以直接在 Windows 上运行 Linux 命令.而WSL 2 是WSL的一个新版本,它支持适 ...

  8. Linux Ubuntu 开发环境配置 ——最具生产力工具一览

    Why Linux and Why exactly Ubuntu 首先这里就不做Mac,Linux,Windows三者之争了.只从个人角度分析下: Mac 不差钱(其实Mac作为超级本性价还行),不喜 ...

  9. 黑猫关键词URL采集工具 Pro v1.0

    功能介绍:黑猫关键词URL采集工具 Pro v1.0 批量关键词自动搜索采集 自动去除垃圾二级泛解析域名 可设置是否保存域名或者url 联系客服QQ:944520563

  10. python打造批量关键词排名查询工具

    自己做站点的时候,都看看收录和关键词排名什么的,所以打造的这个批量关键词查询工具. #encoding:utf-8 import urllib,re,random,time,sys,StringIO, ...

随机推荐

  1. 谷歌浏览器和火狐浏览器如何查看HTTP协议?

    按F12

  2. Java 21中的两个值得关注的Bug修复

    在Java 21中,除了推出很多新特性之外,一些Bug修复,也需要注意一下.因为这些改变可能在升级的时候,造成影响. Double.toString()和Float.toString()的精度问题修复 ...

  3. Java自定义ClassLoader实现插件类隔离加载

    为什么需要类隔离加载 项目开发过程中,需要依赖不同版本的中间件依赖包,以适配不同的中间件服务端 如果这些中间件依赖包版本之间不能向下兼容,高版本依赖无法连接低版本的服务端,相反低版本依赖也无法连接高版 ...

  4. 如何使用sharding-sphere完成读写分离和分库分表?

    一.sharding-sphere配置读写分离 1.先搭建好一个MySQL的主从集群,可以参考[MySQL主从集群搭建] 2.在项目中导入相关依赖(记得刷新Maven) <!--读写分离--&g ...

  5. Java项目整合短信验证码

    一.开通短信服务 本来想整合阿里云短信服务的,可是签名一直审核不过,所以在阿里云的云市场找到了一个替代产品(sddx) 接下来小伙伴们按照自己的经济实力购买或者用免费的5条(我就是用免费的5条了) 购 ...

  6. Tomcat自动化脚本

    /bin/bash war包名称 war_name="tchg.war" 要上传war包指定目录 war_dir="/usr/local/src/tchg" 工 ...

  7. 使用dtd定义属性

  8. Centos7 Zabbix3.2安装(yum)

    http://repo.zabbix.com/zabbix/3.2/rhel/7/x86_64/  #官网下载地址(只包含zabbix的应用包) ftp://47.104.78.123/zabbix/ ...

  9. JavaFx之触发激发鼠标事件(二十三)

    JavaFx之触发激发鼠标事件(二十三) 有时候,我们不能直接触发/激发某个按钮的点击事件,因为发起方可能是子线程.使用屏幕点击又不优雅,javafx已经提供了事件的激发,即使在子线程中也能激发某个按 ...

  10. UnionFind 并查集

    简介 UnionFind 主要用于解决图论中的动态联通性的问题(对于输入的一系列元素集合,判断其中的元素是否是相连通的). 以下图为例: 集合[1, 2, 3, 4] 和 [5, 6]中的每个元素之间 ...