crewly 1.20.127 → 1.20.129
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/templates/growth-marketing-team.json +12 -1
- package/config/templates/personal-assistant-team.json +28 -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 +81 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +49 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +135 -106
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/onboarding/index.d.ts +1 -0
- package/dist/backend/backend/src/controllers/onboarding/index.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/onboarding/index.js +1 -0
- package/dist/backend/backend/src/controllers/onboarding/index.js.map +1 -1
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.controller.d.ts +38 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.controller.js +106 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.routes.d.ts +24 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.routes.js +34 -0
- package/dist/backend/backend/src/controllers/onboarding/onboarding-checklist.routes.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +48 -1
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +5 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +33 -0
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.js +63 -0
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/relogin-agent-resumer.service.d.ts +76 -0
- package/dist/backend/backend/src/services/agent/relogin-agent-resumer.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/relogin-agent-resumer.service.js +108 -0
- package/dist/backend/backend/src/services/agent/relogin-agent-resumer.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +10 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +361 -0
- package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-relogin.service.js +756 -0
- package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness.service.d.ts +16 -1
- package/dist/backend/backend/src/services/harness/harness.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/harness/harness.service.js +28 -1
- package/dist/backend/backend/src/services/harness/harness.service.js.map +1 -1
- package/dist/backend/backend/src/services/harness/harness.types.d.ts +16 -1
- package/dist/backend/backend/src/services/harness/harness.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/harness/harness.types.js.map +1 -1
- package/dist/backend/backend/src/services/harness/login-expiry-rules.d.ts +75 -0
- package/dist/backend/backend/src/services/harness/login-expiry-rules.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/login-expiry-rules.js +159 -0
- package/dist/backend/backend/src/services/harness/login-expiry-rules.js.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.d.ts +45 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.d.ts.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js +103 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts +301 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js +365 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-state.store.d.ts +89 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-state.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-state.store.js +122 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-state.store.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +21 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +39 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +80 -0
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +127 -0
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -0
- package/dist/backend/backend/src/services/template/template.service.d.ts +15 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +22 -1
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/types/team-template.types.d.ts +30 -0
- package/dist/backend/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/team-template.types.js +24 -0
- package/dist/backend/backend/src/types/team-template.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- 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 +81 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +33 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js +63 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -1
- package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +361 -0
- package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-relogin.service.js +756 -0
- package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness.service.d.ts +16 -1
- package/dist/cli/backend/src/services/harness/harness.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/harness/harness.service.js +28 -1
- package/dist/cli/backend/src/services/harness/harness.service.js.map +1 -1
- package/dist/cli/backend/src/services/harness/harness.types.d.ts +16 -1
- package/dist/cli/backend/src/services/harness/harness.types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/harness/harness.types.js.map +1 -1
- package/dist/cli/backend/src/services/harness/login-expiry-rules.d.ts +75 -0
- package/dist/cli/backend/src/services/harness/login-expiry-rules.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/login-expiry-rules.js +159 -0
- package/dist/cli/backend/src/services/harness/login-expiry-rules.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts +301 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js +365 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-state.store.d.ts +89 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-state.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-state.store.js +122 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-state.store.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-tier.service.d.ts +116 -0
- package/dist/cli/backend/src/services/skill/skill-tier.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-tier.service.js +155 -0
- package/dist/cli/backend/src/services/skill/skill-tier.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +21 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +39 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/services/template/template.service.d.ts +152 -0
- package/dist/cli/backend/src/services/template/template.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/template/template.service.js +506 -0
- package/dist/cli/backend/src/services/template/template.service.js.map +1 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts +30 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/team-template.types.js +24 -0
- package/dist/cli/backend/src/types/team-template.types.js.map +1 -1
- package/dist/cli/cli/src/commands/onboard-checklist.d.ts +152 -0
- package/dist/cli/cli/src/commands/onboard-checklist.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/onboard-checklist.js +241 -0
- package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -0
- package/dist/cli/cli/src/commands/onboard.d.ts +87 -6
- package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/onboard.js +203 -37
- package/dist/cli/cli/src/commands/onboard.js.map +1 -1
- package/dist/cli/cli/src/constants.d.ts +5 -0
- package/dist/cli/cli/src/constants.d.ts.map +1 -1
- package/dist/cli/cli/src/constants.js +5 -0
- package/dist/cli/cli/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +2 -1
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/templates.d.ts +27 -0
- package/dist/cli/cli/src/utils/templates.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/templates.js +29 -0
- package/dist/cli/cli/src/utils/templates.js.map +1 -1
- package/frontend/dist/assets/index-2bdb57b9.css +42 -0
- package/frontend/dist/assets/{index-aaf65055.js → index-5b98bf77.js} +285 -285
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-0a2f783c.css +0 -42
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness re-login coordinator — Phase 2 of onboarding: re-login over Slack.
|
|
3
|
+
*
|
|
4
|
+
* When a harness login expires, Crewly notices by itself, starts ONE login
|
|
5
|
+
* broker session for that harness, DMs the owner what to do on their phone,
|
|
6
|
+
* and resumes the agents that were stuck once the login succeeds. Nobody
|
|
7
|
+
* has to touch the machine.
|
|
8
|
+
*
|
|
9
|
+
* Inputs:
|
|
10
|
+
* - {@link HarnessReloginService.reportExpiry} — called by the OAuth
|
|
11
|
+
* re-login monitor when an agent's terminal output matches an expiry rule
|
|
12
|
+
* (login-expiry-rules.ts), and by the periodic status check of the
|
|
13
|
+
* orchestrator's harness ({@link HarnessReloginService.checkOrcHarness}).
|
|
14
|
+
* - {@link HarnessReloginService.handleOwnerReply} — an owner's Slack DM
|
|
15
|
+
* reply. A reply is consumed only when it belongs to a flow: Claude's
|
|
16
|
+
* authorization code (while Claude's login is waiting for it and the reply
|
|
17
|
+
* looks like a code), the retry keyword, or a reply to an unrecognised
|
|
18
|
+
* login screen. Everything else goes through the normal chat path.
|
|
19
|
+
* - The broker's `update` / `finished` events.
|
|
20
|
+
*
|
|
21
|
+
* Rules:
|
|
22
|
+
* - One flow per harness. Repeated detections only add stuck sessions. A
|
|
23
|
+
* flow that failed is restarted by a new detection at most once per
|
|
24
|
+
* {@link HARNESS_CONSTANTS.RELOGIN.REMIND_INTERVAL_MS} (the re-reminder),
|
|
25
|
+
* or at once when the owner replies `relogin` / `重新登录`.
|
|
26
|
+
* - If Crewly holds an API key for the harness it is used silently (no DM).
|
|
27
|
+
* - Claude → `subscription` (`claude setup-token`), Codex → `device`.
|
|
28
|
+
* - Secrets: Claude's code is passed to the broker and never stored, logged
|
|
29
|
+
* or echoed; DMs are built from {@link LoginSession} snapshots, which never
|
|
30
|
+
* contain a token, and the screen text is redacted again.
|
|
31
|
+
*
|
|
32
|
+
* @module services/harness/harness-relogin.service
|
|
33
|
+
*/
|
|
34
|
+
import { HARNESS_CONSTANTS } from '../../constants.js';
|
|
35
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
36
|
+
import { getHarnessCredentialsStore } from './harness-credentials.store.js';
|
|
37
|
+
import { getBrokerLoginMethod, getHarnessDefinition } from './harness-registry.js';
|
|
38
|
+
import { getHarnessService } from './harness.service.js';
|
|
39
|
+
import { SILENT_HARNESS_LOGGER, isHarnessId, isTerminalLoginState, } from './harness.types.js';
|
|
40
|
+
import { LOGIN_BROKER_EVENTS } from './login-broker.service.js';
|
|
41
|
+
import { redactSecrets } from './login-rules.js';
|
|
42
|
+
/** Harnesses the coordinator can log in (they have a broker method). */
|
|
43
|
+
function hasBrokerLogin(harnessId) {
|
|
44
|
+
return getBrokerLoginMethod(harnessId) !== undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Display name of a harness.
|
|
48
|
+
*
|
|
49
|
+
* @param harnessId - Harness id
|
|
50
|
+
* @returns e.g. "Claude Code"
|
|
51
|
+
*/
|
|
52
|
+
function displayName(harnessId) {
|
|
53
|
+
return getHarnessDefinition(harnessId)?.displayName ?? harnessId;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether a reply asks to start the login over.
|
|
57
|
+
*
|
|
58
|
+
* @param text - Owner reply
|
|
59
|
+
* @returns True for `relogin`, `re-login` or `重新登录`
|
|
60
|
+
*/
|
|
61
|
+
export function isRetryKeyword(text) {
|
|
62
|
+
const normalized = text.trim().toLowerCase();
|
|
63
|
+
return HARNESS_CONSTANTS.RELOGIN.RETRY_KEYWORDS.some((keyword) => keyword.toLowerCase() === normalized);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Strip formatting Slack clients add around a pasted value (backticks).
|
|
67
|
+
*
|
|
68
|
+
* @param text - Owner reply
|
|
69
|
+
* @returns The bare value, trimmed
|
|
70
|
+
*/
|
|
71
|
+
export function unwrapReply(text) {
|
|
72
|
+
return text.trim().replace(/^`+|`+$/g, '').trim();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a reply plausibly is Claude's authorization code: one token with
|
|
76
|
+
* no whitespace and a plausible length.
|
|
77
|
+
*
|
|
78
|
+
* @param text - Unwrapped owner reply
|
|
79
|
+
* @returns True when it can be typed into `claude setup-token`
|
|
80
|
+
*/
|
|
81
|
+
export function looksLikeAuthCode(text) {
|
|
82
|
+
const { CODE_MIN_LENGTH, CODE_MAX_LENGTH } = HARNESS_CONSTANTS.RELOGIN;
|
|
83
|
+
return text.length >= CODE_MIN_LENGTH && text.length <= CODE_MAX_LENGTH && !/\s/.test(text);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* "2 agents are waiting: a, b" — capped list of waiting agents.
|
|
87
|
+
*
|
|
88
|
+
* @param agents - Session names
|
|
89
|
+
* @returns Sentence fragment
|
|
90
|
+
*/
|
|
91
|
+
export function describeWaitingAgents(agents) {
|
|
92
|
+
if (agents.length === 0)
|
|
93
|
+
return '目前没有 agent 在用它。';
|
|
94
|
+
const max = HARNESS_CONSTANTS.RELOGIN.DM_MAX_LISTED_AGENTS;
|
|
95
|
+
const listed = agents.slice(0, max).join('、');
|
|
96
|
+
const more = agents.length > max ? ` 等 ${agents.length} 个` : '';
|
|
97
|
+
return `${listed}${more} 在等它。`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Keep text safe for a DM: redact secrets, drop code fences, cap length (tail).
|
|
101
|
+
*
|
|
102
|
+
* @param text - Screen or message text
|
|
103
|
+
* @param max - Maximum length
|
|
104
|
+
* @returns Safe text
|
|
105
|
+
*/
|
|
106
|
+
function safeForDm(text, max) {
|
|
107
|
+
const redacted = redactSecrets(text).replace(/```/g, "'''").trim();
|
|
108
|
+
return redacted.length > max ? redacted.slice(redacted.length - max) : redacted;
|
|
109
|
+
}
|
|
110
|
+
/** Retry hint appended to failure DMs. */
|
|
111
|
+
const RETRY_HINT = '回复「重新登录」(或 `relogin`)再试一次。';
|
|
112
|
+
/**
|
|
113
|
+
* Prefix an owner reply needs before it is typed into an unrecognised login
|
|
114
|
+
* screen. Without it, any one-line DM to the orc during the login would have
|
|
115
|
+
* been swallowed by the terminal.
|
|
116
|
+
*/
|
|
117
|
+
export const SCREEN_REPLY_PREFIXES = ['输入', 'input'];
|
|
118
|
+
/**
|
|
119
|
+
* The text after a screen-reply prefix ("输入 1" → "1"), or null.
|
|
120
|
+
*
|
|
121
|
+
* @param reply - Owner reply (already unwrapped)
|
|
122
|
+
* @returns Text to type, or null when the reply has no prefix
|
|
123
|
+
*/
|
|
124
|
+
export function stripScreenReplyPrefix(reply) {
|
|
125
|
+
for (const prefix of SCREEN_REPLY_PREFIXES) {
|
|
126
|
+
const re = new RegExp(`^${prefix}[\\s::]+`, 'i');
|
|
127
|
+
if (re.test(reply))
|
|
128
|
+
return reply.replace(re, '');
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* DM with the sign-in link (and Codex's one-time code on its own line).
|
|
134
|
+
*
|
|
135
|
+
* @param session - Broker session (never contains a secret)
|
|
136
|
+
* @param waiting - Agents waiting on the login
|
|
137
|
+
* @returns Slack mrkdwn text
|
|
138
|
+
*/
|
|
139
|
+
export function formatLinkDm(session, waiting) {
|
|
140
|
+
const name = displayName(session.harnessId);
|
|
141
|
+
const lines = [`*${name} 登录过期了。* ${describeWaitingAgents(waiting)}`, ''];
|
|
142
|
+
if (session.method === 'device') {
|
|
143
|
+
lines.push('1. 在手机上打开这个链接:', session.url ?? '', '', '2. 输入这个验证码:', session.userCode ?? '', '');
|
|
144
|
+
lines.push('在手机上完成登录就行,这边会自动继续。');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
lines.push('1. 在手机上打开这个链接并授权:', session.url ?? '', '');
|
|
148
|
+
lines.push('2. 把授权后页面显示的代码直接回复在这里(只要代码)。', '');
|
|
149
|
+
lines.push('Crewly 会自动填进去,agent 自己接着干。');
|
|
150
|
+
}
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* DM for a login screen the broker did not recognise.
|
|
155
|
+
*
|
|
156
|
+
* @param session - Broker session
|
|
157
|
+
* @param waiting - Agents waiting on the login
|
|
158
|
+
* @returns Slack mrkdwn text
|
|
159
|
+
*/
|
|
160
|
+
export function formatScreenDm(session, waiting) {
|
|
161
|
+
const screen = safeForDm(session.screen, HARNESS_CONSTANTS.RELOGIN.DM_SCREEN_MAX_CHARS) || '(empty screen)';
|
|
162
|
+
return [
|
|
163
|
+
`*${displayName(session.harnessId)} 登录过期了。* ${describeWaitingAgents(waiting)}`,
|
|
164
|
+
'',
|
|
165
|
+
'Crewly 已经启动了登录,但没认出它的界面。现在显示的是:',
|
|
166
|
+
'```',
|
|
167
|
+
screen,
|
|
168
|
+
'```',
|
|
169
|
+
'要往这个终端里输入,回复「输入 <内容>」(例如「输入 1」)。其他消息照常发给 Orc。',
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* DM after the harness rejected the owner's reply (e.g. a wrong code).
|
|
174
|
+
*
|
|
175
|
+
* @param session - Broker session (awaiting input again)
|
|
176
|
+
* @returns Slack mrkdwn text
|
|
177
|
+
*/
|
|
178
|
+
export function formatRejectedDm(session) {
|
|
179
|
+
const reason = safeForDm(session.message ?? '', HARNESS_CONSTANTS.RELOGIN.DM_MESSAGE_MAX_CHARS);
|
|
180
|
+
const what = session.method === 'subscription' ? '代码' : '输入';
|
|
181
|
+
return `这个${what}没通过${reason ? `(${reason})` : ''}。再回复一次${what},或者${RETRY_HINT}`;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* DM after the login succeeded and the agents were restarted.
|
|
185
|
+
*
|
|
186
|
+
* @param harnessId - Harness
|
|
187
|
+
* @param result - Resume result
|
|
188
|
+
* @returns Slack mrkdwn text
|
|
189
|
+
*/
|
|
190
|
+
export function formatSuccessDm(harnessId, result) {
|
|
191
|
+
const count = result.resumed.length;
|
|
192
|
+
let text = `好了:${displayName(harnessId)} 已重新登录,${count} 个 agent 已恢复。`;
|
|
193
|
+
if (result.failed.length > 0) {
|
|
194
|
+
text += ` 没能重启:${result.failed.join('、')}。`;
|
|
195
|
+
}
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* DM after the login failed or timed out (sent once per failure).
|
|
200
|
+
*
|
|
201
|
+
* @param harnessId - Harness
|
|
202
|
+
* @param reason - Broker message (redacted here)
|
|
203
|
+
* @returns Slack mrkdwn text
|
|
204
|
+
*/
|
|
205
|
+
export function formatFailureDm(harnessId, reason) {
|
|
206
|
+
const detail = safeForDm(reason ?? '', HARNESS_CONSTANTS.RELOGIN.DM_MESSAGE_MAX_CHARS);
|
|
207
|
+
return `${displayName(harnessId)} 登录没完成${detail ? `:${detail}` : '。'}${RETRY_HINT}`;
|
|
208
|
+
}
|
|
209
|
+
/** Coordinates Slack re-login flows, one per harness. */
|
|
210
|
+
export class HarnessReloginService {
|
|
211
|
+
broker;
|
|
212
|
+
credentials;
|
|
213
|
+
apiKeys;
|
|
214
|
+
checkLoginState;
|
|
215
|
+
getOrcHarness;
|
|
216
|
+
notifier;
|
|
217
|
+
resumer;
|
|
218
|
+
now;
|
|
219
|
+
logger;
|
|
220
|
+
flows = new Map();
|
|
221
|
+
/** Expiry reports are ignored until then (resumed transcripts repeat old errors) */
|
|
222
|
+
quietUntil = new Map();
|
|
223
|
+
/** Last silent API-key recovery per harness */
|
|
224
|
+
silentKeyAt = new Map();
|
|
225
|
+
/** Sessions restarted after a login: screen-sweep reports are ignored until live output reports again */
|
|
226
|
+
mutedScreens = new Set();
|
|
227
|
+
/** Harnesses the status check has seen logged in */
|
|
228
|
+
seenLoggedIn = new Set();
|
|
229
|
+
statusTimer = null;
|
|
230
|
+
statusCheckInFlight = false;
|
|
231
|
+
/**
|
|
232
|
+
* @param deps - Dependencies
|
|
233
|
+
*/
|
|
234
|
+
constructor(deps) {
|
|
235
|
+
this.broker = deps.broker;
|
|
236
|
+
this.credentials = deps.credentials;
|
|
237
|
+
this.apiKeys = deps.apiKeys;
|
|
238
|
+
this.checkLoginState = deps.checkLoginState;
|
|
239
|
+
this.getOrcHarness = deps.getOrcHarness;
|
|
240
|
+
this.notifier = deps.notifier ?? null;
|
|
241
|
+
this.resumer = deps.resumer ?? null;
|
|
242
|
+
this.now = deps.now ?? Date.now;
|
|
243
|
+
this.logger = deps.logger ?? SILENT_HARNESS_LOGGER;
|
|
244
|
+
this.broker.on(LOGIN_BROKER_EVENTS.UPDATE, (session) => this.handleUpdate(session));
|
|
245
|
+
this.broker.on(LOGIN_BROKER_EVENTS.FINISHED, (session) => this.handleFinished(session));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Set how the owner is DM'd (the Slack adapter; wired after Slack is up).
|
|
249
|
+
*
|
|
250
|
+
* @param notifier - Notifier, or null
|
|
251
|
+
*/
|
|
252
|
+
setNotifier(notifier) {
|
|
253
|
+
this.notifier = notifier;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Set how agents are restarted after a login.
|
|
257
|
+
*
|
|
258
|
+
* @param resumer - Resumer, or null
|
|
259
|
+
*/
|
|
260
|
+
setResumer(resumer) {
|
|
261
|
+
this.resumer = resumer;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Report an expired harness login.
|
|
265
|
+
*
|
|
266
|
+
* @param report - Which harness, which session, from where
|
|
267
|
+
* @returns True when the coordinator owns the harness's re-login (the
|
|
268
|
+
* caller must not start its own `/login` or notice); false for a
|
|
269
|
+
* harness Crewly cannot log in (no broker method)
|
|
270
|
+
*/
|
|
271
|
+
reportExpiry(report) {
|
|
272
|
+
const { harnessId, sessionName, source } = report;
|
|
273
|
+
if (!hasBrokerLogin(harnessId))
|
|
274
|
+
return false;
|
|
275
|
+
// Right after a login, resumed agents replay their old transcript
|
|
276
|
+
// (including the old error): ignore every report for a while.
|
|
277
|
+
if (this.now() < (this.quietUntil.get(harnessId) ?? 0))
|
|
278
|
+
return true;
|
|
279
|
+
if (sessionName) {
|
|
280
|
+
// A resumed session's screen can keep showing the old error: only
|
|
281
|
+
// new live output counts for it again.
|
|
282
|
+
if (source === 'screen' && this.mutedScreens.has(sessionName))
|
|
283
|
+
return true;
|
|
284
|
+
if (source === 'output')
|
|
285
|
+
this.mutedScreens.delete(sessionName);
|
|
286
|
+
}
|
|
287
|
+
const existing = this.flows.get(harnessId);
|
|
288
|
+
if (existing) {
|
|
289
|
+
if (sessionName)
|
|
290
|
+
existing.stuck.add(sessionName);
|
|
291
|
+
if (existing.phase === 'failed' && this.now() - (existing.lastDmAt ?? 0) >= HARNESS_CONSTANTS.RELOGIN.REMIND_INTERVAL_MS) {
|
|
292
|
+
this.logger.info('Re-login: reminding the owner', { harnessId, source });
|
|
293
|
+
this.restart(existing);
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
this.logger.warn('Harness login expired — starting a Slack re-login', { harnessId, sessionName, source });
|
|
298
|
+
this.begin(harnessId, new Set(sessionName ? [sessionName] : []));
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Offer an owner's Slack DM reply to the re-login flows.
|
|
303
|
+
*
|
|
304
|
+
* @param text - Reply text (never logged)
|
|
305
|
+
* @returns True when consumed; the caller must then NOT pass it on as a chat message
|
|
306
|
+
*/
|
|
307
|
+
handleOwnerReply(text) {
|
|
308
|
+
if (typeof text !== 'string' || this.flows.size === 0)
|
|
309
|
+
return false;
|
|
310
|
+
if (isRetryKeyword(text)) {
|
|
311
|
+
const flows = [...this.flows.values()];
|
|
312
|
+
const failed = flows.filter((flow) => flow.phase === 'failed');
|
|
313
|
+
const targets = failed.length > 0 ? failed : flows;
|
|
314
|
+
for (const flow of targets)
|
|
315
|
+
this.restart(flow);
|
|
316
|
+
this.logger.info('Re-login restarted on the owner\'s request', { harnessIds: targets.map((flow) => flow.harnessId) });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
const reply = unwrapReply(text);
|
|
320
|
+
for (const flow of this.flows.values()) {
|
|
321
|
+
if (flow.phase !== 'running' || !flow.sessionId)
|
|
322
|
+
continue;
|
|
323
|
+
const session = this.readSession(flow.sessionId);
|
|
324
|
+
if (!session || isTerminalLoginState(session.state))
|
|
325
|
+
continue;
|
|
326
|
+
const takesCode = flow.dm === 'link' && session.method === 'subscription' && session.state === 'awaiting_user' && session.needsInput && looksLikeAuthCode(reply);
|
|
327
|
+
const screenText = flow.dm === 'screen' ? stripScreenReplyPrefix(reply) : null;
|
|
328
|
+
const takesScreenReply = screenText !== null &&
|
|
329
|
+
screenText.length > 0 &&
|
|
330
|
+
screenText.length <= HARNESS_CONSTANTS.RELOGIN.SCREEN_REPLY_MAX_LENGTH &&
|
|
331
|
+
!/[\r\n]/.test(screenText);
|
|
332
|
+
if (!takesCode && !takesScreenReply)
|
|
333
|
+
continue;
|
|
334
|
+
try {
|
|
335
|
+
this.broker.input(flow.sessionId, takesCode ? reply : screenText);
|
|
336
|
+
flow.replied = true;
|
|
337
|
+
flow.lastRejection = null;
|
|
338
|
+
this.logger.info('Owner reply typed into the login', { harnessId: flow.harnessId, sessionId: flow.sessionId });
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// The session ended between the check and the input. The reply
|
|
342
|
+
// looked like a code: still keep it out of the chat.
|
|
343
|
+
this.logger.warn('Could not type the owner reply into the login', {
|
|
344
|
+
harnessId: flow.harnessId,
|
|
345
|
+
error: error instanceof Error ? error.message : String(error),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The pending re-login for a harness (shown by `GET /api/harness`).
|
|
354
|
+
*
|
|
355
|
+
* @param harnessId - Harness id
|
|
356
|
+
* @returns The pending re-login, or null when none is running
|
|
357
|
+
*/
|
|
358
|
+
getPending(harnessId) {
|
|
359
|
+
const flow = this.flows.get(harnessId);
|
|
360
|
+
if (!flow || flow.phase !== 'running' || !flow.sessionId)
|
|
361
|
+
return null;
|
|
362
|
+
return { harnessId, sessionId: flow.sessionId, startedAt: new Date(flow.startedAt).toISOString() };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check the orchestrator's harness with its own status command, and
|
|
366
|
+
* report an expiry when it says logged out. Only a harness that was seen
|
|
367
|
+
* logged in, or that agents are running on, counts (a machine that was
|
|
368
|
+
* never logged in is onboarding's job, not a re-login).
|
|
369
|
+
*
|
|
370
|
+
* @returns Resolves when the check is done; never rejects
|
|
371
|
+
*/
|
|
372
|
+
async checkOrcHarness() {
|
|
373
|
+
if (this.statusCheckInFlight)
|
|
374
|
+
return;
|
|
375
|
+
this.statusCheckInFlight = true;
|
|
376
|
+
try {
|
|
377
|
+
const orc = await this.getOrcHarness();
|
|
378
|
+
if (!isHarnessId(orc) || !hasBrokerLogin(orc))
|
|
379
|
+
return;
|
|
380
|
+
const state = await this.checkLoginState(orc);
|
|
381
|
+
if (state === 'logged_in') {
|
|
382
|
+
this.seenLoggedIn.add(orc);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (state !== 'logged_out')
|
|
386
|
+
return;
|
|
387
|
+
const running = this.resumer?.listSessions(orc) ?? [];
|
|
388
|
+
if (!this.seenLoggedIn.has(orc) && running.length === 0)
|
|
389
|
+
return;
|
|
390
|
+
this.reportExpiry({ harnessId: orc, sessionName: null, source: 'status' });
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
this.logger.warn('Harness status check failed', { error: error instanceof Error ? error.message : String(error) });
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
this.statusCheckInFlight = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Start the periodic status check of the orchestrator's harness.
|
|
401
|
+
*
|
|
402
|
+
* @param intervalMs - Cadence (defaults to STATUS_CHECK_INTERVAL_MS)
|
|
403
|
+
*/
|
|
404
|
+
start(intervalMs = HARNESS_CONSTANTS.RELOGIN.STATUS_CHECK_INTERVAL_MS) {
|
|
405
|
+
if (this.statusTimer)
|
|
406
|
+
return;
|
|
407
|
+
this.statusTimer = setInterval(() => void this.checkOrcHarness(), intervalMs);
|
|
408
|
+
this.statusTimer.unref?.();
|
|
409
|
+
}
|
|
410
|
+
/** Stop the periodic check and forget every flow's timers. */
|
|
411
|
+
stop() {
|
|
412
|
+
if (this.statusTimer)
|
|
413
|
+
clearInterval(this.statusTimer);
|
|
414
|
+
this.statusTimer = null;
|
|
415
|
+
for (const flow of this.flows.values())
|
|
416
|
+
this.clearScreenTimer(flow);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Start a flow: silent API key if Crewly holds one, else a broker login.
|
|
420
|
+
*
|
|
421
|
+
* @param harnessId - Harness
|
|
422
|
+
* @param stuck - Sessions known to be stuck
|
|
423
|
+
*/
|
|
424
|
+
begin(harnessId, stuck) {
|
|
425
|
+
const flow = {
|
|
426
|
+
harnessId,
|
|
427
|
+
phase: 'running',
|
|
428
|
+
sessionId: null,
|
|
429
|
+
startedAt: this.now(),
|
|
430
|
+
stuck,
|
|
431
|
+
dm: 'none',
|
|
432
|
+
lastDmAt: null,
|
|
433
|
+
replied: false,
|
|
434
|
+
lastRejection: null,
|
|
435
|
+
screenTimer: null,
|
|
436
|
+
cancelledByUs: false,
|
|
437
|
+
};
|
|
438
|
+
// Registered synchronously: this is the per-harness debounce.
|
|
439
|
+
this.flows.set(harnessId, flow);
|
|
440
|
+
void this.run(flow);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Body of a flow (async part of {@link begin}).
|
|
444
|
+
*
|
|
445
|
+
* @param flow - The flow
|
|
446
|
+
*/
|
|
447
|
+
async run(flow) {
|
|
448
|
+
if (await this.tryStoredApiKey(flow))
|
|
449
|
+
return;
|
|
450
|
+
if (this.flows.get(flow.harnessId) !== flow)
|
|
451
|
+
return;
|
|
452
|
+
const method = getBrokerLoginMethod(flow.harnessId);
|
|
453
|
+
if (!method)
|
|
454
|
+
return;
|
|
455
|
+
let session;
|
|
456
|
+
try {
|
|
457
|
+
session = this.broker.start(flow.harnessId, method.id);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
461
|
+
this.logger.warn('Re-login: could not start the login', { harnessId: flow.harnessId, error: reason });
|
|
462
|
+
flow.phase = 'failed';
|
|
463
|
+
await this.dm(flow, formatFailureDm(flow.harnessId, `Crewly 没能启动登录(${reason})`));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
flow.sessionId = session.id;
|
|
467
|
+
flow.startedAt = Date.parse(session.startedAt) || this.now();
|
|
468
|
+
flow.screenTimer = setTimeout(() => void this.sendScreenIfUnrecognised(flow), HARNESS_CONSTANTS.RELOGIN.UNRECOGNISED_SCREEN_MS);
|
|
469
|
+
flow.screenTimer.unref?.();
|
|
470
|
+
this.logger.info('Re-login broker session started', { harnessId: flow.harnessId, sessionId: session.id, method: method.id });
|
|
471
|
+
// The broker may hand back a session that already shows its link.
|
|
472
|
+
if (isTerminalLoginState(session.state))
|
|
473
|
+
this.handleFinished(session);
|
|
474
|
+
else
|
|
475
|
+
this.handleUpdate(session);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Use an API key Crewly holds for the harness, without asking the owner.
|
|
479
|
+
* Not repeated within SILENT_KEY_RETRY_WINDOW_MS: a key that did not
|
|
480
|
+
* help falls back to the phone login.
|
|
481
|
+
*
|
|
482
|
+
* @param flow - The flow
|
|
483
|
+
* @returns True when the key was used and the agents were resumed
|
|
484
|
+
*/
|
|
485
|
+
async tryStoredApiKey(flow) {
|
|
486
|
+
const { harnessId } = flow;
|
|
487
|
+
const last = this.silentKeyAt.get(harnessId);
|
|
488
|
+
if (last !== undefined && this.now() - last < HARNESS_CONSTANTS.RELOGIN.SILENT_KEY_RETRY_WINDOW_MS)
|
|
489
|
+
return false;
|
|
490
|
+
try {
|
|
491
|
+
if (harnessId === HARNESS_CONSTANTS.IDS.CLAUDE_CODE) {
|
|
492
|
+
// Agents get ANTHROPIC_API_KEY from the store when they are recreated.
|
|
493
|
+
if (this.credentials.getClaudeCredentialKind() !== 'api_key')
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
else if (harnessId === HARNESS_CONSTANTS.IDS.CODEX_CLI) {
|
|
497
|
+
const key = this.credentials.read().codex?.openaiApiKey;
|
|
498
|
+
if (!key)
|
|
499
|
+
return false;
|
|
500
|
+
await this.apiKeys.submit(harnessId, key);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
this.logger.warn('Re-login: the stored API key did not work, asking the owner instead', {
|
|
508
|
+
harnessId,
|
|
509
|
+
error: error instanceof Error ? error.message : String(error),
|
|
510
|
+
});
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
this.silentKeyAt.set(harnessId, this.now());
|
|
514
|
+
this.logger.info('Re-login: using the stored API key', { harnessId });
|
|
515
|
+
await this.succeed(flow, false);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Broker update: send the link once it is complete, report a rejected reply.
|
|
520
|
+
*
|
|
521
|
+
* @param session - Session snapshot
|
|
522
|
+
*/
|
|
523
|
+
handleUpdate(session) {
|
|
524
|
+
const flow = this.flows.get(session.harnessId);
|
|
525
|
+
if (!flow || flow.phase !== 'running' || flow.sessionId !== session.id || isTerminalLoginState(session.state))
|
|
526
|
+
return;
|
|
527
|
+
const linkReady = session.method === 'device' ? Boolean(session.url && session.userCode) : Boolean(session.url);
|
|
528
|
+
if (flow.dm !== 'link' && linkReady) {
|
|
529
|
+
flow.dm = 'link';
|
|
530
|
+
this.clearScreenTimer(flow);
|
|
531
|
+
void this.dm(flow, formatLinkDm(session, this.waitingAgents(flow)));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (flow.replied && session.state === 'awaiting_user' && session.message && session.message !== flow.lastRejection) {
|
|
535
|
+
flow.lastRejection = session.message;
|
|
536
|
+
flow.replied = false;
|
|
537
|
+
void this.dm(flow, formatRejectedDm(session));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Broker session finished.
|
|
542
|
+
*
|
|
543
|
+
* @param session - Final snapshot
|
|
544
|
+
*/
|
|
545
|
+
handleFinished(session) {
|
|
546
|
+
const flow = this.flows.get(session.harnessId);
|
|
547
|
+
if (!flow)
|
|
548
|
+
return;
|
|
549
|
+
if (flow.sessionId !== session.id) {
|
|
550
|
+
// The owner logged in another way (web / phone app) while a flow waited.
|
|
551
|
+
if (session.state === 'succeeded') {
|
|
552
|
+
this.clearScreenTimer(flow);
|
|
553
|
+
this.cancelOwnSession(flow);
|
|
554
|
+
void this.succeed(flow, true);
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (flow.phase !== 'running')
|
|
559
|
+
return;
|
|
560
|
+
this.clearScreenTimer(flow);
|
|
561
|
+
if (session.state === 'succeeded') {
|
|
562
|
+
void this.succeed(flow, true);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
flow.phase = 'failed';
|
|
566
|
+
if (flow.cancelledByUs)
|
|
567
|
+
return;
|
|
568
|
+
if (session.state === 'cancelled') {
|
|
569
|
+
// Cancelled on the web or by a shutdown: the owner knows; stay quiet.
|
|
570
|
+
this.logger.info('Re-login cancelled', { harnessId: flow.harnessId, sessionId: session.id });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
this.logger.warn('Re-login did not finish', { harnessId: flow.harnessId, sessionId: session.id, state: session.state });
|
|
574
|
+
void this.dm(flow, formatFailureDm(flow.harnessId, session.message));
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* The login is back: restart the stuck agents and tell the owner.
|
|
578
|
+
*
|
|
579
|
+
* @param flow - The flow
|
|
580
|
+
* @param notify - DM the owner (false for the silent API-key path)
|
|
581
|
+
*/
|
|
582
|
+
async succeed(flow, notify) {
|
|
583
|
+
if (this.flows.get(flow.harnessId) === flow)
|
|
584
|
+
this.flows.delete(flow.harnessId);
|
|
585
|
+
this.quietUntil.set(flow.harnessId, this.now() + HARNESS_CONSTANTS.RELOGIN.POST_SUCCESS_QUIET_MS);
|
|
586
|
+
this.seenLoggedIn.add(flow.harnessId);
|
|
587
|
+
const sessions = this.waitingAgents(flow);
|
|
588
|
+
let result = { resumed: [], failed: [] };
|
|
589
|
+
if (this.resumer && sessions.length > 0) {
|
|
590
|
+
try {
|
|
591
|
+
result = await this.resumer.resume(sessions);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
this.logger.warn('Re-login: resuming agents failed', { error: error instanceof Error ? error.message : String(error) });
|
|
595
|
+
result = { resumed: [], failed: [...sessions] };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const name of result.resumed)
|
|
599
|
+
this.mutedScreens.add(name);
|
|
600
|
+
this.logger.info('Re-login finished', { harnessId: flow.harnessId, resumed: result.resumed.length, failed: result.failed.length });
|
|
601
|
+
if (notify)
|
|
602
|
+
await this.dm(flow, formatSuccessDm(flow.harnessId, result));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Start a flow over (retry keyword or re-reminder).
|
|
606
|
+
*
|
|
607
|
+
* @param flow - The flow to replace
|
|
608
|
+
*/
|
|
609
|
+
restart(flow) {
|
|
610
|
+
this.clearScreenTimer(flow);
|
|
611
|
+
this.cancelOwnSession(flow);
|
|
612
|
+
if (this.flows.get(flow.harnessId) === flow)
|
|
613
|
+
this.flows.delete(flow.harnessId);
|
|
614
|
+
this.begin(flow.harnessId, new Set(flow.stuck));
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Cancel the flow's live broker session, marking it as cancelled by the
|
|
618
|
+
* coordinator so no failure DM is sent for it.
|
|
619
|
+
*
|
|
620
|
+
* @param flow - The flow
|
|
621
|
+
*/
|
|
622
|
+
cancelOwnSession(flow) {
|
|
623
|
+
if (flow.phase !== 'running' || !flow.sessionId)
|
|
624
|
+
return;
|
|
625
|
+
flow.cancelledByUs = true;
|
|
626
|
+
try {
|
|
627
|
+
this.broker.cancel(flow.sessionId);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Already gone.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* After UNRECOGNISED_SCREEN_MS without a link: send the screen as-is.
|
|
635
|
+
*
|
|
636
|
+
* @param flow - The flow
|
|
637
|
+
*/
|
|
638
|
+
async sendScreenIfUnrecognised(flow) {
|
|
639
|
+
flow.screenTimer = null;
|
|
640
|
+
if (this.flows.get(flow.harnessId) !== flow || flow.phase !== 'running' || flow.dm !== 'none' || !flow.sessionId)
|
|
641
|
+
return;
|
|
642
|
+
const session = this.readSession(flow.sessionId);
|
|
643
|
+
if (!session || isTerminalLoginState(session.state))
|
|
644
|
+
return;
|
|
645
|
+
flow.dm = 'screen';
|
|
646
|
+
this.logger.warn('Re-login: login screen not recognised, sending it to the owner', { harnessId: flow.harnessId, sessionId: session.id });
|
|
647
|
+
await this.dm(flow, formatScreenDm(session, this.waitingAgents(flow)));
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Agents waiting on a flow: the stuck ones, else every session of the harness.
|
|
651
|
+
*
|
|
652
|
+
* @param flow - The flow
|
|
653
|
+
* @returns Session names
|
|
654
|
+
*/
|
|
655
|
+
waitingAgents(flow) {
|
|
656
|
+
if (flow.stuck.size > 0)
|
|
657
|
+
return [...flow.stuck];
|
|
658
|
+
try {
|
|
659
|
+
return this.resumer?.listSessions(flow.harnessId) ?? [];
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* DM the owner (logs when no notifier is wired; never logs the text).
|
|
667
|
+
*
|
|
668
|
+
* @param flow - The flow
|
|
669
|
+
* @param text - DM text
|
|
670
|
+
*/
|
|
671
|
+
async dm(flow, text) {
|
|
672
|
+
flow.lastDmAt = this.now();
|
|
673
|
+
if (!this.notifier) {
|
|
674
|
+
this.logger.warn('Re-login: no Slack DM path to the owner; finish the login from Setup', { harnessId: flow.harnessId });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const delivered = await this.notifier.sendToOwner(text);
|
|
679
|
+
if (!delivered)
|
|
680
|
+
this.logger.warn('Re-login: the owner DM was not delivered', { harnessId: flow.harnessId });
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
this.logger.warn('Re-login: sending the owner DM failed', {
|
|
684
|
+
harnessId: flow.harnessId,
|
|
685
|
+
error: error instanceof Error ? error.message : String(error),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Read a broker session.
|
|
691
|
+
*
|
|
692
|
+
* @param sessionId - Session id
|
|
693
|
+
* @returns The session, or null when the broker forgot it
|
|
694
|
+
*/
|
|
695
|
+
readSession(sessionId) {
|
|
696
|
+
try {
|
|
697
|
+
return this.broker.get(sessionId);
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Clear a flow's unrecognised-screen timer.
|
|
705
|
+
*
|
|
706
|
+
* @param flow - The flow
|
|
707
|
+
*/
|
|
708
|
+
clearScreenTimer(flow) {
|
|
709
|
+
if (flow.screenTimer)
|
|
710
|
+
clearTimeout(flow.screenTimer);
|
|
711
|
+
flow.screenTimer = null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** Backend singleton. */
|
|
715
|
+
let instance = null;
|
|
716
|
+
/**
|
|
717
|
+
* The backend's re-login coordinator, bound to the backend harness service
|
|
718
|
+
* (its broker, credentials, API keys and status). Registers itself as the
|
|
719
|
+
* `reloginPending` source of `GET /api/harness`.
|
|
720
|
+
*
|
|
721
|
+
* @returns The singleton
|
|
722
|
+
*/
|
|
723
|
+
export function getHarnessReloginService() {
|
|
724
|
+
if (!instance) {
|
|
725
|
+
const harness = getHarnessService();
|
|
726
|
+
const service = new HarnessReloginService({
|
|
727
|
+
broker: harness.broker,
|
|
728
|
+
credentials: getHarnessCredentialsStore(),
|
|
729
|
+
apiKeys: harness.apiKeys,
|
|
730
|
+
checkLoginState: async (harnessId) => {
|
|
731
|
+
const def = getHarnessDefinition(harnessId);
|
|
732
|
+
if (!def)
|
|
733
|
+
return 'unknown';
|
|
734
|
+
const installed = await harness.status.getInstalledInfo(def);
|
|
735
|
+
if (!installed.installed)
|
|
736
|
+
return 'unknown';
|
|
737
|
+
return (await harness.status.getLoginInfo(def, installed.path)).loginState;
|
|
738
|
+
},
|
|
739
|
+
getOrcHarness: () => harness.orc.get(),
|
|
740
|
+
logger: LoggerService.getInstance().createComponentLogger('HarnessRelogin'),
|
|
741
|
+
});
|
|
742
|
+
harness.setReloginPendingProvider((harnessId) => service.getPending(harnessId));
|
|
743
|
+
instance = service;
|
|
744
|
+
}
|
|
745
|
+
return instance;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Replace or clear the singleton (tests).
|
|
749
|
+
*
|
|
750
|
+
* @param service - Service, or null
|
|
751
|
+
*/
|
|
752
|
+
export function setHarnessReloginServiceForTesting(service) {
|
|
753
|
+
instance?.stop();
|
|
754
|
+
instance = service;
|
|
755
|
+
}
|
|
756
|
+
//# sourceMappingURL=harness-relogin.service.js.map
|