mioku-plugin-chat 2.5.2 → 2.7.0
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/configs/groups.ts +5 -0
- package/configs/personalization.ts +1 -1
- package/context.ts +7 -3
- package/core/base.ts +1 -1
- package/core/chat-engine.ts +18 -7
- package/core/chat-turn.ts +28 -10
- package/core/media/image-analyzer.ts +63 -397
- package/core/prompt.ts +11 -20
- package/core/tools/web.ts +1 -1
- package/db.ts +8 -12
- package/handlers/idle-debug.ts +96 -97
- package/handlers/message.ts +38 -37
- package/handlers/poke.ts +104 -126
- package/humanize/emoji-agent.ts +210 -321
- package/humanize/emotion-agent.ts +33 -26
- package/humanize/expression.ts +24 -16
- package/humanize/index.ts +9 -8
- package/humanize/memory.ts +10 -7
- package/humanize/planner.ts +11 -16
- package/humanize/topic.ts +22 -15
- package/index.ts +120 -45
- package/manage/cooldown.ts +83 -116
- package/manage/idle-check.ts +173 -75
- package/manage/queue-processor.ts +92 -44
- package/manage/rate-limiter.ts +56 -80
- package/manage/session-turn-scheduler.ts +130 -0
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +15 -3
- package/types.ts +25 -19
- package/utils/config.ts +57 -0
- package/utils/group-config.ts +70 -0
- package/utils/message.ts +4 -7
- package/utils/queue.ts +5 -0
|
@@ -2,6 +2,8 @@ import type { AIInstance } from "mioku";
|
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import { extractJsonObject } from "../utils/json";
|
|
4
4
|
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
5
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
6
|
+
import type { ChatConfigProvider } from "./index";
|
|
5
7
|
|
|
6
8
|
export interface EmotionState {
|
|
7
9
|
current: string;
|
|
@@ -18,41 +20,43 @@ export interface EmotionAnalysisInput {
|
|
|
18
20
|
|
|
19
21
|
export class EmotionAgent {
|
|
20
22
|
private readonly ai: AIInstance;
|
|
21
|
-
private readonly
|
|
23
|
+
private readonly getConfig: ChatConfigProvider;
|
|
22
24
|
private readonly states = new Map<string, EmotionState>();
|
|
23
25
|
|
|
24
|
-
constructor(ai: AIInstance,
|
|
26
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider) {
|
|
25
27
|
this.ai = ai;
|
|
26
|
-
this.
|
|
28
|
+
this.getConfig = configProvider;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
getCurrent(sessionId: string): EmotionState {
|
|
30
32
|
const existing = this.states.get(sessionId);
|
|
31
33
|
if (existing) return existing;
|
|
32
34
|
|
|
33
|
-
const current = this.getDefaultEmotion();
|
|
35
|
+
const current = this.getDefaultEmotion(extractGroupIdFromSession(sessionId));
|
|
34
36
|
const state = { current, updatedAt: 0 };
|
|
35
37
|
this.states.set(sessionId, state);
|
|
36
38
|
return state;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
getAvailableEmotions(): string[] {
|
|
40
|
-
const
|
|
41
|
+
getAvailableEmotions(groupId?: number): string[] {
|
|
42
|
+
const cfg = this.getConfig(groupId);
|
|
43
|
+
const emotions = Object.keys(cfg.emotion?.emotions || {})
|
|
41
44
|
.map((name) => name.trim().toLowerCase())
|
|
42
45
|
.filter(Boolean);
|
|
43
46
|
return Array.from(new Set(["default", ...emotions]));
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
getReferenceExamples(emotion: string): string[] {
|
|
49
|
+
getReferenceExamples(emotion: string, groupId?: number): string[] {
|
|
50
|
+
const cfg = this.getConfig(groupId);
|
|
47
51
|
const normalized = this.normalizeEmotionName(emotion);
|
|
48
|
-
const emotions =
|
|
52
|
+
const emotions = cfg.emotion?.emotions || {};
|
|
49
53
|
const examples = this.normalizeExamples(emotions[normalized]?.examples);
|
|
50
54
|
if (examples.length > 0) return examples;
|
|
51
|
-
return this.normalizeExamples(emotions[this.getDefaultEmotion()]?.examples);
|
|
55
|
+
return this.normalizeExamples(emotions[this.getDefaultEmotion(groupId)]?.examples);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
setEmotion(sessionId: string, emotion: string): EmotionState {
|
|
55
|
-
const current = this.resolveEmotion(emotion);
|
|
58
|
+
setEmotion(sessionId: string, emotion: string, groupId?: number): EmotionState {
|
|
59
|
+
const current = this.resolveEmotion(emotion, groupId);
|
|
56
60
|
const state = { current, updatedAt: Date.now() };
|
|
57
61
|
this.states.set(sessionId, state);
|
|
58
62
|
return state;
|
|
@@ -69,28 +73,30 @@ export class EmotionAgent {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
async refreshIfNeeded(input: EmotionAnalysisInput): Promise<EmotionState> {
|
|
76
|
+
const groupId = extractGroupIdFromSession(input.sessionId);
|
|
77
|
+
const cfg = this.getConfig(groupId);
|
|
72
78
|
const current = this.getCurrent(input.sessionId);
|
|
73
|
-
const intervalMs = Number(
|
|
79
|
+
const intervalMs = Number(cfg.emotion?.updateIntervalMs ?? 60 * 60_000);
|
|
74
80
|
const shouldRefresh =
|
|
75
81
|
Boolean(input.force) || current.updatedAt <= 0 || Date.now() - current.updatedAt >= intervalMs;
|
|
76
82
|
|
|
77
83
|
if (!shouldRefresh) return current;
|
|
78
84
|
|
|
79
85
|
try {
|
|
80
|
-
const nextEmotion = await this.analyzeEmotion(input);
|
|
81
|
-
return this.setEmotion(input.sessionId, nextEmotion);
|
|
86
|
+
const nextEmotion = await this.analyzeEmotion(input, cfg);
|
|
87
|
+
return this.setEmotion(input.sessionId, nextEmotion, groupId);
|
|
82
88
|
} catch (err) {
|
|
83
89
|
logger.warn(`[emotion-agent] emotion analysis failed: ${err}`);
|
|
84
90
|
if (current.updatedAt <= 0) {
|
|
85
|
-
return this.setEmotion(input.sessionId, this.getDefaultEmotion());
|
|
91
|
+
return this.setEmotion(input.sessionId, this.getDefaultEmotion(groupId), groupId);
|
|
86
92
|
}
|
|
87
93
|
return current;
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
private async analyzeEmotion(input: EmotionAnalysisInput): Promise<string> {
|
|
92
|
-
const availableEmotions = this.getAvailableEmotions();
|
|
93
|
-
const model =
|
|
97
|
+
private async analyzeEmotion(input: EmotionAnalysisInput, cfg: ChatConfig = this.getConfig(extractGroupIdFromSession(input.sessionId))): Promise<string> {
|
|
98
|
+
const availableEmotions = this.getAvailableEmotions(extractGroupIdFromSession(input.sessionId));
|
|
99
|
+
const model = cfg.workingModel || cfg.model;
|
|
94
100
|
const systemPrompt = `You are an emotion state selector for a chat bot.
|
|
95
101
|
|
|
96
102
|
Task:
|
|
@@ -134,20 +140,21 @@ ${input.targetMessage.userName}: ${input.targetMessage.content}`;
|
|
|
134
140
|
|
|
135
141
|
const content = response.content || "";
|
|
136
142
|
const parsed = extractJsonObject(content);
|
|
137
|
-
if (parsed === undefined) return this.getDefaultEmotion();
|
|
138
|
-
return this.resolveEmotion(parsed?.emotion);
|
|
143
|
+
if (parsed === undefined) return this.getDefaultEmotion(extractGroupIdFromSession(input.sessionId));
|
|
144
|
+
return this.resolveEmotion(parsed?.emotion, extractGroupIdFromSession(input.sessionId));
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
private resolveEmotion(emotion: unknown): string {
|
|
147
|
+
private resolveEmotion(emotion: unknown, groupId?: number): string {
|
|
142
148
|
const normalized = this.normalizeEmotionName(emotion);
|
|
143
|
-
const available = this.getAvailableEmotions();
|
|
149
|
+
const available = this.getAvailableEmotions(groupId);
|
|
144
150
|
if (available.includes(normalized)) return normalized;
|
|
145
|
-
return this.getDefaultEmotion();
|
|
151
|
+
return this.getDefaultEmotion(groupId);
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
private getDefaultEmotion(): string {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
154
|
+
private getDefaultEmotion(groupId?: number): string {
|
|
155
|
+
const cfg = this.getConfig(groupId);
|
|
156
|
+
const configured = this.normalizeEmotionName(cfg.emotion?.defaultEmotion);
|
|
157
|
+
const available = this.getAvailableEmotions(groupId);
|
|
151
158
|
return available.includes(configured) ? configured : "default";
|
|
152
159
|
}
|
|
153
160
|
|
package/humanize/expression.ts
CHANGED
|
@@ -2,23 +2,25 @@ import type { AIInstance } from "mioku";
|
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import { extractJsonObject } from "../utils/json";
|
|
4
4
|
import type { ChatDatabase } from "../db";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ChatMessage } from "../types";
|
|
6
|
+
import type { ChatConfigProvider } from "./index";
|
|
6
7
|
|
|
7
8
|
export class ExpressionLearner {
|
|
8
9
|
private ai: AIInstance;
|
|
9
|
-
private
|
|
10
|
+
private getConfig: ChatConfigProvider;
|
|
10
11
|
private db: ChatDatabase;
|
|
11
12
|
private pendingMessagesByUser: Map<number, ChatMessage[]> = new Map();
|
|
12
13
|
private learningUsers: Set<number> = new Set();
|
|
13
14
|
|
|
14
|
-
constructor(ai: AIInstance,
|
|
15
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
|
|
15
16
|
this.ai = ai;
|
|
16
|
-
this.
|
|
17
|
+
this.getConfig = configProvider;
|
|
17
18
|
this.db = db;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async onMessage(message: ChatMessage): Promise<void> {
|
|
21
|
-
|
|
22
|
+
const cfg = this.getConfig(message.groupId);
|
|
23
|
+
if (!cfg.expression?.enabled) return;
|
|
22
24
|
if (message.role !== "user") return;
|
|
23
25
|
if (!message.content || message.content.length < 4) return;
|
|
24
26
|
if (!message.userId) return;
|
|
@@ -27,13 +29,18 @@ export class ExpressionLearner {
|
|
|
27
29
|
pending.push(message);
|
|
28
30
|
this.pendingMessagesByUser.set(message.userId, pending);
|
|
29
31
|
|
|
30
|
-
await this.tryLearn(message.userId);
|
|
32
|
+
await this.tryLearn(message.userId, cfg);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
getExpressionContextForUser(
|
|
34
|
-
|
|
35
|
+
getExpressionContextForUser(
|
|
36
|
+
userId: number,
|
|
37
|
+
userName: string,
|
|
38
|
+
groupId?: number,
|
|
39
|
+
): string {
|
|
40
|
+
const cfg = this.getConfig(groupId);
|
|
41
|
+
if (!cfg.expression?.enabled) return "";
|
|
35
42
|
|
|
36
|
-
const sampleSize =
|
|
43
|
+
const sampleSize = cfg.expression?.sampleSize ?? 8;
|
|
37
44
|
const expressions = this.db.getExpressionsByUser(userId, sampleSize);
|
|
38
45
|
if (expressions.length === 0) {
|
|
39
46
|
logger.info(
|
|
@@ -55,12 +62,12 @@ export class ExpressionLearner {
|
|
|
55
62
|
return context;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
private async tryLearn(userId: number): Promise<void> {
|
|
65
|
+
private async tryLearn(userId: number, cfg = this.getConfig()): Promise<void> {
|
|
59
66
|
if (this.learningUsers.has(userId)) return;
|
|
60
67
|
|
|
61
68
|
const threshold = Math.max(
|
|
62
69
|
1,
|
|
63
|
-
|
|
70
|
+
cfg.expression?.learnAfterMessages ?? 100,
|
|
64
71
|
);
|
|
65
72
|
const pending = this.pendingMessagesByUser.get(userId) ?? [];
|
|
66
73
|
logger.debug(
|
|
@@ -76,7 +83,7 @@ export class ExpressionLearner {
|
|
|
76
83
|
|
|
77
84
|
const batch = current.slice(0, threshold);
|
|
78
85
|
this.pendingMessagesByUser.set(userId, current.slice(threshold));
|
|
79
|
-
await this.learnForUser(userId, batch);
|
|
86
|
+
await this.learnForUser(userId, batch, cfg);
|
|
80
87
|
}
|
|
81
88
|
} catch (err) {
|
|
82
89
|
logger.warn(
|
|
@@ -90,10 +97,11 @@ export class ExpressionLearner {
|
|
|
90
97
|
private async learnForUser(
|
|
91
98
|
userId: number,
|
|
92
99
|
messages: ChatMessage[],
|
|
100
|
+
cfg = this.getConfig(),
|
|
93
101
|
): Promise<void> {
|
|
94
102
|
if (messages.length === 0) return;
|
|
95
103
|
|
|
96
|
-
const maxHabits = Math.max(1,
|
|
104
|
+
const maxHabits = Math.max(1, cfg.expression?.sampleSize ?? 8);
|
|
97
105
|
const userName = messages[messages.length - 1].userName || `User${userId}`;
|
|
98
106
|
const msgTexts = messages.map((m) => m.content).join("\n");
|
|
99
107
|
|
|
@@ -128,7 +136,7 @@ Output strictly in JSON:
|
|
|
128
136
|
|
|
129
137
|
If nothing reliable can be extracted, keep stable previous habits when possible. If still nothing, output {"expressions":[]}.`,
|
|
130
138
|
messages: [],
|
|
131
|
-
model:
|
|
139
|
+
model: cfg.workingModel || cfg.model,
|
|
132
140
|
temperature: 0.2,
|
|
133
141
|
max_tokens: 600,
|
|
134
142
|
});
|
|
@@ -154,7 +162,7 @@ If nothing reliable can be extracted, keep stable previous habits when possible.
|
|
|
154
162
|
|
|
155
163
|
const keepCount = Math.max(
|
|
156
164
|
1,
|
|
157
|
-
|
|
165
|
+
cfg.retention?.expressionKeepPerUser ?? maxHabits,
|
|
158
166
|
);
|
|
159
167
|
const pruned = this.db.pruneExpressionsBySession(
|
|
160
168
|
`user:${userId}`,
|
|
@@ -170,4 +178,4 @@ If nothing reliable can be extracted, keep stable previous habits when possible.
|
|
|
170
178
|
`[ExpressionLearner] Updated ${normalized.length} habits for ${userName} (${userId}): ${JSON.stringify(normalized)}`,
|
|
171
179
|
);
|
|
172
180
|
}
|
|
173
|
-
}
|
|
181
|
+
}
|
package/humanize/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { TopicTracker } from "./topic";
|
|
|
16
16
|
import { ActionPlanner } from "./planner";
|
|
17
17
|
import { ExpressionLearner } from "./expression";
|
|
18
18
|
|
|
19
|
+
export type ChatConfigProvider = (groupId?: number) => ChatConfig;
|
|
20
|
+
|
|
19
21
|
export class HumanizeEngine {
|
|
20
22
|
readonly memoryRetrieval: MemoryRetrieval;
|
|
21
23
|
readonly topicTracker: TopicTracker;
|
|
@@ -25,17 +27,16 @@ export class HumanizeEngine {
|
|
|
25
27
|
readonly expressionLearner: ExpressionLearner;
|
|
26
28
|
|
|
27
29
|
constructor(
|
|
28
|
-
mainAI: AIInstance,
|
|
29
30
|
workAI: AIInstance,
|
|
30
|
-
config: ChatConfig,
|
|
31
31
|
db: ChatDatabase,
|
|
32
|
+
configProvider: ChatConfigProvider,
|
|
32
33
|
) {
|
|
33
|
-
this.memoryRetrieval = new MemoryRetrieval(workAI,
|
|
34
|
-
this.topicTracker = new TopicTracker(workAI,
|
|
35
|
-
this.actionPlanner = new ActionPlanner(workAI,
|
|
36
|
-
this.emotionAgent = new EmotionAgent(workAI,
|
|
37
|
-
this.emojiAgent = new EmojiAgent(workAI,
|
|
38
|
-
this.expressionLearner = new ExpressionLearner(workAI,
|
|
34
|
+
this.memoryRetrieval = new MemoryRetrieval(workAI, configProvider, db);
|
|
35
|
+
this.topicTracker = new TopicTracker(workAI, configProvider, db);
|
|
36
|
+
this.actionPlanner = new ActionPlanner(workAI, configProvider);
|
|
37
|
+
this.emotionAgent = new EmotionAgent(workAI, configProvider);
|
|
38
|
+
this.emojiAgent = new EmojiAgent(workAI, configProvider);
|
|
39
|
+
this.expressionLearner = new ExpressionLearner(workAI, configProvider, db);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
async init(): Promise<void> {
|
package/humanize/memory.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AIInstance } from "mioku";
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import type { ChatDatabase } from "../db";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ChatMessage } from "../types";
|
|
5
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
6
|
+
import type { ChatConfigProvider } from "./index";
|
|
5
7
|
|
|
6
8
|
export interface MemoryUserHistoryChunk {
|
|
7
9
|
userId: number;
|
|
@@ -18,17 +20,18 @@ export interface MemoryRecallInput {
|
|
|
18
20
|
|
|
19
21
|
export class MemoryRetrieval {
|
|
20
22
|
private ai: AIInstance;
|
|
21
|
-
private
|
|
23
|
+
private getConfig: ChatConfigProvider;
|
|
22
24
|
private db: ChatDatabase;
|
|
23
25
|
|
|
24
|
-
constructor(ai: AIInstance,
|
|
26
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
|
|
25
27
|
this.ai = ai;
|
|
26
|
-
this.
|
|
28
|
+
this.getConfig = configProvider;
|
|
27
29
|
this.db = db;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
async retrieveByQuestion(input: MemoryRecallInput): Promise<string | null> {
|
|
31
|
-
|
|
33
|
+
const cfg = this.getConfig(extractGroupIdFromSession(input.sessionId));
|
|
34
|
+
if (!cfg.memory?.enabled) return null;
|
|
32
35
|
|
|
33
36
|
const question = String(input.question || "").trim();
|
|
34
37
|
if (!question) return null;
|
|
@@ -68,7 +71,7 @@ Output requirements:
|
|
|
68
71
|
- If useful evidence exists, summarize key facts with timestamps.
|
|
69
72
|
- If no useful memory exists, output exactly: NO_USEFUL_MEMORY_FOUND`,
|
|
70
73
|
messages: [],
|
|
71
|
-
model:
|
|
74
|
+
model: cfg.workingModel || cfg.model,
|
|
72
75
|
temperature: 0.2,
|
|
73
76
|
max_tokens: 700,
|
|
74
77
|
});
|
|
@@ -128,4 +131,4 @@ Output requirements:
|
|
|
128
131
|
private formatTimestamp(timestamp: number): string {
|
|
129
132
|
return new Date(timestamp).toLocaleString("zh-CN", { hour12: false });
|
|
130
133
|
}
|
|
131
|
-
}
|
|
134
|
+
}
|
package/humanize/planner.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import type { AIInstance } from "mioku";
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import type {
|
|
4
|
-
ChatConfig,
|
|
5
4
|
ChatMessage,
|
|
6
5
|
PlannerAction,
|
|
7
6
|
PlannerResult,
|
|
8
7
|
} from "../types";
|
|
8
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
9
|
+
import type { ChatConfigProvider } from "./index";
|
|
9
10
|
|
|
10
11
|
export class ActionPlanner {
|
|
11
12
|
private ai: AIInstance;
|
|
12
|
-
private
|
|
13
|
+
private getConfig: ChatConfigProvider;
|
|
13
14
|
private actionHistory: Map<
|
|
14
15
|
string,
|
|
15
16
|
{ action: PlannerAction; time: number }[]
|
|
16
17
|
> = new Map();
|
|
17
18
|
private pendingPlans: Map<string, Promise<PlannerResult>> = new Map();
|
|
18
19
|
|
|
19
|
-
constructor(ai: AIInstance,
|
|
20
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider) {
|
|
20
21
|
this.ai = ai;
|
|
21
|
-
this.
|
|
22
|
+
this.getConfig = configProvider;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async plan(
|
|
@@ -28,18 +29,18 @@ export class ActionPlanner {
|
|
|
28
29
|
lastTriggerMessage: string,
|
|
29
30
|
isIdleCheck: boolean = false,
|
|
30
31
|
): Promise<PlannerResult> {
|
|
31
|
-
|
|
32
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
33
|
+
if (!cfg.planner?.enabled) {
|
|
32
34
|
return { action: "reply", reason: "planner disabled" };
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// Guard: if a plan call is already in-flight for this session, wait for it
|
|
36
37
|
const existing = this.pendingPlans.get(sessionId);
|
|
37
38
|
if (existing) {
|
|
38
39
|
logger.info(`[ActionPlanner] Session ${sessionId} has a plan already in-flight, waiting...`);
|
|
39
40
|
return existing;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck);
|
|
43
|
+
const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck, cfg);
|
|
43
44
|
this.pendingPlans.set(sessionId, planPromise);
|
|
44
45
|
|
|
45
46
|
try {
|
|
@@ -55,8 +56,9 @@ export class ActionPlanner {
|
|
|
55
56
|
recentHistory: ChatMessage[],
|
|
56
57
|
lastTriggerMessage: string,
|
|
57
58
|
isIdleCheck: boolean,
|
|
59
|
+
cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
|
|
58
60
|
): Promise<PlannerResult> {
|
|
59
|
-
if (!
|
|
61
|
+
if (!cfg.planner?.enabled) {
|
|
60
62
|
return { action: "reply", reason: "planner disabled" };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -81,7 +83,6 @@ export class ActionPlanner {
|
|
|
81
83
|
let prompt: string;
|
|
82
84
|
|
|
83
85
|
if (isIdleCheck) {
|
|
84
|
-
// 空闲检测模式:没有明确触发消息,观察群友聊天并决定是否要融入对话
|
|
85
86
|
prompt = `It is ${timeStr}. Your name is ${botName}.
|
|
86
87
|
|
|
87
88
|
Here is the recent chat content (from oldest to most recent):
|
|
@@ -120,7 +121,6 @@ OR for complete:
|
|
|
120
121
|
|
|
121
122
|
DO NOT include any explanation, markdown formatting, or additional text. Only output the JSON.`;
|
|
122
123
|
} else {
|
|
123
|
-
// 正常模式:由触发消息驱动的 planner
|
|
124
124
|
prompt = `It is ${timeStr}. Your name is ${botName}.
|
|
125
125
|
|
|
126
126
|
Here is the chat content:
|
|
@@ -169,12 +169,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
169
169
|
const content = await this.ai.generateText({
|
|
170
170
|
prompt,
|
|
171
171
|
messages: [],
|
|
172
|
-
model:
|
|
172
|
+
model: cfg.workingModel || cfg.model,
|
|
173
173
|
temperature: isIdleCheck ? 0.3 : 0.2,
|
|
174
174
|
max_tokens: isIdleCheck ? 300 : 500,
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// 如果返回内容为空,使用默认值
|
|
178
177
|
if (!content || !content.trim()) {
|
|
179
178
|
logger.warn(
|
|
180
179
|
`[ActionPlanner] Empty response from AI, using default reply`,
|
|
@@ -182,7 +181,6 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
182
181
|
return { action: "reply", reason: "empty response" };
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
// 尝试提取 JSON 块
|
|
186
184
|
let jsonStr = "";
|
|
187
185
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
188
186
|
if (jsonMatch) {
|
|
@@ -196,14 +194,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
196
194
|
return { action: "reply", reason: "parse failed" };
|
|
197
195
|
}
|
|
198
196
|
|
|
199
|
-
// 尝试解析 JSON
|
|
200
197
|
let parsed: any;
|
|
201
198
|
try {
|
|
202
199
|
parsed = JSON.parse(jsonStr);
|
|
203
200
|
} catch {
|
|
204
|
-
// 尝试修复常见的 JSON 错误
|
|
205
201
|
try {
|
|
206
|
-
// 移除可能存在的尾随逗号
|
|
207
202
|
jsonStr = jsonStr.replace(/,\s*}/g, "}").replace(/,\s*]/g, "]");
|
|
208
203
|
parsed = JSON.parse(jsonStr);
|
|
209
204
|
} catch (e) {
|
package/humanize/topic.ts
CHANGED
|
@@ -2,25 +2,28 @@ import type { AIInstance } from "mioku";
|
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import { extractJsonObject } from "../utils/json";
|
|
4
4
|
import type { ChatDatabase } from "../db";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ChatMessage } from "../types";
|
|
6
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
7
|
+
import type { ChatConfigProvider } from "./index";
|
|
6
8
|
|
|
7
9
|
export class TopicTracker {
|
|
8
10
|
private ai: AIInstance;
|
|
9
|
-
private
|
|
11
|
+
private getConfig: ChatConfigProvider;
|
|
10
12
|
private db: ChatDatabase;
|
|
11
13
|
private lastAnalyzedWindowEnd: Map<string, number> = new Map();
|
|
12
14
|
|
|
13
|
-
constructor(ai: AIInstance,
|
|
15
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
|
|
14
16
|
this.ai = ai;
|
|
15
|
-
this.
|
|
17
|
+
this.getConfig = configProvider;
|
|
16
18
|
this.db = db;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
async onMessage(sessionId: string): Promise<void> {
|
|
20
|
-
|
|
22
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
23
|
+
if (!cfg.topic?.enabled) return;
|
|
21
24
|
if (!sessionId.startsWith("group:")) return;
|
|
22
25
|
|
|
23
|
-
const windowMs = this.getWindowMs();
|
|
26
|
+
const windowMs = this.getWindowMs(cfg);
|
|
24
27
|
const completedWindowEnd = Math.floor(Date.now() / windowMs) * windowMs;
|
|
25
28
|
if (completedWindowEnd <= 0) return;
|
|
26
29
|
|
|
@@ -32,18 +35,19 @@ export class TopicTracker {
|
|
|
32
35
|
this.lastAnalyzedWindowEnd.set(sessionId, completedWindowEnd);
|
|
33
36
|
const windowStartAt = completedWindowEnd - windowMs;
|
|
34
37
|
|
|
35
|
-
this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd).catch(
|
|
38
|
+
this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd, cfg).catch(
|
|
36
39
|
(err) => logger.warn(`[TopicTracker] Analysis failed: ${err}`),
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
getTopicContext(sessionId: string, historyStartAt?: number): string {
|
|
41
|
-
|
|
44
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
45
|
+
if (!cfg.topic?.enabled) return "";
|
|
42
46
|
if (!sessionId.startsWith("group:")) return "";
|
|
43
47
|
if (!historyStartAt) return "";
|
|
44
48
|
|
|
45
|
-
const windowMs = this.getWindowMs();
|
|
46
|
-
const windowCount = this.getHistoryWindowCount();
|
|
49
|
+
const windowMs = this.getWindowMs(cfg);
|
|
50
|
+
const windowCount = this.getHistoryWindowCount(cfg);
|
|
47
51
|
const firstWindowEnd = Math.floor(historyStartAt / windowMs) * windowMs;
|
|
48
52
|
if (firstWindowEnd <= 0) return "";
|
|
49
53
|
|
|
@@ -79,6 +83,7 @@ export class TopicTracker {
|
|
|
79
83
|
sessionId: string,
|
|
80
84
|
windowStartAt: number,
|
|
81
85
|
windowEndAt: number,
|
|
86
|
+
cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
|
|
82
87
|
): Promise<void> {
|
|
83
88
|
if (windowEndAt <= windowStartAt) return;
|
|
84
89
|
|
|
@@ -119,7 +124,7 @@ Output strictly in JSON format:
|
|
|
119
124
|
"summary": "one concise abstract summary sentence"
|
|
120
125
|
}`,
|
|
121
126
|
messages: [],
|
|
122
|
-
model:
|
|
127
|
+
model: cfg.workingModel || cfg.model,
|
|
123
128
|
temperature: 0.2,
|
|
124
129
|
max_tokens: 300,
|
|
125
130
|
});
|
|
@@ -163,14 +168,14 @@ Output strictly in JSON format:
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
private getWindowMs(): number {
|
|
167
|
-
const hours = Number(
|
|
171
|
+
private getWindowMs(cfg = this.getConfig()): number {
|
|
172
|
+
const hours = Number(cfg.topic?.windowHours);
|
|
168
173
|
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 5;
|
|
169
174
|
return Math.max(1, Math.floor(safeHours)) * 3600_000;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
private getHistoryWindowCount(): number {
|
|
173
|
-
const count = Number(
|
|
177
|
+
private getHistoryWindowCount(cfg = this.getConfig()): number {
|
|
178
|
+
const count = Number(cfg.topic?.historyWindowCount);
|
|
174
179
|
if (!Number.isFinite(count) || count <= 0) return 3;
|
|
175
180
|
return Math.max(1, Math.floor(count));
|
|
176
181
|
}
|
|
@@ -202,3 +207,5 @@ Output strictly in JSON format:
|
|
|
202
207
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
203
208
|
}
|
|
204
209
|
}
|
|
210
|
+
|
|
211
|
+
export type { ChatMessage };
|