@mushinjs/bot-plugin-llm-chat 0.0.0-stage → 0.2.0-next.20261112165327
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy.d.ts +44 -0
- package/dist/legacy.js +169 -0
- package/dist/legacy.js.map +1 -0
- package/package.json +60 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Setobox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LEGACY_KEYS, LegacyConfig, chatFlow, legacySettings } from "./legacy.js";
|
|
2
|
+
import { Context, z } from "@mushinjs/core";
|
|
3
|
+
export declare const name = "llm-chat";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export declare const PACKAGE = "@mushinjs/bot-plugin-llm-chat";
|
|
6
|
+
/**
|
|
7
|
+
* No settings: LLM chat is the flow this plugin makes on each bot ("大模型对话"), edited on the
|
|
8
|
+
* canvas. Settings from older versions are accepted and turned into that flow.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Config: z.ZodObject<{}, z.core.$loose>;
|
|
11
|
+
export declare function apply(ctx: Context, config: Record<string, unknown>): Promise<void>;
|
|
12
|
+
export { LEGACY_KEYS, LegacyConfig, chatFlow, legacySettings };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LEGACY_KEYS, LegacyConfig, chatFlow, legacySettings } from "./legacy.js";
|
|
2
|
+
import { z } from "@mushinjs/core";
|
|
3
|
+
// src/index.ts
|
|
4
|
+
const name = "llm-chat";
|
|
5
|
+
const inject = [
|
|
6
|
+
"bot",
|
|
7
|
+
"flows",
|
|
8
|
+
"storage"
|
|
9
|
+
];
|
|
10
|
+
const PACKAGE = "@mushinjs/bot-plugin-llm-chat";
|
|
11
|
+
const STORAGE_KEY = "llm-chat/flows";
|
|
12
|
+
/**
|
|
13
|
+
* No settings: LLM chat is the flow this plugin makes on each bot ("大模型对话"), edited on the
|
|
14
|
+
* canvas. Settings from older versions are accepted and turned into that flow.
|
|
15
|
+
*/
|
|
16
|
+
const Config = z.looseObject({});
|
|
17
|
+
let queue = Promise.resolve();
|
|
18
|
+
function serial(task) {
|
|
19
|
+
const next = queue.catch(() => {}).then(task);
|
|
20
|
+
queue = next;
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
const exists = (ctx, id) => {
|
|
24
|
+
try {
|
|
25
|
+
ctx.flows.get(id);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** Makes the bot's chat flow, or rewrites it when legacy settings changed; drops flows of deleted bots. */
|
|
32
|
+
async function reconcile(ctx, raw) {
|
|
33
|
+
const logger = ctx.logger("llm-chat");
|
|
34
|
+
const botId = ctx.bot.id;
|
|
35
|
+
const state = await ctx.storage.get(STORAGE_KEY) ?? {};
|
|
36
|
+
const known = new Set(ctx.bots.list().map((bot) => bot.id));
|
|
37
|
+
for (const [id, entry] of Object.entries(state)) {
|
|
38
|
+
if (known.has(id)) continue;
|
|
39
|
+
if (exists(ctx, entry.flow)) await ctx.flows.remove(entry.flow);
|
|
40
|
+
delete state[id];
|
|
41
|
+
}
|
|
42
|
+
const legacy = legacySettings(raw);
|
|
43
|
+
const parsed = LegacyConfig.safeParse(legacy);
|
|
44
|
+
if (!parsed.success) logger.warn("ignoring old LLM chat settings that are not valid: %s", z.prettifyError(parsed.error));
|
|
45
|
+
const settings = parsed.success ? parsed.data : LegacyConfig.parse({});
|
|
46
|
+
const fingerprint = JSON.stringify(parsed.success ? legacy : {});
|
|
47
|
+
const entry = state[botId];
|
|
48
|
+
if (!entry || !exists(ctx, entry.flow)) {
|
|
49
|
+
const doc = await ctx.flows.create({
|
|
50
|
+
name: `大模型对话 · ${ctx.bot.record.name}`,
|
|
51
|
+
description: "被 @ 或私聊时用大模型回复。由“大模型对话”插件创建;在这个机器人上停用该插件会删除它。",
|
|
52
|
+
adapter: ctx.bot.platform,
|
|
53
|
+
bots: [botId],
|
|
54
|
+
source: {
|
|
55
|
+
kind: "plugin",
|
|
56
|
+
plugin: PACKAGE
|
|
57
|
+
},
|
|
58
|
+
...chatFlow(settings)
|
|
59
|
+
});
|
|
60
|
+
state[botId] = {
|
|
61
|
+
flow: doc.id,
|
|
62
|
+
settings: fingerprint
|
|
63
|
+
};
|
|
64
|
+
logger.info(Object.keys(legacy).length ? "turned the LLM chat settings into the flow \"%s\"" : "created the flow \"%s\"", doc.name);
|
|
65
|
+
} else if (Object.keys(legacy).length && fingerprint !== entry.settings) {
|
|
66
|
+
await ctx.flows.update(entry.flow, chatFlow(settings));
|
|
67
|
+
entry.settings = fingerprint;
|
|
68
|
+
logger.info("rewrote the flow \"%s\" from the LLM chat settings", ctx.flows.get(entry.flow).name);
|
|
69
|
+
}
|
|
70
|
+
await ctx.storage.set(STORAGE_KEY, state);
|
|
71
|
+
}
|
|
72
|
+
/** Deletes the bot's chat flow once the plugin is turned off on it (not when the bot only restarts). */
|
|
73
|
+
async function cleanup(ctx, botId) {
|
|
74
|
+
let enabled;
|
|
75
|
+
try {
|
|
76
|
+
enabled = ctx.bots.get(botId).plugins.find((item) => item.package === PACKAGE)?.enabled;
|
|
77
|
+
} catch {}
|
|
78
|
+
if (enabled !== false) return;
|
|
79
|
+
const state = await ctx.storage.get(STORAGE_KEY) ?? {};
|
|
80
|
+
const entry = state[botId];
|
|
81
|
+
if (!entry) return;
|
|
82
|
+
if (exists(ctx, entry.flow)) await ctx.flows.remove(entry.flow);
|
|
83
|
+
delete state[botId];
|
|
84
|
+
await ctx.storage.set(STORAGE_KEY, state);
|
|
85
|
+
ctx.logger("llm-chat").info("removed the LLM chat flow of bot %s", botId);
|
|
86
|
+
}
|
|
87
|
+
async function apply(ctx, config) {
|
|
88
|
+
const botId = ctx.bot.id;
|
|
89
|
+
const logger = ctx.logger("llm-chat");
|
|
90
|
+
ctx.effect(() => () => serial(() => cleanup(ctx, botId)).catch((error) => logger.warn("removing the LLM chat flow failed: %s", error)), "llm-chat flow");
|
|
91
|
+
await serial(() => reconcile(ctx, config ?? {}));
|
|
92
|
+
}
|
|
93
|
+
export { Config, LEGACY_KEYS, LegacyConfig, PACKAGE, apply, chatFlow, inject, legacySettings, name };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Context } from '@mushinjs/core'\r\nimport { z } from '@mushinjs/core'\r\nimport type {} from '@mushinjs/plugin-flows'\r\nimport type {} from '@mushinjs/plugin-storage'\r\nimport { chatFlow, LegacyConfig, legacySettings } from './legacy'\r\n\r\nexport * from './legacy'\r\n\r\nexport const name = 'llm-chat'\r\nexport const inject = ['bot', 'flows', 'storage']\r\n\r\nexport const PACKAGE = '@mushinjs/bot-plugin-llm-chat'\r\nconst STORAGE_KEY = 'llm-chat/flows'\r\n\r\n/**\r\n * No settings: LLM chat is the flow this plugin makes on each bot (\"大模型对话\"), edited on the\r\n * canvas. Settings from older versions are accepted and turned into that flow.\r\n */\r\nexport const Config = z.looseObject({})\r\n\r\n/** Bot id → the flow made for it, and the legacy settings it was last made from. */\r\ntype State = Record<string, { flow: string; settings: string }>\r\n\r\n// Bots start in parallel; changes to the shared state go one at a time.\r\nlet queue: Promise<unknown> = Promise.resolve()\r\nfunction serial<T>(task: () => Promise<T>): Promise<T> {\r\n const next = queue.catch(() => {}).then(task)\r\n queue = next\r\n return next\r\n}\r\n\r\nconst exists = (ctx: Context, id: string) => {\r\n try {\r\n ctx.flows.get(id)\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Makes the bot's chat flow, or rewrites it when legacy settings changed; drops flows of deleted bots. */\r\nasync function reconcile(ctx: Context, raw: Record<string, unknown>) {\r\n const logger = ctx.logger('llm-chat')\r\n const botId = ctx.bot.id\r\n const state = (await ctx.storage.get<State>(STORAGE_KEY)) ?? {}\r\n const known = new Set(ctx.bots.list().map((bot) => bot.id))\r\n for (const [id, entry] of Object.entries(state)) {\r\n if (known.has(id)) continue\r\n if (exists(ctx, entry.flow)) await ctx.flows.remove(entry.flow)\r\n delete state[id]\r\n }\r\n\r\n const legacy = legacySettings(raw)\r\n const parsed = LegacyConfig.safeParse(legacy)\r\n if (!parsed.success) logger.warn('ignoring old LLM chat settings that are not valid: %s', z.prettifyError(parsed.error))\r\n const settings = parsed.success ? parsed.data : LegacyConfig.parse({})\r\n const fingerprint = JSON.stringify(parsed.success ? legacy : {})\r\n const entry = state[botId]\r\n if (!entry || !exists(ctx, entry.flow)) {\r\n const doc = await ctx.flows.create({\r\n name: `大模型对话 · ${ctx.bot.record.name}`,\r\n description: '被 @ 或私聊时用大模型回复。由“大模型对话”插件创建;在这个机器人上停用该插件会删除它。',\r\n adapter: ctx.bot.platform,\r\n bots: [botId],\r\n source: { kind: 'plugin', plugin: PACKAGE },\r\n ...chatFlow(settings),\r\n })\r\n state[botId] = { flow: doc.id, settings: fingerprint }\r\n logger.info(Object.keys(legacy).length ? 'turned the LLM chat settings into the flow \"%s\"' : 'created the flow \"%s\"', doc.name)\r\n } else if (Object.keys(legacy).length && fingerprint !== entry.settings) {\r\n await ctx.flows.update(entry.flow, chatFlow(settings))\r\n entry.settings = fingerprint\r\n logger.info('rewrote the flow \"%s\" from the LLM chat settings', ctx.flows.get(entry.flow).name)\r\n }\r\n await ctx.storage.set(STORAGE_KEY, state)\r\n}\r\n\r\n/** Deletes the bot's chat flow once the plugin is turned off on it (not when the bot only restarts). */\r\nasync function cleanup(ctx: Context, botId: string) {\r\n let enabled: boolean | undefined\r\n try {\r\n enabled = ctx.bots.get(botId).plugins.find((item) => item.package === PACKAGE)?.enabled\r\n } catch {}\r\n if (enabled !== false) return\r\n const state = (await ctx.storage.get<State>(STORAGE_KEY)) ?? {}\r\n const entry = state[botId]\r\n if (!entry) return\r\n if (exists(ctx, entry.flow)) await ctx.flows.remove(entry.flow)\r\n delete state[botId]\r\n await ctx.storage.set(STORAGE_KEY, state)\r\n ctx.logger('llm-chat').info('removed the LLM chat flow of bot %s', botId)\r\n}\r\n\r\nexport async function apply(ctx: Context, config: Record<string, unknown>) {\r\n const botId = ctx.bot.id\r\n const logger = ctx.logger('llm-chat')\r\n // Runs while the plugin stops, before its context goes away.\r\n ctx.effect(() => () => serial(() => cleanup(ctx, botId)).catch((error) => logger.warn('removing the LLM chat flow failed: %s', error)), 'llm-chat flow')\r\n await serial(() => reconcile(ctx, config ?? {}))\r\n}\r\n"],"mappings":"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"}
|
package/dist/legacy.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "@mushinjs/core";
|
|
2
|
+
import { FlowEdge, FlowNode } from "@mushinjs/plugin-flows";
|
|
3
|
+
/**
|
|
4
|
+
* The settings this plugin had before LLM chat became a flow. Bots that still carry them (or API
|
|
5
|
+
* callers that send them) get an equivalent flow; the settings themselves are no longer used.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LegacyConfig: z.ZodObject<{
|
|
8
|
+
trigger: z.ZodDefault<z.ZodEnum<{
|
|
9
|
+
mention: "mention";
|
|
10
|
+
always: "always";
|
|
11
|
+
}>>;
|
|
12
|
+
systemPrompt: z.ZodDefault<z.ZodString>;
|
|
13
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
14
|
+
model: z.ZodOptional<z.ZodString>;
|
|
15
|
+
historySize: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
privateChat: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
groups: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
19
|
+
ignoreUsers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
cooldown: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
userLimit: z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
userWindow: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
limitNotice: z.ZodDefault<z.ZodString>;
|
|
24
|
+
dailyReplies: z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
dailyTokens: z.ZodDefault<z.ZodNumber>;
|
|
26
|
+
maxInputChars: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type LegacyConfig = z.infer<typeof LegacyConfig>;
|
|
29
|
+
export declare const LEGACY_KEYS: string[];
|
|
30
|
+
/** Only the legacy settings of a plugin config. */
|
|
31
|
+
export declare function legacySettings(config: Record<string, unknown> | undefined): {
|
|
32
|
+
[k: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
type NodeSpec = Omit<FlowNode, 'position'>;
|
|
35
|
+
type EdgeSpec = Omit<FlowEdge, 'id'>;
|
|
36
|
+
/**
|
|
37
|
+
* The flow equivalent to a set of settings: on message → cooldown → rate limit → daily quota →
|
|
38
|
+
* ask the LLM (with chat history) → reply. Steps whose limit is off are left out.
|
|
39
|
+
*/
|
|
40
|
+
export declare function chatFlow(settings: LegacyConfig): {
|
|
41
|
+
nodes: NodeSpec[];
|
|
42
|
+
edges: EdgeSpec[];
|
|
43
|
+
};
|
|
44
|
+
export {};
|
package/dist/legacy.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from "@mushinjs/core";
|
|
2
|
+
// src/legacy.ts
|
|
3
|
+
/**
|
|
4
|
+
* The settings this plugin had before LLM chat became a flow. Bots that still carry them (or API
|
|
5
|
+
* callers that send them) get an equivalent flow; the settings themselves are no longer used.
|
|
6
|
+
*/
|
|
7
|
+
const LegacyConfig = z.object({
|
|
8
|
+
trigger: z.enum(["mention", "always"]).default("mention"),
|
|
9
|
+
systemPrompt: z.string().default(""),
|
|
10
|
+
provider: z.string().optional(),
|
|
11
|
+
model: z.string().optional(),
|
|
12
|
+
historySize: z.number().int().min(0).max(50).default(10),
|
|
13
|
+
maxTokens: z.number().int().min(64).max(64e3).default(2e3),
|
|
14
|
+
privateChat: z.boolean().default(true),
|
|
15
|
+
groups: z.array(z.string()).default([]),
|
|
16
|
+
ignoreUsers: z.array(z.string()).default([]),
|
|
17
|
+
cooldown: z.number().min(0).default(3),
|
|
18
|
+
userLimit: z.number().int().min(0).default(5),
|
|
19
|
+
userWindow: z.number().min(1).default(60),
|
|
20
|
+
limitNotice: z.string().default(""),
|
|
21
|
+
dailyReplies: z.number().int().min(0).default(300),
|
|
22
|
+
dailyTokens: z.number().int().min(0).default(0),
|
|
23
|
+
maxInputChars: z.number().int().min(50).default(1e3)
|
|
24
|
+
});
|
|
25
|
+
const LEGACY_KEYS = Object.keys(LegacyConfig.shape);
|
|
26
|
+
/** The system prompt the plugin used to fill in; the flow leaves it out so a persona can speak. */
|
|
27
|
+
const OLD_DEFAULT_PROMPT = "You are a friendly chat bot in a QQ chat. Keep replies short and natural, and answer in the language the user writes in.";
|
|
28
|
+
/** Only the legacy settings of a plugin config. */
|
|
29
|
+
function legacySettings(config) {
|
|
30
|
+
return Object.fromEntries(Object.entries(config ?? {}).filter(([key]) => LEGACY_KEYS.includes(key)));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The flow equivalent to a set of settings: on message → cooldown → rate limit → daily quota →
|
|
34
|
+
* ask the LLM (with chat history) → reply. Steps whose limit is off are left out.
|
|
35
|
+
*/
|
|
36
|
+
function chatFlow(settings) {
|
|
37
|
+
const nodes = [];
|
|
38
|
+
const edges = [];
|
|
39
|
+
let from;
|
|
40
|
+
const step = (node, exit = "next") => {
|
|
41
|
+
nodes.push(node);
|
|
42
|
+
if (from) edges.push({
|
|
43
|
+
source: from.node,
|
|
44
|
+
sourceHandle: `exec:${from.exit}`,
|
|
45
|
+
target: node.id,
|
|
46
|
+
targetHandle: "exec"
|
|
47
|
+
});
|
|
48
|
+
from = {
|
|
49
|
+
node: node.id,
|
|
50
|
+
exit
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
step({
|
|
54
|
+
id: "message",
|
|
55
|
+
type: "trigger.message",
|
|
56
|
+
label: "收到消息",
|
|
57
|
+
priority: -100,
|
|
58
|
+
config: {
|
|
59
|
+
when: settings.trigger === "always" ? settings.privateChat ? "any" : "group" : settings.privateChat ? "mention" : "groupMention",
|
|
60
|
+
groups: settings.groups,
|
|
61
|
+
ignoreUsers: settings.ignoreUsers,
|
|
62
|
+
skipCommands: true
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (settings.cooldown > 0) step({
|
|
66
|
+
id: "cooldown",
|
|
67
|
+
type: "logic.cooldown",
|
|
68
|
+
label: "会话冷却",
|
|
69
|
+
config: {
|
|
70
|
+
seconds: settings.cooldown,
|
|
71
|
+
scope: "chat"
|
|
72
|
+
}
|
|
73
|
+
}, "ready");
|
|
74
|
+
if (settings.userLimit > 0) {
|
|
75
|
+
step({
|
|
76
|
+
id: "rate",
|
|
77
|
+
type: "logic.rateLimit",
|
|
78
|
+
label: "每人限额",
|
|
79
|
+
config: {
|
|
80
|
+
limit: settings.userLimit,
|
|
81
|
+
seconds: settings.userWindow,
|
|
82
|
+
per: "user"
|
|
83
|
+
}
|
|
84
|
+
}, "ok");
|
|
85
|
+
if (settings.limitNotice) {
|
|
86
|
+
nodes.push({
|
|
87
|
+
id: "firstLimited",
|
|
88
|
+
type: "logic.if",
|
|
89
|
+
label: "第一次超限",
|
|
90
|
+
config: {},
|
|
91
|
+
inputs: { condition: "{{rate.first}}" }
|
|
92
|
+
}, {
|
|
93
|
+
id: "notice",
|
|
94
|
+
type: "action.reply",
|
|
95
|
+
label: "限流提示",
|
|
96
|
+
config: {
|
|
97
|
+
text: settings.limitNotice,
|
|
98
|
+
quote: false
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
edges.push({
|
|
102
|
+
source: "rate",
|
|
103
|
+
sourceHandle: "exec:limited",
|
|
104
|
+
target: "firstLimited",
|
|
105
|
+
targetHandle: "exec"
|
|
106
|
+
}, {
|
|
107
|
+
source: "firstLimited",
|
|
108
|
+
sourceHandle: "exec:true",
|
|
109
|
+
target: "notice",
|
|
110
|
+
targetHandle: "exec"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (settings.dailyReplies > 0 || settings.dailyTokens > 0) step({
|
|
115
|
+
id: "quota",
|
|
116
|
+
type: "logic.dailyQuota",
|
|
117
|
+
label: "每日额度",
|
|
118
|
+
config: {
|
|
119
|
+
times: settings.dailyReplies,
|
|
120
|
+
tokens: settings.dailyTokens,
|
|
121
|
+
per: "flow"
|
|
122
|
+
}
|
|
123
|
+
}, "ok");
|
|
124
|
+
step({
|
|
125
|
+
id: "ask",
|
|
126
|
+
type: "ai.chat",
|
|
127
|
+
label: "询问大模型",
|
|
128
|
+
config: {
|
|
129
|
+
prompt: [
|
|
130
|
+
{ kind: "persona" },
|
|
131
|
+
...settings.systemPrompt && settings.systemPrompt !== OLD_DEFAULT_PROMPT ? [{
|
|
132
|
+
kind: "system",
|
|
133
|
+
text: settings.systemPrompt
|
|
134
|
+
}] : [],
|
|
135
|
+
...settings.historySize ? [{
|
|
136
|
+
kind: "history",
|
|
137
|
+
rounds: settings.historySize,
|
|
138
|
+
source: "chat"
|
|
139
|
+
}] : [],
|
|
140
|
+
{
|
|
141
|
+
kind: "message",
|
|
142
|
+
text: ""
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
maxTokens: settings.maxTokens,
|
|
146
|
+
maxInputChars: settings.maxInputChars,
|
|
147
|
+
timeout: 45,
|
|
148
|
+
...settings.provider ? { provider: settings.provider } : {},
|
|
149
|
+
...settings.model ? { model: settings.model } : {}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
step({
|
|
153
|
+
id: "reply",
|
|
154
|
+
type: "action.reply",
|
|
155
|
+
label: "回复",
|
|
156
|
+
config: {
|
|
157
|
+
text: "",
|
|
158
|
+
quote: false
|
|
159
|
+
},
|
|
160
|
+
inputs: { text: "{{ask.reply}}" }
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
nodes,
|
|
164
|
+
edges
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export { LEGACY_KEYS, LegacyConfig, chatFlow, legacySettings };
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=legacy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy.js","names":[],"sources":["../src/legacy.ts"],"sourcesContent":["import { z } from '@mushinjs/core'\r\nimport type { FlowEdge, FlowNode } from '@mushinjs/plugin-flows'\r\n\r\n/**\r\n * The settings this plugin had before LLM chat became a flow. Bots that still carry them (or API\r\n * callers that send them) get an equivalent flow; the settings themselves are no longer used.\r\n */\r\nexport const LegacyConfig = z.object({\r\n trigger: z.enum(['mention', 'always']).default('mention'),\r\n systemPrompt: z.string().default(''),\r\n provider: z.string().optional(),\r\n model: z.string().optional(),\r\n historySize: z.number().int().min(0).max(50).default(10),\r\n maxTokens: z.number().int().min(64).max(64000).default(2000),\r\n privateChat: z.boolean().default(true),\r\n groups: z.array(z.string()).default([]),\r\n ignoreUsers: z.array(z.string()).default([]),\r\n cooldown: z.number().min(0).default(3),\r\n userLimit: z.number().int().min(0).default(5),\r\n userWindow: z.number().min(1).default(60),\r\n limitNotice: z.string().default(''),\r\n dailyReplies: z.number().int().min(0).default(300),\r\n dailyTokens: z.number().int().min(0).default(0),\r\n maxInputChars: z.number().int().min(50).default(1000),\r\n})\r\n\r\nexport type LegacyConfig = z.infer<typeof LegacyConfig>\r\n\r\nexport const LEGACY_KEYS = Object.keys(LegacyConfig.shape)\r\n\r\n/** The system prompt the plugin used to fill in; the flow leaves it out so a persona can speak. */\r\nconst OLD_DEFAULT_PROMPT = 'You are a friendly chat bot in a QQ chat. Keep replies short and natural, and answer in the language the user writes in.'\r\n\r\n/** Only the legacy settings of a plugin config. */\r\nexport function legacySettings(config: Record<string, unknown> | undefined) {\r\n return Object.fromEntries(Object.entries(config ?? {}).filter(([key]) => LEGACY_KEYS.includes(key)))\r\n}\r\n\r\ntype NodeSpec = Omit<FlowNode, 'position'>\r\ntype EdgeSpec = Omit<FlowEdge, 'id'>\r\n\r\n/**\r\n * The flow equivalent to a set of settings: on message → cooldown → rate limit → daily quota →\r\n * ask the LLM (with chat history) → reply. Steps whose limit is off are left out.\r\n */\r\nexport function chatFlow(settings: LegacyConfig): { nodes: NodeSpec[]; edges: EdgeSpec[] } {\r\n const nodes: NodeSpec[] = []\r\n const edges: EdgeSpec[] = []\r\n let from: { node: string; exit: string } | undefined\r\n const step = (node: NodeSpec, exit = 'next') => {\r\n nodes.push(node)\r\n if (from) edges.push({ source: from.node, sourceHandle: `exec:${from.exit}`, target: node.id, targetHandle: 'exec' })\r\n from = { node: node.id, exit }\r\n }\r\n\r\n const when = settings.trigger === 'always'\r\n ? (settings.privateChat ? 'any' : 'group')\r\n : (settings.privateChat ? 'mention' : 'groupMention')\r\n // Below other flows, so commands and keyword flows answer first.\r\n step({ id: 'message', type: 'trigger.message', label: '收到消息', priority: -100, config: { when, groups: settings.groups, ignoreUsers: settings.ignoreUsers, skipCommands: true } })\r\n if (settings.cooldown > 0) step({ id: 'cooldown', type: 'logic.cooldown', label: '会话冷却', config: { seconds: settings.cooldown, scope: 'chat' } }, 'ready')\r\n if (settings.userLimit > 0) {\r\n step({ id: 'rate', type: 'logic.rateLimit', label: '每人限额', config: { limit: settings.userLimit, seconds: settings.userWindow, per: 'user' } }, 'ok')\r\n if (settings.limitNotice) {\r\n nodes.push(\r\n { id: 'firstLimited', type: 'logic.if', label: '第一次超限', config: {}, inputs: { condition: '{{rate.first}}' } },\r\n { id: 'notice', type: 'action.reply', label: '限流提示', config: { text: settings.limitNotice, quote: false } },\r\n )\r\n edges.push(\r\n { source: 'rate', sourceHandle: 'exec:limited', target: 'firstLimited', targetHandle: 'exec' },\r\n { source: 'firstLimited', sourceHandle: 'exec:true', target: 'notice', targetHandle: 'exec' },\r\n )\r\n }\r\n }\r\n if (settings.dailyReplies > 0 || settings.dailyTokens > 0) {\r\n step({ id: 'quota', type: 'logic.dailyQuota', label: '每日额度', config: { times: settings.dailyReplies, tokens: settings.dailyTokens, per: 'flow' } }, 'ok')\r\n }\r\n step({\r\n id: 'ask',\r\n type: 'ai.chat',\r\n label: '询问大模型',\r\n config: {\r\n prompt: [\r\n { kind: 'persona' },\r\n ...(settings.systemPrompt && settings.systemPrompt !== OLD_DEFAULT_PROMPT ? [{ kind: 'system', text: settings.systemPrompt }] : []),\r\n ...(settings.historySize ? [{ kind: 'history', rounds: settings.historySize, source: 'chat' }] : []),\r\n { kind: 'message', text: '' },\r\n ],\r\n maxTokens: settings.maxTokens,\r\n maxInputChars: settings.maxInputChars,\r\n timeout: 45,\r\n ...(settings.provider ? { provider: settings.provider } : {}),\r\n ...(settings.model ? { model: settings.model } : {}),\r\n },\r\n })\r\n step({ id: 'reply', type: 'action.reply', label: '回复', config: { text: '', quote: false }, inputs: { text: '{{ask.reply}}' } })\r\n return { nodes, edges }\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushinjs/bot-plugin-llm-chat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-next.20261112165327",
|
|
4
|
+
"description": "LLM chat bot plugin: makes an editable chat flow on each bot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mushinjs/mushin.git",
|
|
10
|
+
"directory": "packages/bot-plugin-llm-chat"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mushin-plugin"
|
|
27
|
+
],
|
|
28
|
+
"mushin": {
|
|
29
|
+
"kind": "bot",
|
|
30
|
+
"platforms": [
|
|
31
|
+
"onebot"
|
|
32
|
+
],
|
|
33
|
+
"category": "ai",
|
|
34
|
+
"displayName": {
|
|
35
|
+
"en": "LLM chat",
|
|
36
|
+
"zh-CN": "大模型对话"
|
|
37
|
+
},
|
|
38
|
+
"description": {
|
|
39
|
+
"en": "Makes a flow that replies with an LLM when mentioned or in private chat; edit it on the canvas",
|
|
40
|
+
"zh-CN": "创建一个被 @ 或私聊时用大模型回复的流程,可以在画布上调整"
|
|
41
|
+
},
|
|
42
|
+
"requires": {
|
|
43
|
+
"plugins": {
|
|
44
|
+
"@mushinjs/plugin-bots": "*",
|
|
45
|
+
"@mushinjs/plugin-flows": "*",
|
|
46
|
+
"@mushinjs/plugin-providers": "*",
|
|
47
|
+
"@mushinjs/plugin-storage": "*"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"optional": {
|
|
51
|
+
"plugins": {
|
|
52
|
+
"@mushinjs/plugin-personas": "*"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@mushinjs/core": "0.2.0-next.20261112165327",
|
|
58
|
+
"@mushinjs/plugin-bots": "0.2.0-next.20261112165327",
|
|
59
|
+
"@mushinjs/plugin-providers": "0.2.0-next.20261112165327",
|
|
60
|
+
"@mushinjs/plugin-storage": "0.2.0-next.20261112165327",
|
|
61
|
+
"@mushinjs/plugin-flows": "0.2.0-next.20261112165327"
|
|
62
|
+
}
|
|
6
63
|
}
|
package/README.md
DELETED