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.
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-events.js +18 -63
- package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
- package/node_modules/@maka/core/dist/record-schema.js +13 -0
- package/node_modules/@maka/core/dist/redaction.js +41 -58
- package/node_modules/@maka/core/dist/session-trace.js +5 -1
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
- package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
- package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
- package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
- package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
- package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
- package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
- package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
- package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
- package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
- package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
- package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
- package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
- package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
- package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
- package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
- 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
|
|
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
|
|
223
|
-
|
|
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:
|
|
241
|
-
imperative: Boolean(
|
|
242
|
-
...(
|
|
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
|
|
379
|
+
function directWorkHubNamedAction(value, malformedLiteral, patterns) {
|
|
359
380
|
if (malformedLiteral || /[??]\s*$/u.test(value))
|
|
360
|
-
return
|
|
361
|
-
const match =
|
|
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
|
|
365
|
-
const target =
|
|
366
|
-
|
|
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
|
|
389
|
+
function stripMatchingActionQuotes(value) {
|
|
376
390
|
const pairs = new Map([
|
|
377
391
|
['"', '"'],
|
|
378
392
|
["'", "'"],
|
|
@@ -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
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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,
|
|
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,
|
|
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:
|
|
96
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
async function testWechat(channel) {
|
|
91
|
-
if (
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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,
|
|
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,
|
|
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 ?
|
|
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,
|
|
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,
|
|
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
|
-
?
|
|
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,
|
|
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,
|
|
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()
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 : '
|
|
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.
|
|
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.
|
|
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.
|
|
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 = '
|
|
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.
|
|
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.
|
|
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.
|
|
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()
|
|
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.
|
|
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.
|
|
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.
|
|
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 = '
|
|
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.
|
|
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.
|
|
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 = '
|
|
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.
|
|
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.
|
|
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.
|
|
353
|
+
this.recordFailure(error);
|
|
355
354
|
this.readiness =
|
|
356
355
|
this.readiness === 'operational' ? 'degraded' : botReadinessFromSettings(this.settings);
|
|
357
356
|
this.emitStatusChange();
|