@auto-ai/agent 2.1.95 → 2.1.96
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/dist/agent-office.html +40 -35
- package/dist/ws-test.html +369 -108
- package/mcps-runtime/claude-tuitui/.mcp.json +18 -0
- package/mcps-runtime/claude-tuitui/cli.mjs +78 -0
- package/mcps-runtime/claude-tuitui/package-lock.json +1167 -0
- package/mcps-runtime/claude-tuitui/server/boot.mjs +25 -0
- package/mcps-runtime/claude-tuitui/server/index.mjs +935 -0
- package/mcps-runtime/claude-tuitui/server/state.mjs +229 -0
- package/mcps-runtime/claude-tuitui/server/tuitui-api.mjs +358 -0
- package/package.json +7 -6
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
appendFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
|
|
11
|
+
function ensureDir(path) {
|
|
12
|
+
mkdirSync(path, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readJson(file, fallback) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
18
|
+
} catch {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJsonAtomic(file, data) {
|
|
24
|
+
ensureDir(dirname(file));
|
|
25
|
+
const tmp = `${file}.tmp`;
|
|
26
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
27
|
+
writeFileSync(file, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function defaultMeta() {
|
|
31
|
+
return {
|
|
32
|
+
cursor: 0,
|
|
33
|
+
lastWsConnectAt: 0,
|
|
34
|
+
lastMessageAt: 0,
|
|
35
|
+
lastDeliveredChat: null,
|
|
36
|
+
lastPermissionRequestAt: 0,
|
|
37
|
+
lastPermissionRelay: null,
|
|
38
|
+
lastPermissionDecisionAt: 0,
|
|
39
|
+
lastPermissionDecision: null,
|
|
40
|
+
lastPermissionPersistAt: 0,
|
|
41
|
+
lastPermissionPersist: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class StateStore {
|
|
46
|
+
constructor(stateDir) {
|
|
47
|
+
this.stateDir = stateDir;
|
|
48
|
+
this.accessFile = join(stateDir, "access.json");
|
|
49
|
+
this.inboxFile = join(stateDir, "inbox.jsonl");
|
|
50
|
+
this.metaFile = join(stateDir, "meta.json");
|
|
51
|
+
this.seenFile = join(stateDir, "seen.json");
|
|
52
|
+
this.rawEventsFile = join(stateDir, "raw-events.jsonl");
|
|
53
|
+
ensureDir(stateDir);
|
|
54
|
+
if (!existsSync(this.accessFile)) {
|
|
55
|
+
this.saveAccess({
|
|
56
|
+
dmPolicy: process.env.TUITUI_DM_POLICY || "pairing",
|
|
57
|
+
allowFrom: [],
|
|
58
|
+
groupPolicy: process.env.TUITUI_GROUP_POLICY || "allowlist",
|
|
59
|
+
groupAllowFrom: [],
|
|
60
|
+
pending: {},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!existsSync(this.metaFile)) this.saveMeta(defaultMeta());
|
|
64
|
+
if (!existsSync(this.seenFile)) this.saveSeen({ keys: [] });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
loadAccess() {
|
|
68
|
+
return readJson(this.accessFile, {
|
|
69
|
+
dmPolicy: "pairing",
|
|
70
|
+
allowFrom: [],
|
|
71
|
+
groupPolicy: "allowlist",
|
|
72
|
+
groupAllowFrom: [],
|
|
73
|
+
pending: {},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
saveAccess(data) {
|
|
77
|
+
writeJsonAtomic(this.accessFile, data);
|
|
78
|
+
}
|
|
79
|
+
loadMeta() {
|
|
80
|
+
return { ...defaultMeta(), ...readJson(this.metaFile, defaultMeta()) };
|
|
81
|
+
}
|
|
82
|
+
saveMeta(data) {
|
|
83
|
+
writeJsonAtomic(this.metaFile, { ...defaultMeta(), ...(data || {}) });
|
|
84
|
+
}
|
|
85
|
+
loadSeen() {
|
|
86
|
+
return readJson(this.seenFile, { keys: [] });
|
|
87
|
+
}
|
|
88
|
+
saveSeen(data) {
|
|
89
|
+
writeJsonAtomic(this.seenFile, data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
appendRawEvent(event) {
|
|
93
|
+
ensureDir(this.stateDir);
|
|
94
|
+
appendFileSync(this.rawEventsFile, JSON.stringify(event) + "\n", {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
mode: 0o600,
|
|
97
|
+
flag: "a",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
hasSeen(key) {
|
|
102
|
+
const seen = this.loadSeen();
|
|
103
|
+
return (seen.keys || []).includes(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rememberSeen(key, maxKeys = 2000) {
|
|
107
|
+
const seen = this.loadSeen();
|
|
108
|
+
const keys = Array.isArray(seen.keys) ? seen.keys : [];
|
|
109
|
+
if (!keys.includes(key)) keys.push(key);
|
|
110
|
+
while (keys.length > maxKeys) keys.shift();
|
|
111
|
+
this.saveSeen({ keys });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
appendInbox(entry) {
|
|
115
|
+
ensureDir(this.stateDir);
|
|
116
|
+
const meta = this.loadMeta();
|
|
117
|
+
meta.lastMessageAt = Date.now();
|
|
118
|
+
meta.cursor = (meta.cursor || 0) + 1;
|
|
119
|
+
this.saveMeta(meta);
|
|
120
|
+
const row = { ...entry, cursor: meta.cursor };
|
|
121
|
+
appendFileSync(this.inboxFile, JSON.stringify(row) + "\n", {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
mode: 0o600,
|
|
124
|
+
flag: "a",
|
|
125
|
+
});
|
|
126
|
+
return meta.cursor;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
listMessages(limit = 20, unreadOnly = false, afterCursor = 0) {
|
|
130
|
+
let rows = [];
|
|
131
|
+
try {
|
|
132
|
+
rows = readFileSync(this.inboxFile, "utf8")
|
|
133
|
+
.trim()
|
|
134
|
+
.split(/\n+/)
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map((line) => JSON.parse(line));
|
|
137
|
+
} catch {}
|
|
138
|
+
if (afterCursor)
|
|
139
|
+
rows = rows.filter((x) => Number(x.cursor || 0) > Number(afterCursor));
|
|
140
|
+
if (unreadOnly) rows = rows.filter((x) => !x.readAt);
|
|
141
|
+
return rows.slice(-limit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
markRead(uptoCursor = Infinity) {
|
|
145
|
+
let rows = [];
|
|
146
|
+
try {
|
|
147
|
+
rows = readFileSync(this.inboxFile, "utf8")
|
|
148
|
+
.trim()
|
|
149
|
+
.split(/\n+/)
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.map((line) => JSON.parse(line));
|
|
152
|
+
} catch {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
let changed = 0;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
rows = rows.map((row) => {
|
|
158
|
+
if (!row.readAt && Number(row.cursor || 0) <= uptoCursor) {
|
|
159
|
+
changed += 1;
|
|
160
|
+
return { ...row, readAt: now };
|
|
161
|
+
}
|
|
162
|
+
return row;
|
|
163
|
+
});
|
|
164
|
+
writeFileSync(
|
|
165
|
+
this.inboxFile,
|
|
166
|
+
rows.map((x) => JSON.stringify(x)).join("\n") + (rows.length ? "\n" : ""),
|
|
167
|
+
"utf8",
|
|
168
|
+
);
|
|
169
|
+
return changed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
markMessageReplied(messageId, replyText) {
|
|
173
|
+
let rows = [];
|
|
174
|
+
try {
|
|
175
|
+
rows = readFileSync(this.inboxFile, "utf8")
|
|
176
|
+
.trim()
|
|
177
|
+
.split(/\n+/)
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.map((line) => JSON.parse(line));
|
|
180
|
+
} catch {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
let changed = 0;
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
rows = rows.map((row) => {
|
|
186
|
+
if (String(row.messageId) === String(messageId) && !row.repliedAt) {
|
|
187
|
+
changed += 1;
|
|
188
|
+
return { ...row, repliedAt: now, replyText };
|
|
189
|
+
}
|
|
190
|
+
return row;
|
|
191
|
+
});
|
|
192
|
+
writeFileSync(
|
|
193
|
+
this.inboxFile,
|
|
194
|
+
rows.map((x) => JSON.stringify(x)).join("\n") + (rows.length ? "\n" : ""),
|
|
195
|
+
"utf8",
|
|
196
|
+
);
|
|
197
|
+
return changed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
createPairing(senderId, chatId) {
|
|
201
|
+
const access = this.loadAccess();
|
|
202
|
+
const code = crypto.randomBytes(3).toString("hex");
|
|
203
|
+
access.pending ||= {};
|
|
204
|
+
access.pending[code] = {
|
|
205
|
+
senderId,
|
|
206
|
+
chatId,
|
|
207
|
+
createdAt: Date.now(),
|
|
208
|
+
expiresAt: Date.now() + 3600_000,
|
|
209
|
+
replies: 0,
|
|
210
|
+
};
|
|
211
|
+
this.saveAccess(access);
|
|
212
|
+
return code;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
approvePairing(code) {
|
|
216
|
+
const access = this.loadAccess();
|
|
217
|
+
const pending = access.pending?.[code];
|
|
218
|
+
if (!pending) return null;
|
|
219
|
+
access.allowFrom = Array.from(
|
|
220
|
+
new Set([
|
|
221
|
+
...(access.allowFrom || []),
|
|
222
|
+
String(pending.senderId).toLowerCase().trim(),
|
|
223
|
+
]),
|
|
224
|
+
);
|
|
225
|
+
delete access.pending[code];
|
|
226
|
+
this.saveAccess(access);
|
|
227
|
+
return pending;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import WebSocket from "ws";
|
|
4
|
+
|
|
5
|
+
const TUITUI_HOST = "https://im.live.360.cn:8282";
|
|
6
|
+
|
|
7
|
+
export const CHAT_TYPE_DIRECT = "direct";
|
|
8
|
+
export const CHAT_TYPE_GROUP = "group";
|
|
9
|
+
export const CHAT_TYPE_CHANNEL = "channel";
|
|
10
|
+
|
|
11
|
+
export function addParams2Url(urlStr, params) {
|
|
12
|
+
const url = new URL(urlStr);
|
|
13
|
+
for (const [k, v] of Object.entries(params || {})) {
|
|
14
|
+
if (v !== undefined && v !== null && v !== "")
|
|
15
|
+
url.searchParams.set(k, String(v));
|
|
16
|
+
}
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function teamsBuildChatId(team_id, channel_id, thread_id) {
|
|
21
|
+
return `teams_${team_id}_${channel_id}_${thread_id}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function teamsParseChatId(chatId) {
|
|
25
|
+
const [team_id, channel_id, parent_id] = String(chatId)
|
|
26
|
+
.replace(/^teams_/, "")
|
|
27
|
+
.split("_");
|
|
28
|
+
if (!team_id || !channel_id || !parent_id)
|
|
29
|
+
throw new Error(`Invalid teams chat ID: ${chatId}`);
|
|
30
|
+
return { team_id, channel_id, parent_id, post_id: "" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function teamsBuildReplyTarget(
|
|
34
|
+
team_id,
|
|
35
|
+
channel_id,
|
|
36
|
+
parent_id,
|
|
37
|
+
post_id = "",
|
|
38
|
+
) {
|
|
39
|
+
if (!team_id || !channel_id || !parent_id) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid teams reply target: ${team_id || ""}/${channel_id || ""}/${parent_id || ""}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return { team_id, channel_id, parent_id, post_id };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function guessChatType(chatId) {
|
|
48
|
+
if (String(chatId).startsWith("teams_")) return CHAT_TYPE_CHANNEL;
|
|
49
|
+
if (/^\d+$/.test(String(chatId))) return CHAT_TYPE_GROUP;
|
|
50
|
+
return CHAT_TYPE_DIRECT;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mimeFromFilename(filename) {
|
|
54
|
+
const ext = String(filename).split(".").pop()?.toLowerCase();
|
|
55
|
+
const map = {
|
|
56
|
+
jpg: "image/jpeg",
|
|
57
|
+
jpeg: "image/jpeg",
|
|
58
|
+
png: "image/png",
|
|
59
|
+
gif: "image/gif",
|
|
60
|
+
webp: "image/webp",
|
|
61
|
+
pdf: "application/pdf",
|
|
62
|
+
txt: "text/plain",
|
|
63
|
+
json: "application/json",
|
|
64
|
+
mp3: "audio/mpeg",
|
|
65
|
+
mp4: "video/mp4",
|
|
66
|
+
};
|
|
67
|
+
return map[ext] || "application/octet-stream";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function postJson(account, path, payload, auditCtx = "tuitui.post") {
|
|
71
|
+
const url = addParams2Url(`${TUITUI_HOST}${path}`, {
|
|
72
|
+
appid: account.appId,
|
|
73
|
+
secret: account.appSecret,
|
|
74
|
+
});
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
});
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
let parsed = {};
|
|
82
|
+
try {
|
|
83
|
+
parsed = text ? JSON.parse(text) : {};
|
|
84
|
+
} catch {}
|
|
85
|
+
if (!res.ok)
|
|
86
|
+
throw new Error(
|
|
87
|
+
`${auditCtx} failed: ${res.status} ${res.statusText} ${text}`,
|
|
88
|
+
);
|
|
89
|
+
if (Number(parsed.errcode || 0) !== 0)
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${auditCtx} errcode=${parsed.errcode} errmsg=${parsed.errmsg || "unknown"}`,
|
|
92
|
+
);
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getTargets(chatId, chatType, targetOverrides = undefined) {
|
|
97
|
+
if (chatType === CHAT_TYPE_DIRECT) return { tousers: [chatId] };
|
|
98
|
+
if (chatType === CHAT_TYPE_GROUP) return { togroups: [chatId] };
|
|
99
|
+
return {
|
|
100
|
+
toteams: [targetOverrides || teamsParseChatId(chatId)],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getMentionsRegex() {
|
|
105
|
+
return /(?<=^|[\s\r\n\u3000\u3001\u3002\uFF0C\uFF01\uFF1F\u2026])@([^\s]+)/g;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractMentions(text) {
|
|
109
|
+
const mentions = [];
|
|
110
|
+
const regex = getMentionsRegex();
|
|
111
|
+
let match;
|
|
112
|
+
while ((match = regex.exec(text)) !== null) {
|
|
113
|
+
if (!mentions.includes(match[1])) mentions.push(match[1]);
|
|
114
|
+
}
|
|
115
|
+
return mentions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function replaceMentions(text) {
|
|
119
|
+
return String(text).replace(
|
|
120
|
+
getMentionsRegex(),
|
|
121
|
+
(_, mention) => `{{tuitui_at \"${mention}\"}}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function replaceSingleNewlines(content) {
|
|
126
|
+
return String(content).replace(/([^\n])\n([^\n])/g, "$1\n\n$2");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildMessageBody(data = {}) {
|
|
130
|
+
const parts = [];
|
|
131
|
+
const pushImgs = (imgs) => {
|
|
132
|
+
if (!imgs?.length) return;
|
|
133
|
+
if (imgs.length === 1) parts.push(`[图片] ${imgs[0]}`);
|
|
134
|
+
else {
|
|
135
|
+
parts.push(`[图片] 共 ${imgs.length} 张图片:`);
|
|
136
|
+
imgs.forEach((url, i) => parts.push(` ${i + 1}. ${url}`));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
switch (data.msg_type) {
|
|
140
|
+
case "text":
|
|
141
|
+
parts.push(data.text || "");
|
|
142
|
+
break;
|
|
143
|
+
case "mixed":
|
|
144
|
+
parts.push(data.text || "");
|
|
145
|
+
pushImgs(data.images);
|
|
146
|
+
break;
|
|
147
|
+
case "image":
|
|
148
|
+
pushImgs(data.images);
|
|
149
|
+
break;
|
|
150
|
+
case "voice":
|
|
151
|
+
if (data.voice) parts.push(`[语音] ${data.voice}`);
|
|
152
|
+
break;
|
|
153
|
+
case "file":
|
|
154
|
+
if (data.file) parts.push(`[文件] ${data.file.name}: ${data.file.url}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (data.ref) {
|
|
158
|
+
const ref = data.ref;
|
|
159
|
+
const refText =
|
|
160
|
+
ref.text ||
|
|
161
|
+
ref.file?.url ||
|
|
162
|
+
ref.images?.join("\n") ||
|
|
163
|
+
`[${ref.msg_type}]`;
|
|
164
|
+
parts.push(`\n[引用来自 ${ref.user_name} 的消息]\n${refText}`);
|
|
165
|
+
}
|
|
166
|
+
return parts.join("\n").trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function sendTextMsg(
|
|
170
|
+
account,
|
|
171
|
+
chatId,
|
|
172
|
+
chatType,
|
|
173
|
+
content,
|
|
174
|
+
options = {},
|
|
175
|
+
) {
|
|
176
|
+
if (!chatId) throw new Error("Missing chatId");
|
|
177
|
+
const teamsTarget =
|
|
178
|
+
chatType === CHAT_TYPE_CHANNEL && options.reply_to_post_id
|
|
179
|
+
? (() => {
|
|
180
|
+
const parsed = teamsParseChatId(chatId);
|
|
181
|
+
return teamsBuildReplyTarget(
|
|
182
|
+
parsed.team_id,
|
|
183
|
+
parsed.channel_id,
|
|
184
|
+
parsed.parent_id,
|
|
185
|
+
options.reply_to_post_id,
|
|
186
|
+
);
|
|
187
|
+
})()
|
|
188
|
+
: undefined;
|
|
189
|
+
if (chatType === CHAT_TYPE_CHANNEL) {
|
|
190
|
+
const markdown = replaceMentions(replaceSingleNewlines(content));
|
|
191
|
+
return postJson(
|
|
192
|
+
account,
|
|
193
|
+
"/robot/message/custom/send",
|
|
194
|
+
{
|
|
195
|
+
...getTargets(chatId, chatType, teamsTarget),
|
|
196
|
+
msgtype: "richtext/markdown",
|
|
197
|
+
richtext: {
|
|
198
|
+
markdown,
|
|
199
|
+
delims_left: markdown.includes("{{tuitui_at") ? "{{" : "",
|
|
200
|
+
delims_right: markdown.includes("{{tuitui_at") ? "}}" : "",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"tuitui.send.text",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return postJson(
|
|
207
|
+
account,
|
|
208
|
+
"/robot/message/custom/send",
|
|
209
|
+
{
|
|
210
|
+
...getTargets(chatId, chatType),
|
|
211
|
+
msgtype: "text",
|
|
212
|
+
at: chatType === CHAT_TYPE_GROUP ? extractMentions(content) : [],
|
|
213
|
+
text: { content },
|
|
214
|
+
},
|
|
215
|
+
"tuitui.send.text",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function uploadFileToTuiTui(fileSrc, account, type = "file") {
|
|
220
|
+
let fileBuffer;
|
|
221
|
+
let contentType;
|
|
222
|
+
let filename;
|
|
223
|
+
if (/^data:/.test(fileSrc)) {
|
|
224
|
+
const matches = fileSrc.match(/^data:([^;,]*)(;base64)?,(.*)$/);
|
|
225
|
+
if (!matches) throw new Error("Invalid data URL");
|
|
226
|
+
contentType = matches[1] || "application/octet-stream";
|
|
227
|
+
const data = matches[3];
|
|
228
|
+
fileBuffer = matches[2]
|
|
229
|
+
? Buffer.from(data, "base64")
|
|
230
|
+
: Buffer.from(decodeURIComponent(data));
|
|
231
|
+
filename = `media_${Date.now()}.${contentType.split("/")[1] || "bin"}`;
|
|
232
|
+
} else if (/^https?:/.test(fileSrc)) {
|
|
233
|
+
const res = await fetch(fileSrc);
|
|
234
|
+
if (!res.ok) throw new Error(`download failed ${res.status}`);
|
|
235
|
+
fileBuffer = Buffer.from(await res.arrayBuffer());
|
|
236
|
+
contentType = res.headers.get("content-type") || "application/octet-stream";
|
|
237
|
+
filename = basename(new URL(fileSrc).pathname || "media");
|
|
238
|
+
} else {
|
|
239
|
+
if (!existsSync(fileSrc))
|
|
240
|
+
throw new Error(`Local file not found: ${fileSrc}`);
|
|
241
|
+
const stats = statSync(fileSrc);
|
|
242
|
+
if (stats.size > 10 * 1024 * 1024)
|
|
243
|
+
throw new Error(`File too large: ${fileSrc}`);
|
|
244
|
+
fileBuffer = readFileSync(fileSrc);
|
|
245
|
+
filename = basename(fileSrc);
|
|
246
|
+
contentType = mimeFromFilename(filename);
|
|
247
|
+
}
|
|
248
|
+
const body = new FormData();
|
|
249
|
+
body.append("media", new Blob([fileBuffer], { type: contentType }), filename);
|
|
250
|
+
const url = addParams2Url(`${TUITUI_HOST}/robot/media/upload`, {
|
|
251
|
+
appid: account.appId,
|
|
252
|
+
secret: account.appSecret,
|
|
253
|
+
type,
|
|
254
|
+
});
|
|
255
|
+
const res = await fetch(url, { method: "POST", body });
|
|
256
|
+
const text = await res.text();
|
|
257
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
258
|
+
if (!res.ok || Number(parsed.errcode || 0) !== 0 || !parsed.media_id) {
|
|
259
|
+
throw new Error(`upload failed: ${text}`);
|
|
260
|
+
}
|
|
261
|
+
return { fid: parsed.media_id, filename };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function sendMediaMsg(account, chatId, chatType, mediaUrl) {
|
|
265
|
+
const isImage =
|
|
266
|
+
/^data:image\//i.test(mediaUrl) ||
|
|
267
|
+
/\.(jpg|jpeg|png|gif|webp)(?:$|[?#])/i.test(mediaUrl);
|
|
268
|
+
const { fid, filename } = await uploadFileToTuiTui(
|
|
269
|
+
mediaUrl,
|
|
270
|
+
account,
|
|
271
|
+
isImage ? "image" : "file",
|
|
272
|
+
);
|
|
273
|
+
if (chatType === CHAT_TYPE_CHANNEL) {
|
|
274
|
+
const markdown = isImage
|
|
275
|
+
? ``
|
|
276
|
+
: `[${filename}]({{tuitui_file \"${fid}\"}})`;
|
|
277
|
+
return postJson(
|
|
278
|
+
account,
|
|
279
|
+
"/robot/message/custom/send",
|
|
280
|
+
{
|
|
281
|
+
...getTargets(chatId, chatType),
|
|
282
|
+
msgtype: "richtext/markdown",
|
|
283
|
+
richtext: { markdown, delims_left: "{{", delims_right: "}}" },
|
|
284
|
+
},
|
|
285
|
+
"tuitui.send.media",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return postJson(
|
|
289
|
+
account,
|
|
290
|
+
"/robot/message/custom/send",
|
|
291
|
+
{
|
|
292
|
+
...getTargets(chatId, chatType),
|
|
293
|
+
msgtype: isImage ? "image" : "attachment",
|
|
294
|
+
[isImage ? "image" : "attachment"]: { media_id: fid },
|
|
295
|
+
},
|
|
296
|
+
"tuitui.send.media",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function emojiReaction(
|
|
301
|
+
account,
|
|
302
|
+
target,
|
|
303
|
+
chatType,
|
|
304
|
+
msgid,
|
|
305
|
+
emoji = "收到",
|
|
306
|
+
) {
|
|
307
|
+
const payload = {
|
|
308
|
+
msgtype: "emoji_reaction",
|
|
309
|
+
tousers: [],
|
|
310
|
+
togroups: [],
|
|
311
|
+
toteams: [],
|
|
312
|
+
emoji_reaction: { emoji, cancel: false },
|
|
313
|
+
};
|
|
314
|
+
if (chatType === CHAT_TYPE_DIRECT)
|
|
315
|
+
payload.tousers.push({ user: target, msgid });
|
|
316
|
+
else if (chatType === CHAT_TYPE_GROUP)
|
|
317
|
+
payload.togroups.push({ group: target, msgid });
|
|
318
|
+
else {
|
|
319
|
+
const team = teamsParseChatId(target);
|
|
320
|
+
payload.toteams.push({ ...team, parent_id: "", post_id: msgid });
|
|
321
|
+
}
|
|
322
|
+
return postJson(
|
|
323
|
+
account,
|
|
324
|
+
"/robot/message/custom/modify",
|
|
325
|
+
payload,
|
|
326
|
+
"tuitui.react",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function createWebSocketClient(
|
|
331
|
+
account,
|
|
332
|
+
{ onJson, onOpen, onClose, onError, logger = console } = {},
|
|
333
|
+
) {
|
|
334
|
+
const wsUrl = `wss://im.live.360.cn:8282/robot/callback/ws?auth=${account.appId}.${account.appSecret}`;
|
|
335
|
+
const ws = new WebSocket(wsUrl, { rejectUnauthorized: true });
|
|
336
|
+
ws.on("open", () => onOpen?.());
|
|
337
|
+
ws.on("error", (err) => onError?.(err));
|
|
338
|
+
ws.on("close", (code, reason) => onClose?.(code, String(reason || "")));
|
|
339
|
+
ws.on("message", async (buf) => {
|
|
340
|
+
let json;
|
|
341
|
+
try {
|
|
342
|
+
json = JSON.parse(String(buf));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
logger.warn?.("[tuitui] failed to parse ws frame", err);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const ack = json?.event_id || json?.header?.event_id;
|
|
349
|
+
if (ack) {
|
|
350
|
+
// TuiTui server expects { ack: <event_id> } rather than echoing event_id back.
|
|
351
|
+
ws.send(JSON.stringify({ ack }));
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
if (!json?.body?.event) return;
|
|
355
|
+
await onJson?.(json);
|
|
356
|
+
});
|
|
357
|
+
return ws;
|
|
358
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auto-ai/agent",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.96",
|
|
4
4
|
"description": "Auto AI Agent 网关 CLI(WebSocket,独立二进制,无需 Bun)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"maintainers": [
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"bin/agent.js",
|
|
17
17
|
"lib/launcher-env.js",
|
|
18
18
|
"dist",
|
|
19
|
+
"mcps-runtime",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE",
|
|
21
22
|
".env.example"
|
|
@@ -24,11 +25,11 @@
|
|
|
24
25
|
"node": ">=18"
|
|
25
26
|
},
|
|
26
27
|
"optionalDependencies": {
|
|
27
|
-
"@auto-ai/agent-linux-x64": "2.1.
|
|
28
|
-
"@auto-ai/agent-linux-arm64": "2.1.
|
|
29
|
-
"@auto-ai/agent-darwin-x64": "2.1.
|
|
30
|
-
"@auto-ai/agent-darwin-arm64": "2.1.
|
|
31
|
-
"@auto-ai/agent-win-x64": "2.1.
|
|
28
|
+
"@auto-ai/agent-linux-x64": "2.1.96",
|
|
29
|
+
"@auto-ai/agent-linux-arm64": "2.1.96",
|
|
30
|
+
"@auto-ai/agent-darwin-x64": "2.1.96",
|
|
31
|
+
"@auto-ai/agent-darwin-arm64": "2.1.96",
|
|
32
|
+
"@auto-ai/agent-win-x64": "2.1.96"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"prepare": "node ../../scripts/sync-launcher-env.js",
|