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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Bot } from "mioku";
|
|
2
|
+
|
|
3
|
+
/** 需要按平台换取下载地址的附件引用 */
|
|
4
|
+
export interface PlatformFileRef {
|
|
5
|
+
fileId: string;
|
|
6
|
+
groupId?: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PlatformFileLookup {
|
|
11
|
+
sources: string[];
|
|
12
|
+
names: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const EMPTY_FILE_LOOKUP: PlatformFileLookup = { sources: [], names: [] };
|
|
16
|
+
|
|
17
|
+
/** 把平台返回的原始结果合并成下载来源(各平台实现共用) */
|
|
18
|
+
export const mergeFileLookup = (
|
|
19
|
+
target: PlatformFileLookup,
|
|
20
|
+
result: unknown,
|
|
21
|
+
): void => {
|
|
22
|
+
if (!result || typeof result !== "object") return;
|
|
23
|
+
const record = result as Record<string, unknown>;
|
|
24
|
+
for (const key of ["url", "file", "path"]) {
|
|
25
|
+
const value = record[key];
|
|
26
|
+
if (typeof value === "string" && value.trim()) {
|
|
27
|
+
target.sources.push(value.trim());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const base64 = record.base64 ?? record.data;
|
|
31
|
+
if (typeof base64 === "string" && base64.trim()) {
|
|
32
|
+
target.sources.push(`base64://${base64.trim()}`);
|
|
33
|
+
}
|
|
34
|
+
for (const key of ["file_name", "name"]) {
|
|
35
|
+
const value = record[key];
|
|
36
|
+
if (typeof value === "string" && value.trim()) {
|
|
37
|
+
target.names.push(value.trim());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 一个平台分支:自己订阅 `adapter:message` 路由,自己处理平台专有差异。
|
|
44
|
+
* 新增平台只要加一个文件并登记到 `platforms/index.ts`。
|
|
45
|
+
*/
|
|
46
|
+
export interface AgentPlatform {
|
|
47
|
+
/** 适配器名,与 `bot.adapter` 对应 */
|
|
48
|
+
readonly adapter: string;
|
|
49
|
+
/** 该分支订阅的事件路由,如 `onebotv11:message` */
|
|
50
|
+
readonly route: string;
|
|
51
|
+
/** 平台专有的 file_id → 下载来源;没有该能力时返回空 */
|
|
52
|
+
resolveFile(bot: Bot, ref: PlatformFileRef): Promise<PlatformFileLookup>;
|
|
53
|
+
}
|
package/tools/approval.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentPermissionLevel } from "../types";
|
|
|
2
2
|
|
|
3
3
|
export interface PendingApproval {
|
|
4
4
|
id: string;
|
|
5
|
-
userId:
|
|
5
|
+
userId: string;
|
|
6
6
|
command: string;
|
|
7
7
|
cwd: string;
|
|
8
8
|
level: AgentPermissionLevel;
|
|
@@ -18,7 +18,7 @@ interface PendingEntry extends PendingApproval {
|
|
|
18
18
|
|
|
19
19
|
export class ApprovalManager {
|
|
20
20
|
private pending = new Map<string, PendingEntry>();
|
|
21
|
-
private latestByUser = new Map<
|
|
21
|
+
private latestByUser = new Map<string, string>();
|
|
22
22
|
private seq = 0;
|
|
23
23
|
|
|
24
24
|
create(
|
|
@@ -55,7 +55,7 @@ export class ApprovalManager {
|
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
resolveLatest(userId:
|
|
58
|
+
resolveLatest(userId: string, approved: boolean): PendingApproval | null {
|
|
59
59
|
const id = this.latestByUser.get(userId) ?? this.latestFor(userId);
|
|
60
60
|
if (!id) return null;
|
|
61
61
|
const entry = this.pending.get(id);
|
|
@@ -65,13 +65,13 @@ export class ApprovalManager {
|
|
|
65
65
|
return approval;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
latestByUserId(userId:
|
|
68
|
+
latestByUserId(userId: string): PendingApproval | null {
|
|
69
69
|
const id = this.latestByUser.get(userId) ?? this.latestFor(userId);
|
|
70
70
|
const entry = id ? this.pending.get(id) : undefined;
|
|
71
71
|
return entry ? this.toPending(entry) : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
cancelByUser(userId:
|
|
74
|
+
cancelByUser(userId: string): number {
|
|
75
75
|
const ids = [...this.pending.values()]
|
|
76
76
|
.filter((entry) => entry.userId === userId)
|
|
77
77
|
.map((entry) => entry.id);
|
|
@@ -79,7 +79,7 @@ export class ApprovalManager {
|
|
|
79
79
|
return ids.length;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
listByUser(userId:
|
|
82
|
+
listByUser(userId: string): PendingApproval[] {
|
|
83
83
|
return [...this.pending.values()]
|
|
84
84
|
.filter((entry) => entry.userId === userId)
|
|
85
85
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
@@ -95,7 +95,7 @@ export class ApprovalManager {
|
|
|
95
95
|
this.latestByUser.clear();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
private latestFor(userId:
|
|
98
|
+
private latestFor(userId: string): string | undefined {
|
|
99
99
|
let latest: PendingEntry | undefined;
|
|
100
100
|
for (const entry of this.pending.values()) {
|
|
101
101
|
if (entry.userId !== userId) continue;
|
package/tools/bash.ts
CHANGED
package/tools/deliver.ts
CHANGED
package/tools/index.ts
CHANGED
|
@@ -25,7 +25,10 @@ import { assessCommandRisk } from "../core/risk";
|
|
|
25
25
|
import type { TurnActivity } from "../core/activity";
|
|
26
26
|
|
|
27
27
|
export interface TurnToolOptions {
|
|
28
|
-
|
|
28
|
+
/** 会话/工作区隔离键(适配器 + 平台用户 id) */
|
|
29
|
+
userId: string;
|
|
30
|
+
/** 平台原始用户 id,用于发送消息 */
|
|
31
|
+
sendUserId: string;
|
|
29
32
|
bot: Bot | undefined;
|
|
30
33
|
runId: number;
|
|
31
34
|
reporter: BashReporter;
|
|
@@ -120,13 +123,13 @@ export function buildTurnTools(
|
|
|
120
123
|
createSendFileTool({
|
|
121
124
|
ctx: host.ctx,
|
|
122
125
|
bot: options.bot,
|
|
123
|
-
userId: options.
|
|
126
|
+
userId: options.sendUserId,
|
|
124
127
|
policy,
|
|
125
128
|
}),
|
|
126
129
|
createSendImageTool({
|
|
127
130
|
ctx: host.ctx,
|
|
128
131
|
bot: options.bot,
|
|
129
|
-
userId: options.
|
|
132
|
+
userId: options.sendUserId,
|
|
130
133
|
policy,
|
|
131
134
|
}),
|
|
132
135
|
];
|
|
@@ -175,6 +178,7 @@ export function buildTurnTools(
|
|
|
175
178
|
createTodoTool({
|
|
176
179
|
host,
|
|
177
180
|
userId: options.userId,
|
|
181
|
+
sendUserId: options.sendUserId,
|
|
178
182
|
bot: options.bot,
|
|
179
183
|
quiet: isQuietMode(policy.level),
|
|
180
184
|
}),
|
package/tools/perm.ts
CHANGED
|
@@ -19,12 +19,16 @@ export function normalizePermissionLevel(value: unknown): AgentPermissionLevel {
|
|
|
19
19
|
return "workspace-write";
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** 作用域键里可能含 `:`(适配器前缀),统一替换成目录安全字符 */
|
|
23
|
+
const sanitizeSegment = (value: string): string =>
|
|
24
|
+
String(value ?? "").replace(/[^A-Za-z0-9._-]+/g, "_") || "unknown";
|
|
25
|
+
|
|
22
26
|
export interface FsPolicy {
|
|
23
27
|
level: AgentPermissionLevel;
|
|
24
28
|
workspaceRoot: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
export function workspaceRootFor(baseDir: string, userId:
|
|
31
|
+
export function workspaceRootFor(baseDir: string, userId: string): string {
|
|
28
32
|
const raw = String(baseDir ?? "").trim();
|
|
29
33
|
const base =
|
|
30
34
|
raw && path.isAbsolute(raw)
|
|
@@ -33,7 +37,7 @@ export function workspaceRootFor(baseDir: string, userId: number): string {
|
|
|
33
37
|
process.cwd(),
|
|
34
38
|
raw || path.join("data", "agent", "workspace"),
|
|
35
39
|
);
|
|
36
|
-
return path.resolve(base,
|
|
40
|
+
return path.resolve(base, sanitizeSegment(userId));
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export function resolveWorkspacePath(policy: FsPolicy, target: string): string {
|
package/tools/todo.ts
CHANGED
|
@@ -38,12 +38,15 @@ export function formatPlanText(items: SessionPlanItem[]): string {
|
|
|
38
38
|
|
|
39
39
|
export function createTodoTool(options: {
|
|
40
40
|
host: AgentHost;
|
|
41
|
-
|
|
41
|
+
/** 会话隔离键 */
|
|
42
|
+
userId: string;
|
|
43
|
+
/** 平台原始用户 id,用于推送 */
|
|
44
|
+
sendUserId: string;
|
|
42
45
|
bot: Bot | undefined;
|
|
43
46
|
/** yolo 模式:不推送清单,用户只看最终回复 */
|
|
44
47
|
quiet?: boolean;
|
|
45
48
|
}): AITool {
|
|
46
|
-
const { host, userId, bot, quiet } = options;
|
|
49
|
+
const { host, userId, sendUserId, bot, quiet } = options;
|
|
47
50
|
return {
|
|
48
51
|
name: "todo_write",
|
|
49
52
|
description: DESCRIPTION,
|
|
@@ -114,7 +117,7 @@ export function createTodoTool(options: {
|
|
|
114
117
|
if (bot && !quiet) {
|
|
115
118
|
await bot
|
|
116
119
|
.sendMessage(
|
|
117
|
-
{ type: "private", user_id:
|
|
120
|
+
{ type: "private", user_id: sendUserId },
|
|
118
121
|
[host.ctx.segment.text(formatPlanText(items))],
|
|
119
122
|
)
|
|
120
123
|
.catch((err) =>
|
package/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type AgentPermissionLevel =
|
|
|
19
19
|
|
|
20
20
|
export interface AgentAccessConfig {
|
|
21
21
|
allowAdmins: boolean;
|
|
22
|
-
users:
|
|
22
|
+
users: string[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface AgentBaseConfig {
|
|
@@ -104,8 +104,8 @@ export interface AgentHost {
|
|
|
104
104
|
getSettings(): AgentSettingsConfig;
|
|
105
105
|
getChatShared(): Promise<ChatSharedConfig>;
|
|
106
106
|
resolveModel(): ResolvedModel | null;
|
|
107
|
-
workspaceRoot(userId:
|
|
108
|
-
isAllowed(userId:
|
|
107
|
+
workspaceRoot(userId: string): string;
|
|
108
|
+
isAllowed(userId: string): Promise<boolean>;
|
|
109
109
|
updateBase(patch: Partial<AgentBaseConfig>): Promise<void>;
|
|
110
110
|
logger: MiokuContext["logger"];
|
|
111
111
|
}
|