crewly 1.20.144 → 1.20.145

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.
Files changed (97) hide show
  1. package/config/roles/orchestrator/prompt.md +9 -0
  2. package/config/skills/orchestrator/harness-login/SKILL.md +88 -0
  3. package/config/skills/orchestrator/harness-login/execute.sh +75 -0
  4. package/config/skills/orchestrator/harness-login/execute.test.sh +72 -0
  5. package/dist/backend/backend/src/constants.d.ts +12 -0
  6. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  7. package/dist/backend/backend/src/constants.js +12 -0
  8. package/dist/backend/backend/src/constants.js.map +1 -1
  9. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts +1 -0
  10. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts.map +1 -1
  11. package/dist/backend/backend/src/controllers/harness/harness.routes.js +3 -0
  12. package/dist/backend/backend/src/controllers/harness/harness.routes.js.map +1 -1
  13. package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts +61 -0
  14. package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts.map +1 -0
  15. package/dist/backend/backend/src/controllers/harness/owner-login.controller.js +133 -0
  16. package/dist/backend/backend/src/controllers/harness/owner-login.controller.js.map +1 -0
  17. package/dist/backend/backend/src/index.d.ts.map +1 -1
  18. package/dist/backend/backend/src/index.js +4 -1
  19. package/dist/backend/backend/src/index.js.map +1 -1
  20. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/agent/agent-registration.service.js +23 -1
  22. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  23. package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
  24. package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/agent/codex-runtime.service.js +18 -0
  26. package/dist/backend/backend/src/services/agent/codex-runtime.service.js.map +1 -1
  27. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -0
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +18 -3
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  31. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +29 -0
  32. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
  34. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  35. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  37. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  38. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
  39. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/harness/harness-relogin.service.js +210 -20
  41. package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  42. package/dist/backend/backend/src/services/harness/owner-login-request.d.ts +87 -0
  43. package/dist/backend/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
  44. package/dist/backend/backend/src/services/harness/owner-login-request.js +191 -0
  45. package/dist/backend/backend/src/services/harness/owner-login-request.js.map +1 -0
  46. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +68 -9
  47. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +121 -11
  49. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -1
  50. package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
  51. package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
  52. package/dist/backend/backend/src/utils/agent-reply-filter.utils.js +127 -0
  53. package/dist/backend/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
  54. package/dist/backend/build-info.json +2 -2
  55. package/dist/cli/backend/src/constants.d.ts +12 -0
  56. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  57. package/dist/cli/backend/src/constants.js +12 -0
  58. package/dist/cli/backend/src/constants.js.map +1 -1
  59. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  60. package/dist/cli/backend/src/services/agent/agent-registration.service.js +23 -1
  61. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  62. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
  63. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
  64. package/dist/cli/backend/src/services/agent/codex-runtime.service.js +18 -0
  65. package/dist/cli/backend/src/services/agent/codex-runtime.service.js.map +1 -1
  66. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -0
  67. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  68. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +18 -3
  69. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  70. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts +29 -0
  71. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  72. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
  73. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  74. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  75. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  76. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  77. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
  78. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  79. package/dist/cli/backend/src/services/harness/harness-relogin.service.js +210 -20
  80. package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  81. package/dist/cli/backend/src/services/harness/owner-login-request.d.ts +87 -0
  82. package/dist/cli/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
  83. package/dist/cli/backend/src/services/harness/owner-login-request.js +191 -0
  84. package/dist/cli/backend/src/services/harness/owner-login-request.js.map +1 -0
  85. package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
  86. package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
  87. package/dist/cli/backend/src/utils/agent-reply-filter.utils.js +127 -0
  88. package/dist/cli/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
  89. package/package.json +1 -1
  90. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +39 -0
  91. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +17 -17
  92. package/packages/crewly-agent/src/runtime/interactive-login-guard.test.ts +60 -0
  93. package/packages/crewly-agent/src/runtime/interactive-login-guard.ts +76 -0
  94. package/packages/crewly-agent/src/runtime/reply-tool-calls.test.ts +53 -0
  95. package/packages/crewly-agent/src/runtime/reply-tool-calls.ts +76 -0
  96. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +8 -0
  97. package/packages/crewly-agent/src/runtime/tool-registry.ts +14 -0
@@ -17,6 +17,12 @@
17
17
  * looks like a code), the retry keyword, or a reply to an unrecognised
18
18
  * login screen. Everything else goes through the normal chat path.
19
19
  * - The broker's `update` / `finished` events.
