@dhf-hermes/grix 0.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.
Potentially problematic release.
This version of @dhf-hermes/grix might be problematic. Click here for more details.
- package/.gitignore +6 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/grix-hermes.mjs +93 -0
- package/grix-admin/SKILL.md +109 -0
- package/grix-admin/agents/openai.yaml +7 -0
- package/grix-admin/scripts/admin.mjs +12 -0
- package/grix-admin/scripts/bind_from_json.py +118 -0
- package/grix-admin/scripts/bind_local.py +226 -0
- package/grix-egg/SKILL.md +73 -0
- package/grix-egg/agents/openai.yaml +7 -0
- package/grix-egg/references/acceptance-checklist.md +10 -0
- package/grix-egg/scripts/card-link.mjs +12 -0
- package/grix-egg/scripts/validate_install_context.mjs +74 -0
- package/grix-group/SKILL.md +42 -0
- package/grix-group/agents/openai.yaml +7 -0
- package/grix-group/scripts/group.mjs +12 -0
- package/grix-query/SKILL.md +53 -0
- package/grix-query/agents/openai.yaml +7 -0
- package/grix-query/scripts/query.mjs +12 -0
- package/grix-register/SKILL.md +68 -0
- package/grix-register/agents/openai.yaml +7 -0
- package/grix-register/references/handoff-contract.md +21 -0
- package/grix-register/scripts/create_api_agent_and_bind.py +105 -0
- package/grix-register/scripts/grix_auth.py +487 -0
- package/grix-update/SKILL.md +50 -0
- package/grix-update/agents/openai.yaml +7 -0
- package/grix-update/references/cron-setup.md +11 -0
- package/grix-update/scripts/grix_update.py +99 -0
- package/lib/manifest.mjs +68 -0
- package/message-send/SKILL.md +71 -0
- package/message-send/agents/openai.yaml +7 -0
- package/message-send/scripts/card-link.mjs +40 -0
- package/message-send/scripts/send.mjs +12 -0
- package/message-unsend/SKILL.md +39 -0
- package/message-unsend/agents/openai.yaml +7 -0
- package/message-unsend/scripts/unsend.mjs +12 -0
- package/openclaw-memory-setup/SKILL.md +38 -0
- package/openclaw-memory-setup/agents/openai.yaml +7 -0
- package/openclaw-memory-setup/scripts/bench_ollama_embeddings.py +257 -0
- package/openclaw-memory-setup/scripts/set_openclaw_memory_model.py +240 -0
- package/openclaw-memory-setup/scripts/survey_host_readiness.py +379 -0
- package/package.json +51 -0
- package/shared/cli/actions.mjs +339 -0
- package/shared/cli/aibot-client.mjs +274 -0
- package/shared/cli/card-links.mjs +90 -0
- package/shared/cli/config.mjs +141 -0
- package/shared/cli/grix-hermes.mjs +87 -0
- package/shared/cli/targets.mjs +119 -0
- package/shared/references/grix-card-links.md +27 -0
- package/shared/references/hermes-grix-config.md +30 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { resolveAibotOutboundTarget, resolveSilentUnsendPlan } from "./targets.mjs";
|
|
2
|
+
|
|
3
|
+
function cleanText(value) {
|
|
4
|
+
return String(value ?? "").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function cleanInt(value, fallback = undefined) {
|
|
8
|
+
const numeric = Number.parseInt(String(value ?? ""), 10);
|
|
9
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cleanBool(value, fallback = undefined) {
|
|
13
|
+
if (typeof value === "boolean") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
const normalized = cleanText(value).toLowerCase();
|
|
17
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanList(value) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => cleanText(item)).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
return cleanText(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asArray(value) {
|
|
34
|
+
return Array.isArray(value) ? value : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function asRecord(value) {
|
|
38
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractCategoryList(data) {
|
|
42
|
+
if (Array.isArray(data)) {
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
const record = asRecord(data);
|
|
46
|
+
if (!record) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
for (const key of ["categories", "list", "items", "rows", "data"]) {
|
|
50
|
+
if (Array.isArray(record[key])) {
|
|
51
|
+
return record[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractAgentId(data) {
|
|
58
|
+
const record = asRecord(data) || {};
|
|
59
|
+
return cleanText(record.id || record.agent_id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractCategoryId(data) {
|
|
63
|
+
const record = asRecord(data) || {};
|
|
64
|
+
return cleanText(record.id || record.category_id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findCategoryByName(data, name, parentId) {
|
|
68
|
+
return extractCategoryList(data).find((item) => {
|
|
69
|
+
const record = asRecord(item) || {};
|
|
70
|
+
return cleanText(record.name) === cleanText(name)
|
|
71
|
+
&& cleanText(record.parent_id ?? record.parentId ?? "0") === cleanText(parentId || "0");
|
|
72
|
+
}) || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function runQuery(client, options) {
|
|
76
|
+
const action = cleanText(options.action);
|
|
77
|
+
const map = {
|
|
78
|
+
contact_search: "contact_search",
|
|
79
|
+
session_search: "session_search",
|
|
80
|
+
message_history: "message_history",
|
|
81
|
+
message_search: "message_search"
|
|
82
|
+
};
|
|
83
|
+
if (!map[action]) {
|
|
84
|
+
throw new Error(`Unsupported grix query action: ${action}`);
|
|
85
|
+
}
|
|
86
|
+
const params = {};
|
|
87
|
+
if (cleanText(options.id)) {
|
|
88
|
+
params.id = cleanText(options.id);
|
|
89
|
+
}
|
|
90
|
+
if (cleanText(options.keyword)) {
|
|
91
|
+
params.keyword = cleanText(options.keyword);
|
|
92
|
+
}
|
|
93
|
+
if (cleanText(options.sessionId)) {
|
|
94
|
+
params.session_id = cleanText(options.sessionId);
|
|
95
|
+
}
|
|
96
|
+
if (cleanText(options.beforeId)) {
|
|
97
|
+
params.before_id = cleanText(options.beforeId);
|
|
98
|
+
}
|
|
99
|
+
const limit = cleanInt(options.limit);
|
|
100
|
+
if (limit !== undefined) {
|
|
101
|
+
params.limit = limit;
|
|
102
|
+
}
|
|
103
|
+
const offset = cleanInt(options.offset);
|
|
104
|
+
if (offset !== undefined) {
|
|
105
|
+
params.offset = offset;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
accountId: options.accountId,
|
|
110
|
+
action,
|
|
111
|
+
data: await client.agentInvoke(map[action], params)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function runGroup(client, options) {
|
|
116
|
+
const action = cleanText(options.action);
|
|
117
|
+
const map = {
|
|
118
|
+
create: "group_create",
|
|
119
|
+
detail: "group_detail_read",
|
|
120
|
+
leave: "group_leave_self",
|
|
121
|
+
add_members: "group_member_add",
|
|
122
|
+
remove_members: "group_member_remove",
|
|
123
|
+
update_member_role: "group_member_role_update",
|
|
124
|
+
update_all_members_muted: "group_all_members_muted_update",
|
|
125
|
+
update_member_speaking: "group_member_speaking_update",
|
|
126
|
+
dissolve: "group_dissolve"
|
|
127
|
+
};
|
|
128
|
+
if (!map[action]) {
|
|
129
|
+
throw new Error(`Unsupported grix group action: ${action}`);
|
|
130
|
+
}
|
|
131
|
+
const params = {};
|
|
132
|
+
if (cleanText(options.name)) {
|
|
133
|
+
params.name = cleanText(options.name);
|
|
134
|
+
}
|
|
135
|
+
if (cleanText(options.sessionId)) {
|
|
136
|
+
params.session_id = cleanText(options.sessionId);
|
|
137
|
+
}
|
|
138
|
+
const memberIds = cleanList(options.memberIds);
|
|
139
|
+
if (memberIds.length > 0) {
|
|
140
|
+
params.member_ids = memberIds;
|
|
141
|
+
}
|
|
142
|
+
const memberTypes = cleanList(options.memberTypes).map((item) => Number.parseInt(item, 10)).filter((item) => Number.isFinite(item));
|
|
143
|
+
if (memberTypes.length > 0) {
|
|
144
|
+
params.member_types = memberTypes;
|
|
145
|
+
}
|
|
146
|
+
if (cleanText(options.memberId)) {
|
|
147
|
+
params.member_id = cleanText(options.memberId);
|
|
148
|
+
}
|
|
149
|
+
const memberType = cleanInt(options.memberType);
|
|
150
|
+
if (memberType !== undefined) {
|
|
151
|
+
params.member_type = memberType;
|
|
152
|
+
}
|
|
153
|
+
const role = cleanInt(options.role);
|
|
154
|
+
if (role !== undefined) {
|
|
155
|
+
params.role = role;
|
|
156
|
+
}
|
|
157
|
+
const allMembersMuted = cleanBool(options.allMembersMuted);
|
|
158
|
+
if (allMembersMuted !== undefined) {
|
|
159
|
+
params.all_members_muted = allMembersMuted;
|
|
160
|
+
}
|
|
161
|
+
const isSpeakMuted = cleanBool(options.isSpeakMuted);
|
|
162
|
+
if (isSpeakMuted !== undefined) {
|
|
163
|
+
params.is_speak_muted = isSpeakMuted;
|
|
164
|
+
}
|
|
165
|
+
const canSpeakWhenAllMuted = cleanBool(options.canSpeakWhenAllMuted);
|
|
166
|
+
if (canSpeakWhenAllMuted !== undefined) {
|
|
167
|
+
params.can_speak_when_all_muted = canSpeakWhenAllMuted;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
accountId: options.accountId,
|
|
172
|
+
action,
|
|
173
|
+
data: await client.agentInvoke(map[action], params)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function listCategories(client) {
|
|
178
|
+
return client.agentInvoke("agent_category_list", {});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function runAdmin(client, options) {
|
|
182
|
+
const action = cleanText(options.action || "create_agent");
|
|
183
|
+
if (action === "list_categories") {
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
accountId: options.accountId,
|
|
187
|
+
action,
|
|
188
|
+
data: await listCategories(client)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (action === "create_category") {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
accountId: options.accountId,
|
|
195
|
+
action,
|
|
196
|
+
data: await client.agentInvoke("agent_category_create", {
|
|
197
|
+
name: cleanText(options.name),
|
|
198
|
+
parent_id: cleanText(options.parentId || options.parentCategoryId || "0"),
|
|
199
|
+
sort_order: cleanInt(options.sortOrder || options.categorySortOrder)
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (action === "update_category") {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
accountId: options.accountId,
|
|
207
|
+
action,
|
|
208
|
+
data: await client.agentInvoke("agent_category_update", {
|
|
209
|
+
category_id: cleanText(options.categoryId),
|
|
210
|
+
name: cleanText(options.name),
|
|
211
|
+
parent_id: cleanText(options.parentId || options.parentCategoryId || "0"),
|
|
212
|
+
sort_order: cleanInt(options.sortOrder || options.categorySortOrder)
|
|
213
|
+
})
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (action === "assign_category") {
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
accountId: options.accountId,
|
|
220
|
+
action,
|
|
221
|
+
data: await client.agentInvoke("agent_category_assign", {
|
|
222
|
+
agent_id: cleanText(options.agentId),
|
|
223
|
+
category_id: cleanText(options.categoryId)
|
|
224
|
+
})
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (action !== "create_agent") {
|
|
228
|
+
throw new Error(`Unsupported grix admin action: ${action}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const createPayload = {
|
|
232
|
+
agent_name: cleanText(options.agentName),
|
|
233
|
+
introduction: cleanText(options.introduction) || undefined,
|
|
234
|
+
is_main: cleanBool(options.isMain, false) ?? false
|
|
235
|
+
};
|
|
236
|
+
const createdAgent = await client.agentInvoke("agent_api_create", createPayload);
|
|
237
|
+
const createdAgentId = extractAgentId(createdAgent);
|
|
238
|
+
let category = null;
|
|
239
|
+
let assignment = null;
|
|
240
|
+
|
|
241
|
+
const categoryId = cleanText(options.categoryId);
|
|
242
|
+
const categoryName = cleanText(options.categoryName);
|
|
243
|
+
if (categoryId && categoryName) {
|
|
244
|
+
throw new Error("create_agent cannot accept both categoryId and categoryName");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let resolvedCategoryId = categoryId;
|
|
248
|
+
if (!resolvedCategoryId && categoryName) {
|
|
249
|
+
const parentCategoryId = cleanText(options.parentCategoryId || "0");
|
|
250
|
+
const rawListData = await listCategories(client);
|
|
251
|
+
category = findCategoryByName(rawListData, categoryName, parentCategoryId);
|
|
252
|
+
if (!category) {
|
|
253
|
+
category = await client.agentInvoke("agent_category_create", {
|
|
254
|
+
name: categoryName,
|
|
255
|
+
parent_id: parentCategoryId,
|
|
256
|
+
sort_order: cleanInt(options.categorySortOrder)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
resolvedCategoryId = extractCategoryId(category);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (resolvedCategoryId && createdAgentId) {
|
|
263
|
+
assignment = await client.agentInvoke("agent_category_assign", {
|
|
264
|
+
agent_id: createdAgentId,
|
|
265
|
+
category_id: resolvedCategoryId
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
accountId: options.accountId,
|
|
272
|
+
action,
|
|
273
|
+
createdAgent,
|
|
274
|
+
category,
|
|
275
|
+
assignment
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function runUnsend(client, options) {
|
|
280
|
+
const plan = await resolveSilentUnsendPlan({
|
|
281
|
+
client,
|
|
282
|
+
accountId: options.accountId,
|
|
283
|
+
messageId: options.messageId,
|
|
284
|
+
targetSessionId: options.sessionId,
|
|
285
|
+
targetTo: options.to,
|
|
286
|
+
targetTopic: options.topic,
|
|
287
|
+
currentChannelId: options.currentChannelId,
|
|
288
|
+
currentMessageId: options.currentMessageId
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const targetAck = await client.deleteMessage(plan.targetDelete.sessionId, plan.targetDelete.messageId);
|
|
292
|
+
let commandAck = null;
|
|
293
|
+
if (plan.commandDelete) {
|
|
294
|
+
commandAck = await client.deleteMessage(plan.commandDelete.sessionId, plan.commandDelete.messageId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
accountId: options.accountId,
|
|
300
|
+
targetDelete: plan.targetDelete,
|
|
301
|
+
commandDelete: plan.commandDelete || null,
|
|
302
|
+
completionMessageId: plan.completionMessageId || null,
|
|
303
|
+
targetAck,
|
|
304
|
+
commandAck
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function runSend(client, options) {
|
|
309
|
+
const message = String(options.message ?? "");
|
|
310
|
+
if (!message.trim()) {
|
|
311
|
+
throw new Error("message-send requires message");
|
|
312
|
+
}
|
|
313
|
+
const target = cleanText(options.to || options.target);
|
|
314
|
+
if (!target) {
|
|
315
|
+
throw new Error("message-send requires to/target");
|
|
316
|
+
}
|
|
317
|
+
const resolved = await resolveAibotOutboundTarget({
|
|
318
|
+
client,
|
|
319
|
+
accountId: options.accountId,
|
|
320
|
+
to: target
|
|
321
|
+
});
|
|
322
|
+
const ack = await client.sendText(
|
|
323
|
+
resolved.sessionId,
|
|
324
|
+
message,
|
|
325
|
+
{
|
|
326
|
+
threadId: cleanText(options.threadId) || resolved.threadId,
|
|
327
|
+
replyToMessageId: cleanText(options.replyToMessageId),
|
|
328
|
+
eventId: cleanText(options.eventId)
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
accountId: options.accountId,
|
|
334
|
+
target,
|
|
335
|
+
resolvedTarget: resolved,
|
|
336
|
+
message,
|
|
337
|
+
ack
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
|
|
4
|
+
function nowMs() {
|
|
5
|
+
return Date.now();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeText(value) {
|
|
9
|
+
return String(value ?? "").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseCode(payload) {
|
|
13
|
+
const code = Number(payload?.code ?? 0);
|
|
14
|
+
return Number.isFinite(code) ? code : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseMessage(payload) {
|
|
18
|
+
return normalizeText(payload?.msg) || normalizeText(payload?.message) || "unknown error";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function packetError(packet) {
|
|
22
|
+
return new Error(`grix ${packet.cmd}: code=${parseCode(packet.payload)} msg=${parseMessage(packet.payload)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildAuthPayload(config) {
|
|
26
|
+
return {
|
|
27
|
+
agent_id: config.agentId,
|
|
28
|
+
api_key: config.apiKey,
|
|
29
|
+
client: config.client,
|
|
30
|
+
client_type: config.clientType,
|
|
31
|
+
client_version: config.clientVersion,
|
|
32
|
+
protocol_version: "aibot-agent-api-v1",
|
|
33
|
+
contract_version: config.contractVersion ?? 1,
|
|
34
|
+
host_type: config.hostType || "hermes",
|
|
35
|
+
capabilities: config.capabilities || ["session_route", "thread_v1", "inbound_media_v1", "local_action_v1"],
|
|
36
|
+
local_actions: config.localActions || ["exec_approve", "exec_reject"]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class AibotWsClient {
|
|
41
|
+
constructor(config) {
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.ws = null;
|
|
44
|
+
this.seq = nowMs();
|
|
45
|
+
this.authed = false;
|
|
46
|
+
this.pending = new Map();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ensureReady(requireAuthed = true) {
|
|
50
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
51
|
+
throw new Error("grix websocket is not connected");
|
|
52
|
+
}
|
|
53
|
+
if (requireAuthed && !this.authed) {
|
|
54
|
+
throw new Error("grix websocket is not authenticated");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async connect() {
|
|
59
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.authed) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
const ws = new WebSocket(this.config.endpoint, {
|
|
64
|
+
handshakeTimeout: this.config.connectTimeoutMs
|
|
65
|
+
});
|
|
66
|
+
this.ws = ws;
|
|
67
|
+
|
|
68
|
+
ws.on("open", () => resolve());
|
|
69
|
+
ws.on("error", (error) => reject(error));
|
|
70
|
+
ws.on("close", (_code, reason) => {
|
|
71
|
+
const message = normalizeText(reason) || "grix websocket closed";
|
|
72
|
+
this.rejectAll(new Error(message));
|
|
73
|
+
this.authed = false;
|
|
74
|
+
});
|
|
75
|
+
ws.on("message", (data) => {
|
|
76
|
+
void this.handleMessage(data);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const authPacket = await this.request(
|
|
81
|
+
"auth",
|
|
82
|
+
buildAuthPayload(this.config),
|
|
83
|
+
{ expected: ["auth_ack"], timeoutMs: 10000, requireAuthed: false }
|
|
84
|
+
);
|
|
85
|
+
if (parseCode(authPacket.payload) !== 0) {
|
|
86
|
+
throw packetError(authPacket);
|
|
87
|
+
}
|
|
88
|
+
this.authed = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async disconnect(reason = "done") {
|
|
92
|
+
this.rejectAll(new Error(reason));
|
|
93
|
+
this.authed = false;
|
|
94
|
+
const ws = this.ws;
|
|
95
|
+
this.ws = null;
|
|
96
|
+
if (!ws) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
ws.once("close", () => resolve());
|
|
101
|
+
ws.close(1000, reason);
|
|
102
|
+
setTimeout(resolve, 500);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rejectAll(error) {
|
|
107
|
+
for (const { timer, reject } of this.pending.values()) {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
reject(error);
|
|
110
|
+
}
|
|
111
|
+
this.pending.clear();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
nextSeq() {
|
|
115
|
+
this.seq += 1;
|
|
116
|
+
return this.seq;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sendPacket(cmd, payload, seq = 0) {
|
|
120
|
+
this.ensureReady(false);
|
|
121
|
+
this.ws.send(JSON.stringify({ cmd, seq, payload }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
request(cmd, payload, options = {}) {
|
|
125
|
+
this.ensureReady(options.requireAuthed !== false);
|
|
126
|
+
const seq = this.nextSeq();
|
|
127
|
+
const expected = new Set(options.expected || []);
|
|
128
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : (this.config.requestTimeoutMs || 20000);
|
|
129
|
+
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
this.pending.delete(seq);
|
|
133
|
+
reject(new Error(`${cmd} timeout`));
|
|
134
|
+
}, timeoutMs);
|
|
135
|
+
this.pending.set(seq, { expected, resolve, reject, timer });
|
|
136
|
+
this.sendPacket(cmd, payload, seq);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async handleMessage(data) {
|
|
141
|
+
const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data ?? "");
|
|
142
|
+
if (!text) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let packet;
|
|
146
|
+
try {
|
|
147
|
+
packet = JSON.parse(text);
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!packet || typeof packet !== "object") {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (packet.cmd === "ping") {
|
|
155
|
+
this.sendPacket("pong", { ts: nowMs() }, Number(packet.seq) > 0 ? Number(packet.seq) : 0);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const seq = Number(packet.seq || 0);
|
|
159
|
+
const pending = this.pending.get(seq);
|
|
160
|
+
if (!pending) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!pending.expected.has(packet.cmd)) {
|
|
164
|
+
if (packet.cmd === "error") {
|
|
165
|
+
clearTimeout(pending.timer);
|
|
166
|
+
this.pending.delete(seq);
|
|
167
|
+
pending.reject(packetError(packet));
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
clearTimeout(pending.timer);
|
|
172
|
+
this.pending.delete(seq);
|
|
173
|
+
pending.resolve(packet);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async agentInvoke(action, params = {}, options = {}) {
|
|
177
|
+
const normalizedAction = normalizeText(action);
|
|
178
|
+
if (!normalizedAction) {
|
|
179
|
+
throw new Error("grix agent_invoke requires action");
|
|
180
|
+
}
|
|
181
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, options.timeoutMs) : 15000;
|
|
182
|
+
const packet = await this.request(
|
|
183
|
+
"agent_invoke",
|
|
184
|
+
{
|
|
185
|
+
invoke_id: randomUUID(),
|
|
186
|
+
action: normalizedAction,
|
|
187
|
+
params,
|
|
188
|
+
timeout_ms: timeoutMs
|
|
189
|
+
},
|
|
190
|
+
{ expected: ["agent_invoke_result"], timeoutMs }
|
|
191
|
+
);
|
|
192
|
+
if (parseCode(packet.payload) !== 0) {
|
|
193
|
+
throw packetError(packet);
|
|
194
|
+
}
|
|
195
|
+
return packet.payload?.data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async sendText(sessionId, text, options = {}) {
|
|
199
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
200
|
+
const normalizedText = String(text ?? "");
|
|
201
|
+
if (!normalizedSessionId) {
|
|
202
|
+
throw new Error("grix send_msg requires session_id");
|
|
203
|
+
}
|
|
204
|
+
if (!normalizedText.trim()) {
|
|
205
|
+
throw new Error("grix send_msg requires content");
|
|
206
|
+
}
|
|
207
|
+
const payload = {
|
|
208
|
+
session_id: normalizedSessionId,
|
|
209
|
+
msg_type: 1,
|
|
210
|
+
content: normalizedText
|
|
211
|
+
};
|
|
212
|
+
if (normalizeText(options.replyToMessageId)) {
|
|
213
|
+
payload.quoted_message_id = normalizeText(options.replyToMessageId);
|
|
214
|
+
}
|
|
215
|
+
if (normalizeText(options.threadId)) {
|
|
216
|
+
payload.thread_id = normalizeText(options.threadId);
|
|
217
|
+
}
|
|
218
|
+
if (normalizeText(options.eventId)) {
|
|
219
|
+
payload.event_id = normalizeText(options.eventId);
|
|
220
|
+
}
|
|
221
|
+
const packet = await this.request(
|
|
222
|
+
"send_msg",
|
|
223
|
+
payload,
|
|
224
|
+
{ expected: ["send_ack", "send_nack", "error"], timeoutMs: options.timeoutMs }
|
|
225
|
+
);
|
|
226
|
+
if (packet.cmd !== "send_ack") {
|
|
227
|
+
throw packetError(packet);
|
|
228
|
+
}
|
|
229
|
+
return packet.payload;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async deleteMessage(sessionId, messageId, options = {}) {
|
|
233
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
234
|
+
const normalizedMessageId = normalizeText(messageId);
|
|
235
|
+
if (!normalizedSessionId) {
|
|
236
|
+
throw new Error("grix delete_msg requires session_id");
|
|
237
|
+
}
|
|
238
|
+
if (!/^\d+$/.test(normalizedMessageId)) {
|
|
239
|
+
throw new Error("grix delete_msg requires numeric msg_id");
|
|
240
|
+
}
|
|
241
|
+
const packet = await this.request(
|
|
242
|
+
"delete_msg",
|
|
243
|
+
{
|
|
244
|
+
session_id: normalizedSessionId,
|
|
245
|
+
msg_id: normalizedMessageId
|
|
246
|
+
},
|
|
247
|
+
{ expected: ["send_ack", "send_nack", "error"], timeoutMs: options.timeoutMs }
|
|
248
|
+
);
|
|
249
|
+
if (packet.cmd !== "send_ack") {
|
|
250
|
+
throw packetError(packet);
|
|
251
|
+
}
|
|
252
|
+
return packet.payload;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async resolveSessionRoute(channel, accountId, routeSessionKey, options = {}) {
|
|
256
|
+
const packet = await this.request(
|
|
257
|
+
"session_route_resolve",
|
|
258
|
+
{
|
|
259
|
+
channel: normalizeText(channel),
|
|
260
|
+
account_id: normalizeText(accountId),
|
|
261
|
+
route_session_key: normalizeText(routeSessionKey)
|
|
262
|
+
},
|
|
263
|
+
{ expected: ["send_ack", "send_nack", "error"], timeoutMs: options.timeoutMs }
|
|
264
|
+
);
|
|
265
|
+
if (packet.cmd !== "send_ack") {
|
|
266
|
+
throw packetError(packet);
|
|
267
|
+
}
|
|
268
|
+
const sessionId = normalizeText(packet.payload?.session_id);
|
|
269
|
+
if (!sessionId) {
|
|
270
|
+
throw new Error("grix session_route_resolve ack missing session_id");
|
|
271
|
+
}
|
|
272
|
+
return packet.payload;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function cleanText(value) {
|
|
2
|
+
return String(value ?? "").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function encodeValue(value) {
|
|
6
|
+
return encodeURIComponent(cleanText(value));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildLink(label, url) {
|
|
10
|
+
return `[${cleanText(label)}](${url})`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildConversationCard(params) {
|
|
14
|
+
const sessionId = cleanText(params.sessionId);
|
|
15
|
+
const sessionType = cleanText(params.sessionType);
|
|
16
|
+
const title = cleanText(params.title);
|
|
17
|
+
if (!sessionId || !sessionType || !title) {
|
|
18
|
+
throw new Error("conversation card requires sessionId, sessionType, and title");
|
|
19
|
+
}
|
|
20
|
+
const query = new URLSearchParams({
|
|
21
|
+
session_id: sessionId,
|
|
22
|
+
session_type: sessionType,
|
|
23
|
+
title,
|
|
24
|
+
});
|
|
25
|
+
const peerId = cleanText(params.peerId);
|
|
26
|
+
if (peerId) {
|
|
27
|
+
query.set("peer_id", peerId);
|
|
28
|
+
}
|
|
29
|
+
return buildLink(cleanText(params.label) || "打开会话", `grix://card/conversation?${query.toString()}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildUserProfileCard(params) {
|
|
33
|
+
const userId = cleanText(params.userId);
|
|
34
|
+
const nickname = cleanText(params.nickname);
|
|
35
|
+
if (!userId || !nickname) {
|
|
36
|
+
throw new Error("user profile card requires userId and nickname");
|
|
37
|
+
}
|
|
38
|
+
const query = new URLSearchParams({
|
|
39
|
+
user_id: userId,
|
|
40
|
+
peer_type: cleanText(params.peerType) || "2",
|
|
41
|
+
nickname,
|
|
42
|
+
});
|
|
43
|
+
const avatarUrl = cleanText(params.avatarUrl);
|
|
44
|
+
if (avatarUrl) {
|
|
45
|
+
query.set("avatar_url", avatarUrl);
|
|
46
|
+
}
|
|
47
|
+
return buildLink(cleanText(params.label) || "查看 Agent 资料", `grix://card/user_profile?${query.toString()}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildEggStatusCard(params) {
|
|
51
|
+
const installId = cleanText(params.installId);
|
|
52
|
+
const status = cleanText(params.status);
|
|
53
|
+
const step = cleanText(params.step);
|
|
54
|
+
const summary = cleanText(params.summary);
|
|
55
|
+
if (!installId || !status || !step || !summary) {
|
|
56
|
+
throw new Error("egg status card requires installId, status, step, and summary");
|
|
57
|
+
}
|
|
58
|
+
const query = new URLSearchParams({
|
|
59
|
+
install_id: installId,
|
|
60
|
+
status,
|
|
61
|
+
step,
|
|
62
|
+
summary,
|
|
63
|
+
});
|
|
64
|
+
const targetAgentId = cleanText(params.targetAgentId);
|
|
65
|
+
if (targetAgentId) {
|
|
66
|
+
query.set("target_agent_id", targetAgentId);
|
|
67
|
+
}
|
|
68
|
+
const errorCode = cleanText(params.errorCode);
|
|
69
|
+
if (errorCode) {
|
|
70
|
+
query.set("error_code", errorCode);
|
|
71
|
+
}
|
|
72
|
+
const errorMessage = cleanText(params.errorMessage);
|
|
73
|
+
if (errorMessage) {
|
|
74
|
+
query.set("error_msg", errorMessage);
|
|
75
|
+
}
|
|
76
|
+
return buildLink(cleanText(params.label) || "安装状态", `grix://card/egg_install_status?${query.toString()}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function dispatchCardBuilder(kind, params) {
|
|
80
|
+
if (kind === "conversation") {
|
|
81
|
+
return buildConversationCard(params);
|
|
82
|
+
}
|
|
83
|
+
if (kind === "user-profile") {
|
|
84
|
+
return buildUserProfileCard(params);
|
|
85
|
+
}
|
|
86
|
+
if (kind === "egg-status") {
|
|
87
|
+
return buildEggStatusCard(params);
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unsupported card kind: ${kind}`);
|
|
90
|
+
}
|