crewly 1.20.147 → 1.20.149
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/config/constants.ts +43 -0
- package/config/roles/developer/prompt.md +2 -1
- package/config/roles/orchestrator/prompt.md +3 -2
- package/config/roles/product-manager/prompt.md +2 -1
- package/config/roles/team-leader/tl-addon.md +11 -2
- package/config/skills/team-leader/set-member-model/SKILL.md +60 -0
- package/config/skills/team-leader/set-member-model/execute.sh +55 -0
- package/config/skills/team-leader/set-member-model/execute.test.sh +76 -0
- package/dist/backend/backend/src/constants.d.ts +75 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +58 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +21 -0
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +11 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +86 -11
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +9 -6
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +22 -16
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts +248 -0
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js +572 -0
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +8 -0
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +12 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.d.ts +200 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js +485 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts +163 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js +176 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-login-runner.d.ts +106 -0
- package/dist/backend/backend/src/services/cloud/cloud-login-runner.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-login-runner.js +238 -0
- package/dist/backend/backend/src/services/cloud/cloud-login-runner.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +16 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +38 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +14 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts +10 -0
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js +41 -3
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-state-persistence.d.ts +9 -0
- package/dist/backend/backend/src/services/session/session-state-persistence.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-state-persistence.js +20 -0
- package/dist/backend/backend/src/services/session/session-state-persistence.js.map +1 -1
- package/dist/backend/backend/src/services/skill/skill-permission.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/skill/skill-permission.service.js +1 -0
- package/dist/backend/backend/src/services/skill/skill-permission.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-owner-direct-dm.d.ts +55 -0
- package/dist/backend/backend/src/services/slack/slack-owner-direct-dm.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-owner-direct-dm.js +114 -0
- package/dist/backend/backend/src/services/slack/slack-owner-direct-dm.js.map +1 -0
- package/dist/backend/backend/src/services/v3/tl-auto-verify.service.d.ts +25 -1
- package/dist/backend/backend/src/services/v3/tl-auto-verify.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/tl-auto-verify.service.js +82 -2
- package/dist/backend/backend/src/services/v3/tl-auto-verify.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +27 -0
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +48 -2
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/settings.types.d.ts +8 -0
- package/dist/backend/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/settings.types.js +4 -0
- package/dist/backend/backend/src/types/settings.types.js.map +1 -1
- package/dist/backend/backend/src/utils/member-default-model.utils.d.ts +66 -0
- package/dist/backend/backend/src/utils/member-default-model.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/member-default-model.utils.js +95 -0
- package/dist/backend/backend/src/utils/member-default-model.utils.js.map +1 -0
- package/dist/backend/backend/src/utils/trigger-workitem-dedupe.utils.d.ts +53 -0
- package/dist/backend/backend/src/utils/trigger-workitem-dedupe.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/trigger-workitem-dedupe.utils.js +75 -0
- package/dist/backend/backend/src/utils/trigger-workitem-dedupe.utils.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +42 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +42 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +75 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +58 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +9 -6
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +22 -16
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +8 -0
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +12 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +16 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +38 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +14 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-state-persistence.d.ts +9 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-state-persistence.js +20 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.js.map +1 -1
- package/dist/cli/backend/src/types/settings.types.d.ts +8 -0
- package/dist/cli/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/settings.types.js +4 -0
- package/dist/cli/backend/src/types/settings.types.js.map +1 -1
- package/dist/cli/backend/src/utils/member-default-model.utils.d.ts +66 -0
- package/dist/cli/backend/src/utils/member-default-model.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/member-default-model.utils.js +95 -0
- package/dist/cli/backend/src/utils/member-default-model.utils.js.map +1 -0
- package/dist/cli/config/constants.d.ts +42 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +42 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud disconnect notice — tells the owner on Slack when this machine has
|
|
3
|
+
* lost Crewly Cloud, and lets them fix it from their phone.
|
|
4
|
+
*
|
|
5
|
+
* Why: inbound Slack reaches a machine only through Cloud → relay. When a
|
|
6
|
+
* machine's Cloud sign-in expires (or Cloud is unreachable) its agents can
|
|
7
|
+
* still post, but every message to them queues in Cloud unseen. On
|
|
8
|
+
* 2026-09-27 a second Mac sat like that for hours; nobody was told, and the
|
|
9
|
+
* owner — never at the machine — could not fix it remotely.
|
|
10
|
+
*
|
|
11
|
+
* What it does, once a minute:
|
|
12
|
+
* 1. Detect ({@link evaluateDisconnect}): signed in (a Cloud config exists)
|
|
13
|
+
* AND (sync is `auth_expired` OR Cloud has not answered for 15 min).
|
|
14
|
+
* 2. When the sign-in is the problem, run `crewly cloud login --no-browser`
|
|
15
|
+
* in a PTY ({@link startCloudLogin}) — the same device pairing the owner
|
|
16
|
+
* would run by hand — and take the approve link it prints.
|
|
17
|
+
* 3. DM the owner through the Slack Web API with the bot tokens this machine
|
|
18
|
+
* already holds (outbound needs no Cloud), link included.
|
|
19
|
+
* 4. The CLI waits for the approval and saves the credentials itself; then
|
|
20
|
+
* CloudSync is restarted and the owner gets 「已重新连上 Cloud…」.
|
|
21
|
+
* If the CLI asks for typed input, the owner's reply is read straight from
|
|
22
|
+
* the DM (`conversations.history`) — it cannot arrive through Cloud.
|
|
23
|
+
*
|
|
24
|
+
* Rate limit: one notice per disconnect episode, repeated at most every 6 h,
|
|
25
|
+
* persisted under the Crewly home so a restart does not re-notify. An
|
|
26
|
+
* expired link is replaced by editing the same message (no new
|
|
27
|
+
* notification); a failed login gets one brief note and waits for the next
|
|
28
|
+
* window. Kill switch: `CREWLY_CLOUD_DISCONNECT_NOTICE=0`.
|
|
29
|
+
*
|
|
30
|
+
* @module services/cloud/cloud-disconnect-notice.service
|
|
31
|
+
*/
|
|
32
|
+
import * as fs from 'fs';
|
|
33
|
+
import * as path from 'path';
|
|
34
|
+
import { CLOUD_DISCONNECT_NOTICE_CONSTANTS } from '../../../../config/constants.js';
|
|
35
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
36
|
+
import { resolveRunningPackageRoot } from '../system/auto-update.utils.js';
|
|
37
|
+
import { CloudClientService } from './cloud-client.service.js';
|
|
38
|
+
import { CloudSyncService } from './cloud-sync.service.js';
|
|
39
|
+
import { isCloudLoginFinished, startCloudLogin } from './cloud-login-runner.js';
|
|
40
|
+
import { RECONNECTED_NOTICE, clearNoticeState, composeDisconnectNotice, composeLoginFailedNotice, evaluateDisconnect, readNoticeState, shouldNotify, writeNoticeState, } from './cloud-disconnect-notice.utils.js';
|
|
41
|
+
const C = CLOUD_DISCONNECT_NOTICE_CONSTANTS;
|
|
42
|
+
/**
|
|
43
|
+
* Owner-facing text for how a login run ended.
|
|
44
|
+
*
|
|
45
|
+
* @param snapshot - The finished run
|
|
46
|
+
* @returns Short Chinese reason
|
|
47
|
+
*/
|
|
48
|
+
function loginFailureDetail(snapshot) {
|
|
49
|
+
if (snapshot.state === 'timed_out')
|
|
50
|
+
return '等待超时';
|
|
51
|
+
if (snapshot.state === 'expired')
|
|
52
|
+
return '链接已过期';
|
|
53
|
+
if (snapshot.message && /denied/i.test(snapshot.message))
|
|
54
|
+
return '登录被拒绝';
|
|
55
|
+
return '登录命令出错';
|
|
56
|
+
}
|
|
57
|
+
/** Watches the Cloud connection and DMs the owner when it is lost. */
|
|
58
|
+
export class CloudDisconnectNoticeService {
|
|
59
|
+
deps;
|
|
60
|
+
thresholdMs;
|
|
61
|
+
repeatMs;
|
|
62
|
+
checkIntervalMs;
|
|
63
|
+
linkWaitMs;
|
|
64
|
+
loginRetryMs;
|
|
65
|
+
replyPollMs;
|
|
66
|
+
replyMaxWaitMs;
|
|
67
|
+
monitorStartedAt = 0;
|
|
68
|
+
timer = null;
|
|
69
|
+
stopped = true;
|
|
70
|
+
ticking = false;
|
|
71
|
+
login = null;
|
|
72
|
+
lastLoginAttemptAt = 0;
|
|
73
|
+
replyTimer = null;
|
|
74
|
+
replyDeadline = 0;
|
|
75
|
+
lastReplyTs = null;
|
|
76
|
+
/** Where the current notice is (for reading the owner's replies) */
|
|
77
|
+
notice = null;
|
|
78
|
+
/** Serialises checks and login outcomes so neither overwrites the other's state */
|
|
79
|
+
chain = Promise.resolve();
|
|
80
|
+
/**
|
|
81
|
+
* @param deps - Collaborators (see {@link CloudDisconnectNoticeDeps})
|
|
82
|
+
*/
|
|
83
|
+
constructor(deps) {
|
|
84
|
+
this.deps = deps;
|
|
85
|
+
this.thresholdMs = deps.thresholdMs ?? C.DISCONNECT_THRESHOLD_MS;
|
|
86
|
+
this.repeatMs = deps.repeatMs ?? C.REPEAT_INTERVAL_MS;
|
|
87
|
+
this.checkIntervalMs = deps.checkIntervalMs ?? C.CHECK_INTERVAL_MS;
|
|
88
|
+
this.linkWaitMs = deps.linkWaitMs ?? C.LINK_WAIT_MS;
|
|
89
|
+
this.loginRetryMs = deps.loginRetryMs ?? C.LOGIN_RETRY_MS;
|
|
90
|
+
this.replyPollMs = deps.replyPollMs ?? C.REPLY_POLL_INTERVAL_MS;
|
|
91
|
+
this.replyMaxWaitMs = deps.replyMaxWaitMs ?? C.REPLY_MAX_WAIT_MS;
|
|
92
|
+
}
|
|
93
|
+
/** Start checking every minute. */
|
|
94
|
+
start() {
|
|
95
|
+
if (!this.stopped)
|
|
96
|
+
return;
|
|
97
|
+
this.stopped = false;
|
|
98
|
+
this.monitorStartedAt = this.deps.now();
|
|
99
|
+
this.scheduleNext();
|
|
100
|
+
}
|
|
101
|
+
/** Stop checking and kill any login run. */
|
|
102
|
+
stop() {
|
|
103
|
+
this.stopped = true;
|
|
104
|
+
if (this.timer !== null)
|
|
105
|
+
this.deps.cancel(this.timer);
|
|
106
|
+
this.timer = null;
|
|
107
|
+
this.stopReplyPolling();
|
|
108
|
+
this.login?.cancel();
|
|
109
|
+
this.login = null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One check. Public for tests; the timer calls it every minute.
|
|
113
|
+
*
|
|
114
|
+
* @returns Resolves when the check (and any notice) is done
|
|
115
|
+
*/
|
|
116
|
+
async tick() {
|
|
117
|
+
if (this.ticking)
|
|
118
|
+
return;
|
|
119
|
+
this.ticking = true;
|
|
120
|
+
try {
|
|
121
|
+
await this.serial(() => this.check());
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.ticking = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Run `fn` after every earlier check / login outcome has finished.
|
|
129
|
+
*
|
|
130
|
+
* @param fn - Work that reads and writes the state file
|
|
131
|
+
* @returns Resolves when `fn` is done (never rejects)
|
|
132
|
+
*/
|
|
133
|
+
serial(fn) {
|
|
134
|
+
const run = this.chain.then(fn).catch((error) => {
|
|
135
|
+
this.deps.logger.warn('Cloud disconnect check failed', { error: error instanceof Error ? error.message : String(error) });
|
|
136
|
+
});
|
|
137
|
+
this.chain = run;
|
|
138
|
+
return run;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The check itself: detect, then notify, refresh the link, or close the episode.
|
|
142
|
+
*/
|
|
143
|
+
async check() {
|
|
144
|
+
const now = this.deps.now();
|
|
145
|
+
const verdict = evaluateDisconnect({
|
|
146
|
+
signedIn: await this.deps.isSignedIn(),
|
|
147
|
+
health: this.deps.getHealth(),
|
|
148
|
+
monitorStartedAt: this.monitorStartedAt || now,
|
|
149
|
+
now,
|
|
150
|
+
thresholdMs: this.thresholdMs,
|
|
151
|
+
});
|
|
152
|
+
const state = readNoticeState(this.deps.stateFile);
|
|
153
|
+
if (verdict.status === 'signed_out') {
|
|
154
|
+
// Logged out on purpose: nothing to tell, nothing to fix.
|
|
155
|
+
if (state)
|
|
156
|
+
await this.endEpisode(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (verdict.status === 'connected') {
|
|
160
|
+
if (state)
|
|
161
|
+
await this.endEpisode(true);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (verdict.status !== 'disconnected')
|
|
165
|
+
return;
|
|
166
|
+
const current = state ?? {
|
|
167
|
+
episodeStartedAt: new Date(verdict.since).toISOString(),
|
|
168
|
+
reason: verdict.reason,
|
|
169
|
+
lastNotifiedAt: null,
|
|
170
|
+
};
|
|
171
|
+
current.reason = verdict.reason;
|
|
172
|
+
if (current.channelId && current.messageTs)
|
|
173
|
+
this.notice = { channelId: current.channelId, ts: current.messageTs };
|
|
174
|
+
if (shouldNotify(state, now, this.repeatMs)) {
|
|
175
|
+
await this.sendNotice(current, verdict.reason);
|
|
176
|
+
}
|
|
177
|
+
else if (this.shouldRefreshLink(current, now)) {
|
|
178
|
+
await this.refreshLink(current);
|
|
179
|
+
}
|
|
180
|
+
writeNoticeState(this.deps.stateFile, current);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Send the notice (first of the episode, or the 6-hourly repeat).
|
|
184
|
+
*
|
|
185
|
+
* @param state - Episode state (updated in place)
|
|
186
|
+
* @param reason - Why
|
|
187
|
+
*/
|
|
188
|
+
async sendNotice(state, reason) {
|
|
189
|
+
const now = this.deps.now();
|
|
190
|
+
const dm = this.deps.getDm();
|
|
191
|
+
if (!dm) {
|
|
192
|
+
this.deps.logger.warn('Disconnected from Crewly Cloud; Slack is not configured here, so the owner was not notified', { reason });
|
|
193
|
+
state.lastNotifiedAt = new Date(now).toISOString();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// A new window lifts the block a failed login set.
|
|
197
|
+
delete state.loginBlockedUntil;
|
|
198
|
+
const link = reason === 'auth' ? await this.ensureLoginLink() : null;
|
|
199
|
+
const text = composeDisconnectNotice({
|
|
200
|
+
deviceName: await this.deviceName(),
|
|
201
|
+
reason,
|
|
202
|
+
since: Date.parse(state.episodeStartedAt),
|
|
203
|
+
loginUrl: link?.url ?? null,
|
|
204
|
+
userCode: link?.userCode ?? null,
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
const posted = await dm.send(text);
|
|
208
|
+
state.lastNotifiedAt = new Date(now).toISOString();
|
|
209
|
+
state.channelId = posted.channelId;
|
|
210
|
+
state.messageTs = posted.ts;
|
|
211
|
+
state.hasLink = !!link?.url;
|
|
212
|
+
this.notice = { channelId: posted.channelId, ts: posted.ts };
|
|
213
|
+
this.lastReplyTs = posted.ts;
|
|
214
|
+
this.deps.logger.info('Owner told that this machine is disconnected from Crewly Cloud', { reason, withLink: state.hasLink });
|
|
215
|
+
if (this.login?.get().needsInput)
|
|
216
|
+
this.startReplyPolling();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Not recorded as notified: the next check tries again.
|
|
220
|
+
this.deps.logger.warn('Could not DM the owner about the Cloud disconnect', { error: error instanceof Error ? error.message : String(error) });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Whether to start a new login run and put its link into the notice:
|
|
225
|
+
* sign-in problem, a notice to edit, no live run, not blocked by a failed
|
|
226
|
+
* login, and not retried within the last few minutes.
|
|
227
|
+
*
|
|
228
|
+
* @param state - Episode state
|
|
229
|
+
* @param now - Now (epoch ms)
|
|
230
|
+
* @returns True to refresh
|
|
231
|
+
*/
|
|
232
|
+
shouldRefreshLink(state, now) {
|
|
233
|
+
if (state.reason !== 'auth' || !state.channelId || !state.messageTs)
|
|
234
|
+
return false;
|
|
235
|
+
if (this.login && !isCloudLoginFinished(this.login.get().state))
|
|
236
|
+
return false;
|
|
237
|
+
if (state.loginBlockedUntil && Date.parse(state.loginBlockedUntil) > now)
|
|
238
|
+
return false;
|
|
239
|
+
return now - this.lastLoginAttemptAt >= this.loginRetryMs;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Start a login run and edit the notice to carry its link (the link in it
|
|
243
|
+
* is missing or has expired). Editing does not notify the owner again.
|
|
244
|
+
*
|
|
245
|
+
* @param state - Episode state (updated in place)
|
|
246
|
+
*/
|
|
247
|
+
async refreshLink(state) {
|
|
248
|
+
const dm = this.deps.getDm();
|
|
249
|
+
if (!dm || !state.channelId || !state.messageTs)
|
|
250
|
+
return;
|
|
251
|
+
const link = await this.ensureLoginLink();
|
|
252
|
+
if (!link?.url)
|
|
253
|
+
return;
|
|
254
|
+
const text = composeDisconnectNotice({
|
|
255
|
+
deviceName: await this.deviceName(),
|
|
256
|
+
reason: 'auth',
|
|
257
|
+
since: Date.parse(state.episodeStartedAt),
|
|
258
|
+
loginUrl: link.url,
|
|
259
|
+
userCode: link.userCode,
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
await dm.update(state.channelId, state.messageTs, text);
|
|
263
|
+
state.hasLink = true;
|
|
264
|
+
this.deps.logger.info('Cloud re-login link in the owner notice refreshed');
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
this.deps.logger.warn('Could not update the Cloud disconnect notice with a new link', { error: error instanceof Error ? error.message : String(error) });
|
|
268
|
+
this.login?.cancel();
|
|
269
|
+
await this.blockUntilNextWindow(state, '链接已过期');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Start (or reuse) a CLI login run and wait briefly for its link.
|
|
274
|
+
*
|
|
275
|
+
* @returns The run's snapshot with a link, or null when none came
|
|
276
|
+
*/
|
|
277
|
+
async ensureLoginLink() {
|
|
278
|
+
if (!this.login || isCloudLoginFinished(this.login.get().state)) {
|
|
279
|
+
this.lastLoginAttemptAt = this.deps.now();
|
|
280
|
+
let handle;
|
|
281
|
+
try {
|
|
282
|
+
handle = this.deps.startLogin();
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
this.deps.logger.warn('Could not start `crewly cloud login`', { error: error instanceof Error ? error.message : String(error) });
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
this.login = handle;
|
|
289
|
+
handle.onChange((snapshot) => {
|
|
290
|
+
if (snapshot.needsInput)
|
|
291
|
+
this.startReplyPolling();
|
|
292
|
+
});
|
|
293
|
+
void handle.done.then((snapshot) => this.serial(() => this.onLoginFinished(handle, snapshot)));
|
|
294
|
+
}
|
|
295
|
+
const snapshot = await this.login.waitForLink(this.linkWaitMs);
|
|
296
|
+
return snapshot.url && !isCloudLoginFinished(snapshot.state) ? snapshot : null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A login run ended.
|
|
300
|
+
*
|
|
301
|
+
* @param handle - The run
|
|
302
|
+
* @param snapshot - How it ended
|
|
303
|
+
*/
|
|
304
|
+
async onLoginFinished(handle, snapshot) {
|
|
305
|
+
if (this.login === handle)
|
|
306
|
+
this.login = null;
|
|
307
|
+
this.stopReplyPolling();
|
|
308
|
+
if (this.stopped || snapshot.state === 'cancelled')
|
|
309
|
+
return;
|
|
310
|
+
if (snapshot.state === 'succeeded') {
|
|
311
|
+
let ok = false;
|
|
312
|
+
try {
|
|
313
|
+
ok = await this.deps.reconnect();
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
this.deps.logger.warn('Cloud login succeeded but reconnecting failed', { error: error instanceof Error ? error.message : String(error) });
|
|
317
|
+
}
|
|
318
|
+
if (ok) {
|
|
319
|
+
this.deps.logger.info('Reconnected to Crewly Cloud after the owner signed in again');
|
|
320
|
+
await this.endEpisode(true);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const state = readNoticeState(this.deps.stateFile);
|
|
325
|
+
if (!state?.lastNotifiedAt)
|
|
326
|
+
return;
|
|
327
|
+
// An expired link is replaced in place by the next check when the
|
|
328
|
+
// notice can be edited; anything else waits for the next window.
|
|
329
|
+
if (snapshot.state === 'expired' && state.channelId && state.messageTs) {
|
|
330
|
+
state.hasLink = false;
|
|
331
|
+
writeNoticeState(this.deps.stateFile, state);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.deps.logger.info('Cloud re-login did not finish', { state: snapshot.state });
|
|
335
|
+
await this.blockUntilNextWindow(state, loginFailureDetail(snapshot));
|
|
336
|
+
writeNoticeState(this.deps.stateFile, state);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Tell the owner briefly that the login did not finish, and hold further
|
|
340
|
+
* login runs until the next notice window.
|
|
341
|
+
*
|
|
342
|
+
* @param state - Episode state (updated in place)
|
|
343
|
+
* @param detail - Owner-facing reason
|
|
344
|
+
*/
|
|
345
|
+
async blockUntilNextWindow(state, detail) {
|
|
346
|
+
const last = state.lastNotifiedAt ? Date.parse(state.lastNotifiedAt) : this.deps.now();
|
|
347
|
+
const retryAt = last + this.repeatMs;
|
|
348
|
+
state.loginBlockedUntil = new Date(retryAt).toISOString();
|
|
349
|
+
state.hasLink = false;
|
|
350
|
+
const dm = this.deps.getDm();
|
|
351
|
+
if (!dm)
|
|
352
|
+
return;
|
|
353
|
+
try {
|
|
354
|
+
await dm.send(composeLoginFailedNotice(detail, retryAt));
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
this.deps.logger.warn('Could not tell the owner the Cloud re-login did not finish', { error: error instanceof Error ? error.message : String(error) });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The episode is over: forget it and, when the owner was told and the
|
|
362
|
+
* machine is back, say so.
|
|
363
|
+
*
|
|
364
|
+
* @param reconnected - Back on Cloud (false = logged out on purpose)
|
|
365
|
+
*/
|
|
366
|
+
async endEpisode(reconnected) {
|
|
367
|
+
const state = readNoticeState(this.deps.stateFile);
|
|
368
|
+
clearNoticeState(this.deps.stateFile);
|
|
369
|
+
this.notice = null;
|
|
370
|
+
this.lastReplyTs = null;
|
|
371
|
+
this.stopReplyPolling();
|
|
372
|
+
if (this.login && !isCloudLoginFinished(this.login.get().state))
|
|
373
|
+
this.login.cancel();
|
|
374
|
+
this.login = null;
|
|
375
|
+
this.lastLoginAttemptAt = 0;
|
|
376
|
+
if (!reconnected || !state?.lastNotifiedAt)
|
|
377
|
+
return;
|
|
378
|
+
const dm = this.deps.getDm();
|
|
379
|
+
if (!dm)
|
|
380
|
+
return;
|
|
381
|
+
try {
|
|
382
|
+
await dm.send(RECONNECTED_NOTICE);
|
|
383
|
+
this.deps.logger.info('Owner told that this machine is back on Crewly Cloud');
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.deps.logger.warn('Could not tell the owner the machine is back on Cloud', { error: error instanceof Error ? error.message : String(error) });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Read the owner's DM replies while the CLI waits for typed input. */
|
|
390
|
+
startReplyPolling() {
|
|
391
|
+
if (this.replyTimer !== null || this.stopped || !this.notice)
|
|
392
|
+
return;
|
|
393
|
+
this.replyDeadline = this.deps.now() + this.replyMaxWaitMs;
|
|
394
|
+
const { channelId, ts: noticeTs } = this.notice;
|
|
395
|
+
const poll = async () => {
|
|
396
|
+
this.replyTimer = null;
|
|
397
|
+
const login = this.login;
|
|
398
|
+
if (this.stopped || !login || !login.get().needsInput)
|
|
399
|
+
return;
|
|
400
|
+
if (this.deps.now() >= this.replyDeadline) {
|
|
401
|
+
this.deps.logger.info('Stopped waiting for the owner to answer the Cloud login in Slack');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const dm = this.deps.getDm();
|
|
406
|
+
const replies = dm ? await dm.readOwnerReplies(channelId, this.lastReplyTs ?? noticeTs) : [];
|
|
407
|
+
const reply = replies[replies.length - 1];
|
|
408
|
+
if (reply) {
|
|
409
|
+
this.lastReplyTs = reply.ts;
|
|
410
|
+
// Never log the reply: it may be a token.
|
|
411
|
+
login.input(reply.text);
|
|
412
|
+
this.deps.logger.info('Owner reply typed into `crewly cloud login`');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
this.deps.logger.warn('Could not read the owner DM for a login reply', { error: error instanceof Error ? error.message : String(error) });
|
|
418
|
+
}
|
|
419
|
+
this.replyTimer = this.deps.schedule(() => void poll(), this.replyPollMs);
|
|
420
|
+
};
|
|
421
|
+
this.replyTimer = this.deps.schedule(() => void poll(), this.replyPollMs);
|
|
422
|
+
}
|
|
423
|
+
/** Stop reading replies. */
|
|
424
|
+
stopReplyPolling() {
|
|
425
|
+
if (this.replyTimer !== null)
|
|
426
|
+
this.deps.cancel(this.replyTimer);
|
|
427
|
+
this.replyTimer = null;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* This machine's name (hostname fallback is the caller's job).
|
|
431
|
+
*
|
|
432
|
+
* @returns The name
|
|
433
|
+
*/
|
|
434
|
+
async deviceName() {
|
|
435
|
+
try {
|
|
436
|
+
return await this.deps.getDeviceName();
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return 'unknown';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** Schedule the next check. */
|
|
443
|
+
scheduleNext() {
|
|
444
|
+
if (this.stopped)
|
|
445
|
+
return;
|
|
446
|
+
this.timer = this.deps.schedule(() => {
|
|
447
|
+
this.timer = null;
|
|
448
|
+
void this.tick().finally(() => this.scheduleNext());
|
|
449
|
+
}, this.checkIntervalMs);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Build the service with the real Cloud config, CloudSync health, CLI login,
|
|
454
|
+
* clock and timers.
|
|
455
|
+
*
|
|
456
|
+
* @param wiring - Server hooks
|
|
457
|
+
* @returns The service (not started)
|
|
458
|
+
*/
|
|
459
|
+
export function createCloudDisconnectNoticeService(wiring) {
|
|
460
|
+
const logger = LoggerService.getInstance().createComponentLogger('CloudDisconnectNotice');
|
|
461
|
+
return new CloudDisconnectNoticeService({
|
|
462
|
+
stateFile: path.join(wiring.crewlyHome, C.STATE_FILE),
|
|
463
|
+
isSignedIn: async () => fs.existsSync(CloudClientService.getConfigPath()),
|
|
464
|
+
getHealth: () => CloudSyncService.getInstance().getHealth(),
|
|
465
|
+
getDeviceName: wiring.getDeviceName,
|
|
466
|
+
getDm: wiring.getDm,
|
|
467
|
+
startLogin: () => {
|
|
468
|
+
const root = resolveRunningPackageRoot(process.argv[1]);
|
|
469
|
+
const cliEntry = root ? path.join(root, C.CLI_ENTRY) : null;
|
|
470
|
+
if (!cliEntry || !fs.existsSync(cliEntry))
|
|
471
|
+
throw new Error('crewly CLI not found next to the running backend');
|
|
472
|
+
return startCloudLogin({ cliEntry });
|
|
473
|
+
},
|
|
474
|
+
reconnect: wiring.reconnect,
|
|
475
|
+
logger,
|
|
476
|
+
now: Date.now,
|
|
477
|
+
schedule: (fn, ms) => {
|
|
478
|
+
const t = setTimeout(fn, ms);
|
|
479
|
+
t.unref?.();
|
|
480
|
+
return t;
|
|
481
|
+
},
|
|
482
|
+
cancel: (handle) => clearTimeout(handle),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
//# sourceMappingURL=cloud-disconnect-notice.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-disconnect-notice.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/cloud/cloud-disconnect-notice.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud disconnect notice — pure rules.
|
|
3
|
+
*
|
|
4
|
+
* Detection ({@link evaluateDisconnect}), the notice rate limit
|
|
5
|
+
* ({@link shouldNotify}), the owner-facing Chinese texts and the persisted
|
|
6
|
+
* state file. No timers, no network: {@link CloudDisconnectNoticeService}
|
|
7
|
+
* drives these.
|
|
8
|
+
*
|
|
9
|
+
* Background (2026-09-27): a second Mac stopped heart-beating to Crewly
|
|
10
|
+
* Cloud and never reconnected, while its agents kept posting to Slack with
|
|
11
|
+
* their own bot tokens. Inbound Slack only reaches a machine through Cloud →
|
|
12
|
+
* relay, so every DM to its agents queued in Cloud for hours and nobody was
|
|
13
|
+
* told.
|
|
14
|
+
*
|
|
15
|
+
* @module services/cloud/cloud-disconnect-notice.utils
|
|
16
|
+
*/
|
|
17
|
+
import type { CloudSyncHealth } from './cloud-sync.types.js';
|
|
18
|
+
/** Why this machine cannot talk to Cloud. */
|
|
19
|
+
export type DisconnectReason = 'auth' | 'unreachable';
|
|
20
|
+
/** Inputs to {@link evaluateDisconnect}. */
|
|
21
|
+
export interface DisconnectInput {
|
|
22
|
+
/** A Cloud config exists on disk (the machine was signed in and nobody logged out) */
|
|
23
|
+
signedIn: boolean;
|
|
24
|
+
/** CloudSyncService health */
|
|
25
|
+
health: CloudSyncHealth;
|
|
26
|
+
/** When the monitor started (epoch ms) — the baseline when sync never ran */
|
|
27
|
+
monitorStartedAt: number;
|
|
28
|
+
/** Now (epoch ms) */
|
|
29
|
+
now: number;
|
|
30
|
+
/** No successful Cloud request for this long = disconnected (ms) */
|
|
31
|
+
thresholdMs: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Result of {@link evaluateDisconnect}:
|
|
35
|
+
* - `signed_out` — no Cloud config (never signed in, or logged out on purpose)
|
|
36
|
+
* - `connected` — Cloud answered within the threshold
|
|
37
|
+
* - `pending` — no answer yet since this start, still within the threshold
|
|
38
|
+
* - `disconnected` — see `reason`; `since` is the last contact (or the start)
|
|
39
|
+
*/
|
|
40
|
+
export type DisconnectVerdict = {
|
|
41
|
+
status: 'signed_out' | 'connected' | 'pending';
|
|
42
|
+
} | {
|
|
43
|
+
status: 'disconnected';
|
|
44
|
+
reason: DisconnectReason;
|
|
45
|
+
since: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether this machine is disconnected from Cloud.
|
|
49
|
+
*
|
|
50
|
+
* Disconnected = signed in AND (sync is `auth_expired`, OR Cloud has not
|
|
51
|
+
* answered a request for `thresholdMs`). Never fires on a machine that was
|
|
52
|
+
* never signed in or was logged out on purpose — both leave no Cloud config.
|
|
53
|
+
* `connected` needs a real answer from Cloud, so a restart that has not
|
|
54
|
+
* reached Cloud yet reads `pending`, not `connected`.
|
|
55
|
+
*
|
|
56
|
+
* @param input - Sign-in, health, clock and threshold
|
|
57
|
+
* @returns The verdict
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* evaluateDisconnect({ signedIn: true, health: { state: 'auth_expired', lastContactAt: t0, startedAt: t0, authRejected: true },
|
|
62
|
+
* monitorStartedAt: t0, now: t0 + 60_000, thresholdMs: 900_000 });
|
|
63
|
+
* // { status: 'disconnected', reason: 'auth', since: t0 }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function evaluateDisconnect(input: DisconnectInput): DisconnectVerdict;
|
|
67
|
+
/** State persisted under the Crewly home so a restart does not re-notify. */
|
|
68
|
+
export interface DisconnectNoticeState {
|
|
69
|
+
/** When this disconnect episode began (ISO) */
|
|
70
|
+
episodeStartedAt: string;
|
|
71
|
+
/** Last reason seen */
|
|
72
|
+
reason: DisconnectReason;
|
|
73
|
+
/** When the owner was last notified (ISO), null = not yet */
|
|
74
|
+
lastNotifiedAt: string | null;
|
|
75
|
+
/** Slack DM channel of the last notice (for edits and the follow-up) */
|
|
76
|
+
channelId?: string;
|
|
77
|
+
/** Slack ts of the last notice (for edits) */
|
|
78
|
+
messageTs?: string;
|
|
79
|
+
/** Whether the last notice carries a working login link */
|
|
80
|
+
hasLink?: boolean;
|
|
81
|
+
/** No further login runs before this time (ISO) — set after a failed login */
|
|
82
|
+
loginBlockedUntil?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether a new notice may go out: none yet in this episode, or the last one
|
|
86
|
+
* is at least `repeatMs` old.
|
|
87
|
+
*
|
|
88
|
+
* @param state - Persisted state (null = no episode yet)
|
|
89
|
+
* @param now - Now (epoch ms)
|
|
90
|
+
* @param repeatMs - Minimum gap between notices
|
|
91
|
+
* @returns True when a notice may be sent
|
|
92
|
+
*/
|
|
93
|
+
export declare function shouldNotify(state: DisconnectNoticeState | null, now: number, repeatMs: number): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether the notice is switched on (`CREWLY_CLOUD_DISCONNECT_NOTICE`).
|
|
96
|
+
*
|
|
97
|
+
* @param value - The env value
|
|
98
|
+
* @returns False for `0`, `false`, `off`, `no`; true otherwise (default on)
|
|
99
|
+
*/
|
|
100
|
+
export declare function isNoticeEnabled(value: string | undefined): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Local time in the owner's reading format, e.g. `9月27日 23:38`.
|
|
103
|
+
*
|
|
104
|
+
* @param ms - Epoch ms
|
|
105
|
+
* @returns Local month/day hour:minute
|
|
106
|
+
*/
|
|
107
|
+
export declare function formatLocalTime(ms: number): string;
|
|
108
|
+
/** Inputs to {@link composeDisconnectNotice}. */
|
|
109
|
+
export interface DisconnectNoticeInput {
|
|
110
|
+
/** This machine's name */
|
|
111
|
+
deviceName: string;
|
|
112
|
+
/** Why */
|
|
113
|
+
reason: DisconnectReason;
|
|
114
|
+
/** Episode start (epoch ms) */
|
|
115
|
+
since: number;
|
|
116
|
+
/** Re-login link (the approve page with the code filled in), when one is live */
|
|
117
|
+
loginUrl?: string | null;
|
|
118
|
+
/** Code shown on the approve page, when known */
|
|
119
|
+
userCode?: string | null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The notice DMed to the owner.
|
|
123
|
+
*
|
|
124
|
+
* @param input - Device, reason, start time and the login link (if any)
|
|
125
|
+
* @returns Plain text (the sender escapes it for Slack)
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* composeDisconnectNotice({ deviceName: 'iriss-air.lan', reason: 'auth', since, loginUrl: 'https://crewlyai.com/cloud/pair?code=ABCD-2345' });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export declare function composeDisconnectNotice(input: DisconnectNoticeInput): string;
|
|
133
|
+
/** Follow-up once the machine is back. */
|
|
134
|
+
export declare const RECONNECTED_NOTICE = "\u5DF2\u91CD\u65B0\u8FDE\u4E0A Cloud\uFF0C\u6392\u961F\u7684\u6D88\u606F\u6B63\u5728\u9001\u8FBE\u3002";
|
|
135
|
+
/**
|
|
136
|
+
* Brief note when a re-login did not finish.
|
|
137
|
+
*
|
|
138
|
+
* @param detail - What happened (e.g. 登录被拒绝)
|
|
139
|
+
* @param retryAt - When the next link goes out (epoch ms)
|
|
140
|
+
* @returns The note
|
|
141
|
+
*/
|
|
142
|
+
export declare function composeLoginFailedNotice(detail: string, retryAt: number): string;
|
|
143
|
+
/**
|
|
144
|
+
* Read the persisted state.
|
|
145
|
+
*
|
|
146
|
+
* @param file - State file
|
|
147
|
+
* @returns The state, or null when missing / unreadable
|
|
148
|
+
*/
|
|
149
|
+
export declare function readNoticeState(file: string): DisconnectNoticeState | null;
|
|
150
|
+
/**
|
|
151
|
+
* Persist the state.
|
|
152
|
+
*
|
|
153
|
+
* @param file - State file
|
|
154
|
+
* @param state - State to write
|
|
155
|
+
*/
|
|
156
|
+
export declare function writeNoticeState(file: string, state: DisconnectNoticeState): void;
|
|
157
|
+
/**
|
|
158
|
+
* Remove the state (episode over).
|
|
159
|
+
*
|
|
160
|
+
* @param file - State file
|
|
161
|
+
*/
|
|
162
|
+
export declare function clearNoticeState(file: string): void;
|
|
163
|
+
//# sourceMappingURL=cloud-disconnect-notice.utils.d.ts.map
|