@phuetz/code-buddy 1.0.0 → 1.2.0
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/README.md +160 -164
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +101 -13
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/lesson-auto-proposer.js +10 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/session-duration.d.ts +36 -0
- package/dist/agent/middleware/session-duration.js +78 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +71 -0
- package/dist/agent/session-end-flush.js +233 -0
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +107 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +5 -0
- package/dist/commands/goal-cli.d.ts +71 -0
- package/dist/commands/goal-cli.js +280 -0
- package/dist/commands/handlers/goal-handler.d.ts +34 -0
- package/dist/commands/handlers/goal-handler.js +155 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/slash/builtin-commands.js +20 -0
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +770 -0
- package/dist/config/feature-flags.js +7 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.d.ts +39 -0
- package/dist/context/context-manager-v2.js +91 -0
- package/dist/daemon/agent-task-executor.js +12 -1
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +5 -3
- package/dist/daemon/autonomous-loop.d.ts +21 -1
- package/dist/daemon/autonomous-loop.js +69 -0
- package/dist/daemon/colab-goal.d.ts +38 -0
- package/dist/daemon/colab-goal.js +81 -0
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.d.ts +21 -0
- package/dist/fleet/colab-store.js +28 -0
- package/dist/fleet/peer-session-bridge.d.ts +1 -1
- package/dist/fleet/peer-session-bridge.js +243 -2
- package/dist/fleet/peer-session-store.d.ts +3 -0
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/fleet/privacy-lint.d.ts +8 -0
- package/dist/fleet/privacy-lint.js +22 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +38 -0
- package/dist/goals/goal-judge.js +158 -0
- package/dist/goals/goal-loop.d.ts +37 -0
- package/dist/goals/goal-loop.js +101 -0
- package/dist/goals/goal-manager.d.ts +77 -0
- package/dist/goals/goal-manager.js +289 -0
- package/dist/goals/goal-state.d.ts +99 -0
- package/dist/goals/goal-state.js +315 -0
- package/dist/goals/goal-store.d.ts +28 -0
- package/dist/goals/goal-store.js +87 -0
- package/dist/goals/index.d.ts +5 -0
- package/dist/goals/index.js +6 -0
- package/dist/hooks/use-input-handler.js +36 -1
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +506 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/observability/run-store.d.ts +1 -1
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.d.ts +13 -1
- package/dist/server/websocket/fleet-bridge.js +16 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- package/dist/utils/kokoro-tts.js +0 -67
|
@@ -9,7 +9,148 @@
|
|
|
9
9
|
* identity-aware headers, full thread context.
|
|
10
10
|
*/
|
|
11
11
|
import { logger } from '../../utils/logger.js';
|
|
12
|
-
import { BaseChannel } from '../core.js';
|
|
12
|
+
import { BaseChannel, } from '../core.js';
|
|
13
|
+
/** Map a Feishu `message_type` onto a Code Buddy {@link ContentType}. */
|
|
14
|
+
function feishuMessageTypeToContentType(messageType) {
|
|
15
|
+
switch (messageType) {
|
|
16
|
+
case 'image':
|
|
17
|
+
return 'image';
|
|
18
|
+
case 'audio':
|
|
19
|
+
return 'audio';
|
|
20
|
+
case 'media':
|
|
21
|
+
return 'video';
|
|
22
|
+
case 'file':
|
|
23
|
+
return 'file';
|
|
24
|
+
case 'sticker':
|
|
25
|
+
return 'sticker';
|
|
26
|
+
// 'text', 'post', 'interactive', 'share_chat', … all surface as text.
|
|
27
|
+
default:
|
|
28
|
+
return 'text';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort plain-text extraction from a Feishu message `content` JSON string.
|
|
33
|
+
*
|
|
34
|
+
* - `text` → `{ "text": "hello" }`
|
|
35
|
+
* - `post` → `{ "<locale>": { "title": "...", "content": [[{tag,text}, …]] } }`
|
|
36
|
+
* - otherwise → the raw JSON string (so callers still see *something*).
|
|
37
|
+
*/
|
|
38
|
+
function extractFeishuText(messageType, contentJson) {
|
|
39
|
+
if (!contentJson)
|
|
40
|
+
return '';
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(contentJson);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not JSON (shouldn't happen for real events) — fall back to the raw string.
|
|
47
|
+
return contentJson;
|
|
48
|
+
}
|
|
49
|
+
if (parsed && typeof parsed === 'object') {
|
|
50
|
+
const obj = parsed;
|
|
51
|
+
// text / share_chat / etc.
|
|
52
|
+
if (typeof obj.text === 'string')
|
|
53
|
+
return obj.text;
|
|
54
|
+
// post: locale-keyed rich text. Walk the first locale's `content` matrix
|
|
55
|
+
// and concatenate every `text` segment.
|
|
56
|
+
if (messageType === 'post' || (!('text' in obj) && hasPostShape(obj))) {
|
|
57
|
+
const localeKey = Object.keys(obj)[0];
|
|
58
|
+
const post = localeKey ? obj[localeKey] : undefined;
|
|
59
|
+
const rows = post?.['content'];
|
|
60
|
+
if (Array.isArray(rows)) {
|
|
61
|
+
const pieces = [];
|
|
62
|
+
const title = typeof post?.['title'] === 'string' ? post['title'] : '';
|
|
63
|
+
if (title)
|
|
64
|
+
pieces.push(title);
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
if (!Array.isArray(row))
|
|
67
|
+
continue;
|
|
68
|
+
for (const seg of row) {
|
|
69
|
+
if (seg && typeof seg === 'object') {
|
|
70
|
+
const t = seg.text;
|
|
71
|
+
if (typeof t === 'string')
|
|
72
|
+
pieces.push(t);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return pieces.join(' ').trim();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Unknown structure — return the raw JSON so nothing is silently dropped.
|
|
81
|
+
return contentJson;
|
|
82
|
+
}
|
|
83
|
+
function hasPostShape(obj) {
|
|
84
|
+
const firstKey = Object.keys(obj)[0];
|
|
85
|
+
if (!firstKey)
|
|
86
|
+
return false;
|
|
87
|
+
const v = obj[firstKey];
|
|
88
|
+
return !!v && typeof v === 'object' && 'content' in v;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse a Lark `im.message.receive_v1` event into a Code Buddy
|
|
92
|
+
* {@link InboundMessage}.
|
|
93
|
+
*
|
|
94
|
+
* Envelope-tolerant: accepts either the unwrapped event body (what the SDK's
|
|
95
|
+
* `EventDispatcher` hands the handler) or the full `{ schema, header, event }`
|
|
96
|
+
* webhook envelope.
|
|
97
|
+
*
|
|
98
|
+
* Returns `null` when the event carries no usable message (e.g. a non-message
|
|
99
|
+
* event slipped through, or no chat/text could be resolved).
|
|
100
|
+
*/
|
|
101
|
+
export function parseFeishuMessageEvent(raw) {
|
|
102
|
+
if (!raw || typeof raw !== 'object')
|
|
103
|
+
return null;
|
|
104
|
+
// Unwrap `{ event: … }` if present; otherwise treat `raw` as the body.
|
|
105
|
+
const body = 'event' in raw && raw.event ? raw.event : raw;
|
|
106
|
+
const message = body.message;
|
|
107
|
+
if (!message)
|
|
108
|
+
return null;
|
|
109
|
+
const chatId = message.chat_id;
|
|
110
|
+
if (!chatId)
|
|
111
|
+
return null;
|
|
112
|
+
const senderId = body.sender?.sender_id?.open_id ??
|
|
113
|
+
body.sender?.sender_id?.union_id ??
|
|
114
|
+
body.sender?.sender_id?.user_id ??
|
|
115
|
+
'unknown';
|
|
116
|
+
const content = extractFeishuText(message.message_type, message.content);
|
|
117
|
+
const createTimeMs = message.create_time ? Number(message.create_time) : NaN;
|
|
118
|
+
const timestamp = Number.isFinite(createTimeMs) ? new Date(createTimeMs) : new Date();
|
|
119
|
+
const inbound = {
|
|
120
|
+
id: message.message_id ?? `feishu_${Date.now()}`,
|
|
121
|
+
channel: {
|
|
122
|
+
id: chatId,
|
|
123
|
+
type: 'feishu',
|
|
124
|
+
isDM: message.chat_type === 'p2p',
|
|
125
|
+
isGroup: message.chat_type === 'group',
|
|
126
|
+
},
|
|
127
|
+
sender: {
|
|
128
|
+
id: senderId,
|
|
129
|
+
raw: body.sender,
|
|
130
|
+
},
|
|
131
|
+
content,
|
|
132
|
+
contentType: feishuMessageTypeToContentType(message.message_type),
|
|
133
|
+
timestamp,
|
|
134
|
+
raw,
|
|
135
|
+
};
|
|
136
|
+
if (message.thread_id)
|
|
137
|
+
inbound.threadId = message.thread_id;
|
|
138
|
+
if (message.parent_id)
|
|
139
|
+
inbound.replyTo = message.parent_id;
|
|
140
|
+
return inbound;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Legacy in-process adapter.
|
|
144
|
+
*
|
|
145
|
+
* The real outbound transport now lives in {@link FeishuChannel}, which mints a
|
|
146
|
+
* genuine `tenant_access_token` and POSTs through the REST `im/v1/messages`
|
|
147
|
+
* API. This class is retained for its still-useful, network-free helpers — the
|
|
148
|
+
* interactive-card builders, reasoning-stream hooks, and `getThreadMessages`
|
|
149
|
+
* (exercised by `tests/channels/feishu-cards.test.ts`). Its `sendText` /
|
|
150
|
+
* `sendCard` / `sendImage` / `replyMessage` methods, however, perform NO
|
|
151
|
+
* network I/O and return synthetic ids; they are kept only for backward-compat
|
|
152
|
+
* and are not on the real send path. Prefer {@link FeishuChannel.send}.
|
|
153
|
+
*/
|
|
13
154
|
export class FeishuAdapter {
|
|
14
155
|
config;
|
|
15
156
|
running = false;
|
|
@@ -39,6 +180,7 @@ export class FeishuAdapter {
|
|
|
39
180
|
isRunning() {
|
|
40
181
|
return this.running;
|
|
41
182
|
}
|
|
183
|
+
/** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
|
|
42
184
|
async sendText(chatId, text) {
|
|
43
185
|
if (!this.running) {
|
|
44
186
|
throw new Error('FeishuAdapter is not running');
|
|
@@ -47,6 +189,7 @@ export class FeishuAdapter {
|
|
|
47
189
|
logger.debug('FeishuAdapter: send text', { chatId, textLength: text.length });
|
|
48
190
|
return { success: true, messageId };
|
|
49
191
|
}
|
|
192
|
+
/** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
|
|
50
193
|
async sendCard(chatId, card) {
|
|
51
194
|
if (!this.running) {
|
|
52
195
|
throw new Error('FeishuAdapter is not running');
|
|
@@ -55,6 +198,7 @@ export class FeishuAdapter {
|
|
|
55
198
|
logger.debug('FeishuAdapter: send card', { chatId });
|
|
56
199
|
return { success: true, messageId };
|
|
57
200
|
}
|
|
201
|
+
/** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
|
|
58
202
|
async sendImage(chatId, imageKey) {
|
|
59
203
|
if (!this.running) {
|
|
60
204
|
throw new Error('FeishuAdapter is not running');
|
|
@@ -63,6 +207,7 @@ export class FeishuAdapter {
|
|
|
63
207
|
logger.debug('FeishuAdapter: send image', { chatId, imageKey });
|
|
64
208
|
return { success: true, messageId };
|
|
65
209
|
}
|
|
210
|
+
/** @deprecated No network I/O. Use {@link FeishuChannel.send} with `replyTo` instead. */
|
|
66
211
|
async replyMessage(messageId, text) {
|
|
67
212
|
if (!this.running) {
|
|
68
213
|
throw new Error('FeishuAdapter is not running');
|
|
@@ -194,11 +339,46 @@ export class FeishuAdapter {
|
|
|
194
339
|
return [];
|
|
195
340
|
}
|
|
196
341
|
}
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Real-transport status
|
|
344
|
+
// ============================================================================
|
|
345
|
+
const DEFAULT_FEISHU_BASE_URL = 'https://open.feishu.cn';
|
|
197
346
|
export class FeishuChannel extends BaseChannel {
|
|
198
347
|
adapter = null;
|
|
348
|
+
baseUrl;
|
|
349
|
+
/** Cached tenant_access_token (re-minted on demand if the API rejects it). */
|
|
350
|
+
tenantToken = null;
|
|
351
|
+
receiveStatus = null;
|
|
352
|
+
/** Live official-SDK long-connection client, when the SDK is installed. */
|
|
353
|
+
wsClient = null;
|
|
199
354
|
constructor(config) {
|
|
200
355
|
super('feishu', config);
|
|
356
|
+
const rawBase = config.options?.['baseUrl'] ??
|
|
357
|
+
process.env.FEISHU_BASE_URL ??
|
|
358
|
+
process.env.LARK_BASE_URL ??
|
|
359
|
+
DEFAULT_FEISHU_BASE_URL;
|
|
360
|
+
this.baseUrl = rawBase.trim().replace(/\/+$/, '');
|
|
201
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Bring the channel up.
|
|
364
|
+
*
|
|
365
|
+
* Outbound (`send()`) is fully functional: it lazily mints a
|
|
366
|
+
* `tenant_access_token` and POSTs to `/open-apis/im/v1/messages`.
|
|
367
|
+
*
|
|
368
|
+
* Inbound (receiving user messages) requires Feishu's proprietary
|
|
369
|
+
* long-connection — see {@link FeishuReceiveStatus}. We do NOT fake it. We
|
|
370
|
+
* attempt to bring up the REAL inbound socket using the official Lark SDK
|
|
371
|
+
* (`@larksuiteoapi/node-sdk`), imported OPTIONALLY at runtime — it is not a
|
|
372
|
+
* declared dependency, so most installs won't have it. When it IS present
|
|
373
|
+
* (and app credentials are configured) we start its `WSClient` and register
|
|
374
|
+
* the `im.message.receive_v1` handler, which parses each message into an
|
|
375
|
+
* {@link InboundMessage} and re-emits it via `this.emit('message' | 'command')`.
|
|
376
|
+
* The SDK owns the long-connection + reconnect internally.
|
|
377
|
+
*
|
|
378
|
+
* When the SDK is absent (the default) we keep the honest "send-only" state:
|
|
379
|
+
* `status.connected = false`, the receive reason surfaced in
|
|
380
|
+
* `status.error` / `status.info`. No throw — outbound still works.
|
|
381
|
+
*/
|
|
202
382
|
async connect() {
|
|
203
383
|
const cfg = this.config;
|
|
204
384
|
this.adapter = new FeishuAdapter({
|
|
@@ -209,28 +389,185 @@ export class FeishuChannel extends BaseChannel {
|
|
|
209
389
|
port: cfg.port,
|
|
210
390
|
});
|
|
211
391
|
await this.adapter.start();
|
|
212
|
-
|
|
392
|
+
// Default to the honest send-only state. `tryStartInbound()` upgrades it
|
|
393
|
+
// in place if (and only if) the optional SDK is installed and usable.
|
|
394
|
+
this.receiveStatus = {
|
|
395
|
+
connected: false,
|
|
396
|
+
reason: 'lark-sdk-required',
|
|
397
|
+
detail: 'Feishu inbound long-connection (real-time receive) is not active: the ' +
|
|
398
|
+
'official Lark SDK (@larksuiteoapi/node-sdk) — which owns the proprietary ' +
|
|
399
|
+
"Protobuf 'pbbp2' long-connection framing — is not installed (it is an " +
|
|
400
|
+
'optional dependency). Install it and configure app credentials to enable ' +
|
|
401
|
+
'real-time receive. Outbound send() is fully functional via the REST ' +
|
|
402
|
+
'im/v1/messages API.',
|
|
403
|
+
};
|
|
404
|
+
this.status.connected = false;
|
|
405
|
+
this.status.authenticated = false;
|
|
406
|
+
this.status.error = this.receiveStatus.detail;
|
|
407
|
+
this.status.info = {
|
|
408
|
+
outbound: 'ready',
|
|
409
|
+
inbound: this.receiveStatus.reason,
|
|
410
|
+
};
|
|
411
|
+
await this.tryStartInbound(cfg);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Attempt to bring up the REAL inbound long-connection via the optional
|
|
415
|
+
* official Lark SDK. Mutates `this.receiveStatus` / `this.status` to the live
|
|
416
|
+
* `'lark-ws'` state on success; otherwise leaves the honest send-only state
|
|
417
|
+
* untouched. Never throws — a missing SDK or a start() failure degrades
|
|
418
|
+
* gracefully to outbound-only.
|
|
419
|
+
*/
|
|
420
|
+
async tryStartInbound(cfg) {
|
|
421
|
+
// Optional dependency: the `as string` specifier keeps TS/Vite from trying
|
|
422
|
+
// to statically resolve a package that isn't installed, so this becomes a
|
|
423
|
+
// genuine runtime import that rejects (→ caught) when the SDK is absent.
|
|
424
|
+
const lark = (await import('@larksuiteoapi/node-sdk').catch(() => null));
|
|
425
|
+
if (!lark || typeof lark.WSClient !== 'function' || typeof lark.EventDispatcher !== 'function') {
|
|
426
|
+
// SDK not installed — keep the honest 'lark-sdk-required' state.
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!cfg.appId || !cfg.appSecret) {
|
|
430
|
+
// SDK present but unconfigured — still send-only. Keep the reason code
|
|
431
|
+
// (`lark-sdk-required`, so the honest-state contract holds) but correct
|
|
432
|
+
// the detail: the SDK is here; the missing piece is app credentials.
|
|
433
|
+
logger.warn('Feishu: Lark SDK installed but appId/appSecret missing — inbound disabled');
|
|
434
|
+
if (this.receiveStatus) {
|
|
435
|
+
this.receiveStatus.detail =
|
|
436
|
+
'Feishu inbound long-connection is not active: the official Lark SDK ' +
|
|
437
|
+
'(@larksuiteoapi/node-sdk) IS installed, but appId/appSecret are not ' +
|
|
438
|
+
'configured, so the WSClient cannot authenticate. Provide app credentials ' +
|
|
439
|
+
'to enable real-time receive. Outbound send() is fully functional.';
|
|
440
|
+
this.status.error = this.receiveStatus.detail;
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const dispatcher = new lark.EventDispatcher({}).register({
|
|
446
|
+
'im.message.receive_v1': (data) => {
|
|
447
|
+
try {
|
|
448
|
+
this.dispatchInboundEvent(data);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
logger.warn(`Feishu inbound handler error: ${err instanceof Error ? err.message : err}`);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
const wsClient = new lark.WSClient({
|
|
456
|
+
appId: cfg.appId,
|
|
457
|
+
appSecret: cfg.appSecret,
|
|
458
|
+
});
|
|
459
|
+
await wsClient.start({ eventDispatcher: dispatcher });
|
|
460
|
+
this.wsClient = wsClient;
|
|
461
|
+
this.receiveStatus = {
|
|
462
|
+
connected: true,
|
|
463
|
+
reason: 'lark-ws',
|
|
464
|
+
detail: 'Feishu inbound is live over the official Lark SDK WSClient long-connection ' +
|
|
465
|
+
'(@larksuiteoapi/node-sdk). im.message.receive_v1 events are parsed into ' +
|
|
466
|
+
'InboundMessages and re-emitted; the SDK manages reconnect internally.',
|
|
467
|
+
};
|
|
468
|
+
this.status.connected = true;
|
|
469
|
+
this.status.authenticated = true;
|
|
470
|
+
delete this.status.error;
|
|
471
|
+
this.status.info = { outbound: 'ready', inbound: 'lark-ws' };
|
|
472
|
+
this.status.lastActivity = new Date();
|
|
473
|
+
logger.info('Feishu: inbound long-connection established via @larksuiteoapi/node-sdk');
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
// start() failed (bad creds, network). Stay honest: send-only.
|
|
477
|
+
logger.warn(`Feishu: Lark WSClient failed to start, falling back to send-only: ${err instanceof Error ? err.message : err}`);
|
|
478
|
+
this.wsClient = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Parse a raw `im.message.receive_v1` event and re-emit it as a Code Buddy
|
|
483
|
+
* `message` (and `command`) event. This is the seam the live SDK handler
|
|
484
|
+
* delegates to; it is also unit-testable directly without a live tenant.
|
|
485
|
+
*
|
|
486
|
+
* @returns the parsed {@link InboundMessage}, or `null` if the event carried
|
|
487
|
+
* no usable message.
|
|
488
|
+
*/
|
|
489
|
+
dispatchInboundEvent(event) {
|
|
490
|
+
const parsed = parseFeishuMessageEvent(event);
|
|
491
|
+
if (!parsed)
|
|
492
|
+
return null;
|
|
493
|
+
// Skip messages from the bot itself / disallowed users where configured.
|
|
494
|
+
if (!this.isUserAllowed(parsed.sender.id))
|
|
495
|
+
return null;
|
|
496
|
+
if (!this.isChannelAllowed(parsed.channel.id))
|
|
497
|
+
return null;
|
|
498
|
+
const withCommand = this.parseCommand(parsed);
|
|
499
|
+
this.status.lastActivity = new Date();
|
|
500
|
+
this.emit('message', withCommand);
|
|
501
|
+
if (withCommand.isCommand) {
|
|
502
|
+
this.emit('command', withCommand);
|
|
503
|
+
}
|
|
504
|
+
return withCommand;
|
|
213
505
|
}
|
|
214
506
|
async disconnect() {
|
|
507
|
+
if (this.wsClient) {
|
|
508
|
+
try {
|
|
509
|
+
await this.wsClient.stop?.();
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
logger.debug(`Feishu: WSClient stop error: ${err instanceof Error ? err.message : err}`);
|
|
513
|
+
}
|
|
514
|
+
this.wsClient = null;
|
|
515
|
+
}
|
|
215
516
|
if (this.adapter) {
|
|
216
517
|
await this.adapter.stop();
|
|
217
518
|
this.adapter = null;
|
|
218
519
|
}
|
|
520
|
+
this.tenantToken = null;
|
|
521
|
+
this.receiveStatus = null;
|
|
219
522
|
this.status.connected = false;
|
|
523
|
+
this.status.authenticated = false;
|
|
524
|
+
delete this.status.error;
|
|
525
|
+
this.emit('disconnected', this.type);
|
|
220
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Send an outbound message through the real Feishu REST API.
|
|
529
|
+
*
|
|
530
|
+
* Cards are sent with `msg_type: 'interactive'`; everything else as
|
|
531
|
+
* `msg_type: 'text'`. The chat id (`message.channelId`) becomes the
|
|
532
|
+
* `receive_id` with `receive_id_type=chat_id`.
|
|
533
|
+
*/
|
|
221
534
|
async send(message) {
|
|
222
535
|
if (!this.adapter) {
|
|
223
536
|
return { success: false, error: 'Not connected', timestamp: new Date() };
|
|
224
537
|
}
|
|
225
538
|
const chatId = message.channelId || '';
|
|
226
|
-
|
|
539
|
+
if (!chatId) {
|
|
540
|
+
return { success: false, error: 'Missing channelId (Feishu chat_id)', timestamp: new Date() };
|
|
541
|
+
}
|
|
227
542
|
const feishuData = message.channelData?.feishu;
|
|
543
|
+
let msgType;
|
|
544
|
+
let content;
|
|
228
545
|
if (feishuData?.card) {
|
|
229
|
-
|
|
230
|
-
|
|
546
|
+
msgType = 'interactive';
|
|
547
|
+
content = JSON.stringify(feishuData.card);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
msgType = 'text';
|
|
551
|
+
content = JSON.stringify({ text: message.content });
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
return await this.postMessage(chatId, msgType, content);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
error: error instanceof Error ? error.message : String(error),
|
|
560
|
+
timestamp: new Date(),
|
|
561
|
+
};
|
|
231
562
|
}
|
|
232
|
-
|
|
233
|
-
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Honest, structured account of the inbound receive channel. Returns `null`
|
|
566
|
+
* before `connect()` has run; otherwise a {@link FeishuReceiveStatus}
|
|
567
|
+
* explaining why no live socket exists.
|
|
568
|
+
*/
|
|
569
|
+
getReceiveStatus() {
|
|
570
|
+
return this.receiveStatus;
|
|
234
571
|
}
|
|
235
572
|
/**
|
|
236
573
|
* Get the underlying adapter (for direct card/reasoning API access).
|
|
@@ -238,5 +575,93 @@ export class FeishuChannel extends BaseChannel {
|
|
|
238
575
|
getAdapter() {
|
|
239
576
|
return this.adapter;
|
|
240
577
|
}
|
|
578
|
+
// ==========================================================================
|
|
579
|
+
// REST outbound (real)
|
|
580
|
+
// ==========================================================================
|
|
581
|
+
/**
|
|
582
|
+
* POST a message to `/open-apis/im/v1/messages`, minting (and caching) a
|
|
583
|
+
* tenant_access_token first. On an auth-class failure the token is dropped
|
|
584
|
+
* and the call retried once with a fresh token.
|
|
585
|
+
*/
|
|
586
|
+
async postMessage(chatId, msgType, content, retried = false) {
|
|
587
|
+
const token = await this.ensureTenantToken();
|
|
588
|
+
const url = `${this.baseUrl}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
|
589
|
+
const response = await fetch(url, {
|
|
590
|
+
method: 'POST',
|
|
591
|
+
headers: {
|
|
592
|
+
Authorization: `Bearer ${token}`,
|
|
593
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
594
|
+
},
|
|
595
|
+
body: JSON.stringify({ receive_id: chatId, msg_type: msgType, content }),
|
|
596
|
+
});
|
|
597
|
+
const text = await response.text().catch(() => '');
|
|
598
|
+
let parsed = {};
|
|
599
|
+
try {
|
|
600
|
+
parsed = text ? JSON.parse(text) : {};
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
parsed = {};
|
|
604
|
+
}
|
|
605
|
+
const code = typeof parsed.code === 'number' ? parsed.code : undefined;
|
|
606
|
+
// Feishu signals an expired/invalid tenant token with a non-zero `code`
|
|
607
|
+
// (99991663/99991664/...) and HTTP 200. Drop the cached token and retry once.
|
|
608
|
+
if (!retried && (code === 99991663 || code === 99991664 || response.status === 401)) {
|
|
609
|
+
this.tenantToken = null;
|
|
610
|
+
return this.postMessage(chatId, msgType, content, true);
|
|
611
|
+
}
|
|
612
|
+
if (!response.ok || (code !== undefined && code !== 0)) {
|
|
613
|
+
const msg = typeof parsed.msg === 'string' ? parsed.msg : text.slice(0, 300);
|
|
614
|
+
return {
|
|
615
|
+
success: false,
|
|
616
|
+
error: `Feishu send failed: status=${response.status} code=${code ?? 'n/a'} msg=${msg}`,
|
|
617
|
+
timestamp: new Date(),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const data = (parsed.data ?? {});
|
|
621
|
+
const messageId = typeof data.message_id === 'string' ? data.message_id : undefined;
|
|
622
|
+
this.status.lastActivity = new Date();
|
|
623
|
+
const result = { success: true, timestamp: new Date() };
|
|
624
|
+
if (messageId)
|
|
625
|
+
result.messageId = messageId;
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Return a cached tenant_access_token, minting one via
|
|
630
|
+
* `/open-apis/auth/v3/tenant_access_token/internal` if needed. This is the
|
|
631
|
+
* same internal-app credential exchange used by `src/tools/feishu-tool.ts`.
|
|
632
|
+
*/
|
|
633
|
+
async ensureTenantToken() {
|
|
634
|
+
if (this.tenantToken)
|
|
635
|
+
return this.tenantToken;
|
|
636
|
+
const cfg = this.config;
|
|
637
|
+
if (!cfg.appId || !cfg.appSecret) {
|
|
638
|
+
throw new Error('Feishu appId/appSecret are required to mint a tenant_access_token');
|
|
639
|
+
}
|
|
640
|
+
const response = await fetch(`${this.baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
641
|
+
method: 'POST',
|
|
642
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
643
|
+
body: JSON.stringify({ app_id: cfg.appId, app_secret: cfg.appSecret }),
|
|
644
|
+
});
|
|
645
|
+
const text = await response.text().catch(() => '');
|
|
646
|
+
let parsed = {};
|
|
647
|
+
try {
|
|
648
|
+
parsed = text ? JSON.parse(text) : {};
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
parsed = {};
|
|
652
|
+
}
|
|
653
|
+
const code = typeof parsed.code === 'number' ? parsed.code : undefined;
|
|
654
|
+
if (!response.ok || (code !== undefined && code !== 0)) {
|
|
655
|
+
const msg = typeof parsed.msg === 'string' ? parsed.msg : text.slice(0, 300);
|
|
656
|
+
throw new Error(`Feishu tenant_access_token request failed: status=${response.status} code=${code ?? 'n/a'} msg=${msg}`);
|
|
657
|
+
}
|
|
658
|
+
const token = typeof parsed.tenant_access_token === 'string' ? parsed.tenant_access_token : '';
|
|
659
|
+
if (!token) {
|
|
660
|
+
throw new Error('Feishu tenant_access_token response missing tenant_access_token');
|
|
661
|
+
}
|
|
662
|
+
this.tenantToken = token;
|
|
663
|
+
this.status.authenticated = true;
|
|
664
|
+
return token;
|
|
665
|
+
}
|
|
241
666
|
}
|
|
242
667
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* Unified lifecycle management (start/stop/restart/status) for the
|
|
5
|
+
* messaging gateway subsystem. Tracks which channel adapters are
|
|
6
|
+
* currently active and provides per-channel readiness information.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import type { ChannelType } from './core.js';
|
|
10
|
+
import { ChannelManager } from './core.js';
|
|
11
|
+
/**
|
|
12
|
+
* Per-channel readiness state
|
|
13
|
+
*/
|
|
14
|
+
export type ChannelReadiness = 'connected' | 'disconnected' | 'error' | 'not-configured';
|
|
15
|
+
/**
|
|
16
|
+
* Per-channel status entry in the gateway status report
|
|
17
|
+
*/
|
|
18
|
+
export interface GatewayChannelStatus {
|
|
19
|
+
/** Channel type identifier */
|
|
20
|
+
channelId: ChannelType;
|
|
21
|
+
/** Current readiness state */
|
|
22
|
+
readiness: ChannelReadiness;
|
|
23
|
+
/** Whether the channel is authenticated */
|
|
24
|
+
authenticated: boolean;
|
|
25
|
+
/** Last activity timestamp (ISO string) */
|
|
26
|
+
lastActivity?: string;
|
|
27
|
+
/** Error message if readiness is 'error' */
|
|
28
|
+
error?: string;
|
|
29
|
+
/** Additional platform-specific info */
|
|
30
|
+
info?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Full gateway status report
|
|
34
|
+
*/
|
|
35
|
+
export interface GatewayStatus {
|
|
36
|
+
/** Overall gateway health */
|
|
37
|
+
ok: boolean;
|
|
38
|
+
/** Timestamp of this report */
|
|
39
|
+
generatedAt: string;
|
|
40
|
+
/** Total number of registered channels */
|
|
41
|
+
totalChannels: number;
|
|
42
|
+
/** Number of connected channels */
|
|
43
|
+
connectedCount: number;
|
|
44
|
+
/** Number of channels in error state */
|
|
45
|
+
errorCount: number;
|
|
46
|
+
/** Number of disconnected channels */
|
|
47
|
+
disconnectedCount: number;
|
|
48
|
+
/** Per-channel status entries */
|
|
49
|
+
channels: GatewayChannelStatus[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Events emitted by the GatewayLifecycleManager
|
|
53
|
+
*/
|
|
54
|
+
export interface GatewayLifecycleEvents {
|
|
55
|
+
'channel:started': (channelId: ChannelType) => void;
|
|
56
|
+
'channel:stopped': (channelId: ChannelType) => void;
|
|
57
|
+
'channel:error': (channelId: ChannelType, error: Error) => void;
|
|
58
|
+
'gateway:started': () => void;
|
|
59
|
+
'gateway:stopped': () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Manages the lifecycle of the messaging gateway and its channel adapters.
|
|
63
|
+
*
|
|
64
|
+
* Wraps the existing ChannelManager with explicit start/stop/restart/status
|
|
65
|
+
* semantics for individual channels and the entire gateway.
|
|
66
|
+
*/
|
|
67
|
+
export declare class GatewayLifecycleManager extends EventEmitter {
|
|
68
|
+
private manager;
|
|
69
|
+
private activeChannels;
|
|
70
|
+
private channelErrors;
|
|
71
|
+
constructor(manager?: ChannelManager);
|
|
72
|
+
/**
|
|
73
|
+
* Get the underlying ChannelManager.
|
|
74
|
+
*/
|
|
75
|
+
getManager(): ChannelManager;
|
|
76
|
+
/**
|
|
77
|
+
* Start a single channel adapter by its channel type.
|
|
78
|
+
*
|
|
79
|
+
* The channel must already be registered with the ChannelManager.
|
|
80
|
+
* This calls connect() on the adapter and marks it as active.
|
|
81
|
+
*
|
|
82
|
+
* @param channelId - The channel type to start
|
|
83
|
+
* @throws Error if the channel is not registered
|
|
84
|
+
*/
|
|
85
|
+
start(channelId: ChannelType): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Stop a single channel adapter by its channel type.
|
|
88
|
+
*
|
|
89
|
+
* Calls disconnect() on the adapter and removes it from the active set.
|
|
90
|
+
*
|
|
91
|
+
* @param channelId - The channel type to stop
|
|
92
|
+
* @throws Error if the channel is not registered
|
|
93
|
+
*/
|
|
94
|
+
stop(channelId: ChannelType): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Restart a single channel adapter (stop + start).
|
|
97
|
+
*
|
|
98
|
+
* @param channelId - The channel type to restart
|
|
99
|
+
*/
|
|
100
|
+
restart(channelId: ChannelType): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Start all registered channel adapters.
|
|
103
|
+
*/
|
|
104
|
+
startAll(): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Stop all active channel adapters.
|
|
107
|
+
*/
|
|
108
|
+
stopAll(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Get the full gateway status report.
|
|
111
|
+
*
|
|
112
|
+
* Aggregates per-channel readiness from the ChannelManager's status,
|
|
113
|
+
* the active set, and any recorded errors.
|
|
114
|
+
*/
|
|
115
|
+
status(): GatewayStatus;
|
|
116
|
+
/**
|
|
117
|
+
* Check whether a specific channel is currently active.
|
|
118
|
+
*/
|
|
119
|
+
isActive(channelId: ChannelType): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Get the set of currently active channel IDs.
|
|
122
|
+
*/
|
|
123
|
+
getActiveChannels(): ChannelType[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get the singleton GatewayLifecycleManager instance.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getGatewayLifecycle(): GatewayLifecycleManager;
|
|
129
|
+
/**
|
|
130
|
+
* Reset the singleton (for testing).
|
|
131
|
+
*/
|
|
132
|
+
export declare function resetGatewayLifecycle(): void;
|