mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-17 00:53:22 +08:00
fix: preserve Telegram compatibility and stabilize Office
This commit is contained in:
parent
7f114e1c36
commit
24e690498b
23 changed files with 1679 additions and 314 deletions
|
|
@ -95,6 +95,7 @@ export class OfficeJobExecutor {
|
|||
#transport;
|
||||
#createHarness;
|
||||
#logger;
|
||||
#sleep;
|
||||
#active = new Map();
|
||||
#queued = new Set();
|
||||
#completed = new Set();
|
||||
|
|
@ -107,7 +108,13 @@ export class OfficeJobExecutor {
|
|||
lastJobAt: null,
|
||||
};
|
||||
|
||||
constructor({ config, transport, createHarness, logger = console }) {
|
||||
constructor({
|
||||
config,
|
||||
transport,
|
||||
createHarness,
|
||||
logger = console,
|
||||
sleepImpl = sleep,
|
||||
}) {
|
||||
if (!config || !transport || typeof createHarness !== 'function') {
|
||||
throw new TypeError('OfficeJobExecutor requires config, transport, and createHarness');
|
||||
}
|
||||
|
|
@ -115,6 +122,7 @@ export class OfficeJobExecutor {
|
|||
this.#transport = transport;
|
||||
this.#createHarness = createHarness;
|
||||
this.#logger = logger;
|
||||
this.#sleep = sleepImpl;
|
||||
}
|
||||
|
||||
get status() { return structuredClone(this.#status); }
|
||||
|
|
@ -256,18 +264,10 @@ export class OfficeJobExecutor {
|
|||
|
||||
async #renew(jobId, entry) {
|
||||
while (!entry.controller.signal.aborted) {
|
||||
try { await sleep(RENEW_MS, undefined, { signal: entry.controller.signal }); }
|
||||
try { await this.#sleep(RENEW_MS, undefined, { signal: entry.controller.signal }); }
|
||||
catch { return; }
|
||||
try {
|
||||
await this.#transport.renewJob(jobId, entry.leaseToken, { signal: entry.controller.signal });
|
||||
const snapshot = await this.#transport.getJob(jobId, { signal: entry.controller.signal });
|
||||
const approval = snapshot?.job?.approval;
|
||||
if (approval && (approval.status === 'approved' || approval.status === 'rejected')) {
|
||||
entry.approvals.get(approval.id)?.resolve({
|
||||
decision: approval.status,
|
||||
answer: clean(approval.answer),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (entry.controller.signal.aborted) return;
|
||||
entry.cancelled = true;
|
||||
|
|
@ -280,6 +280,22 @@ export class OfficeJobExecutor {
|
|||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const snapshot = await this.#transport.getJob(jobId, { signal: entry.controller.signal });
|
||||
const approval = snapshot?.job?.approval;
|
||||
if (approval && (approval.status === 'approved' || approval.status === 'rejected')) {
|
||||
entry.approvals.get(approval.id)?.resolve({
|
||||
decision: approval.status,
|
||||
answer: clean(approval.answer),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (entry.controller.signal.aborted) return;
|
||||
this.#logger.warn?.(
|
||||
`[dsh-im:office] Job ${jobId} approval poll failed; will retry after the next renewal:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,15 +22,25 @@ export class OfficeRuntime {
|
|||
#token;
|
||||
#logger;
|
||||
#transport;
|
||||
#sleep;
|
||||
#controller = null;
|
||||
#task = null;
|
||||
#status;
|
||||
#jobs;
|
||||
|
||||
constructor({ config, token, logger = console, transport, createHarness, jobExecutor }) {
|
||||
constructor({
|
||||
config,
|
||||
token,
|
||||
logger = console,
|
||||
transport,
|
||||
createHarness,
|
||||
jobExecutor,
|
||||
sleepImpl = sleep,
|
||||
}) {
|
||||
this.#config = config;
|
||||
this.#token = token;
|
||||
this.#logger = logger;
|
||||
this.#sleep = sleepImpl;
|
||||
this.#transport = transport ?? new OfficeTransport({
|
||||
baseUrl: config.baseUrl, deviceId: config.deviceId, token,
|
||||
});
|
||||
|
|
@ -91,7 +101,10 @@ export class OfficeRuntime {
|
|||
const heartbeat = await this.#transport.heartbeat(this.capabilities(), { signal: attemptSignal });
|
||||
this.#offerJobs(heartbeat?.jobs);
|
||||
this.#status.lastHeartbeatAt = new Date().toISOString();
|
||||
const heartbeatTask = this.#heartbeatLoop(attemptSignal);
|
||||
let streamOpened = false;
|
||||
const heartbeatTask = this.#heartbeatLoop(attemptSignal, () => {
|
||||
if (streamOpened && !attemptSignal.aborted) attempt = 0;
|
||||
});
|
||||
const stream = this.#transport.stream({
|
||||
signal: attemptSignal,
|
||||
lastEventId: this.#status.lastEventId,
|
||||
|
|
@ -99,7 +112,7 @@ export class OfficeRuntime {
|
|||
this.#status.connected = true;
|
||||
this.#status.state = 'connected';
|
||||
this.#status.error = null;
|
||||
attempt = 0;
|
||||
streamOpened = true;
|
||||
},
|
||||
onEvent: async (event) => {
|
||||
this.#status.lastEventAt = new Date().toISOString();
|
||||
|
|
@ -112,13 +125,14 @@ export class OfficeRuntime {
|
|||
await Promise.race([stream, heartbeatTask]);
|
||||
} catch (error) {
|
||||
if (signal.aborted) break;
|
||||
attemptController.abort();
|
||||
this.#status.connected = false;
|
||||
this.#status.state = 'reconnecting';
|
||||
this.#status.error = safeConnectionError(error);
|
||||
this.#status.reconnects += 1;
|
||||
const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)];
|
||||
attempt += 1;
|
||||
try { await sleep(delay, undefined, { signal }); } catch { break; }
|
||||
try { await this.#sleep(delay, undefined, { signal }); } catch { break; }
|
||||
} finally {
|
||||
attemptController.abort();
|
||||
}
|
||||
|
|
@ -127,12 +141,13 @@ export class OfficeRuntime {
|
|||
this.#status.state = 'idle';
|
||||
}
|
||||
|
||||
async #heartbeatLoop(signal) {
|
||||
async #heartbeatLoop(signal, onSuccess) {
|
||||
while (!signal.aborted) {
|
||||
await sleep(this.#config.heartbeatSeconds * 1_000, undefined, { signal });
|
||||
await this.#sleep(this.#config.heartbeatSeconds * 1_000, undefined, { signal });
|
||||
const heartbeat = await this.#transport.heartbeat(this.capabilities(), { signal });
|
||||
this.#offerJobs(heartbeat?.jobs);
|
||||
this.#status.lastHeartbeatAt = new Date().toISOString();
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue