agent-sin 0.1.12 → 0.1.15
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/CHANGELOG.md +66 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +568 -14
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -6,6 +6,7 @@ export interface SkillOutputDefinition {
|
|
|
6
6
|
path: string;
|
|
7
7
|
filename: string;
|
|
8
8
|
append?: boolean;
|
|
9
|
+
merge_mode?: "overwrite" | "append" | "update_or_append";
|
|
9
10
|
show_saved?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export type DiscordSlashOptionType = "string" | "integer" | "number" | "boolean";
|
|
@@ -53,6 +54,10 @@ export interface SkillManifest {
|
|
|
53
54
|
read?: boolean;
|
|
54
55
|
write?: boolean;
|
|
55
56
|
};
|
|
57
|
+
history?: {
|
|
58
|
+
read?: boolean;
|
|
59
|
+
write?: boolean;
|
|
60
|
+
};
|
|
56
61
|
retry?: {
|
|
57
62
|
max_attempts?: number;
|
|
58
63
|
delay_ms?: number;
|
|
@@ -67,11 +67,22 @@ export async function loadSkillManifest(skillDir) {
|
|
|
67
67
|
enabled: parsed.enabled ?? true,
|
|
68
68
|
output_mode: parsed.output_mode,
|
|
69
69
|
side_effect: parsed.side_effect === true,
|
|
70
|
+
history: normalizeHistoryConfig(parsed.history),
|
|
70
71
|
dir: skillDir,
|
|
71
72
|
source,
|
|
72
73
|
override,
|
|
73
74
|
};
|
|
74
75
|
}
|
|
76
|
+
function normalizeHistoryConfig(value) {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
78
|
+
return undefined;
|
|
79
|
+
const raw = value;
|
|
80
|
+
const read = raw.read === true;
|
|
81
|
+
const write = raw.write === true;
|
|
82
|
+
if (!read && !write)
|
|
83
|
+
return undefined;
|
|
84
|
+
return { read, write };
|
|
85
|
+
}
|
|
75
86
|
export async function resolveSkillEntryPath(manifest) {
|
|
76
87
|
assertSafeRelativeSkillPath(manifest.entry, "entry");
|
|
77
88
|
const logicalDir = path.resolve(manifest.dir);
|
package/dist/discord/bot.d.ts
CHANGED
package/dist/discord/bot.js
CHANGED
|
@@ -3,10 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { loadConfig, loadModels } from "../core/config.js";
|
|
5
5
|
import { appendEventLog } from "../core/logger.js";
|
|
6
|
-
import { createIntentRuntime, renderBuildFooter, shouldShowBuildFooter, } from "../builder/build-flow.js";
|
|
6
|
+
import { createIntentRuntime, enterEditModeForSkill, parseSlashBuildDirect, renderBuildFooter, shouldShowBuildFooter, } from "../builder/build-flow.js";
|
|
7
7
|
import { routeConversationMessage, } from "../builder/conversation-router.js";
|
|
8
8
|
import { formatBuildProgress, progressIntervalMs } from "../builder/progress-format.js";
|
|
9
9
|
import { chunkText, cleanAttachmentText, formatAttachmentLabel, formatBytes, guessImageMimeType as guessImageMimeTypeFromName, indentAttachmentContent, isImageLikeFile, isTextLikeFile, } from "../core/message-utils.js";
|
|
10
|
+
import { collectLocalFileAttachments, collectLocalImageAttachments, } from "../core/image-attachments.js";
|
|
10
11
|
import { isEmptyIntentRuntime, loadIntentRuntimeMap, saveIntentRuntimeMap, } from "../builder/intent-runtime-store.js";
|
|
11
12
|
import { inferLocaleFromText, l, lLines, withLocale } from "../core/i18n.js";
|
|
12
13
|
import { modelsLines, skillsLines } from "../core/info-lines.js";
|
|
@@ -106,6 +107,7 @@ export async function runDiscordBot(config) {
|
|
|
106
107
|
historiesLoaded: new Set(),
|
|
107
108
|
intentRuntimes: persistedIntentRuntimes,
|
|
108
109
|
intentRuntimesFile,
|
|
110
|
+
buildPickPending: new Map(),
|
|
109
111
|
ws: null,
|
|
110
112
|
seq: null,
|
|
111
113
|
sessionId: null,
|
|
@@ -173,11 +175,12 @@ export function parseSnowflakeList(raw) {
|
|
|
173
175
|
export const parseAllowedUserIds = parseSnowflakeList;
|
|
174
176
|
export function classifyMessage(message, botUserId, allowedUserIds, listenChannelIds = new Set(), botThreadIds = new Set()) {
|
|
175
177
|
const isDirect = !message.guild_id;
|
|
176
|
-
const
|
|
178
|
+
const isListenChannel = listenChannelIds.has(message.channel_id);
|
|
179
|
+
const hasBotUserMention = botUserId
|
|
177
180
|
? Array.isArray(message.mentions) && message.mentions.some((user) => user.id === botUserId)
|
|
178
181
|
: false;
|
|
182
|
+
const isMentioned = hasBotUserMention || (isListenChannel && hasLeadingRoleMention(message));
|
|
179
183
|
const isAllowed = allowedUserIds.has(message.author.id);
|
|
180
|
-
const isListenChannel = listenChannelIds.has(message.channel_id);
|
|
181
184
|
const isBotThread = botThreadIds.has(message.channel_id);
|
|
182
185
|
return { isDirect, isMentioned, isAllowed, isListenChannel, isBotThread };
|
|
183
186
|
}
|
|
@@ -186,7 +189,13 @@ export function stripBotMention(content, botUserId) {
|
|
|
186
189
|
return content.trim();
|
|
187
190
|
}
|
|
188
191
|
const pattern = new RegExp(`<@!?${botUserId}>`, "g");
|
|
189
|
-
return content.replace(pattern, "").trim();
|
|
192
|
+
return stripLeadingRoleMentions(content.replace(pattern, "")).trim();
|
|
193
|
+
}
|
|
194
|
+
function hasLeadingRoleMention(message) {
|
|
195
|
+
return /^\s*(?:<@&\d+>\s*)+/u.test(message.content || "");
|
|
196
|
+
}
|
|
197
|
+
function stripLeadingRoleMentions(content) {
|
|
198
|
+
return content.replace(/^\s*(?:<@&\d+>\s*)+/u, "");
|
|
190
199
|
}
|
|
191
200
|
const BADGE_PREFIX_PATTERN = /^(💬|🔧|✏️|✏)/u;
|
|
192
201
|
const BUILD_FOOTER_FIRST_LINE = /^(?:(?:✏️|✏|🔧)\s*現在ビルドモードです|((?:現在:「[^」]*」の)?ビルドモード(?:です。抜けるには|を抜けるには).+?と返事してください))\s*$/u;
|
|
@@ -453,7 +462,7 @@ async function persistDiscordAttachmentBuffer(state, attachment, buffer, mimeTyp
|
|
|
453
462
|
const HH = String(now.getHours()).padStart(2, "0");
|
|
454
463
|
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
455
464
|
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
456
|
-
const dir = path.join(state.config.
|
|
465
|
+
const dir = path.join(state.config.memory_dir, "attachments", yyyy, MM);
|
|
457
466
|
await mkdir(dir, { recursive: true });
|
|
458
467
|
const ext = pickAttachmentExtension(attachment.filename || attachment.title, mimeType);
|
|
459
468
|
const random = Math.random().toString(36).slice(2, 8);
|
|
@@ -726,9 +735,11 @@ async function handleMessage(state, message) {
|
|
|
726
735
|
is_listen_channel: ctx.isListenChannel,
|
|
727
736
|
is_bot_thread: ctx.isBotThread,
|
|
728
737
|
mention_count: Array.isArray(message.mentions) ? message.mentions.length : 0,
|
|
738
|
+
role_mention_count: Array.isArray(message.mention_roles) ? message.mention_roles.length : 0,
|
|
729
739
|
mention_user_ids: Array.isArray(message.mentions)
|
|
730
740
|
? message.mentions.map((user) => user.id).slice(0, 5)
|
|
731
741
|
: [],
|
|
742
|
+
mention_role_ids: Array.isArray(message.mention_roles) ? message.mention_roles.slice(0, 5) : [],
|
|
732
743
|
bot_user_id: state.botUserId,
|
|
733
744
|
},
|
|
734
745
|
});
|
|
@@ -745,6 +756,7 @@ async function handleMessage(state, message) {
|
|
|
745
756
|
if (cleanText === "!reset") {
|
|
746
757
|
state.histories.delete(message.channel_id);
|
|
747
758
|
state.historiesLoaded.delete(message.channel_id);
|
|
759
|
+
state.buildPickPending.delete(message.channel_id);
|
|
748
760
|
await sendChannelMessage(state, message.channel_id, l("Chat history reset.", "会話履歴をリセットしました。"));
|
|
749
761
|
return;
|
|
750
762
|
}
|
|
@@ -753,6 +765,25 @@ async function handleMessage(state, message) {
|
|
|
753
765
|
await sendChannelMessage(state, message.channel_id, lines.join("\n"));
|
|
754
766
|
return;
|
|
755
767
|
}
|
|
768
|
+
if (cleanText === "/build") {
|
|
769
|
+
const lines = await startBuildPickerForChannel(state, message.channel_id);
|
|
770
|
+
await sendChannelMessage(state, message.channel_id, lines.join("\n"));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const buildSlashSkillId = parseSlashBuildDirect(cleanText);
|
|
774
|
+
if (buildSlashSkillId) {
|
|
775
|
+
const lines = await enterBuildModeFromSlashForChannel(state, message.channel_id, buildSlashSkillId);
|
|
776
|
+
if (lines) {
|
|
777
|
+
await sendChannelMessage(state, message.channel_id, lines.join("\n"));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const pendingPick = state.buildPickPending.get(message.channel_id);
|
|
782
|
+
if (pendingPick && /^\d+$/.test(cleanText.trim())) {
|
|
783
|
+
const lines = await resolveBuildPickSelection(state, message.channel_id, pendingPick, cleanText.trim());
|
|
784
|
+
await sendChannelMessage(state, message.channel_id, lines.join("\n"));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
756
787
|
if (cleanText === "/models") {
|
|
757
788
|
const lines = await modelsLines(state.config);
|
|
758
789
|
await sendChannelMessage(state, message.channel_id, lines.join("\n"));
|
|
@@ -838,20 +869,20 @@ async function handleMessage(state, message) {
|
|
|
838
869
|
const typing = startTypingKeepalive(state, replyChannelId);
|
|
839
870
|
const prevMode = intentRuntime.mode;
|
|
840
871
|
try {
|
|
841
|
-
const
|
|
872
|
+
const routed = await routeDiscordMessage(state, userText, history, intentRuntime, status, replyChannelId, userMessage.images);
|
|
842
873
|
typing.stop();
|
|
843
874
|
void saveIntentRuntimes(state);
|
|
844
875
|
const isBuildEntry = prevMode !== "build" && intentRuntime.mode === "build";
|
|
845
|
-
const decorated = withModeBadge(intentRuntime, lines, { userText, isBuildEntry });
|
|
876
|
+
const decorated = withModeBadge(intentRuntime, routed.lines, { userText, isBuildEntry });
|
|
846
877
|
scheduleUpdateCheck(state.config.workspace);
|
|
847
878
|
const banner = await consumeUpdateBanner(state.config.workspace);
|
|
848
879
|
const finalLines = banner ? [banner, "", ...decorated] : decorated;
|
|
849
880
|
const reply = finalLines.filter((line) => line !== undefined && line !== null).join("\n").trim();
|
|
850
881
|
if (reply) {
|
|
851
|
-
await
|
|
882
|
+
await sendChannelMessageWithLocalAttachments(state, replyChannelId, reply, routed.localAttachmentPaths);
|
|
852
883
|
}
|
|
853
884
|
else {
|
|
854
|
-
await
|
|
885
|
+
await sendChannelMessageWithLocalAttachments(state, replyChannelId, l("(no response)", "(応答なし)"), routed.localAttachmentPaths);
|
|
855
886
|
}
|
|
856
887
|
}
|
|
857
888
|
catch (error) {
|
|
@@ -900,6 +931,7 @@ async function refreshDiscordHistoryBeforeCurrentMessage(state, channelId, befor
|
|
|
900
931
|
}
|
|
901
932
|
async function routeDiscordMessage(state, text, history, intentRuntime, status, replyChannelId, images = []) {
|
|
902
933
|
let modelFailed = false;
|
|
934
|
+
const localAttachmentPaths = [];
|
|
903
935
|
const lines = await routeConversationMessage({
|
|
904
936
|
config: state.config,
|
|
905
937
|
text,
|
|
@@ -916,9 +948,12 @@ async function routeDiscordMessage(state, text, history, intentRuntime, status,
|
|
|
916
948
|
modelFailed = true;
|
|
917
949
|
}
|
|
918
950
|
},
|
|
951
|
+
onLocalAttachments: (paths) => {
|
|
952
|
+
localAttachmentPaths.push(...paths);
|
|
953
|
+
},
|
|
919
954
|
});
|
|
920
955
|
await status.set(modelFailed ? "error" : "done");
|
|
921
|
-
return lines;
|
|
956
|
+
return { lines, localAttachmentPaths };
|
|
922
957
|
}
|
|
923
958
|
async function onChatProgressForReactions(event, status) {
|
|
924
959
|
switch (event.kind) {
|
|
@@ -2431,6 +2466,72 @@ async function editInteractionOriginal(state, interaction, content) {
|
|
|
2431
2466
|
}
|
|
2432
2467
|
}
|
|
2433
2468
|
}
|
|
2469
|
+
async function enterBuildModeFromSlashForChannel(state, channelId, skillId) {
|
|
2470
|
+
if (!skillId)
|
|
2471
|
+
return null;
|
|
2472
|
+
const skills = await listSkillManifests(state.config.skills_dir);
|
|
2473
|
+
const userSkills = skills.filter((skill) => skill.source !== "builtin");
|
|
2474
|
+
const exists = userSkills.some((skill) => skill.id === skillId);
|
|
2475
|
+
if (!exists) {
|
|
2476
|
+
const head = l(`"${skillId}" is not a registered user skill. Pick one below:`, `「${skillId}」は登録済みのユーザースキルではありません。下から番号で選んでください:`);
|
|
2477
|
+
const picker = await startBuildPickerForChannel(state, channelId);
|
|
2478
|
+
return [head, ...picker];
|
|
2479
|
+
}
|
|
2480
|
+
state.buildPickPending.delete(channelId);
|
|
2481
|
+
let intentRuntime = state.intentRuntimes.get(channelId);
|
|
2482
|
+
if (!intentRuntime) {
|
|
2483
|
+
intentRuntime = createIntentRuntime(true);
|
|
2484
|
+
state.intentRuntimes.set(channelId, intentRuntime);
|
|
2485
|
+
}
|
|
2486
|
+
const lines = await enterEditModeForSkill(state.config, skillId, intentRuntime, "discord");
|
|
2487
|
+
if (isEmptyIntentRuntime(intentRuntime)) {
|
|
2488
|
+
state.intentRuntimes.delete(channelId);
|
|
2489
|
+
}
|
|
2490
|
+
else {
|
|
2491
|
+
state.intentRuntimes.set(channelId, intentRuntime);
|
|
2492
|
+
}
|
|
2493
|
+
void saveIntentRuntimes(state);
|
|
2494
|
+
return lines;
|
|
2495
|
+
}
|
|
2496
|
+
async function startBuildPickerForChannel(state, channelId) {
|
|
2497
|
+
const skills = await listSkillManifests(state.config.skills_dir);
|
|
2498
|
+
const userSkills = skills.filter((skill) => skill.source !== "builtin");
|
|
2499
|
+
if (userSkills.length === 0) {
|
|
2500
|
+
state.buildPickPending.delete(channelId);
|
|
2501
|
+
return [
|
|
2502
|
+
l("No user-created skills yet. Ask in chat to create one.", "編集できるスキルはまだありません。チャットで新しく作成を依頼してください。"),
|
|
2503
|
+
];
|
|
2504
|
+
}
|
|
2505
|
+
state.buildPickPending.set(channelId, { skillIds: userSkills.map((skill) => skill.id) });
|
|
2506
|
+
const header = l("Pick a skill to edit. Reply with a number:", "編集するスキルを番号で返信してください:");
|
|
2507
|
+
const rows = userSkills.map((skill, index) => {
|
|
2508
|
+
const enabled = skill.enabled === false ? l("disabled", "無効") : l("enabled", "有効");
|
|
2509
|
+
return ` ${String(index + 1).padStart(2)}) ${skill.id} — ${skill.name} (${enabled})`;
|
|
2510
|
+
});
|
|
2511
|
+
return [header, ...rows];
|
|
2512
|
+
}
|
|
2513
|
+
async function resolveBuildPickSelection(state, channelId, pending, pickText) {
|
|
2514
|
+
const num = Number.parseInt(pickText, 10);
|
|
2515
|
+
if (!Number.isInteger(num) || num < 1 || num > pending.skillIds.length) {
|
|
2516
|
+
return [l("Invalid selection. Send `/build` again to redisplay the list.", "無効な選択です。もう一度 `/build` を送ってください。")];
|
|
2517
|
+
}
|
|
2518
|
+
state.buildPickPending.delete(channelId);
|
|
2519
|
+
const skillId = pending.skillIds[num - 1];
|
|
2520
|
+
let intentRuntime = state.intentRuntimes.get(channelId);
|
|
2521
|
+
if (!intentRuntime) {
|
|
2522
|
+
intentRuntime = createIntentRuntime(true);
|
|
2523
|
+
state.intentRuntimes.set(channelId, intentRuntime);
|
|
2524
|
+
}
|
|
2525
|
+
const lines = await enterEditModeForSkill(state.config, skillId, intentRuntime, "discord");
|
|
2526
|
+
if (isEmptyIntentRuntime(intentRuntime)) {
|
|
2527
|
+
state.intentRuntimes.delete(channelId);
|
|
2528
|
+
}
|
|
2529
|
+
else {
|
|
2530
|
+
state.intentRuntimes.set(channelId, intentRuntime);
|
|
2531
|
+
}
|
|
2532
|
+
void saveIntentRuntimes(state);
|
|
2533
|
+
return lines;
|
|
2534
|
+
}
|
|
2434
2535
|
function handleProgressCommand(state, channelId, text) {
|
|
2435
2536
|
if (text !== "!progress" && !text.startsWith("!progress ")) {
|
|
2436
2537
|
return null;
|
|
@@ -2535,6 +2636,76 @@ async function sendChannelMessage(state, channelId, content) {
|
|
|
2535
2636
|
}
|
|
2536
2637
|
}
|
|
2537
2638
|
}
|
|
2639
|
+
async function sendChannelMessageWithLocalAttachments(state, channelId, content, localAttachmentPaths = []) {
|
|
2640
|
+
const explicitAttachments = await collectLocalFileAttachments({
|
|
2641
|
+
paths: localAttachmentPaths,
|
|
2642
|
+
cwd: state.config.workspace,
|
|
2643
|
+
allowedRoots: [state.config.workspace, state.config.notes_dir],
|
|
2644
|
+
});
|
|
2645
|
+
const inlineImages = await collectLocalImageAttachments({
|
|
2646
|
+
text: content,
|
|
2647
|
+
cwd: state.config.workspace,
|
|
2648
|
+
allowedRoots: [state.config.workspace, state.config.notes_dir],
|
|
2649
|
+
});
|
|
2650
|
+
const attachments = dedupeLocalAttachments([...explicitAttachments, ...inlineImages]);
|
|
2651
|
+
if (content.trim()) {
|
|
2652
|
+
await sendChannelMessage(state, channelId, content);
|
|
2653
|
+
}
|
|
2654
|
+
for (const attachment of attachments) {
|
|
2655
|
+
await sendDiscordFileAttachment(state, channelId, attachment);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
async function sendDiscordFileAttachment(state, channelId, attachment) {
|
|
2659
|
+
try {
|
|
2660
|
+
const buffer = await readFile(attachment.path);
|
|
2661
|
+
const form = new FormData();
|
|
2662
|
+
form.append("payload_json", JSON.stringify({ content: "" }));
|
|
2663
|
+
form.append("files[0]", blobFromBuffer(buffer, attachment.mimeType), attachment.filename);
|
|
2664
|
+
const response = await fetch(`${REST_BASE}/channels/${channelId}/messages`, {
|
|
2665
|
+
method: "POST",
|
|
2666
|
+
headers: { authorization: `Bot ${state.token}` },
|
|
2667
|
+
body: form,
|
|
2668
|
+
});
|
|
2669
|
+
if (!response.ok) {
|
|
2670
|
+
const detail = await response.text().catch(() => "");
|
|
2671
|
+
console.error(`agent-sin discord: file send failed: HTTP ${response.status}: ${detail.slice(0, 200)}`);
|
|
2672
|
+
await appendEventLog(state.config, {
|
|
2673
|
+
level: "error",
|
|
2674
|
+
source: "discord",
|
|
2675
|
+
event: "file_send_failed",
|
|
2676
|
+
message: `HTTP ${response.status}`,
|
|
2677
|
+
details: { channel_id: channelId, path: attachment.path },
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
catch (error) {
|
|
2682
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2683
|
+
console.error(`agent-sin discord: file send error: ${message}`);
|
|
2684
|
+
await appendEventLog(state.config, {
|
|
2685
|
+
level: "error",
|
|
2686
|
+
source: "discord",
|
|
2687
|
+
event: "file_send_error",
|
|
2688
|
+
message,
|
|
2689
|
+
details: { channel_id: channelId, path: attachment.path },
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
function dedupeLocalAttachments(attachments) {
|
|
2694
|
+
const seen = new Set();
|
|
2695
|
+
const out = [];
|
|
2696
|
+
for (const attachment of attachments) {
|
|
2697
|
+
if (seen.has(attachment.path))
|
|
2698
|
+
continue;
|
|
2699
|
+
seen.add(attachment.path);
|
|
2700
|
+
out.push(attachment);
|
|
2701
|
+
}
|
|
2702
|
+
return out;
|
|
2703
|
+
}
|
|
2704
|
+
function blobFromBuffer(buffer, type) {
|
|
2705
|
+
const bytes = new Uint8Array(buffer.length);
|
|
2706
|
+
bytes.set(buffer);
|
|
2707
|
+
return new Blob([bytes.buffer], { type });
|
|
2708
|
+
}
|
|
2538
2709
|
async function sendTypingIndicator(state, channelId) {
|
|
2539
2710
|
try {
|
|
2540
2711
|
await fetch(`${REST_BASE}/channels/${channelId}/typing`, {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AppConfig } from "../core/config.js";
|
|
2
|
+
export interface EvenG2GatewayOptions {
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
token?: string;
|
|
6
|
+
historyChannel?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EvenG2GatewayHandle {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
url: string;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function startEvenG2Gateway(config: AppConfig, options?: EvenG2GatewayOptions): Promise<EvenG2GatewayHandle>;
|
|
15
|
+
export declare function runEvenG2Gateway(config: AppConfig, options?: EvenG2GatewayOptions): Promise<number>;
|