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
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
6
|
+
import { appendEventLog } from "../core/logger.js";
|
|
7
|
+
import { stripInternalControlBlocks, } from "../core/chat-engine.js";
|
|
8
|
+
import { createIntentRuntime, } from "../builder/build-flow.js";
|
|
9
|
+
import { loadIntentRuntimeMap, saveIntentRuntimeMap, } from "../builder/intent-runtime-store.js";
|
|
10
|
+
import { routeConversationMessage, } from "../builder/conversation-router.js";
|
|
11
|
+
import { notify } from "../core/notifier.js";
|
|
12
|
+
import { getApiKeys } from "../core/secrets.js";
|
|
13
|
+
import { inferLocaleFromText, withLocale } from "../core/i18n.js";
|
|
14
|
+
const DEFAULT_PORT = 8765;
|
|
15
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
16
|
+
const HISTORY_LIMIT = 20;
|
|
17
|
+
const INBOX_LIMIT = 50;
|
|
18
|
+
const REQUEST_BODY_LIMIT = 16 * 1024 * 1024;
|
|
19
|
+
const AUDIO_BODY_LIMIT = 24 * 1024 * 1024;
|
|
20
|
+
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
21
|
+
const DISCORD_PUBLIC_THREAD = 11;
|
|
22
|
+
export async function startEvenG2Gateway(config, options = {}) {
|
|
23
|
+
const host = options.host || process.env.AGENT_SIN_G2_HOST || DEFAULT_HOST;
|
|
24
|
+
const port = options.port ?? numberFromEnv("AGENT_SIN_G2_PORT", DEFAULT_PORT);
|
|
25
|
+
const token = options.token ?? process.env.AGENT_SIN_G2_TOKEN ?? "";
|
|
26
|
+
const historyChannel = options.historyChannel || process.env.AGENT_SIN_G2_HISTORY_CHANNEL || "";
|
|
27
|
+
if (!isLoopbackHost(host) && !token) {
|
|
28
|
+
throw new Error("AGENT_SIN_G2_TOKEN is required when binding the G2 gateway outside localhost.");
|
|
29
|
+
}
|
|
30
|
+
const dir = path.join(config.workspace, "even-g2");
|
|
31
|
+
const state = {
|
|
32
|
+
config,
|
|
33
|
+
histories: await loadG2Histories(path.join(dir, "histories.json")),
|
|
34
|
+
historiesFile: path.join(dir, "histories.json"),
|
|
35
|
+
intentRuntimes: await loadIntentRuntimeMap(path.join(dir, "intent-runtimes.json"), "chats"),
|
|
36
|
+
intentRuntimesFile: path.join(dir, "intent-runtimes.json"),
|
|
37
|
+
inbox: await loadG2Inbox(path.join(dir, "inbox.json")),
|
|
38
|
+
inboxFile: path.join(dir, "inbox.json"),
|
|
39
|
+
discordThreadId: await loadG2DiscordThreadId(path.join(dir, "discord-thread.json")),
|
|
40
|
+
discordThreadFile: path.join(dir, "discord-thread.json"),
|
|
41
|
+
clients: new Set(),
|
|
42
|
+
historyChannel,
|
|
43
|
+
};
|
|
44
|
+
const server = http.createServer((request, response) => {
|
|
45
|
+
void handleHttpRequest(state, token, request, response);
|
|
46
|
+
});
|
|
47
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
48
|
+
server.on("upgrade", (request, socket, head) => {
|
|
49
|
+
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
|
50
|
+
if (url.pathname !== "/ws" || !isAuthorized(token, request, url)) {
|
|
51
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
52
|
+
socket.destroy();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
56
|
+
wss.emit("connection", ws, request);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
wss.on("connection", (ws) => {
|
|
60
|
+
state.clients.add(ws);
|
|
61
|
+
sendWs(ws, {
|
|
62
|
+
type: "hello",
|
|
63
|
+
inbox: unreadInbox(state),
|
|
64
|
+
message: "connected",
|
|
65
|
+
});
|
|
66
|
+
ws.on("message", (data) => {
|
|
67
|
+
void handleWsMessage(state, ws, data);
|
|
68
|
+
});
|
|
69
|
+
ws.on("close", () => {
|
|
70
|
+
state.clients.delete(ws);
|
|
71
|
+
});
|
|
72
|
+
ws.on("error", () => {
|
|
73
|
+
state.clients.delete(ws);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
server.once("error", reject);
|
|
78
|
+
server.listen(port, host, () => {
|
|
79
|
+
server.off("error", reject);
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
const actual = server.address();
|
|
84
|
+
const actualPort = typeof actual === "object" && actual ? actual.port : port;
|
|
85
|
+
const urlHost = host === "0.0.0.0" ? localLanHint() || host : host;
|
|
86
|
+
const url = `http://${urlHost}:${actualPort}`;
|
|
87
|
+
await appendEventLog(config, {
|
|
88
|
+
level: "info",
|
|
89
|
+
source: "g2",
|
|
90
|
+
event: "gateway_started",
|
|
91
|
+
message: url,
|
|
92
|
+
details: { host, port: actualPort, auth: Boolean(token), history_channel: historyChannel || "none" },
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
host,
|
|
96
|
+
port: actualPort,
|
|
97
|
+
url,
|
|
98
|
+
close: () => new Promise((resolve) => {
|
|
99
|
+
for (const client of state.clients) {
|
|
100
|
+
client.close();
|
|
101
|
+
}
|
|
102
|
+
wss.close(() => {
|
|
103
|
+
server.close(() => resolve());
|
|
104
|
+
});
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function runEvenG2Gateway(config, options = {}) {
|
|
109
|
+
const handle = await startEvenG2Gateway(config, options);
|
|
110
|
+
console.log(`agent-sin g2: listening on ${handle.url}`);
|
|
111
|
+
if (!process.env.AGENT_SIN_G2_TOKEN && isLoopbackHost(handle.host)) {
|
|
112
|
+
console.log("agent-sin g2: auth token is not set; localhost-only access is allowed.");
|
|
113
|
+
}
|
|
114
|
+
if (handle.host === "0.0.0.0" && handle.url.includes("0.0.0.0")) {
|
|
115
|
+
console.log("agent-sin g2: set --host to your LAN IP for the QR URL if needed.");
|
|
116
|
+
}
|
|
117
|
+
let stopping = false;
|
|
118
|
+
const shutdown = () => {
|
|
119
|
+
if (stopping)
|
|
120
|
+
return;
|
|
121
|
+
stopping = true;
|
|
122
|
+
void handle.close().then(() => {
|
|
123
|
+
console.log("agent-sin g2: stopped");
|
|
124
|
+
process.exit(0);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
process.once("SIGINT", shutdown);
|
|
128
|
+
process.once("SIGTERM", shutdown);
|
|
129
|
+
await new Promise(() => undefined);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
async function handleHttpRequest(state, token, request, response) {
|
|
133
|
+
setCors(response);
|
|
134
|
+
if (request.method === "OPTIONS") {
|
|
135
|
+
response.writeHead(204);
|
|
136
|
+
response.end();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
|
140
|
+
try {
|
|
141
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
142
|
+
sendHtml(response, browserConsoleHtml());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
146
|
+
sendJson(response, 200, {
|
|
147
|
+
ok: true,
|
|
148
|
+
clients: state.clients.size,
|
|
149
|
+
unread: unreadInbox(state).length,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!isAuthorized(token, request, url)) {
|
|
154
|
+
sendJson(response, 401, { ok: false, error: "unauthorized" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (request.method === "GET" && url.pathname === "/api/inbox") {
|
|
158
|
+
sendJson(response, 200, { ok: true, notifications: unreadInbox(state) });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (request.method === "POST" && url.pathname === "/api/ack") {
|
|
162
|
+
const body = await readJsonBody(request);
|
|
163
|
+
await ackNotification(state, stringField(body, "id"));
|
|
164
|
+
sendJson(response, 200, { ok: true });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (request.method === "POST" && url.pathname === "/api/notify") {
|
|
168
|
+
const body = await readJsonBody(request);
|
|
169
|
+
const notification = await addNotification(state, {
|
|
170
|
+
title: stringField(body, "title") || "Agent-Sin",
|
|
171
|
+
body: stringField(body, "body"),
|
|
172
|
+
subtitle: stringField(body, "subtitle") || undefined,
|
|
173
|
+
});
|
|
174
|
+
await mirrorText(state, "notification", formatNotificationForMirror(notification));
|
|
175
|
+
sendJson(response, 200, { ok: true, notification });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (request.method === "POST" && url.pathname === "/api/message") {
|
|
179
|
+
const body = await readJsonBody(request);
|
|
180
|
+
const result = await handleG2UserMessage(state, {
|
|
181
|
+
text: stringField(body, "text"),
|
|
182
|
+
conversationId: stringField(body, "conversation_id") || "g2",
|
|
183
|
+
inputMode: stringField(body, "input_mode") || "text",
|
|
184
|
+
});
|
|
185
|
+
sendJson(response, 200, { ok: true, ...result });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (request.method === "POST" && url.pathname === "/api/audio-message") {
|
|
189
|
+
const body = await readJsonBody(request, AUDIO_BODY_LIMIT);
|
|
190
|
+
const transcript = stringField(body, "text") || await transcribeAudioJson(body);
|
|
191
|
+
const result = await handleG2UserMessage(state, {
|
|
192
|
+
text: transcript,
|
|
193
|
+
conversationId: stringField(body, "conversation_id") || "g2",
|
|
194
|
+
inputMode: "voice",
|
|
195
|
+
});
|
|
196
|
+
sendJson(response, 200, { ok: true, transcript, ...result });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
sendJson(response, 404, { ok: false, error: "not_found" });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
await appendEventLog(state.config, {
|
|
204
|
+
level: "error",
|
|
205
|
+
source: "g2",
|
|
206
|
+
event: "request_failed",
|
|
207
|
+
message: message.slice(0, 200),
|
|
208
|
+
details: { path: url.pathname, method: request.method },
|
|
209
|
+
});
|
|
210
|
+
sendJson(response, 500, { ok: false, error: message });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function handleWsMessage(state, ws, data) {
|
|
214
|
+
let message;
|
|
215
|
+
try {
|
|
216
|
+
message = JSON.parse(data.toString());
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
sendWs(ws, { type: "error", error: "invalid_json" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const type = stringField(message, "type");
|
|
223
|
+
try {
|
|
224
|
+
if (type === "message") {
|
|
225
|
+
sendWs(ws, { type: "thinking" });
|
|
226
|
+
const result = await handleG2UserMessage(state, {
|
|
227
|
+
text: stringField(message, "text"),
|
|
228
|
+
conversationId: stringField(message, "conversation_id") || "g2",
|
|
229
|
+
inputMode: stringField(message, "input_mode") || "text",
|
|
230
|
+
});
|
|
231
|
+
sendWs(ws, { type: "reply", ...result });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (type === "audio") {
|
|
235
|
+
sendWs(ws, { type: "transcribing" });
|
|
236
|
+
const transcript = stringField(message, "text") || await transcribeAudioJson(message);
|
|
237
|
+
sendWs(ws, { type: "transcript", text: transcript });
|
|
238
|
+
const result = await handleG2UserMessage(state, {
|
|
239
|
+
text: transcript,
|
|
240
|
+
conversationId: stringField(message, "conversation_id") || "g2",
|
|
241
|
+
inputMode: "voice",
|
|
242
|
+
});
|
|
243
|
+
sendWs(ws, { type: "reply", transcript, ...result });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (type === "ack") {
|
|
247
|
+
await ackNotification(state, stringField(message, "id"));
|
|
248
|
+
sendWs(ws, { type: "ack", ok: true });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
sendWs(ws, { type: "error", error: "unknown_type" });
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
255
|
+
sendWs(ws, { type: "error", error: detail });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function handleG2UserMessage(state, input) {
|
|
259
|
+
const text = input.text.trim();
|
|
260
|
+
if (!text) {
|
|
261
|
+
throw new Error("message text is required");
|
|
262
|
+
}
|
|
263
|
+
const conversationId = normalizeConversationId(input.conversationId);
|
|
264
|
+
let history = state.histories.get(conversationId);
|
|
265
|
+
if (!history) {
|
|
266
|
+
history = [];
|
|
267
|
+
state.histories.set(conversationId, history);
|
|
268
|
+
}
|
|
269
|
+
let intentRuntime = state.intentRuntimes.get(conversationId);
|
|
270
|
+
if (!intentRuntime) {
|
|
271
|
+
intentRuntime = createIntentRuntime(true);
|
|
272
|
+
state.intentRuntimes.set(conversationId, intentRuntime);
|
|
273
|
+
}
|
|
274
|
+
await mirrorText(state, "user", `G2から:\n${text}`);
|
|
275
|
+
const lines = await withLocale(inferLocaleFromText(text), () => routeConversationMessage({
|
|
276
|
+
config: state.config,
|
|
277
|
+
text,
|
|
278
|
+
history,
|
|
279
|
+
intentRuntime,
|
|
280
|
+
eventSource: "g2",
|
|
281
|
+
createBuildProgress: () => createG2BuildProgressReporter(state),
|
|
282
|
+
onChatProgress: (event) => broadcastProgress(state, event),
|
|
283
|
+
}));
|
|
284
|
+
trimHistory(history);
|
|
285
|
+
await saveG2Histories(state);
|
|
286
|
+
await saveIntentRuntimeMap(state.intentRuntimesFile, "chats", state.intentRuntimes);
|
|
287
|
+
const reply = stripInternalControlBlocks(lines.join("\n")).trim() || "(no response)";
|
|
288
|
+
const display = compactForG2(reply);
|
|
289
|
+
await mirrorText(state, "assistant", `Agent:\n${reply}`);
|
|
290
|
+
broadcast(state, { type: "reply_broadcast", text, reply, display, input_mode: input.inputMode });
|
|
291
|
+
return { text, reply, display };
|
|
292
|
+
}
|
|
293
|
+
function createG2BuildProgressReporter(state) {
|
|
294
|
+
const messages = [];
|
|
295
|
+
return {
|
|
296
|
+
onProgress(event) {
|
|
297
|
+
const text = progressText(event);
|
|
298
|
+
if (!text)
|
|
299
|
+
return;
|
|
300
|
+
messages.push(text);
|
|
301
|
+
broadcast(state, { type: "build_progress", text });
|
|
302
|
+
},
|
|
303
|
+
async flush() {
|
|
304
|
+
if (messages.length === 0)
|
|
305
|
+
return;
|
|
306
|
+
await appendEventLog(state.config, {
|
|
307
|
+
level: "info",
|
|
308
|
+
source: "g2",
|
|
309
|
+
event: "build_progress",
|
|
310
|
+
message: messages[messages.length - 1].slice(0, 200),
|
|
311
|
+
details: { count: messages.length },
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function broadcastProgress(state, event) {
|
|
317
|
+
if (event.kind === "thinking") {
|
|
318
|
+
broadcast(state, { type: "progress", text: "thinking" });
|
|
319
|
+
}
|
|
320
|
+
else if (event.kind === "tool_running") {
|
|
321
|
+
broadcast(state, { type: "progress", text: `tool:${event.skill_id}` });
|
|
322
|
+
}
|
|
323
|
+
else if (event.kind === "model_failed") {
|
|
324
|
+
broadcast(state, { type: "progress", text: "model_failed" });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function progressText(event) {
|
|
328
|
+
if (event.kind === "thinking")
|
|
329
|
+
return event.text || "Thinking";
|
|
330
|
+
if (event.kind === "tool")
|
|
331
|
+
return event.text || event.name || "Running tool";
|
|
332
|
+
if (event.kind === "message")
|
|
333
|
+
return event.text || "";
|
|
334
|
+
if (event.kind === "info")
|
|
335
|
+
return event.text;
|
|
336
|
+
if (event.kind === "stderr")
|
|
337
|
+
return event.text;
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
async function addNotification(state, input) {
|
|
341
|
+
const notification = {
|
|
342
|
+
id: crypto.randomUUID(),
|
|
343
|
+
title: input.title.trim() || "Agent-Sin",
|
|
344
|
+
body: input.body.trim(),
|
|
345
|
+
subtitle: input.subtitle?.trim() || undefined,
|
|
346
|
+
created_at: new Date().toISOString(),
|
|
347
|
+
read: false,
|
|
348
|
+
};
|
|
349
|
+
state.inbox.unshift(notification);
|
|
350
|
+
state.inbox = state.inbox.slice(0, INBOX_LIMIT);
|
|
351
|
+
await saveG2Inbox(state);
|
|
352
|
+
broadcast(state, { type: "notification", notification });
|
|
353
|
+
return notification;
|
|
354
|
+
}
|
|
355
|
+
async function ackNotification(state, id) {
|
|
356
|
+
if (!id)
|
|
357
|
+
return;
|
|
358
|
+
let changed = false;
|
|
359
|
+
for (const item of state.inbox) {
|
|
360
|
+
if (item.id === id && !item.read) {
|
|
361
|
+
item.read = true;
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (changed) {
|
|
366
|
+
await saveG2Inbox(state);
|
|
367
|
+
broadcast(state, { type: "inbox", notifications: unreadInbox(state) });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function unreadInbox(state) {
|
|
371
|
+
return state.inbox.filter((item) => !item.read);
|
|
372
|
+
}
|
|
373
|
+
async function mirrorText(state, kind, body) {
|
|
374
|
+
const channels = configuredMirrorChannels(state.historyChannel);
|
|
375
|
+
if (channels.length === 0)
|
|
376
|
+
return;
|
|
377
|
+
for (const channel of channels) {
|
|
378
|
+
const discordThreadId = channel === "discord" ? await resolveG2DiscordThreadId(state) : undefined;
|
|
379
|
+
const result = await notify({
|
|
380
|
+
channel,
|
|
381
|
+
title: kind === "notification" ? "G2通知" : "G2 Agent",
|
|
382
|
+
body,
|
|
383
|
+
discordThreadId,
|
|
384
|
+
});
|
|
385
|
+
if (!result.ok) {
|
|
386
|
+
await appendEventLog(state.config, {
|
|
387
|
+
level: "warn",
|
|
388
|
+
source: "g2",
|
|
389
|
+
event: "mirror_failed",
|
|
390
|
+
message: `${channel}: ${result.detail || "failed"}`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function resolveG2DiscordThreadId(state) {
|
|
396
|
+
const explicit = normalizeDiscordSnowflake(firstListValue(process.env.AGENT_SIN_G2_DISCORD_THREAD_ID));
|
|
397
|
+
if (explicit)
|
|
398
|
+
return explicit;
|
|
399
|
+
if (!shouldAutoCreateG2DiscordThread())
|
|
400
|
+
return undefined;
|
|
401
|
+
if (state.discordThreadId)
|
|
402
|
+
return state.discordThreadId;
|
|
403
|
+
const token = (process.env.AGENT_SIN_DISCORD_BOT_TOKEN || "").trim();
|
|
404
|
+
const parentChannelId = g2DiscordThreadParentChannelId();
|
|
405
|
+
if (!token || !parentChannelId) {
|
|
406
|
+
await appendEventLog(state.config, {
|
|
407
|
+
level: "warn",
|
|
408
|
+
source: "g2",
|
|
409
|
+
event: "discord_thread_skipped",
|
|
410
|
+
message: "Discord bot token or parent channel id is missing",
|
|
411
|
+
});
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const thread = await createG2DiscordThread(state, token, parentChannelId);
|
|
415
|
+
if (!thread)
|
|
416
|
+
return undefined;
|
|
417
|
+
state.discordThreadId = thread.id;
|
|
418
|
+
await saveG2DiscordThread(state, thread);
|
|
419
|
+
return thread.id;
|
|
420
|
+
}
|
|
421
|
+
function shouldAutoCreateG2DiscordThread() {
|
|
422
|
+
const raw = firstListValue(process.env.AGENT_SIN_G2_DISCORD_THREAD || process.env.AGENT_SIN_G2_DISCORD_THREAD_ID)
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
return raw === "auto" || raw === "create" || raw === "1" || raw === "true" || raw === "yes";
|
|
425
|
+
}
|
|
426
|
+
function g2DiscordThreadParentChannelId() {
|
|
427
|
+
return normalizeDiscordSnowflake(firstListValue(process.env.AGENT_SIN_G2_DISCORD_THREAD_PARENT_CHANNEL_ID) ||
|
|
428
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_NOTIFY_CHANNEL_ID) ||
|
|
429
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_CHANNEL_ID) ||
|
|
430
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_LISTEN_CHANNEL_IDS));
|
|
431
|
+
}
|
|
432
|
+
async function createG2DiscordThread(state, token, parentChannelId) {
|
|
433
|
+
const name = makeG2DiscordThreadName();
|
|
434
|
+
const archiveMinutes = numberFromEnv("AGENT_SIN_G2_DISCORD_THREAD_ARCHIVE_MINUTES", 10080);
|
|
435
|
+
try {
|
|
436
|
+
const response = await fetch(`${DISCORD_API_BASE}/channels/${parentChannelId}/threads`, {
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: {
|
|
439
|
+
authorization: `Bot ${token}`,
|
|
440
|
+
"content-type": "application/json",
|
|
441
|
+
},
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
name,
|
|
444
|
+
auto_archive_duration: archiveMinutes,
|
|
445
|
+
type: DISCORD_PUBLIC_THREAD,
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
const detail = await response.text().catch(() => "");
|
|
450
|
+
await appendEventLog(state.config, {
|
|
451
|
+
level: "warn",
|
|
452
|
+
source: "g2",
|
|
453
|
+
event: "discord_thread_create_failed",
|
|
454
|
+
message: `HTTP ${response.status}`,
|
|
455
|
+
details: { parent_channel_id: parentChannelId, detail: detail.slice(0, 300) },
|
|
456
|
+
});
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const json = await response.json();
|
|
460
|
+
const id = normalizeDiscordSnowflake(typeof json.id === "string" ? json.id : "");
|
|
461
|
+
if (!id) {
|
|
462
|
+
await appendEventLog(state.config, {
|
|
463
|
+
level: "warn",
|
|
464
|
+
source: "g2",
|
|
465
|
+
event: "discord_thread_create_failed",
|
|
466
|
+
message: "Discord response did not include a thread id",
|
|
467
|
+
details: { parent_channel_id: parentChannelId },
|
|
468
|
+
});
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
id,
|
|
473
|
+
parent_channel_id: typeof json.parent_id === "string" ? json.parent_id : parentChannelId,
|
|
474
|
+
name: typeof json.name === "string" ? json.name : name,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
await appendEventLog(state.config, {
|
|
479
|
+
level: "warn",
|
|
480
|
+
source: "g2",
|
|
481
|
+
event: "discord_thread_create_failed",
|
|
482
|
+
message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200),
|
|
483
|
+
details: { parent_channel_id: parentChannelId },
|
|
484
|
+
});
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function makeG2DiscordThreadName() {
|
|
489
|
+
const raw = (process.env.AGENT_SIN_G2_DISCORD_THREAD_NAME || `Agent-Sin G2 ${new Date().toISOString().slice(0, 10)}`)
|
|
490
|
+
.replace(/[\r\n]+/g, " ")
|
|
491
|
+
.replace(/\s+/g, " ")
|
|
492
|
+
.trim();
|
|
493
|
+
const name = raw || "Agent-Sin G2";
|
|
494
|
+
return name.length <= 95 ? name : `${name.slice(0, 95).trimEnd()}…`;
|
|
495
|
+
}
|
|
496
|
+
function normalizeDiscordSnowflake(value) {
|
|
497
|
+
const trimmed = value.trim();
|
|
498
|
+
return /^\d+$/.test(trimmed) ? trimmed : "";
|
|
499
|
+
}
|
|
500
|
+
function configuredMirrorChannels(historyChannel) {
|
|
501
|
+
const selected = (historyChannel || process.env.AGENT_SIN_G2_HISTORY_CHANNEL || "").trim();
|
|
502
|
+
if (selected) {
|
|
503
|
+
return historyChannelsFromSetting(selected);
|
|
504
|
+
}
|
|
505
|
+
const legacy = process.env.AGENT_SIN_G2_MIRROR_CHANNELS;
|
|
506
|
+
if (legacy) {
|
|
507
|
+
return historyChannelsFromSetting(legacy);
|
|
508
|
+
}
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
function historyChannelsFromSetting(raw) {
|
|
512
|
+
const normalized = raw.trim().toLowerCase();
|
|
513
|
+
if (/^(none|off|false|0)$/i.test(normalized)) {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
if (normalized === "auto") {
|
|
517
|
+
return autoHistoryChannel();
|
|
518
|
+
}
|
|
519
|
+
const channels = normalized
|
|
520
|
+
.split(/[,\s]+/)
|
|
521
|
+
.map((item) => item.trim())
|
|
522
|
+
.filter((item) => item === "discord" || item === "telegram");
|
|
523
|
+
return Array.from(new Set(channels));
|
|
524
|
+
}
|
|
525
|
+
function autoHistoryChannel() {
|
|
526
|
+
if (hasTelegramNotifyConfig())
|
|
527
|
+
return ["telegram"];
|
|
528
|
+
if (hasDiscordNotifyConfig())
|
|
529
|
+
return ["discord"];
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
function hasDiscordNotifyConfig() {
|
|
533
|
+
return Boolean(process.env.AGENT_SIN_DISCORD_WEBHOOK_URL ||
|
|
534
|
+
(process.env.AGENT_SIN_DISCORD_BOT_TOKEN &&
|
|
535
|
+
(firstListValue(process.env.AGENT_SIN_DISCORD_NOTIFY_THREAD_ID) ||
|
|
536
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_THREAD_ID) ||
|
|
537
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_NOTIFY_CHANNEL_ID) ||
|
|
538
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_CHANNEL_ID) ||
|
|
539
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_LISTEN_CHANNEL_IDS))));
|
|
540
|
+
}
|
|
541
|
+
function hasTelegramNotifyConfig() {
|
|
542
|
+
return Boolean(process.env.AGENT_SIN_TELEGRAM_BOT_TOKEN &&
|
|
543
|
+
(firstListValue(process.env.AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID) ||
|
|
544
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_CHAT_ID) ||
|
|
545
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_LISTEN_CHAT_IDS)));
|
|
546
|
+
}
|
|
547
|
+
function firstListValue(value) {
|
|
548
|
+
return (value || "")
|
|
549
|
+
.split(/[,\s]+/)
|
|
550
|
+
.map((item) => item.trim())
|
|
551
|
+
.find(Boolean) || "";
|
|
552
|
+
}
|
|
553
|
+
async function transcribeAudioJson(body) {
|
|
554
|
+
const audio = audioBufferFromJson(body);
|
|
555
|
+
if (audio.length === 0) {
|
|
556
|
+
throw new Error("audio data is required");
|
|
557
|
+
}
|
|
558
|
+
const sampleRate = numericField(body, "sample_rate") || numericField(body, "sampleRate") || 16000;
|
|
559
|
+
const channels = numericField(body, "channels") || 1;
|
|
560
|
+
const format = stringField(body, "format") || "pcm16";
|
|
561
|
+
const wav = format === "wav" ? audio : wavFromPcm16(audio, sampleRate, channels);
|
|
562
|
+
return transcribeWavWithOpenAI(wav, {
|
|
563
|
+
language: stringField(body, "language") || process.env.AGENT_SIN_G2_STT_LANGUAGE || "ja",
|
|
564
|
+
prompt: stringField(body, "prompt") || process.env.AGENT_SIN_G2_STT_PROMPT || "",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function audioBufferFromJson(body) {
|
|
568
|
+
const wavBase64 = stringField(body, "wavBase64") || stringField(body, "audioBase64");
|
|
569
|
+
if (wavBase64)
|
|
570
|
+
return decodeBase64(wavBase64);
|
|
571
|
+
const pcmBase64 = stringField(body, "pcmBase64") || stringField(body, "pcm_base64");
|
|
572
|
+
if (pcmBase64)
|
|
573
|
+
return decodeBase64(pcmBase64);
|
|
574
|
+
const pcm = body.pcm;
|
|
575
|
+
if (Array.isArray(pcm)) {
|
|
576
|
+
const bytes = pcm.map((value) => {
|
|
577
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
578
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(255, Math.trunc(n))) : 0;
|
|
579
|
+
});
|
|
580
|
+
return Buffer.from(bytes);
|
|
581
|
+
}
|
|
582
|
+
return Buffer.alloc(0);
|
|
583
|
+
}
|
|
584
|
+
function decodeBase64(value) {
|
|
585
|
+
const base64 = value.replace(/^data:[^,]+,/, "").replace(/\s+/g, "");
|
|
586
|
+
return Buffer.from(base64, "base64");
|
|
587
|
+
}
|
|
588
|
+
async function transcribeWavWithOpenAI(wav, options) {
|
|
589
|
+
const keys = getApiKeys("openai");
|
|
590
|
+
if (keys.length === 0) {
|
|
591
|
+
throw new Error("OPENAI_API_KEY is required for G2 voice transcription");
|
|
592
|
+
}
|
|
593
|
+
const model = process.env.AGENT_SIN_G2_STT_MODEL || "gpt-4o-mini-transcribe";
|
|
594
|
+
const form = new FormData();
|
|
595
|
+
form.set("model", model);
|
|
596
|
+
form.set("response_format", "json");
|
|
597
|
+
if (options.language)
|
|
598
|
+
form.set("language", options.language);
|
|
599
|
+
if (options.prompt)
|
|
600
|
+
form.set("prompt", options.prompt);
|
|
601
|
+
form.set("file", new Blob([new Uint8Array(wav)], { type: "audio/wav" }), "g2.wav");
|
|
602
|
+
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { authorization: `Bearer ${keys[0]}` },
|
|
605
|
+
body: form,
|
|
606
|
+
});
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
const detail = await response.text();
|
|
609
|
+
throw new Error(`OpenAI transcription failed: HTTP ${response.status} ${detail.slice(0, 300)}`);
|
|
610
|
+
}
|
|
611
|
+
const json = (await response.json());
|
|
612
|
+
const text = (json.text || "").trim();
|
|
613
|
+
if (!text) {
|
|
614
|
+
throw new Error("transcription was empty");
|
|
615
|
+
}
|
|
616
|
+
return text;
|
|
617
|
+
}
|
|
618
|
+
function wavFromPcm16(pcm, sampleRate, channels) {
|
|
619
|
+
const bitsPerSample = 16;
|
|
620
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
621
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
622
|
+
const header = Buffer.alloc(44);
|
|
623
|
+
header.write("RIFF", 0);
|
|
624
|
+
header.writeUInt32LE(36 + pcm.length, 4);
|
|
625
|
+
header.write("WAVE", 8);
|
|
626
|
+
header.write("fmt ", 12);
|
|
627
|
+
header.writeUInt32LE(16, 16);
|
|
628
|
+
header.writeUInt16LE(1, 20);
|
|
629
|
+
header.writeUInt16LE(channels, 22);
|
|
630
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
631
|
+
header.writeUInt32LE(byteRate, 28);
|
|
632
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
633
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
634
|
+
header.write("data", 36);
|
|
635
|
+
header.writeUInt32LE(pcm.length, 40);
|
|
636
|
+
return Buffer.concat([header, pcm]);
|
|
637
|
+
}
|
|
638
|
+
async function loadG2Histories(file) {
|
|
639
|
+
try {
|
|
640
|
+
const raw = await readFile(file, "utf8");
|
|
641
|
+
const data = JSON.parse(raw);
|
|
642
|
+
const map = new Map();
|
|
643
|
+
for (const [key, turns] of Object.entries(data.chats || {})) {
|
|
644
|
+
if (Array.isArray(turns)) {
|
|
645
|
+
map.set(key, turns.filter(isChatTurn).slice(-HISTORY_LIMIT));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return map;
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return new Map();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function saveG2Histories(state) {
|
|
655
|
+
await mkdir(path.dirname(state.historiesFile), { recursive: true });
|
|
656
|
+
const chats = {};
|
|
657
|
+
for (const [key, history] of state.histories) {
|
|
658
|
+
if (history.length > 0) {
|
|
659
|
+
chats[key] = history.slice(-HISTORY_LIMIT);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
await writeFile(state.historiesFile, `${JSON.stringify({ saved_at: new Date().toISOString(), chats }, null, 2)}\n`, "utf8");
|
|
663
|
+
}
|
|
664
|
+
async function loadG2Inbox(file) {
|
|
665
|
+
try {
|
|
666
|
+
const raw = await readFile(file, "utf8");
|
|
667
|
+
const data = JSON.parse(raw);
|
|
668
|
+
return Array.isArray(data.notifications)
|
|
669
|
+
? data.notifications.filter(isNotification).slice(0, INBOX_LIMIT)
|
|
670
|
+
: [];
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function saveG2Inbox(state) {
|
|
677
|
+
await mkdir(path.dirname(state.inboxFile), { recursive: true });
|
|
678
|
+
await writeFile(state.inboxFile, `${JSON.stringify({ saved_at: new Date().toISOString(), notifications: state.inbox }, null, 2)}\n`, "utf8");
|
|
679
|
+
}
|
|
680
|
+
async function loadG2DiscordThreadId(file) {
|
|
681
|
+
try {
|
|
682
|
+
const raw = await readFile(file, "utf8");
|
|
683
|
+
const data = JSON.parse(raw);
|
|
684
|
+
return normalizeDiscordSnowflake(typeof data.thread_id === "string" ? data.thread_id : "");
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function saveG2DiscordThread(state, thread) {
|
|
691
|
+
await mkdir(path.dirname(state.discordThreadFile), { recursive: true });
|
|
692
|
+
await writeFile(state.discordThreadFile, `${JSON.stringify({
|
|
693
|
+
thread_id: thread.id,
|
|
694
|
+
parent_channel_id: thread.parent_channel_id,
|
|
695
|
+
name: thread.name,
|
|
696
|
+
saved_at: new Date().toISOString(),
|
|
697
|
+
}, null, 2)}\n`, "utf8");
|
|
698
|
+
}
|
|
699
|
+
function isChatTurn(value) {
|
|
700
|
+
if (!value || typeof value !== "object")
|
|
701
|
+
return false;
|
|
702
|
+
const turn = value;
|
|
703
|
+
return ((turn.role === "user" || turn.role === "assistant" || turn.role === "tool") &&
|
|
704
|
+
typeof turn.content === "string");
|
|
705
|
+
}
|
|
706
|
+
function isNotification(value) {
|
|
707
|
+
if (!value || typeof value !== "object")
|
|
708
|
+
return false;
|
|
709
|
+
const item = value;
|
|
710
|
+
return typeof item.id === "string" && typeof item.title === "string" && typeof item.body === "string";
|
|
711
|
+
}
|
|
712
|
+
function trimHistory(history) {
|
|
713
|
+
while (history.length > HISTORY_LIMIT) {
|
|
714
|
+
history.shift();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function broadcast(state, payload) {
|
|
718
|
+
for (const client of state.clients) {
|
|
719
|
+
sendWs(client, payload);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function sendWs(ws, payload) {
|
|
723
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
724
|
+
return;
|
|
725
|
+
ws.send(JSON.stringify(payload));
|
|
726
|
+
}
|
|
727
|
+
function sendJson(response, status, payload) {
|
|
728
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
729
|
+
response.end(JSON.stringify(payload));
|
|
730
|
+
}
|
|
731
|
+
function sendHtml(response, html) {
|
|
732
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
733
|
+
response.end(html);
|
|
734
|
+
}
|
|
735
|
+
function setCors(response) {
|
|
736
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
737
|
+
response.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
|
|
738
|
+
response.setHeader("access-control-allow-headers", "content-type,authorization,x-agent-sin-g2-token");
|
|
739
|
+
}
|
|
740
|
+
async function readJsonBody(request, limit = REQUEST_BODY_LIMIT) {
|
|
741
|
+
const chunks = [];
|
|
742
|
+
let total = 0;
|
|
743
|
+
for await (const chunk of request) {
|
|
744
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
745
|
+
total += buffer.length;
|
|
746
|
+
if (total > limit) {
|
|
747
|
+
throw new Error("request body is too large");
|
|
748
|
+
}
|
|
749
|
+
chunks.push(buffer);
|
|
750
|
+
}
|
|
751
|
+
if (chunks.length === 0)
|
|
752
|
+
return {};
|
|
753
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
754
|
+
return JSON.parse(raw);
|
|
755
|
+
}
|
|
756
|
+
function isAuthorized(token, request, url) {
|
|
757
|
+
if (!token)
|
|
758
|
+
return true;
|
|
759
|
+
const authorization = request.headers.authorization || "";
|
|
760
|
+
if (authorization === `Bearer ${token}`)
|
|
761
|
+
return true;
|
|
762
|
+
if (request.headers["x-agent-sin-g2-token"] === token)
|
|
763
|
+
return true;
|
|
764
|
+
return url.searchParams.get("token") === token;
|
|
765
|
+
}
|
|
766
|
+
function stringField(source, key) {
|
|
767
|
+
const value = source[key];
|
|
768
|
+
return typeof value === "string" ? value : "";
|
|
769
|
+
}
|
|
770
|
+
function numericField(source, key) {
|
|
771
|
+
const value = source[key];
|
|
772
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
773
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
774
|
+
}
|
|
775
|
+
function numberFromEnv(name, fallback) {
|
|
776
|
+
const value = process.env[name];
|
|
777
|
+
if (!value)
|
|
778
|
+
return fallback;
|
|
779
|
+
const numeric = Number.parseInt(value, 10);
|
|
780
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
|
|
781
|
+
}
|
|
782
|
+
function normalizeConversationId(value) {
|
|
783
|
+
const normalized = value.trim().replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 80);
|
|
784
|
+
return normalized || "g2";
|
|
785
|
+
}
|
|
786
|
+
function compactForG2(text) {
|
|
787
|
+
const cleaned = text.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
788
|
+
if (cleaned.length <= 900)
|
|
789
|
+
return cleaned;
|
|
790
|
+
return `${cleaned.slice(0, 880).trimEnd()}\n...`;
|
|
791
|
+
}
|
|
792
|
+
function formatNotificationForMirror(notification) {
|
|
793
|
+
const head = notification.subtitle
|
|
794
|
+
? `${notification.title} - ${notification.subtitle}`
|
|
795
|
+
: notification.title;
|
|
796
|
+
return `${head}\n${notification.body}`.trim();
|
|
797
|
+
}
|
|
798
|
+
function isLoopbackHost(host) {
|
|
799
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
800
|
+
}
|
|
801
|
+
function localLanHint() {
|
|
802
|
+
return process.env.AGENT_SIN_G2_PUBLIC_HOST || "";
|
|
803
|
+
}
|
|
804
|
+
function browserConsoleHtml() {
|
|
805
|
+
return `<!doctype html>
|
|
806
|
+
<html lang="ja">
|
|
807
|
+
<head>
|
|
808
|
+
<meta charset="utf-8">
|
|
809
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
810
|
+
<title>Agent-Sin G2</title>
|
|
811
|
+
<style>
|
|
812
|
+
:root { color-scheme: dark; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
813
|
+
body { margin: 0; min-height: 100vh; background: #101412; color: #eef6ef; }
|
|
814
|
+
main { width: min(760px, calc(100vw - 32px)); margin: 0 auto; padding: 32px 0; }
|
|
815
|
+
h1 { font-size: 24px; line-height: 1.2; margin: 0 0 20px; font-weight: 650; }
|
|
816
|
+
#log { min-height: 280px; border: 1px solid #38443d; border-radius: 8px; padding: 16px; background: #161d19; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
817
|
+
form { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin-top: 12px; }
|
|
818
|
+
input, button { font: inherit; border-radius: 6px; border: 1px solid #405048; padding: 10px 12px; }
|
|
819
|
+
input { background: #0e1310; color: #eef6ef; min-width: 0; }
|
|
820
|
+
button { background: #d7f7d8; color: #102014; cursor: pointer; }
|
|
821
|
+
.muted { color: #a7b7ad; font-size: 13px; margin-top: 12px; }
|
|
822
|
+
</style>
|
|
823
|
+
</head>
|
|
824
|
+
<body>
|
|
825
|
+
<main>
|
|
826
|
+
<h1>Agent-Sin G2</h1>
|
|
827
|
+
<div id="log">connecting...</div>
|
|
828
|
+
<form id="form">
|
|
829
|
+
<input id="text" autocomplete="off" placeholder="G2の代わりにテスト入力">
|
|
830
|
+
<button>送信</button>
|
|
831
|
+
</form>
|
|
832
|
+
<p class="muted">Even G2アプリからは同じゲートウェイにWebSocket/HTTPで接続します。</p>
|
|
833
|
+
</main>
|
|
834
|
+
<script>
|
|
835
|
+
const params = new URLSearchParams(location.search);
|
|
836
|
+
const token = params.get("token") || localStorage.getItem("agentSinG2Token") || "";
|
|
837
|
+
if (params.get("token")) localStorage.setItem("agentSinG2Token", token);
|
|
838
|
+
const log = document.querySelector("#log");
|
|
839
|
+
const input = document.querySelector("#text");
|
|
840
|
+
const form = document.querySelector("#form");
|
|
841
|
+
const wsUrl = new URL("/ws", location.href);
|
|
842
|
+
wsUrl.protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
843
|
+
if (token) wsUrl.searchParams.set("token", token);
|
|
844
|
+
const ws = new WebSocket(wsUrl);
|
|
845
|
+
function add(label, text) {
|
|
846
|
+
log.textContent += "\\n\\n" + label + "\\n" + text;
|
|
847
|
+
log.scrollTop = log.scrollHeight;
|
|
848
|
+
}
|
|
849
|
+
ws.addEventListener("open", () => { log.textContent = "connected"; });
|
|
850
|
+
ws.addEventListener("message", (event) => {
|
|
851
|
+
const msg = JSON.parse(event.data);
|
|
852
|
+
if (msg.type === "reply") add("Agent", msg.display || msg.reply || "");
|
|
853
|
+
if (msg.type === "notification") add("通知", msg.notification.title + "\\n" + msg.notification.body);
|
|
854
|
+
if (msg.type === "thinking") add("status", "thinking...");
|
|
855
|
+
if (msg.type === "error") add("error", msg.error);
|
|
856
|
+
});
|
|
857
|
+
form.addEventListener("submit", (event) => {
|
|
858
|
+
event.preventDefault();
|
|
859
|
+
const text = input.value.trim();
|
|
860
|
+
if (!text) return;
|
|
861
|
+
add("G2", text);
|
|
862
|
+
ws.send(JSON.stringify({ type: "message", text }));
|
|
863
|
+
input.value = "";
|
|
864
|
+
});
|
|
865
|
+
</script>
|
|
866
|
+
</body>
|
|
867
|
+
</html>`;
|
|
868
|
+
}
|