mioku-plugin-agent 0.1.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/LICENSE +21 -0
- package/README.md +58 -0
- package/commands/index.ts +603 -0
- package/configs/base.ts +11 -0
- package/configs/context-window.ts +15 -0
- package/configs/settings.ts +39 -0
- package/core/activity.ts +236 -0
- package/core/attachment.ts +50 -0
- package/core/chat-config.ts +24 -0
- package/core/compaction.ts +97 -0
- package/core/download.ts +340 -0
- package/core/emotion.ts +47 -0
- package/core/loop.ts +597 -0
- package/core/media.ts +170 -0
- package/core/prompt.ts +214 -0
- package/core/risk.ts +61 -0
- package/core/send.ts +131 -0
- package/core/session.ts +58 -0
- package/core/title.ts +52 -0
- package/core/units.ts +208 -0
- package/db.ts +464 -0
- package/handlers/message.ts +13 -0
- package/index.ts +212 -0
- package/package.json +28 -0
- package/tools/approval.ts +119 -0
- package/tools/bash.ts +280 -0
- package/tools/deliver.ts +75 -0
- package/tools/fs.ts +296 -0
- package/tools/index.ts +194 -0
- package/tools/perm.ts +87 -0
- package/tools/todo.ts +136 -0
- package/tools/view-image.ts +85 -0
- package/tools/web.ts +133 -0
- package/tsconfig.json +7 -0
- package/types.ts +111 -0
- package/utils/config.ts +31 -0
- package/utils/json.ts +5 -0
package/tools/todo.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { AITool, Bot } from "mioku";
|
|
2
|
+
import type { AgentHost } from "../types";
|
|
3
|
+
import type { SessionPlanItem, SessionPlanStatus } from "../db";
|
|
4
|
+
|
|
5
|
+
const STATUSES: readonly SessionPlanStatus[] = [
|
|
6
|
+
"pending",
|
|
7
|
+
"in_progress",
|
|
8
|
+
"completed",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const MAX_ITEMS = 50;
|
|
12
|
+
|
|
13
|
+
const MARKER: Record<SessionPlanStatus, string> = {
|
|
14
|
+
pending: "[ ]",
|
|
15
|
+
in_progress: "[~]",
|
|
16
|
+
completed: "[x]",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DESCRIPTION =
|
|
20
|
+
"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). " +
|
|
21
|
+
"Use it to plan multi-step work and show progress: add one todo per concrete step before you start. " +
|
|
22
|
+
"Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. " +
|
|
23
|
+
"Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. " +
|
|
24
|
+
"Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). " +
|
|
25
|
+
"Every update is pushed to the user in the chat, so keep items short and imperative.";
|
|
26
|
+
|
|
27
|
+
export function formatPlanText(items: SessionPlanItem[]): string {
|
|
28
|
+
const count = (status: SessionPlanStatus) =>
|
|
29
|
+
items.filter((item) => item.status === status).length;
|
|
30
|
+
const summary = `${count("in_progress")} 进行中 · ${count("pending")} 待办 · ${count("completed")} 已完成`;
|
|
31
|
+
return [
|
|
32
|
+
`📋 任务清单已更新(${summary})`,
|
|
33
|
+
...items.map(
|
|
34
|
+
(item, index) => `${MARKER[item.status]} ${index + 1}. ${item.content}`,
|
|
35
|
+
),
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createTodoTool(options: {
|
|
40
|
+
host: AgentHost;
|
|
41
|
+
userId: number;
|
|
42
|
+
bot: Bot | undefined;
|
|
43
|
+
/** yolo 模式:不推送清单,用户只看最终回复 */
|
|
44
|
+
quiet?: boolean;
|
|
45
|
+
}): AITool {
|
|
46
|
+
const { host, userId, bot, quiet } = options;
|
|
47
|
+
return {
|
|
48
|
+
name: "todo_write",
|
|
49
|
+
description: DESCRIPTION,
|
|
50
|
+
parameters: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
todos: {
|
|
54
|
+
type: "array",
|
|
55
|
+
description: "The COMPLETE task list, replacing any previous list",
|
|
56
|
+
items: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
content: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "What the task is — a short imperative line",
|
|
62
|
+
},
|
|
63
|
+
status: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "pending (not started) | in_progress (now) | completed (done)",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ["content", "status"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ["todos"],
|
|
73
|
+
},
|
|
74
|
+
handler: async (args: Record<string, unknown>) => {
|
|
75
|
+
const raw = args?.todos;
|
|
76
|
+
if (!Array.isArray(raw)) return { error: "todos must be an array" };
|
|
77
|
+
|
|
78
|
+
const items: SessionPlanItem[] = [];
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
for (const entry of raw) {
|
|
81
|
+
if (!entry || typeof entry !== "object") {
|
|
82
|
+
return { error: "each todo must be an object with content and status" };
|
|
83
|
+
}
|
|
84
|
+
const record = entry as Record<string, unknown>;
|
|
85
|
+
const content = String(record.content ?? "").trim();
|
|
86
|
+
if (!content) return { error: "todo content must be a non-empty string" };
|
|
87
|
+
if (seen.has(content)) {
|
|
88
|
+
return { error: `duplicate todo content: ${content}` };
|
|
89
|
+
}
|
|
90
|
+
seen.add(content);
|
|
91
|
+
const status = String(record.status ?? "").trim() as SessionPlanStatus;
|
|
92
|
+
if (!STATUSES.includes(status)) {
|
|
93
|
+
return {
|
|
94
|
+
error: `invalid status "${record.status}": use pending | in_progress | completed`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
items.push({ content, status });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (items.length > MAX_ITEMS) {
|
|
101
|
+
return { error: `too many items (max ${MAX_ITEMS})` };
|
|
102
|
+
}
|
|
103
|
+
const inProgress = items.filter((item) => item.status === "in_progress");
|
|
104
|
+
if (inProgress.length > 1) {
|
|
105
|
+
return {
|
|
106
|
+
error:
|
|
107
|
+
"at most one todo may be in_progress; mark finished work completed before starting the next",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const session = host.sessions.get(userId);
|
|
112
|
+
host.db.setSessionMeta(session.sessionId, { plan: items });
|
|
113
|
+
|
|
114
|
+
if (bot && !quiet) {
|
|
115
|
+
await bot
|
|
116
|
+
.sendMessage(
|
|
117
|
+
{ type: "private", user_id: userId },
|
|
118
|
+
[host.ctx.segment.text(formatPlanText(items))],
|
|
119
|
+
)
|
|
120
|
+
.catch((err) =>
|
|
121
|
+
host.logger.warn(`[agent] failed to push todo update: ${err}`),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
todos: items,
|
|
128
|
+
counts: {
|
|
129
|
+
pending: items.filter((item) => item.status === "pending").length,
|
|
130
|
+
inProgress: inProgress.length,
|
|
131
|
+
completed: items.filter((item) => item.status === "completed").length,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AITool } from "mioku";
|
|
5
|
+
import { TOOL_RESULT_FOLLOWUP_KEY } from "mioku";
|
|
6
|
+
import { readImageDataUrl } from "../core/download";
|
|
7
|
+
import type { FsPolicy } from "./perm";
|
|
8
|
+
import { resolveWorkspacePath } from "./perm";
|
|
9
|
+
|
|
10
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"];
|
|
12
|
+
|
|
13
|
+
export interface ViewImageDeps {
|
|
14
|
+
policy: FsPolicy;
|
|
15
|
+
describeImage?: (filePath: string) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createViewImageTool(deps: ViewImageDeps): AITool {
|
|
19
|
+
return {
|
|
20
|
+
name: "view_image",
|
|
21
|
+
description:
|
|
22
|
+
"View a local image file (png/jpeg/webp/gif/bmp) such as a screenshot or a downloaded photo. " +
|
|
23
|
+
(deps.describeImage
|
|
24
|
+
? "The image is sent to the vision model and its description is returned."
|
|
25
|
+
: "The image is attached to the conversation so you can inspect it directly."),
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
file_path: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Image path (absolute, or relative to the workspace)",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["file_path"],
|
|
35
|
+
},
|
|
36
|
+
handler: async (args: Record<string, unknown>) => {
|
|
37
|
+
const filePath = resolveWorkspacePath(
|
|
38
|
+
deps.policy,
|
|
39
|
+
String(args?.file_path ?? ""),
|
|
40
|
+
);
|
|
41
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
42
|
+
if (ext && !IMAGE_EXTENSIONS.includes(ext)) {
|
|
43
|
+
return {
|
|
44
|
+
error: `unsupported image type "${ext}": use ${IMAGE_EXTENSIONS.join(", ")}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let stat: fs.Stats;
|
|
49
|
+
try {
|
|
50
|
+
stat = await fsp.stat(filePath);
|
|
51
|
+
} catch {
|
|
52
|
+
return { error: `File not found: ${filePath}` };
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isFile()) return { error: `Not a regular file: ${filePath}` };
|
|
55
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
56
|
+
return {
|
|
57
|
+
error: `Image too large (${stat.size} bytes, max ${MAX_IMAGE_BYTES})`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (deps.describeImage) {
|
|
62
|
+
const description = await deps
|
|
63
|
+
.describeImage(filePath)
|
|
64
|
+
.catch((err) => `describe failed: ${err}`);
|
|
65
|
+
return { file: filePath, size: stat.size, description };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let dataUrl: string;
|
|
69
|
+
try {
|
|
70
|
+
dataUrl = await readImageDataUrl(filePath);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { error: `Failed to read image: ${err}` };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
file: filePath,
|
|
76
|
+
size: stat.size,
|
|
77
|
+
seen: true,
|
|
78
|
+
[TOOL_RESULT_FOLLOWUP_KEY]: {
|
|
79
|
+
text: `Image attached from ${filePath}.`,
|
|
80
|
+
images: [{ url: dataUrl }],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/tools/web.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { AITool } from "mioku";
|
|
2
|
+
import type { AgentSettingsConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
interface WebToolDeps {
|
|
5
|
+
settings: AgentSettingsConfig;
|
|
6
|
+
onSearch?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SearxngResult {
|
|
10
|
+
title?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function htmlToText(html: string): string {
|
|
16
|
+
return html
|
|
17
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
18
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
19
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
|
|
20
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
21
|
+
.replace(/<\/(p|div|li|h[1-6]|tr|section|article|blockquote|pre)>/gi, "\n")
|
|
22
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
23
|
+
.replace(/<li[^>]*>/gi, "- ")
|
|
24
|
+
.replace(/<[^>]+>/g, " ")
|
|
25
|
+
.replace(/ /g, " ")
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, "'")
|
|
31
|
+
.replace(/[ \t]+/g, " ")
|
|
32
|
+
.replace(/\n\s*\n\s*\n+/g, "\n\n")
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractTitle(html: string): string {
|
|
37
|
+
const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
38
|
+
return match ? match[1].trim().slice(0, 200) : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createWebSearchTool(deps: WebToolDeps): AITool {
|
|
42
|
+
const { settings, onSearch } = deps;
|
|
43
|
+
return {
|
|
44
|
+
name: "web_search",
|
|
45
|
+
description:
|
|
46
|
+
"Search the web via SearXNG and return a list of results (title, url, snippet).",
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
query: { type: "string", description: "Search query" },
|
|
51
|
+
limit: { type: "number", description: `Max results (default ${settings.webSearch.defaultLimit})` },
|
|
52
|
+
},
|
|
53
|
+
required: ["query"],
|
|
54
|
+
},
|
|
55
|
+
handler: async (args) => {
|
|
56
|
+
const query = String(args?.query ?? "").trim();
|
|
57
|
+
if (!query) return { error: "query must be a non-empty string" };
|
|
58
|
+
if (!settings.webSearch.enabled) {
|
|
59
|
+
return { error: "web search is disabled in agent settings" };
|
|
60
|
+
}
|
|
61
|
+
onSearch?.();
|
|
62
|
+
const limit = Math.min(
|
|
63
|
+
Math.max(1, Math.floor(Number(args?.limit) || settings.webSearch.defaultLimit)),
|
|
64
|
+
settings.webSearch.maxLimit,
|
|
65
|
+
);
|
|
66
|
+
const url = new URL(settings.webSearch.baseUrl);
|
|
67
|
+
url.searchParams.set("q", query);
|
|
68
|
+
url.searchParams.set("format", "json");
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
signal: AbortSignal.timeout(settings.webSearch.timeoutMs),
|
|
71
|
+
headers: { Accept: "application/json" },
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
return { error: `Search request failed: HTTP ${response.status}` };
|
|
75
|
+
}
|
|
76
|
+
const data = (await response.json()) as { results?: SearxngResult[] };
|
|
77
|
+
const results = (data.results ?? []).slice(0, limit).map((item) => ({
|
|
78
|
+
title: item.title ?? "",
|
|
79
|
+
url: item.url ?? "",
|
|
80
|
+
snippet: item.content ?? "",
|
|
81
|
+
}));
|
|
82
|
+
return { query, count: results.length, results };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createWebFetchTool(deps: WebToolDeps): AITool {
|
|
88
|
+
const { settings } = deps;
|
|
89
|
+
return {
|
|
90
|
+
name: "web_fetch",
|
|
91
|
+
description:
|
|
92
|
+
"Fetch a web page URL and return the extracted main text content (HTML converted to plain text).",
|
|
93
|
+
parameters: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
url: { type: "string", description: "HTTP(S) URL to fetch" },
|
|
97
|
+
},
|
|
98
|
+
required: ["url"],
|
|
99
|
+
},
|
|
100
|
+
handler: async (args) => {
|
|
101
|
+
const target = String(args?.url ?? "").trim();
|
|
102
|
+
if (!/^https?:\/\//i.test(target)) {
|
|
103
|
+
return { error: "url must start with http:// or https://" };
|
|
104
|
+
}
|
|
105
|
+
if (!settings.webFetch.enabled) {
|
|
106
|
+
return { error: "web fetch is disabled in agent settings" };
|
|
107
|
+
}
|
|
108
|
+
const response = await fetch(target, {
|
|
109
|
+
signal: AbortSignal.timeout(settings.webFetch.timeoutMs),
|
|
110
|
+
headers: {
|
|
111
|
+
"User-Agent":
|
|
112
|
+
"Mozilla/5.0 (compatible; mioku-agent/0.1; +https://github.com/mioku-lab)",
|
|
113
|
+
Accept: "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.5",
|
|
114
|
+
},
|
|
115
|
+
redirect: "follow",
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
return { error: `Fetch failed: HTTP ${response.status}` };
|
|
119
|
+
}
|
|
120
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
121
|
+
const raw = await response.text();
|
|
122
|
+
if (contentType.includes("text/plain")) {
|
|
123
|
+
return { url: target, contentType, content: raw.slice(0, settings.webFetch.maxChars) };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
url: target,
|
|
127
|
+
contentType,
|
|
128
|
+
title: extractTitle(raw),
|
|
129
|
+
content: htmlToText(raw).slice(0, settings.webFetch.maxChars),
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/tsconfig.json
ADDED
package/types.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIInstance,
|
|
3
|
+
AIService,
|
|
4
|
+
ConfigService,
|
|
5
|
+
MiokuContext,
|
|
6
|
+
ScreenshotService,
|
|
7
|
+
} from "mioku";
|
|
8
|
+
import type { AgentDatabase } from "./db";
|
|
9
|
+
import type { SessionManager } from "./core/session";
|
|
10
|
+
import type { EmotionManager } from "./core/emotion";
|
|
11
|
+
import type { ApprovalManager } from "./tools/approval";
|
|
12
|
+
|
|
13
|
+
export type AgentPermissionLevel =
|
|
14
|
+
| "read-only"
|
|
15
|
+
| "workspace-write"
|
|
16
|
+
| "auto"
|
|
17
|
+
| "full"
|
|
18
|
+
| "yolo";
|
|
19
|
+
|
|
20
|
+
export interface AgentAccessConfig {
|
|
21
|
+
allowAdmins: boolean;
|
|
22
|
+
users: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentBaseConfig {
|
|
26
|
+
access: AgentAccessConfig;
|
|
27
|
+
workspaceDir: string;
|
|
28
|
+
permissionLevel: AgentPermissionLevel;
|
|
29
|
+
model: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompactionConfig {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
keepRecentMessages: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WebSearchConfig {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
defaultLimit: number;
|
|
42
|
+
maxLimit: number;
|
|
43
|
+
maxSearchCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WebFetchConfig {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
maxChars: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BashConfig {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
timeoutMs: number;
|
|
55
|
+
approvalTimeoutMs: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AgentSettingsConfig {
|
|
59
|
+
maxIterations: number;
|
|
60
|
+
temperature: number;
|
|
61
|
+
maxContextTokens: number;
|
|
62
|
+
stream: boolean;
|
|
63
|
+
enableMarkdownScreenshot: boolean;
|
|
64
|
+
compaction: CompactionConfig;
|
|
65
|
+
webSearch: WebSearchConfig;
|
|
66
|
+
webFetch: WebFetchConfig;
|
|
67
|
+
bash: BashConfig;
|
|
68
|
+
dataCollection: { enabled: boolean };
|
|
69
|
+
debug: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ChatEmotionConfig {
|
|
73
|
+
defaultEmotion: string;
|
|
74
|
+
emotions: Record<string, { examples: string[] }>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ChatSharedConfig {
|
|
78
|
+
persona: string;
|
|
79
|
+
replyStyle: string;
|
|
80
|
+
emotion: ChatEmotionConfig | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ResolvedModel {
|
|
84
|
+
instance: AIInstance;
|
|
85
|
+
model: string;
|
|
86
|
+
working: AIInstance;
|
|
87
|
+
workingModel: string;
|
|
88
|
+
vision: AIInstance | undefined;
|
|
89
|
+
visionModel: string;
|
|
90
|
+
isMultimodal: boolean;
|
|
91
|
+
contextWindow: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AgentHost {
|
|
95
|
+
ctx: MiokuContext;
|
|
96
|
+
aiService: AIService;
|
|
97
|
+
configService: ConfigService | undefined;
|
|
98
|
+
screenshot: ScreenshotService | undefined;
|
|
99
|
+
db: AgentDatabase;
|
|
100
|
+
approvals: ApprovalManager;
|
|
101
|
+
sessions: SessionManager;
|
|
102
|
+
emotions: EmotionManager;
|
|
103
|
+
getBase(): AgentBaseConfig;
|
|
104
|
+
getSettings(): AgentSettingsConfig;
|
|
105
|
+
getChatShared(): Promise<ChatSharedConfig>;
|
|
106
|
+
resolveModel(): ResolvedModel | null;
|
|
107
|
+
workspaceRoot(userId: number): string;
|
|
108
|
+
isAllowed(userId: number): Promise<boolean>;
|
|
109
|
+
updateBase(patch: Partial<AgentBaseConfig>): Promise<void>;
|
|
110
|
+
logger: MiokuContext["logger"];
|
|
111
|
+
}
|
package/utils/config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function isConfigObject(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function mergeAgentConfig<T extends object>(
|
|
6
|
+
defaults: T,
|
|
7
|
+
overrides: Record<string, unknown>,
|
|
8
|
+
): T {
|
|
9
|
+
const result: Record<string, unknown> = {};
|
|
10
|
+
for (const [key, value] of Object.entries(defaults as Record<string, unknown>)) {
|
|
11
|
+
if (isConfigObject(value)) {
|
|
12
|
+
result[key] = mergeAgentConfig(value, {});
|
|
13
|
+
} else if (Array.isArray(value)) {
|
|
14
|
+
result[key] = [...value];
|
|
15
|
+
} else {
|
|
16
|
+
result[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
20
|
+
if (value === undefined || value === null) continue;
|
|
21
|
+
if (isConfigObject(value)) {
|
|
22
|
+
const baseValue = isConfigObject(result[key]) ? result[key] : {};
|
|
23
|
+
result[key] = mergeAgentConfig(baseValue, value);
|
|
24
|
+
} else if (Array.isArray(value)) {
|
|
25
|
+
result[key] = [...value];
|
|
26
|
+
} else {
|
|
27
|
+
result[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result as T;
|
|
31
|
+
}
|