evolclaw 2.8.3 → 3.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/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createShortConnection } from '../rpc/index.js';
|
|
2
|
+
import { uploadFileAndBuildPayload } from './upload.js';
|
|
3
|
+
export async function msgSend(args) {
|
|
4
|
+
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
5
|
+
try {
|
|
6
|
+
let payload;
|
|
7
|
+
switch (args.body.mode) {
|
|
8
|
+
case 'text':
|
|
9
|
+
payload = { type: 'text', text: args.body.text };
|
|
10
|
+
break;
|
|
11
|
+
case 'payload':
|
|
12
|
+
payload = args.body.payload;
|
|
13
|
+
break;
|
|
14
|
+
case 'link':
|
|
15
|
+
payload = { type: 'link', url: args.body.url };
|
|
16
|
+
if (args.body.title)
|
|
17
|
+
payload.title = args.body.title;
|
|
18
|
+
if (args.body.description)
|
|
19
|
+
payload.description = args.body.description;
|
|
20
|
+
break;
|
|
21
|
+
case 'file': {
|
|
22
|
+
const built = await uploadFileAndBuildPayload(conn, args.from, args.body.filePath, {
|
|
23
|
+
as: args.body.as,
|
|
24
|
+
contentType: args.body.contentType,
|
|
25
|
+
text: args.body.text,
|
|
26
|
+
transcript: args.body.transcript,
|
|
27
|
+
});
|
|
28
|
+
payload = built.payload;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const sendParams = { to: args.to, payload };
|
|
33
|
+
// Default: plaintext. Set encrypt: true to enable E2EE.
|
|
34
|
+
sendParams.encrypt = args.encrypt === true;
|
|
35
|
+
const result = await conn.call('message.send', sendParams);
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
message_id: result?.message_id,
|
|
39
|
+
seq: result?.seq,
|
|
40
|
+
timestamp: result?.timestamp,
|
|
41
|
+
status: result?.status,
|
|
42
|
+
delivery_mode: result?.delivery_mode,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return formatRpcError(e);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await conn.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function msgPull(args) {
|
|
53
|
+
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
54
|
+
try {
|
|
55
|
+
const params = {};
|
|
56
|
+
if (args.afterSeq !== undefined)
|
|
57
|
+
params.after_seq = args.afterSeq;
|
|
58
|
+
if (args.limit !== undefined)
|
|
59
|
+
params.limit = args.limit;
|
|
60
|
+
const result = await conn.call('message.pull', params);
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
messages: result?.messages ?? [],
|
|
64
|
+
count: result?.count ?? 0,
|
|
65
|
+
latest_seq: result?.latest_seq ?? 0,
|
|
66
|
+
earliest_available_seq: result?.earliest_available_seq ?? null,
|
|
67
|
+
ephemeral_earliest_available_seq: result?.ephemeral_earliest_available_seq ?? null,
|
|
68
|
+
ephemeral_dropped_count: result?.ephemeral_dropped_count ?? 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return formatRpcError(e);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await conn.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function msgAck(args) {
|
|
79
|
+
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
80
|
+
try {
|
|
81
|
+
const result = await conn.call('message.ack', { seq: args.seq });
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
ack_seq: result?.ack_seq ?? args.seq,
|
|
85
|
+
event_published: result?.event_published,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
return formatRpcError(e);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await conn.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function msgRecall(args) {
|
|
96
|
+
if (args.messageIds.length === 0) {
|
|
97
|
+
return { ok: false, error: 'message_ids 不能为空' };
|
|
98
|
+
}
|
|
99
|
+
if (args.messageIds.length > 100) {
|
|
100
|
+
return { ok: false, error: 'message_ids 最多 100 个' };
|
|
101
|
+
}
|
|
102
|
+
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
103
|
+
try {
|
|
104
|
+
const result = await conn.call('message.recall', { message_ids: args.messageIds });
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
accepted: result?.accepted ?? 0,
|
|
108
|
+
recalled: result?.recalled ?? 0,
|
|
109
|
+
errors: result?.errors ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
return formatRpcError(e);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await conn.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function msgOnline(args) {
|
|
120
|
+
if (args.targets.length === 0) {
|
|
121
|
+
return { ok: false, error: '查询目标不能为空' };
|
|
122
|
+
}
|
|
123
|
+
if (args.targets.length > 100) {
|
|
124
|
+
return { ok: false, error: '一次最多查询 100 个 AID' };
|
|
125
|
+
}
|
|
126
|
+
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
127
|
+
try {
|
|
128
|
+
const result = await conn.call('message.query_online', { aids: args.targets });
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
online: result?.online ?? {},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return formatRpcError(e);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await conn.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ==================== Internal ====================
|
|
142
|
+
function formatRpcError(e) {
|
|
143
|
+
if (e?.code !== undefined && e?.message !== undefined) {
|
|
144
|
+
return { ok: false, error: String(e.message), code: e.code };
|
|
145
|
+
}
|
|
146
|
+
return { ok: false, error: String(e?.message ?? e) };
|
|
147
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
const EXT_TYPE_MAP = {
|
|
3
|
+
// image
|
|
4
|
+
'.png': 'image', '.jpg': 'image', '.jpeg': 'image',
|
|
5
|
+
'.gif': 'image', '.webp': 'image', '.svg': 'image',
|
|
6
|
+
'.bmp': 'image', '.heic': 'image', '.heif': 'image',
|
|
7
|
+
// video
|
|
8
|
+
'.mp4': 'video', '.mov': 'video', '.webm': 'video',
|
|
9
|
+
'.avi': 'video', '.mkv': 'video', '.m4v': 'video',
|
|
10
|
+
// voice
|
|
11
|
+
'.opus': 'voice', '.mp3': 'voice', '.aac': 'voice',
|
|
12
|
+
'.m4a': 'voice', '.wav': 'voice', '.flac': 'voice', '.ogg': 'voice',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 按扩展名推断 payload.type。
|
|
16
|
+
* 未识别的扩展名归类为 'file'。
|
|
17
|
+
*/
|
|
18
|
+
export function inferPayloadType(filename) {
|
|
19
|
+
const ext = path.extname(filename).toLowerCase();
|
|
20
|
+
return EXT_TYPE_MAP[ext] ?? 'file';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 校验显式传入的 --as 值。
|
|
24
|
+
*/
|
|
25
|
+
export function isValidPayloadType(value) {
|
|
26
|
+
return value === 'image' || value === 'video' || value === 'voice' || value === 'file';
|
|
27
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { guessMime, formatSize } from '../../utils/media-cache.js';
|
|
5
|
+
import { inferPayloadType, isValidPayloadType } from './payload-type.js';
|
|
6
|
+
/** 小文件阈值:≤64KB 走 storage.put_object 内联 base64;>64KB 走 create_upload_session + HTTP PUT。 */
|
|
7
|
+
const INLINE_UPLOAD_LIMIT = 64 * 1024;
|
|
8
|
+
/** 单次上传最大大小(与 daemon sendFile 一致)。 */
|
|
9
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* 上传本地文件并构造发送用的 payload。
|
|
12
|
+
*
|
|
13
|
+
* 流程:
|
|
14
|
+
* 1. 读文件、算 sha256、推断 content_type
|
|
15
|
+
* 2. 小文件 storage.put_object,大文件 storage.create_upload_session + HTTP PUT + storage.complete_upload
|
|
16
|
+
* 3. 按 as / 扩展名 确定 payload.type
|
|
17
|
+
* 4. 构造 payload(含 attachments 引用)
|
|
18
|
+
*
|
|
19
|
+
* 不做 outbox 持久化、不做 E2EE 加密兜底——这些是 daemon 的职责。
|
|
20
|
+
* CLI 短连接场景假定网络稳定,失败抛异常给调用方处理。
|
|
21
|
+
*/
|
|
22
|
+
export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts) {
|
|
23
|
+
const absPath = path.resolve(filePath);
|
|
24
|
+
if (!fs.existsSync(absPath)) {
|
|
25
|
+
throw new Error(`文件不存在: ${absPath}`);
|
|
26
|
+
}
|
|
27
|
+
const stat = fs.statSync(absPath);
|
|
28
|
+
if (stat.size === 0) {
|
|
29
|
+
throw new Error(`文件为空: ${absPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
32
|
+
throw new Error(`文件过大 (${formatSize(stat.size)}, 上 ${formatSize(MAX_FILE_SIZE)}): ${absPath}`);
|
|
33
|
+
}
|
|
34
|
+
const filename = path.basename(absPath);
|
|
35
|
+
const fileData = fs.readFileSync(absPath);
|
|
36
|
+
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
37
|
+
const contentType = opts?.contentType ?? guessMime(filename);
|
|
38
|
+
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
39
|
+
if (stat.size <= INLINE_UPLOAD_LIMIT) {
|
|
40
|
+
await conn.call('storage.put_object', {
|
|
41
|
+
object_key: objectKey,
|
|
42
|
+
content: fileData.toString('base64'),
|
|
43
|
+
content_type: contentType,
|
|
44
|
+
is_private: false,
|
|
45
|
+
overwrite: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const session = await conn.call('storage.create_upload_session', {
|
|
50
|
+
object_key: objectKey,
|
|
51
|
+
size_bytes: stat.size,
|
|
52
|
+
content_type: contentType,
|
|
53
|
+
});
|
|
54
|
+
const uploadUrl = session?.upload_url;
|
|
55
|
+
if (!uploadUrl)
|
|
56
|
+
throw new Error('storage.create_upload_session 未返回 upload_url');
|
|
57
|
+
const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
|
|
58
|
+
if (!uploadResp.ok)
|
|
59
|
+
throw new Error(`HTTP 上传失败: ${uploadResp.status}`);
|
|
60
|
+
await conn.call('storage.complete_upload', {
|
|
61
|
+
object_key: objectKey,
|
|
62
|
+
sha256,
|
|
63
|
+
content_type: contentType,
|
|
64
|
+
is_private: false,
|
|
65
|
+
size_bytes: stat.size,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const attachment = {
|
|
69
|
+
owner_aid: ownerAid,
|
|
70
|
+
object_key: objectKey,
|
|
71
|
+
filename,
|
|
72
|
+
size_bytes: stat.size,
|
|
73
|
+
sha256,
|
|
74
|
+
content_type: contentType,
|
|
75
|
+
};
|
|
76
|
+
// 确定渲染类型
|
|
77
|
+
let type;
|
|
78
|
+
if (opts?.as) {
|
|
79
|
+
if (!isValidPayloadType(opts.as)) {
|
|
80
|
+
throw new Error(`--as 必须是 image|video|voice|file,收到: ${opts.as}`);
|
|
81
|
+
}
|
|
82
|
+
type = opts.as;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
type = inferPayloadType(filename);
|
|
86
|
+
}
|
|
87
|
+
const payload = {
|
|
88
|
+
type,
|
|
89
|
+
attachments: [attachment],
|
|
90
|
+
};
|
|
91
|
+
if (opts?.text)
|
|
92
|
+
payload.text = opts.text;
|
|
93
|
+
else if (type === 'file')
|
|
94
|
+
payload.text = `📎 ${filename} (${formatSize(stat.size)})`;
|
|
95
|
+
if (type === 'voice' && opts?.transcript)
|
|
96
|
+
payload.transcript = opts.transcript;
|
|
97
|
+
return { payload, type, attachment };
|
|
98
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { resolvePaths } from '../paths.js';
|
|
5
|
+
const MAX_ENTRIES_PER_AID = 20;
|
|
6
|
+
const DEFAULT_TTL = 300_000; // 5 minutes
|
|
7
|
+
function outboxDir() {
|
|
8
|
+
return resolvePaths().outboxDir;
|
|
9
|
+
}
|
|
10
|
+
function outboxFile(aid) {
|
|
11
|
+
return path.join(outboxDir(), `${aid}.jsonl`);
|
|
12
|
+
}
|
|
13
|
+
function generateId() {
|
|
14
|
+
const ts = Date.now();
|
|
15
|
+
const rand = crypto.randomBytes(2).toString('hex');
|
|
16
|
+
return `out-${ts}-${rand}`;
|
|
17
|
+
}
|
|
18
|
+
function isExpired(entry) {
|
|
19
|
+
return Date.now() - entry.ts > entry.ttl;
|
|
20
|
+
}
|
|
21
|
+
function readEntries(aid) {
|
|
22
|
+
const file = outboxFile(aid);
|
|
23
|
+
if (!fs.existsSync(file))
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(file, 'utf-8').trim();
|
|
27
|
+
if (!content)
|
|
28
|
+
return [];
|
|
29
|
+
return content.split('\n').map(line => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(line);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}).filter((e) => e !== null);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeEntries(aid, entries) {
|
|
43
|
+
const file = outboxFile(aid);
|
|
44
|
+
if (entries.length === 0) {
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(file);
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const dir = outboxDir();
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(file, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
54
|
+
}
|
|
55
|
+
export function enqueue(aid, opts) {
|
|
56
|
+
const entry = {
|
|
57
|
+
id: generateId(),
|
|
58
|
+
ts: Date.now(),
|
|
59
|
+
aid,
|
|
60
|
+
channelId: opts.channelId,
|
|
61
|
+
type: opts.type,
|
|
62
|
+
text: opts.text,
|
|
63
|
+
filePath: opts.filePath,
|
|
64
|
+
context: opts.context,
|
|
65
|
+
ttl: opts.ttl ?? DEFAULT_TTL,
|
|
66
|
+
};
|
|
67
|
+
const dir = outboxDir();
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
const file = outboxFile(aid);
|
|
70
|
+
// Enforce cap: read existing, drop oldest if over limit
|
|
71
|
+
let entries = readEntries(aid);
|
|
72
|
+
if (entries.length >= MAX_ENTRIES_PER_AID) {
|
|
73
|
+
entries = entries.slice(entries.length - MAX_ENTRIES_PER_AID + 1);
|
|
74
|
+
writeEntries(aid, [...entries, entry]);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
export function remove(aid, id) {
|
|
82
|
+
const entries = readEntries(aid).filter(e => e.id !== id);
|
|
83
|
+
writeEntries(aid, entries);
|
|
84
|
+
}
|
|
85
|
+
export function load(aid) {
|
|
86
|
+
return readEntries(aid).filter(e => !isExpired(e));
|
|
87
|
+
}
|
|
88
|
+
export function cleanup(aid) {
|
|
89
|
+
const all = readEntries(aid);
|
|
90
|
+
const valid = all.filter(e => !isExpired(e));
|
|
91
|
+
const removed = all.length - valid.length;
|
|
92
|
+
if (removed > 0)
|
|
93
|
+
writeEntries(aid, valid);
|
|
94
|
+
return removed;
|
|
95
|
+
}
|
|
96
|
+
export async function drain(aid, sender) {
|
|
97
|
+
const entries = readEntries(aid);
|
|
98
|
+
if (entries.length === 0)
|
|
99
|
+
return { sent: 0, expired: 0, failed: 0 };
|
|
100
|
+
let sent = 0;
|
|
101
|
+
let expired = 0;
|
|
102
|
+
let failed = 0;
|
|
103
|
+
const remaining = [];
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (isExpired(entry)) {
|
|
106
|
+
expired++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const ok = await sender(entry);
|
|
111
|
+
if (ok) {
|
|
112
|
+
sent++;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
failed++;
|
|
116
|
+
remaining.push(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
failed++;
|
|
121
|
+
remaining.push(entry);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
writeEntries(aid, remaining);
|
|
125
|
+
return { sent, expired, failed };
|
|
126
|
+
}
|
|
127
|
+
export function hasPending(aid) {
|
|
128
|
+
const file = outboxFile(aid);
|
|
129
|
+
if (!fs.existsSync(file))
|
|
130
|
+
return false;
|
|
131
|
+
try {
|
|
132
|
+
const stat = fs.statSync(file);
|
|
133
|
+
return stat.size > 0;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createShortConnection } from './connection.js';
|
|
2
|
+
export async function rpcCall(aid, method, params, opts) {
|
|
3
|
+
const conn = await createShortConnection(aid, opts);
|
|
4
|
+
try {
|
|
5
|
+
const result = await conn.call(method, params);
|
|
6
|
+
return { ok: true, result };
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
if (e.code !== undefined && e.message !== undefined) {
|
|
10
|
+
return { ok: false, error: { code: e.code, message: e.message, data: e.data } };
|
|
11
|
+
}
|
|
12
|
+
return { ok: false, error: { code: -1, message: String(e.message || e) } };
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await conn.close();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function rpcBatch(aid, calls, opts) {
|
|
19
|
+
const conn = await createShortConnection(aid, opts);
|
|
20
|
+
const results = [];
|
|
21
|
+
try {
|
|
22
|
+
for (const { method, params } of calls) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await conn.call(method, params);
|
|
25
|
+
results.push({ ok: true, result });
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e.code !== undefined && e.message !== undefined) {
|
|
29
|
+
results.push({ ok: false, error: { code: e.code, message: e.message, data: e.data } });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
results.push({ ok: false, error: { code: -1, message: String(e.message || e) } });
|
|
33
|
+
}
|
|
34
|
+
break; // stop on first failure
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await conn.close();
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
export async function createShortConnection(aid, opts) {
|
|
5
|
+
const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
|
|
6
|
+
const slotId = opts?.slotId ?? '';
|
|
7
|
+
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
8
|
+
const { AUNClient } = await import('@agentunion/fastaun');
|
|
9
|
+
const clientOpts = { aun_path: aunPath, debug: false };
|
|
10
|
+
if (fs.existsSync(caCertPath))
|
|
11
|
+
clientOpts.root_ca_path = caCertPath;
|
|
12
|
+
const client = new AUNClient(clientOpts);
|
|
13
|
+
await client.auth.createAid({ aid });
|
|
14
|
+
const authResult = await client.auth.authenticate({ aid });
|
|
15
|
+
const accessToken = authResult?.access_token ?? client._access_token;
|
|
16
|
+
const gateway = client._gatewayUrl ?? authResult?.gateway;
|
|
17
|
+
await client.connect({
|
|
18
|
+
access_token: accessToken,
|
|
19
|
+
gateway,
|
|
20
|
+
slot_id: slotId,
|
|
21
|
+
connection_kind: 'short',
|
|
22
|
+
}, { auto_reconnect: false });
|
|
23
|
+
return {
|
|
24
|
+
async call(method, params) {
|
|
25
|
+
return client.call(method, params);
|
|
26
|
+
},
|
|
27
|
+
async close() {
|
|
28
|
+
try {
|
|
29
|
+
await client.close();
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { rpcCall } from '../rpc/index.js';
|
|
4
|
+
export async function storageDownload(aid, url, localPath, opts) {
|
|
5
|
+
// Parse URL: strip https:// prefix, split into owner + path
|
|
6
|
+
const cleaned = url.replace(/^https?:\/\//, '');
|
|
7
|
+
const slashIdx = cleaned.indexOf('/');
|
|
8
|
+
if (slashIdx === -1) {
|
|
9
|
+
return { ok: false, localPath: '', size: 0, error: 'URL 格式错误,需要 <owner-aid>/<path>' };
|
|
10
|
+
}
|
|
11
|
+
const ownerAid = cleaned.slice(0, slashIdx);
|
|
12
|
+
const objectKey = cleaned.slice(slashIdx + 1);
|
|
13
|
+
const ticketResult = await rpcCall(aid, 'storage.create_download_ticket', {
|
|
14
|
+
owner: ownerAid,
|
|
15
|
+
object_key: objectKey,
|
|
16
|
+
}, { aunPath: opts?.aunPath });
|
|
17
|
+
if (!ticketResult.ok) {
|
|
18
|
+
return { ok: false, localPath: '', size: 0, error: JSON.stringify(ticketResult.error) };
|
|
19
|
+
}
|
|
20
|
+
const downloadUrl = ticketResult.result.download_url;
|
|
21
|
+
const resp = await fetch(downloadUrl);
|
|
22
|
+
if (!resp.ok) {
|
|
23
|
+
return { ok: false, localPath: '', size: 0, error: `HTTP GET failed: ${resp.status}` };
|
|
24
|
+
}
|
|
25
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
26
|
+
const outPath = localPath ?? path.basename(objectKey);
|
|
27
|
+
fs.writeFileSync(outPath, buffer);
|
|
28
|
+
return { ok: true, localPath: outPath, size: buffer.length };
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { rpcCall } from '../rpc/caller.js';
|
|
2
|
+
export async function storageLs(aid, prefix, opts) {
|
|
3
|
+
return rpcCall(aid, 'storage.list_objects', { prefix: prefix || '' }, { aunPath: opts?.aunPath });
|
|
4
|
+
}
|
|
5
|
+
export async function storageRm(aid, remotePath, opts) {
|
|
6
|
+
return rpcCall(aid, 'storage.delete_object', { object_key: remotePath }, { aunPath: opts?.aunPath });
|
|
7
|
+
}
|
|
8
|
+
export async function storageQuota(aid, opts) {
|
|
9
|
+
return rpcCall(aid, 'storage.get_quota', {}, { aunPath: opts?.aunPath });
|
|
10
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { rpcCall } from '../rpc/index.js';
|
|
4
|
+
export async function storageUpload(aid, localFile, remotePath, opts) {
|
|
5
|
+
const fileBuffer = fs.readFileSync(localFile);
|
|
6
|
+
const contentType = 'application/octet-stream';
|
|
7
|
+
const sha256 = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
8
|
+
const createResult = await rpcCall(aid, 'storage.create_upload_session', {
|
|
9
|
+
object_key: remotePath,
|
|
10
|
+
content_type: contentType,
|
|
11
|
+
content_length: fileBuffer.length,
|
|
12
|
+
is_private: !(opts?.isPublic),
|
|
13
|
+
}, { aunPath: opts?.aunPath });
|
|
14
|
+
if (!createResult.ok) {
|
|
15
|
+
return { ok: false, objectKey: remotePath, error: JSON.stringify(createResult.error) };
|
|
16
|
+
}
|
|
17
|
+
const uploadUrl = createResult.result.upload_url;
|
|
18
|
+
const putResp = await fetch(uploadUrl, {
|
|
19
|
+
method: 'PUT',
|
|
20
|
+
headers: { 'Content-Type': contentType },
|
|
21
|
+
body: new Blob([fileBuffer]),
|
|
22
|
+
redirect: 'follow',
|
|
23
|
+
});
|
|
24
|
+
if (!putResp.ok) {
|
|
25
|
+
return { ok: false, objectKey: remotePath, error: `HTTP PUT failed: ${putResp.status}` };
|
|
26
|
+
}
|
|
27
|
+
const completeResult = await rpcCall(aid, 'storage.complete_upload', {
|
|
28
|
+
object_key: remotePath,
|
|
29
|
+
sha256,
|
|
30
|
+
}, { aunPath: opts?.aunPath });
|
|
31
|
+
if (!completeResult.ok) {
|
|
32
|
+
return { ok: false, objectKey: remotePath, error: JSON.stringify(completeResult.error) };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, objectKey: remotePath };
|
|
35
|
+
}
|