Commit 51fa10d4 authored by nanahira's avatar nanahira

bump and dev server

parent da50de67
/install-npm.sh
.git*
/data
/output
/dest
/config.yaml
.idea
.dockerignore
Dockerfile
/src
.eslintrc.js
.prettierrc
webpack.config.js
tsconfig.json/dist/full
/dist/full
/coverage
/tests
/dist/tests
/dev
/dist/dev
/webpack.config.js
/.eslint*
.*ignore
.prettierrc*
import { Context } from 'koishi';
export default class ExtrasInDev {
constructor(ctx: Context) {}
}
import { App } from 'koishi';
import TargetPlugin from '../src';
import ConsolePlugin from '@koishijs/plugin-console';
import SandboxPlugin from '@koishijs/plugin-sandbox';
import DatabasePlugin from '@koishijs/plugin-database-memory';
import CachePlugin from '@koishijs/plugin-cache-lru';
import ExtrasInDev from './extras';
const app = new App({
port: 14514,
host: 'localhost',
prefix: '.',
});
// Console and sandbox
app.plugin(SandboxPlugin);
app.plugin(ConsolePlugin, {
open: false,
});
// Some services
app.plugin(CachePlugin);
app.plugin(DatabasePlugin);
// Some extras
app.plugin(ExtrasInDev);
// Target plugin
app.plugin(TargetPlugin, {});
app.start();
This diff is collapsed.
......@@ -6,7 +6,9 @@
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint --fix .",
"build": "webpack && env PACK_ALL=1 webpack"
"build": "webpack && env PACK_ALL=1 webpack",
"start": "ts-node ./dev",
"test": "jest --passWithNoTests"
},
"repository": {
"type": "git",
......@@ -26,11 +28,16 @@
},
"homepage": "https://code.mycard.moe/3rdeye/koishi-plugin-thesaurus",
"dependencies": {
"koishi-thirdeye": "^8.3.3",
"lodash": "^4.17.21",
"schemastery-gen": "^2.4.2",
"source-map-support": "^0.5.20"
},
"devDependencies": {
"@koishijs/plugin-cache-lru": "^1.0.0-rc.0",
"@koishijs/plugin-console": "^3.1.1",
"@koishijs/plugin-database-memory": "^1.0.2",
"@koishijs/plugin-sandbox": "^1.0.0",
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.175",
"@types/node": "^16.11.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
......@@ -38,15 +45,35 @@
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"jest": "^27.5.1",
"prettier": "^2.4.1",
"raw-loader": "^4.0.2",
"ts-jest": "^27.1.3",
"ts-loader": "^9.2.6",
"ts-node": "^10.5.0",
"typescript": "^4.4.4",
"webpack": "^5.58.2",
"webpack-cli": "^4.9.1",
"ws": "^8.3.0"
},
"peerDependencies": {
"koishi": "^4.2.1"
"koishi": "^4.2.2"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "tests",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
import 'source-map-support/register';
import { DefineSchema, RegisterSchema } from 'schemastery-gen';
import { DefineSchema, RegisterSchema } from 'koishi-thirdeye';
@RegisterSchema()
export class MyPluginConfig {
constructor(config: Partial<MyPluginConfig>) {}
export class ThesaurusPluginConfig {
constructor(config: Partial<ThesaurusPluginConfig>) {}
@DefineSchema({
description: '词库本地路径或 URL。',
default:
......@@ -23,12 +23,6 @@ export class MyPluginConfig {
default: 10,
})
trackingLength: number;
@DefineSchema({
description: '从数据库获取用户名。',
default: false,
})
useDatabase: boolean;
}
export type MyPluginConfigLike = Partial<MyPluginConfig>;
export type ThesaurusPluginConfigLike = Partial<ThesaurusPluginConfig>;
import 'source-map-support/register';
import { Context } from 'koishi';
import { MyPlugin } from './plugin';
import { MyPluginConfig } from './config';
export { MyPluginConfig } from './config';
export { ChatSession } from './plugin';
export const name = 'thesaurus';
const plugin = new MyPlugin();
export const Config = plugin.schema;
export const using = ['cache'];
export function apply(ctx: Context, config: MyPluginConfig) {
ctx.plugin(plugin, config);
import { Random, sleep, Cache } from 'koishi';
import { ThesaurusPluginConfig } from './config';
import { promises as fs } from 'fs';
import _ from 'lodash';
import {
BasePlugin,
DefinePlugin,
Inject,
LifecycleEvents,
} from 'koishi-thirdeye';
export interface ChatSession {
name: string;
playedWords: string[];
}
export * from './config';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cache {
interface Tables {
thesaurusChatSession: ChatSession;
}
}
}
@DefinePlugin({ name: 'thesaurus', schema: ThesaurusPluginConfig })
export default class ThesaurusPlugin
extends BasePlugin<ThesaurusPluginConfig>
implements LifecycleEvents
{
@Inject(true)
private cache: Cache;
private wordData: Record<string, string[]> = {};
private async loadWords(): Promise<void> {
const path = this.config.path;
try {
if (path.match(/^https?:\/\//)) {
this.wordData = await this.ctx.http.get(path);
} else {
this.wordData = JSON.parse(await fs.readFile(path, 'utf-8'));
}
this.ctx
.logger('thesaurus')
.info(
`Loaded ${
Object.keys(this.wordData).length
} patterns from source ${path} .`,
);
} catch (e) {
this.ctx
.logger('thesaurus')
.error(`Failed to load words from source ${path}: ${e.toString()}`);
await sleep(5000);
return this.loadWords();
}
}
private async getFromCache(sessionId: string) {
if (!sessionId) {
return;
}
return this.cache.get('thesaurusChatSession', sessionId);
}
private async setToCache(sessionId: string, chatSession: ChatSession) {
if (!sessionId) {
return;
}
return this.cache.set(
'thesaurusChatSession',
sessionId,
chatSession,
this.config.chatTimeout,
);
}
private async clearCacheOf(sessionId: string) {
if (!sessionId) {
return;
}
return this.cache.del('thesaurusChatSession', sessionId);
}
private async replyWith(
word: string,
sessionId: string,
chatSession: ChatSession,
) {
if (!word) {
return;
}
chatSession.playedWords.push(word);
if (chatSession.playedWords.length > this.config.trackingLength) {
chatSession.playedWords.shift();
}
await this.setToCache(sessionId, chatSession);
return word.replace(/你/g, chatSession.name).trim();
}
private tryPatterns(patterns: string[], usedWords: Set<string>) {
if (!patterns.length) {
return;
}
const pattern = Random.pick(patterns);
const availableWords = this.wordData[pattern].filter(
(w) => !usedWords.has(w),
);
if (!availableWords.length) {
const shrinkedPatterns = patterns.filter((p) => p !== pattern);
return this.tryPatterns(shrinkedPatterns, usedWords);
}
return Random.pick(availableWords);
}
private async replyChat(
word: string,
sessionId: string,
chatSession: ChatSession,
) {
const matchingPatterns = Object.keys(this.wordData).filter((w) =>
word.includes(w),
);
if (!matchingPatterns.length) {
const allWords = _.flatten(Object.values(this.wordData));
let remainingAllWords = allWords.filter(
(w) => !chatSession.playedWords.includes(w),
);
if (!remainingAllWords.length) {
remainingAllWords = allWords;
}
return this.replyWith(
Random.pick(remainingAllWords),
sessionId,
chatSession,
);
}
const replyWord =
this.tryPatterns(
matchingPatterns,
new Set<string>(chatSession.playedWords),
) || this.tryPatterns(matchingPatterns, new Set<string>());
return this.replyWith(replyWord, sessionId, chatSession);
}
private formatName(name: string) {
if (name.match(/^[a-zA-Z0-9]/)) {
name = ` ${name}`;
}
if (name.match(/[a-zA-Z0-9]$/)) {
name = `${name} `;
}
return name;
}
async onConnect() {
const ctx = this.ctx;
this.cache.table('thesaurusChatSession', {
maxAge: this.config.chatTimeout,
});
await this.loadWords();
ctx.middleware(async (session, next) => {
const sessionId = `${session.platform}.${session.selfId}.${session.userId}`;
const chatSession = await this.getFromCache(sessionId);
if (chatSession) {
//ctx
// .logger('thesaurus')
// .warn(`${session.userId} in chat ${JSON.stringify(chatSession)}`);
if (session.content === 'quit') {
await this.clearCacheOf(sessionId);
await session.send('记得下次再找我聊喔~');
return;
}
const reply = await this.replyChat(
session.content,
sessionId,
chatSession,
);
await session.send(reply);
}
//ctx.logger('thesaurus').warn(`${session.userId} outside session`);
return next();
});
ctx
.command('startchat', '开始聊天')
.usage('聊天开始后, quit 停止聊天。')
.example('startchat --name Shigma 可以使用 Shigma 作为自己的名字来聊天。')
.option('name', '--name <name:string>')
.userFields(['name'])
.action(async (argv) => {
const { session, options } = argv;
const sessionId = `${session.platform}.${session.selfId}.${session.userId}`;
let name = session.username;
if (options.name) {
name = options.name;
}
if (!name) {
name = '';
}
const formattedName = this.formatName(name);
await this.setToCache(sessionId, {
name: formattedName,
playedWords: [],
});
return `${formattedName},开始和我聊天吧。聊累了的话,输入 quit 就可以结束聊天哦。`.trim();
});
}
}
import 'source-map-support/register';
import { Context, Random } from 'koishi';
import { MyPluginConfig } from './config';
import { promises as fs } from 'fs';
import { sleep } from 'koishi';
import _ from 'lodash';
export interface ChatSession {
name: string;
playedWords: string[];
}
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cache {
interface Tables {
thesaurusChatSession: ChatSession;
}
}
}
export class MyPlugin {
private config: MyPluginConfig;
private ctx: Context;
private wordData: Record<string, string[]> = {};
private useDatabase = false;
private async loadWords(): Promise<void> {
const path = this.config.path;
try {
if (path.match(/^https?:\/\//)) {
this.wordData = await this.ctx.http.get(path);
} else {
this.wordData = JSON.parse(await fs.readFile(path, 'utf-8'));
}
this.ctx
.logger('thesaurus')
.info(
`Loaded ${
Object.keys(this.wordData).length
} patterns from source ${path} .`,
);
} catch (e) {
this.ctx
.logger('thesaurus')
.error(`Failed to load words from source ${path}: ${e.toString()}`);
await sleep(5000);
return this.loadWords();
}
}
private async getFromCache(sessionId: string) {
if (!sessionId) {
return;
}
return this.ctx.cache.get('thesaurusChatSession', sessionId);
}
private async setToCache(sessionId: string, chatSession: ChatSession) {
if (!sessionId) {
return;
}
return this.ctx.cache.set(
'thesaurusChatSession',
sessionId,
chatSession,
this.config.chatTimeout,
);
}
private async clearCacheOf(sessionId: string) {
if (!sessionId) {
return;
}
return this.ctx.cache.del('thesaurusChatSession', sessionId);
}
private async replyWith(
word: string,
sessionId: string,
chatSession: ChatSession,
) {
if (!word) {
return;
}
chatSession.playedWords.push(word);
if (chatSession.playedWords.length > this.config.trackingLength) {
chatSession.playedWords.shift();
}
await this.setToCache(sessionId, chatSession);
return word.replace(/你/g, chatSession.name).trim();
}
private tryPatterns(patterns: string[], usedWords: Set<string>) {
if (!patterns.length) {
return;
}
const pattern = Random.pick(patterns);
const availableWords = this.wordData[pattern].filter(
(w) => !usedWords.has(w),
);
if (!availableWords.length) {
const shrinkedPatterns = patterns.filter((p) => p !== pattern);
return this.tryPatterns(shrinkedPatterns, usedWords);
}
return Random.pick(availableWords);
}
private async replyChat(
word: string,
sessionId: string,
chatSession: ChatSession,
) {
const matchingPatterns = Object.keys(this.wordData).filter((w) =>
word.includes(w),
);
if (!matchingPatterns.length) {
const allWords = _.flatten(Object.values(this.wordData));
let remainingAllWords = allWords.filter(
(w) => !chatSession.playedWords.includes(w),
);
if (!remainingAllWords.length) {
remainingAllWords = allWords;
}
return this.replyWith(
Random.pick(remainingAllWords),
sessionId,
chatSession,
);
}
const replyWord =
this.tryPatterns(
matchingPatterns,
new Set<string>(chatSession.playedWords),
) || this.tryPatterns(matchingPatterns, new Set<string>());
return this.replyWith(replyWord, sessionId, chatSession);
}
private formatName(name: string) {
if (name.match(/^[a-zA-Z0-9]/)) {
name = ` ${name}`;
}
if (name.match(/[a-zA-Z0-9]$/)) {
name = `${name} `;
}
return name;
}
name = 'thesaurus-main';
schema = MyPluginConfig;
async apply(ctx: Context, config: MyPluginConfig) {
this.ctx = ctx;
this.config = config;
ctx.on('service', (name) => {
if (this.config.useDatabase && name === 'database')
this.useDatabase = !!ctx.database;
});
ctx.on('service', (name) => {
if (!ctx.cache || name !== 'cache') {
return;
}
ctx.cache.table('thesaurusChatSession', {
maxAge: this.config.chatTimeout,
});
});
await this.loadWords();
ctx.middleware(async (session, next) => {
const sessionId = `${session.platform}.${session.selfId}.${session.userId}`;
const chatSession = await this.getFromCache(sessionId);
if (chatSession) {
//ctx
// .logger('thesaurus')
// .warn(`${session.userId} in chat ${JSON.stringify(chatSession)}`);
if (session.content === 'quit') {
await this.clearCacheOf(sessionId);
await session.send('记得下次再找我聊喔~');
return
}
const reply = await this.replyChat(
session.content,
sessionId,
chatSession,
);
await session.send(reply);
}
//ctx.logger('thesaurus').warn(`${session.userId} outside session`);
return next();
});
ctx
.command('startchat', '开始聊天')
.usage('聊天开始后, quit 停止聊天。')
.example('startchat --name Shigma 可以使用 Shigma 作为自己的名字来聊天。')
.option('name', '--name <name:string>')
.userFields(['name'])
.action(async (argv) => {
const { session, options } = argv;
const sessionId = `${session.platform}.${session.selfId}.${session.userId}`;
let name =
session.author?.nickname ||
session.author?.username ||
session.username;
if (this.useDatabase) {
if (session.user.name) {
name = session.user.name;
}
}
if (options.name) {
name = options.name;
}
if (!name) {
name = '';
}
const formattedName = this.formatName(name);
await this.setToCache(sessionId, {
name: formattedName,
playedWords: [],
});
return `${formattedName},开始和我聊天吧。聊累了的话,输入 quit 就可以结束聊天哦。`.trim();
});
}
}
import { App } from 'koishi';
import TargetPlugin from '../src';
describe('Test of plugin.', () => {
let app: App;
beforeEach(async () => {
app = new App();
// app.plugin(TargetPlugin);
await app.start();
});
it('should pass', () => {
expect(true).toBe(true);
});
});
......@@ -7,12 +7,15 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true
"sourceMap": true,
"skipLibCheck": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts"
"src/**/*.ts",
"tests/**/*.ts",
"dev/**/*.ts"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment