evolclaw 2.8.2 → 3.0.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.
Files changed (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1064 -279
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +437 -273
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. package/dist/utils/stats-collector.js +0 -99
@@ -0,0 +1,291 @@
1
+ import { createShortConnection } from '../rpc/index.js';
2
+ import { uploadFileAndBuildPayload } from './upload.js';
3
+ export async function groupSend(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 'file': {
15
+ const built = await uploadFileAndBuildPayload(conn, args.from, args.body.filePath, {
16
+ as: args.body.as,
17
+ contentType: args.body.contentType,
18
+ text: args.body.text,
19
+ transcript: args.body.transcript,
20
+ });
21
+ payload = built.payload;
22
+ break;
23
+ }
24
+ }
25
+ if (args.mentions && args.mentions.length > 0) {
26
+ payload.mentions = args.mentions;
27
+ }
28
+ const result = await conn.call('group.send', { group_id: args.groupId, payload });
29
+ return {
30
+ ok: true,
31
+ group_id: result?.group_id ?? args.groupId,
32
+ message: result?.message,
33
+ event: result?.event,
34
+ };
35
+ }
36
+ catch (e) {
37
+ return formatRpcError(e);
38
+ }
39
+ finally {
40
+ await conn.close();
41
+ }
42
+ }
43
+ export async function groupPull(args) {
44
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
45
+ try {
46
+ const params = { group_id: args.groupId };
47
+ if (args.afterSeq !== undefined)
48
+ params.after_message_seq = args.afterSeq;
49
+ if (args.limit !== undefined)
50
+ params.limit = args.limit;
51
+ const result = await conn.call('group.pull', params);
52
+ return {
53
+ ok: true,
54
+ group_id: result?.group_id ?? args.groupId,
55
+ messages: result?.messages ?? [],
56
+ latest_message_seq: result?.latest_message_seq ?? 0,
57
+ has_more: !!result?.has_more,
58
+ };
59
+ }
60
+ catch (e) {
61
+ return formatRpcError(e);
62
+ }
63
+ finally {
64
+ await conn.close();
65
+ }
66
+ }
67
+ export async function groupAck(args) {
68
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
69
+ try {
70
+ const result = await conn.call('group.ack', { group_id: args.groupId, seq: args.seq });
71
+ return {
72
+ ok: true,
73
+ group_id: result?.group_id ?? args.groupId,
74
+ ack_seq: result?.ack_seq ?? args.seq,
75
+ latest_message_seq: result?.latest_message_seq,
76
+ };
77
+ }
78
+ catch (e) {
79
+ return formatRpcError(e);
80
+ }
81
+ finally {
82
+ await conn.close();
83
+ }
84
+ }
85
+ export async function groupCreate(args) {
86
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
87
+ try {
88
+ const params = { name: args.name };
89
+ if (args.groupId)
90
+ params.group_id = args.groupId;
91
+ if (args.visibility)
92
+ params.visibility = args.visibility;
93
+ if (args.description)
94
+ params.description = args.description;
95
+ if (args.joinMode)
96
+ params.join_mode = args.joinMode;
97
+ const result = await conn.call('group.create', params);
98
+ return { ok: true, group: result?.group };
99
+ }
100
+ catch (e) {
101
+ return formatRpcError(e);
102
+ }
103
+ finally {
104
+ await conn.close();
105
+ }
106
+ }
107
+ export async function groupInfo(args) {
108
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
109
+ try {
110
+ const result = await conn.call('group.get', { group_id: args.groupId });
111
+ return { ok: true, group: result?.group };
112
+ }
113
+ catch (e) {
114
+ return formatRpcError(e);
115
+ }
116
+ finally {
117
+ await conn.close();
118
+ }
119
+ }
120
+ export async function groupList(args) {
121
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
122
+ try {
123
+ const params = {};
124
+ if (args.size !== undefined)
125
+ params.size = args.size;
126
+ const result = await conn.call('group.list_my', params);
127
+ return {
128
+ ok: true,
129
+ items: result?.items ?? [],
130
+ total: result?.total ?? 0,
131
+ };
132
+ }
133
+ catch (e) {
134
+ return formatRpcError(e);
135
+ }
136
+ finally {
137
+ await conn.close();
138
+ }
139
+ }
140
+ export async function groupUpdate(args) {
141
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
142
+ try {
143
+ const params = { group_id: args.groupId };
144
+ if (args.name !== undefined)
145
+ params.name = args.name;
146
+ if (args.description !== undefined)
147
+ params.description = args.description;
148
+ const result = await conn.call('group.update', params);
149
+ return { ok: true, group: result?.group };
150
+ }
151
+ catch (e) {
152
+ return formatRpcError(e);
153
+ }
154
+ finally {
155
+ await conn.close();
156
+ }
157
+ }
158
+ export async function groupDissolve(args) {
159
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
160
+ try {
161
+ const result = await conn.call('group.dissolve', { group_id: args.groupId });
162
+ return {
163
+ ok: true,
164
+ group_id: result?.group_id ?? args.groupId,
165
+ status: result?.status ?? 'dissolved',
166
+ };
167
+ }
168
+ catch (e) {
169
+ return formatRpcError(e);
170
+ }
171
+ finally {
172
+ await conn.close();
173
+ }
174
+ }
175
+ export async function groupJoin(args) {
176
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
177
+ try {
178
+ const params = { group_id: args.groupId };
179
+ if (args.message)
180
+ params.message = args.message;
181
+ if (args.answer)
182
+ params.answer = args.answer;
183
+ const result = await conn.call('group.request_join', params);
184
+ return { ok: true, group_id: args.groupId, data: result };
185
+ }
186
+ catch (e) {
187
+ return formatRpcError(e);
188
+ }
189
+ finally {
190
+ await conn.close();
191
+ }
192
+ }
193
+ export async function groupLeave(args) {
194
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
195
+ try {
196
+ const result = await conn.call('group.leave', { group_id: args.groupId });
197
+ return { ok: true, group_id: args.groupId, data: result };
198
+ }
199
+ catch (e) {
200
+ return formatRpcError(e);
201
+ }
202
+ finally {
203
+ await conn.close();
204
+ }
205
+ }
206
+ export async function groupInvite(args) {
207
+ if (args.members.length === 0) {
208
+ return { ok: false, error: '至少需要一个成员 AID' };
209
+ }
210
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
211
+ const added = [];
212
+ const failed = [];
213
+ try {
214
+ for (const memberAid of args.members) {
215
+ try {
216
+ await conn.call('group.add_member', { group_id: args.groupId, aid: memberAid });
217
+ added.push(memberAid);
218
+ }
219
+ catch (e) {
220
+ failed.push({ aid: memberAid, error: String(e?.message ?? e) });
221
+ }
222
+ }
223
+ return { ok: true, group_id: args.groupId, added, failed };
224
+ }
225
+ finally {
226
+ await conn.close();
227
+ }
228
+ }
229
+ export async function groupKick(args) {
230
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
231
+ try {
232
+ const result = await conn.call('group.kick', { group_id: args.groupId, aid: args.memberAid });
233
+ return { ok: true, group_id: args.groupId, data: result };
234
+ }
235
+ catch (e) {
236
+ return formatRpcError(e);
237
+ }
238
+ finally {
239
+ await conn.close();
240
+ }
241
+ }
242
+ export async function groupMembers(args) {
243
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
244
+ try {
245
+ const params = { group_id: args.groupId };
246
+ if (args.page !== undefined)
247
+ params.page = args.page;
248
+ if (args.size !== undefined)
249
+ params.size = args.size;
250
+ const result = await conn.call('group.get_members', params);
251
+ return {
252
+ ok: true,
253
+ members: result?.members ?? [],
254
+ total: result?.total ?? 0,
255
+ page: result?.page ?? 1,
256
+ size: result?.size ?? 50,
257
+ };
258
+ }
259
+ catch (e) {
260
+ return formatRpcError(e);
261
+ }
262
+ finally {
263
+ await conn.close();
264
+ }
265
+ }
266
+ export async function groupOnline(args) {
267
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
268
+ try {
269
+ const result = await conn.call('group.get_online_members', { group_id: args.groupId });
270
+ return {
271
+ ok: true,
272
+ group_id: result?.group_id ?? args.groupId,
273
+ members: result?.members ?? [],
274
+ online_count: result?.online_count ?? 0,
275
+ total: result?.total ?? 0,
276
+ };
277
+ }
278
+ catch (e) {
279
+ return formatRpcError(e);
280
+ }
281
+ finally {
282
+ await conn.close();
283
+ }
284
+ }
285
+ // ==================== Internal ====================
286
+ function formatRpcError(e) {
287
+ if (e?.code !== undefined && e?.message !== undefined) {
288
+ return { ok: false, error: String(e.message), code: e.code };
289
+ }
290
+ return { ok: false, error: String(e?.message ?? e) };
291
+ }
@@ -0,0 +1,4 @@
1
+ export { msgSend, msgPull, msgAck, msgRecall, msgOnline, } from './p2p.js';
2
+ export { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, } from './group.js';
3
+ export { uploadFileAndBuildPayload } from './upload.js';
4
+ export { inferPayloadType, isValidPayloadType } from './payload-type.js';
@@ -0,0 +1,144 @@
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 result = await conn.call('message.send', { to: args.to, payload });
33
+ return {
34
+ ok: true,
35
+ message_id: result?.message_id,
36
+ seq: result?.seq,
37
+ timestamp: result?.timestamp,
38
+ status: result?.status,
39
+ delivery_mode: result?.delivery_mode,
40
+ };
41
+ }
42
+ catch (e) {
43
+ return formatRpcError(e);
44
+ }
45
+ finally {
46
+ await conn.close();
47
+ }
48
+ }
49
+ export async function msgPull(args) {
50
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
51
+ try {
52
+ const params = {};
53
+ if (args.afterSeq !== undefined)
54
+ params.after_seq = args.afterSeq;
55
+ if (args.limit !== undefined)
56
+ params.limit = args.limit;
57
+ const result = await conn.call('message.pull', params);
58
+ return {
59
+ ok: true,
60
+ messages: result?.messages ?? [],
61
+ count: result?.count ?? 0,
62
+ latest_seq: result?.latest_seq ?? 0,
63
+ earliest_available_seq: result?.earliest_available_seq ?? null,
64
+ ephemeral_earliest_available_seq: result?.ephemeral_earliest_available_seq ?? null,
65
+ ephemeral_dropped_count: result?.ephemeral_dropped_count ?? 0,
66
+ };
67
+ }
68
+ catch (e) {
69
+ return formatRpcError(e);
70
+ }
71
+ finally {
72
+ await conn.close();
73
+ }
74
+ }
75
+ export async function msgAck(args) {
76
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
77
+ try {
78
+ const result = await conn.call('message.ack', { seq: args.seq });
79
+ return {
80
+ ok: true,
81
+ ack_seq: result?.ack_seq ?? args.seq,
82
+ event_published: result?.event_published,
83
+ };
84
+ }
85
+ catch (e) {
86
+ return formatRpcError(e);
87
+ }
88
+ finally {
89
+ await conn.close();
90
+ }
91
+ }
92
+ export async function msgRecall(args) {
93
+ if (args.messageIds.length === 0) {
94
+ return { ok: false, error: 'message_ids 不能为空' };
95
+ }
96
+ if (args.messageIds.length > 100) {
97
+ return { ok: false, error: 'message_ids 最多 100 个' };
98
+ }
99
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
100
+ try {
101
+ const result = await conn.call('message.recall', { message_ids: args.messageIds });
102
+ return {
103
+ ok: true,
104
+ accepted: result?.accepted ?? 0,
105
+ recalled: result?.recalled ?? 0,
106
+ errors: result?.errors ?? null,
107
+ };
108
+ }
109
+ catch (e) {
110
+ return formatRpcError(e);
111
+ }
112
+ finally {
113
+ await conn.close();
114
+ }
115
+ }
116
+ export async function msgOnline(args) {
117
+ if (args.targets.length === 0) {
118
+ return { ok: false, error: '查询目标不能为空' };
119
+ }
120
+ if (args.targets.length > 100) {
121
+ return { ok: false, error: '一次最多查询 100 个 AID' };
122
+ }
123
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
124
+ try {
125
+ const result = await conn.call('message.query_online', { aids: args.targets });
126
+ return {
127
+ ok: true,
128
+ online: result?.online ?? {},
129
+ };
130
+ }
131
+ catch (e) {
132
+ return formatRpcError(e);
133
+ }
134
+ finally {
135
+ await conn.close();
136
+ }
137
+ }
138
+ // ==================== Internal ====================
139
+ function formatRpcError(e) {
140
+ if (e?.code !== undefined && e?.message !== undefined) {
141
+ return { ok: false, error: String(e.message), code: e.code };
142
+ }
143
+ return { ok: false, error: String(e?.message ?? e) };
144
+ }
@@ -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
+ }