oh-my-im 0.1.1 → 0.1.2
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 +1 -1
- package/dist/dws-dashboard.js +2 -2
- package/dist/dws-listener.js +219 -57
- package/dist/monitor-command.js +37 -0
- package/dist/omi-bot.js +3 -3
- package/dist/omi.js +14 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,6 @@ npm link
|
|
|
11
11
|
omi
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
首次运行会在 `~/.oh-my-im` 创建本地配置;在管理页填写钉钉应用凭证、钉钉规则和机器人单聊授权人员即可。`omi` 的状态、日志和全部运行配置都固定保存在此目录,因此可在任意目录执行 `omi status`、`omi stop` 或 `omi update`。
|
|
15
15
|
|
|
16
16
|
详见 [docs/development.md](docs/development.md)。
|
package/dist/dws-dashboard.js
CHANGED
|
@@ -11,8 +11,8 @@ function normalizeConfig(value) {
|
|
|
11
11
|
if (!value || typeof value !== "object")
|
|
12
12
|
throw new Error("配置必须是 JSON 对象");
|
|
13
13
|
const source = value;
|
|
14
|
-
if (!Array.isArray(source.targets)
|
|
15
|
-
throw new Error("
|
|
14
|
+
if (!Array.isArray(source.targets))
|
|
15
|
+
throw new Error("钉钉规则格式无效");
|
|
16
16
|
const keys = new Set();
|
|
17
17
|
const targets = source.targets.map((item, index) => {
|
|
18
18
|
if (!item || typeof item !== "object")
|
package/dist/dws-listener.js
CHANGED
|
@@ -2,10 +2,12 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { unlinkSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline";
|
|
7
8
|
import { runCodex } from "./codex.js";
|
|
8
9
|
import { DingTalkCardClient } from "./dingtalk-card.js";
|
|
10
|
+
import { applyMonitorCommand, DU_ZHENXUN_OPEN_DINGTALK_ID, parseMonitorCommand } from "./monitor-command.js";
|
|
9
11
|
import { startDashboard, } from "./dws-dashboard.js";
|
|
10
12
|
import { createLogger } from "./logger.js";
|
|
11
13
|
const log = createLogger("DwsCodexListener");
|
|
@@ -13,7 +15,7 @@ const DEFAULT_DWS_GROUP_IDS = [
|
|
|
13
15
|
"cid4SeVl1pUe7Z3wiI9IUxtfA==", // 公司自测专用
|
|
14
16
|
];
|
|
15
17
|
const DEFAULT_ALLOWED_SENDER_IDS = [
|
|
16
|
-
|
|
18
|
+
DU_ZHENXUN_OPEN_DINGTALK_ID, // 杜振训
|
|
17
19
|
];
|
|
18
20
|
const IGNORED_ROBOT_SENDER_IDS = new Set([
|
|
19
21
|
"DZyRu3o9aXvl2Ve04YPzcrrTokgiSOyA9A", // 映客活动AI in 公司自测专用
|
|
@@ -22,11 +24,14 @@ const IGNORED_ROBOT_SENDER_IDS = new Set([
|
|
|
22
24
|
const DEFAULT_CODEX_WORK_DIR = "/Users/dds/go/src/git.inke.cn/opd/activitys/inke.activity.service";
|
|
23
25
|
const DEFAULT_DWS_CODEX_TIMEOUT_MS = 300_000;
|
|
24
26
|
const DWS_GROUP_MESSAGE_EVENT = "user_im_message_receive_group_all";
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
27
|
+
const DATA_DIR = join(homedir(), ".oh-my-im");
|
|
28
|
+
const LEGACY_DATA_DIR = join(process.cwd(), ".oh-my-im");
|
|
29
|
+
const CONFIG_MIGRATION_FILE = join(DATA_DIR, ".config-location-v1");
|
|
30
|
+
const LISTENER_LOCK_FILE = join(DATA_DIR, "dws-listener.lock");
|
|
31
|
+
const CARD_STATE_FILE = join(DATA_DIR, "dws-cards.json");
|
|
32
|
+
const DASHBOARD_CONFIG_FILE = join(DATA_DIR, "dws-dashboard.json");
|
|
33
|
+
const DASHBOARD_SERVER_CONFIG_FILE = join(DATA_DIR, "dws-dashboard-server.json");
|
|
34
|
+
const REPLY_HISTORY_FILE = join(DATA_DIR, "dws-replies.json");
|
|
30
35
|
const DEFAULT_DWS_CARD_ROBOT_CODE = "dingn9wrup8mqq1ptabn";
|
|
31
36
|
const DEFAULT_DINGTALK_CLIENT_ID = "";
|
|
32
37
|
const DEFAULT_DINGTALK_CLIENT_SECRET = "";
|
|
@@ -35,13 +40,18 @@ const dwsPath = process.env.DWS_CLI_PATH?.trim() || "dws";
|
|
|
35
40
|
const maxReplyLength = 8_000;
|
|
36
41
|
const cardUpdateIntervalMs = 1_200;
|
|
37
42
|
const historyPollIntervalMs = 3_000;
|
|
43
|
+
const commandPollIntervalMs = 5_000;
|
|
38
44
|
function eventKey(event, groupId) {
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const messageId = event.message_id || event.messageId || event.openMessageId;
|
|
46
|
+
if (messageId?.trim())
|
|
47
|
+
return `${groupId}:message:${messageId.trim()}`;
|
|
41
48
|
if (event.event_id?.trim())
|
|
42
49
|
return `${groupId}:event:${event.event_id.trim()}`;
|
|
43
50
|
return undefined;
|
|
44
51
|
}
|
|
52
|
+
function eventGroupId(event) {
|
|
53
|
+
return event.conversation_id?.trim() || event.conversationId?.trim() || event.openConversationId?.trim();
|
|
54
|
+
}
|
|
45
55
|
function formatDwsTime(date) {
|
|
46
56
|
const part = (value) => String(value).padStart(2, "0");
|
|
47
57
|
return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())} ${part(date.getHours())}:${part(date.getMinutes())}:${part(date.getSeconds())}`;
|
|
@@ -69,9 +79,9 @@ function listGroupMessages(groupId, from) {
|
|
|
69
79
|
conversation_id: message.conversationId,
|
|
70
80
|
create_time: message.createTime,
|
|
71
81
|
message_id: message.messageId,
|
|
72
|
-
sender_open_dingtalk_id: message.senderId,
|
|
82
|
+
sender_open_dingtalk_id: message.senderOpenDingTalkId || message.senderId,
|
|
73
83
|
sender: message.sender ? { name: message.sender } : undefined,
|
|
74
|
-
content: message.text,
|
|
84
|
+
content: message.text || message.content,
|
|
75
85
|
})));
|
|
76
86
|
}
|
|
77
87
|
catch (err) {
|
|
@@ -80,6 +90,37 @@ function listGroupMessages(groupId, from) {
|
|
|
80
90
|
});
|
|
81
91
|
});
|
|
82
92
|
}
|
|
93
|
+
async function searchMonitorCommands(from) {
|
|
94
|
+
const result = await runDwsJson([
|
|
95
|
+
"chat", "+search-msg",
|
|
96
|
+
"--senders", DU_ZHENXUN_OPEN_DINGTALK_ID,
|
|
97
|
+
"--start", from.toISOString(),
|
|
98
|
+
"--end", new Date().toISOString(),
|
|
99
|
+
"--order", "asc",
|
|
100
|
+
"--limit", "50",
|
|
101
|
+
]);
|
|
102
|
+
return (result.messages ?? []).map((message) => ({
|
|
103
|
+
conversation_id: message.conversationId,
|
|
104
|
+
create_time: message.createTime,
|
|
105
|
+
message_id: message.messageId,
|
|
106
|
+
sender_open_dingtalk_id: message.senderOpenDingTalkId || message.senderId,
|
|
107
|
+
sender: message.sender ? { name: message.sender } : undefined,
|
|
108
|
+
content: message.text || message.content,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
async function sendRobotText(groupId, robotCode, content) {
|
|
112
|
+
const result = await runDwsJson([
|
|
113
|
+
"chat", "+messages-send",
|
|
114
|
+
"--as", "bot",
|
|
115
|
+
"--robot-code", robotCode,
|
|
116
|
+
"--groups", groupId,
|
|
117
|
+
"--text", content,
|
|
118
|
+
"--yes",
|
|
119
|
+
]);
|
|
120
|
+
if (result.success === false || (result.failedCount ?? 0) > 0) {
|
|
121
|
+
throw new Error("机器人普通消息发送失败");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
83
124
|
function runDwsJson(args) {
|
|
84
125
|
return new Promise((resolve, reject) => {
|
|
85
126
|
const child = spawn(dwsPath, [...args, "--format", "json"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -168,7 +209,7 @@ async function loadCardState() {
|
|
|
168
209
|
}
|
|
169
210
|
}
|
|
170
211
|
async function saveCardState(state) {
|
|
171
|
-
await mkdir(
|
|
212
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
172
213
|
const temporary = `${CARD_STATE_FILE}.${process.pid}.tmp`;
|
|
173
214
|
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
174
215
|
await rename(temporary, CARD_STATE_FILE);
|
|
@@ -193,48 +234,88 @@ function createDefaultTargets() {
|
|
|
193
234
|
function defaultBotAllowedUserIds(targets) {
|
|
194
235
|
return [...new Set(targets.map((target) => target.senderId).filter(Boolean))];
|
|
195
236
|
}
|
|
196
|
-
|
|
237
|
+
function normalizeDashboardConfig(parsed) {
|
|
238
|
+
if (!Array.isArray(parsed.targets))
|
|
239
|
+
throw new Error("targets is invalid");
|
|
240
|
+
if (parsed.replyFormat !== "markdown" && parsed.replyFormat !== "plain")
|
|
241
|
+
throw new Error("replyFormat is invalid");
|
|
242
|
+
const targets = parsed.targets;
|
|
243
|
+
return {
|
|
244
|
+
...parsed,
|
|
245
|
+
targets,
|
|
246
|
+
botAllowedUserIds: Array.isArray(parsed.botAllowedUserIds) && parsed.botAllowedUserIds.length > 0
|
|
247
|
+
? [...new Set(parsed.botAllowedUserIds.map((id) => id.trim()).filter(Boolean))]
|
|
248
|
+
: defaultBotAllowedUserIds(targets),
|
|
249
|
+
robotName: parsed.robotName?.trim() || DEFAULT_ROBOT_NAME,
|
|
250
|
+
clientId: parsed.clientId?.trim() || DEFAULT_DINGTALK_CLIENT_ID,
|
|
251
|
+
clientSecret: parsed.clientSecret?.trim() || DEFAULT_DINGTALK_CLIENT_SECRET,
|
|
252
|
+
robotCode: parsed.robotCode?.trim() || DEFAULT_DWS_CARD_ROBOT_CODE,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function readStoredDashboardConfig(file) {
|
|
197
256
|
try {
|
|
198
|
-
|
|
199
|
-
if (!Array.isArray(parsed.targets) || parsed.targets.length === 0)
|
|
200
|
-
throw new Error("targets is empty");
|
|
201
|
-
if (parsed.replyFormat !== "markdown" && parsed.replyFormat !== "plain")
|
|
202
|
-
throw new Error("replyFormat is invalid");
|
|
203
|
-
const targets = parsed.targets;
|
|
204
|
-
return {
|
|
205
|
-
...parsed,
|
|
206
|
-
targets,
|
|
207
|
-
botAllowedUserIds: Array.isArray(parsed.botAllowedUserIds) && parsed.botAllowedUserIds.length > 0
|
|
208
|
-
? [...new Set(parsed.botAllowedUserIds.map((id) => id.trim()).filter(Boolean))]
|
|
209
|
-
: defaultBotAllowedUserIds(targets),
|
|
210
|
-
robotName: parsed.robotName?.trim() || DEFAULT_ROBOT_NAME,
|
|
211
|
-
clientId: parsed.clientId?.trim() || DEFAULT_DINGTALK_CLIENT_ID,
|
|
212
|
-
clientSecret: parsed.clientSecret?.trim() || DEFAULT_DINGTALK_CLIENT_SECRET,
|
|
213
|
-
robotCode: parsed.robotCode?.trim() || DEFAULT_DWS_CARD_ROBOT_CODE,
|
|
214
|
-
};
|
|
257
|
+
return normalizeDashboardConfig(JSON.parse(await readFile(file, "utf8")));
|
|
215
258
|
}
|
|
216
259
|
catch (err) {
|
|
217
260
|
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
218
261
|
if (code !== "ENOENT")
|
|
219
|
-
log.warn(`dashboard config ignored: ${String(err)}`);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
targets,
|
|
223
|
-
botAllowedUserIds: defaultBotAllowedUserIds(targets),
|
|
224
|
-
replyFormat: "markdown",
|
|
225
|
-
robotName: DEFAULT_ROBOT_NAME,
|
|
226
|
-
clientId: DEFAULT_DINGTALK_CLIENT_ID,
|
|
227
|
-
clientSecret: DEFAULT_DINGTALK_CLIENT_SECRET,
|
|
228
|
-
robotCode: DEFAULT_DWS_CARD_ROBOT_CODE,
|
|
229
|
-
};
|
|
262
|
+
log.warn(`dashboard config ignored (${file}): ${String(err)}`);
|
|
263
|
+
return undefined;
|
|
230
264
|
}
|
|
231
265
|
}
|
|
232
266
|
async function saveDashboardConfig(config) {
|
|
233
|
-
await mkdir(
|
|
267
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
234
268
|
const temporary = `${DASHBOARD_CONFIG_FILE}.${process.pid}.tmp`;
|
|
235
269
|
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
236
270
|
await rename(temporary, DASHBOARD_CONFIG_FILE);
|
|
237
271
|
}
|
|
272
|
+
function mergeDashboardConfigs(primary, legacy) {
|
|
273
|
+
const targets = [...primary.targets, ...legacy.targets].filter((target, index, all) => all.findIndex((item) => item.groupId === target.groupId && item.senderId === target.senderId) === index);
|
|
274
|
+
return {
|
|
275
|
+
...legacy,
|
|
276
|
+
...primary,
|
|
277
|
+
targets,
|
|
278
|
+
botAllowedUserIds: [...new Set([...primary.botAllowedUserIds, ...legacy.botAllowedUserIds])],
|
|
279
|
+
clientId: primary.clientId || legacy.clientId,
|
|
280
|
+
clientSecret: primary.clientSecret || legacy.clientSecret,
|
|
281
|
+
robotCode: primary.robotCode || legacy.robotCode,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function migrateDashboardConfig() {
|
|
285
|
+
try {
|
|
286
|
+
await readFile(CONFIG_MIGRATION_FILE, "utf8");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// A single migration avoids resurrecting deleted legacy rules after future restarts.
|
|
291
|
+
}
|
|
292
|
+
const primary = await readStoredDashboardConfig(DASHBOARD_CONFIG_FILE);
|
|
293
|
+
const legacy = LEGACY_DATA_DIR === DATA_DIR
|
|
294
|
+
? undefined
|
|
295
|
+
: await readStoredDashboardConfig(join(LEGACY_DATA_DIR, "dws-dashboard.json"));
|
|
296
|
+
if (primary && legacy)
|
|
297
|
+
await saveDashboardConfig(mergeDashboardConfigs(primary, legacy));
|
|
298
|
+
else if (legacy)
|
|
299
|
+
await saveDashboardConfig(legacy);
|
|
300
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
301
|
+
await writeFile(CONFIG_MIGRATION_FILE, "migrated\n", "utf8");
|
|
302
|
+
}
|
|
303
|
+
async function loadDashboardConfig() {
|
|
304
|
+
await migrateDashboardConfig();
|
|
305
|
+
const stored = await readStoredDashboardConfig(DASHBOARD_CONFIG_FILE);
|
|
306
|
+
if (stored)
|
|
307
|
+
return stored;
|
|
308
|
+
const targets = createDefaultTargets();
|
|
309
|
+
return {
|
|
310
|
+
targets,
|
|
311
|
+
botAllowedUserIds: defaultBotAllowedUserIds(targets),
|
|
312
|
+
replyFormat: "markdown",
|
|
313
|
+
robotName: DEFAULT_ROBOT_NAME,
|
|
314
|
+
clientId: DEFAULT_DINGTALK_CLIENT_ID,
|
|
315
|
+
clientSecret: DEFAULT_DINGTALK_CLIENT_SECRET,
|
|
316
|
+
robotCode: DEFAULT_DWS_CARD_ROBOT_CODE,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
238
319
|
function normalizeDashboardServerConfig(value) {
|
|
239
320
|
if (!value || typeof value !== "object")
|
|
240
321
|
throw new Error("dashboard server config is invalid");
|
|
@@ -248,7 +329,7 @@ function normalizeDashboardServerConfig(value) {
|
|
|
248
329
|
return { port, host };
|
|
249
330
|
}
|
|
250
331
|
async function saveDashboardServerConfig(config) {
|
|
251
|
-
await mkdir(
|
|
332
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
252
333
|
const temporary = `${DASHBOARD_SERVER_CONFIG_FILE}.${process.pid}.tmp`;
|
|
253
334
|
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
254
335
|
await rename(temporary, DASHBOARD_SERVER_CONFIG_FILE);
|
|
@@ -289,7 +370,7 @@ async function recordReply(replies, reply) {
|
|
|
289
370
|
replies.unshift(reply);
|
|
290
371
|
if (replies.length > 100)
|
|
291
372
|
replies.length = 100;
|
|
292
|
-
await mkdir(
|
|
373
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
293
374
|
const temporary = `${REPLY_HISTORY_FILE}.${process.pid}.tmp`;
|
|
294
375
|
await writeFile(temporary, `${JSON.stringify(replies, null, 2)}\n`, "utf8");
|
|
295
376
|
await rename(temporary, REPLY_HISTORY_FILE);
|
|
@@ -298,6 +379,53 @@ function groupTitle(groupId, config) {
|
|
|
298
379
|
const target = config.targets.find((item) => item.groupId === groupId);
|
|
299
380
|
return `钉钉群 ${target?.groupName || knownGroupName(groupId)}`;
|
|
300
381
|
}
|
|
382
|
+
function eventGroupName(event, groupId, config) {
|
|
383
|
+
const configuredName = config.targets.find((item) => item.groupId === groupId)?.groupName;
|
|
384
|
+
const eventName = [event.conversation_title, event.conversation_name, event.conversationTitle, event.conversationName]
|
|
385
|
+
.find((value) => typeof value === "string" && Boolean(value.trim()));
|
|
386
|
+
return configuredName || eventName?.trim() || knownGroupName(groupId);
|
|
387
|
+
}
|
|
388
|
+
const conversationNameCache = new Map();
|
|
389
|
+
async function resolveCommandGroupName(groupId, config) {
|
|
390
|
+
const configuredName = config.targets.find((item) => item.groupId === groupId)?.groupName?.trim();
|
|
391
|
+
if (configuredName && configuredName !== groupId && !configuredName.startsWith("cid"))
|
|
392
|
+
return configuredName;
|
|
393
|
+
const cached = conversationNameCache.get(groupId);
|
|
394
|
+
if (cached)
|
|
395
|
+
return cached;
|
|
396
|
+
try {
|
|
397
|
+
const result = await runDwsJson([
|
|
398
|
+
"chat", "+conversation-list", "--page-all", "--page-limit", "5", "--max-items", "500",
|
|
399
|
+
]);
|
|
400
|
+
for (const conversation of result.conversations ?? []) {
|
|
401
|
+
const id = conversation.openConversationId?.trim();
|
|
402
|
+
const name = conversation.conversationName?.trim();
|
|
403
|
+
if (id && name)
|
|
404
|
+
conversationNameCache.set(id, name);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
log.warn(`unable to resolve group name for monitor command: ${String(err)}`);
|
|
409
|
+
}
|
|
410
|
+
return conversationNameCache.get(groupId) || eventGroupName({}, groupId, config);
|
|
411
|
+
}
|
|
412
|
+
async function handleMonitorCommand(command, groupId, getDashboardConfig, updateDashboardConfig) {
|
|
413
|
+
const config = getDashboardConfig();
|
|
414
|
+
if (!config.robotCode.trim())
|
|
415
|
+
throw new Error("无法执行 AI 管理命令:请先在管理页配置机器人 Robot Code");
|
|
416
|
+
const target = {
|
|
417
|
+
groupId,
|
|
418
|
+
groupName: await resolveCommandGroupName(groupId, config),
|
|
419
|
+
senderId: DU_ZHENXUN_OPEN_DINGTALK_ID,
|
|
420
|
+
senderName: "杜振训",
|
|
421
|
+
};
|
|
422
|
+
const result = applyMonitorCommand(config, command, target);
|
|
423
|
+
if (result.changed)
|
|
424
|
+
await updateDashboardConfig(result.config);
|
|
425
|
+
const detail = command === "open" ? "注意啦~本群 映客活动AI 解锁🔓" : "注意啦~本群 映客活动AI 已休眠💤";
|
|
426
|
+
await sendRobotText(groupId, config.robotCode, detail);
|
|
427
|
+
log.info(`monitor command=${command} group=${groupId} changed=${result.changed}`);
|
|
428
|
+
}
|
|
301
429
|
function acceptsTarget(event, config) {
|
|
302
430
|
const groupId = event.conversation_id?.trim();
|
|
303
431
|
if (!groupId)
|
|
@@ -333,6 +461,7 @@ function isMissingCardError(err) {
|
|
|
333
461
|
function getSenderId(event) {
|
|
334
462
|
const sender = event.sender ?? {};
|
|
335
463
|
return event.sender_open_dingtalk_id?.trim() ||
|
|
464
|
+
event.senderOpenDingTalkId?.trim() ||
|
|
336
465
|
event.sender_user_id?.trim() ||
|
|
337
466
|
(typeof sender.openDingtalkId === "string" ? sender.openDingtalkId.trim() : "") ||
|
|
338
467
|
(typeof sender.open_dingtalk_id === "string" ? sender.open_dingtalk_id.trim() : "") ||
|
|
@@ -555,7 +684,7 @@ function resolveGroupIds() {
|
|
|
555
684
|
throw new Error("No DWS group configured");
|
|
556
685
|
return Array.from(new Set(groupIds));
|
|
557
686
|
}
|
|
558
|
-
function startGroupListener(seen, queues, cardState, sessions, cardClient, getDashboardConfig, replies, liveReplies, runtime) {
|
|
687
|
+
function startGroupListener(seen, queues, cardState, sessions, cardClient, getDashboardConfig, updateDashboardConfig, replies, liveReplies, runtime) {
|
|
559
688
|
const args = [
|
|
560
689
|
"event", "consume", DWS_GROUP_MESSAGE_EVENT,
|
|
561
690
|
"--flatten", "--format", "ndjson",
|
|
@@ -566,6 +695,7 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
566
695
|
log.info(`starting ${dwsPath} ${args.join(" ")}`);
|
|
567
696
|
const listener = spawn(dwsPath, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
568
697
|
runtime.eventConnected = true;
|
|
698
|
+
let monitorCommandChain = Promise.resolve();
|
|
569
699
|
const rl = createInterface({ input: listener.stdout });
|
|
570
700
|
listener.stderr.on("data", (chunk) => {
|
|
571
701
|
const message = chunk.toString().trim();
|
|
@@ -573,17 +703,13 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
573
703
|
log.debug(`dws: ${message}`);
|
|
574
704
|
});
|
|
575
705
|
const acceptEvent = (event) => {
|
|
576
|
-
const groupId = event
|
|
706
|
+
const groupId = eventGroupId(event);
|
|
577
707
|
if (!groupId)
|
|
578
708
|
return;
|
|
579
709
|
if (isIgnoredRobotEvent(event)) {
|
|
580
710
|
log.debug(`ignored robot message event=${event.event_id || "unknown"}`);
|
|
581
711
|
return;
|
|
582
712
|
}
|
|
583
|
-
if (!acceptsTarget(event, getDashboardConfig())) {
|
|
584
|
-
log.debug(`ignored unmonitored sender=${getSenderId(event)} event=${event.event_id || "unknown"}`);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
713
|
const key = eventKey(event, groupId);
|
|
588
714
|
if (key && seen.has(key))
|
|
589
715
|
return;
|
|
@@ -593,6 +719,19 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
593
719
|
seen.delete(seen.values().next().value);
|
|
594
720
|
}
|
|
595
721
|
runtime.lastEventAt = new Date().toISOString();
|
|
722
|
+
const command = parseMonitorCommand({ senderId: getSenderId(event), content: event.content || event.text });
|
|
723
|
+
if (command) {
|
|
724
|
+
if (command === "stop")
|
|
725
|
+
queues.get(groupId)?.pending.splice(0);
|
|
726
|
+
monitorCommandChain = monitorCommandChain
|
|
727
|
+
.then(() => handleMonitorCommand(command, groupId, getDashboardConfig, updateDashboardConfig))
|
|
728
|
+
.catch((err) => log.error(`monitor command failed: ${String(err)}`));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!acceptsTarget(event, getDashboardConfig())) {
|
|
732
|
+
log.debug(`ignored unmonitored sender=${getSenderId(event)} event=${event.event_id || "unknown"}`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
596
735
|
void enqueueGroupEvent(event, groupId, queues, cardState, sessions, cardClient, getDashboardConfig, replies, liveReplies, runtime)
|
|
597
736
|
.catch((err) => log.error(String(err)));
|
|
598
737
|
};
|
|
@@ -618,11 +757,33 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
618
757
|
void pollHistory().catch((err) => log.warn(`history poll failed: ${String(err)}`));
|
|
619
758
|
}, historyPollIntervalMs);
|
|
620
759
|
pollTimer.unref();
|
|
760
|
+
let lastCommandPollAt = new Date(Date.now() - 60_000);
|
|
761
|
+
let commandPollInFlight = false;
|
|
762
|
+
const pollCommands = async () => {
|
|
763
|
+
if (commandPollInFlight)
|
|
764
|
+
return;
|
|
765
|
+
commandPollInFlight = true;
|
|
766
|
+
const queryStartedAt = new Date();
|
|
767
|
+
try {
|
|
768
|
+
(await searchMonitorCommands(lastCommandPollAt)).forEach(acceptEvent);
|
|
769
|
+
// Overlap protects messages created during the request; message ID de-duplication is shared with Stream events.
|
|
770
|
+
lastCommandPollAt = new Date(queryStartedAt.getTime() - 1_000);
|
|
771
|
+
}
|
|
772
|
+
finally {
|
|
773
|
+
commandPollInFlight = false;
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
void pollCommands().catch((err) => log.warn(`monitor command poll failed: ${String(err)}`));
|
|
777
|
+
const commandPollTimer = setInterval(() => {
|
|
778
|
+
void pollCommands().catch((err) => log.warn(`monitor command poll failed: ${String(err)}`));
|
|
779
|
+
}, commandPollIntervalMs);
|
|
780
|
+
commandPollTimer.unref();
|
|
621
781
|
return new Promise((resolve, reject) => {
|
|
622
782
|
listener.on("error", reject);
|
|
623
783
|
listener.on("close", (code) => {
|
|
624
784
|
runtime.eventConnected = false;
|
|
625
785
|
clearInterval(pollTimer);
|
|
786
|
+
clearInterval(commandPollTimer);
|
|
626
787
|
const close = code === 0
|
|
627
788
|
? Promise.resolve()
|
|
628
789
|
: Promise.reject(new Error(`dws event consume exited with code ${code}`));
|
|
@@ -661,6 +822,13 @@ async function main() {
|
|
|
661
822
|
const sessions = new Map();
|
|
662
823
|
const cardClient = new DingTalkCardClient();
|
|
663
824
|
let dashboardConfig = await loadDashboardConfig();
|
|
825
|
+
const applyDashboardConfig = async (config) => {
|
|
826
|
+
await saveDashboardConfig(config);
|
|
827
|
+
dashboardConfig = config;
|
|
828
|
+
cardClient.setCredentials(config.clientId, config.clientSecret);
|
|
829
|
+
cardClient.setRobotCode(config.robotCode);
|
|
830
|
+
log.info(`dashboard config applied: ${configuredGroupIds(config).length} group(s), ${config.targets.length} rule(s)`);
|
|
831
|
+
};
|
|
664
832
|
await saveDashboardConfig(dashboardConfig);
|
|
665
833
|
const dashboardServerConfig = await loadDashboardServerConfig();
|
|
666
834
|
cardClient.setCredentials(dashboardConfig.clientId, dashboardConfig.clientSecret);
|
|
@@ -674,13 +842,7 @@ async function main() {
|
|
|
674
842
|
};
|
|
675
843
|
startDashboard(dashboardServerConfig.port, {
|
|
676
844
|
getConfig: () => dashboardConfig,
|
|
677
|
-
updateConfig:
|
|
678
|
-
await saveDashboardConfig(config);
|
|
679
|
-
dashboardConfig = config;
|
|
680
|
-
cardClient.setCredentials(config.clientId, config.clientSecret);
|
|
681
|
-
cardClient.setRobotCode(config.robotCode);
|
|
682
|
-
log.info(`dashboard config applied: ${configuredGroupIds(config).length} group(s), ${config.targets.length} rule(s)`);
|
|
683
|
-
},
|
|
845
|
+
updateConfig: applyDashboardConfig,
|
|
684
846
|
getStatus: () => ({ ...runtime }),
|
|
685
847
|
getReplies: () => [...liveReplies.values(), ...replies],
|
|
686
848
|
searchGroups,
|
|
@@ -688,7 +850,7 @@ async function main() {
|
|
|
688
850
|
}, { host: dashboardServerConfig.host });
|
|
689
851
|
log.info(`dashboard started at http://${dashboardServerConfig.host}:${dashboardServerConfig.port}`);
|
|
690
852
|
log.info(`monitoring ${configuredGroupIds(dashboardConfig).length} group(s) with ${dashboardConfig.targets.length} rule(s)`);
|
|
691
|
-
await startGroupListener(seen, queues, cardState, sessions, cardClient, () => dashboardConfig, replies, liveReplies, runtime);
|
|
853
|
+
await startGroupListener(seen, queues, cardState, sessions, cardClient, () => dashboardConfig, applyDashboardConfig, replies, liveReplies, runtime);
|
|
692
854
|
}
|
|
693
855
|
main().catch((err) => {
|
|
694
856
|
log.error(err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const DU_ZHENXUN_OPEN_DINGTALK_ID = "DZyRu3o9aXvmiSh7BJa5S4EQiEiE";
|
|
2
|
+
export function parseMonitorCommand(event) {
|
|
3
|
+
if (event.senderId !== DU_ZHENXUN_OPEN_DINGTALK_ID)
|
|
4
|
+
return undefined;
|
|
5
|
+
const content = event.content?.replace(/\s+/g, "").toLowerCase();
|
|
6
|
+
if (content === "打开ai" || content === "启动ai" || content === "醒醒")
|
|
7
|
+
return "open";
|
|
8
|
+
if (content === "停止ai" || content === "关闭ai" || content === "睡吧")
|
|
9
|
+
return "stop";
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
export function applyMonitorCommand(config, command, target) {
|
|
13
|
+
const existing = config.targets.find((item) => item.groupId === target.groupId && item.senderId === target.senderId);
|
|
14
|
+
if (command === "open") {
|
|
15
|
+
if (existing) {
|
|
16
|
+
if (existing.groupName === target.groupName && existing.senderName === target.senderName)
|
|
17
|
+
return { config, changed: false };
|
|
18
|
+
return {
|
|
19
|
+
changed: true,
|
|
20
|
+
config: {
|
|
21
|
+
...config,
|
|
22
|
+
targets: config.targets.map((item) => item === existing ? { ...item, groupName: target.groupName, senderName: target.senderName } : item),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { config: { ...config, targets: [...config.targets, target] }, changed: true };
|
|
27
|
+
}
|
|
28
|
+
if (!existing)
|
|
29
|
+
return { config, changed: false };
|
|
30
|
+
return {
|
|
31
|
+
config: {
|
|
32
|
+
...config,
|
|
33
|
+
targets: config.targets.filter((item) => item.groupId !== target.groupId || item.senderId !== target.senderId),
|
|
34
|
+
},
|
|
35
|
+
changed: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/omi-bot.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { open, unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { runApp } from "./index.js";
|
|
5
|
-
const
|
|
6
|
-
const stateDir = join(workspace, ".oh-my-im");
|
|
6
|
+
const stateDir = join(homedir(), ".oh-my-im");
|
|
7
7
|
const dashboardConfigFile = join(stateDir, "dws-dashboard.json");
|
|
8
|
-
const lockFile = join(
|
|
8
|
+
const lockFile = join(stateDir, "omi-bot.lock");
|
|
9
9
|
const defaultWorkDir = "/Users/dds/go/src/git.inke.cn/opd/activitys/inke.activity.service";
|
|
10
10
|
async function acquireLock() {
|
|
11
11
|
let lock;
|
package/dist/omi.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const command = args.find((argument) => !argument.startsWith("-")) ?? "start";
|
|
8
9
|
const noListen = args.includes("--no-listen");
|
|
9
|
-
const
|
|
10
|
-
const stateDir = join(
|
|
10
|
+
const launchWorkspace = process.cwd();
|
|
11
|
+
const stateDir = join(homedir(), ".oh-my-im");
|
|
11
12
|
const stateFile = join(stateDir, "omi-state.json");
|
|
12
|
-
const listenerLockFile = join(workspace, ".oh-my-im-dws-listener.lock");
|
|
13
|
-
const serverConfigFile = join(stateDir, "dws-dashboard-server.json");
|
|
14
13
|
const logFile = join(stateDir, "omi.log");
|
|
15
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const listenerPath = join(here, "dws-listener.js");
|
|
@@ -49,7 +48,7 @@ function pidFromFile(path) {
|
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
function unmanagedListenerPid() {
|
|
52
|
-
const pid = pidFromFile(
|
|
51
|
+
const pid = pidFromFile(join(stateDir, "dws-listener.lock"));
|
|
53
52
|
return pid && isRunning(pid) ? pid : undefined;
|
|
54
53
|
}
|
|
55
54
|
function readState() {
|
|
@@ -72,8 +71,9 @@ function writeState(state) {
|
|
|
72
71
|
mkdirSync(stateDir, { recursive: true });
|
|
73
72
|
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
74
73
|
}
|
|
75
|
-
function dashboardAddress() {
|
|
74
|
+
function dashboardAddress(state) {
|
|
76
75
|
try {
|
|
76
|
+
const serverConfigFile = join(stateDir, "dws-dashboard-server.json");
|
|
77
77
|
const config = JSON.parse(readFileSync(serverConfigFile, "utf8"));
|
|
78
78
|
if (!config.port || !config.host)
|
|
79
79
|
return undefined;
|
|
@@ -84,7 +84,7 @@ function dashboardAddress() {
|
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
function startProcess(role, path) {
|
|
87
|
+
function startProcess(role, path, workspace) {
|
|
88
88
|
if (!existsSync(path))
|
|
89
89
|
throw new Error("omi is not built. Run npm run build first.");
|
|
90
90
|
const logFd = openSync(logFile, "a");
|
|
@@ -98,7 +98,7 @@ function startProcess(role, path) {
|
|
|
98
98
|
throw new Error(`Unable to start omi ${role}`);
|
|
99
99
|
return { role, pid: child.pid };
|
|
100
100
|
}
|
|
101
|
-
function start(mode) {
|
|
101
|
+
function start(mode, workspace = launchWorkspace) {
|
|
102
102
|
const current = readState();
|
|
103
103
|
if (current) {
|
|
104
104
|
console.log(`omi is already running in ${current.mode} mode`);
|
|
@@ -111,9 +111,9 @@ function start(mode) {
|
|
|
111
111
|
mkdirSync(stateDir, { recursive: true });
|
|
112
112
|
const processes = [];
|
|
113
113
|
if (mode === "listen")
|
|
114
|
-
processes.push(startProcess("listener", listenerPath));
|
|
115
|
-
processes.push(startProcess("bot", botPath));
|
|
116
|
-
writeState({ mode, startedAt: new Date().toISOString(), processes });
|
|
114
|
+
processes.push(startProcess("listener", listenerPath, workspace));
|
|
115
|
+
processes.push(startProcess("bot", botPath, workspace));
|
|
116
|
+
writeState({ mode, startedAt: new Date().toISOString(), workspace, processes });
|
|
117
117
|
console.log(`omi started in ${mode === "listen" ? "group listening + one-to-one" : "one-to-one"} mode`);
|
|
118
118
|
console.log(`log: ${logFile}`);
|
|
119
119
|
}
|
|
@@ -148,9 +148,10 @@ function status() {
|
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
console.log(`omi: running (${current.mode === "listen" ? "group listening + one-to-one" : "one-to-one"})`);
|
|
151
|
+
console.log(`workspace: ${current.workspace || homedir()}`);
|
|
151
152
|
for (const managed of current.processes)
|
|
152
153
|
console.log(`${managed.role}: running (pid ${managed.pid})`);
|
|
153
|
-
const address = dashboardAddress();
|
|
154
|
+
const address = dashboardAddress(current);
|
|
154
155
|
if (address)
|
|
155
156
|
console.log(`dashboard: ${address}`);
|
|
156
157
|
console.log(`log: ${logFile}`);
|
|
@@ -171,7 +172,7 @@ else if (command === "update") {
|
|
|
171
172
|
const current = readState();
|
|
172
173
|
const mode = current?.mode ?? "listen";
|
|
173
174
|
await stop();
|
|
174
|
-
start(mode);
|
|
175
|
+
start(mode, current?.workspace || launchWorkspace);
|
|
175
176
|
}
|
|
176
177
|
else {
|
|
177
178
|
console.error(`Unknown command: ${command}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-im",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "git@github.com
|
|
16
|
+
"url": "git+ssh://git@github.com/duzhenxun/oh-my-im.git"
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://github.com/duzhenxun/oh-my-im#readme",
|
|
19
19
|
"publishConfig": {
|