mioku-plugin-agent 0.1.0 → 0.2.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/README.md +2 -2
- package/commands/index.ts +10 -9
- package/core/compaction.ts +1 -1
- package/core/download.ts +18 -48
- package/core/emotion.ts +2 -2
- package/core/identity.ts +27 -0
- package/core/loop.ts +37 -30
- package/core/model.ts +179 -0
- package/core/send.ts +1 -1
- package/core/session.ts +10 -10
- package/db.ts +55 -15
- package/handlers/message.ts +14 -8
- package/index.ts +49 -84
- package/package.json +1 -1
- package/platforms/generic.ts +11 -0
- package/platforms/icqq.ts +47 -0
- package/platforms/index.ts +49 -0
- package/platforms/onebotv11.ts +40 -0
- package/platforms/qq-official.ts +14 -0
- package/platforms/types.ts +53 -0
- package/tools/approval.ts +7 -7
- package/tools/bash.ts +1 -1
- package/tools/deliver.ts +1 -1
- package/tools/index.ts +7 -3
- package/tools/perm.ts +6 -2
- package/tools/todo.ts +6 -3
- package/types.ts +3 -3
package/core/session.ts
CHANGED
|
@@ -3,44 +3,44 @@ import type { AgentDatabase, AgentSessionRow } from "../db";
|
|
|
3
3
|
export class SessionManager {
|
|
4
4
|
constructor(private db: AgentDatabase) {}
|
|
5
5
|
|
|
6
|
-
generation(userId:
|
|
6
|
+
generation(userId: string): number {
|
|
7
7
|
return this.db.getUserGeneration(userId);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
sessionId(userId:
|
|
10
|
+
sessionId(userId: string): string {
|
|
11
11
|
return `agent:${userId}:g${this.generation(userId)}`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
get(userId:
|
|
14
|
+
get(userId: string): AgentSessionRow {
|
|
15
15
|
return this.db.getOrCreateSession(this.sessionId(userId), userId);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
newSession(userId:
|
|
18
|
+
newSession(userId: string): AgentSessionRow {
|
|
19
19
|
this.db.bumpUserGenerationTo(userId, this.db.maxGeneration(userId) + 1);
|
|
20
20
|
return this.get(userId);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
resume(userId:
|
|
23
|
+
resume(userId: string, generation: number): AgentSessionRow | undefined {
|
|
24
24
|
const target = this.db.getSessionByGeneration(userId, generation);
|
|
25
25
|
if (!target) return undefined;
|
|
26
26
|
this.db.bumpUserGenerationTo(userId, generation);
|
|
27
27
|
return this.db.getOrCreateSession(target.sessionId, userId);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
resumableSessions(userId:
|
|
30
|
+
resumableSessions(userId: string, excludeSessionId: string): AgentSessionRow[] {
|
|
31
31
|
return this.db
|
|
32
32
|
.listSessions(userId, { archived: false })
|
|
33
33
|
.filter((session) => session.sessionId !== excludeSessionId);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
archive(userId:
|
|
36
|
+
archive(userId: string, generation: number): AgentSessionRow | undefined {
|
|
37
37
|
const target = this.db.getSessionByGeneration(userId, generation);
|
|
38
38
|
if (!target) return undefined;
|
|
39
39
|
this.db.setSessionMeta(target.sessionId, { archived: true });
|
|
40
40
|
return this.db.getSessionByGeneration(userId, generation);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
reset(userId:
|
|
43
|
+
reset(userId: string): void {
|
|
44
44
|
this.db.resetSession(this.sessionId(userId));
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -48,11 +48,11 @@ export class SessionManager {
|
|
|
48
48
|
return this.db.getMessagesAfter(session.sessionId, session.summaryUpTo);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
append(userId:
|
|
51
|
+
append(userId: string, role: "user" | "assistant", content: string): number {
|
|
52
52
|
return this.db.appendMessage(this.sessionId(userId), role, content);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
setEmotion(userId:
|
|
55
|
+
setEmotion(userId: string, emotion: string): void {
|
|
56
56
|
this.db.setSessionMeta(this.sessionId(userId), { emotion });
|
|
57
57
|
}
|
|
58
58
|
}
|
package/db.ts
CHANGED
|
@@ -18,6 +18,14 @@ function rowString(row: SqlRow, key: string, fallback = ""): string {
|
|
|
18
18
|
return typeof value === "string" ? value : fallback;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** id 列可能是 TEXT(openid)也可能是 INTEGER 亲和存下的数字,统一转字符串 */
|
|
22
|
+
function rowId(row: SqlRow | null | undefined, key: string, fallback = ""): string {
|
|
23
|
+
const value = row?.[key];
|
|
24
|
+
if (value == null) return fallback;
|
|
25
|
+
const text = String(value);
|
|
26
|
+
return text.length > 0 ? text : fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export type SessionPlanStatus = "pending" | "in_progress" | "completed";
|
|
22
30
|
|
|
23
31
|
export interface SessionPlanItem {
|
|
@@ -27,7 +35,7 @@ export interface SessionPlanItem {
|
|
|
27
35
|
|
|
28
36
|
export interface AgentSessionRow {
|
|
29
37
|
sessionId: string;
|
|
30
|
-
userId:
|
|
38
|
+
userId: string;
|
|
31
39
|
generation: number;
|
|
32
40
|
emotion: string;
|
|
33
41
|
summary: string;
|
|
@@ -51,7 +59,7 @@ export interface AgentMessageRow {
|
|
|
51
59
|
export interface AgentRunRow {
|
|
52
60
|
id: number;
|
|
53
61
|
sessionId: string;
|
|
54
|
-
userId:
|
|
62
|
+
userId: string;
|
|
55
63
|
model: string;
|
|
56
64
|
status: "ok" | "error";
|
|
57
65
|
iterations: number;
|
|
@@ -93,7 +101,7 @@ export class AgentDatabase {
|
|
|
93
101
|
this.db.run(`
|
|
94
102
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
95
103
|
session_id TEXT PRIMARY KEY,
|
|
96
|
-
user_id
|
|
104
|
+
user_id TEXT NOT NULL,
|
|
97
105
|
generation INTEGER NOT NULL DEFAULT 0,
|
|
98
106
|
emotion TEXT NOT NULL DEFAULT '',
|
|
99
107
|
summary TEXT NOT NULL DEFAULT '',
|
|
@@ -106,7 +114,7 @@ export class AgentDatabase {
|
|
|
106
114
|
updated_at INTEGER NOT NULL
|
|
107
115
|
);
|
|
108
116
|
CREATE TABLE IF NOT EXISTS user_state (
|
|
109
|
-
user_id
|
|
117
|
+
user_id TEXT PRIMARY KEY,
|
|
110
118
|
generation INTEGER NOT NULL DEFAULT 0,
|
|
111
119
|
updated_at INTEGER NOT NULL
|
|
112
120
|
);
|
|
@@ -121,7 +129,7 @@ export class AgentDatabase {
|
|
|
121
129
|
CREATE TABLE IF NOT EXISTS runs (
|
|
122
130
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
131
|
session_id TEXT NOT NULL,
|
|
124
|
-
user_id
|
|
132
|
+
user_id TEXT NOT NULL,
|
|
125
133
|
model TEXT NOT NULL DEFAULT '',
|
|
126
134
|
status TEXT NOT NULL DEFAULT 'ok',
|
|
127
135
|
iterations INTEGER NOT NULL DEFAULT 0,
|
|
@@ -142,23 +150,55 @@ export class AgentDatabase {
|
|
|
142
150
|
created_at INTEGER NOT NULL
|
|
143
151
|
);
|
|
144
152
|
`);
|
|
153
|
+
this.#widenLegacyIdColumns();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 旧库的 user_state.user_id 是 INTEGER PRIMARY KEY(rowid 别名),
|
|
158
|
+
* 写入 openid 会直接抛 SQLiteError: datatype mismatch,这里重建成 TEXT。
|
|
159
|
+
*/
|
|
160
|
+
#widenLegacyIdColumns(): void {
|
|
161
|
+
const info = this.db
|
|
162
|
+
.query("PRAGMA table_info(user_state)")
|
|
163
|
+
.all() as Array<{ name?: string; type?: string }>;
|
|
164
|
+
const column = info.find((item) => item.name === "user_id");
|
|
165
|
+
if (!column || String(column.type ?? "").toUpperCase() === "TEXT") return;
|
|
166
|
+
|
|
167
|
+
this.db.run("BEGIN");
|
|
168
|
+
try {
|
|
169
|
+
this.db.run("ALTER TABLE user_state RENAME TO user_state_legacy");
|
|
170
|
+
this.db.run(`
|
|
171
|
+
CREATE TABLE user_state (
|
|
172
|
+
user_id TEXT PRIMARY KEY,
|
|
173
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
174
|
+
updated_at INTEGER NOT NULL
|
|
175
|
+
);
|
|
176
|
+
INSERT INTO user_state (user_id, generation, updated_at)
|
|
177
|
+
SELECT CAST(user_id AS TEXT), generation, updated_at FROM user_state_legacy;
|
|
178
|
+
DROP TABLE user_state_legacy;
|
|
179
|
+
`);
|
|
180
|
+
this.db.run("COMMIT");
|
|
181
|
+
} catch (err) {
|
|
182
|
+
this.db.run("ROLLBACK");
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
145
185
|
}
|
|
146
186
|
|
|
147
|
-
getUserGeneration(userId:
|
|
187
|
+
getUserGeneration(userId: string): number {
|
|
148
188
|
const row = this.db
|
|
149
189
|
.query("SELECT generation FROM user_state WHERE user_id = ?")
|
|
150
190
|
.get(userId) as SqlRow | null;
|
|
151
191
|
return rowNumber(row, "generation", 0);
|
|
152
192
|
}
|
|
153
193
|
|
|
154
|
-
maxGeneration(userId:
|
|
194
|
+
maxGeneration(userId: string): number {
|
|
155
195
|
const row = this.db
|
|
156
196
|
.query("SELECT MAX(generation) AS max FROM sessions WHERE user_id = ?")
|
|
157
197
|
.get(userId) as SqlRow | null;
|
|
158
198
|
return rowNumber(row, "max", -1);
|
|
159
199
|
}
|
|
160
200
|
|
|
161
|
-
bumpUserGenerationTo(userId:
|
|
201
|
+
bumpUserGenerationTo(userId: string, generation: number): void {
|
|
162
202
|
this.db.run(
|
|
163
203
|
`INSERT INTO user_state (user_id, generation, updated_at) VALUES (?, ?, ?)
|
|
164
204
|
ON CONFLICT(user_id) DO UPDATE SET generation = ?, updated_at = ?`,
|
|
@@ -166,7 +206,7 @@ export class AgentDatabase {
|
|
|
166
206
|
);
|
|
167
207
|
}
|
|
168
208
|
|
|
169
|
-
getOrCreateSession(sessionId: string, userId:
|
|
209
|
+
getOrCreateSession(sessionId: string, userId: string): AgentSessionRow {
|
|
170
210
|
const now = Date.now();
|
|
171
211
|
this.db.run(
|
|
172
212
|
`INSERT INTO sessions (session_id, user_id, generation, created_at, updated_at)
|
|
@@ -181,7 +221,7 @@ export class AgentDatabase {
|
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
getSessionByGeneration(
|
|
184
|
-
userId:
|
|
224
|
+
userId: string,
|
|
185
225
|
generation: number,
|
|
186
226
|
): AgentSessionRow | undefined {
|
|
187
227
|
const row = this.db
|
|
@@ -191,7 +231,7 @@ export class AgentDatabase {
|
|
|
191
231
|
}
|
|
192
232
|
|
|
193
233
|
listSessions(
|
|
194
|
-
userId:
|
|
234
|
+
userId: string,
|
|
195
235
|
options: { archived?: boolean } = {},
|
|
196
236
|
): AgentSessionRow[] {
|
|
197
237
|
const rows = options.archived === undefined
|
|
@@ -314,7 +354,7 @@ export class AgentDatabase {
|
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
/** 删除该用户的全部会话(含归档)及其消息、运行记录与工具调用明细。 */
|
|
317
|
-
clearUserSessions(userId:
|
|
357
|
+
clearUserSessions(userId: string): ClearSessionsResult {
|
|
318
358
|
const ids = this.listSessions(userId).map((session) => session.sessionId);
|
|
319
359
|
const result: ClearSessionsResult = {
|
|
320
360
|
sessions: ids.length,
|
|
@@ -360,7 +400,7 @@ export class AgentDatabase {
|
|
|
360
400
|
return rowNumber(row, "count", 0);
|
|
361
401
|
}
|
|
362
402
|
|
|
363
|
-
startRun(sessionId: string, userId:
|
|
403
|
+
startRun(sessionId: string, userId: string, model: string): number {
|
|
364
404
|
const result = this.db
|
|
365
405
|
.query(
|
|
366
406
|
"INSERT INTO runs (session_id, user_id, model, started_at) VALUES (?, ?, ?, ?)",
|
|
@@ -408,7 +448,7 @@ export class AgentDatabase {
|
|
|
408
448
|
);
|
|
409
449
|
}
|
|
410
450
|
|
|
411
|
-
getRunStats(userId:
|
|
451
|
+
getRunStats(userId: string): { runs: number; toolCalls: number } {
|
|
412
452
|
const runRow = this.db
|
|
413
453
|
.query("SELECT COUNT(*) AS count FROM runs WHERE user_id = ?")
|
|
414
454
|
.get(userId) as SqlRow | null;
|
|
@@ -449,7 +489,7 @@ function toSessionRow(row: SqlRow): AgentSessionRow {
|
|
|
449
489
|
const sessionId = rowString(row, "session_id");
|
|
450
490
|
return {
|
|
451
491
|
sessionId,
|
|
452
|
-
userId:
|
|
492
|
+
userId: rowId(row, "user_id"),
|
|
453
493
|
generation: rowNumber(row, "generation", parseGeneration(sessionId)),
|
|
454
494
|
emotion: rowString(row, "emotion"),
|
|
455
495
|
summary: rowString(row, "summary"),
|
package/handlers/message.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { MessageEvent } from "mioku";
|
|
2
2
|
import type { AgentHost } from "../types";
|
|
3
|
+
import { identityOf } from "../core/identity";
|
|
3
4
|
import { runAgentTurn } from "../core/loop";
|
|
5
|
+
import { genericPlatform } from "../platforms/generic";
|
|
6
|
+
import type { AgentPlatform } from "../platforms/types";
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/** 各平台分支共用的入口:权限/自消息过滤后进入 agent 轮次 */
|
|
9
|
+
export async function handleAgentMessage(
|
|
10
|
+
host: AgentHost,
|
|
11
|
+
event: MessageEvent,
|
|
12
|
+
platform: AgentPlatform = genericPlatform,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (event.message_type === "group") return;
|
|
15
|
+
const identity = identityOf(event);
|
|
16
|
+
if (!identity.userId || identity.userId === identity.botId) return;
|
|
17
|
+
if (!(await host.isAllowed(identity.userId))) return;
|
|
18
|
+
await runAgentTurn(host, event, platform);
|
|
13
19
|
}
|
package/index.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { definePlugin, getService, Services } from "mioku";
|
|
2
|
-
import type {
|
|
2
|
+
import type { MiokuContext } from "mioku";
|
|
3
3
|
import { initDatabase } from "./db";
|
|
4
4
|
import { SessionManager } from "./core/session";
|
|
5
5
|
import { EmotionManager } from "./core/emotion";
|
|
6
6
|
import { ApprovalManager } from "./tools/approval";
|
|
7
7
|
import { readChatSharedConfig } from "./core/chat-config";
|
|
8
|
-
import {
|
|
8
|
+
import { registerAgentPlatforms } from "./platforms";
|
|
9
9
|
import { registerCommands } from "./commands";
|
|
10
10
|
import { mergeAgentConfig } from "./utils/config";
|
|
11
11
|
import { workspaceRootFor } from "./tools/perm";
|
|
12
|
+
import {
|
|
13
|
+
prepareModelOverride,
|
|
14
|
+
resolveAgentModel,
|
|
15
|
+
type ModelOverride,
|
|
16
|
+
} from "./core/model";
|
|
12
17
|
import { BASE_CONFIG } from "./configs/base";
|
|
13
18
|
import { SETTINGS_CONFIG } from "./configs/settings";
|
|
14
19
|
import type {
|
|
@@ -18,13 +23,13 @@ import type {
|
|
|
18
23
|
ResolvedModel,
|
|
19
24
|
} from "./types";
|
|
20
25
|
|
|
21
|
-
function normalizeIdList(input: unknown):
|
|
26
|
+
function normalizeIdList(input: unknown): string[] {
|
|
22
27
|
if (!Array.isArray(input)) return [];
|
|
23
28
|
return Array.from(
|
|
24
29
|
new Set(
|
|
25
30
|
input
|
|
26
|
-
.map((item) =>
|
|
27
|
-
.filter((id) =>
|
|
31
|
+
.map((item) => String(item ?? "").trim())
|
|
32
|
+
.filter((id) => id.length > 0),
|
|
28
33
|
),
|
|
29
34
|
);
|
|
30
35
|
}
|
|
@@ -56,12 +61,33 @@ export default definePlugin({
|
|
|
56
61
|
);
|
|
57
62
|
if (!Array.isArray(cachedBase.access.users)) cachedBase.access.users = [];
|
|
58
63
|
|
|
64
|
+
let modelOverride: ModelOverride | undefined;
|
|
65
|
+
|
|
66
|
+
const refreshOverride = async (): Promise<void> => {
|
|
67
|
+
const full = String(cachedBase.model ?? "").trim();
|
|
68
|
+
if (!full) {
|
|
69
|
+
modelOverride = undefined;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const alive = modelOverride
|
|
73
|
+
? Boolean(aiService.get?.(modelOverride.instanceName))
|
|
74
|
+
: false;
|
|
75
|
+
if (modelOverride?.key === full && alive) return;
|
|
76
|
+
modelOverride = await prepareModelOverride(aiService, full, (message) =>
|
|
77
|
+
ctx.logger.warn(`[agent] ${message}`),
|
|
78
|
+
);
|
|
79
|
+
ctx.logger.info(
|
|
80
|
+
`[agent] 覆盖模型 ${full} -> ${modelOverride ? `实例 ${modelOverride.instanceName}` : "未绑定,退回主模型"}`,
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
59
84
|
const refreshBase = async () => {
|
|
60
85
|
cachedBase = mergeAgentConfig(
|
|
61
86
|
BASE_CONFIG,
|
|
62
87
|
(await configService?.getConfig("agent", "base")) ?? {},
|
|
63
88
|
);
|
|
64
89
|
if (!Array.isArray(cachedBase.access.users)) cachedBase.access.users = [];
|
|
90
|
+
await refreshOverride();
|
|
65
91
|
};
|
|
66
92
|
const refreshSettings = async () => {
|
|
67
93
|
cachedSettings = mergeAgentConfig(
|
|
@@ -69,6 +95,7 @@ export default definePlugin({
|
|
|
69
95
|
(await configService?.getConfig("agent", "settings")) ?? {},
|
|
70
96
|
);
|
|
71
97
|
};
|
|
98
|
+
await refreshOverride();
|
|
72
99
|
if (configService) {
|
|
73
100
|
configService.onConfigChange("agent", "base", () =>
|
|
74
101
|
refreshBase().catch((err) =>
|
|
@@ -89,75 +116,12 @@ export default definePlugin({
|
|
|
89
116
|
sessions.setEmotion(userId, emotion),
|
|
90
117
|
);
|
|
91
118
|
|
|
92
|
-
const resolveModel = (): ResolvedModel | null =>
|
|
93
|
-
|
|
94
|
-
aiService
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const vision = getByRole("vision") ?? working;
|
|
99
|
-
const bindings = aiService.getRoleBindings?.() ?? {
|
|
100
|
-
main: undefined,
|
|
101
|
-
working: undefined,
|
|
102
|
-
vision: undefined,
|
|
103
|
-
};
|
|
104
|
-
const models = aiService.listModels?.() ?? [];
|
|
105
|
-
const instanceName = (instance: AIInstance): string | undefined => {
|
|
106
|
-
const name = (instance as { name?: unknown }).name;
|
|
107
|
-
return typeof name === "string" ? name : undefined;
|
|
108
|
-
};
|
|
109
|
-
const pickModel = (full: string | undefined, instance: AIInstance) => {
|
|
110
|
-
if (full && full.includes("/")) {
|
|
111
|
-
return full.split("/").slice(1).join("/");
|
|
112
|
-
}
|
|
113
|
-
const name = instanceName(instance);
|
|
114
|
-
const info = aiService
|
|
115
|
-
.listInstances?.()
|
|
116
|
-
?.find((item) => item.role === name || item.name === name);
|
|
117
|
-
return info?.modelId ?? "";
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const overrideFullId = String(cachedBase.model ?? "").trim();
|
|
121
|
-
const overrideDesc = overrideFullId
|
|
122
|
-
? models.find((item) => item.id === overrideFullId)
|
|
123
|
-
: undefined;
|
|
124
|
-
let instance = main;
|
|
125
|
-
let model = pickModel(bindings.main, main) || "";
|
|
126
|
-
if (overrideDesc) {
|
|
127
|
-
model = overrideDesc.modelId;
|
|
128
|
-
const info = aiService
|
|
129
|
-
.listInstances?.()
|
|
130
|
-
?.find((item) => item.providerId === overrideDesc.providerId);
|
|
131
|
-
const candidate = info ? aiService.get?.(info.name) : undefined;
|
|
132
|
-
if (candidate) instance = candidate;
|
|
133
|
-
} else if (overrideFullId) {
|
|
134
|
-
model = overrideFullId.includes("/")
|
|
135
|
-
? overrideFullId.split("/").slice(1).join("/")
|
|
136
|
-
: overrideFullId;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const workingModel = pickModel(bindings.working, working) || model;
|
|
140
|
-
const visionModel = pickModel(bindings.vision, vision) || workingModel;
|
|
141
|
-
const visionDesc =
|
|
142
|
-
models.find((item) => item.id === bindings.vision) ||
|
|
143
|
-
models.find((item) => item.modelId === visionModel);
|
|
144
|
-
const isMultimodal =
|
|
145
|
-
visionDesc?.capabilities?.includes("vision") ?? Boolean(visionModel);
|
|
146
|
-
const mainDesc =
|
|
147
|
-
overrideDesc ||
|
|
148
|
-
models.find((item) => item.id === bindings.main) ||
|
|
149
|
-
models.find((item) => item.modelId === model);
|
|
150
|
-
return {
|
|
151
|
-
instance,
|
|
152
|
-
model,
|
|
153
|
-
working,
|
|
154
|
-
workingModel,
|
|
155
|
-
vision,
|
|
156
|
-
visionModel,
|
|
157
|
-
isMultimodal,
|
|
158
|
-
contextWindow: mainDesc?.contextWindow ?? 0,
|
|
159
|
-
};
|
|
160
|
-
};
|
|
119
|
+
const resolveModel = (): ResolvedModel | null =>
|
|
120
|
+
resolveAgentModel({
|
|
121
|
+
aiService,
|
|
122
|
+
overrideFullId: String(cachedBase.model ?? "").trim(),
|
|
123
|
+
override: modelOverride,
|
|
124
|
+
});
|
|
161
125
|
|
|
162
126
|
const host: AgentHost = {
|
|
163
127
|
ctx,
|
|
@@ -172,18 +136,18 @@ export default definePlugin({
|
|
|
172
136
|
getSettings: () => cachedSettings,
|
|
173
137
|
getChatShared: () => readChatSharedConfig(configService),
|
|
174
138
|
resolveModel,
|
|
175
|
-
workspaceRoot: (userId:
|
|
139
|
+
workspaceRoot: (userId: string) =>
|
|
176
140
|
workspaceRootFor(cachedBase.workspaceDir, userId),
|
|
177
|
-
isAllowed: async (userId:
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
) {
|
|
141
|
+
isAllowed: async (userId: string) => {
|
|
142
|
+
const target = String(userId ?? "").trim();
|
|
143
|
+
if (!target) return false;
|
|
144
|
+
const matches = (list: readonly unknown[] | undefined): boolean =>
|
|
145
|
+
(list ?? []).some((item) => String(item ?? "").trim() === target);
|
|
146
|
+
if (matches(ctx.config.owners)) return true;
|
|
147
|
+
if (cachedBase.access.allowAdmins && matches(ctx.config.admins)) {
|
|
184
148
|
return true;
|
|
185
149
|
}
|
|
186
|
-
return normalizeIdList(cachedBase.access.users).includes(
|
|
150
|
+
return normalizeIdList(cachedBase.access.users).includes(target);
|
|
187
151
|
},
|
|
188
152
|
updateBase: async (patch) => {
|
|
189
153
|
if (configService) {
|
|
@@ -196,7 +160,7 @@ export default definePlugin({
|
|
|
196
160
|
};
|
|
197
161
|
|
|
198
162
|
registerCommands(host);
|
|
199
|
-
ctx
|
|
163
|
+
const disposePlatforms = registerAgentPlatforms(ctx, host);
|
|
200
164
|
|
|
201
165
|
const resolved = resolveModel();
|
|
202
166
|
ctx.logger.info(
|
|
@@ -204,6 +168,7 @@ export default definePlugin({
|
|
|
204
168
|
);
|
|
205
169
|
|
|
206
170
|
return () => {
|
|
171
|
+
disposePlatforms();
|
|
207
172
|
approvals.dispose();
|
|
208
173
|
db.close();
|
|
209
174
|
ctx.logger.info("agent 插件已卸载");
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentPlatform } from "./types";
|
|
2
|
+
import { EMPTY_FILE_LOOKUP } from "./types";
|
|
3
|
+
|
|
4
|
+
/** 未单独登记的适配器(如 stdin、未来的新平台)走的兜底分支 */
|
|
5
|
+
export const genericPlatform: AgentPlatform = {
|
|
6
|
+
adapter: "",
|
|
7
|
+
route: "message",
|
|
8
|
+
async resolveFile() {
|
|
9
|
+
return EMPTY_FILE_LOOKUP;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AgentPlatform, PlatformFileLookup } from "./types";
|
|
2
|
+
import { EMPTY_FILE_LOOKUP, mergeFileLookup } from "./types";
|
|
3
|
+
|
|
4
|
+
interface IcqqFileHolder {
|
|
5
|
+
getFileUrl?(fileId: string): Promise<string>;
|
|
6
|
+
getFileInfo?(fileId: string): Promise<{ url?: string; name?: string } | null>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface IcqqFileClient {
|
|
10
|
+
pickGroup?(groupId: string): IcqqFileHolder;
|
|
11
|
+
pickFriend?(userId: string): IcqqFileHolder;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** icqq:pickGroup / pickFriend 上的 getFileInfo / getFileUrl */
|
|
15
|
+
export const icqqPlatform: AgentPlatform = {
|
|
16
|
+
adapter: "icqq",
|
|
17
|
+
route: "icqq:message",
|
|
18
|
+
async resolveFile(bot, ref): Promise<PlatformFileLookup> {
|
|
19
|
+
const client = bot.as<IcqqFileClient>();
|
|
20
|
+
const holder = ref.groupId
|
|
21
|
+
? client.pickGroup?.(ref.groupId)
|
|
22
|
+
: ref.userId
|
|
23
|
+
? client.pickFriend?.(ref.userId)
|
|
24
|
+
: undefined;
|
|
25
|
+
if (!holder) return EMPTY_FILE_LOOKUP;
|
|
26
|
+
|
|
27
|
+
const found: PlatformFileLookup = { sources: [], names: [] };
|
|
28
|
+
if (holder.getFileInfo) {
|
|
29
|
+
try {
|
|
30
|
+
mergeFileLookup(found, await holder.getFileInfo(ref.fileId));
|
|
31
|
+
} catch {
|
|
32
|
+
// 文件不存在或无权限时继续尝试 getFileUrl
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (found.sources.length === 0 && holder.getFileUrl) {
|
|
36
|
+
try {
|
|
37
|
+
const url = await holder.getFileUrl(ref.fileId);
|
|
38
|
+
if (typeof url === "string" && url.trim()) {
|
|
39
|
+
found.sources.push(url.trim());
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// 调用方会回退到消息段里自带的 url/file
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MessageEvent, MiokuContext } from "mioku";
|
|
2
|
+
import type { AgentHost } from "../types";
|
|
3
|
+
import { handleAgentMessage } from "../handlers/message";
|
|
4
|
+
import { genericPlatform } from "./generic";
|
|
5
|
+
import { icqqPlatform } from "./icqq";
|
|
6
|
+
import { onebotv11Platform } from "./onebotv11";
|
|
7
|
+
import { qqOfficialPlatform } from "./qq-official";
|
|
8
|
+
import type { AgentPlatform } from "./types";
|
|
9
|
+
|
|
10
|
+
/** 每个平台一个分支文件,新增平台在这里登记即可 */
|
|
11
|
+
export const AGENT_PLATFORMS: readonly AgentPlatform[] = [
|
|
12
|
+
onebotv11Platform,
|
|
13
|
+
icqqPlatform,
|
|
14
|
+
qqOfficialPlatform,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 按适配器前缀路由注册平台分支:
|
|
19
|
+
* `onebotv11:message` / `icqq:message` / `qq-official:message` 各管自己的差异,
|
|
20
|
+
* 未登记的适配器由通用 `message` 分支兜底。
|
|
21
|
+
*/
|
|
22
|
+
export function registerAgentPlatforms(
|
|
23
|
+
ctx: MiokuContext,
|
|
24
|
+
host: AgentHost,
|
|
25
|
+
): () => void {
|
|
26
|
+
const disposers: Array<() => void> = [];
|
|
27
|
+
|
|
28
|
+
for (const platform of AGENT_PLATFORMS) {
|
|
29
|
+
disposers.push(
|
|
30
|
+
ctx.handle(platform.route, (event) =>
|
|
31
|
+
handleAgentMessage(host, event as unknown as MessageEvent, platform),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const owned = new Set(AGENT_PLATFORMS.map((platform) => platform.adapter));
|
|
37
|
+
disposers.push(
|
|
38
|
+
ctx.handle("message", (event) => {
|
|
39
|
+
const messageEvent = event as unknown as MessageEvent;
|
|
40
|
+
const adapter = String(messageEvent?.bot?.adapter ?? "");
|
|
41
|
+
if (owned.has(adapter)) return;
|
|
42
|
+
return handleAgentMessage(host, messageEvent, genericPlatform);
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
for (const dispose of disposers) dispose();
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AgentPlatform, PlatformFileLookup } from "./types";
|
|
2
|
+
import { EMPTY_FILE_LOOKUP, mergeFileLookup } from "./types";
|
|
3
|
+
|
|
4
|
+
/** onebotv11:get_file / get_group_file_url / get_private_file_url */
|
|
5
|
+
export const onebotv11Platform: AgentPlatform = {
|
|
6
|
+
adapter: "onebotv11",
|
|
7
|
+
route: "onebotv11:message",
|
|
8
|
+
async resolveFile(bot, ref): Promise<PlatformFileLookup> {
|
|
9
|
+
const attempts: Array<[string, Record<string, unknown>]> = [
|
|
10
|
+
["get_file", { file_id: ref.fileId }],
|
|
11
|
+
];
|
|
12
|
+
if (ref.groupId) {
|
|
13
|
+
attempts.push([
|
|
14
|
+
"get_group_file_url",
|
|
15
|
+
{ group_id: ref.groupId, file_id: ref.fileId },
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
if (ref.userId) {
|
|
19
|
+
attempts.push([
|
|
20
|
+
"get_private_file_url",
|
|
21
|
+
{ user_id: ref.userId, file_id: ref.fileId },
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
const found: PlatformFileLookup = { sources: [], names: [] };
|
|
25
|
+
for (const [action, params] of attempts) {
|
|
26
|
+
try {
|
|
27
|
+
mergeFileLookup(
|
|
28
|
+
found,
|
|
29
|
+
await bot.sendApi<Record<string, unknown>>(action, params),
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
// 协议端不支持该 action 时继续尝试下一个
|
|
33
|
+
}
|
|
34
|
+
if (found.sources.length > 0) break;
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { EMPTY_FILE_LOOKUP };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentPlatform } from "./types";
|
|
2
|
+
import { EMPTY_FILE_LOOKUP } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* QQ 官方通道:附件事件自带公网 URL(见 adapter 的 segments.ts),
|
|
6
|
+
* 官方没有 file_id 换下载地址的公开接口,所以这里不做事。
|
|
7
|
+
*/
|
|
8
|
+
export const qqOfficialPlatform: AgentPlatform = {
|
|
9
|
+
adapter: "qq-official",
|
|
10
|
+
route: "qq-official:message",
|
|
11
|
+
async resolveFile() {
|
|
12
|
+
return EMPTY_FILE_LOOKUP;
|
|
13
|
+
},
|
|
14
|
+
};
|