@nanhara/hara 0.121.0 → 0.122.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/CHANGELOG.md +85 -0
- package/README.md +40 -6
- package/dist/agent/failover.js +1 -1
- package/dist/agent/loop.js +158 -21
- package/dist/agent/reminders.js +22 -7
- package/dist/agent/repeat-guard.js +26 -7
- package/dist/agent/structured.js +231 -0
- package/dist/agent/touched.js +20 -6
- package/dist/config.js +62 -26
- package/dist/cron/deliver.js +37 -3
- package/dist/feedback.js +5 -9
- package/dist/fs-read.js +106 -12
- package/dist/fs-write.js +242 -16
- package/dist/gateway/dingtalk.js +4 -1
- package/dist/gateway/discord.js +53 -18
- package/dist/gateway/feishu.js +158 -57
- package/dist/gateway/flows-pending.js +720 -0
- package/dist/gateway/flows.js +391 -16
- package/dist/gateway/matrix.js +80 -15
- package/dist/gateway/mattermost.js +44 -32
- package/dist/gateway/media.js +659 -0
- package/dist/gateway/serve.js +657 -162
- package/dist/gateway/sessions.js +475 -78
- package/dist/gateway/signal.js +27 -22
- package/dist/gateway/slack.js +26 -17
- package/dist/gateway/telegram.js +32 -18
- package/dist/gateway/wecom.js +32 -24
- package/dist/gateway/weixin.js +127 -49
- package/dist/hooks.js +32 -20
- package/dist/index.js +640 -219
- package/dist/org/projects.js +347 -0
- package/dist/org/roles.js +38 -11
- package/dist/security/secrets.js +150 -0
- package/dist/serve/server.js +772 -317
- package/dist/serve/sessions.js +112 -28
- package/dist/session/store.js +337 -44
- package/dist/tools/all.js +1 -0
- package/dist/tools/builtin.js +61 -23
- package/dist/tools/codebase.js +3 -1
- package/dist/tools/computer.js +98 -92
- package/dist/tools/edit.js +11 -8
- package/dist/tools/patch.js +230 -31
- package/dist/tools/search.js +482 -72
- package/dist/tools/task.js +453 -0
- package/dist/tools/todo.js +67 -16
- package/dist/tools/web.js +364 -64
- package/dist/tui/run.js +26 -23
- package/dist/undo.js +83 -7
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// allow users via HARA_GATEWAY_ALLOWED (Mattermost user ids). Same ChatAdapter shape as Telegram/Discord, so all
|
|
5
5
|
// the cross-platform gateway plumbing (send_file, in-chat system context, stuck-guard, image attach/describe)
|
|
6
6
|
// works unchanged. Auth is a WS "authentication_challenge" rather than an HTTP header.
|
|
7
|
-
import { readFileSync
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import { InboundMediaBudget, savePrivateResponse } from "./media.js";
|
|
10
10
|
import { chunkText } from "./telegram.js";
|
|
11
11
|
const WSImpl = globalThis.WebSocket;
|
|
12
12
|
const sleep = (ms, signal) => new Promise((r) => {
|
|
@@ -24,18 +24,15 @@ function normalizeBase(raw) {
|
|
|
24
24
|
function wsUrlFromBase(base) {
|
|
25
25
|
return normalizeBase(base).replace(/^http/i, "ws") + "/api/v4/websocket";
|
|
26
26
|
}
|
|
27
|
-
async function downloadMattermostFile(base, token, fileId, name) {
|
|
27
|
+
async function downloadMattermostFile(base, token, fileId, name, options) {
|
|
28
28
|
try {
|
|
29
29
|
const r = await fetch(`${normalizeBase(base)}/api/v4/files/${encodeURIComponent(fileId)}`, {
|
|
30
30
|
headers: { Authorization: `Bearer ${token}` },
|
|
31
|
+
signal: options.signal,
|
|
31
32
|
});
|
|
32
33
|
if (!r.ok)
|
|
33
34
|
return null;
|
|
34
|
-
|
|
35
|
-
mkdirSync(dir, { recursive: true });
|
|
36
|
-
const path = join(dir, `mm_${Date.now()}_${basename(name) || "file.bin"}`);
|
|
37
|
-
writeFileSync(path, Buffer.from(await r.arrayBuffer()));
|
|
38
|
-
return path;
|
|
35
|
+
return await savePrivateResponse(r, { platform: "mattermost", filenameHint: name, ...options });
|
|
39
36
|
}
|
|
40
37
|
catch {
|
|
41
38
|
return null;
|
|
@@ -44,7 +41,11 @@ async function downloadMattermostFile(base, token, fileId, name) {
|
|
|
44
41
|
/** Parse a Mattermost "posted" event's post → InboundMsg + its image file ids (pure; download happens in
|
|
45
42
|
* start()). The post is the already-parsed object (the event's data.post is a JSON string the caller decodes).
|
|
46
43
|
* null = ignore (own post / system post / empty). */
|
|
47
|
-
export function
|
|
44
|
+
export function mattermostChatType(channelType) {
|
|
45
|
+
// Mattermost channel types: D=direct, G=group DM, O=open, P=private. Unknown is never assumed to be a DM.
|
|
46
|
+
return String(channelType ?? "").trim().toUpperCase() === "D" ? "p2p" : "group";
|
|
47
|
+
}
|
|
48
|
+
export function parseMattermostPost(post, selfUserId, channelType) {
|
|
48
49
|
if (!post?.channel_id || !post?.user_id)
|
|
49
50
|
return null;
|
|
50
51
|
if (post.user_id === selfUserId)
|
|
@@ -64,6 +65,7 @@ export function parseMattermostPost(post, selfUserId) {
|
|
|
64
65
|
userId: String(post.user_id),
|
|
65
66
|
userName: String(post.user_id), // enriched by start() from the event's sender_name when available
|
|
66
67
|
text: text || "[图片]",
|
|
68
|
+
chatType: mattermostChatType(channelType),
|
|
67
69
|
},
|
|
68
70
|
imageFileIds,
|
|
69
71
|
};
|
|
@@ -101,13 +103,13 @@ export function mattermostAdapter(serverUrl, token) {
|
|
|
101
103
|
body: JSON.stringify({ channel_id: chatId, message: "", file_ids: [fileId] }),
|
|
102
104
|
}).catch(() => { });
|
|
103
105
|
},
|
|
104
|
-
async start(onMessage, signal) {
|
|
106
|
+
async start(onMessage, signal, shouldDownload) {
|
|
105
107
|
if (!WSImpl) {
|
|
106
108
|
console.error("hara gateway: Mattermost needs Node ≥ 22 (global WebSocket). Upgrade Node.");
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
111
|
while (!signal.aborted) {
|
|
110
|
-
await connectOnce(base, token, onMessage, signal);
|
|
112
|
+
await connectOnce(base, token, onMessage, signal, shouldDownload);
|
|
111
113
|
if (!signal.aborted)
|
|
112
114
|
await sleep(3000, signal); // reconnect backoff
|
|
113
115
|
}
|
|
@@ -117,7 +119,7 @@ export function mattermostAdapter(serverUrl, token) {
|
|
|
117
119
|
/** One WS connection: send authentication_challenge, then dispatch "posted" events. Resolves on close/abort;
|
|
118
120
|
* the caller reconnects. v1 keeps it simple — fresh auth each time, no resume. The bot's own user id is
|
|
119
121
|
* resolved via GET /users/me so we can drop our own echoes. */
|
|
120
|
-
function connectOnce(base, token, onMessage, signal) {
|
|
122
|
+
function connectOnce(base, token, onMessage, signal, shouldDownload) {
|
|
121
123
|
return new Promise((resolve) => {
|
|
122
124
|
const ws = new WSImpl(wsUrlFromBase(base));
|
|
123
125
|
let seq = 1;
|
|
@@ -172,33 +174,43 @@ function connectOnce(base, token, onMessage, signal) {
|
|
|
172
174
|
catch {
|
|
173
175
|
return;
|
|
174
176
|
}
|
|
175
|
-
const parsed = parseMattermostPost(post, selfId);
|
|
177
|
+
const parsed = parseMattermostPost(post, selfId, p.data?.channel_type);
|
|
176
178
|
if (!parsed)
|
|
177
179
|
return;
|
|
178
180
|
// enrich the display name from the event payload when the server includes it
|
|
179
181
|
const senderName = String(p.data?.sender_name ?? "").replace(/^@/, "");
|
|
180
182
|
if (senderName)
|
|
181
183
|
parsed.msg.userName = senderName;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
if (shouldDownload?.(parsed.msg) === true) {
|
|
185
|
+
const budget = new InboundMediaBudget("mattermost", signal);
|
|
186
|
+
for (const fid of parsed.imageFileIds) {
|
|
187
|
+
const path = await budget.download(async (options) => {
|
|
188
|
+
// Resolve mime/name inside the same bounded slot; metadata is part of the download lifecycle too.
|
|
189
|
+
let name = "image";
|
|
190
|
+
let mime = "";
|
|
191
|
+
try {
|
|
192
|
+
const info = await fetch(`${base}/api/v4/files/${encodeURIComponent(fid)}/info`, {
|
|
193
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
194
|
+
signal: options.signal,
|
|
195
|
+
});
|
|
196
|
+
if (info.ok) {
|
|
197
|
+
const ij = (await info.json());
|
|
198
|
+
name = String(ij?.name ?? name);
|
|
199
|
+
mime = String(ij?.mime_type ?? "");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* fall back to extension sniff below */
|
|
204
|
+
}
|
|
205
|
+
if (!isImage(name, mime))
|
|
206
|
+
return null;
|
|
207
|
+
return downloadMattermostFile(base, token, fid, name, options);
|
|
208
|
+
});
|
|
209
|
+
if (path) {
|
|
210
|
+
(parsed.msg.images ??= []).push(path);
|
|
211
|
+
(parsed.msg.transientFiles ??= []).push(path);
|
|
192
212
|
}
|
|
193
213
|
}
|
|
194
|
-
catch {
|
|
195
|
-
/* fall back to extension sniff below */
|
|
196
|
-
}
|
|
197
|
-
if (!isImage(name, mime))
|
|
198
|
-
continue;
|
|
199
|
-
const path = await downloadMattermostFile(base, token, fid, name);
|
|
200
|
-
if (path)
|
|
201
|
-
(parsed.msg.images ??= []).push(path);
|
|
202
214
|
}
|
|
203
215
|
await onMessage(parsed.msg).catch(() => { });
|
|
204
216
|
});
|