diff --git a/.mocharc.js b/.mocharc.js index ceb525497a..f8674754a7 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -8,5 +8,8 @@ module.exports = { 'packages/plugin-eval/tests/*.spec.ts', 'packages/plugin-teach/tests/*.spec.ts', ], - require: 'ts-node/register/transpile-only', + require: [ + 'ts-node/register/transpile-only', + 'tsconfig-paths/register', + ], } diff --git a/package.json b/package.json index f29f4548df..63979713b7 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,23 @@ "private": true, "workspaces": [ "atri", + "addons", "shiki", "shiki/core", "shiki/cosmos", "shiki/games", + "shiki/others", "packages/*" ], "scripts": { "atri": "yarn workspace atri", + "addons": "yarn workspace addons", "build": "tsc -b && yarn workspace koishi-plugin-eval wrap", "build:ci": "tsc -b --listEmittedFiles && yarn workspace koishi-plugin-eval wrap", "bump": "ts-node build/bump", "dep": "ts-node build/dep", "docs": "cd docs && yarn dev", - "test": "mocha --experimental-vm-modules --enable-source-maps", + "test": "cross-env TS_NODE_PROJECT=tsconfig.test.json mocha --experimental-vm-modules --enable-source-maps", "test:json": "c8 -r json yarn test", "test:lcov": "rimraf coverage && c8 -r lcov yarn test", "test:text": "c8 -r text yarn test", @@ -41,6 +44,7 @@ "cac": "^6.6.1", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", + "cross-env": "^7.0.2", "cross-spawn": "^7.0.3", "del": "^5.1.0", "eslint": "^7.7.0", @@ -64,6 +68,7 @@ "rimraf": "^3.0.2", "semver": "^7.3.2", "ts-node": "^9.0.0", + "tsconfig-paths": "^3.9.0", "typescript": "^4.0.2" } } diff --git a/packages/adapter-cqhttp/package.json b/packages/adapter-cqhttp/package.json index 0fcef6cea8..5e9bee43b3 100644 --- a/packages/adapter-cqhttp/package.json +++ b/packages/adapter-cqhttp/package.json @@ -1,7 +1,7 @@ { "name": "koishi-adapter-cqhttp", "description": "CQHTTP adapter for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -31,7 +31,7 @@ "koishi" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "@types/ms": "^0.7.31", @@ -43,6 +43,6 @@ "axios": "^0.20.0", "ms": "^2.1.2", "ws": "^7.3.1", - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/adapter-cqhttp/src/api.ts b/packages/adapter-cqhttp/src/api.ts index d56d1c748a..cc62e7ed5d 100644 --- a/packages/adapter-cqhttp/src/api.ts +++ b/packages/adapter-cqhttp/src/api.ts @@ -360,6 +360,7 @@ export interface ImageInfo { } export interface VersionInfo { + version?: string goCqhttp?: boolean runtimeVersion?: string runtimeOs?: string @@ -395,9 +396,9 @@ interface CQNode { defineAsync('set_group_name', 'group_id', 'name') export function toVersion(data: VersionInfo) { - const { coolqEdition, pluginVersion, goCqhttp } = data + const { coolqEdition, pluginVersion, goCqhttp, version } = data if (goCqhttp) { - return `Go-CQHTTP` + return `Go-CQHTTP/${version.slice(1)}` } else { return `CoolQ/${capitalize(coolqEdition)} CQHTTP/${pluginVersion}` } diff --git a/packages/koishi-core/package.json b/packages/koishi-core/package.json index db2a5926ba..aecb8a3857 100644 --- a/packages/koishi-core/package.json +++ b/packages/koishi-core/package.json @@ -1,7 +1,7 @@ { "name": "koishi-core", "description": "Core features for Koishi", - "version": "2.0.0", + "version": "2.0.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -44,7 +44,7 @@ "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-router": "^9.4.0", - "koishi-utils": "^3.1.0", + "koishi-utils": "^3.1.1", "leven": "^3.1.0", "lru-cache": "^6.0.0" } diff --git a/packages/koishi-core/src/command.ts b/packages/koishi-core/src/command.ts index 5c2cbda296..e36f933683 100644 --- a/packages/koishi-core/src/command.ts +++ b/packages/koishi-core/src/command.ts @@ -102,7 +102,10 @@ type Extend = { [P in K | keyof O]: (P extends keyof O ? O[P] : unknown) & (P extends K ? T : unknown) } -type ArgvInferred = Iterable | ((argv: ParsedArgv, fields: Set) => Iterable) +export type FieldCollector = + | Iterable + | ((argv: ParsedArgv, fields: Set) => void) + export type CommandAction = (this: Command, config: ParsedArgv, ...args: string[]) => void | string | Promise @@ -117,36 +120,37 @@ export class Command = {} private _optionSymbolMap: Record = {} - private _userFields: ArgvInferred[] = [] - private _groupFields: ArgvInferred[] = [] + private _userFields: FieldCollector<'user'>[] = [] + private _groupFields: FieldCollector<'group'>[] = [] _action?: CommandAction static defaultConfig: CommandConfig = {} static defaultOptionConfig: OptionConfig = {} - private static _userFields: ArgvInferred[] = [] - private static _groupFields: ArgvInferred[] = [] + private static _userFields: FieldCollector<'user'>[] = [] + private static _groupFields: FieldCollector<'group'>[] = [] - static userFields(fields: ArgvInferred) { + static userFields(fields: FieldCollector<'user'>) { this._userFields.push(fields) return this } - static groupFields(fields: ArgvInferred) { + static groupFields(fields: FieldCollector<'group'>) { this._groupFields.push(fields) return this } static collect(argv: ParsedArgv, key: T, fields = new Set()) { if (!argv) return - const values: ArgvInferred[] = [ + const values: FieldCollector[] = [ ...this[`_${key}Fields`], ...argv.command[`_${key}Fields`], ] - for (let value of values) { + for (const value of values) { if (typeof value === 'function') { - value = value(argv, fields) + value(argv, fields) + continue } for (const field of value) { fields.add(field) @@ -183,18 +187,14 @@ export class Command` } - userFields(fields: Iterable): Command - userFields(fields: (argv: ParsedArgv, fields: Set) => Iterable): Command - userFields(fields: ArgvInferred) { + userFields(fields: FieldCollector<'user', T, O>): Command { this._userFields.push(fields) - return this + return this as any } - groupFields(fields: Iterable): Command - groupFields(fields: (argv: ParsedArgv, fields: Set) => Iterable): Command - groupFields(fields: ArgvInferred) { + groupFields(fields: FieldCollector<'group', T, O>): Command { this._groupFields.push(fields) - return this + return this as any } alias(...names: string[]) { diff --git a/packages/koishi-core/src/database.ts b/packages/koishi-core/src/database.ts index 3a6c67c8dd..c72e163380 100644 --- a/packages/koishi-core/src/database.ts +++ b/packages/koishi-core/src/database.ts @@ -23,7 +23,7 @@ export namespace User { export type Field = keyof User export const fields: Field[] = [] - export type Observed = utils.Observed> + export type Observed = utils.Observed, Promise> type Getter = (id: number, authority: number) => Partial const getters: Getter[] = [] @@ -94,13 +94,13 @@ export interface Database { getUser(userId: number, defaultAuthority?: number, fields?: readonly K[]): Promise> getUsers(fields?: readonly K[]): Promise[]> getUsers(ids: readonly number[], fields?: readonly K[]): Promise[]> - setUser(userId: number, data: Partial): Promise + setUser(userId: number, data: Partial): Promise getGroup(groupId: number, fields?: readonly K[]): Promise> getGroup(groupId: number, selfId?: number, fields?: readonly K[]): Promise> getAllGroups(assignees?: readonly number[]): Promise[]> getAllGroups(fields?: readonly K[], assignees?: readonly number[]): Promise[]> - setGroup(groupId: number, data: Partial): Promise + setGroup(groupId: number, data: Partial): Promise } type DatabaseExtensionMethods = { diff --git a/packages/koishi-core/src/plugins/help.ts b/packages/koishi-core/src/plugins/help.ts index 080c6e4a15..091eb2f6a8 100644 --- a/packages/koishi-core/src/plugins/help.ts +++ b/packages/koishi-core/src/plugins/help.ts @@ -1,6 +1,6 @@ import { getUsage, getUsageName, ValidationField } from './validate' import { User, Group, TableType, Tables } from '../database' -import { Command, ParsedArgv } from '../command' +import { Command, FieldCollector, ParsedArgv } from '../command' import { Session } from '../session' import { App } from '../app' import { Message } from './message' @@ -59,17 +59,15 @@ export default function apply(app: App) { return '' }) - function createCollector(key: T) { - return function* (argv: ParsedArgv, fields: Set) { - const { args: [name] } = argv - const command = app._commandMap[name] || app._shortcutMap[name] - if (!command) return - yield* Command.collect({ ...argv, args: [], options: { help: true } }, key, fields) - } + const createCollector = (key: T): FieldCollector => (argv, fields) => { + const { args: [name] } = argv + const command = app._commandMap[name] || app._shortcutMap[name] + if (!command) return + Command.collect({ ...argv, command, args: [], options: { help: true } }, key, fields) } app.command('help [command]', '显示帮助信息', { authority: 0 }) - .userFields(['authority']) + .userFields(['authority']) .userFields(createCollector('user')) .groupFields(createCollector('group')) .shortcut('帮助', { fuzzy: true }) @@ -98,6 +96,7 @@ export default function apply(app: App) { }, }) } + return showHelp(command, session, options) }) } diff --git a/packages/koishi-core/src/plugins/validate.ts b/packages/koishi-core/src/plugins/validate.ts index 57275137ea..007ba3f524 100644 --- a/packages/koishi-core/src/plugins/validate.ts +++ b/packages/koishi-core/src/plugins/validate.ts @@ -59,7 +59,7 @@ Object.assign(Command.defaultOptionConfig, { authority: 0, }) -Command.userFields(function* ({ command, options = {} }, fields) { +Command.userFields(({ command, options = {} }, fields) => { const { maxUsage, minInterval, authority } = command.config let shouldFetchAuthority = !fields.has('authority') && authority > 0 let shouldFetchUsage = !!(maxUsage || minInterval) @@ -69,10 +69,10 @@ Command.userFields(function* ({ command, options = {} }, fields) { if (notUsage) shouldFetchUsage = false } } - if (shouldFetchAuthority) yield 'authority' + if (shouldFetchAuthority) fields.add('authority') if (shouldFetchUsage) { - if (maxUsage) yield 'usage' - if (minInterval) yield 'timers' + if (maxUsage) fields.add('usage') + if (minInterval) fields.add('timers') } }) diff --git a/packages/koishi-core/src/session.ts b/packages/koishi-core/src/session.ts index 669ff78d11..b496d9f860 100644 --- a/packages/koishi-core/src/session.ts +++ b/packages/koishi-core/src/session.ts @@ -200,7 +200,7 @@ export class Session Promise.resolve()) return this.$user = user } diff --git a/packages/koishi-core/tests/command.spec.ts b/packages/koishi-core/tests/command.spec.ts index 2221ec5dae..1fec565cf4 100644 --- a/packages/koishi-core/tests/command.spec.ts +++ b/packages/koishi-core/tests/command.spec.ts @@ -1,12 +1,11 @@ import { App } from 'koishi-test-utils' +import { inspect } from 'util' import { expect } from 'chai' import '@shigma/chai-extended' -let app: App - describe('Command API', () => { describe('Register Commands', () => { - before(() => app = new App()) + const app = new App() it('constructor checks', () => { expect(() => app.command('')).to.throw() @@ -26,6 +25,10 @@ describe('Command API', () => { expect(app._commandMap.c.context).to.equal(ctx2) }) + it('custom inspect', () => { + expect(inspect(app.command('a'))).to.equal('Command ') + }) + it('modify commands', () => { const d1 = app.command('d', 'foo', { authority: 1 }) expect(app._commandMap.d.config.description).to.equal('foo') @@ -63,6 +66,7 @@ describe('Command API', () => { }) describe('Register Subcommands', () => { + let app: App beforeEach(() => app = new App()) it('command.subcommand', () => { @@ -123,4 +127,28 @@ describe('Command API', () => { expect(() => app.command('a/d')).not.to.throw() }) }) + + describe('Dispose Commands', () => { + const app = new App() + const foo = app.command('foo') + const bar = foo.subcommand('bar') + const test = bar.subcommand('test') + bar.alias('baz').shortcut('1') + test.alias('it').shortcut('2') + + it('before dispose', () => { + // don't forget help + expect(app._commands).to.have.length(4) + expect(app._shortcuts).to.have.length(3) + expect(foo.children).to.have.length(1) + }) + + it('after dispose', () => { + bar.dispose() + // don't forget help + expect(app._commands).to.have.length(2) + expect(app._shortcuts).to.have.length(1) + expect(foo.children).to.have.length(0) + }) + }) }) diff --git a/packages/koishi-core/tests/hook.spec.ts b/packages/koishi-core/tests/hook.spec.ts index fae3561e46..351b132c2a 100644 --- a/packages/koishi-core/tests/hook.spec.ts +++ b/packages/koishi-core/tests/hook.spec.ts @@ -6,7 +6,7 @@ import { expect } from 'chai' const app = new App() -Logger.baseLevel = 1 +Logger.baseLevel = Logger.ERROR const appLogger = new Logger('app') const appWarn = spyOn(appLogger, 'warn') const midLogger = new Logger('middleware') diff --git a/packages/koishi-core/tests/parser.spec.ts b/packages/koishi-core/tests/parser.spec.ts index 000d151c4c..fc952ee757 100644 --- a/packages/koishi-core/tests/parser.spec.ts +++ b/packages/koishi-core/tests/parser.spec.ts @@ -1,6 +1,5 @@ import { Command } from 'koishi-core' import { App } from 'koishi-test-utils' -import { inspect } from 'util' import { expect } from 'chai' import '@shigma/chai-extended' @@ -10,28 +9,23 @@ let cmd: Command describe('Parser API', () => { describe('Basic Support', () => { - it('register', () => { - // there is a built-in help command - expect(app._commands).to.have.length(1) - - cmd = app.command('cmd1 [...bar]') - expect(app._commands).to.have.length(2) - }) - - it('inspect', () => { - expect(inspect(cmd)).to.equal('Command ') - }) - it('parse arguments', () => { + cmd = app.command('cmd1 [...bar]') expect(cmd.parse('')).to.have.shape({ args: [] }) expect(cmd.parse('a')).to.have.shape({ args: ['a'] }) expect(cmd.parse('a b')).to.have.shape({ args: ['a', 'b'] }) expect(cmd.parse('a b c')).to.have.shape({ args: ['a', 'b', 'c'] }) }) - it('dispose', () => { - cmd.dispose() - expect(app._commands).to.have.length(1) + it('stringify arguments', () => { + cmd = app.command('cmd4') + cmd.option('alpha', '-a ') + cmd.option('beta', '-b') + expect(cmd.stringify(['foo', 'bar'], {})).to.equal('cmd4 foo bar') + expect(cmd.stringify([], { alpha: 2 })).to.equal('cmd4 --alpha 2') + expect(cmd.stringify([], { alpha: ' ' })).to.equal('cmd4 --alpha " "') + expect(cmd.stringify([], { beta: true })).to.equal('cmd4 --beta') + expect(cmd.stringify([], { beta: false })).to.equal('cmd4 --no-beta') }) }) @@ -140,17 +134,4 @@ describe('Parser API', () => { expect(cmd.parse('-- "foo;bar";baz', ';')).to.have.shape({ options: { rest: 'foo;bar' }, rest: ';baz' }) }) }) - - describe('Stringify Argv', () => { - it('basic support', () => { - cmd = app.command('cmd4') - cmd.option('alpha', '-a ') - cmd.option('beta', '-b') - expect(cmd.stringify(['foo', 'bar'], {})).to.equal('cmd4 foo bar') - expect(cmd.stringify([], { alpha: 2 })).to.equal('cmd4 --alpha 2') - expect(cmd.stringify([], { alpha: ' ' })).to.equal('cmd4 --alpha " "') - expect(cmd.stringify([], { beta: true })).to.equal('cmd4 --beta') - expect(cmd.stringify([], { beta: false })).to.equal('cmd4 --no-beta') - }) - }) }) diff --git a/packages/koishi-test-utils/package.json b/packages/koishi-test-utils/package.json index ec94f6fcc1..32026528c5 100644 --- a/packages/koishi-test-utils/package.json +++ b/packages/koishi-test-utils/package.json @@ -38,7 +38,7 @@ "dependencies": { "@shigma/chai-extended": "^0.1.4", "chai": "^4.2.0", - "koishi-core": "^2.0.0-rc.2", - "koishi-utils": "^3.1.0" + "koishi-core": "^2.0.1", + "koishi-utils": "^3.1.1" } } diff --git a/packages/koishi-utils/package.json b/packages/koishi-utils/package.json index 694ee7b6ee..d35d499235 100644 --- a/packages/koishi-utils/package.json +++ b/packages/koishi-utils/package.json @@ -1,7 +1,7 @@ { "name": "koishi-utils", "description": "Utilities for Koishi", - "version": "3.1.0", + "version": "3.1.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/koishi-utils/src/logger.ts b/packages/koishi-utils/src/logger.ts index 36fc5e272a..2b270eb811 100644 --- a/packages/koishi-utils/src/logger.ts +++ b/packages/koishi-utils/src/logger.ts @@ -15,6 +15,12 @@ const instances: Record = {} type LogFunction = (format: any, ...param: any[]) => void export class Logger { + static readonly SUCCESS = 1 + static readonly ERROR = 1 + static readonly INFO = 2 + static readonly WARN = 2 + static readonly DEBUG = 3 + static baseLevel = 2 static showDiff = false static levels: Record = {} @@ -54,11 +60,11 @@ export class Logger { instances[name] = this this.code = colors[Math.abs(hash) % colors.length] this.displayName = name ? this.color(name + ' ', ';1') : '' - this.createMethod('success', '[S] ', 1) - this.createMethod('error', '[E] ', 1) - this.createMethod('info', '[I] ', 2) - this.createMethod('warn', '[W] ', 2) - this.createMethod('debug', '[D] ', 3) + this.createMethod('success', '[S] ', Logger.SUCCESS) + this.createMethod('error', '[E] ', Logger.ERROR) + this.createMethod('info', '[I] ', Logger.INFO) + this.createMethod('warn', '[W] ', Logger.WARN) + this.createMethod('debug', '[D] ', Logger.DEBUG) } private color(value: any, decoration = '') { diff --git a/packages/koishi-utils/src/observe.ts b/packages/koishi-utils/src/observe.ts index a2fc74592c..14b2e5e88c 100644 --- a/packages/koishi-utils/src/observe.ts +++ b/packages/koishi-utils/src/observe.ts @@ -134,7 +134,7 @@ export function observe(target: T, ...args: [(string | numb const observer = observeObject(target, label, null) as Observed - defineProperty(observer, '_update', function (this: Observed) { + defineProperty(observer, '_update', function _update(this: Observed) { const diff = { ...this._diff } const fields = Object.keys(diff) if (fields.length) { @@ -146,7 +146,7 @@ export function observe(target: T, ...args: [(string | numb } }) - defineProperty(observer, '_merge', function (this: Observed, value: Partial) { + defineProperty(observer, '_merge', function _merge(this: Observed, value: Partial) { for (const key in value) { if (key in this._diff) { throw new Error(`unresolved diff key "${key}"`) diff --git a/packages/koishi/package.json b/packages/koishi/package.json index a69f726f0e..0f78675635 100644 --- a/packages/koishi/package.json +++ b/packages/koishi/package.json @@ -1,7 +1,7 @@ { "name": "koishi", "description": "A QQ bot framework based on CQHTTP", - "version": "2.0.0", + "version": "2.0.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -40,14 +40,15 @@ "dependencies": { "cac": "^6.6.1", "kleur": "^4.1.1", - "koishi-core": "^2.0.0", - "koishi-adapter-cqhttp": "^1.0.0", + "koishi-core": "^2.0.1", + "koishi-adapter-cqhttp": "^1.0.1", "koishi-plugin-common": "^3.0.0-beta.15", "koishi-plugin-mysql": "^2.0.0", + "koishi-plugin-mongo": "^1.0.1", "koishi-plugin-schedule": "^2.0.0", "koishi-plugin-status": "^2.0.0-beta.13", - "koishi-plugin-teach": "^1.0.0", - "koishi-utils": "^3.1.0", + "koishi-plugin-teach": "^1.0.1", + "koishi-utils": "^3.1.1", "prompts": "^2.3.2" } } diff --git a/packages/plugin-chess/package.json b/packages/plugin-chess/package.json index 8be642a83b..c01afd05ed 100644 --- a/packages/plugin-chess/package.json +++ b/packages/plugin-chess/package.json @@ -33,10 +33,10 @@ "game" ], "peerDependencies": { - "koishi-core": "^2.0.0", + "koishi-core": "^2.0.1", "koishi-plugin-puppeteer": "^1.0.0" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-common/package.json b/packages/plugin-common/package.json index 404054b2f5..4a782180b2 100644 --- a/packages/plugin-common/package.json +++ b/packages/plugin-common/package.json @@ -31,12 +31,12 @@ "plugin" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-eval-addons/package.json b/packages/plugin-eval-addons/package.json index 8773eebc69..71d279aceb 100644 --- a/packages/plugin-eval-addons/package.json +++ b/packages/plugin-eval-addons/package.json @@ -1,6 +1,6 @@ { "name": "koishi-plugin-eval-addons", - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.8", "description": "Execute JavaScript in Koishi", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -36,13 +36,17 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.0.0", - "koishi-plugin-eval": "^2.0.0-beta.15" + "koishi-core": "^2.0.1", + "koishi-plugin-eval": "^2.0.0-beta.17" }, "dependencies": { "js-yaml": "^3.14.0", - "koishi-utils": "^3.1.0", + "json5": "^2.1.3", + "koishi-utils": "^3.1.1", "simple-git": "^2.20.1", "typescript": "^4.0.2" + }, + "devDependencies": { + "@types/json5": "^0.0.30" } } diff --git a/packages/plugin-eval-addons/src/index.ts b/packages/plugin-eval-addons/src/index.ts index 302000c26f..a5d726b312 100644 --- a/packages/plugin-eval-addons/src/index.ts +++ b/packages/plugin-eval-addons/src/index.ts @@ -1,46 +1,54 @@ -import { Context, CommandAction, CommandConfig, OptionConfig, User } from 'koishi-core' -import { resolve } from 'path' -import {} from 'koishi-plugin-eval' +import { Context, CommandConfig, OptionConfig, User } from 'koishi-core' import { assertProperty, Logger, noop } from 'koishi-utils' +import { resolve } from 'path' import { safeLoad } from 'js-yaml' -import { promises } from 'fs' +import { promises as fs } from 'fs' +import { UserTrap } from 'koishi-plugin-eval' import Git, { CheckRepoActions } from 'simple-git' const logger = new Logger('addon') +type AddonConfig = Config + export interface Config { gitRemote?: string - moduleRoot?: string exclude?: RegExp } -interface Option extends OptionConfig { +declare module 'koishi-plugin-eval' { + interface MainConfig extends AddonConfig {} +} + +interface OptionManifest extends OptionConfig { name: string desc: string } -interface Command extends CommandConfig { +type Permission = T[] | { + readable?: T[] + writable?: T[] +} + +interface CommandManifest extends CommandConfig { name: string desc: string - options?: Option[] + options?: OptionManifest[] + userFields?: Permission } interface Manifest { version: number - commands?: Command[] -} - -const addonAction: CommandAction = ({ session, command: { name }, options, rest }, ...args) => { - return session.$app.evalRemote.addon(session.$uuid, session.$user, { name, args, options, rest }) + commands?: CommandManifest[] } export function apply(ctx: Context, config: Config) { const { evalConfig } = ctx.app Object.assign(evalConfig, config) - const moduleRoot = assertProperty(evalConfig, 'moduleRoot') + const root = resolve(process.cwd(), assertProperty(evalConfig, 'moduleRoot')) + evalConfig.moduleRoot = root + evalConfig.dataKeys.push('addonNames', 'moduleRoot') evalConfig.setupFiles['koishi/addons.ts'] = resolve(__dirname, 'worker.js') - const root = resolve(process.cwd(), moduleRoot) const git = Git(root) const addon = ctx.command('addon', '扩展功能') @@ -57,34 +65,50 @@ export function apply(ctx: Context, config: Config) { ctx.on('before-connect', async () => { const isRepo = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT) - if (!isRepo) throw new Error(`moduleRoot "${moduleRoot}" is not git repository`) + if (!isRepo) throw new Error(`moduleRoot "${root}" is not git repository`) }) + let manifests: Record> const { exclude = /^(\..+|node_modules)$/ } = evalConfig + ctx.on('worker/start', async () => { - const dirents = await promises.readdir(root, { withFileTypes: true }) - evalConfig.addonNames = dirents + const dirents = await fs.readdir(root, { withFileTypes: true }) + const paths = evalConfig.addonNames = dirents .filter(dir => dir.isDirectory() && !exclude.test(dir.name)) .map(dir => dir.name) // cmd.dispose() may affect addon.children, so here we make a slice addon.children.slice().forEach(cmd => cmd.dispose()) + manifests = Object.fromEntries(paths.map(path => [path, loadManifest(path).catch(noop)])) }) + async function loadManifest(path: string) { + const content = await fs.readFile(resolve(root, path, 'manifest.yml'), 'utf8') + return safeLoad(content) as Manifest + } + ctx.on('worker/ready', (response) => { evalConfig.addonNames.map(async (path) => { - const content = await promises.readFile(resolve(root, path, 'manifest.yml'), 'utf8').catch(noop) - if (!content) return - const { commands = [] } = safeLoad(content) as Manifest + const manifest = await manifests[path] + if (!manifest) return + const { commands = [] } = manifest commands.forEach((config) => { - const { name: rawName, desc, options = [] } = config + const { name: rawName, desc, options = [], userFields = [] } = config const [name] = rawName.split(' ', 1) if (!response.commands.includes(name)) { return logger.warn('unregistered command manifest: %c', name) } + const cmd = addon .subcommand(rawName, desc, config) - .userFields(User.fields) - .action(addonAction) + .option('debug', '启用调试模式', { type: 'boolean', hidden: true }) + + UserTrap.attach(cmd, userFields, async ({ session, command, options, user, writable }, ...args) => { + const { $app, $uuid } = session + const { name } = command + const result = await $app.evalRemote.callAddon($uuid, user, writable, { name, args, options }) + return result + }) + options.forEach((config) => { const { name, desc } = config cmd.option(name, desc, config) diff --git a/packages/plugin-eval-addons/src/worker.ts b/packages/plugin-eval-addons/src/worker.ts index c3f5289d03..de7b1ad401 100644 --- a/packages/plugin-eval-addons/src/worker.ts +++ b/packages/plugin-eval-addons/src/worker.ts @@ -1,8 +1,9 @@ -import { config, context, internal, WorkerAPI, contextFactory, response } from 'koishi-plugin-eval/dist/worker' +import { config, context, internal, WorkerAPI, Context, response, mapDirectory, formatError } from 'koishi-plugin-eval/dist/worker' import { promises, readFileSync } from 'fs' -import { resolve } from 'path' -import { Logger } from 'koishi-utils' -import { Config } from '.' +import { resolve, posix, dirname } from 'path' +import { User } from 'koishi-core' +import { Logger, Time, CQCode, Random } from 'koishi-utils' +import json5 from 'json5' import ts from 'typescript' const logger = new Logger('addon') @@ -10,14 +11,16 @@ const logger = new Logger('addon') const { SourceTextModule, SyntheticModule } = require('vm') declare module 'koishi-plugin-eval/dist/worker' { - interface WorkerConfig extends Config {} + interface WorkerConfig { + moduleRoot?: string + } interface WorkerData { addonNames: string[] } interface WorkerAPI { - addon(sid: string, user: {}, argv: WorkerArgv): string | void | Promise + callAddon(sid: string, user: Partial, writable: User.Field[], argv: AddonArgv): Promise } interface Response { @@ -25,64 +28,136 @@ declare module 'koishi-plugin-eval/dist/worker' { } } -interface WorkerArgv { +interface AddonArgv { name: string args: string[] options: Record - rest: string } -type AddonAction = (argv: WorkerArgv) => string | void | Promise +interface AddonContext extends AddonArgv, Context {} + +type AddonAction = (ctx: AddonContext) => string | void | Promise const commandMap: Record = {} -WorkerAPI.prototype.addon = async function (sid, user, argv) { +WorkerAPI.prototype.callAddon = async function (sid, user, writable, argv) { const callback = commandMap[argv.name] try { - return await callback({ ...argv, ...contextFactory(sid, user) }) + const context = { ...argv, ...Context(sid, user, writable) } + const result = await callback(context) + await context.user._update() + return result } catch (error) { - logger.warn(error) + if (!argv.options.debug) return logger.warn(error) + return formatError(error) + .replace('WorkerAPI.worker_1.WorkerAPI.callAddon', 'WorkerAPI.callAddon') } } -const koishi = new SyntheticModule(['registerCommand'], function () { - this.setExport('registerCommand', function registerCommand(name: string, callback: AddonAction) { +// TODO pending @types/node +interface Module { + status: string + identifier: string + namespace: any + link(linker: (specifier: string, referenceModule: Module) => Promise): Promise + evaluate(): Promise +} + +export const modules: Record = {} + +export function synthetize(identifier: string, namespace: {}) { + const module = new SyntheticModule(Object.keys(namespace), function () { + for (const key in namespace) { + this.setExport(key, internal.contextify(namespace[key])) + } + }, { context, identifier }) + modules[identifier] = module + config.addonNames.unshift(identifier) +} + +synthetize('koishi/addons.ts', { + registerCommand(name: string, callback: AddonAction) { commandMap[name] = callback - }) -}, { context }) + }, +}) + +synthetize('koishi/utils.ts', { + Time, CQCode, Random, +}) + +const suffixes = ['', '.ts', '/index.ts'] +const relativeRE = /^\.\.?[\\/]/ + +function locateModule(specifier: string) { + for (const suffix of suffixes) { + const target = specifier + suffix + if (target in modules) return modules[target] + } +} -const root = resolve(process.cwd(), config.moduleRoot) -const modules: Record = { koishi } -config.addonNames.unshift(...Object.keys(modules)) +async function linker(specifier: string, { identifier }: Module) { + // resolve path based on reference module + if (relativeRE.test(specifier)) { + specifier = `${dirname(identifier)}/${specifier}` + } + specifier = posix.normalize(specifier) + + // load from cache + const module = locateModule(specifier) + if (module) return module -function linker(specifier: string, reference: any) { - if (specifier in modules) { - return modules[specifier] + // create new module + const [dir] = specifier.split('/', 1) + if (config.addonNames.includes(dir)) { + return await createModule(specifier) } - throw new Error(`Unable to resolve dependency "${specifier}"`) + + throw new Error(`Unable to resolve dependency "${specifier}" in "${identifier}"`) } -const json = JSON.parse(readFileSync(resolve(root, 'tsconfig.json'), 'utf8')) -const { options: compilerOptions } = ts.parseJsonConfigFileContent(json, ts.sys, root) +const json = json5.parse(readFileSync(resolve(config.moduleRoot, 'tsconfig.json'), 'utf8')) +const { options: compilerOptions } = ts.parseJsonConfigFileContent(json, ts.sys, config.moduleRoot) + +async function loadSource(path: string) { + for (const postfix of suffixes) { + try { + const target = path + postfix + return [await promises.readFile(resolve(config.moduleRoot, target), 'utf8'), target] + } catch {} + } + throw new Error(`cannot load source file "${path}"`) +} async function createModule(path: string) { - if (!modules[path]) { - const content = await promises.readFile(resolve(root, path, 'index.ts'), 'utf8') - const { outputText } = ts.transpileModule(content, { + let module = locateModule(path) + if (!module) { + const [source, identifier] = await loadSource(path) + const { outputText } = ts.transpileModule(source, { compilerOptions, }) - modules[path] = new SourceTextModule(outputText, { context, identifier: path }) + module = modules[identifier] = new SourceTextModule(outputText, { context, identifier }) } - const module = modules[path] + + const type = module instanceof SyntheticModule ? 'synthetic' : 'source text' + logger.debug('creating %s module %c', type, module.identifier) await module.link(linker) await module.evaluate() + + if (!path.includes('/')) { + internal.setGlobal(path, module.namespace) + } + return module +} + +export async function evaluate(path: string) { + try { + await createModule(path) + } catch (error) { + logger.warn(`cannot load module %c\n` + error.stack, path) + } } -export default Promise.all(config.addonNames.map(path => createModule(path).then(() => { - logger.debug('load module %c', path) - internal.setGlobal(path, modules[path].namespace) -}, (error) => { - logger.warn(`cannot load module %c\n` + error.stack, path) - delete modules[path] -}))).then(() => { +export default Promise.all(config.addonNames.map(evaluate)).then(() => { response.commands = Object.keys(commandMap) + mapDirectory('koishi/utils/', require.resolve('koishi-utils')) + internal.setGlobal('utils', modules['koishi/utils.ts'].namespace) }) diff --git a/packages/plugin-eval/package.json b/packages/plugin-eval/package.json index 7d47165187..11b32c7a95 100644 --- a/packages/plugin-eval/package.json +++ b/packages/plugin-eval/package.json @@ -1,6 +1,6 @@ { "name": "koishi-plugin-eval", - "version": "2.0.0-beta.15", + "version": "2.0.0-beta.17", "description": "Execute JavaScript in Koishi", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -37,12 +37,12 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-eval/src/index.ts b/packages/plugin-eval/src/index.ts index e8de3389b0..c985d4d381 100644 --- a/packages/plugin-eval/src/index.ts +++ b/packages/plugin-eval/src/index.ts @@ -1,10 +1,13 @@ -import { App, Context, User, Session } from 'koishi-core' -import { CQCode, Logger, defineProperty, omit, Random } from 'koishi-utils' +import { Context, Session, User } from 'koishi-core' import { Worker, ResourceLimits } from 'worker_threads' +import { CQCode, Logger, defineProperty, Random, pick } from 'koishi-utils' import { WorkerAPI, WorkerConfig, WorkerData, Response } from './worker' import { wrap, expose, Remote } from './transfer' +import { MainAPI, Access, UserTrap } from './main' import { resolve } from 'path' +export * from './main' + declare module 'koishi-core/dist/app' { interface App { _sessions: Record @@ -14,11 +17,17 @@ declare module 'koishi-core/dist/app' { } } +declare module 'koishi-core/dist/command' { + interface CommandConfig { + noEval?: boolean + } +} + declare module 'koishi-core/dist/context' { interface EventMap { - 'worker/start' (): void | Promise - 'worker/ready' (response: Response): void - 'worker/exit' (): void + 'worker/start'(): void | Promise + 'worker/ready'(response: Response): void + 'worker/exit'(): void } } @@ -26,58 +35,35 @@ declare module 'koishi-core/dist/session' { interface Session { $uuid: string _isEval: boolean - _logCount: number + _sendCount: number } } -interface MainConfig { +export interface MainConfig { prefix?: string timeout?: number maxLogs?: number - prohibitedCommands?: string[] + userFields?: Access resourceLimits?: ResourceLimits + dataKeys?: (keyof WorkerData)[] } -interface EvalConfig extends MainConfig, WorkerData {} +export interface EvalConfig extends MainConfig, WorkerData {} export interface Config extends MainConfig, WorkerConfig {} -const defaultConfig: Config = { +const defaultConfig: EvalConfig = { prefix: '>', timeout: 1000, setupFiles: {}, maxLogs: Infinity, - prohibitedCommands: ['evaluate', 'echo', 'broadcast', 'teach', 'contextify'], + userFields: ['id', 'authority'], + dataKeys: ['inspect', 'setupFiles'], } const logger = new Logger('eval') -export class MainAPI { - constructor(public app: App) {} - - private getSession(uuid: string) { - const session = this.app._sessions[uuid] - if (!session) throw new Error(`session ${uuid} not found`) - return session - } - - async execute(uuid: string, message: string) { - const session = this.getSession(uuid) - const send = session.$send - const sendQueued = session.$sendQueued - await session.$execute(message) - session.$sendQueued = sendQueued - session.$send = send - } - - async send(uuid: string, message: string) { - const session = this.getSession(uuid) - if (!session._logCount) session._logCount = 0 - if (this.app.evalConfig.maxLogs > session._logCount++) { - return await session.$sendQueued(message) - } - } -} +export const workerScript = `require(${JSON.stringify(resolve(__dirname, 'worker.js'))});` export function apply(ctx: Context, config: Config = {}) { const { prefix } = config = { ...defaultConfig, ...config } @@ -92,10 +78,11 @@ export function apply(ctx: Context, config: Config = {}) { async function createWorker() { await app.parallel('worker/start') - const worker = app.evalWorker = new Worker(resolve(__dirname, 'worker.js'), { + const worker = app.evalWorker = new Worker(workerScript, { + eval: true, workerData: { logLevels: Logger.levels, - ...omit(config, ['maxLogs', 'resourceLimits', 'timeout', 'prohibitedCommands']), + ...pick(config, config.dataKeys), }, resourceLimits: config.resourceLimits, }) @@ -133,69 +120,71 @@ export function apply(ctx: Context, config: Config = {}) { }) ctx.on('before-command', ({ command, session }) => { - if (config.prohibitedCommands.includes(command.name) && session._isEval) { + if (command.config.noEval && session._isEval) { return `不能在 evaluate 指令中调用 ${command.name} 指令。` } }) - const evaluate = ctx.command('evaluate [expr...]', '执行 JavaScript 脚本') + const cmd = ctx.command('evaluate [expr...]', '执行 JavaScript 脚本', { noEval: true }) .alias('eval') - .userFields(User.fields) + .userFields(['authority']) .option('slient', '-s 不输出最后的结果') .option('restart', '-r 重启子线程', { authority: 3 }) .before((session) => { if (!session['_redirected'] && session.$user?.authority < 2) return '权限不足。' }) - .action(async ({ session, options }, expr) => { - if (options.restart) { - await session.$app.evalWorker.terminate() - return '子线程已重启。' - } - if (!expr) return '请输入要执行的脚本。' - expr = CQCode.unescape(expr) + UserTrap.attach(cmd, config.userFields, async ({ session, options, user, writable }, expr) => { + if (options.restart) { + await session.$app.evalWorker.terminate() + return '子线程已重启。' + } - return await new Promise((resolve) => { - logger.debug(expr) - defineProperty(session, '_isEval', true) + if (!expr) return '请输入要执行的脚本。' + expr = CQCode.unescape(expr) - const _resolve = (result?: string) => { - clearTimeout(timer) - app.evalWorker.off('error', listener) - session._isEval = false - resolve(result) - } + return await new Promise((resolve) => { + logger.debug(expr) + defineProperty(session, '_isEval', true) - const timer = setTimeout(async () => { - await app.evalWorker.terminate() - _resolve(!session._logCount && '执行超时。') - }, config.timeout) - - const listener = (error: Error) => { - let message = ERROR_CODES[error['code']] - if (!message) { - logger.warn(error) - message = '执行过程中遇到错误。' - } - _resolve(message) - } + const _resolve = (result?: string) => { + clearTimeout(timer) + app.evalWorker.off('error', listener) + session._isEval = false + resolve(result) + } - app.evalWorker.on('error', listener) - app.evalRemote.eval({ - sid: session.$uuid, - user: session.$user, - silent: options.slient, - source: expr, - }).then(_resolve, (error) => { + const timer = setTimeout(async () => { + await app.evalWorker.terminate() + _resolve(!session._sendCount && '执行超时。') + }, config.timeout) + + const listener = (error: Error) => { + let message = ERROR_CODES[error['code']] + if (!message) { logger.warn(error) - _resolve() - }) + message = '执行过程中遇到错误。' + } + _resolve(message) + } + + app.evalWorker.on('error', listener) + app.evalRemote.eval({ + user, + writable, + sid: session.$uuid, + silent: options.slient, + source: expr, + }).then(_resolve, (error) => { + logger.warn(error) + _resolve() }) }) + }) if (prefix) { - evaluate.shortcut(prefix, { oneArg: true, fuzzy: true }) - evaluate.shortcut(prefix + prefix, { oneArg: true, fuzzy: true, options: { slient: true } }) + cmd.shortcut(prefix, { oneArg: true, fuzzy: true }) + cmd.shortcut(prefix + prefix, { oneArg: true, fuzzy: true, options: { slient: true } }) } } diff --git a/packages/plugin-eval/src/internal.ts b/packages/plugin-eval/src/internal.ts index 371313f690..231247575e 100644 --- a/packages/plugin-eval/src/internal.ts +++ b/packages/plugin-eval/src/internal.ts @@ -314,7 +314,7 @@ Helper.function = function (this: Helper, fnc, traps, deepTraps, mock) { if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key] if (key === 'constructor') return this.local.Function if (key === '__proto__') return this.local.Function.prototype - if (key === 'toString' && deepTraps === frozenTraps) return () => `function ${fnc.name}() { [native code] }` + if (key === 'toString' && this === Contextify) return () => `function ${fnc.name}() { [native code] }` } catch (e) { // Never pass the handled expcetion through! This block can't throw an exception under normal conditions. return null @@ -622,23 +622,17 @@ const frozenTraps: Trap = createObject({ }) function readonly(value: any, mock: any = {}) { - for (const key in mock) { - const value = mock[key] - if (typeof value === 'function') { - value.toString = () => `function ${value.name}() { [native code] }` - } - } return Contextify.value(value, null, frozenTraps, mock) } -export function setGlobal(name: keyof any, value: any, writable = false, configurable = false) { +export function setGlobal(name: keyof any, value: any, writable = false) { const prop = Contextify.value(name) try { Object.defineProperty(GLOBAL, prop, { value: writable ? Contextify.value(value) : readonly(value), enumerable: true, + configurable: writable, writable, - configurable, }) } catch (e) { throw Decontextify.value(e) @@ -687,7 +681,8 @@ connect(host.Buffer.prototype['inspect'], function inspect() { return `<${this.constructor.name} ${str}>` }) -export const value: (value: T) => T = Decontextify.value.bind(Decontextify) +export const contextify: (value: T) => T = Contextify.value.bind(Contextify) +export const decontextify: (value: T) => T = Decontextify.value.bind(Decontextify) export const sandbox = Decontextify.value(GLOBAL) delete global.console diff --git a/packages/plugin-eval/src/main.ts b/packages/plugin-eval/src/main.ts new file mode 100644 index 0000000000..dde0a36a51 --- /dev/null +++ b/packages/plugin-eval/src/main.ts @@ -0,0 +1,89 @@ +import { App, Command, CommandAction, ParsedArgv, User } from 'koishi-core' + +interface TrappedArgv extends ParsedArgv { + user: Partial + writable: User.Field[] +} + +type TrappedAction = (argv: TrappedArgv, ...args: string[]) => ReturnType + +export interface UserTrap { + fields: Iterable + get(data: Pick): T + set(data: Pick, value: T): void +} + +export namespace UserTrap { + const traps: Record> = {} + + export function define(key: string, trap: UserTrap) { + traps[key] = trap + } + + export function attach(command: Command, fields: Access, action: TrappedAction) { + const { readable = [], writable = [] } = Array.isArray(fields) ? { readable: fields } : fields + for (const field of readable) { + const trap = traps[field] + command.userFields(trap ? trap.fields : [field]) + command.action((argv, ...args) => { + const user = get(argv.session.$user, readable) + return action({ ...argv, user, writable }, ...args) + }) + } + } + + export function get($user: User.Observed, fields: string[]) { + if (!$user) return {} + const result: Partial = {} + for (const field of fields) { + const trap = traps[field] + Reflect.set(result, field, trap ? trap.get($user) : $user[field]) + } + return result + } + + export function set($user: User.Observed, data: Partial) { + for (const field in data) { + const trap = traps[field] + trap ? trap.set($user, data[field]) : $user[field] = data[field] + } + return $user._update() + } +} + +export type Access = T[] | { + readable?: T[] + writable?: T[] +} + +export class MainAPI { + constructor(public app: App) {} + + private getSession(uuid: string) { + const session = this.app._sessions[uuid] + if (!session) throw new Error(`session ${uuid} not found`) + return session + } + + async execute(uuid: string, message: string) { + const session = this.getSession(uuid) + const send = session.$send + const sendQueued = session.$sendQueued + await session.$execute(message) + session.$sendQueued = sendQueued + session.$send = send + } + + async send(uuid: string, message: string) { + const session = this.getSession(uuid) + if (!session._sendCount) session._sendCount = 0 + if (this.app.evalConfig.maxLogs > session._sendCount++) { + return await session.$sendQueued(message) + } + } + + async updateUser(uuid: string, data: Partial) { + const session = this.getSession(uuid) + return UserTrap.set(session.$user, data) + } +} diff --git a/packages/plugin-eval/src/vm.ts b/packages/plugin-eval/src/vm.ts index 405aa5a950..e733b3e50f 100644 --- a/packages/plugin-eval/src/vm.ts +++ b/packages/plugin-eval/src/vm.ts @@ -24,7 +24,7 @@ export class VM { codeGeneration: { strings, wasm }, }) - const filename = resolve(__dirname, 'internal.js') + const filename = resolve(__dirname, '../dist/internal.js') const data = readFileSync(filename, 'utf8') const script = new Script(data, { filename, @@ -39,9 +39,9 @@ export class VM { const script = new Script(code, options) try { - return this.internal.value(script.runInContext(this.context, { displayErrors: false })) + return this.internal.decontextify(script.runInContext(this.context, { displayErrors: false })) } catch (e) { - throw this.internal.value(e) + throw this.internal.decontextify(e) } } } diff --git a/packages/plugin-eval/src/worker.ts b/packages/plugin-eval/src/worker.ts index d596c14110..92b2a35be9 100644 --- a/packages/plugin-eval/src/worker.ts +++ b/packages/plugin-eval/src/worker.ts @@ -1,7 +1,8 @@ -import { Logger, escapeRegExp } from 'koishi-utils' +import { Logger, escapeRegExp, observe, contain } from 'koishi-utils' import { parentPort, workerData } from 'worker_threads' import { InspectOptions, formatWithOptions } from 'util' import { findSourceMap } from 'module' +import { dirname, sep } from 'path' /* eslint-disable import/first */ @@ -11,6 +12,7 @@ const logger = new Logger('eval') import { expose, wrap } from './transfer' import { VM } from './vm' import { MainAPI } from '.' +import { User } from 'koishi-core' export interface WorkerConfig { setupFiles?: Record @@ -29,9 +31,10 @@ export const config: WorkerData = { interface EvalOptions { sid: string - user: {} + user: Partial silent: boolean source: string + writable: User.Field[] } const vm = new VM() @@ -45,7 +48,7 @@ function formatResult(...param: [string, ...any[]]) { return formatWithOptions(config.inspect, ...param) } -function formatError(error: Error) { +export function formatError(error: Error) { if (!(error instanceof Error)) return `Uncaught: ${error}` if (error.name === 'SyntaxError') { @@ -59,7 +62,7 @@ function formatError(error: Error) { } return error.stack - .replace(/\s*.+Script[\s\S]*/, '') + .replace(/\s*.+(Script|MessagePort)[\s\S]*/, '') .split('\n') .map((line) => { for (const name in pathMapper) { @@ -72,21 +75,33 @@ function formatError(error: Error) { const main = wrap(parentPort) -export function contextFactory(sid: string, user: {}) { - return { - user, - async send(...param: [string, ...any[]]) { - return await main.send(sid, formatResult(...param)) - }, - async exec(message: string) { - if (typeof message !== 'string') { - throw new TypeError('The "message" argument must be of type string') - } - return await main.execute(sid, message) - }, - } +export interface Context { + user: User.Observed + send(...param: any[]): Promise + exec(message: string): Promise } +export const Context = (sid: string, user: Partial, writable: User.Field[]): Context => ({ + user: observe(user, async (diff) => { + const diffKeys = Object.keys(diff) + if (!contain(writable, diffKeys)) { + throw new TypeError(`cannot set user field: ${diffKeys.join(', ')}`) + } + await main.updateUser(sid, diff) + }), + + async send(...param: [string, ...any[]]) { + return await main.send(sid, formatResult(...param)) + }, + + async exec(message: string) { + if (typeof message !== 'string') { + throw new TypeError('The "message" argument must be of type string') + } + return await main.execute(sid, message) + }, +}) + export interface Response {} export const response: Response = {} @@ -97,10 +112,11 @@ export class WorkerAPI { } async eval(options: EvalOptions) { - const { sid, source, user, silent } = options + const { sid, user, source, silent, writable } = options - const key = 'koishi-eval-session:' + sid - internal.setGlobal(Symbol.for(key), contextFactory(sid, user), false, true) + const key = 'koishi-eval-context:' + sid + const ctx = Context(sid, user, writable) + internal.setGlobal(Symbol.for(key), ctx, true) let result: any try { @@ -112,6 +128,7 @@ export class WorkerAPI { filename: 'stdin', lineOffset: -4, }) + await ctx.user._update() } catch (error) { return formatError(error) } @@ -121,9 +138,15 @@ export class WorkerAPI { } } +export function mapDirectory(identifier: string, filename: string) { + const sourceMap = findSourceMap(filename) + if (!sourceMap) return logger.warn('cannot find source map for %c', filename) + const path = dirname(sourceMap.payload.sources[0].slice(7)) + sep + pathMapper[identifier] = new RegExp(`(at | \\()${escapeRegExp(path)}`, 'g') +} + Promise.all(Object.values(config.setupFiles).map(file => require(file).default)).then(() => { - const path = findSourceMap(__filename).payload.sources[0].slice(7, -9) - pathMapper['koishi/'] = new RegExp(`(at | \\()${escapeRegExp(path)}`, 'g') + mapDirectory('koishi/', __filename) Object.entries(config.setupFiles).forEach(([name, path]) => { const sourceMap = findSourceMap(path) if (sourceMap) path = sourceMap.payload.sources[0].slice(7) diff --git a/packages/plugin-eval/tests/index.spec.ts b/packages/plugin-eval/tests/index.spec.ts index 5c55a77020..71ba193881 100644 --- a/packages/plugin-eval/tests/index.spec.ts +++ b/packages/plugin-eval/tests/index.spec.ts @@ -1,9 +1,22 @@ +import { defineProperty } from 'koishi-utils' import { App } from 'koishi-test-utils' import { inspect } from 'util' -import * as _eval from '../dist' +import { resolve } from 'path' +import * as _eval from 'koishi-plugin-eval' + +defineProperty(_eval, 'workerScript', [ + 'require("ts-node/register/transpile-only");', + 'require("tsconfig-paths/register");', + `require(${JSON.stringify(resolve(__dirname, '../src/worker.ts'))})`, +].join('\n')) const app = new App() -app.plugin(_eval) + +app.plugin(_eval, { + setupFiles: { + 'test-worker': resolve(__dirname, 'worker.ts'), + }, +}) const ses = app.createSession('user', 123) @@ -30,8 +43,8 @@ describe('koishi-plugin-eval', () => { }) it('exec', async () => { - await ses.shouldHaveReply('> exec()').which.matches(/TypeError: The "message" argument must be of type string/) - await ses.shouldHaveReply('> exec("help")').which.matches(/当前可用的指令有:/) + await ses.shouldHaveReply('> exec()').which.matches(/^TypeError: The "message" argument must be of type string/) + await ses.shouldHaveReply('> exec("help")').which.matches(/^当前可用的指令有:/) }) it('global', async () => { @@ -44,10 +57,49 @@ describe('koishi-plugin-eval', () => { await ses.shouldHaveReply('> Buffer.alloc.toString()', 'function alloc() { [native code] }') }) + it('contextify', async () => { + await ses.shouldHaveReply('> test.null === null', 'true') + await ses.shouldHaveReply('> test.undefined === undefined', 'true') + await ses.shouldHaveReply('> test.string.constructor === String', 'true') + await ses.shouldHaveReply('> test.number.constructor === Number', 'true') + await ses.shouldHaveReply('> test.boolean.constructor === Boolean', 'true') + await ses.shouldHaveReply('> test.stringO instanceof String', 'true') + await ses.shouldHaveReply('> test.numberO instanceof Number', 'true') + await ses.shouldHaveReply('> test.booleanO instanceof Boolean', 'true') + await ses.shouldHaveReply('> test.date instanceof Date', 'true') + await ses.shouldHaveReply('> test.regexp instanceof RegExp', 'true') + await ses.shouldHaveReply('> test.buffer instanceof Buffer', 'true') + await ses.shouldHaveReply('> test.error instanceof Error', 'true') + await ses.shouldHaveReply('> test.rangeError instanceof RangeError', 'true') + await ses.shouldHaveReply('> test.syntaxError instanceof SyntaxError', 'true') + await ses.shouldHaveReply('> test.referenceError instanceof ReferenceError', 'true') + await ses.shouldHaveReply('> test.typeError instanceof TypeError', 'true') + await ses.shouldHaveReply('> test.evalError instanceof EvalError', 'true') + await ses.shouldHaveReply('> test.uriError instanceof URIError', 'true') + }) + + it('function', async () => { + await ses.shouldHaveReply('> test.function instanceof Function', 'true') + await ses.shouldHaveReply('> test.function() instanceof Function', 'true') + await ses.shouldHaveReply('> test.function()() instanceof Object', 'true') + }) + + it('symbol', async () => { + await ses.shouldHaveReply('> Symbol.for("test") === test.symbol2', 'true') + await ses.shouldHaveReply('> test.symbol1.constructor.constructor === Function', 'true') + await ses.shouldHaveReply('> test.symbol2.constructor.constructor === Function', 'true') + await ses.shouldHaveReply('> test.symbol3.constructor.constructor === Function', 'true') + await ses.shouldHaveReply('> Symbol("test").constructor.constructor === Function', 'true') + await ses.shouldHaveReply('> Symbol("foobar").constructor.constructor === Function', 'true') + await ses.shouldHaveReply('> Symbol.keyFor(test.symbol2)', 'test') + }) + it('host inspect', async () => { await ses.shouldHaveReply('> [1, 2]', inspect([1, 2])) await ses.shouldHaveReply('> new Set([1, 2])', inspect(new Set([1, 2]))) await ses.shouldHaveReply('> new Map([[1, 2]])', inspect(new Map([[1, 2]]))) + await ses.shouldHaveReply('> new WeakSet([[1]])', inspect(new WeakSet([[1]]))) + await ses.shouldHaveReply('> new WeakMap([[[1], 2]])', inspect(new WeakMap([[[1], 2]]))) await ses.shouldHaveReply('> new RegExp()', inspect(new RegExp(undefined))) await ses.shouldHaveReply('> Proxy', inspect(Proxy)) }) @@ -62,10 +114,28 @@ describe('koishi-plugin-eval', () => { await ses.shouldHaveReply(`> const ForeignFunction = global.constructor.constructor; const process1 = ForeignFunction("return process")(); - `).which.matches(/ReferenceError: process is not defined/) + `).which.matches(/^ReferenceError: process is not defined/) + }) + + it('deprecated api attack', async () => { + await ses.shouldHaveReply(`> Buffer.prototype.__defineGetter__ === {}.__defineGetter__`, 'true') + await ses.shouldHaveReply(`> Buffer.prototype.__defineSetter__ === {}.__defineSetter__`, 'true') + await ses.shouldHaveReply(`> Buffer.prototype.__lookupGetter__ === {}.__lookupGetter__`, 'true') + await ses.shouldHaveReply(`> Buffer.prototype.__lookupSetter__ === {}.__lookupSetter__`, 'true') + + await ses + .shouldHaveReply(`> Buffer.prototype.__defineGetter__("toString", () => {})`) + .which.matches(/'defineProperty' on proxy: trap returned falsish for property 'toString'/) + + await ses.shouldHaveReply(`> + global.__defineGetter__("foo", () => 123); + global.foo; + `, '123') + + await ses.shouldHaveReply(`> Buffer.from.__lookupGetter__("__proto__") === Object.prototype.__lookupGetter__.call(Buffer.from, "__proto__")`, 'true') }) - it('buffer attack', async () => { + it('buffer operations', async () => { await ses.shouldHaveReply(`> Buffer.allocUnsafe(100).constructor.constructor === Function && Buffer.allocUnsafeSlow(100).constructor.constructor === Function; @@ -75,5 +145,8 @@ describe('koishi-plugin-eval', () => { Buffer.allocUnsafe(100).toString('hex') + Buffer.allocUnsafeSlow(100).toString('hex'); `, '00'.repeat(200)) + + await ses.shouldHaveReply('> test.buffer.inspect()', ``) + await ses.shouldHaveReply('> Buffer.from(Array(51).fill(1)).inspect()', ``) }) }) diff --git a/packages/plugin-eval/tests/worker.ts b/packages/plugin-eval/tests/worker.ts new file mode 100644 index 0000000000..6534e6309e --- /dev/null +++ b/packages/plugin-eval/tests/worker.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-new-wrappers */ + +import { internal } from 'koishi-plugin-eval/dist/worker' + +internal.setGlobal('test', { + null: null, + undefined: undefined, + string: 'text', + number: 1, + boolean: true, + stringO: new String('text'), + numberO: new Number(1), + booleanO: new Boolean(true), + date: new Date(), + regexp: /xxx/, + buffer: Buffer.from(Array(1000).fill(1)), + symbol1: Symbol('test'), + symbol2: Symbol.for('test'), + symbol3: Symbol.iterator, + error: new Error('test'), + rangeError: new RangeError('test'), + syntaxError: new SyntaxError('test'), + referenceError: new ReferenceError('test'), + typeError: new TypeError('test'), + evalError: new EvalError('test'), + uriError: new URIError('test'), + function() { + return () => ({}) + }, +}) diff --git a/packages/plugin-github/package.json b/packages/plugin-github/package.json index 9ce2828dbb..b8797a97b6 100644 --- a/packages/plugin-github/package.json +++ b/packages/plugin-github/package.json @@ -36,12 +36,12 @@ "koishi-test-utils": "^4.0.0-beta.9" }, "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { "@octokit/rest": "^18.0.3", "@octokit/webhooks": "^7.11.0", "axios": "^0.20.0", - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-image-search/package.json b/packages/plugin-image-search/package.json index 7356b48478..45ab8cc20b 100644 --- a/packages/plugin-image-search/package.json +++ b/packages/plugin-image-search/package.json @@ -37,12 +37,12 @@ "pixiv" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { "axios": "^0.20.0", "cheerio": "^1.0.0-rc.3", - "koishi-utils": "^3.1.0", + "koishi-utils": "^3.1.1", "nhentai-api": "^3.0.2" } } diff --git a/packages/plugin-mongo/package.json b/packages/plugin-mongo/package.json index fc9a856f75..055e172908 100644 --- a/packages/plugin-mongo/package.json +++ b/packages/plugin-mongo/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-mongo", "description": "MongoDB support for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -39,7 +39,7 @@ "@types/mongodb": "^3.5.26" }, "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { "mongodb": "^3.6.0" diff --git a/packages/plugin-mongo/src/index.ts b/packages/plugin-mongo/src/index.ts index cc1111a219..e3aa4e5c10 100644 --- a/packages/plugin-mongo/src/index.ts +++ b/packages/plugin-mongo/src/index.ts @@ -106,21 +106,23 @@ extendDatabase(MongoDatabase, { }, async setUser(userId, data) { - const $set = { ...data } - if ($set.timers) { - for (const key in $set.timers) { - if (key === '$date') $set['timers._date'] = $set.timers.$date - else $set[`timers.${key.replace(/\./gmi, '_')}`] = $set.timers[key] + const $set: any = { ...data } + delete $set.timers + delete $set.usage + if (data.timers) { + $set.timers = {} + for (const key in data.timers) { + if (key === '$date') $set.timer._date = data.timers.$date + else $set.timer[key.replace(/\./gmi, '_')] = data.timers[key] } } - if ($set.usage) { - for (const key in $set.usage) { - if (key === '$date') $set['usage._date'] = $set.usage.$date - else $set[`usage.${key.replace(/\./gmi, '_')}`] = $set.usage[key] + if (data.usage) { + $set.usage = {} + for (const key in data.usage) { + if (key === '$date') $set.usage._date = data.usage.$date + else $set.usage[key.replace(/\./gmi, '_')] = data.usage[key] } } - delete $set.timers - delete $set.usage await this.user.updateOne({ _id: userId }, { $set }, { upsert: true }) }, diff --git a/packages/plugin-monitor/package.json b/packages/plugin-monitor/package.json index b0c7cd2123..3fcc86d7bc 100644 --- a/packages/plugin-monitor/package.json +++ b/packages/plugin-monitor/package.json @@ -21,9 +21,9 @@ }, "homepage": "https://github.com/koishijs/koishi#readme", "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-mysql/package.json b/packages/plugin-mysql/package.json index fb39e87046..0ed7a7e646 100644 --- a/packages/plugin-mysql/package.json +++ b/packages/plugin-mysql/package.json @@ -36,10 +36,10 @@ "@types/mysql": "^2.15.15" }, "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { - "koishi-utils": "^3.1.0", + "koishi-utils": "^3.1.1", "mysql": "^2.18.1" } } diff --git a/packages/plugin-puppeteer/package.json b/packages/plugin-puppeteer/package.json index 29b3a3b7e3..0359bf14a8 100644 --- a/packages/plugin-puppeteer/package.json +++ b/packages/plugin-puppeteer/package.json @@ -39,12 +39,12 @@ "koishi-test-utils": "^4.0.0-beta.9" }, "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { "chrome-finder": "^1.0.7", "pngjs": "^5.0.0", "puppeteer-core": "^5.2.1", - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-recorder/package.json b/packages/plugin-recorder/package.json index 0882aba867..5a659c5140 100644 --- a/packages/plugin-recorder/package.json +++ b/packages/plugin-recorder/package.json @@ -34,13 +34,13 @@ "recorder" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "del": "^5.1.0", "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-rss/package.json b/packages/plugin-rss/package.json index 970e76f320..65bb74bda5 100644 --- a/packages/plugin-rss/package.json +++ b/packages/plugin-rss/package.json @@ -36,13 +36,13 @@ "rss" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { "rss-feed-emitter": "^3.2.2", - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-schedule/package.json b/packages/plugin-schedule/package.json index 6216ae7e8a..3fd904f29c 100644 --- a/packages/plugin-schedule/package.json +++ b/packages/plugin-schedule/package.json @@ -34,14 +34,14 @@ "task" ], "devDependencies": { - "koishi-plugin-mongo": "^1.0.0", + "koishi-plugin-mongo": "^1.0.1", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0-beta.9" }, "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "dependencies": { - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-status/package.json b/packages/plugin-status/package.json index 3eba5fdbe7..2288ee868a 100644 --- a/packages/plugin-status/package.json +++ b/packages/plugin-status/package.json @@ -32,16 +32,16 @@ "status" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { "@types/cross-spawn": "^6.0.2", - "koishi-plugin-mongo": "^1.0.0", + "koishi-plugin-mongo": "^1.0.1", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { "cross-spawn": "^7.0.3", - "koishi-utils": "^3.1.0" + "koishi-utils": "^3.1.1" } } diff --git a/packages/plugin-teach/package.json b/packages/plugin-teach/package.json index a2f91064a3..323430c635 100644 --- a/packages/plugin-teach/package.json +++ b/packages/plugin-teach/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-teach", "description": "Teach plugin for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -38,15 +38,15 @@ "conversation" ], "peerDependencies": { - "koishi-core": "^2.0.0" + "koishi-core": "^2.0.1" }, "devDependencies": { - "koishi-plugin-mongo": "^1.0.0", + "koishi-plugin-mongo": "^1.0.1", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0-beta.9" }, "dependencies": { - "koishi-utils": "^3.1.0", + "koishi-utils": "^3.1.1", "leven": "^3.1.0", "regexpp": "^3.1.0" } diff --git a/packages/plugin-teach/src/database/mysql.ts b/packages/plugin-teach/src/database/mysql.ts index d19f2b888b..c0b3e86b44 100644 --- a/packages/plugin-teach/src/database/mysql.ts +++ b/packages/plugin-teach/src/database/mysql.ts @@ -2,7 +2,6 @@ import { Context, extendDatabase, Message } from 'koishi-core' import { clone, defineProperty, Observed, pick } from 'koishi-utils' import { Dialogue, DialogueTest } from '../utils' import { escape } from 'mysql' -import { RegExpError } from '../internal' import { format } from 'util' import MysqlDatabase from 'koishi-plugin-mysql/dist/database' @@ -12,30 +11,6 @@ declare module 'koishi-core/dist/context' { } } -declare module 'koishi-core/dist/plugins/message' { - namespace Message { - export namespace Teach { - let WhitespaceCharset: string - let NonspaceCharset: string - let UnsupportedCharset: string - let UnsupportedWordBoundary: string - let UnsupportedNongreedy: string - let UnsupportedLookaround: string - let UnsupportedNoncapturing: string - let UnsupportedNamedGroup: string - } - } -} - -Message.Teach.WhitespaceCharset = '问题中的空白字符会被自动删除,你无需使用 \\s。' -Message.Teach.NonspaceCharset = '问题中的空白字符会被自动删除,请使用 . 代替 \\S。' -Message.Teach.UnsupportedCharset = '目前不支持在正则表达式中使用 \\%s,请使用 [%s] 代替。' -Message.Teach.UnsupportedWordBoundary = '目前不支持在正则表达式中使用单词边界。' -Message.Teach.UnsupportedNongreedy = '目前不支持在正则表达式中使用非捕获组。' -Message.Teach.UnsupportedLookaround = '目前不支持在正则表达式中使用断言。' -Message.Teach.UnsupportedNoncapturing = '目前不支持在正则表达式中使用非捕获组。' -Message.Teach.UnsupportedNamedGroup = '目前不支持在正则表达式中使用具名组。' - extendDatabase('koishi-plugin-mysql', { async getDialoguesById(ids, fields) { if (!ids.length) return [] @@ -124,32 +99,22 @@ extendDatabase('koishi-plugin-mysql', ({ listFields }) => export default function apply(ctx: Context, config: Dialogue.Config) { config.validateRegExp = { onEscapeCharacterSet(start, end, kind, negate) { - // eslint-disable-next-line curly - if (kind === 'space') throw negate - ? new RegExpError(Message.Teach.WhitespaceCharset) - : new RegExpError(Message.Teach.NonspaceCharset) - let chars = kind === 'digit' ? '0-9' : '_0-9a-z' - let source = kind === 'digit' ? 'd' : 'w' - if (negate) { - chars = '^' + chars - source = source.toUpperCase() - } - throw new RegExpError(format(Message.Teach.UnsupportedCharset, source, chars)) + throw new SyntaxError('unsupported escape character set') }, onQuantifier(start, end, min, max, greedy) { - if (!greedy) throw new RegExpError(Message.Teach.UnsupportedNongreedy) + if (!greedy) throw new SyntaxError('unsupported non-greedy quantifier') }, onWordBoundaryAssertion() { - throw new RegExpError(Message.Teach.UnsupportedWordBoundary) + throw new SyntaxError('unsupported word boundary assertion') }, onLookaroundAssertionEnter() { - throw new RegExpError(Message.Teach.UnsupportedLookaround) + throw new SyntaxError('unsupported lookaround assertion') }, onGroupEnter() { - throw new RegExpError(Message.Teach.UnsupportedNoncapturing) + throw new SyntaxError('unsupported non-capturing group') }, onCapturingGroupEnter(start, name) { - if (name) throw new RegExpError(Message.Teach.UnsupportedNamedGroup) + if (name) throw new SyntaxError('unsupported named capturing group') }, } diff --git a/packages/plugin-teach/src/internal.ts b/packages/plugin-teach/src/internal.ts index 54ae1a0f6a..31fd67b6b6 100644 --- a/packages/plugin-teach/src/internal.ts +++ b/packages/plugin-teach/src/internal.ts @@ -2,11 +2,17 @@ import { Context, Message } from 'koishi-core' import { Dialogue } from './utils' import { update } from './update' import { RegExpValidator } from 'regexpp' -import { defineProperty, Logger } from 'koishi-utils' +import { defineProperty } from 'koishi-utils' import { formatQuestionAnswers } from './search' -import { format, types } from 'util' +import { format } from 'util' import leven from 'leven' +declare module 'koishi-core/dist/command' { + interface CommandConfig { + noInterp?: boolean + } +} + declare module 'koishi-core/dist/plugins/message' { namespace Message { export namespace Teach { @@ -30,14 +36,7 @@ Message.Teach.IllegalRegExp = '问题含有错误的或不支持的正则表达 Message.Teach.MayModifyAnswer = '推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -i 选项以忽略本提示。' Message.Teach.MaybeRegExp = '推测你想%s的问题是正则表达式。发送空行或句号以添加 -x 选项,使用 -i 选项以忽略本提示。' -export class RegExpError extends Error { - name = 'RegExpError' -} - -const validator = new RegExpValidator() - export default function apply(ctx: Context, config: Dialogue.Config) { - const logger = new Logger('teach') defineProperty(ctx.app, 'teachHistory', {}) ctx.command('teach') @@ -86,6 +85,8 @@ export default function apply(ctx: Context, config: Dialogue.Config) { return question.startsWith('^') || question.endsWith('$') } + const validator = new RegExpValidator(config.validateRegExp) + ctx.on('dialogue/before-modify', async (argv) => { const { options, session, target, dialogues } = argv const { question, answer, ignoreHint, regexp } = options @@ -121,17 +122,7 @@ export default function apply(ctx: Context, config: Dialogue.Config) { try { questions.map(q => validator.validatePattern(q)) } catch (error) { - if (!types.isNativeError(error)) { - logger.warn(question, error) - return Message.Teach.IllegalRegExp - } else if (error.name === 'RegExpError') { - return error.message - } else { - if (!error.message.startsWith('SyntaxError')) { - logger.warn(question, error.stack) - } - return Message.Teach.IllegalRegExp - } + return Message.Teach.IllegalRegExp } } }) @@ -184,9 +175,8 @@ export default function apply(ctx: Context, config: Dialogue.Config) { }) }) - const { prohibitedCommands = [] } = config ctx.on('before-command', ({ command, session }) => { - if (prohibitedCommands.includes(command.name) && session._redirected) { + if (command.config.noInterp && session._redirected) { return format(Message.Teach.ProhibitedCommand, command.name) } }) diff --git a/packages/plugin-teach/src/plugins/context.ts b/packages/plugin-teach/src/plugins/context.ts index 2c139eee1a..71fbc0fc90 100644 --- a/packages/plugin-teach/src/plugins/context.ts +++ b/packages/plugin-teach/src/plugins/context.ts @@ -18,12 +18,17 @@ declare module '../utils' { groups?: string[] partial?: boolean reversed?: boolean - noContextOptions?: boolean + } + + interface Config { + useContext?: boolean } } } export default function apply(ctx: Context, config: Dialogue.Config) { + if (config.useContext === false) return + ctx.command('teach') .option('disable', '-d 在当前环境下禁用问答') .option('disableGlobal', '-D 在所有环境下禁用问答', { authority: 3 }) @@ -46,12 +51,12 @@ export default function apply(ctx: Context, config: Dialogue.Config) { } else if (options.disableGlobal && options.enableGlobal) { return '选项 -D, -E 不能同时使用。' } else if (options.disableGlobal && options.disable) { - return '选项 -d, -D 不能同时使用。' + return '选项 -D, -d 不能同时使用。' } else if (options.enable && options.enableGlobal) { - return '选项 -e, -E 不能同时使用。' + return '选项 -E, -e 不能同时使用。' } - argv.noContextOptions = false + let noContextOptions = false if (options.disable) { argv.reversed = true argv.partial = !options.enableGlobal @@ -65,7 +70,7 @@ export default function apply(ctx: Context, config: Dialogue.Config) { argv.partial = false argv.groups = [] } else { - argv.noContextOptions = !options.enable + noContextOptions = !options.enable if (options.target ? options.enable : !options.global) { argv.reversed = false argv.partial = true @@ -74,13 +79,13 @@ export default function apply(ctx: Context, config: Dialogue.Config) { } if ('groups' in options) { - if (argv.noContextOptions) { - return '参数 -g, --groups 必须与 -d/-D/-e/-E 之一同时使用。' + if (noContextOptions) { + return '选项 -g, --groups 必须与 -d/-D/-e/-E 之一同时使用。' } else { argv.groups = options.groups ? options.groups.split(',') : [] } } else if (session.messageType !== 'group' && argv.partial) { - return '非群聊上下文中请使用 -E/-D 进行操作或指定 -g, --groups 参数。' + return '非群聊上下文中请使用 -E/-D 进行操作或指定 -g, --groups 选项。' } }) diff --git a/packages/plugin-teach/src/plugins/preventLoop.ts b/packages/plugin-teach/src/plugins/preventLoop.ts index 948dac50f1..e2bd410736 100644 --- a/packages/plugin-teach/src/plugins/preventLoop.ts +++ b/packages/plugin-teach/src/plugins/preventLoop.ts @@ -35,6 +35,7 @@ export default function apply(ctx: Context, config: Dialogue.Config) { }) ctx.on('dialogue/receive', (state) => { + if (state.session._redirected) return const timestamp = Date.now() for (const { participants, length, debounce } of preventLoopConfig) { if (state.initiators.length < length) break diff --git a/packages/plugin-teach/src/plugins/successor.ts b/packages/plugin-teach/src/plugins/successor.ts index e2f3afb35f..672b799e4b 100644 --- a/packages/plugin-teach/src/plugins/successor.ts +++ b/packages/plugin-teach/src/plugins/successor.ts @@ -39,6 +39,7 @@ declare module '../utils' { export default function apply(ctx: Context, config: Dialogue.Config) { const { successorTimeout = 20000 } = config + if (!successorTimeout) return ctx.command('teach') .option('setPred', '< 设置前置问题', { type: 'string', validate: RE_DIALOGUES }) diff --git a/packages/plugin-teach/src/plugins/throttle.ts b/packages/plugin-teach/src/plugins/throttle.ts index a07b83fb37..2514c35c11 100644 --- a/packages/plugin-teach/src/plugins/throttle.ts +++ b/packages/plugin-teach/src/plugins/throttle.ts @@ -35,13 +35,15 @@ export default function apply(ctx: Context, config: Dialogue.Config) { state.counters = { ...counters } }) - ctx.on('dialogue/receive', ({ counters }) => { + ctx.on('dialogue/receive', ({ counters, session }) => { + if (session._redirected) return for (const interval in counters) { if (counters[interval] <= 0) return true } }) - ctx.on('dialogue/before-send', ({ counters }) => { + ctx.on('dialogue/before-send', ({ counters, session }) => { + if (session._redirected) return for (const { interval } of throttleConfig) { counters[interval]-- setTimeout(() => counters[interval]++, interval) diff --git a/packages/plugin-teach/src/plugins/time.ts b/packages/plugin-teach/src/plugins/time.ts index 8bac76ca67..e114e22674 100644 --- a/packages/plugin-teach/src/plugins/time.ts +++ b/packages/plugin-teach/src/plugins/time.ts @@ -1,4 +1,5 @@ import { Context } from 'koishi-core' +import { Dialogue } from '../utils' declare module '../utils' { interface DialogueTest { @@ -10,6 +11,12 @@ declare module '../utils' { startTime: number endTime: number } + + namespace Dialogue { + interface Config { + useTime?: boolean + } + } } export function isHours(value: string) { @@ -19,7 +26,9 @@ export function isHours(value: string) { return !(hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) } -export default function apply(ctx: Context) { +export default function apply(ctx: Context, config: Dialogue.Config) { + if (config.useTime === false) return + ctx.command('teach') .option('startTime', '-t