20
+ * - {@link HarnessReloginService.startOwnerLogin} — the owner asked for a
21
+ * login: 「重新登录 claude」 in the orc DM (parsed by
22
+ * {@link HarnessReloginService.handleOwnerReply}, no LLM involved) or the
23
+ * orchestrator's `harness-login` skill. Such a flow is *forced*: it runs
24
+ * even when the harness is still logged in (switching accounts), skips the
25
+ * silent API-key path, and answers in the conversation it was asked in.
20
26
  *
21
27
  * Rules:
22
28
  * - One flow per harness. Repeated detections only add stuck sessions. A
@@ -48,16 +54,63 @@ export interface ExpiryReport {
48
54
  */
49
55
  source: ExpirySource;
50
56
  }
57
+ /**
58
+ * Where an owner-requested login answers: the Slack conversation (and thread)
59
+ * the owner asked in. `agentSession` is set when that conversation is a DM
60
+ * with an agent's own bot (the orc's "Crewly Orc" app), whose token must be
61
+ * used to post there.
62
+ */
63
+ export interface ReloginReplyTarget {
64
+ channelId: string;
65
+ threadTs?: string;
66
+ agentSession?: string;
67
+ }
51
68
  /** Sends a Slack DM to the owner. */
52
69
  export interface ReloginOwnerNotifier {
53
70
  /**
54
71
  * Send a DM to the owner.
55
72
  *
56
73
  * @param text - Slack mrkdwn text (never contains a secret)
74
+ * @param target - Conversation to answer in; absent = the owner's DM with the master bot
57
75
  * @returns True when delivered
58
76
  */
59
- sendToOwner(text: string): Promise<boolean>;
77
+ sendToOwner(text: string, target?: ReloginReplyTarget | null): Promise<boolean>;
78
+ /**
79
+ * Whether a DM can be sent right now (Slack connected).
80
+ *
81
+ * @returns True when {@link sendToOwner} can deliver
82
+ */
83
+ isAvailable?(): boolean;
84
+ }
85
+ /** What started a flow. */
86
+ export type ReloginTrigger = 'expiry' | 'owner';
87
+ /** Options of an owner-requested login. */
88
+ export interface OwnerLoginOptions {
89
+ /** The owner wants a different account (only changes the wording) */
90
+ switchAccount?: boolean;
91
+ /** Conversation to answer in (null = the owner's master-bot DM) */
92
+ replyTarget?: ReloginReplyTarget | null;
93
+ /** Who relayed the request, for logs */
94
+ requestedBy: 'owner_dm' | 'orchestrator';
60
95
  }
96
+ /** An owner-requested login that was started. */
97
+ export interface OwnerLoginStarted {
98
+ /** `restarted`: a flow for the harness was running and was started over */
99
+ status: 'started' | 'restarted';
100
+ harnessId: HarnessId;
101
+ /** Whether the owner can be DM'd right now (false = Slack is down) */
102
+ dmAvailable: boolean;
103
+ }
104
+ /** An owner-requested login for a harness without a link login. */
105
+ export interface OwnerLoginUnsupported {
106
+ /** The harness has no link login (Antigravity: API key; Gemini: enterprise only) */
107
+ status: 'no_broker_login';
108
+ harnessId: HarnessId;
109
+ /** What to tell the owner, in Chinese */
110
+ message: string;
111
+ }
112
+ /** Result of {@link HarnessReloginService.startOwnerLogin}. */
113
+ export type OwnerLoginResult = OwnerLoginStarted | OwnerLoginUnsupported;
61
114
  /** Restarts agents after a login succeeded. */
