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.
@@ -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
- const timer = setInterval(() => {
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
- }, Math.max(1, Number(options.getConfig().historyPollIntervalSeconds || DEFAULT_POLL_INTERVAL_SECONDS)) * 1_000);
93
- timer.unref();
94
- return () => { stopped = true; clearInterval(timer); };
100
+ }, 1_000);
101
+ scheduler.unref();
102
+ return () => { stopped = true; clearInterval(scheduler); };
95
103
  }
@@ -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 && senderValues.includes(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 intervalMs = Math.max(500, getDashboardConfig().cardUpdateIntervalMs || 3_000);
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
- const remaining = intervalMs - (Date.now() - lastCardUpdateAt);
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
- if (!current) {
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
- for (const managed of current.processes)
190
- process.kill(managed.pid, "SIGTERM");
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 (current.processes.some((process) => isRunning(process.pid)) && Date.now() < deadline) {
241
+ while (pids.some((pid) => isRunning(pid)) && Date.now() < deadline) {
193
242
  await new Promise((resolve) => setTimeout(resolve, 100));
194
243
  }
195
- const active = current.processes.filter((process) => isRunning(process.pid));
196
- if (active.length > 0)
197
- throw new Error(`omi did not stop within 5 seconds (pid ${active.map((item) => item.pid).join(", ")})`);
198
- unlinkSync(stateFile);
199
- console.log("omi stopped");
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-im",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "author": {
5
5
  "name": "杜振训",
6
6
  "email": "duzhenxun@126.com"