mioku-plugin-agent 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/core/model.ts +179 -0
  2. package/index.ts +34 -70
  3. package/package.json +1 -1
package/core/model.ts ADDED
@@ -0,0 +1,179 @@
1
+ import type { AIInstance, AIModelRole, AIService } from "mioku";
2
+ import type { ResolvedModel } from "../types";
3
+
4
+ const OVERRIDE_INSTANCE_PREFIX = "__agent_override_";
5
+
6
+ export interface ModelOverride {
7
+ /** 覆盖模型的全 id(providerId/modelId) */
8
+ key: string;
9
+ instanceName: string;
10
+ }
11
+
12
+ export function splitModelFullId(
13
+ fullId: string,
14
+ ): { providerId: string; modelId: string } | null {
15
+ const raw = String(fullId ?? "").trim();
16
+ const index = raw.indexOf("/");
17
+ if (index <= 0 || index >= raw.length - 1) return null;
18
+ return { providerId: raw.slice(0, index), modelId: raw.slice(index + 1) };
19
+ }
20
+
21
+ function instanceName(instance: AIInstance | undefined): string | undefined {
22
+ const name = (instance as { name?: unknown } | undefined)?.name;
23
+ return typeof name === "string" && name ? name : undefined;
24
+ }
25
+
26
+ /** 在现有实例里找跑在指定提供商上的实例,同模型优先。 */
27
+ export function findInstanceByProvider(
28
+ aiService: AIService,
29
+ providerId: string,
30
+ modelId?: string,
31
+ ): { name: string; instance: AIInstance } | undefined {
32
+ if (!providerId) return undefined;
33
+ const infos = aiService.listInstances?.() ?? [];
34
+ const info =
35
+ (modelId
36
+ ? infos.find(
37
+ (item) => item.providerId === providerId && item.modelId === modelId,
38
+ )
39
+ : undefined) ?? infos.find((item) => item.providerId === providerId);
40
+ if (!info) return undefined;
41
+ const instance = aiService.get?.(info.name);
42
+ return instance ? { name: info.name, instance } : undefined;
43
+ }
44
+
45
+ function overrideInstanceName(providerId: string, modelId: string): string {
46
+ const slug = `${providerId}_${modelId}`.replace(/[^A-Za-z0-9_.-]+/g, "_");
47
+ return `${OVERRIDE_INSTANCE_PREFIX}${slug}`;
48
+ }
49
+
50
+ /**
51
+ * 覆盖模型必须跑在它自己的提供商上:实例固定绑定 provider,只改 model 会把别的
52
+ * 提供商的模型名发给主提供商的 client。优先复用现成实例,否则建一个隐藏实例。
53
+ */
54
+ export async function prepareModelOverride(
55
+ aiService: AIService,
56
+ fullId: string,
57
+ warn: (message: string) => void,
58
+ ): Promise<ModelOverride | undefined> {
59
+ const parsed = splitModelFullId(fullId);
60
+ if (!parsed) return undefined;
61
+ const reused = findInstanceByProvider(
62
+ aiService,
63
+ parsed.providerId,
64
+ parsed.modelId,
65
+ );
66
+ if (reused) return { key: fullId, instanceName: reused.name };
67
+ const name = overrideInstanceName(parsed.providerId, parsed.modelId);
68
+ if (aiService.get?.(name)) return { key: fullId, instanceName: name };
69
+ if (!aiService.createInstance) {
70
+ warn(`覆盖模型 ${fullId} 无法切换提供商:AI 服务不支持 createInstance`);
71
+ return undefined;
72
+ }
73
+ try {
74
+ await aiService.createInstance({
75
+ name,
76
+ providerId: parsed.providerId,
77
+ modelId: parsed.modelId,
78
+ });
79
+ return { key: fullId, instanceName: name };
80
+ } catch (err) {
81
+ warn(`覆盖模型 ${fullId} 实例创建失败:${err}`);
82
+ return undefined;
83
+ }
84
+ }
85
+
86
+ export function resolveAgentModel(options: {
87
+ aiService: AIService;
88
+ overrideFullId: string;
89
+ override?: ModelOverride;
90
+ }): ResolvedModel | null {
91
+ const { aiService, overrideFullId, override } = options;
92
+ const getByRole = (role: AIModelRole) =>
93
+ aiService.getInstanceByRole?.(role) ?? aiService.get?.(role);
94
+ const main = getByRole("main") ?? aiService.getDefault?.();
95
+ if (!main) return null;
96
+
97
+ const bindings = aiService.getRoleBindings?.() ?? {
98
+ main: undefined,
99
+ working: undefined,
100
+ vision: undefined,
101
+ };
102
+ const instances = aiService.listInstances?.() ?? [];
103
+ const models = aiService.listModels?.() ?? [];
104
+
105
+ const modelIdOf = (
106
+ full: string | undefined,
107
+ instance: AIInstance,
108
+ ): string => {
109
+ if (full && full.includes("/")) return full.split("/").slice(1).join("/");
110
+ const name = instanceName(instance);
111
+ const info = instances.find(
112
+ (item) => item.role === name || item.name === name,
113
+ );
114
+ return info?.modelId ?? "";
115
+ };
116
+
117
+ const overrideDesc = overrideFullId
118
+ ? models.find((item) => item.id === overrideFullId)
119
+ : undefined;
120
+ const overrideParsed = splitModelFullId(overrideFullId);
121
+
122
+ let instance = main;
123
+ let model = modelIdOf(bindings.main, main);
124
+ if (overrideFullId && overrideParsed) {
125
+ const target =
126
+ (override && override.key === overrideFullId
127
+ ? aiService.get?.(override.instanceName)
128
+ : undefined) ??
129
+ findInstanceByProvider(
130
+ aiService,
131
+ overrideParsed.providerId,
132
+ overrideParsed.modelId,
133
+ )?.instance;
134
+ if (target) {
135
+ // 覆盖模型的提供商可达:用它的实例 + 它的模型
136
+ instance = target;
137
+ model = overrideDesc?.modelId ?? overrideParsed.modelId;
138
+ }
139
+ // 提供商不可达时退回主模型,绝不把别家的模型名发给主提供商的 client
140
+ }
141
+
142
+ let working = getByRole("working") ?? main;
143
+ let workingModel = modelIdOf(bindings.working, working);
144
+ if (!workingModel) {
145
+ // 没有独立绑定的干活模型:跟随主模型,实例也要跟着换,否则模型与提供商错配
146
+ working = instance;
147
+ workingModel = model;
148
+ }
149
+
150
+ let vision = getByRole("vision") ?? working;
151
+ let visionModel = modelIdOf(bindings.vision, vision);
152
+ if (!visionModel) {
153
+ vision = working;
154
+ visionModel = workingModel;
155
+ }
156
+
157
+ const visionDesc =
158
+ models.find((item) => item.id === bindings.vision) ||
159
+ models.find((item) => item.modelId === visionModel);
160
+ // 有覆盖时,直接吃图片的是覆盖模型,能力要以它为准
161
+ const isMultimodal = overrideDesc
162
+ ? (overrideDesc.capabilities?.includes("vision") ?? false)
163
+ : (visionDesc?.capabilities?.includes("vision") ?? Boolean(visionModel));
164
+ const mainDesc =
165
+ overrideDesc ||
166
+ models.find((item) => item.id === bindings.main) ||
167
+ models.find((item) => item.modelId === model);
168
+
169
+ return {
170
+ instance,
171
+ model,
172
+ working,
173
+ workingModel,
174
+ vision,
175
+ visionModel,
176
+ isMultimodal,
177
+ contextWindow: mainDesc?.contextWindow ?? 0,
178
+ };
179
+ }
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { definePlugin, getService, Services } from "mioku";
2
- import type { AIInstance, AIModelRole, MiokuContext } from "mioku";
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";
@@ -9,6 +9,11 @@ import { createMessageHandler } from "./handlers/message";
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 {
@@ -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
- const getByRole = (role: AIModelRole) =>
94
- aiService.getInstanceByRole?.(role) ?? aiService.get?.(role);
95
- const main = getByRole("main") ?? aiService.getDefault?.();
96
- if (!main) return null;
97
- const working = getByRole("working") ?? main;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "超级用户 Agent 插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",