oh-my-im 0.1.8 → 0.1.10
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/dws-history.js +12 -4
- package/dist/dws-listener.js +32 -6
- package/dist/omi.js +67 -9
- package/package.json +1 -1
package/dist/dws-history.js
CHANGED
|
@@ -87,9 +87,17 @@ export function startDwsHistoryPolling(options) {
|
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
void poll().catch((err) => log.warn(`history poll failed: ${String(err)}`));
|
|
90
|
-
|
|
90
|
+
// Use a short scheduler tick instead of fixing the interval at startup, so
|
|
91
|
+
// changing 拉取间隔(秒) in the Web UI affects the current polling process.
|
|
92
|
+
let lastPollStartedAt = Date.now();
|
|
93
|
+
const scheduler = setInterval(() => {
|
|
94
|
+
const seconds = Number(options.getConfig().historyPollIntervalSeconds);
|
|
95
|
+
const intervalMs = Math.max(1, Number.isFinite(seconds) ? seconds : DEFAULT_POLL_INTERVAL_SECONDS) * 1_000;
|
|
96
|
+
if (Date.now() - lastPollStartedAt < intervalMs)
|
|
97
|
+
return;
|
|
98
|
+
lastPollStartedAt = Date.now();
|
|
91
99
|
void poll().catch((err) => log.warn(`history poll failed: ${String(err)}`));
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
return () => { stopped = true; clearInterval(
|
|
100
|
+
}, 1_000);
|
|
101
|
+
scheduler.unref();
|
|
102
|
+
return () => { stopped = true; clearInterval(scheduler); };
|
|
95
103
|
}
|
package/dist/dws-listener.js
CHANGED
|
@@ -666,9 +666,10 @@ function isIgnoredRobotEvent(event, config) {
|
|
|
666
666
|
return normalized.includes("robotcode") || normalized === "botid" || normalized === "openbotid";
|
|
667
667
|
});
|
|
668
668
|
};
|
|
669
|
+
const senderDisplay = senderDisplayName(event).trim().toLowerCase();
|
|
669
670
|
const isRobot = robotMarker(sender) ||
|
|
670
|
-
(configuredRobotId &&
|
|
671
|
-
(robotName && senderValues.some((value) => value.includes(robotName)));
|
|
671
|
+
(configuredRobotId && senderId === configuredRobotId) ||
|
|
672
|
+
(robotName && (senderDisplay === robotName || senderValues.some((value) => value.includes(robotName))));
|
|
672
673
|
if (isRobot)
|
|
673
674
|
return true;
|
|
674
675
|
// DWS history records are not consistent about exposing the sender name.
|
|
@@ -719,6 +720,7 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
|
|
|
719
720
|
const sessionId = sessions.get(sessionKey);
|
|
720
721
|
log.info(`processing batch size=${events.length} groupSession=${sessionId ? "resume" : "new"}`);
|
|
721
722
|
let card;
|
|
723
|
+
let stopCardUpdates = () => undefined;
|
|
722
724
|
let latestVisibleContent = `${label} 正在处理...`;
|
|
723
725
|
let elapsedTimer;
|
|
724
726
|
const title = `【${label}】`;
|
|
@@ -783,13 +785,23 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
|
|
|
783
785
|
let pendingCardUpdate;
|
|
784
786
|
let pendingCardTimer;
|
|
785
787
|
let cardUpdateInFlight;
|
|
788
|
+
let cardUpdatesStopped = false;
|
|
789
|
+
stopCardUpdates = () => {
|
|
790
|
+
cardUpdatesStopped = true;
|
|
791
|
+
if (pendingCardTimer)
|
|
792
|
+
clearTimeout(pendingCardTimer);
|
|
793
|
+
pendingCardTimer = undefined;
|
|
794
|
+
pendingCardUpdate = undefined;
|
|
795
|
+
};
|
|
786
796
|
// Pi emits very small deltas quickly. Coalesce them into one complete card
|
|
787
797
|
// snapshot per second so DingTalk receives fresh content without a backlog.
|
|
788
|
-
const
|
|
798
|
+
const getCardIntervalMs = () => Math.max(500, getDashboardConfig().cardUpdateIntervalMs || 3_000);
|
|
789
799
|
const pumpCardUpdate = () => {
|
|
790
|
-
if (cardUpdateInFlight || !pendingCardUpdate)
|
|
800
|
+
if (cardUpdatesStopped || cardUpdateInFlight || !pendingCardUpdate)
|
|
791
801
|
return;
|
|
792
|
-
|
|
802
|
+
// Read the setting for every update. A Web change therefore affects an
|
|
803
|
+
// already running task, not only the next task.
|
|
804
|
+
const remaining = getCardIntervalMs() - (Date.now() - lastCardUpdateAt);
|
|
793
805
|
if (remaining > 0) {
|
|
794
806
|
if (!pendingCardTimer) {
|
|
795
807
|
pendingCardTimer = setTimeout(() => {
|
|
@@ -819,7 +831,7 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
|
|
|
819
831
|
}
|
|
820
832
|
if (cardUpdateInFlight)
|
|
821
833
|
await cardUpdateInFlight;
|
|
822
|
-
if (pendingCardUpdate) {
|
|
834
|
+
if (pendingCardUpdate && !cardUpdatesStopped) {
|
|
823
835
|
const latest = pendingCardUpdate;
|
|
824
836
|
pendingCardUpdate = undefined;
|
|
825
837
|
await cardClient.update(activeCard, latest.title, formatReply(latest.content, getDashboardConfig().replyFormat))
|
|
@@ -827,6 +839,8 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
|
|
|
827
839
|
}
|
|
828
840
|
};
|
|
829
841
|
const updateCard = (title, content) => {
|
|
842
|
+
if (cardUpdatesStopped)
|
|
843
|
+
return;
|
|
830
844
|
pendingCardUpdate = { title, content };
|
|
831
845
|
pumpCardUpdate();
|
|
832
846
|
};
|
|
@@ -905,6 +919,10 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
|
|
|
905
919
|
if (card) {
|
|
906
920
|
const stopped = queue?.paused === true;
|
|
907
921
|
const stoppedContent = latestVisibleContent;
|
|
922
|
+
// Cancel the interval timer and all delayed updates before the final
|
|
923
|
+
// paused/failed card update. This prevents a queued update from arriving
|
|
924
|
+
// after stop and changing the title back to "处理中".
|
|
925
|
+
stopCardUpdates();
|
|
908
926
|
await cardClient.update(card, stopped ? finishedTitle("🔴", "处理暂停") : finishedTitle("❌", "处理失败"), stopped ? stoppedContent : `${label} 处理失败:${message.slice(0, 2_000)}`)
|
|
909
927
|
.catch((updateErr) => log.warn(`card failure update skipped: ${String(updateErr)}`));
|
|
910
928
|
cardState.cards[groupId] = { cardBizId: card.cardBizId, status: stopped ? "failed" : "failed" };
|
|
@@ -1023,6 +1041,14 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
1023
1041
|
log.debug(`ignored robot message event=${event.event_id || "unknown"}`);
|
|
1024
1042
|
return;
|
|
1025
1043
|
}
|
|
1044
|
+
// A bot acknowledgement may be returned by history before the sender
|
|
1045
|
+
// metadata is complete. Never feed our own steer acknowledgement back into
|
|
1046
|
+
// the active Pi task, even if robot detection missed that event.
|
|
1047
|
+
const eventContent = (event.content || event.text || "").trim();
|
|
1048
|
+
if (queues.get(groupId)?.running && /^已将这条消息作为引导发送给当前 Pi 任务[。.!!]?$/u.test(eventContent)) {
|
|
1049
|
+
log.debug(`ignored self steer acknowledgement group=${groupId}`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1026
1052
|
const key = eventKey(event, groupId);
|
|
1027
1053
|
if (key && seen.has(key))
|
|
1028
1054
|
return;
|
package/dist/omi.js
CHANGED
|
@@ -98,6 +98,44 @@ function isRunning(pid) {
|
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
function omiProcesses() {
|
|
102
|
+
try {
|
|
103
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" });
|
|
104
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
105
|
+
return [];
|
|
106
|
+
return result.stdout.split("\\n").flatMap((line) => {
|
|
107
|
+
const match = line.trim().match(/^(\\d+)\\s+(\\d+)\\s+(.+)$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
return [];
|
|
110
|
+
return [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[3] }];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function allOmiProcessIds() {
|
|
118
|
+
const processes = omiProcesses();
|
|
119
|
+
const ids = new Set();
|
|
120
|
+
// Match only this application's built workers. Do not use a broad
|
|
121
|
+
// "oh-my-im" substring that could terminate an unrelated shell/editor.
|
|
122
|
+
processes.filter((item) => item.command.includes("/dist/omi.js") || item.command.includes("/dist/dashboard-worker.js") || item.command.includes("/dist/dws-listener.js") || item.command.includes("/dist/bot-worker.js"))
|
|
123
|
+
.forEach((item) => ids.add(item.pid));
|
|
124
|
+
// Include Agent/DWS descendants spawned by our workers, such as codex/pi and
|
|
125
|
+
// dws child processes, so stop really leaves no worker behind.
|
|
126
|
+
let changed = true;
|
|
127
|
+
while (changed) {
|
|
128
|
+
changed = false;
|
|
129
|
+
processes.forEach((item) => {
|
|
130
|
+
if (ids.has(item.ppid) && !ids.has(item.pid)) {
|
|
131
|
+
ids.add(item.pid);
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
ids.delete(process.pid);
|
|
137
|
+
return [...ids];
|
|
138
|
+
}
|
|
101
139
|
function pidFromFile(path) {
|
|
102
140
|
try {
|
|
103
141
|
const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
@@ -182,21 +220,41 @@ function start(mode, workspace = launchWorkspace) {
|
|
|
182
220
|
}
|
|
183
221
|
async function stop() {
|
|
184
222
|
const current = readState();
|
|
185
|
-
|
|
223
|
+
const statePids = current?.processes.map((managed) => managed.pid) ?? [];
|
|
224
|
+
const pids = [...new Set([...statePids, ...allOmiProcessIds()])].filter((pid) => pid !== process.pid && isRunning(pid));
|
|
225
|
+
if (pids.length === 0) {
|
|
226
|
+
if (existsSync(stateFile))
|
|
227
|
+
unlinkSync(stateFile);
|
|
186
228
|
console.log("omi is not running");
|
|
187
229
|
return false;
|
|
188
230
|
}
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
// Stop every omi worker and its descendants, including stale workers left by
|
|
232
|
+
// an older state file. Escalate after five seconds so restart cannot overlap
|
|
233
|
+
// with the previous listener/bot instance.
|
|
234
|
+
for (const pid of pids) {
|
|
235
|
+
try {
|
|
236
|
+
process.kill(pid, "SIGTERM");
|
|
237
|
+
}
|
|
238
|
+
catch { /* already exited */ }
|
|
239
|
+
}
|
|
191
240
|
const deadline = Date.now() + 5_000;
|
|
192
|
-
while (
|
|
241
|
+
while (pids.some((pid) => isRunning(pid)) && Date.now() < deadline) {
|
|
193
242
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
194
243
|
}
|
|
195
|
-
const active =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
244
|
+
const active = pids.filter((pid) => isRunning(pid));
|
|
245
|
+
for (const pid of active) {
|
|
246
|
+
try {
|
|
247
|
+
process.kill(pid, "SIGKILL");
|
|
248
|
+
}
|
|
249
|
+
catch { /* already exited */ }
|
|
250
|
+
}
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
252
|
+
const remaining = pids.filter((pid) => isRunning(pid));
|
|
253
|
+
if (remaining.length > 0)
|
|
254
|
+
throw new Error(`omi did not stop (pid ${remaining.join(", ")})`);
|
|
255
|
+
if (existsSync(stateFile))
|
|
256
|
+
unlinkSync(stateFile);
|
|
257
|
+
console.log(`omi stopped (${pids.length} process(es))`);
|
|
200
258
|
return true;
|
|
201
259
|
}
|
|
202
260
|
function status() {
|