@agentprojectcontext/apx 1.32.0 → 1.32.2
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/package.json +6 -1
- package/skills/apc-context/SKILL.md +5 -2
- package/skills/apx/SKILL.md +3 -3
- package/skills/apx-agency-agents/SKILL.md +5 -5
- package/skills/apx-agent/SKILL.md +7 -7
- package/skills/apx-mcp/SKILL.md +6 -4
- package/skills/apx-mcp-builder/SKILL.md +4 -7
- package/skills/apx-project/SKILL.md +4 -5
- package/skills/apx-routine/SKILL.md +14 -12
- package/skills/apx-runtime/SKILL.md +5 -3
- package/skills/apx-sessions/SKILL.md +5 -5
- package/skills/apx-skill-builder/SKILL.md +10 -6
- package/skills/apx-task/SKILL.md +8 -8
- package/skills/apx-telegram/SKILL.md +23 -7
- package/skills/apx-voice/SKILL.md +8 -6
- package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
- package/src/core/agent/index.js +0 -2
- package/src/core/{agent-memory.js → agent/memory.js} +2 -2
- package/src/core/agent/model-router.js +21 -43
- package/src/core/agent/prompt-builder.js +17 -63
- package/src/core/agent/prompts/action-discipline.md +17 -0
- package/src/core/agent/prompts/channels/code.md +8 -12
- package/src/core/agent/prompts/channels/desktop.md +6 -4
- package/src/core/agent/prompts/channels/routine.md +10 -1
- package/src/core/agent/prompts/channels/telegram.md +5 -0
- package/src/core/agent/prompts/channels/web_code.md +20 -0
- package/src/core/agent/prompts/modes/voice.md +2 -2
- package/src/core/agent/prompts/super-agent-base.md +2 -2
- package/src/core/agent/run-agent.js +37 -35
- package/src/core/agent/runtime-bridge.js +42 -0
- package/src/core/agent/self-memory.js +19 -9
- package/src/core/agent/skills/catalog.js +65 -0
- package/src/core/agent/skills/index.js +6 -0
- package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
- package/src/core/agent/skills/rag.js +91 -0
- package/src/core/agent/skills/trigger.js +71 -0
- package/src/{host/daemon → core/agent}/super-agent.js +5 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
- package/src/core/agent/tools/helpers.js +74 -0
- package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
- package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
- package/src/core/apc/agents-vault.js +37 -0
- package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
- package/src/core/{config.js → config/index.js} +21 -27
- package/src/core/config/paths.js +32 -0
- package/src/core/constants/actors.js +8 -0
- package/src/core/constants/channels.js +19 -0
- package/src/core/constants/index.js +5 -0
- package/src/core/constants/permissions.js +17 -0
- package/src/core/constants/roles.js +9 -0
- package/src/core/engines/_streaming.js +63 -0
- package/src/core/engines/anthropic.js +11 -22
- package/src/core/engines/ollama.js +7 -16
- package/src/core/identity/index.js +8 -0
- package/src/core/{identity.js → identity/self.js} +5 -5
- package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
- package/src/core/logging.js +1 -1
- package/src/core/mascot.js +1 -1
- package/src/core/memory/active-threads.js +10 -10
- package/src/core/memory/broker.js +9 -9
- package/src/core/memory/compactor.js +2 -2
- package/src/core/memory/index.js +2 -2
- package/src/core/memory/indexer.js +1 -1
- package/src/core/{code-sessions-store.js → stores/code-sessions.js} +3 -7
- package/src/core/{messages-store.js → stores/messages.js} +6 -4
- package/src/core/stores/routine-memory.js +71 -0
- package/src/core/{routines-store.js → stores/routines.js} +1 -3
- package/src/core/stores/runtime-sessions.js +99 -0
- package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
- package/src/core/update-check.js +1 -1
- package/src/core/util/ids.js +14 -0
- package/src/core/util/index.js +2 -0
- package/src/core/util/text-similarity.js +52 -0
- package/src/core/util/time.js +9 -0
- package/src/core/voice/tts.js +1 -1
- package/src/host/daemon/api/admin-config.js +4 -3
- package/src/host/daemon/api/admin.js +1 -1
- package/src/host/daemon/api/agents.js +4 -25
- package/src/host/daemon/api/artifacts.js +1 -1
- package/src/host/daemon/api/code.js +48 -16
- package/src/host/daemon/api/confirm.js +1 -1
- package/src/host/daemon/api/connections.js +2 -2
- package/src/host/daemon/api/conversations.js +2 -2
- package/src/host/daemon/api/deck.js +1 -1
- package/src/host/daemon/api/desktop.js +1 -1
- package/src/host/daemon/api/embeddings.js +4 -4
- package/src/host/daemon/api/engines.js +2 -2
- package/src/host/daemon/api/exec.js +3 -3
- package/src/host/daemon/api/identity.js +1 -1
- package/src/host/daemon/api/mcps.js +1 -1
- package/src/host/daemon/api/messages.js +1 -1
- package/src/host/daemon/api/runtimes.js +9 -8
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/sessions.js +2 -2
- package/src/host/daemon/api/shared.js +5 -4
- package/src/host/daemon/api/skills.js +30 -0
- package/src/host/daemon/api/super-agent.js +29 -9
- package/src/host/daemon/api/tasks.js +2 -2
- package/src/host/daemon/api/telegram.js +1 -1
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/tts.js +2 -2
- package/src/host/daemon/api/voice.js +14 -12
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/compact.js +1 -1
- package/src/host/daemon/db.js +4 -4
- package/src/host/daemon/desktop-ws.js +1 -1
- package/src/host/daemon/index.js +4 -4
- package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +11 -6
- package/src/host/daemon/plugins/index.js +2 -2
- package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +66 -195
- package/src/host/daemon/plugins/telegram/media.js +162 -0
- package/src/host/daemon/projects-helpers.js +54 -0
- package/src/host/daemon/routines.js +28 -12
- package/src/host/daemon/smoke.js +2 -2
- package/src/host/daemon/token-store.js +1 -1
- package/src/host/daemon/transcription.js +2 -2
- package/src/host/daemon/wakeup.js +2 -2
- package/src/interfaces/cli/commands/agent.js +3 -3
- package/src/interfaces/cli/commands/command.js +1 -1
- package/src/interfaces/cli/commands/config.js +3 -2
- package/src/interfaces/cli/commands/desktop.js +1 -1
- package/src/interfaces/cli/commands/exec.js +2 -1
- package/src/interfaces/cli/commands/identity.js +2 -2
- package/src/interfaces/cli/commands/init.js +1 -1
- package/src/interfaces/cli/commands/mcp.js +1 -1
- package/src/interfaces/cli/commands/memory.js +2 -2
- package/src/interfaces/cli/commands/model.js +16 -6
- package/src/interfaces/cli/commands/project.js +1 -1
- package/src/interfaces/cli/commands/routine.js +58 -0
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/session.js +4 -4
- package/src/interfaces/cli/commands/setup.js +4 -3
- package/src/interfaces/cli/commands/skills.js +25 -4
- package/src/interfaces/cli/commands/status.js +1 -1
- package/src/interfaces/cli/commands/sys.js +11 -4
- package/src/interfaces/cli/commands/update.js +1 -1
- package/src/interfaces/cli/index.js +4 -4
- package/src/interfaces/cli/postinstall.js +2 -2
- package/src/interfaces/mcp-server/index.js +1 -1
- package/src/interfaces/tui/component/prompt/index.tsx +3 -1
- package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
- package/src/interfaces/tui/context/sync-apx.tsx +20 -2
- package/src/interfaces/tui/context/sync.tsx +2 -1
- package/src/interfaces/tui/routes/session/index.tsx +151 -136
- package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
- package/src/interfaces/tui/run.ts +2 -0
- package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +1 -0
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js +570 -0
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/App.tsx +51 -32
- package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
- package/src/interfaces/web/src/components/UiSelect.tsx +1 -1
- package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +33 -18
- package/src/interfaces/web/src/components/common/TabLayout.tsx +9 -5
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
- package/src/interfaces/web/src/hooks/useChat.ts +47 -2
- package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +59 -0
- package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
- package/src/interfaces/web/src/i18n/en.ts +7 -7
- package/src/interfaces/web/src/i18n/es.ts +7 -7
- package/src/interfaces/web/src/lib/api/skills.ts +25 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +18 -18
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +12 -9
- package/src/skills/apc-context/SKILL.md +159 -0
- package/src/core/agent/ghost-guard.js +0 -24
- package/src/core/agent/prompts/channels/terminal.md +0 -16
- package/src/host/daemon/apc-runtime-context.js +0 -124
- package/src/host/daemon/super-agent-tools/helpers.js +0 -124
- package/src/host/daemon/tool-call-parser.js +0 -2
- package/src/interfaces/web/dist/assets/index-63P_ji1a.js +0 -571
- package/src/interfaces/web/dist/assets/index-63P_ji1a.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-DLWy6dYz.css +0 -1
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/ask-questions.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
- /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
- /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
- /package/src/core/{parser.js → apc/parser.js} +0 -0
- /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
- /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
- /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
- /package/src/core/{session-store.js → stores/sessions.js} +0 -0
- /package/src/host/daemon/plugins/{telegram-ask.js → telegram/ask.js} +0 -0
package/src/host/daemon/index.js
CHANGED
|
@@ -14,17 +14,17 @@ import {
|
|
|
14
14
|
LOG_PATH,
|
|
15
15
|
APX_HOME,
|
|
16
16
|
TOKEN_PATH,
|
|
17
|
-
} from "
|
|
17
|
+
} from "#core/config/index.js";
|
|
18
18
|
import { ProjectManager } from "./db.js";
|
|
19
|
-
import { McpRegistry } from "
|
|
19
|
+
import { McpRegistry } from "#core/mcp/runner.js";
|
|
20
20
|
import { PluginManager } from "./plugins/index.js";
|
|
21
21
|
import { RoutineScheduler } from "./routines.js";
|
|
22
22
|
import { buildApi } from "./api.js";
|
|
23
23
|
import { createTokenStore } from "./token-store.js";
|
|
24
24
|
import { triggerWakeup } from "./wakeup.js";
|
|
25
25
|
import { registerDesktopClient } from "./desktop-ws.js";
|
|
26
|
-
import { log as logToUnified } from "
|
|
27
|
-
import { initMemory, stopMemory } from "
|
|
26
|
+
import { log as logToUnified } from "#core/logging.js";
|
|
27
|
+
import { initMemory, stopMemory } from "#core/memory/index.js";
|
|
28
28
|
|
|
29
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
30
|
const __dirname = path.dirname(__filename);
|
|
@@ -21,11 +21,13 @@ import {
|
|
|
21
21
|
broadcastDesktop,
|
|
22
22
|
sendToClient,
|
|
23
23
|
setDesktopMessageHandler,
|
|
24
|
-
} from "
|
|
25
|
-
import { runSuperAgent, isSuperAgentEnabled } from "
|
|
26
|
-
import { appendGlobalMessage } from "
|
|
24
|
+
} from "../../desktop-ws.js";
|
|
25
|
+
import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
|
|
26
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
27
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
28
|
+
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
27
29
|
|
|
28
|
-
const CHANNEL =
|
|
30
|
+
const CHANNEL = CHANNELS.DESKTOP;
|
|
29
31
|
|
|
30
32
|
export default {
|
|
31
33
|
id: "desktop",
|
|
@@ -128,12 +130,15 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
128
130
|
|
|
129
131
|
log(`desktop: super-agent turn start — model=${cfg.model || config?.super_agent?.model || "(default)"} text="${text.slice(0, 60)}"`);
|
|
130
132
|
const t0 = Date.now();
|
|
133
|
+
const slashed = tryResolveSkillCommand(text);
|
|
134
|
+
const slashedPrompt = slashed.handled ? slashed.prompt : text;
|
|
131
135
|
const result = await runSuperAgent({
|
|
132
136
|
globalConfig: config,
|
|
133
137
|
projects,
|
|
134
138
|
plugins,
|
|
135
|
-
prompt:
|
|
136
|
-
channel:
|
|
139
|
+
prompt: slashedPrompt,
|
|
140
|
+
channel: CHANNELS.DESKTOP,
|
|
141
|
+
...(slashed.handled ? { contextNote: slashed.contextNote } : {}),
|
|
137
142
|
channelMeta: { voice: true }, // desktop module is voice-first → spoken mode
|
|
138
143
|
previousMessages: history.slice(0, -1),
|
|
139
144
|
overrideModel: cfg.model || null,
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Plugins are discovered by static import here. Adding a new plugin = importing
|
|
14
14
|
// it and pushing into PLUGINS.
|
|
15
|
-
import telegramPlugin from "./telegram.js";
|
|
16
|
-
import desktopPlugin from "./desktop.js";
|
|
15
|
+
import telegramPlugin from "./telegram/index.js";
|
|
16
|
+
import desktopPlugin from "./desktop/index.js";
|
|
17
17
|
|
|
18
18
|
export const PLUGINS = [telegramPlugin, desktopPlugin];
|
|
19
19
|
|
|
@@ -29,23 +29,26 @@
|
|
|
29
29
|
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import path from "node:path";
|
|
32
|
-
import { TELEGRAM_STATE_PATH, APX_HOME } from "
|
|
33
|
-
import { callEngine } from "
|
|
34
|
-
import { runSuperAgent, isSuperAgentEnabled } from "
|
|
35
|
-
import { stripThinking } from "
|
|
36
|
-
import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "
|
|
37
|
-
import { compactChannelIfNeeded } from "
|
|
38
|
-
import { readAgents } from "
|
|
39
|
-
import { buildAgentSystem } from "
|
|
40
|
-
import { transcribe as transcribeAudioFile } from "
|
|
41
|
-
import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "
|
|
42
|
-
import { registerSender, resolveAllowedTools } from "
|
|
43
|
-
import { buildRelationshipBlock } from "
|
|
44
|
-
import { getConfirmationStore as getConfirmStore } from "
|
|
45
|
-
import {
|
|
46
|
-
import
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
import { TELEGRAM_STATE_PATH, APX_HOME } from "#core/config/index.js";
|
|
33
|
+
import { callEngine } from "#core/engines/index.js";
|
|
34
|
+
import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
|
|
35
|
+
import { stripThinking } from "../../thinking.js";
|
|
36
|
+
import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "#core/stores/messages.js";
|
|
37
|
+
import { compactChannelIfNeeded } from "#core/memory/index.js";
|
|
38
|
+
import { readAgents } from "#core/apc/parser.js";
|
|
39
|
+
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
40
|
+
import { transcribe as transcribeAudioFile } from "../../transcription.js";
|
|
41
|
+
import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
42
|
+
import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
|
|
43
|
+
import { buildRelationshipBlock } from "#core/agent/index.js";
|
|
44
|
+
import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
|
|
45
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
46
|
+
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
47
|
+
import { isLikelyDuplicate } from "#core/util/text-similarity.js";
|
|
48
|
+
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
49
|
+
import * as askFlow from "./ask.js";
|
|
50
|
+
|
|
51
|
+
// API_BASE re-imported from ./media.js below
|
|
49
52
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
50
53
|
|
|
51
54
|
// Build the channelMeta passed to the super-agent loop. The prompt template at
|
|
@@ -85,159 +88,9 @@ function buildTelegramMeta({ channelName, author, chatId, target, routeToAgent }
|
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
* Send a photo to a Telegram chat.
|
|
92
|
-
* @param {string} token Bot token
|
|
93
|
-
* @param {string|number} chatId Telegram chat_id
|
|
94
|
-
* @param {string|Buffer} photo Absolute file path OR Buffer of image data
|
|
95
|
-
* @param {object} [opts]
|
|
96
|
-
* @param {string} [opts.caption]
|
|
97
|
-
* @param {string} [opts.parse_mode] "HTML" | "Markdown" | "MarkdownV2"
|
|
98
|
-
*/
|
|
99
|
-
export async function sendPhoto(token, chatId, photo, { caption, parse_mode } = {}) {
|
|
100
|
-
const url = `${API_BASE}/bot${token}/sendPhoto`;
|
|
101
|
-
const form = new FormData();
|
|
102
|
-
form.append("chat_id", String(chatId));
|
|
103
|
-
if (caption) form.append("caption", caption);
|
|
104
|
-
if (parse_mode) form.append("parse_mode", parse_mode);
|
|
105
|
-
|
|
106
|
-
if (typeof photo === "string" && photo.startsWith("http")) {
|
|
107
|
-
// Public URL — send as string
|
|
108
|
-
form.append("photo", photo);
|
|
109
|
-
} else {
|
|
110
|
-
// Local file path or Buffer
|
|
111
|
-
const buf = Buffer.isBuffer(photo) ? photo : fs.readFileSync(photo);
|
|
112
|
-
const name = typeof photo === "string" ? path.basename(photo) : "photo.jpg";
|
|
113
|
-
const blob = new Blob([buf], { type: name.endsWith(".png") ? "image/png" : "image/jpeg" });
|
|
114
|
-
form.append("photo", blob, name);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const res = await fetch(url, { method: "POST", body: form });
|
|
118
|
-
const json = await res.json();
|
|
119
|
-
if (!json.ok) throw new Error(`sendPhoto failed: ${json.description || res.status}`);
|
|
120
|
-
return json.result;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Send a voice message (OGG/Opus preferred by Telegram).
|
|
125
|
-
* @param {string} token
|
|
126
|
-
* @param {string|number} chatId
|
|
127
|
-
* @param {string|Buffer} audio Path or Buffer
|
|
128
|
-
* @param {object} [opts]
|
|
129
|
-
* @param {string} [opts.caption]
|
|
130
|
-
* @param {number} [opts.duration]
|
|
131
|
-
*/
|
|
132
|
-
export async function sendVoice(token, chatId, audio, { caption, duration } = {}) {
|
|
133
|
-
const url = `${API_BASE}/bot${token}/sendVoice`;
|
|
134
|
-
const form = new FormData();
|
|
135
|
-
form.append("chat_id", String(chatId));
|
|
136
|
-
if (caption) form.append("caption", caption);
|
|
137
|
-
if (duration) form.append("duration", String(duration));
|
|
138
|
-
|
|
139
|
-
const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
|
|
140
|
-
const name = typeof audio === "string" ? path.basename(audio) : "voice.ogg";
|
|
141
|
-
const blob = new Blob([buf], { type: "audio/ogg" });
|
|
142
|
-
form.append("voice", blob, name);
|
|
143
|
-
|
|
144
|
-
const res = await fetch(url, { method: "POST", body: form });
|
|
145
|
-
const json = await res.json();
|
|
146
|
-
if (!json.ok) throw new Error(`sendVoice failed: ${json.description || res.status}`);
|
|
147
|
-
return json.result;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Send an audio file (MP3, M4A, etc — shown in Telegram music player).
|
|
152
|
-
* @param {string} token
|
|
153
|
-
* @param {string|number} chatId
|
|
154
|
-
* @param {string|Buffer} audio Path or Buffer
|
|
155
|
-
* @param {object} [opts]
|
|
156
|
-
* @param {string} [opts.caption]
|
|
157
|
-
* @param {string} [opts.title]
|
|
158
|
-
* @param {string} [opts.performer]
|
|
159
|
-
*/
|
|
160
|
-
/**
|
|
161
|
-
* Send any file as a Telegram document (PDF, zip, txt, etc).
|
|
162
|
-
* @param {string} token
|
|
163
|
-
* @param {string|number} chatId
|
|
164
|
-
* @param {string|Buffer} document Path or Buffer of document data
|
|
165
|
-
* @param {object} [opts]
|
|
166
|
-
* @param {string} [opts.caption]
|
|
167
|
-
* @param {string} [opts.filename] override filename for Buffer input
|
|
168
|
-
* @param {string} [opts.mime_type]
|
|
169
|
-
*/
|
|
170
|
-
export async function sendDocument(token, chatId, document, { caption, filename, mime_type } = {}) {
|
|
171
|
-
const url = `${API_BASE}/bot${token}/sendDocument`;
|
|
172
|
-
const form = new FormData();
|
|
173
|
-
form.append("chat_id", String(chatId));
|
|
174
|
-
if (caption) form.append("caption", caption);
|
|
175
|
-
|
|
176
|
-
// URL string → let Telegram fetch it
|
|
177
|
-
if (typeof document === "string" && /^https?:\/\//.test(document)) {
|
|
178
|
-
form.append("document", document);
|
|
179
|
-
} else {
|
|
180
|
-
const buf = Buffer.isBuffer(document) ? document : fs.readFileSync(document);
|
|
181
|
-
const name =
|
|
182
|
-
filename ||
|
|
183
|
-
(typeof document === "string" ? path.basename(document) : "document.bin");
|
|
184
|
-
const blob = new Blob([buf], { type: mime_type || "application/octet-stream" });
|
|
185
|
-
form.append("document", blob, name);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const res = await fetch(url, { method: "POST", body: form });
|
|
189
|
-
const json = await res.json();
|
|
190
|
-
if (!json.ok) throw new Error(`sendDocument failed: ${json.description || res.status}`);
|
|
191
|
-
return json.result;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export async function sendAudio(token, chatId, audio, { caption, title, performer } = {}) {
|
|
195
|
-
const url = `${API_BASE}/bot${token}/sendAudio`;
|
|
196
|
-
const form = new FormData();
|
|
197
|
-
form.append("chat_id", String(chatId));
|
|
198
|
-
if (caption) form.append("caption", caption);
|
|
199
|
-
if (title) form.append("title", title);
|
|
200
|
-
if (performer) form.append("performer", performer);
|
|
201
|
-
|
|
202
|
-
const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
|
|
203
|
-
const name = typeof audio === "string" ? path.basename(audio) : "audio.mp3";
|
|
204
|
-
const blob = new Blob([buf], { type: "audio/mpeg" });
|
|
205
|
-
form.append("audio", blob, name);
|
|
206
|
-
|
|
207
|
-
const res = await fetch(url, { method: "POST", body: form });
|
|
208
|
-
const json = await res.json();
|
|
209
|
-
if (!json.ok) throw new Error(`sendAudio failed: ${json.description || res.status}`);
|
|
210
|
-
return json.result;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Audio transcription is delegated to the central dispatcher
|
|
214
|
-
// (../transcription.js) which handles local (faster-whisper via Python) +
|
|
215
|
-
// OpenAI cloud fallback. See that module for config keys.
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Download a file from Telegram servers.
|
|
219
|
-
* Returns the local file path where it was saved.
|
|
220
|
-
*/
|
|
221
|
-
async function downloadTelegramFile(token, fileId, destDir) {
|
|
222
|
-
// Step 1: get file path from Telegram
|
|
223
|
-
const infoRes = await fetch(`${API_BASE}/bot${token}/getFile?file_id=${fileId}`);
|
|
224
|
-
const infoJson = await infoRes.json();
|
|
225
|
-
if (!infoJson.ok) throw new Error(`getFile failed: ${infoJson.description}`);
|
|
226
|
-
const filePath = infoJson.result.file_path; // e.g. "photos/file_123.jpg"
|
|
227
|
-
const ext = path.extname(filePath) || ".jpg";
|
|
228
|
-
const fileName = `tg_${fileId.slice(-8)}_${Date.now()}${ext}`;
|
|
229
|
-
const localPath = path.join(destDir, fileName);
|
|
230
|
-
|
|
231
|
-
// Step 2: download
|
|
232
|
-
const dlRes = await fetch(`${API_BASE}/file/bot${token}/${filePath}`);
|
|
233
|
-
if (!dlRes.ok) throw new Error(`download failed: ${dlRes.status}`);
|
|
234
|
-
const buf = Buffer.from(await dlRes.arrayBuffer());
|
|
235
|
-
fs.writeFileSync(localPath, buf);
|
|
236
|
-
return localPath;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ---------- shared state ----------------------------------------------------
|
|
240
|
-
|
|
91
|
+
// Media sending helpers moved to ./media.js.
|
|
92
|
+
import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "./media.js";
|
|
93
|
+
export { sendPhoto, sendVoice, sendDocument, sendAudio };
|
|
241
94
|
function loadState() {
|
|
242
95
|
if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
|
|
243
96
|
try {
|
|
@@ -485,7 +338,7 @@ class ChannelPoller {
|
|
|
485
338
|
const localPath = await downloadTelegramFile(token, bestPhoto.file_id, mediaDir);
|
|
486
339
|
this.log(`telegram[${this.channel.name}] photo saved: ${localPath}`);
|
|
487
340
|
appendGlobalMessage({
|
|
488
|
-
channel:
|
|
341
|
+
channel: CHANNELS.TELEGRAM,
|
|
489
342
|
direction: "in",
|
|
490
343
|
type: "photo",
|
|
491
344
|
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
@@ -551,7 +404,7 @@ class ChannelPoller {
|
|
|
551
404
|
: `[audio] (transcription unavailable${transcribeError ? ": " + transcribeError : ""})`;
|
|
552
405
|
|
|
553
406
|
appendGlobalMessage({
|
|
554
|
-
channel:
|
|
407
|
+
channel: CHANNELS.TELEGRAM,
|
|
555
408
|
direction: "in",
|
|
556
409
|
type: "audio",
|
|
557
410
|
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
@@ -584,7 +437,7 @@ class ChannelPoller {
|
|
|
584
437
|
if (chat_id && text && await this._maybeConsumeAskTextAnswer({ chat_id, text })) {
|
|
585
438
|
// Still log the inbound so the chat history records what the user said.
|
|
586
439
|
appendGlobalMessage({
|
|
587
|
-
channel:
|
|
440
|
+
channel: CHANNELS.TELEGRAM,
|
|
588
441
|
direction: "in",
|
|
589
442
|
type: "user",
|
|
590
443
|
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
@@ -623,7 +476,7 @@ class ChannelPoller {
|
|
|
623
476
|
// the next turn reads a [RESUMEN COMPACTADO] instead of raw history. Never
|
|
624
477
|
// awaited — adds zero latency to this reply, degrades gracefully.
|
|
625
478
|
compactChannelIfNeeded({
|
|
626
|
-
channel:
|
|
479
|
+
channel: CHANNELS.TELEGRAM,
|
|
627
480
|
chat_id,
|
|
628
481
|
config: this.globalConfig,
|
|
629
482
|
log: this.log,
|
|
@@ -647,7 +500,7 @@ class ChannelPoller {
|
|
|
647
500
|
|
|
648
501
|
// Always log inbound to global store (~/.apx/messages/telegram/)
|
|
649
502
|
appendGlobalMessage({
|
|
650
|
-
channel:
|
|
503
|
+
channel: CHANNELS.TELEGRAM,
|
|
651
504
|
direction: "in",
|
|
652
505
|
type: "user",
|
|
653
506
|
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
@@ -679,7 +532,7 @@ class ChannelPoller {
|
|
|
679
532
|
const ack = "Done, context cleared. Starting fresh. What do you need?";
|
|
680
533
|
await this._send({ chat_id, text: ack });
|
|
681
534
|
appendGlobalMessage({
|
|
682
|
-
channel:
|
|
535
|
+
channel: CHANNELS.TELEGRAM,
|
|
683
536
|
direction: "out",
|
|
684
537
|
type: "agent",
|
|
685
538
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -703,7 +556,7 @@ class ChannelPoller {
|
|
|
703
556
|
let replyActorId; // stable id: super_agent | agent slug
|
|
704
557
|
let replyKind; // actor_kind: superagent | agent
|
|
705
558
|
const projectCfg = target.config || this.globalConfig;
|
|
706
|
-
// Display name for the super-agent persona on this channel (
|
|
559
|
+
// Display name for the super-agent persona on this channel (from identity.json).
|
|
707
560
|
const agentDisplay = resolveAgentName(this.globalConfig);
|
|
708
561
|
|
|
709
562
|
// Try the project's chosen agent first (skipped if the legacy
|
|
@@ -777,7 +630,7 @@ class ChannelPoller {
|
|
|
777
630
|
const heads = headsUpPhrase();
|
|
778
631
|
await this._send({ chat_id, text: heads });
|
|
779
632
|
appendGlobalMessage({
|
|
780
|
-
channel:
|
|
633
|
+
channel: CHANNELS.TELEGRAM,
|
|
781
634
|
direction: "out",
|
|
782
635
|
type: "agent",
|
|
783
636
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -792,11 +645,17 @@ class ChannelPoller {
|
|
|
792
645
|
if (ev.type === "assistant_text" && ev.text) {
|
|
793
646
|
const piece = stripThinking(ev.text).trim();
|
|
794
647
|
if (!piece) return;
|
|
648
|
+
// Skip post-tool segments that just restate the pre-tool intro —
|
|
649
|
+
// weaker models (gemini-flash et al.) frequently paraphrase the
|
|
650
|
+
// same content twice within a single turn.
|
|
651
|
+
if (lastStreamedText && isLikelyDuplicate(piece, lastStreamedText)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
795
654
|
await this._send({ chat_id, text: piece });
|
|
796
655
|
lastStreamedText = piece;
|
|
797
656
|
streamedCount += 1;
|
|
798
657
|
appendGlobalMessage({
|
|
799
|
-
channel:
|
|
658
|
+
channel: CHANNELS.TELEGRAM,
|
|
800
659
|
direction: "out",
|
|
801
660
|
type: "agent",
|
|
802
661
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -816,7 +675,7 @@ class ChannelPoller {
|
|
|
816
675
|
// Logged for the audit trail / other channels — NOT sent to Telegram.
|
|
817
676
|
const t = ev.trace;
|
|
818
677
|
appendGlobalMessage({
|
|
819
|
-
channel:
|
|
678
|
+
channel: CHANNELS.TELEGRAM,
|
|
820
679
|
direction: "out",
|
|
821
680
|
type: "tool",
|
|
822
681
|
actor_id: t.tool,
|
|
@@ -846,17 +705,24 @@ class ChannelPoller {
|
|
|
846
705
|
pendingStore: getConfirmStore(),
|
|
847
706
|
});
|
|
848
707
|
|
|
708
|
+
// `/slug ...` shortcut: load the matching skill body into contextNote
|
|
709
|
+
// and strip the prefix from the user prompt before sending to the loop.
|
|
710
|
+
const slashed = tryResolveSkillCommand(text, { projectPath: target?.path });
|
|
711
|
+
const slashedPrompt = slashed.handled ? slashed.prompt : text;
|
|
712
|
+
const slashedContextNote = slashed.handled ? slashed.contextNote : "";
|
|
713
|
+
|
|
849
714
|
try {
|
|
850
715
|
const sa = await runSuperAgent({
|
|
851
716
|
globalConfig: this.globalConfig,
|
|
852
717
|
projects: this.projects,
|
|
853
718
|
plugins: this.plugins,
|
|
854
719
|
registries: this.registries,
|
|
855
|
-
prompt:
|
|
720
|
+
prompt: slashedPrompt,
|
|
856
721
|
previousMessages,
|
|
857
|
-
channel:
|
|
722
|
+
channel: CHANNELS.TELEGRAM,
|
|
858
723
|
relationshipBlock,
|
|
859
724
|
allowedTools,
|
|
725
|
+
contextNote: slashedContextNote || undefined,
|
|
860
726
|
channelMeta: buildTelegramMeta({
|
|
861
727
|
channelName: this.channel.name,
|
|
862
728
|
author,
|
|
@@ -932,8 +798,13 @@ class ChannelPoller {
|
|
|
932
798
|
// turn isn't silently empty.
|
|
933
799
|
const finalClean = replyText ? stripThinking(replyText).trim() : "";
|
|
934
800
|
let toSend = "";
|
|
935
|
-
|
|
936
|
-
|
|
801
|
+
// Fuzzy dedupe against the last streamed segment: catches paraphrases
|
|
802
|
+
// (Jaccard ≥ 0.4 or short-mostly-inside-long), not just exact matches.
|
|
803
|
+
if (finalClean && !isLikelyDuplicate(finalClean, lastStreamedText) && finalClean !== lastStreamedText) {
|
|
804
|
+
toSend = finalClean;
|
|
805
|
+
} else if (!finalClean && streamedCount === 0) {
|
|
806
|
+
toSend = "Listo.";
|
|
807
|
+
}
|
|
937
808
|
|
|
938
809
|
stopTyping();
|
|
939
810
|
if (!toSend) return; // everything was already streamed — nothing left to send
|
|
@@ -949,7 +820,7 @@ class ChannelPoller {
|
|
|
949
820
|
if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
|
|
950
821
|
if (saUsage) meta.usage = saUsage;
|
|
951
822
|
appendGlobalMessage({
|
|
952
|
-
channel:
|
|
823
|
+
channel: CHANNELS.TELEGRAM,
|
|
953
824
|
direction: "out",
|
|
954
825
|
type: "agent",
|
|
955
826
|
actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
@@ -962,7 +833,7 @@ class ChannelPoller {
|
|
|
962
833
|
} catch (e) {
|
|
963
834
|
this.log(`telegram[${this.channel.name}] send-back error: ${e.message}`);
|
|
964
835
|
appendGlobalMessage({
|
|
965
|
-
channel:
|
|
836
|
+
channel: CHANNELS.TELEGRAM,
|
|
966
837
|
direction: "out",
|
|
967
838
|
type: "agent",
|
|
968
839
|
actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
@@ -1138,7 +1009,7 @@ class ChannelPoller {
|
|
|
1138
1009
|
// it up on the NEXT inbound. Mirrors how a normal text reply would be
|
|
1139
1010
|
// recorded.
|
|
1140
1011
|
appendGlobalMessage({
|
|
1141
|
-
channel:
|
|
1012
|
+
channel: CHANNELS.TELEGRAM,
|
|
1142
1013
|
direction: "in",
|
|
1143
1014
|
type: "user",
|
|
1144
1015
|
actor_id: authorId ? String(authorId) : (author || "ask_flow"),
|
|
@@ -1168,10 +1039,10 @@ class ChannelPoller {
|
|
|
1168
1039
|
registries: this.registries,
|
|
1169
1040
|
prompt: compiled,
|
|
1170
1041
|
previousMessages,
|
|
1171
|
-
channel:
|
|
1042
|
+
channel: CHANNELS.TELEGRAM,
|
|
1172
1043
|
relationshipBlock,
|
|
1173
1044
|
allowedTools,
|
|
1174
|
-
channelMeta: { channel:
|
|
1045
|
+
channelMeta: { channel: CHANNELS.TELEGRAM, chat_id, author, route_to_agent: this.channel.route_to_agent },
|
|
1175
1046
|
});
|
|
1176
1047
|
stopTyping();
|
|
1177
1048
|
|
|
@@ -1198,7 +1069,7 @@ class ChannelPoller {
|
|
|
1198
1069
|
if (replyText) {
|
|
1199
1070
|
await this._send({ chat_id, text: replyText });
|
|
1200
1071
|
appendGlobalMessage({
|
|
1201
|
-
channel:
|
|
1072
|
+
channel: CHANNELS.TELEGRAM,
|
|
1202
1073
|
direction: "out",
|
|
1203
1074
|
type: "agent",
|
|
1204
1075
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -1401,7 +1272,7 @@ export default {
|
|
|
1401
1272
|
if (!p) throw new Error("no telegram channel available");
|
|
1402
1273
|
const result = await p._send({ chat_id, text });
|
|
1403
1274
|
appendGlobalMessage({
|
|
1404
|
-
channel:
|
|
1275
|
+
channel: CHANNELS.TELEGRAM,
|
|
1405
1276
|
direction: "out",
|
|
1406
1277
|
type: "agent",
|
|
1407
1278
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -1431,7 +1302,7 @@ export default {
|
|
|
1431
1302
|
if (!p) throw new Error("no telegram channel available");
|
|
1432
1303
|
const result = await p._sendPhoto({ chat_id, photo, caption, parse_mode });
|
|
1433
1304
|
appendGlobalMessage({
|
|
1434
|
-
channel:
|
|
1305
|
+
channel: CHANNELS.TELEGRAM,
|
|
1435
1306
|
direction: "out",
|
|
1436
1307
|
type: "photo",
|
|
1437
1308
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -1455,7 +1326,7 @@ export default {
|
|
|
1455
1326
|
if (!p) throw new Error("no telegram channel available");
|
|
1456
1327
|
const result = await p._sendVoice({ chat_id, audio, caption, duration });
|
|
1457
1328
|
appendGlobalMessage({
|
|
1458
|
-
channel:
|
|
1329
|
+
channel: CHANNELS.TELEGRAM,
|
|
1459
1330
|
direction: "out",
|
|
1460
1331
|
type: "voice",
|
|
1461
1332
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -1479,7 +1350,7 @@ export default {
|
|
|
1479
1350
|
if (!p) throw new Error("no telegram channel available");
|
|
1480
1351
|
const result = await p._sendDocument({ chat_id, document, caption, filename, mime_type });
|
|
1481
1352
|
appendGlobalMessage({
|
|
1482
|
-
channel:
|
|
1353
|
+
channel: CHANNELS.TELEGRAM,
|
|
1483
1354
|
direction: "out",
|
|
1484
1355
|
type: "document",
|
|
1485
1356
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -1503,7 +1374,7 @@ export default {
|
|
|
1503
1374
|
if (!p) throw new Error("no telegram channel available");
|
|
1504
1375
|
const result = await p._sendAudio({ chat_id, audio, caption, title, performer });
|
|
1505
1376
|
appendGlobalMessage({
|
|
1506
|
-
channel:
|
|
1377
|
+
channel: CHANNELS.TELEGRAM,
|
|
1507
1378
|
direction: "out",
|
|
1508
1379
|
type: "audio",
|
|
1509
1380
|
actor_id: SUPERAGENT_ACTOR_ID,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Telegram media helpers: send photo/voice/document/audio + download a remote
|
|
2
|
+
// file. Auto-extracted from plugins/telegram/index.js — these used to live
|
|
3
|
+
// inline next to the poll loop and the super-agent dispatch.
|
|
4
|
+
//
|
|
5
|
+
// Each helper takes the bot token and chat id explicitly so they can be used
|
|
6
|
+
// from any code path (tests, other plugins, future agents). Buffer or
|
|
7
|
+
// absolute path input is accepted for media; for URLs the helpers pass them
|
|
8
|
+
// through to Telegram and let the API fetch them.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export const API_BASE = "https://api.telegram.org";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send a photo to a Telegram chat.
|
|
16
|
+
* @param {string} token Bot token
|
|
17
|
+
* @param {string|number} chatId Telegram chat_id
|
|
18
|
+
* @param {string|Buffer} photo Absolute file path OR Buffer of image data
|
|
19
|
+
* @param {object} [opts]
|
|
20
|
+
* @param {string} [opts.caption]
|
|
21
|
+
* @param {string} [opts.parse_mode] "HTML" | "Markdown" | "MarkdownV2"
|
|
22
|
+
*/
|
|
23
|
+
export async function sendPhoto(token, chatId, photo, { caption, parse_mode } = {}) {
|
|
24
|
+
const url = `${API_BASE}/bot${token}/sendPhoto`;
|
|
25
|
+
const form = new FormData();
|
|
26
|
+
form.append("chat_id", String(chatId));
|
|
27
|
+
if (caption) form.append("caption", caption);
|
|
28
|
+
if (parse_mode) form.append("parse_mode", parse_mode);
|
|
29
|
+
|
|
30
|
+
if (typeof photo === "string" && photo.startsWith("http")) {
|
|
31
|
+
// Public URL — send as string
|
|
32
|
+
form.append("photo", photo);
|
|
33
|
+
} else {
|
|
34
|
+
// Local file path or Buffer
|
|
35
|
+
const buf = Buffer.isBuffer(photo) ? photo : fs.readFileSync(photo);
|
|
36
|
+
const name = typeof photo === "string" ? path.basename(photo) : "photo.jpg";
|
|
37
|
+
const blob = new Blob([buf], { type: name.endsWith(".png") ? "image/png" : "image/jpeg" });
|
|
38
|
+
form.append("photo", blob, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const res = await fetch(url, { method: "POST", body: form });
|
|
42
|
+
const json = await res.json();
|
|
43
|
+
if (!json.ok) throw new Error(`sendPhoto failed: ${json.description || res.status}`);
|
|
44
|
+
return json.result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Send a voice message (OGG/Opus preferred by Telegram).
|
|
49
|
+
* @param {string} token
|
|
50
|
+
* @param {string|number} chatId
|
|
51
|
+
* @param {string|Buffer} audio Path or Buffer
|
|
52
|
+
* @param {object} [opts]
|
|
53
|
+
* @param {string} [opts.caption]
|
|
54
|
+
* @param {number} [opts.duration]
|
|
55
|
+
*/
|
|
56
|
+
export async function sendVoice(token, chatId, audio, { caption, duration } = {}) {
|
|
57
|
+
const url = `${API_BASE}/bot${token}/sendVoice`;
|
|
58
|
+
const form = new FormData();
|
|
59
|
+
form.append("chat_id", String(chatId));
|
|
60
|
+
if (caption) form.append("caption", caption);
|
|
61
|
+
if (duration) form.append("duration", String(duration));
|
|
62
|
+
|
|
63
|
+
const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
|
|
64
|
+
const name = typeof audio === "string" ? path.basename(audio) : "voice.ogg";
|
|
65
|
+
const blob = new Blob([buf], { type: "audio/ogg" });
|
|
66
|
+
form.append("voice", blob, name);
|
|
67
|
+
|
|
68
|
+
const res = await fetch(url, { method: "POST", body: form });
|
|
69
|
+
const json = await res.json();
|
|
70
|
+
if (!json.ok) throw new Error(`sendVoice failed: ${json.description || res.status}`);
|
|
71
|
+
return json.result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Send an audio file (MP3, M4A, etc — shown in Telegram music player).
|
|
76
|
+
* @param {string} token
|
|
77
|
+
* @param {string|number} chatId
|
|
78
|
+
* @param {string|Buffer} audio Path or Buffer
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {string} [opts.caption]
|
|
81
|
+
* @param {string} [opts.title]
|
|
82
|
+
* @param {string} [opts.performer]
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Send any file as a Telegram document (PDF, zip, txt, etc).
|
|
86
|
+
* @param {string} token
|
|
87
|
+
* @param {string|number} chatId
|
|
88
|
+
* @param {string|Buffer} document Path or Buffer of document data
|
|
89
|
+
* @param {object} [opts]
|
|
90
|
+
* @param {string} [opts.caption]
|
|
91
|
+
* @param {string} [opts.filename] override filename for Buffer input
|
|
92
|
+
* @param {string} [opts.mime_type]
|
|
93
|
+
*/
|
|
94
|
+
export async function sendDocument(token, chatId, document, { caption, filename, mime_type } = {}) {
|
|
95
|
+
const url = `${API_BASE}/bot${token}/sendDocument`;
|
|
96
|
+
const form = new FormData();
|
|
97
|
+
form.append("chat_id", String(chatId));
|
|
98
|
+
if (caption) form.append("caption", caption);
|
|
99
|
+
|
|
100
|
+
// URL string → let Telegram fetch it
|
|
101
|
+
if (typeof document === "string" && /^https?:\/\//.test(document)) {
|
|
102
|
+
form.append("document", document);
|
|
103
|
+
} else {
|
|
104
|
+
const buf = Buffer.isBuffer(document) ? document : fs.readFileSync(document);
|
|
105
|
+
const name =
|
|
106
|
+
filename ||
|
|
107
|
+
(typeof document === "string" ? path.basename(document) : "document.bin");
|
|
108
|
+
const blob = new Blob([buf], { type: mime_type || "application/octet-stream" });
|
|
109
|
+
form.append("document", blob, name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const res = await fetch(url, { method: "POST", body: form });
|
|
113
|
+
const json = await res.json();
|
|
114
|
+
if (!json.ok) throw new Error(`sendDocument failed: ${json.description || res.status}`);
|
|
115
|
+
return json.result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function sendAudio(token, chatId, audio, { caption, title, performer } = {}) {
|
|
119
|
+
const url = `${API_BASE}/bot${token}/sendAudio`;
|
|
120
|
+
const form = new FormData();
|
|
121
|
+
form.append("chat_id", String(chatId));
|
|
122
|
+
if (caption) form.append("caption", caption);
|
|
123
|
+
if (title) form.append("title", title);
|
|
124
|
+
if (performer) form.append("performer", performer);
|
|
125
|
+
|
|
126
|
+
const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
|
|
127
|
+
const name = typeof audio === "string" ? path.basename(audio) : "audio.mp3";
|
|
128
|
+
const blob = new Blob([buf], { type: "audio/mpeg" });
|
|
129
|
+
form.append("audio", blob, name);
|
|
130
|
+
|
|
131
|
+
const res = await fetch(url, { method: "POST", body: form });
|
|
132
|
+
const json = await res.json();
|
|
133
|
+
if (!json.ok) throw new Error(`sendAudio failed: ${json.description || res.status}`);
|
|
134
|
+
return json.result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Audio transcription is delegated to the central dispatcher
|
|
138
|
+
// (../transcription.js) which handles local (faster-whisper via Python) +
|
|
139
|
+
// OpenAI cloud fallback. See that module for config keys.
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Download a file from Telegram servers.
|
|
143
|
+
* Returns the local file path where it was saved.
|
|
144
|
+
*/
|
|
145
|
+
export async function downloadTelegramFile(token, fileId, destDir) {
|
|
146
|
+
// Step 1: get file path from Telegram
|
|
147
|
+
const infoRes = await fetch(`${API_BASE}/bot${token}/getFile?file_id=${fileId}`);
|
|
148
|
+
const infoJson = await infoRes.json();
|
|
149
|
+
if (!infoJson.ok) throw new Error(`getFile failed: ${infoJson.description}`);
|
|
150
|
+
const filePath = infoJson.result.file_path; // e.g. "photos/file_123.jpg"
|
|
151
|
+
const ext = path.extname(filePath) || ".jpg";
|
|
152
|
+
const fileName = `tg_${fileId.slice(-8)}_${Date.now()}${ext}`;
|
|
153
|
+
const localPath = path.join(destDir, fileName);
|
|
154
|
+
|
|
155
|
+
// Step 2: download
|
|
156
|
+
const dlRes = await fetch(`${API_BASE}/file/bot${token}/${filePath}`);
|
|
157
|
+
if (!dlRes.ok) throw new Error(`download failed: ${dlRes.status}`);
|
|
158
|
+
const buf = Buffer.from(await dlRes.arrayBuffer());
|
|
159
|
+
fs.writeFileSync(localPath, buf);
|
|
160
|
+
return localPath;
|
|
161
|
+
}
|
|
162
|
+
|