maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.23.20260906

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 (63) hide show
  1. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  2. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  3. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  4. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  5. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  6. package/node_modules/@maka/core/dist/bot-events.js +18 -63
  7. package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
  8. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
  9. package/node_modules/@maka/core/dist/record-schema.js +13 -0
  10. package/node_modules/@maka/core/dist/redaction.js +41 -58
  11. package/node_modules/@maka/core/dist/session-trace.js +5 -1
  12. package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
  13. package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
  14. package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
  15. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
  16. package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
  17. package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
  18. package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
  19. package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
  20. package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
  21. package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
  22. package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
  23. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
  24. package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
  25. package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
  26. package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
  27. package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
  28. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
  29. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  30. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
  31. package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
  32. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
  33. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
  34. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
  35. package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
  36. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  37. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
  38. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
  39. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
  40. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
  41. package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
  42. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
  43. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
  44. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
  45. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
  46. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
  47. package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
  48. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
  49. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
  50. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
  51. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
  52. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
  53. package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
  54. package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
  55. package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
  56. package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
  57. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
  58. package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
  59. package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
  60. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
  61. package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
  62. package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
  63. package/package.json +1 -1
@@ -74,7 +74,9 @@ const DIRECT_STOP_REQUEST = /^\s*(?:(?:please|kindly)\s+)?(?:stop|cancel|termina
74
74
  // which is usually work to do inside a Session, and English admits no
75
75
  // equivalent either.
76
76
  const DIRECT_CHINESE_STOP_REQUEST = /^\s*(?:(?:请|请帮我|帮我|麻烦你?)\s*)?(?:停止|停掉|停下|取消|终止|中止)\s*(?:(?:这个|该)?(?:会话|工作|任务)\s*)?(.+?)\s*[。!]?\s*$/iu;
77
- const UNSAFE_STOP_TARGET = /^(?:it|this|that|one|everything|all|current|session|work|task|job|(?:this|that|current)\s+(?:session|work|task|job)|它|这个|那个|全部|当前|会话|工作|任务|(?:这个|那个|当前)(?:会话|工作|任务))$/iu;
77
+ const DIRECT_RESUME_REQUEST = /^\s*(?:(?:please|kindly)\s+)?resume\s+(?:(?:the|this)\s+)?(?:(?:session|work|task|job)\s+)?(.+?)\s*[.!。!]?\s*$/iu;
78
+ const DIRECT_CHINESE_RESUME_REQUEST = /^\s*(?:(?:请|请帮我|帮我|麻烦你?)\s*)?(?:恢复|恢復|接着跑|接著跑)\s*(?:(?:这个|该)?(?:会话|工作|任务)\s*)?(.+?)\s*[。!]?\s*$/iu;
79
+ const UNSAFE_NAMED_ACTION_TARGET = /^(?:it|this|that|one|everything|all|current|session|work|task|job|(?:this|that|current)\s+(?:session|work|task|job)|它|这个|那个|全部|当前|会话|工作|任务|(?:这个|那个|当前)(?:会话|工作|任务))$/iu;
78
80
  /**
79
81
  * Whether trusted user text affirmatively asks WorkHub to create a new Session.
80
82
  *
@@ -219,8 +221,14 @@ export function readWorkHubRequestIntent(value) {
219
221
  : { kind: 'unusable' };
220
222
  const correctionCue = hasWorkHubCorrectionCue(source);
221
223
  const existingTarget = affirmativeWorkHubExistingCorrectionTarget(source);
222
- const stopCue = directWorkHubStopCue(source, literalMask.malformed);
223
- const stopTarget = stopCue ? directWorkHubStopTarget(source, false) : undefined;
224
+ const stop = directWorkHubNamedAction(source, literalMask.malformed, [
225
+ DIRECT_STOP_REQUEST,
226
+ DIRECT_CHINESE_STOP_REQUEST,
227
+ ]);
228
+ const resume = directWorkHubNamedAction(source, literalMask.malformed, [
229
+ DIRECT_RESUME_REQUEST,
230
+ DIRECT_CHINESE_RESUME_REQUEST,
231
+ ]);
224
232
  const actions = allMatches(masked, EXECUTION_ACTION);
225
233
  const execution = literalMask.malformed || naming.kind === 'unusable' || hasDominatingDeliberation(masked)
226
234
  ? 'non_executable'
@@ -237,9 +245,14 @@ export function readWorkHubRequestIntent(value) {
237
245
  ...(existingTarget ? { existingTarget } : {}),
238
246
  },
239
247
  stop: {
240
- cue: stopCue,
241
- imperative: Boolean(stopTarget),
242
- ...(stopTarget ? { target: stopTarget } : {}),
248
+ cue: stop.cue,
249
+ imperative: Boolean(stop.target),
250
+ ...(stop.target ? { target: stop.target } : {}),
251
+ },
252
+ resume: {
253
+ cue: resume.cue,
254
+ imperative: Boolean(resume.target),
255
+ ...(resume.target ? { target: resume.target } : {}),
243
256
  },
244
257
  };
245
258
  }
@@ -331,6 +344,14 @@ export function matchWorkHubSessionName(reference, sessionName) {
331
344
  }
332
345
  return { kind: 'named', remainder: normalizedTarget.slice(matchedName.length).trim() };
333
346
  }
347
+ /** Whether a direct stop/resume reference names exactly this Session. */
348
+ export function workHubNamedDelegationActionTargetsSession(action, sessionName) {
349
+ if (!action.imperative || !action.target)
350
+ return false;
351
+ const match = matchWorkHubSessionName(action.target, sessionName);
352
+ return (match.kind === 'elided_name_punctuation' ||
353
+ (match.kind === 'named' && /^[.!?。!?]*$/u.test(match.remainder)));
354
+ }
334
355
  /**
335
356
  * The correction policy's tail rule. A correction may name its target and then
336
357
  * say what to do with it, but a withdrawal anywhere in the reference retracts
@@ -355,24 +376,17 @@ export function workHubCorrectionAdmitsReference(reference, match) {
355
376
  function correctionTargetMatchesSession(target, sessionName) {
356
377
  return workHubCorrectionAdmitsReference(target, matchWorkHubSessionName(target, sessionName));
357
378
  }
358
- function directWorkHubStopTarget(value, malformedLiteral) {
379
+ function directWorkHubNamedAction(value, malformedLiteral, patterns) {
359
380
  if (malformedLiteral || /[??]\s*$/u.test(value))
360
- return undefined;
361
- const match = DIRECT_STOP_REQUEST.exec(value) ?? DIRECT_CHINESE_STOP_REQUEST.exec(value);
381
+ return { cue: false };
382
+ const match = patterns[0].exec(value) ?? patterns[1].exec(value);
362
383
  const rawTarget = match?.[1]?.trim();
363
384
  if (!rawTarget)
364
- return undefined;
365
- const target = stripMatchingStopQuotes(rawTarget.replace(/[.!。!]+\s*$/u, '').trim());
366
- if (!target || UNSAFE_STOP_TARGET.test(target))
367
- return undefined;
368
- return target;
369
- }
370
- function directWorkHubStopCue(value, malformedLiteral) {
371
- if (malformedLiteral || /[??]\s*$/u.test(value))
372
- return false;
373
- return Boolean(DIRECT_STOP_REQUEST.test(value) || DIRECT_CHINESE_STOP_REQUEST.test(value));
385
+ return { cue: Boolean(match) };
386
+ const target = stripMatchingActionQuotes(rawTarget.replace(/[.!。!]+\s*$/u, '').trim());
387
+ return !target || UNSAFE_NAMED_ACTION_TARGET.test(target) ? { cue: true } : { cue: true, target };
374
388
  }
375
- function stripMatchingStopQuotes(value) {
389
+ function stripMatchingActionQuotes(value) {
376
390
  const pairs = new Map([
377
391
  ['"', '"'],
378
392
  ["'", "'"],
@@ -59,4 +59,5 @@ exec mitmdump \
59
59
  --set block_global=false \
60
60
  --set rawtcp=false \
61
61
  --set confdir="$STATE_DIR" \
62
- --scripts /opt/maka-eval/egress_filter.py
62
+ --scripts /opt/maka-eval/egress_filter.py \
63
+ "$@"
@@ -735,6 +735,49 @@ export class AiSdkTurn {
735
735
  let contextReportedWindowNoteWritten = false;
736
736
  let contextOverflowAfterCompactionNoteWritten = false;
737
737
  let contextWindowSuggestionNoteWritten = false;
738
+ // A compaction decision is known the moment its stage reports it — the
739
+ // pre-turn replay resolves its fold before the first request goes out —
740
+ // while the settlement path is skipped entirely by a stop or a stream
741
+ // error. Write both notes when the decision is known, once per send,
742
+ // whichever stage reports first (#4850).
743
+ const appendCompactionDecisionNotes = async (contextBudget) => {
744
+ if (!contextCompactionFailedOpenNoteWritten &&
745
+ shouldAppendContextCompactionFailedOpenNote(contextBudget)) {
746
+ // The most recent stage that refused the fold: a send can carry both a
747
+ // priorReplay and an activeStep refusal after a diagnostic merge, and
748
+ // array order would pin the stale one.
749
+ const failOpenReason = contextBudget?.compactionDecisions
750
+ ?.filter((decision) => decision.boundaryKind === 'historyCompact' && decision.decision === 'failedOpen')
751
+ .at(-1)?.failOpenReason;
752
+ const note = {
753
+ type: 'system_note',
754
+ id: this.deps.newId(),
755
+ turnId,
756
+ ts: this.deps.now(),
757
+ kind: 'context_compaction_failed_open',
758
+ ...(failOpenReason !== undefined ? { data: { failOpenReason } } : {}),
759
+ };
760
+ // Mark written only after the append lands: a failed write must leave
761
+ // the flag down so the settlement fallback can still record the note.
762
+ contextCompactionFailedOpenNoteWritten = await this.deps.backend
763
+ .appendMessage(note)
764
+ .then(() => true)
765
+ .catch(() => false);
766
+ }
767
+ if (!contextCompactedNoteWritten && shouldAppendContextCompactedNote(contextBudget)) {
768
+ const note = {
769
+ type: 'system_note',
770
+ id: this.deps.newId(),
771
+ turnId,
772
+ ts: this.deps.now(),
773
+ kind: 'context_compacted',
774
+ };
775
+ contextCompactedNoteWritten = await this.deps.backend
776
+ .appendMessage(note)
777
+ .then(() => true)
778
+ .catch(() => false);
779
+ }
780
+ };
738
781
  // Request index (0-based) at which the active prune last rewrote the
739
782
  // request. A step Maka pruned is not append-only, so usage may legitimately
740
783
  // shrink.
@@ -912,6 +955,10 @@ export class AiSdkTurn {
912
955
  yield* this.drain(queue);
913
956
  return;
914
957
  }
958
+ // The pre-turn replay's fold decision is final here: surface it now so a
959
+ // stop or stream error later in the send cannot keep it from the
960
+ // transcript (#4850).
961
+ await appendCompactionDecisionNotes(priorReplay.contextBudget);
915
962
  if (midTurnState) {
916
963
  // Roll-forward seed: the latest durable checkpoint (loaded or written at
917
964
  // turn start) so a mid-turn summary only re-reads the newly folded span.
@@ -2058,30 +2105,10 @@ export class AiSdkTurn {
2058
2105
  ...usageFields,
2059
2106
  };
2060
2107
  await this.deps.backend.appendMessage(tu).catch(() => { });
2061
- if (!contextCompactionFailedOpenNoteWritten &&
2062
- shouldAppendContextCompactionFailedOpenNote(contextBudgetForUsage)) {
2063
- contextCompactionFailedOpenNoteWritten = true;
2064
- const note = {
2065
- type: 'system_note',
2066
- id: this.deps.newId(),
2067
- turnId,
2068
- ts: this.deps.now(),
2069
- kind: 'context_compaction_failed_open',
2070
- };
2071
- await this.deps.backend.appendMessage(note).catch(() => { });
2072
- }
2073
- if (!contextCompactedNoteWritten &&
2074
- shouldAppendContextCompactedNote(contextBudgetForUsage)) {
2075
- contextCompactedNoteWritten = true;
2076
- const note = {
2077
- type: 'system_note',
2078
- id: this.deps.newId(),
2079
- turnId,
2080
- ts: this.deps.now(),
2081
- kind: 'context_compacted',
2082
- };
2083
- await this.deps.backend.appendMessage(note).catch(() => { });
2084
- }
2108
+ // Settlement fallback: a mid-turn or request-hook fold is only
2109
+ // known here. Notes already written at decision time are skipped
2110
+ // by the flags inside.
2111
+ await appendCompactionDecisionNotes(contextBudgetForUsage);
2085
2112
  queue.push({
2086
2113
  type: 'token_usage',
2087
2114
  id: this.deps.newId(),
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { EventEmitter } from 'node:events';
20
20
  import { hasBotChannelCredentials } from '@maka/core/bot-chat-settings';
21
+ import { classifyGeneralizedError, redactSecrets } from '@maka/core/redaction';
21
22
  export class BaseBotAdapter extends EventEmitter {
22
23
  platform;
23
24
  settings;
@@ -62,10 +63,26 @@ export class BaseBotAdapter extends EventEmitter {
62
63
  emitStatusChange() {
63
64
  this.emit('statusChange', this.getStatus());
64
65
  }
66
+ recordFailure(error, code) {
67
+ const diagnostic = botDiagnosticMessage(this.settings, error);
68
+ this.reason = code ?? classifyGeneralizedError(diagnostic) ?? 'connection_failed';
69
+ console.warn(`[bots:${this.platform}] ${this.reason}: ${diagnostic}`);
70
+ }
65
71
  connectionKind() {
66
72
  return 'none';
67
73
  }
68
74
  }
75
+ export function botDiagnosticMessage(settings, error) {
76
+ let message = error instanceof Error ? error.message : String(error);
77
+ for (const secret of [settings.token.trim(), settings.appSecret?.trim()]) {
78
+ if (!secret)
79
+ continue;
80
+ message = message
81
+ .replaceAll(secret, '[redacted]')
82
+ .replaceAll(encodeURIComponent(secret), '[redacted]');
83
+ }
84
+ return redactSecrets(message);
85
+ }
69
86
  export function botReadinessFromSettings(settings) {
70
87
  if (!settings.enabled)
71
88
  return 'scaffolded';
@@ -16,9 +16,9 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { generalizedErrorMessage } from '@maka/core/redaction';
20
19
  import { WebClient } from '@slack/web-api';
21
20
  import { proxiedFetch } from './proxied-fetch.js';
21
+ import { botDiagnosticMessage } from './base-adapter.js';
22
22
  import { normalizeWechatIlinkBaseUrl, testWechatBridge, testWechatIlinkCredentials, } from './wechat-bridge.js';
23
23
  const BOT_TEST_TIMEOUT_MS = 10_000;
24
24
  /**
@@ -31,6 +31,16 @@ export function feishuOpenApiHost(domain) {
31
31
  return domain?.trim() === 'larksuite.com' ? 'open.larksuite.com' : 'open.feishu.cn';
32
32
  }
33
33
  export async function testBotChannel(provider, channel) {
34
+ const result = await probeBotChannel(provider, channel);
35
+ if (result.ok)
36
+ return result;
37
+ const errorCode = result.errorCode ?? 'connection_failed';
38
+ const error = result.error ? botDiagnosticMessage(channel, result.error) : undefined;
39
+ if (error)
40
+ console.warn(`[bots:${provider}] ${errorCode}: ${error}`);
41
+ return { ...result, errorCode, ...(error ? { error } : {}) };
42
+ }
43
+ async function probeBotChannel(provider, channel) {
34
44
  if (provider !== 'feishu' &&
35
45
  provider !== 'wecom' &&
36
46
  provider !== 'wechat' &&
@@ -38,7 +48,7 @@ export async function testBotChannel(provider, channel) {
38
48
  provider !== 'qq' &&
39
49
  provider !== 'slack' &&
40
50
  !channel.token.trim()) {
41
- return { ok: false, error: 'Bot token is required' };
51
+ return { ok: false, errorCode: 'token_missing' };
42
52
  }
43
53
  switch (provider) {
44
54
  case 'telegram':
@@ -63,7 +73,7 @@ async function testSlack(channel) {
63
73
  const botToken = channel.token.trim();
64
74
  const appToken = channel.appSecret?.trim() ?? '';
65
75
  if (!botToken || !appToken) {
66
- return { ok: false, error: 'Slack 需要 Bot Token 与 App-Level Token' };
76
+ return { ok: false, errorCode: 'slack_tokens_missing' };
67
77
  }
68
78
  try {
69
79
  const identity = await new WebClient(botToken).auth.test();
@@ -80,15 +90,14 @@ async function testSlack(channel) {
80
90
  ...(identity.user ? { username: identity.user, displayName: identity.user } : {}),
81
91
  },
82
92
  capabilities: { auth: true, socketMode: true },
83
- hint: '凭据有效,Socket Mode 连接可用。',
84
93
  };
85
94
  }
86
95
  catch (error) {
87
- return { ok: false, error: generalizedErrorMessage(error) };
96
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
88
97
  }
89
98
  }
90
99
  async function testWechat(channel) {
91
- if (channel.token.trim() && normalizeWechatIlinkBaseUrl(channel.webhookUrl)) {
100
+ if (normalizeWechatIlinkBaseUrl(channel.webhookUrl)) {
92
101
  return testWechatIlinkCredentials(channel);
93
102
  }
94
103
  const appId = channel.appId?.trim() ?? '';
@@ -113,7 +122,6 @@ async function testWechat(channel) {
113
122
  ok: true,
114
123
  identity: { id: appId, username: appId, displayName: appId },
115
124
  capabilities: { auth: true },
116
- hint: '凭据有效;消息收发还需要公众号服务器配置和回调验证。',
117
125
  };
118
126
  }
119
127
  catch (error) {
@@ -123,9 +131,21 @@ async function testWechat(channel) {
123
131
  async function testTelegram(channel) {
124
132
  const base = `https://api.telegram.org/bot${channel.token}`;
125
133
  try {
126
- const me = await (await proxiedFetch(`${base}/getMe`, { method: 'GET', timeoutMs: BOT_TEST_TIMEOUT_MS })).json();
127
- if (!me.ok)
128
- return { ok: false, error: me.description ?? 'Invalid bot token' };
134
+ const response = await proxiedFetch(`${base}/getMe`, {
135
+ method: 'GET',
136
+ timeoutMs: BOT_TEST_TIMEOUT_MS,
137
+ });
138
+ const me = await response.json().catch(() => null);
139
+ if (!response.ok || me?.ok !== true)
140
+ return {
141
+ ok: false,
142
+ errorCode: response.status === 401 || (response.ok && me?.error_code === 401)
143
+ ? 'token_invalid'
144
+ : 'connection_failed',
145
+ error: typeof me?.description === 'string' && me.description.trim()
146
+ ? me.description
147
+ : `Telegram getMe failed (HTTP ${response.status})`,
148
+ };
129
149
  return {
130
150
  ok: true,
131
151
  identity: {
@@ -134,7 +154,6 @@ async function testTelegram(channel) {
134
154
  displayName: me.result.first_name,
135
155
  },
136
156
  messageSent: false,
137
- hint: '发送 /start 给机器人后可在运行态接收消息。',
138
157
  };
139
158
  }
140
159
  catch (error) {
@@ -184,14 +203,13 @@ async function testWeCom(channel) {
184
203
  const botId = channel.appId?.trim() ?? '';
185
204
  const secret = channel.appSecret?.trim() ?? '';
186
205
  if (!botId || !secret) {
187
- return { ok: false, error: '企业微信 AI 机器人需要 Bot ID 与 Secret', verified: false };
206
+ return { ok: false, errorCode: 'wecom_credentials_missing', verified: false };
188
207
  }
189
208
  return {
190
209
  ok: true,
191
210
  verified: false,
192
211
  identity: { id: botId, username: botId, displayName: botId },
193
212
  capabilities: { auth: false },
194
- hint: '已保存凭据;企业微信 AI 机器人的连接状态以运行态长连接为准。',
195
213
  };
196
214
  }
197
215
  /**
@@ -213,7 +231,7 @@ async function testDingTalk(channel) {
213
231
  const appkey = channel.appId?.trim() ?? '';
214
232
  const appsecret = channel.appSecret?.trim() ?? '';
215
233
  if (!appkey || !appsecret) {
216
- return { ok: false, error: '钉钉需要 appkey 与 appsecret' };
234
+ return { ok: false, errorCode: 'dingtalk_credentials_missing' };
217
235
  }
218
236
  const url = 'https://oapi.dingtalk.com/gettoken?appkey=' +
219
237
  encodeURIComponent(appkey) +
@@ -228,17 +246,16 @@ async function testDingTalk(channel) {
228
246
  if (json.errcode && json.errcode !== 0) {
229
247
  return {
230
248
  ok: false,
231
- error: json.errmsg ? `钉钉: ${json.errmsg}` : `钉钉 errcode ${json.errcode}`,
249
+ error: json.errmsg ? String(json.errmsg) : `errcode ${json.errcode}`,
232
250
  };
233
251
  }
234
252
  if (typeof json.access_token !== 'string' || json.access_token.length === 0) {
235
- return { ok: false, error: '钉钉凭据测试未返回 access_token' };
253
+ return { ok: false, errorCode: 'dingtalk_no_access_token' };
236
254
  }
237
255
  return {
238
256
  ok: true,
239
257
  identity: { id: appkey, username: appkey, displayName: appkey },
240
258
  capabilities: { auth: true },
241
- hint: '凭据有效;接收消息需要 outgoing 机器人或 Stream 模式配置。',
242
259
  };
243
260
  }
244
261
  catch (error) {
@@ -264,7 +281,7 @@ async function testQQ(channel) {
264
281
  const appId = channel.appId?.trim() ?? '';
265
282
  const clientSecret = channel.appSecret?.trim() ?? '';
266
283
  if (!appId || !clientSecret) {
267
- return { ok: false, error: 'QQ 官方机器人需要 App ID 与 Client Secret' };
284
+ return { ok: false, errorCode: 'qq_credentials_missing' };
268
285
  }
269
286
  try {
270
287
  const response = await proxiedFetch('https://bots.qq.com/app/getAppAccessToken', {
@@ -276,18 +293,17 @@ async function testQQ(channel) {
276
293
  const json = await response.json().catch(() => ({}));
277
294
  if (!response.ok) {
278
295
  const message = typeof json.message === 'string' && json.message.length > 0
279
- ? `QQ: ${json.message}`
296
+ ? json.message
280
297
  : `HTTP ${response.status}`;
281
298
  return { ok: false, error: message };
282
299
  }
283
300
  if (typeof json.access_token !== 'string' || json.access_token.length === 0) {
284
- return { ok: false, error: 'QQ 官方机器人凭据测试未返回 access_token' };
301
+ return { ok: false, errorCode: 'qq_no_access_token' };
285
302
  }
286
303
  return {
287
304
  ok: true,
288
305
  identity: { id: appId, username: appId, displayName: appId },
289
306
  capabilities: { auth: true },
290
- hint: '凭据有效;接收消息需要 QQ Gateway WebSocket 接入。',
291
307
  };
292
308
  }
293
309
  catch (error) {
@@ -298,7 +314,7 @@ async function testFeishu(channel) {
298
314
  const appId = channel.appId ?? '';
299
315
  const appSecret = channel.appSecret || channel.token;
300
316
  if (!appId || !appSecret)
301
- return { ok: false, error: 'Feishu appId and appSecret are required' };
317
+ return { ok: false, errorCode: 'feishu_credentials_missing' };
302
318
  // PR1197 review (P1-4): brand-switch the account host by the channel domain,
303
319
  // exactly like the onboarding flow does. A Lark tenant (larksuite.com) cannot
304
320
  // issue a tenant_access_token from the feishu.cn host, so a hardcoded host
@@ -163,7 +163,9 @@ export class DingTalkBotBridge extends WsBridgeBase {
163
163
  token = null;
164
164
  closeReasonPrefix = 'stream';
165
165
  checkCredentials() {
166
- return this.settings.appId?.trim() && this.settings.appSecret?.trim() ? null : 'no-credentials';
166
+ return this.settings.appId?.trim() && this.settings.appSecret?.trim()
167
+ ? null
168
+ : 'dingtalk_credentials_missing';
167
169
  }
168
170
  decideClose(code, explicitlyStopped) {
169
171
  const decision = decideDingTalkClose(code, explicitlyStopped);
@@ -221,7 +223,7 @@ export class DingTalkBotBridge extends WsBridgeBase {
221
223
  this.connect(`${json.endpoint}?ticket=${encodeURIComponent(json.ticket)}`);
222
224
  }
223
225
  catch (error) {
224
- this.reason = error instanceof Error ? error.message : String(error);
226
+ this.recordFailure(error);
225
227
  this.readiness = 'configured';
226
228
  this.emitStatusChange();
227
229
  this.scheduleReconnect();
@@ -295,7 +297,7 @@ export class DingTalkBotBridge extends WsBridgeBase {
295
297
  }
296
298
  if (classification.kind !== 'ok') {
297
299
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
298
- this.reason = classification.kind === 'retry' ? 'rate-limited' : classification.description;
300
+ this.recordFailure(classification.kind === 'retry' ? 'rate-limited' : classification.description, classification.kind === 'retry' ? 'rate-limited' : 'send-failed');
299
301
  this.emitStatusChange();
300
302
  return null;
301
303
  }
@@ -340,7 +342,7 @@ export class DingTalkBotBridge extends WsBridgeBase {
340
342
  const response = await proxiedFetch(url, { method: 'GET', timeoutMs: 10_000 });
341
343
  const json = (await response.json().catch(() => null));
342
344
  if (!json || (json.errcode !== undefined && json.errcode !== 0)) {
343
- this.reason = json?.errmsg ?? 'gettoken failed';
345
+ this.recordFailure(json?.errmsg ?? 'gettoken failed', 'dingtalk_no_access_token');
344
346
  return null;
345
347
  }
346
348
  if (typeof json.access_token !== 'string')
@@ -353,7 +355,7 @@ export class DingTalkBotBridge extends WsBridgeBase {
353
355
  return this.token.value;
354
356
  }
355
357
  catch (error) {
356
- this.reason = error instanceof Error ? error.message : String(error);
358
+ this.recordFailure(error);
357
359
  return null;
358
360
  }
359
361
  }
@@ -153,7 +153,7 @@ function sleep(ms) {
153
153
  export class DiscordBotBridge extends GatewayBridgeBase {
154
154
  resumeGatewayUrl = null;
155
155
  checkCredentials() {
156
- return this.settings.token.trim() ? null : 'no-token';
156
+ return this.settings.token.trim() ? null : 'token_missing';
157
157
  }
158
158
  decideClose(code, explicitlyStopped) {
159
159
  return decideDiscordClose(code, explicitlyStopped);
@@ -176,7 +176,7 @@ export class DiscordBotBridge extends GatewayBridgeBase {
176
176
  const json = await response.json().catch(() => null);
177
177
  if (!response.ok || !json || typeof json.url !== 'string') {
178
178
  const message = json?.message;
179
- this.reason = typeof message === 'string' ? message : `gateway-bot-${response.status}`;
179
+ this.recordFailure(message, `gateway-bot-${response.status}`);
180
180
  this.readiness = 'configured';
181
181
  this.emitStatusChange();
182
182
  // Usually a transient outage — openConnection schedules the retry.
@@ -186,7 +186,7 @@ export class DiscordBotBridge extends GatewayBridgeBase {
186
186
  return `${gatewayUrl}/?v=${DISCORD_GATEWAY_VERSION}&encoding=json`;
187
187
  }
188
188
  catch (error) {
189
- this.reason = error instanceof Error ? error.message : String(error);
189
+ this.recordFailure(error);
190
190
  this.readiness = 'configured';
191
191
  this.emitStatusChange();
192
192
  return null;
@@ -257,7 +257,7 @@ export class DiscordBotBridge extends GatewayBridgeBase {
257
257
  }
258
258
  if (classification.kind !== 'ok') {
259
259
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
260
- this.reason = classification.kind === 'retry' ? 'rate-limited' : classification.description;
260
+ this.recordFailure(classification.kind === 'retry' ? 'rate-limited' : classification.description, classification.kind === 'retry' ? 'rate-limited' : 'send-failed');
261
261
  this.emitStatusChange();
262
262
  return null;
263
263
  }
@@ -17,7 +17,6 @@
17
17
  * under the License.
18
18
  */
19
19
  import { Domain, LoggerLevel, createLarkChannel, } from '@larksuiteoapi/node-sdk';
20
- import { generalizedErrorMessage } from '@maka/core/redaction';
21
20
  import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
22
21
  const HANDSHAKE_TIMEOUT_MS = 15_000;
23
22
  /**
@@ -76,7 +75,7 @@ export class FeishuBotBridge extends BaseBotAdapter {
76
75
  const appId = this.settings.appId?.trim() ?? '';
77
76
  const appSecret = this.settings.appSecret?.trim() || this.settings.token.trim();
78
77
  if (!appId || !appSecret) {
79
- this.reason = 'missing-feishu-credentials';
78
+ this.reason = 'feishu_credentials_missing';
80
79
  this.readiness = 'scaffolded';
81
80
  this.emitStatusChange();
82
81
  return;
@@ -110,7 +109,7 @@ export class FeishuBotBridge extends BaseBotAdapter {
110
109
  if (this.explicitlyStopped || this.channel !== channel)
111
110
  return;
112
111
  this.running = false;
113
- this.reason = generalizedErrorMessage(error);
112
+ this.recordFailure(error);
114
113
  this.readiness = 'configured';
115
114
  this.emitStatusChange();
116
115
  await this.disconnectChannel(channel);
@@ -142,7 +141,7 @@ export class FeishuBotBridge extends BaseBotAdapter {
142
141
  }
143
142
  catch (error) {
144
143
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
145
- this.reason = generalizedErrorMessage(error);
144
+ this.recordFailure(error, 'send-failed');
146
145
  this.emitStatusChange();
147
146
  return null;
148
147
  }
@@ -179,7 +178,7 @@ export class FeishuBotBridge extends BaseBotAdapter {
179
178
  }), channel.on('error', (error) => {
180
179
  if (this.channel !== channel || this.explicitlyStopped)
181
180
  return;
182
- this.reason = generalizedErrorMessage(error);
181
+ this.recordFailure(error);
183
182
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'configured';
184
183
  this.emitStatusChange();
185
184
  }), channel.on('reconnecting', () => {
@@ -199,7 +199,9 @@ function sleep(ms) {
199
199
  export class QQBotBridge extends GatewayBridgeBase {
200
200
  token = null;
201
201
  checkCredentials() {
202
- return this.settings.appId?.trim() && this.settings.appSecret?.trim() ? null : 'no-credentials';
202
+ return this.settings.appId?.trim() && this.settings.appSecret?.trim()
203
+ ? null
204
+ : 'qq_credentials_missing';
203
205
  }
204
206
  decideClose(code, explicitlyStopped) {
205
207
  return decideQQClose(code, explicitlyStopped);
@@ -227,7 +229,7 @@ export class QQBotBridge extends GatewayBridgeBase {
227
229
  return json.url;
228
230
  }
229
231
  catch (error) {
230
- this.reason = error instanceof Error ? error.message : String(error);
232
+ this.recordFailure(error);
231
233
  this.readiness = 'configured';
232
234
  this.emitStatusChange();
233
235
  return null;
@@ -311,7 +313,7 @@ export class QQBotBridge extends GatewayBridgeBase {
311
313
  }
312
314
  if (classification.kind !== 'ok') {
313
315
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
314
- this.reason = classification.kind === 'retry' ? 'rate-limited' : classification.description;
316
+ this.recordFailure(classification.kind === 'retry' ? 'rate-limited' : classification.description, classification.kind === 'retry' ? 'rate-limited' : 'send-failed');
315
317
  this.emitStatusChange();
316
318
  return null;
317
319
  }
@@ -399,7 +401,7 @@ export class QQBotBridge extends GatewayBridgeBase {
399
401
  return this.token.value;
400
402
  }
401
403
  catch (error) {
402
- this.reason = error instanceof Error ? error.message : String(error);
404
+ this.recordFailure(error);
403
405
  return null;
404
406
  }
405
407
  }
@@ -16,7 +16,6 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { generalizedErrorMessage } from '@maka/core/redaction';
20
19
  import { SocketModeClient } from '@slack/socket-mode';
21
20
  import { WebClient } from '@slack/web-api';
22
21
  import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
@@ -54,7 +53,7 @@ export class SlackBotBridge extends BaseBotAdapter {
54
53
  if (!botToken || !appToken) {
55
54
  this.running = false;
56
55
  this.readiness = botReadinessFromSettings(this.settings);
57
- this.reason = 'missing-slack-tokens';
56
+ this.reason = 'slack_tokens_missing';
58
57
  this.emitStatusChange();
59
58
  return;
60
59
  }
@@ -106,7 +105,7 @@ export class SlackBotBridge extends BaseBotAdapter {
106
105
  catch (error) {
107
106
  this.running = false;
108
107
  this.readiness = 'degraded';
109
- this.reason = generalizedErrorMessage(error);
108
+ this.recordFailure(error);
110
109
  this.emitStatusChange();
111
110
  await this.stopTransport();
112
111
  throw error;
@@ -132,7 +131,7 @@ export class SlackBotBridge extends BaseBotAdapter {
132
131
  }
133
132
  catch (error) {
134
133
  this.readiness = 'degraded';
135
- this.reason = generalizedErrorMessage(error);
134
+ this.recordFailure(error, 'send-failed');
136
135
  this.emitStatusChange();
137
136
  return null;
138
137
  }
@@ -16,7 +16,6 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { generalizedErrorMessage } from '@maka/core/redaction';
20
19
  import { truncateUtf16Safe } from '@maka/core/text-sanitize';
21
20
  import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
22
21
  import { proxiedFetch } from './proxied-fetch.js';
@@ -216,7 +215,7 @@ export class TelegramBotBridge extends BaseBotAdapter {
216
215
  return;
217
216
  }
218
217
  if (!this.settings.token.trim()) {
219
- this.reason = 'no-token';
218
+ this.reason = 'token_missing';
220
219
  this.readiness = 'scaffolded';
221
220
  return;
222
221
  }
@@ -255,7 +254,7 @@ export class TelegramBotBridge extends BaseBotAdapter {
255
254
  }
256
255
  if (classification.kind !== 'ok') {
257
256
  this.readiness = this.readiness === 'operational' ? 'degraded' : 'credentials_valid';
258
- this.reason = classification.kind === 'retry' ? 'rate-limited' : classification.description;
257
+ this.recordFailure(classification.kind === 'retry' ? 'rate-limited' : classification.description, classification.kind === 'retry' ? 'rate-limited' : 'send-failed');
259
258
  this.emitStatusChange();
260
259
  return null;
261
260
  }
@@ -331,7 +330,7 @@ export class TelegramBotBridge extends BaseBotAdapter {
331
330
  try {
332
331
  const me = await telegramApi(this.settings.token, 'getMe');
333
332
  if (!me.ok) {
334
- this.reason = me.description ?? 'get-me-failed';
333
+ this.recordFailure(me.description ?? 'get-me-failed', me.error_code === 401 ? 'token_invalid' : 'get-me-failed');
335
334
  this.readiness = 'configured';
336
335
  this.emitStatusChange();
337
336
  return;
@@ -351,7 +350,7 @@ export class TelegramBotBridge extends BaseBotAdapter {
351
350
  void this.pollTelegram();
352
351
  }
353
352
  catch (error) {
354
- this.reason = generalizedErrorMessage(error);
353
+ this.recordFailure(error);
355
354
  this.readiness =
356
355
  this.readiness === 'operational' ? 'degraded' : botReadinessFromSettings(this.settings);
357
356
  this.emitStatusChange();