agent-sin 0.1.11 → 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 +79 -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 +179 -0
- package/builtin-skills/memo-list/skill.yaml +51 -0
- 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 +10 -3
- package/dist/core/chat-engine.js +1563 -197
- 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 +13 -0
- package/dist/discord/bot.js +542 -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 +122 -31
- 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
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"));
|
|
@@ -761,6 +792,9 @@ async function handleMessage(state, message) {
|
|
|
761
792
|
if (await tryRunTodoSlashCommand(state, message, cleanText)) {
|
|
762
793
|
return;
|
|
763
794
|
}
|
|
795
|
+
if (await tryRunMemoSlashCommand(state, message, cleanText)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
764
798
|
if (await tryRunModelSlashCommand(state, message, cleanText)) {
|
|
765
799
|
return;
|
|
766
800
|
}
|
|
@@ -835,20 +869,20 @@ async function handleMessage(state, message) {
|
|
|
835
869
|
const typing = startTypingKeepalive(state, replyChannelId);
|
|
836
870
|
const prevMode = intentRuntime.mode;
|
|
837
871
|
try {
|
|
838
|
-
const
|
|
872
|
+
const routed = await routeDiscordMessage(state, userText, history, intentRuntime, status, replyChannelId, userMessage.images);
|
|
839
873
|
typing.stop();
|
|
840
874
|
void saveIntentRuntimes(state);
|
|
841
875
|
const isBuildEntry = prevMode !== "build" && intentRuntime.mode === "build";
|
|
842
|
-
const decorated = withModeBadge(intentRuntime, lines, { userText, isBuildEntry });
|
|
876
|
+
const decorated = withModeBadge(intentRuntime, routed.lines, { userText, isBuildEntry });
|
|
843
877
|
scheduleUpdateCheck(state.config.workspace);
|
|
844
878
|
const banner = await consumeUpdateBanner(state.config.workspace);
|
|
845
879
|
const finalLines = banner ? [banner, "", ...decorated] : decorated;
|
|
846
880
|
const reply = finalLines.filter((line) => line !== undefined && line !== null).join("\n").trim();
|
|
847
881
|
if (reply) {
|
|
848
|
-
await
|
|
882
|
+
await sendChannelMessageWithLocalAttachments(state, replyChannelId, reply, routed.localAttachmentPaths);
|
|
849
883
|
}
|
|
850
884
|
else {
|
|
851
|
-
await
|
|
885
|
+
await sendChannelMessageWithLocalAttachments(state, replyChannelId, l("(no response)", "(応答なし)"), routed.localAttachmentPaths);
|
|
852
886
|
}
|
|
853
887
|
}
|
|
854
888
|
catch (error) {
|
|
@@ -897,6 +931,7 @@ async function refreshDiscordHistoryBeforeCurrentMessage(state, channelId, befor
|
|
|
897
931
|
}
|
|
898
932
|
async function routeDiscordMessage(state, text, history, intentRuntime, status, replyChannelId, images = []) {
|
|
899
933
|
let modelFailed = false;
|
|
934
|
+
const localAttachmentPaths = [];
|
|
900
935
|
const lines = await routeConversationMessage({
|
|
901
936
|
config: state.config,
|
|
902
937
|
text,
|
|
@@ -913,9 +948,12 @@ async function routeDiscordMessage(state, text, history, intentRuntime, status,
|
|
|
913
948
|
modelFailed = true;
|
|
914
949
|
}
|
|
915
950
|
},
|
|
951
|
+
onLocalAttachments: (paths) => {
|
|
952
|
+
localAttachmentPaths.push(...paths);
|
|
953
|
+
},
|
|
916
954
|
});
|
|
917
955
|
await status.set(modelFailed ? "error" : "done");
|
|
918
|
-
return lines;
|
|
956
|
+
return { lines, localAttachmentPaths };
|
|
919
957
|
}
|
|
920
958
|
async function onChatProgressForReactions(event, status) {
|
|
921
959
|
switch (event.kind) {
|
|
@@ -1224,6 +1262,179 @@ async function tryRunTodoSlashCommand(state, message, cleanText) {
|
|
|
1224
1262
|
});
|
|
1225
1263
|
return true;
|
|
1226
1264
|
}
|
|
1265
|
+
export function parseMemoSlashCommand(text) {
|
|
1266
|
+
const trimmed = (text || "").trim();
|
|
1267
|
+
if (trimmed !== "/memo" && !/^\/memo\s/.test(trimmed)) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
const rest = trimmed === "/memo" ? "" : trimmed.replace(/^\/memo\s+/, "").trim();
|
|
1271
|
+
if (!rest || rest === "help" || rest === "--help" || rest === "-h") {
|
|
1272
|
+
return { kind: "help", lines: memoSlashHelpLines() };
|
|
1273
|
+
}
|
|
1274
|
+
const firstSpace = rest.search(/\s/);
|
|
1275
|
+
const sub = (firstSpace >= 0 ? rest.slice(0, firstSpace) : rest).toLowerCase();
|
|
1276
|
+
const remainder = firstSpace >= 0 ? rest.slice(firstSpace + 1).trim() : "";
|
|
1277
|
+
if (sub === "add" || sub === "save") {
|
|
1278
|
+
if (!remainder) {
|
|
1279
|
+
return { kind: "error", lines: [l("Usage: /memo add <text>", "使い方: /memo add <本文>")] };
|
|
1280
|
+
}
|
|
1281
|
+
return { kind: "run", skillId: "memo-save", args: { text: remainder } };
|
|
1282
|
+
}
|
|
1283
|
+
if (sub === "list" || sub === "ls") {
|
|
1284
|
+
const parsed = parseMemoListArgs(remainder);
|
|
1285
|
+
if (parsed.kind === "error")
|
|
1286
|
+
return parsed;
|
|
1287
|
+
return { kind: "run", skillId: "memo-list", args: parsed.args };
|
|
1288
|
+
}
|
|
1289
|
+
if (sub === "delete" || sub === "remove" || sub === "del") {
|
|
1290
|
+
const parsed = parseMemoDeleteArgs(remainder);
|
|
1291
|
+
if (parsed.kind === "error")
|
|
1292
|
+
return parsed;
|
|
1293
|
+
return { kind: "run", skillId: "memo-delete", args: parsed.args };
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
kind: "error",
|
|
1297
|
+
lines: [
|
|
1298
|
+
l(`Unknown subcommand: ${sub}.`, `未対応のサブコマンドです: ${sub}。`),
|
|
1299
|
+
...memoSlashHelpLines(),
|
|
1300
|
+
],
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function parseMemoListArgs(rest) {
|
|
1304
|
+
let body = rest.trim();
|
|
1305
|
+
const dateFlag = extractValueFlag(body, "date");
|
|
1306
|
+
body = dateFlag.text;
|
|
1307
|
+
const limitFlag = extractValueFlag(body, "limit");
|
|
1308
|
+
body = limitFlag.text;
|
|
1309
|
+
const args = {};
|
|
1310
|
+
const bodyParts = body.split(/\s+/).filter(Boolean);
|
|
1311
|
+
if (dateFlag.value) {
|
|
1312
|
+
args.date = dateFlag.value;
|
|
1313
|
+
}
|
|
1314
|
+
else if (bodyParts.length > 0) {
|
|
1315
|
+
args.date = bodyParts[0];
|
|
1316
|
+
bodyParts.shift();
|
|
1317
|
+
}
|
|
1318
|
+
if (bodyParts.length > 0) {
|
|
1319
|
+
return { kind: "error", lines: [l("Usage: /memo list [YYYY-MM-DD] [--limit 20]", "使い方: /memo list [YYYY-MM-DD] [--limit 20]")] };
|
|
1320
|
+
}
|
|
1321
|
+
if (limitFlag.value) {
|
|
1322
|
+
const limit = parseIntegerOption(limitFlag.value);
|
|
1323
|
+
if (!limit || limit < 1 || limit > 50) {
|
|
1324
|
+
return { kind: "error", lines: [l("limit must be an integer from 1 to 50.", "limit は 1〜50 の整数で指定してください。")] };
|
|
1325
|
+
}
|
|
1326
|
+
args.limit = limit;
|
|
1327
|
+
}
|
|
1328
|
+
return { kind: "ok", args };
|
|
1329
|
+
}
|
|
1330
|
+
function parseMemoDeleteArgs(rest) {
|
|
1331
|
+
let target = rest.trim();
|
|
1332
|
+
const dateFlag = extractValueFlag(target, "date");
|
|
1333
|
+
target = dateFlag.text.trim();
|
|
1334
|
+
const args = {};
|
|
1335
|
+
if (dateFlag.value)
|
|
1336
|
+
args.date = dateFlag.value;
|
|
1337
|
+
if (!target) {
|
|
1338
|
+
return { kind: "error", lines: [l("Usage: /memo delete <index|text> [--date YYYY-MM-DD]", "使い方: /memo delete <番号|本文> [--date YYYY-MM-DD]")] };
|
|
1339
|
+
}
|
|
1340
|
+
const numeric = parseIntegerOption(target);
|
|
1341
|
+
if (numeric && String(numeric) === target) {
|
|
1342
|
+
args.index = numeric;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
args.match = target;
|
|
1346
|
+
}
|
|
1347
|
+
return { kind: "ok", args };
|
|
1348
|
+
}
|
|
1349
|
+
function extractValueFlag(rest, name) {
|
|
1350
|
+
const pattern = new RegExp(`(^|\\s)--${escapeRegExp(name)}\\s+(\\S+)(?=\\s|$)`);
|
|
1351
|
+
const match = rest.match(pattern);
|
|
1352
|
+
if (!match)
|
|
1353
|
+
return { text: rest.trim() };
|
|
1354
|
+
const start = match.index ?? 0;
|
|
1355
|
+
const before = rest.slice(0, start);
|
|
1356
|
+
const after = rest.slice(start + match[0].length);
|
|
1357
|
+
const text = [before, after].map((segment) => segment.trim()).filter(Boolean).join(" ").trim();
|
|
1358
|
+
return { text, value: match[2] };
|
|
1359
|
+
}
|
|
1360
|
+
function parseIntegerOption(value) {
|
|
1361
|
+
if (!/^\d+$/.test(value.trim()))
|
|
1362
|
+
return null;
|
|
1363
|
+
const parsed = Number.parseInt(value, 10);
|
|
1364
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
1365
|
+
}
|
|
1366
|
+
function escapeRegExp(value) {
|
|
1367
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1368
|
+
}
|
|
1369
|
+
function memoSlashHelpLines() {
|
|
1370
|
+
return lLines([
|
|
1371
|
+
"Memo commands:",
|
|
1372
|
+
"/memo add <text> - save a memo",
|
|
1373
|
+
"/memo list [YYYY-MM-DD] [--limit 20] - list memos",
|
|
1374
|
+
"/memo delete <index|text> [--date YYYY-MM-DD] - delete a memo",
|
|
1375
|
+
], [
|
|
1376
|
+
"メモのショートカット:",
|
|
1377
|
+
"/memo add <本文> - メモを追加",
|
|
1378
|
+
"/memo list [YYYY-MM-DD] [--limit 20] - メモを一覧表示",
|
|
1379
|
+
"/memo delete <番号|本文> [--date YYYY-MM-DD] - メモを削除",
|
|
1380
|
+
]);
|
|
1381
|
+
}
|
|
1382
|
+
async function tryRunMemoSlashCommand(state, message, cleanText) {
|
|
1383
|
+
const parsed = await withLocale(inferLocaleFromText(cleanText), () => Promise.resolve(parseMemoSlashCommand(cleanText)));
|
|
1384
|
+
if (!parsed)
|
|
1385
|
+
return false;
|
|
1386
|
+
await withLocale(inferLocaleFromText(cleanText), async () => {
|
|
1387
|
+
const status = createStatusReactor(state, message.channel_id, message.id);
|
|
1388
|
+
await status.set("received");
|
|
1389
|
+
if (parsed.kind === "help") {
|
|
1390
|
+
await status.set("done");
|
|
1391
|
+
await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (parsed.kind === "error") {
|
|
1395
|
+
await status.set("error");
|
|
1396
|
+
await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await status.set("tool");
|
|
1400
|
+
try {
|
|
1401
|
+
const response = await runSkill(state.config, parsed.skillId, parsed.args);
|
|
1402
|
+
const display = response.result.summary ||
|
|
1403
|
+
response.result.title ||
|
|
1404
|
+
l("(no response)", "(応答なし)");
|
|
1405
|
+
await status.set(response.result.status === "ok" ? "done" : "error");
|
|
1406
|
+
await sendChannelMessage(state, message.channel_id, display);
|
|
1407
|
+
await appendEventLog(state.config, {
|
|
1408
|
+
level: "info",
|
|
1409
|
+
source: "discord",
|
|
1410
|
+
event: "memo_slash_ran",
|
|
1411
|
+
message: response.result.title || undefined,
|
|
1412
|
+
details: {
|
|
1413
|
+
skill_id: parsed.skillId,
|
|
1414
|
+
status: response.result.status,
|
|
1415
|
+
channel_id: message.channel_id,
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
await status.set("error");
|
|
1421
|
+
const detail = error instanceof SkillRunError
|
|
1422
|
+
? error.originalMessage
|
|
1423
|
+
: error instanceof Error
|
|
1424
|
+
? error.message
|
|
1425
|
+
: String(error);
|
|
1426
|
+
await sendChannelMessage(state, message.channel_id, l(`Error: ${detail}`, `エラー: ${detail}`));
|
|
1427
|
+
await appendEventLog(state.config, {
|
|
1428
|
+
level: "error",
|
|
1429
|
+
source: "discord",
|
|
1430
|
+
event: "memo_slash_failed",
|
|
1431
|
+
message: detail.slice(0, 200),
|
|
1432
|
+
details: { skill_id: parsed.skillId, channel_id: message.channel_id },
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1227
1438
|
export function parseModelSlashCommand(text) {
|
|
1228
1439
|
const trimmed = (text || "").trim();
|
|
1229
1440
|
if (trimmed !== "/model" && !/^\/model\s/.test(trimmed)) {
|
|
@@ -1428,6 +1639,84 @@ const TODO_SLASH_COMMAND_DEFINITION = {
|
|
|
1428
1639
|
},
|
|
1429
1640
|
],
|
|
1430
1641
|
};
|
|
1642
|
+
const MEMO_SLASH_COMMAND_DEFINITION = {
|
|
1643
|
+
name: "memo",
|
|
1644
|
+
description: "Memo shortcut commands",
|
|
1645
|
+
description_localizations: { ja: "メモのショートカット" },
|
|
1646
|
+
type: 1,
|
|
1647
|
+
dm_permission: true,
|
|
1648
|
+
options: [
|
|
1649
|
+
{
|
|
1650
|
+
type: 1,
|
|
1651
|
+
name: "add",
|
|
1652
|
+
description: "Save a memo",
|
|
1653
|
+
description_localizations: { ja: "メモを追加" },
|
|
1654
|
+
options: [
|
|
1655
|
+
{
|
|
1656
|
+
type: 3,
|
|
1657
|
+
name: "text",
|
|
1658
|
+
description: "Memo text",
|
|
1659
|
+
description_localizations: { ja: "メモ本文" },
|
|
1660
|
+
required: true,
|
|
1661
|
+
},
|
|
1662
|
+
],
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
type: 1,
|
|
1666
|
+
name: "list",
|
|
1667
|
+
description: "List memos",
|
|
1668
|
+
description_localizations: { ja: "メモを一覧表示" },
|
|
1669
|
+
options: [
|
|
1670
|
+
{
|
|
1671
|
+
type: 3,
|
|
1672
|
+
name: "date",
|
|
1673
|
+
description: "Target date (YYYY-MM-DD, default today)",
|
|
1674
|
+
description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
|
|
1675
|
+
required: false,
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
type: 4,
|
|
1679
|
+
name: "limit",
|
|
1680
|
+
description: "Maximum number of memos",
|
|
1681
|
+
description_localizations: { ja: "表示する最大件数" },
|
|
1682
|
+
required: false,
|
|
1683
|
+
min_value: 1,
|
|
1684
|
+
max_value: 50,
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
type: 1,
|
|
1690
|
+
name: "delete",
|
|
1691
|
+
description: "Delete a memo",
|
|
1692
|
+
description_localizations: { ja: "メモを削除" },
|
|
1693
|
+
options: [
|
|
1694
|
+
{
|
|
1695
|
+
type: 4,
|
|
1696
|
+
name: "index",
|
|
1697
|
+
description: "Memo number from /memo list",
|
|
1698
|
+
description_localizations: { ja: "/memo list の番号" },
|
|
1699
|
+
required: false,
|
|
1700
|
+
min_value: 1,
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
type: 3,
|
|
1704
|
+
name: "match",
|
|
1705
|
+
description: "Text contained in the memo",
|
|
1706
|
+
description_localizations: { ja: "メモに含まれる文字" },
|
|
1707
|
+
required: false,
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
type: 3,
|
|
1711
|
+
name: "date",
|
|
1712
|
+
description: "Target date (YYYY-MM-DD, default today)",
|
|
1713
|
+
description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
|
|
1714
|
+
required: false,
|
|
1715
|
+
},
|
|
1716
|
+
],
|
|
1717
|
+
},
|
|
1718
|
+
],
|
|
1719
|
+
};
|
|
1431
1720
|
const MODEL_SLASH_COMMAND_DEFINITION = {
|
|
1432
1721
|
name: "model",
|
|
1433
1722
|
description: "Show or switch the chat model",
|
|
@@ -1447,6 +1736,7 @@ const MODEL_SLASH_COMMAND_DEFINITION = {
|
|
|
1447
1736
|
};
|
|
1448
1737
|
const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
|
|
1449
1738
|
TODO_SLASH_COMMAND_DEFINITION,
|
|
1739
|
+
MEMO_SLASH_COMMAND_DEFINITION,
|
|
1450
1740
|
MODEL_SLASH_COMMAND_DEFINITION,
|
|
1451
1741
|
];
|
|
1452
1742
|
const DISCORD_SLASH_OPTION_TYPE_CODE = {
|
|
@@ -1590,6 +1880,12 @@ async function handleInteraction(state, interaction) {
|
|
|
1590
1880
|
}
|
|
1591
1881
|
return;
|
|
1592
1882
|
}
|
|
1883
|
+
if (name === "memo") {
|
|
1884
|
+
if (interaction.type === 2) {
|
|
1885
|
+
await handleMemoInteraction(state, interaction);
|
|
1886
|
+
}
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1593
1889
|
if (name === "model") {
|
|
1594
1890
|
if (interaction.type === 4) {
|
|
1595
1891
|
await handleModelAutocomplete(state, interaction);
|
|
@@ -1774,6 +2070,106 @@ async function handleTodoInteraction(state, interaction) {
|
|
|
1774
2070
|
}
|
|
1775
2071
|
});
|
|
1776
2072
|
}
|
|
2073
|
+
async function handleMemoInteraction(state, interaction) {
|
|
2074
|
+
const userId = await ensureInteractionUserAllowed(state, interaction, "memo");
|
|
2075
|
+
if (!userId)
|
|
2076
|
+
return;
|
|
2077
|
+
const sub = interaction.data?.options?.[0];
|
|
2078
|
+
const subName = sub?.name || "";
|
|
2079
|
+
const optMap = new Map();
|
|
2080
|
+
for (const opt of sub?.options || []) {
|
|
2081
|
+
optMap.set(opt.name, opt.value);
|
|
2082
|
+
}
|
|
2083
|
+
await withLocale(inferLocaleFromText(extractInteractionLocaleHint(sub)), async () => {
|
|
2084
|
+
let skillId;
|
|
2085
|
+
let args;
|
|
2086
|
+
if (subName === "add") {
|
|
2087
|
+
const text = String(optMap.get("text") || "").trim();
|
|
2088
|
+
if (!text) {
|
|
2089
|
+
await respondInteraction(state, interaction, {
|
|
2090
|
+
content: l("Memo text is required.", "メモ本文を指定してください。"),
|
|
2091
|
+
ephemeral: true,
|
|
2092
|
+
});
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
skillId = "memo-save";
|
|
2096
|
+
args = { text };
|
|
2097
|
+
}
|
|
2098
|
+
else if (subName === "list") {
|
|
2099
|
+
skillId = "memo-list";
|
|
2100
|
+
args = {};
|
|
2101
|
+
const date = String(optMap.get("date") || "").trim();
|
|
2102
|
+
if (date)
|
|
2103
|
+
args.date = date;
|
|
2104
|
+
const limit = optMap.get("limit");
|
|
2105
|
+
if (typeof limit === "number")
|
|
2106
|
+
args.limit = limit;
|
|
2107
|
+
}
|
|
2108
|
+
else if (subName === "delete") {
|
|
2109
|
+
skillId = "memo-delete";
|
|
2110
|
+
args = {};
|
|
2111
|
+
const date = String(optMap.get("date") || "").trim();
|
|
2112
|
+
const match = String(optMap.get("match") || "").trim();
|
|
2113
|
+
const index = optMap.get("index");
|
|
2114
|
+
if (date)
|
|
2115
|
+
args.date = date;
|
|
2116
|
+
if (typeof index === "number")
|
|
2117
|
+
args.index = index;
|
|
2118
|
+
if (match)
|
|
2119
|
+
args.match = match;
|
|
2120
|
+
if (args.index === undefined && !args.match) {
|
|
2121
|
+
await respondInteraction(state, interaction, {
|
|
2122
|
+
content: l("Specify a memo number or matching text.", "番号か一致する本文を指定してください。"),
|
|
2123
|
+
ephemeral: true,
|
|
2124
|
+
});
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
else {
|
|
2129
|
+
await respondInteraction(state, interaction, {
|
|
2130
|
+
content: l(`Unknown subcommand: ${subName || "(none)"}.`, `未対応のサブコマンドです: ${subName || "(なし)"}。`),
|
|
2131
|
+
ephemeral: true,
|
|
2132
|
+
});
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const ack = await deferInteraction(state, interaction);
|
|
2136
|
+
if (!ack)
|
|
2137
|
+
return;
|
|
2138
|
+
try {
|
|
2139
|
+
const response = await runSkill(state.config, skillId, args);
|
|
2140
|
+
const display = response.result.summary ||
|
|
2141
|
+
response.result.title ||
|
|
2142
|
+
l("(no response)", "(応答なし)");
|
|
2143
|
+
await editInteractionOriginal(state, interaction, display);
|
|
2144
|
+
await appendEventLog(state.config, {
|
|
2145
|
+
level: "info",
|
|
2146
|
+
source: "discord",
|
|
2147
|
+
event: "memo_slash_ran",
|
|
2148
|
+
message: response.result.title || undefined,
|
|
2149
|
+
details: {
|
|
2150
|
+
skill_id: skillId,
|
|
2151
|
+
status: response.result.status,
|
|
2152
|
+
kind: "interaction",
|
|
2153
|
+
},
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
2157
|
+
const detail = error instanceof SkillRunError
|
|
2158
|
+
? error.originalMessage
|
|
2159
|
+
: error instanceof Error
|
|
2160
|
+
? error.message
|
|
2161
|
+
: String(error);
|
|
2162
|
+
await editInteractionOriginal(state, interaction, l(`Error: ${detail}`, `エラー: ${detail}`));
|
|
2163
|
+
await appendEventLog(state.config, {
|
|
2164
|
+
level: "error",
|
|
2165
|
+
source: "discord",
|
|
2166
|
+
event: "memo_slash_failed",
|
|
2167
|
+
message: detail.slice(0, 200),
|
|
2168
|
+
details: { skill_id: skillId, kind: "interaction" },
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
1777
2173
|
async function handleModelInteraction(state, interaction) {
|
|
1778
2174
|
const userId = await ensureInteractionUserAllowed(state, interaction, "model");
|
|
1779
2175
|
if (!userId)
|
|
@@ -2070,6 +2466,72 @@ async function editInteractionOriginal(state, interaction, content) {
|
|
|
2070
2466
|
}
|
|
2071
2467
|
}
|
|
2072
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
|
+
}
|
|
2073
2535
|
function handleProgressCommand(state, channelId, text) {
|
|
2074
2536
|
if (text !== "!progress" && !text.startsWith("!progress ")) {
|
|
2075
2537
|
return null;
|
|
@@ -2174,6 +2636,76 @@ async function sendChannelMessage(state, channelId, content) {
|
|
|
2174
2636
|
}
|
|
2175
2637
|
}
|
|
2176
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
|
+
}
|
|
2177
2709
|
async function sendTypingIndicator(state, channelId) {
|
|
2178
2710
|
try {
|
|
2179
2711
|
await fetch(`${REST_BASE}/channels/${channelId}/typing`, {
|