-
Notifications
You must be signed in to change notification settings - Fork 9
feat: initialize qq plugin #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4b70233
fc44259
e2ccbeb
ca498a3
2b7d595
0435ce8
05d67e9
f208e37
0d5879b
1ff6c50
3f32d55
1d4509c
08fbca7
67ecce8
6d9b6a5
7cc7068
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| services: | ||
| napcat: | ||
| image: mlikiowa/napcat-docker:latest | ||
| container_name: napcat | ||
| restart: always | ||
| # mac_address: 02:42:ac:11:00:02 # 添加MAC地址固化配置 | ||
| environment: | ||
| - NAPCAT_UID=${NAPCAT_UID} | ||
| - NAPCAT_GID=${NAPCAT_GID} | ||
|
|
||
| ports: | ||
| - 3001:3001 | ||
| - 6096:6096 | ||
| - 6097:6097 | ||
| - 6099:6099 | ||
|
|
||
| volumes: | ||
| - ../.data/napcat/config:/app/napcat/config | ||
| - ../.data/ntqq:/app/.config/QQ | ||
| networks: | ||
| - ema-network | ||
|
|
||
| networks: | ||
| ema-network: | ||
| driver: bridge |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # 插件 | ||
|
|
||
| 插件是 Ema 的扩展机制,可以通过插件来扩展 Ema 的功能。目前支持的插件有: | ||
|
|
||
|
|
||
| ## 插件配置 | ||
|
|
||
| 目前只有一个环境变量 `EMA_PLUGINS`,用于配置插件列表,多个插件用逗号分隔。例如: | ||
|
|
||
| ```bash | ||
| EMA_PLUGINS=qq | ||
| ``` | ||
|
|
||
| ## 添加插件 | ||
|
|
||
| 插件的命名必须以`ema-plugin-`开头,例如 `ema-plugin-discord`。插件的开发可以通过以下步骤进行: | ||
|
|
||
| 1. 创建一个插件包,例如 `ema-plugin-discord`。 | ||
| 2. 在 [`ema-ui/package.json`](/packages/ema-ui/package.json) 的 `peerDependencies` 中添加一行: | ||
|
|
||
| ```jsonc | ||
| { | ||
| "peerDependencies": { | ||
| // PNPM 工作空间依赖 | ||
| "ema-plugin-discord": "workspace:*", | ||
| // 或外部包依赖 | ||
| "ema-plugin-discord": "^1.0.0", | ||
| }, | ||
| } | ||
| ``` | ||
Myriad-Dreamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 3. 重启服务器。 | ||
|
|
||
| ## 插件开发 | ||
|
|
||
| 插件的根文件需要导出 `Plugin` 符号: | ||
|
|
||
| ```ts | ||
| import type { EmaPluginProvider, Server } from "ema"; | ||
| export const Plugin: EmaPluginProvider = class { | ||
| static name = "QQ"; | ||
| constructor(private readonly server: Server) {} | ||
| start(): Promise<void> { | ||
| console.log("[ema-qq] started", !!this.server.chat); | ||
| return Promise.resolve(); | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| 根据编译错误的指引实现 `ema-plugin-discord` 包。 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "name": "ema-plugin-qq", | ||
| "version": "0.1.0", | ||
| "type": "module", | ||
| "keywords": [ | ||
| "qq" | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/EmaFanClub/EverMemoryArchive.git", | ||
| "directory": "packages/ema-plugin-qq" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/EmaFanClub/EverMemoryArchive/issues" | ||
| }, | ||
| "dependencies": { | ||
| "ema": "workspace:*", | ||
| "node-napcat-ts": "^0.4.20" | ||
| }, | ||
| "exports": { | ||
| ".": { | ||
| "types": "./src/index.ts", | ||
| "default": "./src/index.ts" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import type { AgentEvent, EmaPluginProvider, Server } from "ema"; | ||
| import { NCWebsocket, type GroupMessage } from "node-napcat-ts"; | ||
| import { execSync } from "child_process"; | ||
|
|
||
| const gitRev = getGitRev(); | ||
| const gitRemote = getGitRemote(); | ||
|
|
||
| const napcat = new NCWebsocket( | ||
| { | ||
| protocol: "ws", | ||
| host: "172.19.0.2", | ||
| port: 6097, | ||
| accessToken: process.env.NAPCAT_ACCESS_TOKEN, | ||
| // 是否需要在触发 socket.error 时抛出错误, 默认关闭 | ||
| throwPromise: true, | ||
| // ↓ 自动重连(可选) | ||
| reconnection: { | ||
| enable: true, | ||
| attempts: 10, | ||
| delay: 5000, | ||
| }, | ||
| // ↓ 是否开启 DEBUG 模式 | ||
| }, | ||
| true, | ||
| ); | ||
|
|
||
| export const Plugin: EmaPluginProvider = class { | ||
| static name = "QQ"; | ||
| constructor(private readonly server: Server) { } | ||
| async start(): Promise<void> { | ||
| await napcat.connect(); | ||
| await this.startLLM(); | ||
| } | ||
| async startLLM(): Promise<void> { | ||
| const replyPat = process.env.NAPCAT_REPLY_PATTERN; | ||
| if (!replyPat) { | ||
| throw new Error("NAPCAT_REPLY_PATTERN is not set"); | ||
| } | ||
|
|
||
| const actor = await this.server.getActor(1, 1, 1); | ||
|
|
||
| interface GroupMessageTask { | ||
| message: GroupMessage; | ||
| } | ||
|
|
||
| let taskId = 0; | ||
| const tasks: Record<number, GroupMessageTask> = {}; | ||
| const messageCache = new Map<number, string>(); | ||
|
|
||
| actor.events.on('agent', (response) => { | ||
| console.log("[ema-qq] actor response", response); | ||
|
|
||
| if (response.kind === "runFinished") { | ||
| const runFinishedEvent = response.content as AgentEvent<"runFinished">; | ||
| console.log("[ema-qq] actor run finished", runFinishedEvent); | ||
| if (runFinishedEvent.ok) { | ||
| const lastMetadata = (runFinishedEvent.metadata instanceof Array) ? ((runFinishedEvent.metadata as any[]).at(-1)) : runFinishedEvent.metadata; | ||
|
|
||
| const task = tasks[lastMetadata.taskId]; | ||
| if (!task) { | ||
| console.error( | ||
| "[ema-qq] task not found", | ||
| lastMetadata.taskId, | ||
| ); | ||
| return; | ||
| } | ||
| const message = task.message; | ||
| const text = runFinishedEvent.msg.trim() | ||
| .replaceAll( | ||
| gitRev, | ||
| `[${gitRev}]( ${gitRemote}/commit/${gitRev} )`, | ||
| ); | ||
| message.quick_action( | ||
| [ | ||
| { | ||
| type: "text", | ||
| data: { | ||
| text: ` ${text}`, | ||
| }, | ||
| }, | ||
| ], | ||
| true, | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| napcat.on("message.group", async (message) => { | ||
| // { type: 'reply', data: [Object] }, | ||
| console.log("[ema-qq] group message"); | ||
| console.log("[ema-qq] group message", message); | ||
|
|
||
| if (!message.raw_message.includes(replyPat)) { | ||
| console.log("message ignored"); | ||
| return; | ||
| } | ||
| let replyContext = ""; | ||
| const reply = message.message.find((m) => m.type === "reply"); | ||
| if (reply) { | ||
| const replyId = Number.parseInt(reply?.data.id); | ||
| if (replyId && !Number.isNaN(replyId)) { | ||
| const cached = messageCache.get(replyId); | ||
| if (cached) { | ||
| replyContext = cached; | ||
| } else { | ||
| const msg = await napcat.get_msg({ message_id: replyId }); | ||
| if (msg) { | ||
| replyContext = msg.raw_message; | ||
| messageCache.set(replyId, replyContext); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| messageCache.set(message.message_id, message.raw_message); | ||
|
|
||
| const id = taskId++; | ||
| tasks[id] = { message }; | ||
|
|
||
| let contentList = []; | ||
| if (replyContext) { | ||
| contentList.push(`注意:这则消息是在回复:<Reply>`); | ||
| contentList.push(replyContext); | ||
| contentList.push(`</Reply>`); | ||
| } | ||
| contentList.push(message.raw_message); | ||
| // current time | ||
| contentList.push(`当前时间:<Time>${new Date().toLocaleString()}</Time>`); | ||
| contentList.push(`GitRev(分支):<Rev>${gitRev}</Rev>`); | ||
|
|
||
| const text = contentList.join("\n"); | ||
|
|
||
| actor.work({ | ||
| metadata: { taskId: id }, | ||
| inputs: [{ type: "text", text }], | ||
| }); | ||
| }); | ||
|
|
||
| return Promise.resolve(); | ||
| } | ||
Myriad-Dreamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| function getGitRev(): string { | ||
| // git commit hash | ||
| return execSync("git rev-parse HEAD").toString().trim(); | ||
| } | ||
|
|
||
| function getGitRemote(): string { | ||
| return execSync("git remote get-url origin").toString().trim(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ESNext", | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "strict": true, | ||
| "verbatimModuleSyntax": true, | ||
| "experimentalDecorators": true, | ||
| "skipLibCheck": true, | ||
| "types": ["vitest/globals", "node"], | ||
| "outDir": "./dist" | ||
| }, | ||
| "include": ["src/**/*"], | ||
| "exclude": ["node_modules"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { getServer } from "@/app/api/shared-server"; | ||
| import { loadPlugins } from "@/plugin"; | ||
|
|
||
| getServer() | ||
| .then(loadPlugins) | ||
| .catch((error) => { | ||
| console.error("Failed to load plugins:", error); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export async function register() { | ||
| if (process.env.NEXT_RUNTIME === "nodejs") { | ||
| await import("./instrumentation-node"); | ||
| } | ||
|
|
||
| if (process.env.NEXT_RUNTIME === "edge") { | ||
| console.warn("Edge runtime is not supported yet"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { EmaPluginModule, Server } from "ema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as fs from "fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Loads plugins by environment variable `EMA_PLUGINS` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param server - The server instance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function loadPlugins(server: Server): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Myriad-Dreamin marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+4
to
+8
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Comma-separated list of plugins to load | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * EMA_PLUGINS=qq,discord | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const enabledPlugins = new Set( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (process.env.EMA_PLUGINS ?? "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(",") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((name) => name.trim()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((name) => name.length > 0), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getPluginModules() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((name) => enabledPlugins.has(name)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(async (name: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m: EmaPluginModule = await import(`ema-plugin-${name}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (m.Plugin.name in server.plugins) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Plugin ${m.Plugin.name} already loaded`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plugin = new m.Plugin(server); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.plugins[m.Plugin.name] = plugin; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Failed to load plugin package "${name}":`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Myriad-Dreamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+38
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plugins = Object.entries(server.plugins); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plugins.map(async ([name, plugin]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!plugin) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await plugin.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Failed to start plugin "${name}":`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+54
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |
| * Loads plugins by environment variable `EMA_PLUGINS` | |
| * @param server - The server instance | |
| */ | |
| export async function loadPlugins(server: Server): Promise<void> { | |
| /** | |
| * Comma-separated list of plugins to load | |
| * | |
| * @example | |
| * EMA_PLUGINS=qq,discord | |
| */ | |
| const enabledPlugins = new Set( | |
| (process.env.EMA_PLUGINS ?? "") | |
| .split(",") | |
| .map((name) => name.trim()) | |
| .filter((name) => name.length > 0), | |
| ); | |
| await Promise.all( | |
| getPluginModules() | |
| .filter((name) => enabledPlugins.has(name)) | |
| .map(async (name: string) => { | |
| try { | |
| const m: EmaPluginModule = await import(`ema-plugin-${name}`); | |
| if (m.Plugin.name in server.plugins) { | |
| throw new Error(`Plugin ${m.Plugin.name} already loaded`); | |
| } | |
| const plugin = new m.Plugin(server); | |
| server.plugins[m.Plugin.name] = plugin; | |
| } catch (error) { | |
| console.error(`Failed to load plugin package "${name}":`, error); | |
| return; | |
| } | |
| }), | |
| ); | |
| const plugins = Object.entries(server.plugins); | |
| await Promise.all( | |
| plugins.map(async ([name, plugin]) => { | |
| if (!plugin) { | |
| return; | |
| } | |
| try { | |
| return await plugin.start(); | |
| } catch (error) { | |
| console.error(`Failed to start plugin "${name}":`, error); | |
| } | |
| }), | |
| ); | |
| const initializingServers = new WeakMap<Server, Promise<void>>(); | |
| const initializedServers = new WeakSet<Server>(); | |
| /** | |
| * Loads plugins by environment variable `EMA_PLUGINS` | |
| * @param server - The server instance | |
| */ | |
| export async function loadPlugins(server: Server): Promise<void> { | |
| if (initializedServers.has(server)) { | |
| return; | |
| } | |
| const existingInitialization = initializingServers.get(server); | |
| if (existingInitialization) { | |
| await existingInitialization; | |
| return; | |
| } | |
| const initializationPromise = (async () => { | |
| /** | |
| * Comma-separated list of plugins to load | |
| * | |
| * @example | |
| * EMA_PLUGINS=qq,discord | |
| */ | |
| const enabledPlugins = new Set( | |
| (process.env.EMA_PLUGINS ?? "") | |
| .split(",") | |
| .map((name) => name.trim()) | |
| .filter((name) => name.length > 0), | |
| ); | |
| await Promise.all( | |
| getPluginModules() | |
| .filter((name) => enabledPlugins.has(name)) | |
| .map(async (name: string) => { | |
| try { | |
| const m: EmaPluginModule = await import(`ema-plugin-${name}`); | |
| if (m.Plugin.name in server.plugins) { | |
| throw new Error(`Plugin ${m.Plugin.name} already loaded`); | |
| } | |
| const plugin = new m.Plugin(server); | |
| server.plugins[m.Plugin.name] = plugin; | |
| } catch (error) { | |
| console.error(`Failed to load plugin package "${name}":`, error); | |
| return; | |
| } | |
| }), | |
| ); | |
| const plugins = Object.entries(server.plugins); | |
| await Promise.all( | |
| plugins.map(async ([name, plugin]) => { | |
| if (!plugin) { | |
| return; | |
| } | |
| try { | |
| return await plugin.start(); | |
| } catch (error) { | |
| console.error(`Failed to start plugin "${name}":`, error); | |
| } | |
| }), | |
| ); | |
| })(); | |
| initializingServers.set(server, initializationPromise); | |
| try { | |
| await initializationPromise; | |
| initializedServers.add(server); | |
| } finally { | |
| initializingServers.delete(server); | |
| } |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new plugin loading functionality in loadPlugins and plugin infrastructure lacks test coverage. According to the custom guideline in the project (see CONTRIBUTING.md and existing tests in packages/ema/src/server.spec.ts), new features should include test cases. Consider adding tests for: plugin discovery from package.json, plugin loading with EMA_PLUGINS env var, error handling for missing/invalid plugins, and plugin lifecycle (start method).
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect JSDoc description. The comment states "Finds all plugin modules in the ema-ui package.json", but the function actually reads from the ema-ui package directory relative to the current file using import.meta.url. The description should clarify that it reads from the ema-ui package's package.json file.
| * Finds all plugin modules in the `ema-ui` package.json | |
| * Finds all plugin modules declared in the `ema-ui` package's package.json file, | |
| * which is read from the ema-ui package directory relative to this module via `import.meta.url`. |
Uh oh!
There was an error while loading. Please reload this page.