Commit f85f6ea5 authored by nanahira's avatar nanahira

first

parent 5568a190
webpack.config.js
dist/*
build/*
test/*
\ No newline at end of file
......@@ -105,5 +105,6 @@ dist
/build
/output
/dest
/config.yaml
.idea
stages:
- build
- combine
- deploy
variables:
GIT_DEPTH: "1"
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
CONTAINER_TEST_ARM_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
CONTAINER_TEST_X86_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build-x86:
stage: build
tags:
- docker
script:
- docker build --pull -t $CONTAINER_TEST_X86_IMAGE .
- docker push $CONTAINER_TEST_X86_IMAGE
build-arm:
build:
stage: build
tags:
- docker-arm
- linux
script:
- docker build --pull -t $CONTAINER_TEST_ARM_IMAGE .
- docker push $CONTAINER_TEST_ARM_IMAGE
- npm ci
- npm run build
artifacts:
paths:
- dist/
combine:
stage: combine
tags:
- docker
script:
- docker pull $CONTAINER_TEST_X86_IMAGE
- docker pull $CONTAINER_TEST_ARM_IMAGE
- docker manifest create $CONTAINER_TEST_IMAGE --amend $CONTAINER_TEST_X86_IMAGE --amend $CONTAINER_TEST_ARM_IMAGE
- docker manifest push $CONTAINER_TEST_IMAGE
deploy_latest:
upload_to_minio:
stage: deploy
dependencies:
- build
tags:
- docker
- linux
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
- docker push $CONTAINER_RELEASE_IMAGE
- aws s3 --endpoint=https://minio.mycard.moe:9000 sync --delete dist/ s3://nanahira/koishi-plugin/hisoutensoku-jammer
only:
- master
deploy_tag:
deploy_npm:
stage: deploy
dependencies:
- build
tags:
- docker
variables:
CONTAINER_TAG_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- linux
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_TAG_IMAGE
- docker push $CONTAINER_TAG_IMAGE
- apt update;apt -y install coreutils
- echo $NPMRC | base64 --decode > ~/.npmrc
- npm publish . || true
only:
- tags
- master
......@@ -6,3 +6,9 @@
.idea
.dockerignore
Dockerfile
/test
/src
/test-playbook*
/playbooks
/.eslint*
/webpack.config.js
FROM node:buster-slim
LABEL Author="Nanahira <nanahira@momobako.com>"
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/app
COPY ./package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
CMD ["npm", "run", "start"]
This diff is collapsed.
# koishi-plugin-act
# koishi-plugin-init
#!/bin/bash
npm install --save \
lodash
source-map-support
npm install --save-dev \
lodash \
@types/node \
@types/lodash \
typescript \
......@@ -11,4 +12,9 @@ npm install --save-dev \
'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0' \
prettier
prettier \
raw-loader \
ts-loader \
webpack \
webpack-cli \
koishi-core
This diff is collapsed.
......@@ -2,27 +2,42 @@
"name": "koishi-plugin-act",
"version": "1.0.0",
"description": "",
"main": "build/index.js",
"main": "dist/index.js",
"dependencies": {
"class-transformer": "^0.4.0",
"delay": "^5.0.0",
"lodash": "^4.17.21",
"mustache": "^4.2.0"
"source-map-support": "^0.5.19"
},
"peerDependencies": {
"koishi-adapter-onebot": "^3.1.0",
"koishi-core": "^3.13.0"
},
"devDependencies": {
"@types/lodash": "^4.14.172",
"@types/mustache": "^4.1.2",
"@types/node": "^16.4.10",
"@types/node": "^16.4.11",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"class-transformer": "^0.4.0",
"delay": "^5.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"koishi-adapter-onebot": "^3.1.0",
"koishi-core": "^3.13.0",
"load-json-file": "^6.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mustache": "^4.2.0",
"prettier": "^2.3.2",
"typescript": "^4.3.5"
"raw-loader": "^4.0.2",
"reflect-metadata": "^0.1.13",
"ts-loader": "^9.2.5",
"typescript": "^4.3.5",
"webpack": "^5.48.0",
"webpack-cli": "^4.7.2"
},
"scripts": {
"lint": "echo \"Error: no test specified\" && exit 1"
"lint": "eslint --fix .",
"build": "webpack"
},
"repository": {
"type": "git",
......
import 'source-map-support/register';
import type { Context } from 'koishi-core';
import { Config, MyPlugin } from './plugin';
export { Config } from './plugin';
export const name = 'act-index';
export function apply(ctx: Context, config: Config) {
ctx.plugin(new MyPlugin(), config);
}
......@@ -4,12 +4,10 @@ export class Character {
playFun: (line: PlaybookLine, text: string) => Promise<void>;
constructor(
public id: number,
public name: string,
public displayName?: string,
) {
if (!displayName) {
this.displayName = name;
}
public name: string, //public displayName?: string,
) {}
getDisplayName() {
return /*this.displayName || */ this.name;
}
async play(line: PlaybookLine, text: string) {
if (this.playFun) {
......
import { PlaybookLine } from './PlaybookLine';
import { matchHeader, TuneTimeOptions } from './utility';
import { Character } from './Character';
import { Transform } from 'class-transformer';
import { Type } from 'class-transformer';
import 'reflect-metadata';
export class Playbook {
@Type(() => Character)
characters: Character[] = [];
@Type(() => PlaybookLine)
lines: PlaybookLine[] = [];
constructor(public characterNameMap: Record<string, string> = {}) {}
getCharacterDisplayName(name: string) {
constructor(/*public characterNameMap: Record<string, string> = {}*/) {}
/*getCharacterDisplayName(name: string) {
if (this.characterNameMap[name]) {
return this.characterNameMap[name];
} else {
return name;
}
}
}*/
resetTiming() {
const firstLineTime = this.lines[0].time;
if (!firstLineTime) {
......@@ -28,13 +31,7 @@ export class Playbook {
let currentCharacterIndex = 0;
for (const line of this.lines) {
if (!characterMap.has(line.name)) {
this.characters.push(
new Character(
currentCharacterIndex++,
line.name,
this.getCharacterDisplayName(line.name),
),
);
this.characters.push(new Character(currentCharacterIndex++, line.name));
characterMap.set(line.name, true);
}
}
......@@ -51,7 +48,7 @@ export class Playbook {
continue;
}
if (matchHeader(line)) {
if (lastLineIndex > 0 && i - lastLineIndex > 1) {
if (lastLineIndex >= 0 && i - lastLineIndex > 1) {
this.lines.push(
PlaybookLine.FromLines(lines.slice(lastLineIndex, i)),
);
......@@ -68,7 +65,7 @@ export class Playbook {
this.resetTiming();
this.fixupCharacters();
}
private findPreviousSelfLine(i) {
private findPreviousSelfLine(i: number) {
const currentLine = this.lines[i];
for (let j = i - 1; j >= 0; j--) {
const tryLine = this.lines[j];
......@@ -87,7 +84,7 @@ export class Playbook {
const currentLine = this.lines[i];
const previousLine = this.lines[i - 1];
const selfPreviousLine = this.findPreviousSelfLine(i);
const randomAddValue = Math.ceil(Math.random() * options.maxRandomDelay);
const randomAddValue = Math.random() * options.maxRandomDelay;
const possibleReadTiming =
previousLine.time +
options.readSlope * previousLine.renderContent(this.characters).length +
......@@ -100,8 +97,9 @@ export class Playbook {
currentLine.renderContent(this.characters).length +
options.writeIntercept;
}
currentLine.time =
Math.max(possibleReadTiming, possibleWriteTiming) + randomAddValue;
currentLine.time = Math.ceil(
Math.max(possibleReadTiming, possibleWriteTiming) + randomAddValue,
);
}
}
async play(tick: number) {
......
......@@ -4,8 +4,8 @@ import { Character } from './Character';
export class PlaybookLine {
characterId?: number;
name?: string;
content: string;
time: number;
content = '';
time = 0;
static FromLines(lines: string[]) {
const l = new PlaybookLine();
l.fromLines(lines);
......@@ -39,16 +39,23 @@ export class PlaybookLine {
for (let i = 0; i < characters.length; ++i) {
this.content = this.content.replace(
new RegExp(characters[i].name, 'g'),
`{{${i}.displayName}}`,
`{{${i}}}`,
);
}
this.name = undefined;
}
renderContent(characters: Character[]) {
return render(this.content, characters);
const view: Record<number, string> = {};
for (const c of characters) {
view[c.id] = c.name;
}
return render(this.content, view);
}
play(characters: Character[]) {
const character = characters.find((c) => c.id === this.characterId);
if (!character) {
return;
}
return character.play(this, this.renderContent(characters));
}
}
import { Playbook } from './Playbook';
import type { CQBot } from 'koishi-adapter-onebot';
import moment, { Moment } from 'moment';
export enum ShowStatus {
Idle,
Running,
Finished,
}
export class Show {
status: ShowStatus = ShowStatus.Idle;
private characterBotMap = new Map<number, CQBot>();
// eslint-disable-next-line @typescript-eslint/no-empty-function
onFinish: (message: string, show: Show) => void = () => {};
private step = 0;
private launchTime: Moment;
constructor(
public groupId: number,
private playbook: Playbook,
private autoChangeName = false,
) {
for (const character of playbook.characters) {
character.playFun = async (line, text) => {
const bot = this.characterBotMap.get(character.id);
await bot.$sendGroupMsg(this.groupId, text, true);
};
}
}
private getPerformingBots() {
return Array.from(this.characterBotMap.values());
}
useCharacter(id: number, bot: CQBot) {
this.characterBotMap.set(id, bot);
}
isCharacterFull() {
return this.playbook.characters.every((c) =>
this.characterBotMap.has(c.id),
);
}
async autoCharacters(bots: CQBot[]) {
let availableBots: CQBot[] = [];
const botNameMap = new Map<string, string>();
for (const bot of bots) {
if (!(bot.type && bot.type.startsWith('onebot'))) {
continue;
}
const groups = await bot.$getGroupList();
const matchGroup = groups.find((g) => g.groupId === this.groupId);
if (matchGroup) {
availableBots.push(bot);
botNameMap.set(
bot.selfId,
(await bot.$getGroupMemberInfo(this.groupId, bot.selfId)).nickname,
);
}
}
for (const character of this.playbook.characters) {
// exact
let matchBot = availableBots.find(
(b) => botNameMap.get(b.selfId) === character.name,
);
// match 1
if (!matchBot) {
matchBot = availableBots.find((b) =>
botNameMap.get(b.selfId).includes(character.name),
);
}
// match 2
if (!matchBot) {
matchBot = availableBots.find((b) =>
character.name.includes(botNameMap.get(b.selfId)),
);
}
if (matchBot) {
this.useCharacter(character.id, matchBot);
availableBots = availableBots.filter(
(b) => !this.getPerformingBots().includes(b),
);
}
}
for (const character of this.playbook.characters) {
if (this.characterBotMap.has(character.id)) {
continue;
}
const matchBot = availableBots.find((b) => true);
if (matchBot) {
this.useCharacter(character.id, matchBot);
availableBots = availableBots.filter(
(b) => !this.getPerformingBots().includes(b),
);
}
}
return this.isCharacterFull();
}
private showInterval: NodeJS.Timer;
async launchShow() {
if (this.status !== ShowStatus.Idle || !this.isCharacterFull()) {
return 'Characters missing or not idle.';
}
this.status = ShowStatus.Running;
if (this.autoChangeName) {
for (const character of this.playbook.characters) {
const bot = this.characterBotMap.get(character.id);
try {
await bot.$setGroupCard(this.groupId, bot.selfId, character.name);
} catch (e) {
return `Change name for ${bot.selfId} ${
character.name
} failed: ${e.toString()}`;
}
}
}
this.launchTime = moment();
this.showInterval = setInterval(async () => {
await this.showProcess();
}, 1000);
return null;
}
private async showProcess() {
try {
const exitShow = await this.playbook.play(this.step++);
if (exitShow) {
this.endShow('Show ended.');
}
return exitShow;
} catch (e) {
this.endShow(`Show errored: ${e.toString()}`);
return true;
}
}
endShow(message: string) {
if (this.status !== ShowStatus.Running) {
return false;
}
clearInterval(this.showInterval);
this.showInterval = undefined;
this.onFinish(message, this);
return true;
}
getCharacterList(): string {
return Array.from(this.characterBotMap.entries())
.map(
([characterId, bot]) =>
`${this.playbook.characters
.find((c) => c.id === characterId)
.getDisplayName()} => ${bot.selfId}`,
)
.join('\n');
}
getStatus(): string {
return `群: ${this.groupId}\n开始时间: ${this.launchTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n人物: ${this.getCharacterList()}`;
}
}
......@@ -3,7 +3,7 @@ import Mustache from 'mustache';
export function matchHeader(input: string) {
return input
.trim()
.match(/^(.+?) ((上午|下午) ?)?(\d{1,2}):(\d{1,2}):(\d{1,2})( (AM|PM))?$/);
.match(/^(.+?) +((上午|下午) ?)?(\d{1,2}):(\d{1,2}):(\d{1,2})( (AM|PM))?$/);
}
export function render(template: string, view: any) {
......
import 'source-map-support/register';
import type { Context, Session } from 'koishi-core';
import { Show } from './playbook/Show';
import { Playbook } from './playbook/Playbook';
import { plainToClass } from 'class-transformer';
import loadJsonFile from 'load-json-file';
import type { CQBot } from 'koishi-adapter-onebot';
export interface Config {
adminContext: (ctx: Context) => Context;
autoChangeName: boolean;
dropHelp: boolean;
playbookPathPrefix: string;
}
export class MyPlugin {
config: Config;
ctx: Context;
adminCtx: Context;
shows = new Map<number, Show>();
name = 'act';
apply(ctx: Context, config: Config) {
this.ctx = ctx;
this.config = {
adminContext: (ctx) => ctx.private(),
autoChangeName: false,
dropHelp: false,
playbookPathPrefix: './playbooks',
...config,
};
if (this.config.dropHelp) {
ctx.command('help').dispose();
}
this.adminCtx = this.config.adminContext(ctx);
const showComamnd = this.adminCtx
.command('act [groupId:number]', '获取公演状态')
.usage('不带参数获取所有正在公演的群,带参数则获取特定群。')
.action((argv, groupId) => {
if (!groupId) {
return `当前正在公演的群有:\n${Array.from(this.shows.keys()).join(
' ',
)}`;
}
if (!this.shows.has(groupId)) {
return `群 ${groupId} 并不在公演哦。`;
}
const show = this.shows.get(groupId);
return show.getStatus();
});
showComamnd
.subcommand(
'.create <groupId:number> <playbookFilename:string> [...specificCharacters]',
'创建公演',
)
.usage(
'groupId 目标群。playbookFilename 是公演剧本文件,需要在服务器放好。后面的参数可以以 人物名=帐号 的形式指定特定的人物,否则随机分配。',
)
.example(
'act.create 11111111 play1 幽幽子=2222222 在群 11111111 创建一个公演,剧本是 play1,同时指定人物幽幽子为 2222222 扮演。',
)
.action(
async (argv, groupId, playbookFilename, ...specificCharacters) => {
if (!groupId || !playbookFilename) {
return '缺少参数。';
}
return this.createShow(
argv.session,
groupId,
playbookFilename,
specificCharacters,
);
},
);
showComamnd
.subcommand('.delete <groupId:number>', '停止')
.usage('groupId 目标群。')
.example('act.delete 11111111 停止群 11111111 的公演。')
.action((argv, groupId) => {
if (!groupId) {
return '缺少参数。';
}
if (!this.shows.has(groupId)) {
return `群 ${groupId} 并不在公演哦。`;
}
const show = this.shows.get(groupId);
show.endShow(`Ended by user ${argv.session.userId}`);
return '停止成功。';
});
}
async createShow(
session: Session,
groupId: number,
playbookFilename: string,
specificCharacters: string[] = [],
) {
if (this.shows.has(groupId)) {
return `群 ${groupId} 正在演呢!`;
}
const playbookPath = `${this.config.playbookPathPrefix}/${playbookFilename}.json`;
let playbook: Playbook;
try {
playbook = plainToClass(
Playbook,
(await loadJsonFile(playbookPath)) as any,
);
} catch (e) {
return `无法加载剧本文件 ${playbookPath}: ${e.toString()}`;
}
const bots: CQBot[] = (this.ctx.bots.filter(
(b) => b.type && b.type.startsWith('onebot'),
) as unknown) as CQBot[];
const show = new Show(groupId, playbook, this.config.autoChangeName);
for (const specificCharacter of specificCharacters) {
const [characterName, botId] = specificCharacter.split('=');
if (!botId) {
return `非法指定人物格式: ${specificCharacter}`;
}
const bot = bots.find((b) => b.selfId === botId);
if (!bot) {
return `没有找到机器人 ${botId}`;
}
const character = playbook.characters.find(
(c) => c.id === parseInt(characterName) || c.name === characterName,
);
if (!character) {
return `没有找到人物 ${characterName}`;
}
show.useCharacter(character.id, bot);
}
if (!(await show.autoCharacters(bots))) {
return '有人物缺位。';
}
show.onFinish = async (message, _show) => {
this.shows.delete(_show.groupId);
await session.send(`群 ${_show.groupId} 的公演结束了: ${message}`);
};
const launchErrorMessage = await show.launchShow();
if (launchErrorMessage) {
return `创建公演失败: ${launchErrorMessage}`;
}
this.shows.set(groupId, show);
return `成功创建群 ${groupId} 的公演 ${playbookFilename}\n出演名单:\n${show.getCharacterList()}`;
}
}
This diff is collapsed.
import * as fs from 'fs';
import { Playbook } from '../src/Playbook';
import { Playbook } from '../src/playbook/Playbook';
import { classToPlain, plainToClass } from 'class-transformer';
import delay from 'delay';
......@@ -8,7 +8,7 @@ async function main() {
'./test/test-playbook.txt',
'utf-8',
);
const playbook = new Playbook({ Andria: 'Nanahira' });
const playbook = new Playbook();
playbook.loadText(content);
playbook.tuneTime({
readSlope: 0,
......@@ -18,11 +18,10 @@ async function main() {
maxRandomDelay: 5,
});
const plain = classToPlain(playbook);
console.log(JSON.stringify(plain, null, 2));
playbook.characters.forEach(
(c) =>
(c.playFun = async (l, text) =>
console.error(`${c.displayName}: ${text}`)),
console.error(`${c.getDisplayName()}: ${text}`)),
);
for (let i = 0; ; i++) {
if (await playbook.play(i)) {
......
{
"compilerOptions": {
"outDir": "build",
"outDir": "dist",
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
......@@ -13,7 +13,6 @@
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts",
"test/**/*.ts"
"src/**/*.ts"
]
}
const path = require("path");
module.exports = {
entry: "./src/index.ts",
mode: "production",
target: "node",
devtool: "source-map",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{ test: /\.mustache$/, use: "raw-loader" },
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "index.js",
library: {
type: "commonjs",
},
path: path.resolve(__dirname, "dist"),
},
};
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