62
115
  export interface ReloginAgentResumer {
63
116
  /**
@@ -136,22 +189,31 @@ export declare const SCREEN_REPLY_PREFIXES: readonly ["输入", "input"];
136
189
  * @returns Text to type, or null when the reply has no prefix
137
190
  */
138
191
  export declare function stripScreenReplyPrefix(reply: string): string | null;
192
+ /** How a DM introduces the login. */
193
+ export interface LoginDmWording {
194
+ /** The owner asked for this login (not an expiry) */
195
+ ownerRequested?: boolean;
196
+ /** The owner asked for a different account */
197
+ switchAccount?: boolean;
198
+ }
139
199
  /**
140
200
  * DM with the sign-in link (and Codex's one-time code on its own line).
141
201
  *
142
202
  * @param session - Broker session (never contains a secret)
143
203
  * @param waiting - Agents waiting on the login
204
+ * @param wording - Owner-requested / account-switch wording (default: expiry)
144
205
  * @returns Slack mrkdwn text
145
206
  */
146
- export declare function formatLinkDm(session: LoginSession, waiting: readonly string[]): string;
207
+ export declare function formatLinkDm(session: LoginSession, waiting: readonly string[], wording?: LoginDmWording): string;
147
208
  /**
148
209
  * DM for a login screen the broker did not recognise.
149
210
  *
150
211
  * @param session - Broker session
151
212
  * @param waiting - Agents waiting on the login
213
+ * @param wording - Owner-requested / account-switch wording (default: expiry)
152
214
  * @returns Slack mrkdwn text
153
215
  */
154
- export declare function formatScreenDm(session: LoginSession, waiting: readonly string[]): string;
216
+ export declare function formatScreenDm(session: LoginSession, waiting: readonly string[], wording?: LoginDmWording): string;
155
217
  /**
156
218
  * DM after the harness rejected the owner's reply (e.g. a wrong code).
157
219
  *
@@ -170,6 +232,31 @@ export declare function formatSuccessDm(harnessId: HarnessId, result: {
170
232
  resumed: string[];
171
233
  failed: string[];
172
234
  }): string;
235
+ /**
236
+ * One-line DM after an owner-requested login succeeded.
237
+ *
238
+ * @param harnessId - Harness
239
+ * @param result - Which agents were restarted onto the new login
240
+ * @returns Slack mrkdwn text
241
+ */
242
+ export declare function formatOwnerSuccessDm(harnessId: HarnessId, result: {
243
+ resumed: string[];
244
+ failed: string[];
245
+ }): string;
246
+ /**
247
+ * Reply to a login request that named no harness Crewly can log in.
248
+ *
249
+ * @param name - What the owner wrote, or null when they named none
250
+ * @returns Slack mrkdwn text
251
+ */
252
+ export declare function formatWhichHarnessDm(name: string | null): string;
253
+ /**
254
+ * Reply for a harness that has no link login.
255
+ *
256
+ * @param harnessId - Harness without a broker login method
257
+ * @returns Slack mrkdwn text
258
+ */
259
+ export declare function formatNoBrokerLoginDm(harnessId: HarnessId): string;
173
260
  /**
174
261
  * DM after the login failed or timed out (sent once per failure).
175
262
  *
@@ -226,12 +313,45 @@ export declare class HarnessReloginService {
226
313
  */
227
314
  reportExpiry(report: ExpiryReport): boolean;
228
315
  /**
229
- * Offer an owner's Slack DM reply to the re-login flows.
316
+ * Offer an owner's Slack DM to the re-login coordinator.
317
+ *
318
+ * Consumed, in this order: the retry keyword (while a flow exists), a
319
+ * login code or screen reply for a running flow, and finally an owner
320
+ * login request (「重新登录 claude」, "relogin codex" — see
321
+ * {@link parseOwnerLoginRequest}), which starts a forced login answered in
322
+ * `target`.
230
323
  *
231
324
  * @param text - Reply text (never logged)
325
+ * @param target - Conversation the owner wrote in (answers go there)
232
326
  * @returns True when consumed; the caller must then NOT pass it on as a chat message
233
327
  */
234
- handleOwnerReply(text: string): boolean;
328
+ handleOwnerReply(text: string, target?: ReloginReplyTarget | null): boolean;
329
+ /**
330
+ * Start a login the owner asked for, through the broker: forced (runs
331
+ * even when the harness is still logged in — the owner may be switching
332
+ * accounts), no silent API key, answered in `replyTarget`. A flow already
333
+ * running for the harness is started over with a fresh link.
334
+ *
335
+ * @param harnessId - Harness to log in
336
+ * @param options - Account switch, reply target, who relayed the request
337
+ * @returns What happened (never throws)
338
+ */
339
+ startOwnerLogin(harnessId: HarnessId, options: OwnerLoginOptions): OwnerLoginResult;
340
+ /**
341
+ * Act on a parsed owner login request from the DM.
342
+ *
343
+ * @param request - Parsed request
344
+ * @param target - Conversation to answer in
345
+ */
346
+ private handleOwnerLoginRequest;
347
+ /**
348
+ * Offer a DM to the running flows: the retry keyword, a login code, or a
349
+ * reply to an unrecognised screen.
350
+ *
351
+ * @param text - Reply text (never logged)
352
+ * @returns True when consumed
353
+ */
354
+ private consumeFlowReply;
235
355
  /**
236
356
  * The pending re-login for a harness (shown by `GET /api/harness`).
237
357
  *
@@ -261,6 +381,7 @@ export declare class HarnessReloginService {
261
381
  *
262
382
  * @param harnessId - Harness
263
383
  * @param stuck - Sessions known to be stuck
384
+ * @param options - Trigger, wording and reply target (default: a detected expiry)
264
385
  */
265
386
  private begin;
266
387
  /**
@@ -269,6 +390,12 @@ export declare class HarnessReloginService {
269
390
  * @param flow - The flow
270
391
  */
271
392
  private run;
393
+ /**
394
+ * Start (or adopt) the harness's broker login session for a flow.
395
+ *
396
+ * @param flow - The flow
397
+ */
398
+ private startBrokerSession;
272
399
  /**
273
400
  * Use an API key Crewly holds for the harness, without asking the owner.
274
401
  * Not repeated within SILENT_KEY_RETRY_WINDOW_MS: a key that did not
@@ -303,6 +430,13 @@ export declare class HarnessReloginService {
303
430
  * @param flow - The flow to replace
304
431
  */
305
432
  private restart;
433
+ /**
434
+ * DM wording for a flow.
435
+ *
436
+ * @param flow - The flow
437
+ * @returns Owner-requested / account-switch flags
438
+ */
439
+ private wordingOf;
306
440
  /**
307
441
  * Cancel the flow's live broker session, marking it as cancelled by the
308
442
  * coordinator so no failure DM is sent for it.
@@ -330,6 +464,14 @@ export declare class HarnessReloginService {
330
464
  * @param text - DM text
331
465
  */
332
466
  private dm;
467
+ /**
468
+ * Send a message to the owner (logs when no notifier is wired; never logs the text).
469
+ *
470
+ * @param text - DM text
471
+ * @param target - Conversation to answer in (null = master-bot DM)
472
+ * @param harnessId - Harness, for logs
473
+ */
474
+ private notify;
333
475
  /**
334
476
  * Read a broker session.
335
477
  *
@@ -1 +1 @@
1
- {"version":3,"file":"harness-relogin.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-relogin.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness-relogin.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-relogin.service.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,12 @@
17
17
  * looks like a code), the retry keyword, or a reply to an unrecognised
18
18
  * login screen. Everything else goes through the normal chat path.
19
19
  * - The broker's `update` / `finished` events.
20
+ * - {@link HarnessReloginService.startOwnerLogin} — the owner asked for a
21
+ * login: 「重新登录 claude」 in the orc DM (parsed by
22
+ * {@link HarnessReloginService.handleOwnerReply}, no LLM involved) or the
23
+ * orchestrator's `harness-login` skill. Such a flow is *forced*: it runs
24
+ * even when the harness is still logged in (switching accounts), skips the
25
+ * silent API-key path, and answers in the conversation it was asked in.
20
26
  *
21
27
  * Rules:
22
28
  * - One flow per harness. Repeated detections only add stuck sessions. A
@@ -39,6 +45,9 @@ import { getHarnessService } from './harness.service.js';
39
45
  import { SILENT_HARNESS_LOGGER, isHarnessId, isTerminalLoginState, } from './harness.types.js';
40
46
  import { LOGIN_BROKER_EVENTS } from './login-broker.service.js';
41
47
  import { redactSecrets } from './login-rules.js';
48
+ import { parseOwnerLoginRequest } from './owner-login-request.js';
49
+ /** A detected expiry: not forced, standard wording, master-bot DM. */
50
+ const EXPIRY_FLOW = { trigger: 'expiry', switchAccount: false, replyTarget: null };
42
51
  /** Harnesses the coordinator can log in (they have a broker method). */
43
52
  function hasBrokerLogin(harnessId) {
44
53
  return getBrokerLoginMethod(harnessId) !== undefined;
@@ -129,22 +138,40 @@ export function stripScreenReplyPrefix(reply) {
129
138
  }
130
139
  return null;
131
140
  }
141
+ /**
142
+ * First line of a login DM.
143
+ *
144
+ * @param harnessId - Harness
145
+ * @param waiting - Agents waiting on the login (expiry wording only)
146
+ * @param wording - Owner-requested / account switch
147
+ * @returns Slack mrkdwn line
148
+ */
149
+ function loginDmHeader(harnessId, waiting, wording) {
150
+ const name = displayName(harnessId);
151
+ if (!wording.ownerRequested)
152
+ return `*${name} 登录过期了。* ${describeWaitingAgents(waiting)}`;
153
+ return wording.switchAccount ? `*换账号登录 ${name}*` : `*登录 ${name}*`;
154
+ }
132
155
  /**
133
156
  * DM with the sign-in link (and Codex's one-time code on its own line).
134
157
  *
135
158
  * @param session - Broker session (never contains a secret)
136
159
  * @param waiting - Agents waiting on the login
160
+ * @param wording - Owner-requested / account-switch wording (default: expiry)
137
161
  * @returns Slack mrkdwn text
138
162
  */
139
- export function formatLinkDm(session, waiting) {
140
- const name = displayName(session.harnessId);
141
- const lines = [`*${name} 登录过期了。* ${describeWaitingAgents(waiting)}`, ''];
163
+ export function formatLinkDm(session, waiting, wording = {}) {
164
+ const lines = [loginDmHeader(session.harnessId, waiting, wording), ''];
142
165
  if (session.method === 'device') {
143
166
  lines.push('1. 在手机上打开这个链接:', session.url ?? '', '', '2. 输入这个验证码:', session.userCode ?? '', '');
167
+ if (wording.switchAccount)
168
+ lines.push('要换账号的话,先在打开的页面里切到要用的那个账号。');
144
169
  lines.push('在手机上完成登录就行,这边会自动继续。');
145
170
  }
146
171
  else {
147
172
  lines.push('1. 在手机上打开这个链接并授权:', session.url ?? '', '');
173
+ if (wording.switchAccount)
174
+ lines.push(' 要换账号的话,先在打开的页面里切到要用的那个账号再授权。', '');
148
175
  lines.push('2. 把授权后页面显示的代码直接回复在这里(只要代码)。', '');
149
176
  lines.push('Crewly 会自动填进去,agent 自己接着干。');
150
177
  }
@@ -155,12 +182,13 @@ export function formatLinkDm(session, waiting) {
155
182
  *
156
183
  * @param session - Broker session
157
184
  * @param waiting - Agents waiting on the login
185
+ * @param wording - Owner-requested / account-switch wording (default: expiry)
158
186
  * @returns Slack mrkdwn text
159
187
  */
160
- export function formatScreenDm(session, waiting) {
188
+ export function formatScreenDm(session, waiting, wording = {}) {
161
189
  const screen = safeForDm(session.screen, HARNESS_CONSTANTS.RELOGIN.DM_SCREEN_MAX_CHARS) || '(empty screen)';
162
190
  return [
163
- `*${displayName(session.harnessId)} 登录过期了。* ${describeWaitingAgents(waiting)}`,
191
+ loginDmHeader(session.harnessId, waiting, wording),
164
192
  '',
165
193
  'Crewly 已经启动了登录,但没认出它的界面。现在显示的是:',
166
194
  '```',
@@ -195,6 +223,48 @@ export function formatSuccessDm(harnessId, result) {
195
223
  }
196
224
  return text;
197
225
  }
226
+ /**
227
+ * One-line DM after an owner-requested login succeeded.
228
+ *
229
+ * @param harnessId - Harness
230
+ * @param result - Which agents were restarted onto the new login
231
+ * @returns Slack mrkdwn text
232
+ */
233
+ export function formatOwnerSuccessDm(harnessId, result) {
234
+ let text = `好了:${displayName(harnessId)} 已登录。`;
235
+ if (result.resumed.length > 0)
236
+ text += ` ${result.resumed.length} 个 agent 已重启,用上了新登录。`;
237
+ if (result.failed.length > 0)
238
+ text += ` 没能重启:${result.failed.join('、')}。`;
239
+ return text;
240
+ }
241
+ /** Harness names the owner can type, for the "which one?" reply. */
242
+ const WHICH_HARNESS_HINT = '回复「重新登录 claude」或「重新登录 codex」(要换账号就说「换个账号登录 claude」)。';
243
+ /**
244
+ * Reply to a login request that named no harness Crewly can log in.
245
+ *
246
+ * @param name - What the owner wrote, or null when they named none
247
+ * @returns Slack mrkdwn text
248
+ */
249
+ export function formatWhichHarnessDm(name) {
250
+ return name ? `Crewly 没有「${name}」这个编程助手。要登录哪个?${WHICH_HARNESS_HINT}` : `要登录哪个?${WHICH_HARNESS_HINT}`;
251
+ }
252
+ /**
253
+ * Reply for a harness that has no link login.
254
+ *
255
+ * @param harnessId - Harness without a broker login method
256
+ * @returns Slack mrkdwn text
257
+ */
258
+ export function formatNoBrokerLoginDm(harnessId) {
259
+ const name = displayName(harnessId);
260
+ if (harnessId === HARNESS_CONSTANTS.IDS.ANTIGRAVITY_CLI) {
261
+ return `${name} 用的是 Gemini API key,不走链接登录。在电脑上运行 \`crewly login antigravity\`,或者在 Crewly 设置页(Setup → 登录)填 key。`;
262
+ }
263
+ if (harnessId === HARNESS_CONSTANTS.IDS.GEMINI_CLI) {
264
+ return `${name} 只给企业版用,Crewly 没法帮它登录。${WHICH_HARNESS_HINT}`;
265
+ }
266
+ return `${name} 没有链接登录,Crewly 帮不了它。${WHICH_HARNESS_HINT}`;
267
+ }
198
268
  /**
199
269
  * DM after the login failed or timed out (sent once per failure).
200
270
  *
@@ -299,14 +369,91 @@ export class HarnessReloginService {
299
369
  return true;
300
370
  }
301
371
  /**
302
- * Offer an owner's Slack DM reply to the re-login flows.
372
+ * Offer an owner's Slack DM to the re-login coordinator.
373
+ *
374
+ * Consumed, in this order: the retry keyword (while a flow exists), a
375
+ * login code or screen reply for a running flow, and finally an owner
376
+ * login request (「重新登录 claude」, "relogin codex" — see
377
+ * {@link parseOwnerLoginRequest}), which starts a forced login answered in
378
+ * `target`.
303
379
  *
304
380
  * @param text - Reply text (never logged)
381
+ * @param target - Conversation the owner wrote in (answers go there)
305
382
  * @returns True when consumed; the caller must then NOT pass it on as a chat message
306
383
  */
307
- handleOwnerReply(text) {
308
- if (typeof text !== 'string' || this.flows.size === 0)
384
+ handleOwnerReply(text, target = null) {
385
+ if (typeof text !== 'string')
386
+ return false;
387
+ if (this.flows.size > 0 && this.consumeFlowReply(text))
388
+ return true;
389
+ const request = parseOwnerLoginRequest(text);
390
+ if (!request)
309
391
  return false;
392
+ void this.handleOwnerLoginRequest(request, target);
393
+ return true;
394
+ }
395
+ /**
396
+ * Start a login the owner asked for, through the broker: forced (runs
397
+ * even when the harness is still logged in — the owner may be switching
398
+ * accounts), no silent API key, answered in `replyTarget`. A flow already
399
+ * running for the harness is started over with a fresh link.
400
+ *
401
+ * @param harnessId - Harness to log in
402
+ * @param options - Account switch, reply target, who relayed the request
403
+ * @returns What happened (never throws)
404
+ */
405
+ startOwnerLogin(harnessId, options) {
406
+ if (!hasBrokerLogin(harnessId)) {
407
+ return { status: 'no_broker_login', harnessId, message: formatNoBrokerLoginDm(harnessId) };
408
+ }
409
+ const existing = this.flows.get(harnessId);
410
+ const stuck = existing ? new Set(existing.stuck) : new Set();
411
+ if (existing) {
412
+ this.clearScreenTimer(existing);
413
+ this.cancelOwnSession(existing);
414
+ if (this.flows.get(harnessId) === existing)
415
+ this.flows.delete(harnessId);
416
+ }
417
+ // The owner asked: a quiet period after the last login must not swallow it.
418
+ this.quietUntil.delete(harnessId);
419
+ this.logger.info('Owner-requested login started', {
420
+ harnessId,
421
+ requestedBy: options.requestedBy,
422
+ switchAccount: options.switchAccount === true,
423
+ restarted: Boolean(existing),
424
+ answersInThread: Boolean(options.replyTarget),
425
+ });
426
+ this.begin(harnessId, stuck, {
427
+ trigger: 'owner',
428
+ switchAccount: options.switchAccount === true,
429
+ replyTarget: options.replyTarget ?? null,
430
+ });
431
+ const dmAvailable = this.notifier ? (this.notifier.isAvailable?.() ?? true) : false;
432
+ return { status: existing ? 'restarted' : 'started', harnessId, dmAvailable };
433
+ }
434
+ /**
435
+ * Act on a parsed owner login request from the DM.
436
+ *
437
+ * @param request - Parsed request
438
+ * @param target - Conversation to answer in
439
+ */
440
+ async handleOwnerLoginRequest(request, target) {
441
+ if (request.kind === 'unknown') {
442
+ await this.notify(formatWhichHarnessDm(request.name), target);
443
+ return;
444
+ }
445
+ const result = this.startOwnerLogin(request.harnessId, { switchAccount: request.switchAccount, replyTarget: target, requestedBy: 'owner_dm' });
446
+ if (result.status === 'no_broker_login')
447
+ await this.notify(result.message, target);
448
+ }
449
+ /**
450
+ * Offer a DM to the running flows: the retry keyword, a login code, or a
451
+ * reply to an unrecognised screen.
452
+ *
453
+ * @param text - Reply text (never logged)
454
+ * @returns True when consumed
455
+ */
456
+ consumeFlowReply(text) {
310
457
  if (isRetryKeyword(text)) {
311
458
  const flows = [...this.flows.values()];
312
459
  const failed = flows.filter((flow) => flow.phase === 'failed');
@@ -420,8 +567,9 @@ export class HarnessReloginService {
420
567
  *
421
568
  * @param harnessId - Harness
422
569
  * @param stuck - Sessions known to be stuck
570
+ * @param options - Trigger, wording and reply target (default: a detected expiry)
423
571
  */
424
- begin(harnessId, stuck) {
572
+ begin(harnessId, stuck, options = EXPIRY_FLOW) {
425
573
  const flow = {
426
574
  harnessId,
427
575
  phase: 'running',
@@ -434,6 +582,9 @@ export class HarnessReloginService {
434
582
  lastRejection: null,
435
583
  screenTimer: null,
436
584
  cancelledByUs: false,
585
+ trigger: options.trigger,
586
+ switchAccount: options.switchAccount,
587
+ replyTarget: options.replyTarget,
437
588
  };
438
589
  // Registered synchronously: this is the per-harness debounce.
439
590
  this.flows.set(harnessId, flow);
@@ -445,6 +596,12 @@ export class HarnessReloginService {
445
596
  * @param flow - The flow
446
597
  */
447
598
  async run(flow) {
599
+ // The owner asked for this login (possibly to switch accounts): no
600
+ // "still logged in" check and no silent API key — they want the link.
601
+ if (flow.trigger === 'owner') {
602
+ this.startBrokerSession(flow);
603
+ return;
604
+ }
448
605
  // An agent's "401" is not proof the login expired — a bad API key or
449
606
  // text typed into the sign-in screen gives one too. Starting a login
450
607
  // then *logs the harness out* (`codex login` clears its credentials):
@@ -464,6 +621,14 @@ export class HarnessReloginService {
464
621
  return;
465
622
  if (this.flows.get(flow.harnessId) !== flow)
466
623
  return;
624
+ this.startBrokerSession(flow);
625
+ }
626
+ /**
627
+ * Start (or adopt) the harness's broker login session for a flow.
628
+ *
629
+ * @param flow - The flow
630
+ */
631
+ startBrokerSession(flow) {
467
632
  const method = getBrokerLoginMethod(flow.harnessId);
468
633
  if (!method)
469
634
  return;
@@ -475,14 +640,14 @@ export class HarnessReloginService {
475
640
  const reason = error instanceof Error ? error.message : String(error);
476
641
  this.logger.warn('Re-login: could not start the login', { harnessId: flow.harnessId, error: reason });
477
642
  flow.phase = 'failed';
478
- await this.dm(flow, formatFailureDm(flow.harnessId, `Crewly 没能启动登录(${reason})`));
643
+ void this.dm(flow, formatFailureDm(flow.harnessId, `Crewly 没能启动登录(${reason})`));
479
644
  return;
480
645
  }
481
646
  flow.sessionId = session.id;
482
647
  flow.startedAt = Date.parse(session.startedAt) || this.now();
483
648
  flow.screenTimer = setTimeout(() => void this.sendScreenIfUnrecognised(flow), HARNESS_CONSTANTS.RELOGIN.UNRECOGNISED_SCREEN_MS);
484
649
  flow.screenTimer.unref?.();
485
- this.logger.info('Re-login broker session started', { harnessId: flow.harnessId, sessionId: session.id, method: method.id });
650
+ this.logger.info('Re-login broker session started', { harnessId: flow.harnessId, sessionId: session.id, method: method.id, trigger: flow.trigger });
486
651
  // The broker may hand back a session that already shows its link.
487
652
  if (isTerminalLoginState(session.state))
488
653
  this.handleFinished(session);
@@ -543,7 +708,7 @@ export class HarnessReloginService {
543
708
  if (flow.dm !== 'link' && linkReady) {
544
709
  flow.dm = 'link';
545
710
  this.clearScreenTimer(flow);
546
- void this.dm(flow, formatLinkDm(session, this.waitingAgents(flow)));
711
+ void this.dm(flow, formatLinkDm(session, this.waitingAgents(flow), this.wordingOf(flow)));
547
712
  return;
548
713
  }
549
714
  if (flow.replied && session.state === 'awaiting_user' && session.message && session.message !== flow.lastRejection) {
@@ -613,8 +778,10 @@ export class HarnessReloginService {
613
778
  for (const name of result.resumed)
614
779
  this.mutedScreens.add(name);
615
780
  this.logger.info('Re-login finished', { harnessId: flow.harnessId, resumed: result.resumed.length, failed: result.failed.length });
616
- if (notify)
617
- await this.dm(flow, formatSuccessDm(flow.harnessId, result));
781
+ if (notify) {
782
+ const text = flow.trigger === 'owner' ? formatOwnerSuccessDm(flow.harnessId, result) : formatSuccessDm(flow.harnessId, result);
783
+ await this.dm(flow, text);
784
+ }
618
785
  }
619
786
  /**
620
787
  * Start a flow over (retry keyword or re-reminder).
@@ -626,7 +793,20 @@ export class HarnessReloginService {
626
793
  this.cancelOwnSession(flow);
627
794
  if (this.flows.get(flow.harnessId) === flow)
628
795
  this.flows.delete(flow.harnessId);
629
- this.begin(flow.harnessId, new Set(flow.stuck));
796
+ this.begin(flow.harnessId, new Set(flow.stuck), {
797
+ trigger: flow.trigger,
798
+ switchAccount: flow.switchAccount,
799
+ replyTarget: flow.replyTarget,
800
+ });
801
+ }
802
+ /**
803
+ * DM wording for a flow.
804
+ *
805
+ * @param flow - The flow
806
+ * @returns Owner-requested / account-switch flags
807
+ */
808
+ wordingOf(flow) {
809
+ return { ownerRequested: flow.trigger === 'owner', switchAccount: flow.switchAccount };
630
810
  }
631
811
  /**
632
812
  * Cancel the flow's live broker session, marking it as cancelled by the
@@ -659,7 +839,7 @@ export class HarnessReloginService {
659
839
  return;
660
840
  flow.dm = 'screen';
661
841
  this.logger.warn('Re-login: login screen not recognised, sending it to the owner', { harnessId: flow.harnessId, sessionId: session.id });
662
- await this.dm(flow, formatScreenDm(session, this.waitingAgents(flow)));
842
+ await this.dm(flow, formatScreenDm(session, this.waitingAgents(flow), this.wordingOf(flow)));
663
843
  }
664
844
  /**
665
845
  * Agents waiting on a flow: the stuck ones, else every session of the harness.
@@ -685,18 +865,28 @@ export class HarnessReloginService {
685
865
  */
686
866
  async dm(flow, text) {
687
867
  flow.lastDmAt = this.now();
868
+ await this.notify(text, flow.replyTarget, flow.harnessId);
869
+ }
870
+ /**
871
+ * Send a message to the owner (logs when no notifier is wired; never logs the text).
872
+ *
873
+ * @param text - DM text
874
+ * @param target - Conversation to answer in (null = master-bot DM)
875
+ * @param harnessId - Harness, for logs
876
+ */
877
+ async notify(text, target, harnessId) {
688
878
  if (!this.notifier) {
689
- this.logger.warn('Re-login: no Slack DM path to the owner; finish the login from Setup', { harnessId: flow.harnessId });
879
+ this.logger.warn('Re-login: no Slack DM path to the owner; finish the login from Setup', { harnessId });
690
880
  return;
691
881
  }
692
882
  try {
693
- const delivered = await this.notifier.sendToOwner(text);
883
+ const delivered = await this.notifier.sendToOwner(text, target);
694
884
  if (!delivered)
695
- this.logger.warn('Re-login: the owner DM was not delivered', { harnessId: flow.harnessId });
885
+ this.logger.warn('Re-login: the owner DM was not delivered', { harnessId });
696
886
  }
697
887
  catch (error) {
698
888
  this.logger.warn('Re-login: sending the owner DM failed', {
699
- harnessId: flow.harnessId,
889
+ harnessId,
700
890
  error: error instanceof Error ? error.message : String(error),
701
891
  });
702
892
  }