@jcheesepkg/nanobot 0.8.9 → 0.8.96
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/dist/agent/loop.d.mts.map +1 -1
- package/dist/agent/loop.mjs +4 -0
- package/dist/agent/loop.mjs.map +1 -1
- package/dist/agent/tools/flex.d.mts.map +1 -1
- package/dist/agent/tools/flex.mjs +1 -1
- package/dist/agent/tools/flex.mjs.map +1 -1
- package/dist/agent/tools/flex.test.mjs +3 -3
- package/dist/agent/tools/flex.test.mjs.map +1 -1
- package/dist/channels/line.d.mts +1 -1
- package/dist/channels/line.d.mts.map +1 -1
- package/dist/channels/line.mjs +15 -8
- package/dist/channels/line.mjs.map +1 -1
- package/dist/channels/line.test.mjs +10 -0
- package/dist/channels/line.test.mjs.map +1 -1
- package/dist/config/schema.d.mts +54 -54
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.mts","names":[],"sources":["../../src/agent/loop.ts"],"mappings":";;;;;;;;;;;;AAqCA;;;;;;;cAAa,SAAA;EAAA,QACH,GAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,kBAAA;EAAA,QACA,SAAA;EAAA,QACA,aAAA;EAAA,QACA,YAAA;EAAA,SAEC,OAAA,EAAS,cAAA;EAAA,SACT,QAAA,EAAU,cAAA;EAAA,SACV,KAAA,EAAO,YAAA;EAAA,SACP,SAAA,EAAW,eAAA;EAAA,QAEZ,QAAA;EARA;EAAA,QAWA,QAAA;cAEI,MAAA;IACV,GAAA,EAAK,UAAA;IACL,QAAA,EAAU,WAAA;IACV,SAAA;IACA,KAAA;IACA,kBAAA;IACA,SAAA;IACA,aAAA;IACA,YAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;IACb,mBAAA;IACA,YAAA;IACA,aAAA;IACA,WAAA,GAAc,IAAA;EAAA;EAAA,QAqCR,oBAAA;EA9CN;EA+GI,GAAA,CAAA,GAAO,OAAA;EA7GX;EA8IF,IAAA,CAAA;EA5IE;EAAA,QAkJY,cAAA;EAAA,QA4FA,oBAAA;EAAA,QA4CA,YAAA;EAvRZ;EAAA,
|
|
1
|
+
{"version":3,"file":"loop.d.mts","names":[],"sources":["../../src/agent/loop.ts"],"mappings":";;;;;;;;;;;;AAqCA;;;;;;;cAAa,SAAA;EAAA,QACH,GAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,kBAAA;EAAA,QACA,SAAA;EAAA,QACA,aAAA;EAAA,QACA,YAAA;EAAA,SAEC,OAAA,EAAS,cAAA;EAAA,SACT,QAAA,EAAU,cAAA;EAAA,SACV,KAAA,EAAO,YAAA;EAAA,SACP,SAAA,EAAW,eAAA;EAAA,QAEZ,QAAA;EARA;EAAA,QAWA,QAAA;cAEI,MAAA;IACV,GAAA,EAAK,UAAA;IACL,QAAA,EAAU,WAAA;IACV,SAAA;IACA,KAAA;IACA,kBAAA;IACA,SAAA;IACA,aAAA;IACA,YAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;IACb,mBAAA;IACA,YAAA;IACA,aAAA;IACA,WAAA,GAAc,IAAA;EAAA;EAAA,QAqCR,oBAAA;EA9CN;EA+GI,GAAA,CAAA,GAAO,OAAA;EA7GX;EA8IF,IAAA,CAAA;EA5IE;EAAA,QAkJY,cAAA;EAAA,QA4FA,oBAAA;EAAA,QA4CA,YAAA;EAvRZ;EAAA,QA6XM,uBAAA;EAAA,QAcA,kBAAA;EAzYQ;EA2ZV,aAAA,CACJ,OAAA,UACA,UAAA,WACA,OAAA,WACA,MAAA,YACC,OAAA;EA3XK;EAAA,QAmZM,iBAAA;AAAA"}
|
package/dist/agent/loop.mjs
CHANGED
|
@@ -241,6 +241,10 @@ var AgentLoop = class {
|
|
|
241
241
|
}
|
|
242
242
|
const result = await this.tools.execute(tc.name, tc.arguments);
|
|
243
243
|
this.context.addToolResult(messages, tc.id, tc.name, result);
|
|
244
|
+
if (tc.name === "flex_message" && onToolCallText && !result.startsWith("Error")) {
|
|
245
|
+
const jsonOnly = result.split("\n\n(")[0];
|
|
246
|
+
onToolCallText(jsonOnly);
|
|
247
|
+
}
|
|
244
248
|
}
|
|
245
249
|
} else {
|
|
246
250
|
finalContent = response.content;
|
package/dist/agent/loop.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.mjs","names":[],"sources":["../../src/agent/loop.ts"],"sourcesContent":["import type { LLMProvider, ChatMessage, ToolCallRequest } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type {\n InboundMessage,\n OutboundMessage,\n} from \"../bus/events.js\";\nimport { createOutboundMessage } from \"../bus/events.js\";\nimport { ContextBuilder } from \"./context.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport {\n ReadFileTool,\n WriteFileTool,\n EditFileTool,\n ListDirTool,\n} from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport { MessageTool } from \"./tools/message.js\";\nimport { SpawnTool } from \"./tools/spawn.js\";\nimport { CronTool } from \"./tools/cron.js\";\nimport { FlexTool } from \"./tools/flex.js\";\nimport { SubagentManager } from \"./subagent.js\";\nimport { getBuiltinSkillsDir } from \"./skills.js\";\nimport { SessionManager, Session } from \"../session/manager.js\";\nimport { MemoryStore } from \"./memory.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\nimport type { Tool } from \"./tools/base.js\";\n\n/**\n * The agent loop: core processing engine.\n *\n * 1. Receives messages from the bus\n * 2. Builds context with history, memory, skills\n * 3. Calls the LLM\n * 4. Executes tool calls\n * 5. Sends responses back\n */\nexport class AgentLoop {\n private bus: MessageBus;\n private provider: LLMProvider;\n private workspace: string;\n private model: string;\n private consolidationModel: string;\n private maxTokens: number;\n private maxIterations: number;\n private memoryWindow: number;\n\n readonly context: ContextBuilder;\n readonly sessions: SessionManager;\n readonly tools: ToolRegistry;\n readonly subagents: SubagentManager;\n\n private _running = false;\n\n /** In-flight AbortControllers keyed by session key. */\n private inflight = new Map<string, AbortController>();\n\n constructor(params: {\n bus: MessageBus;\n provider: LLMProvider;\n workspace: string;\n model?: string;\n consolidationModel?: string;\n maxTokens?: number;\n maxIterations?: number;\n memoryWindow?: number;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n restrictToWorkspace?: boolean;\n toolsEnabled?: string[];\n toolsDisabled?: string[];\n customTools?: Tool[];\n }) {\n this.bus = params.bus;\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.consolidationModel = params.consolidationModel ?? this.model;\n this.maxTokens = params.maxTokens ?? 8192;\n this.maxIterations = params.maxIterations ?? 20;\n this.memoryWindow = params.memoryWindow ?? 50;\n\n const execConfig = params.execConfig ?? { timeout: 60 };\n const restrictToWorkspace = params.restrictToWorkspace ?? false;\n\n this.context = new ContextBuilder(params.workspace);\n this.sessions = new SessionManager(params.workspace);\n this.tools = new ToolRegistry();\n this.subagents = new SubagentManager({\n provider: params.provider,\n workspace: params.workspace,\n bus: params.bus,\n model: this.model,\n braveApiKey: params.braveApiKey,\n execConfig,\n restrictToWorkspace,\n });\n\n this.registerDefaultTools(\n execConfig,\n restrictToWorkspace,\n params.braveApiKey,\n params.toolsEnabled,\n params.toolsDisabled,\n params.customTools,\n );\n }\n\n private registerDefaultTools(\n execConfig: ExecToolConfig,\n restrictToWorkspace: boolean,\n braveApiKey?: string,\n toolsEnabled?: string[],\n toolsDisabled?: string[],\n customTools?: Tool[],\n ): void {\n const enabled = new Set(toolsEnabled ?? []);\n const disabled = new Set(toolsDisabled ?? []);\n const hasAllowlist = enabled.size > 0;\n const shouldRegister = (name: string): boolean =>\n (hasAllowlist ? enabled.has(name) : true) && !disabled.has(name);\n\n const registerIfEnabled = (tool: Tool): void => {\n if (shouldRegister(tool.name)) {\n this.tools.register(tool);\n }\n };\n\n // File tools — pass allowedDir when restrictToWorkspace is enabled\n const allowedDir = restrictToWorkspace ? this.workspace : undefined;\n const readOnlyPaths = restrictToWorkspace ? [getBuiltinSkillsDir()] : undefined;\n registerIfEnabled(new ReadFileTool({ allowedDir, readOnlyPaths }));\n registerIfEnabled(new WriteFileTool({ allowedDir }));\n registerIfEnabled(new EditFileTool({ allowedDir }));\n registerIfEnabled(new ListDirTool({ allowedDir, readOnlyPaths }));\n\n // Shell tool\n registerIfEnabled(\n new ExecTool({\n workingDir: this.workspace,\n timeout: execConfig.timeout,\n restrictToWorkspace,\n }),\n );\n\n // Web tools\n registerIfEnabled(new WebSearchTool({ apiKey: braveApiKey }));\n registerIfEnabled(new WebFetchTool());\n\n // Message tool\n const messageTool = new MessageTool({\n sendCallback: (msg) => this.bus.publishOutbound(msg),\n });\n registerIfEnabled(messageTool);\n\n // Spawn tool\n const spawnTool = new SpawnTool(this.subagents);\n registerIfEnabled(spawnTool);\n\n // Cron tool — always registered, uses DO scheduling via worker API\n registerIfEnabled(new CronTool());\n\n // Flex Message tool — builds LINE Flex JSON from structured data\n registerIfEnabled(new FlexTool());\n\n if (customTools && customTools.length > 0) {\n for (const tool of customTools) {\n registerIfEnabled(tool);\n }\n }\n }\n\n /** Run the agent loop, processing messages from the bus. */\n async run(): Promise<void> {\n this._running = true;\n console.log(\"Agent loop started\");\n\n while (this._running) {\n try {\n const msg = await this.bus.consumeInboundTimeout(1000);\n\n // Process concurrently so new messages can abort in-flight ones\n this.processMessage(msg)\n .then(async (response) => {\n if (response) {\n await this.bus.publishOutbound(response);\n }\n })\n .catch(async (err) => {\n if (isAbortError(err)) return; // Already handled\n console.error(\"Error processing message:\", err);\n await this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : err}`,\n }),\n );\n });\n } catch {\n // timeout, continue\n }\n }\n }\n\n /** Stop the agent loop. */\n stop(): void {\n this._running = false;\n console.log(\"Agent loop stopping\");\n }\n\n /** Process a single inbound message. */\n private async processMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n // Handle system messages (subagent announces)\n if (msg.channel === \"system\") {\n return this.processSystemMessage(msg);\n }\n\n console.log(`Processing message from ${msg.channel}:${msg.senderId}`);\n\n const sessionKey = `${msg.channel}:${msg.chatId}`;\n\n // Abort any in-flight request for this session\n const existing = this.inflight.get(sessionKey);\n if (existing) {\n console.log(`Aborting in-flight request for ${sessionKey}`);\n existing.abort();\n }\n\n // Create a new AbortController for this request\n const controller = new AbortController();\n this.inflight.set(sessionKey, controller);\n\n const session = this.sessions.getOrCreate(sessionKey);\n\n // Consolidate memory if session is too large\n if (session.history.length > this.memoryWindow) {\n await this.consolidateMemory(session);\n }\n\n // Update tool contexts\n this.updateToolContexts(msg.channel, msg.chatId);\n\n // Build initial messages\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n media: msg.media.length > 0 ? msg.media : undefined,\n channel: msg.channel,\n chatId: msg.chatId,\n });\n\n // The messages array is: [system, ...history, currentUser]\n // We want to save from the current user message onward (skip system + old history).\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen; // 1 for system prompt\n\n try {\n // Agent loop (mutates messages by appending assistant/tool messages)\n const result = await this.runAgentLoop(messages, controller.signal, (text) => {\n if (text && text.trim().length > 0) {\n this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: text,\n }),\n );\n }\n });\n\n // Save the new messages from this turn (user + all agent loop messages)\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: result.content,\n });\n } catch (err) {\n if (isAbortError(err)) {\n // Request was aborted because a new message arrived.\n // Save the user message to history so the next request has context,\n // but don't save any assistant response.\n const userMessages = messages.slice(newMsgStart).filter((m) => m.role === \"user\");\n if (userMessages.length > 0) {\n session.addTurnMessages(userMessages);\n this.sessions.save(session);\n }\n console.log(`Request aborted for ${sessionKey}, user message saved to history`);\n return null; // No response -- the new message will handle it\n }\n throw err; // Re-throw non-abort errors\n } finally {\n // Clean up if this is still our controller\n if (this.inflight.get(sessionKey) === controller) {\n this.inflight.delete(sessionKey);\n }\n }\n }\n\n private async processSystemMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n console.log(`Processing system message from ${msg.senderId}`);\n\n let originChannel: string;\n let originChatId: string;\n\n if (msg.chatId.includes(\":\")) {\n const [ch, id] = msg.chatId.split(\":\", 2);\n originChannel = ch;\n originChatId = id;\n } else {\n originChannel = \"cli\";\n originChatId = msg.chatId;\n }\n\n const sessionKey = `${originChannel}:${originChatId}`;\n const session = this.sessions.getOrCreate(sessionKey);\n\n this.updateToolContexts(originChannel, originChatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n channel: originChannel,\n chatId: originChatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const result = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: originChannel,\n chatId: originChatId,\n content: result.content,\n });\n }\n\n private async runAgentLoop(\n messages: ChatMessage[],\n signal?: AbortSignal,\n onToolCallText?: (text: string) => void,\n ): Promise<{ content: string; toolsUsed: string[] }> {\n let finalContent: string | null = null;\n let sentToolCallNotice = false;\n const toolsUsed: string[] = [];\n\n for (let i = 0; i < this.maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: this.tools.getDefinitions(),\n model: this.model,\n maxTokens: this.maxTokens,\n signal,\n });\n\n if (response.hasToolCalls) {\n // Send an interim message so the user knows we're working\n if (!sentToolCallNotice && onToolCallText) {\n const interimText = response.content?.trim()\n || this.getToolCallFallbackText(response.toolCalls);\n if (interimText) {\n onToolCallText(interimText);\n }\n sentToolCallNotice = true;\n }\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n this.context.addAssistantMessage(\n messages,\n response.content,\n toolCallDicts,\n );\n\n for (const tc of response.toolCalls) {\n toolsUsed.push(tc.name);\n console.log(`Tool: ${tc.name}(${JSON.stringify(tc.arguments)})`);\n\n // Detect skill reads\n if (tc.name === \"read_file\") {\n const path = String(tc.arguments?.path ?? \"\");\n const skillMatch = path.match(/skills\\/([^/]+)\\/SKILL\\.md$/);\n if (skillMatch) {\n console.log(`Skill activated: ${skillMatch[1]}`);\n }\n }\n\n const result = await this.tools.execute(tc.name, tc.arguments);\n this.context.addToolResult(messages, tc.id, tc.name, result);\n }\n } else {\n finalContent = response.content;\n\n // If the LLM returned empty content after tool use, nudge it to respond\n if ((!finalContent || finalContent.trim().length === 0) && i > 0) {\n messages.push({\n role: \"assistant\",\n content: \"\",\n });\n messages.push({\n role: \"user\",\n content: \"(You used tools but didn't respond to the user. Please provide a brief response summarizing what you did.)\",\n });\n continue;\n }\n\n // Push the final assistant message so it gets persisted with the turn\n messages.push({ role: \"assistant\", content: finalContent ?? \"\" });\n break;\n }\n }\n\n if (!finalContent || finalContent.trim().length === 0) {\n finalContent = \"I've completed processing but have no response to give.\";\n }\n\n // If we exhausted iterations without a non-tool-call response, still persist the final text\n if (messages[messages.length - 1]?.role !== \"assistant\" || messages[messages.length - 1]?.content !== finalContent) {\n messages.push({ role: \"assistant\", content: finalContent });\n }\n\n return { content: finalContent, toolsUsed };\n }\n\n /** Generate a fallback interim message based on which tools are being called. */\n private getToolCallFallbackText(toolCalls: ToolCallRequest[]): string {\n const toolNames = toolCalls.map((tc) => tc.name);\n const fallbacks: Record<string, string> = {\n web_search: \"検索中...\",\n web_fetch: \"ページを読み込み中...\",\n spawn: \"サブエージェントを起動中...\",\n flex_message: \"カードを作成中...\",\n };\n for (const name of toolNames) {\n if (fallbacks[name]) return fallbacks[name];\n }\n return \"ちょっと待ってね...\";\n }\n\n private updateToolContexts(channel: string, chatId: string): void {\n const messageTool = this.tools.get(\"message\");\n if (messageTool instanceof MessageTool) {\n messageTool.setContext(channel, chatId);\n }\n\n const spawnTool = this.tools.get(\"spawn\");\n if (spawnTool instanceof SpawnTool) {\n spawnTool.setContext(channel, chatId);\n }\n\n const cronTool = this.tools.get(\"cron\");\n if (cronTool instanceof CronTool) {\n cronTool.setContext(channel, chatId);\n }\n }\n\n /** Process a message directly (for CLI or cron usage). */\n async processDirect(\n content: string,\n sessionKey = \"cli:direct\",\n channel = \"cli\",\n chatId = \"direct\",\n ): Promise<string> {\n // Use inline version of processMessage for direct calls\n const session = this.sessions.getOrCreate(sessionKey);\n this.updateToolContexts(channel, chatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: content,\n channel,\n chatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const result = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return result.content;\n }\n\n /** Consolidate old messages into MEMORY.md + HISTORY.md, then trim session. */\n private async consolidateMemory(session: Session): Promise<void> {\n const memory = new MemoryStore(this.workspace);\n const keepCount = Math.min(10, Math.max(2, Math.floor(this.memoryWindow / 2)));\n const oldMessages = session.history.slice(0, -keepCount);\n if (oldMessages.length === 0) return;\n\n console.log(`Memory consolidation: ${session.history.length} messages, archiving ${oldMessages.length}, keeping ${keepCount}`);\n\n // Format messages for LLM\n const lines: string[] = [];\n for (const m of oldMessages) {\n const content = typeof m.content === \"string\" ? m.content : \"\";\n if (!content) continue;\n const role = m.role.toUpperCase();\n const tools = m.toolsUsed ? ` [tools: ${m.toolsUsed.join(\", \")}]` : \"\";\n const ts = m.timestamp ? `[${m.timestamp.slice(0, 16)}] ` : \"\";\n lines.push(`${ts}${role}${tools}: ${content}`);\n }\n const conversation = lines.join(\"\\n\");\n const currentMemory = memory.readLongTerm();\n\n const prompt = `You are a memory consolidation agent. Process this conversation and return a JSON object with exactly two keys:\n\n1. \"history_entry\": A paragraph (2-5 sentences) summarizing the key events/decisions/topics. Start with a timestamp like [YYYY-MM-DD HH:MM]. Include enough detail to be useful when found by grep search later.\n\n2. \"memory_update\": The updated long-term memory content. Add any new facts: user location, preferences, personal info, habits, project context, technical decisions, tools/services used. If nothing new, return the existing content unchanged.\n\n## Current Long-term Memory\n${currentMemory || \"(empty)\"}\n\n## Conversation to Process\n${conversation}\n\nRespond with ONLY valid JSON, no markdown fences.`;\n\n try {\n const response = await this.provider.chat({\n messages: [\n { role: \"system\", content: \"You are a memory consolidation agent. Respond only with valid JSON.\" },\n { role: \"user\", content: prompt },\n ],\n model: this.consolidationModel,\n });\n\n let text = (response.content || \"\").trim();\n if (text.startsWith(\"```\")) {\n text = text.split(\"\\n\").slice(1).join(\"\\n\").split(\"```\")[0].trim();\n }\n\n const result = JSON.parse(text);\n\n if (result.history_entry) {\n const entry = typeof result.history_entry === \"string\"\n ? result.history_entry\n : JSON.stringify(result.history_entry, null, 2);\n memory.appendHistory(entry);\n }\n if (result.memory_update && result.memory_update !== currentMemory) {\n const update = typeof result.memory_update === \"string\"\n ? result.memory_update\n : JSON.stringify(result.memory_update, null, 2);\n memory.writeLongTerm(update);\n }\n } catch (err) {\n console.error(\"Memory consolidation failed:\", err);\n // Fallback: append raw conversation to history so it's not lost\n const fallbackEntry = `[${new Date().toISOString().slice(0, 16)}] Consolidation failed, archiving raw messages:\\n${conversation.slice(0, 2000)}`;\n memory.appendHistory(fallbackEntry);\n }\n\n // Always trim session to prevent unbounded growth\n session.trimHistory(keepCount);\n this.sessions.save(session);\n console.log(`Memory consolidation done, session trimmed to ${session.history.length} messages`);\n }\n}\n\n/** Check if an error is an abort/cancellation error. */\nfunction isAbortError(err: unknown): boolean {\n if (err instanceof DOMException && err.name === \"AbortError\") return true;\n if (err instanceof Error) {\n if (err.name === \"AbortError\") return true;\n // OpenAI SDK wraps abort as APIUserAbortError\n if (err.name === \"APIUserAbortError\") return true;\n if (err.message.includes(\"abort\")) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"loop.mjs","names":[],"sources":["../../src/agent/loop.ts"],"sourcesContent":["import type { LLMProvider, ChatMessage, ToolCallRequest } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type {\n InboundMessage,\n OutboundMessage,\n} from \"../bus/events.js\";\nimport { createOutboundMessage } from \"../bus/events.js\";\nimport { ContextBuilder } from \"./context.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport {\n ReadFileTool,\n WriteFileTool,\n EditFileTool,\n ListDirTool,\n} from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport { MessageTool } from \"./tools/message.js\";\nimport { SpawnTool } from \"./tools/spawn.js\";\nimport { CronTool } from \"./tools/cron.js\";\nimport { FlexTool } from \"./tools/flex.js\";\nimport { SubagentManager } from \"./subagent.js\";\nimport { getBuiltinSkillsDir } from \"./skills.js\";\nimport { SessionManager, Session } from \"../session/manager.js\";\nimport { MemoryStore } from \"./memory.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\nimport type { Tool } from \"./tools/base.js\";\n\n/**\n * The agent loop: core processing engine.\n *\n * 1. Receives messages from the bus\n * 2. Builds context with history, memory, skills\n * 3. Calls the LLM\n * 4. Executes tool calls\n * 5. Sends responses back\n */\nexport class AgentLoop {\n private bus: MessageBus;\n private provider: LLMProvider;\n private workspace: string;\n private model: string;\n private consolidationModel: string;\n private maxTokens: number;\n private maxIterations: number;\n private memoryWindow: number;\n\n readonly context: ContextBuilder;\n readonly sessions: SessionManager;\n readonly tools: ToolRegistry;\n readonly subagents: SubagentManager;\n\n private _running = false;\n\n /** In-flight AbortControllers keyed by session key. */\n private inflight = new Map<string, AbortController>();\n\n constructor(params: {\n bus: MessageBus;\n provider: LLMProvider;\n workspace: string;\n model?: string;\n consolidationModel?: string;\n maxTokens?: number;\n maxIterations?: number;\n memoryWindow?: number;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n restrictToWorkspace?: boolean;\n toolsEnabled?: string[];\n toolsDisabled?: string[];\n customTools?: Tool[];\n }) {\n this.bus = params.bus;\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.consolidationModel = params.consolidationModel ?? this.model;\n this.maxTokens = params.maxTokens ?? 8192;\n this.maxIterations = params.maxIterations ?? 20;\n this.memoryWindow = params.memoryWindow ?? 50;\n\n const execConfig = params.execConfig ?? { timeout: 60 };\n const restrictToWorkspace = params.restrictToWorkspace ?? false;\n\n this.context = new ContextBuilder(params.workspace);\n this.sessions = new SessionManager(params.workspace);\n this.tools = new ToolRegistry();\n this.subagents = new SubagentManager({\n provider: params.provider,\n workspace: params.workspace,\n bus: params.bus,\n model: this.model,\n braveApiKey: params.braveApiKey,\n execConfig,\n restrictToWorkspace,\n });\n\n this.registerDefaultTools(\n execConfig,\n restrictToWorkspace,\n params.braveApiKey,\n params.toolsEnabled,\n params.toolsDisabled,\n params.customTools,\n );\n }\n\n private registerDefaultTools(\n execConfig: ExecToolConfig,\n restrictToWorkspace: boolean,\n braveApiKey?: string,\n toolsEnabled?: string[],\n toolsDisabled?: string[],\n customTools?: Tool[],\n ): void {\n const enabled = new Set(toolsEnabled ?? []);\n const disabled = new Set(toolsDisabled ?? []);\n const hasAllowlist = enabled.size > 0;\n const shouldRegister = (name: string): boolean =>\n (hasAllowlist ? enabled.has(name) : true) && !disabled.has(name);\n\n const registerIfEnabled = (tool: Tool): void => {\n if (shouldRegister(tool.name)) {\n this.tools.register(tool);\n }\n };\n\n // File tools — pass allowedDir when restrictToWorkspace is enabled\n const allowedDir = restrictToWorkspace ? this.workspace : undefined;\n const readOnlyPaths = restrictToWorkspace ? [getBuiltinSkillsDir()] : undefined;\n registerIfEnabled(new ReadFileTool({ allowedDir, readOnlyPaths }));\n registerIfEnabled(new WriteFileTool({ allowedDir }));\n registerIfEnabled(new EditFileTool({ allowedDir }));\n registerIfEnabled(new ListDirTool({ allowedDir, readOnlyPaths }));\n\n // Shell tool\n registerIfEnabled(\n new ExecTool({\n workingDir: this.workspace,\n timeout: execConfig.timeout,\n restrictToWorkspace,\n }),\n );\n\n // Web tools\n registerIfEnabled(new WebSearchTool({ apiKey: braveApiKey }));\n registerIfEnabled(new WebFetchTool());\n\n // Message tool\n const messageTool = new MessageTool({\n sendCallback: (msg) => this.bus.publishOutbound(msg),\n });\n registerIfEnabled(messageTool);\n\n // Spawn tool\n const spawnTool = new SpawnTool(this.subagents);\n registerIfEnabled(spawnTool);\n\n // Cron tool — always registered, uses DO scheduling via worker API\n registerIfEnabled(new CronTool());\n\n // Flex Message tool — builds LINE Flex JSON from structured data\n registerIfEnabled(new FlexTool());\n\n if (customTools && customTools.length > 0) {\n for (const tool of customTools) {\n registerIfEnabled(tool);\n }\n }\n }\n\n /** Run the agent loop, processing messages from the bus. */\n async run(): Promise<void> {\n this._running = true;\n console.log(\"Agent loop started\");\n\n while (this._running) {\n try {\n const msg = await this.bus.consumeInboundTimeout(1000);\n\n // Process concurrently so new messages can abort in-flight ones\n this.processMessage(msg)\n .then(async (response) => {\n if (response) {\n await this.bus.publishOutbound(response);\n }\n })\n .catch(async (err) => {\n if (isAbortError(err)) return; // Already handled\n console.error(\"Error processing message:\", err);\n await this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : err}`,\n }),\n );\n });\n } catch {\n // timeout, continue\n }\n }\n }\n\n /** Stop the agent loop. */\n stop(): void {\n this._running = false;\n console.log(\"Agent loop stopping\");\n }\n\n /** Process a single inbound message. */\n private async processMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n // Handle system messages (subagent announces)\n if (msg.channel === \"system\") {\n return this.processSystemMessage(msg);\n }\n\n console.log(`Processing message from ${msg.channel}:${msg.senderId}`);\n\n const sessionKey = `${msg.channel}:${msg.chatId}`;\n\n // Abort any in-flight request for this session\n const existing = this.inflight.get(sessionKey);\n if (existing) {\n console.log(`Aborting in-flight request for ${sessionKey}`);\n existing.abort();\n }\n\n // Create a new AbortController for this request\n const controller = new AbortController();\n this.inflight.set(sessionKey, controller);\n\n const session = this.sessions.getOrCreate(sessionKey);\n\n // Consolidate memory if session is too large\n if (session.history.length > this.memoryWindow) {\n await this.consolidateMemory(session);\n }\n\n // Update tool contexts\n this.updateToolContexts(msg.channel, msg.chatId);\n\n // Build initial messages\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n media: msg.media.length > 0 ? msg.media : undefined,\n channel: msg.channel,\n chatId: msg.chatId,\n });\n\n // The messages array is: [system, ...history, currentUser]\n // We want to save from the current user message onward (skip system + old history).\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen; // 1 for system prompt\n\n try {\n // Agent loop (mutates messages by appending assistant/tool messages)\n const result = await this.runAgentLoop(messages, controller.signal, (text) => {\n if (text && text.trim().length > 0) {\n this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: text,\n }),\n );\n }\n });\n\n // Save the new messages from this turn (user + all agent loop messages)\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: result.content,\n });\n } catch (err) {\n if (isAbortError(err)) {\n // Request was aborted because a new message arrived.\n // Save the user message to history so the next request has context,\n // but don't save any assistant response.\n const userMessages = messages.slice(newMsgStart).filter((m) => m.role === \"user\");\n if (userMessages.length > 0) {\n session.addTurnMessages(userMessages);\n this.sessions.save(session);\n }\n console.log(`Request aborted for ${sessionKey}, user message saved to history`);\n return null; // No response -- the new message will handle it\n }\n throw err; // Re-throw non-abort errors\n } finally {\n // Clean up if this is still our controller\n if (this.inflight.get(sessionKey) === controller) {\n this.inflight.delete(sessionKey);\n }\n }\n }\n\n private async processSystemMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n console.log(`Processing system message from ${msg.senderId}`);\n\n let originChannel: string;\n let originChatId: string;\n\n if (msg.chatId.includes(\":\")) {\n const [ch, id] = msg.chatId.split(\":\", 2);\n originChannel = ch;\n originChatId = id;\n } else {\n originChannel = \"cli\";\n originChatId = msg.chatId;\n }\n\n const sessionKey = `${originChannel}:${originChatId}`;\n const session = this.sessions.getOrCreate(sessionKey);\n\n this.updateToolContexts(originChannel, originChatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n channel: originChannel,\n chatId: originChatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const result = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: originChannel,\n chatId: originChatId,\n content: result.content,\n });\n }\n\n private async runAgentLoop(\n messages: ChatMessage[],\n signal?: AbortSignal,\n onToolCallText?: (text: string) => void,\n ): Promise<{ content: string; toolsUsed: string[] }> {\n let finalContent: string | null = null;\n let sentToolCallNotice = false;\n const toolsUsed: string[] = [];\n\n for (let i = 0; i < this.maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: this.tools.getDefinitions(),\n model: this.model,\n maxTokens: this.maxTokens,\n signal,\n });\n\n if (response.hasToolCalls) {\n // Send an interim message so the user knows we're working\n if (!sentToolCallNotice && onToolCallText) {\n const interimText = response.content?.trim()\n || this.getToolCallFallbackText(response.toolCalls);\n if (interimText) {\n onToolCallText(interimText);\n }\n sentToolCallNotice = true;\n }\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n this.context.addAssistantMessage(\n messages,\n response.content,\n toolCallDicts,\n );\n\n for (const tc of response.toolCalls) {\n toolsUsed.push(tc.name);\n console.log(`Tool: ${tc.name}(${JSON.stringify(tc.arguments)})`);\n\n // Detect skill reads\n if (tc.name === \"read_file\") {\n const path = String(tc.arguments?.path ?? \"\");\n const skillMatch = path.match(/skills\\/([^/]+)\\/SKILL\\.md$/);\n if (skillMatch) {\n console.log(`Skill activated: ${skillMatch[1]}`);\n }\n }\n\n const result = await this.tools.execute(tc.name, tc.arguments);\n this.context.addToolResult(messages, tc.id, tc.name, result);\n\n // Auto-send flex_message results so they get rendered as Flex Messages\n // regardless of whether the LLM echoes the JSON in its response\n if (tc.name === \"flex_message\" && onToolCallText && !result.startsWith(\"Error\")) {\n // Extract just the JSON part (before the instruction suffix)\n const jsonOnly = result.split(\"\\n\\n(\")[0];\n onToolCallText(jsonOnly);\n }\n }\n } else {\n finalContent = response.content;\n\n // If the LLM returned empty content after tool use, nudge it to respond\n if ((!finalContent || finalContent.trim().length === 0) && i > 0) {\n messages.push({\n role: \"assistant\",\n content: \"\",\n });\n messages.push({\n role: \"user\",\n content: \"(You used tools but didn't respond to the user. Please provide a brief response summarizing what you did.)\",\n });\n continue;\n }\n\n // Push the final assistant message so it gets persisted with the turn\n messages.push({ role: \"assistant\", content: finalContent ?? \"\" });\n break;\n }\n }\n\n if (!finalContent || finalContent.trim().length === 0) {\n finalContent = \"I've completed processing but have no response to give.\";\n }\n\n // If we exhausted iterations without a non-tool-call response, still persist the final text\n if (messages[messages.length - 1]?.role !== \"assistant\" || messages[messages.length - 1]?.content !== finalContent) {\n messages.push({ role: \"assistant\", content: finalContent });\n }\n\n return { content: finalContent, toolsUsed };\n }\n\n /** Generate a fallback interim message based on which tools are being called. */\n private getToolCallFallbackText(toolCalls: ToolCallRequest[]): string {\n const toolNames = toolCalls.map((tc) => tc.name);\n const fallbacks: Record<string, string> = {\n web_search: \"検索中...\",\n web_fetch: \"ページを読み込み中...\",\n spawn: \"サブエージェントを起動中...\",\n flex_message: \"カードを作成中...\",\n };\n for (const name of toolNames) {\n if (fallbacks[name]) return fallbacks[name];\n }\n return \"ちょっと待ってね...\";\n }\n\n private updateToolContexts(channel: string, chatId: string): void {\n const messageTool = this.tools.get(\"message\");\n if (messageTool instanceof MessageTool) {\n messageTool.setContext(channel, chatId);\n }\n\n const spawnTool = this.tools.get(\"spawn\");\n if (spawnTool instanceof SpawnTool) {\n spawnTool.setContext(channel, chatId);\n }\n\n const cronTool = this.tools.get(\"cron\");\n if (cronTool instanceof CronTool) {\n cronTool.setContext(channel, chatId);\n }\n }\n\n /** Process a message directly (for CLI or cron usage). */\n async processDirect(\n content: string,\n sessionKey = \"cli:direct\",\n channel = \"cli\",\n chatId = \"direct\",\n ): Promise<string> {\n // Use inline version of processMessage for direct calls\n const session = this.sessions.getOrCreate(sessionKey);\n this.updateToolContexts(channel, chatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: content,\n channel,\n chatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const result = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart), result.toolsUsed);\n this.sessions.save(session);\n\n return result.content;\n }\n\n /** Consolidate old messages into MEMORY.md + HISTORY.md, then trim session. */\n private async consolidateMemory(session: Session): Promise<void> {\n const memory = new MemoryStore(this.workspace);\n const keepCount = Math.min(10, Math.max(2, Math.floor(this.memoryWindow / 2)));\n const oldMessages = session.history.slice(0, -keepCount);\n if (oldMessages.length === 0) return;\n\n console.log(`Memory consolidation: ${session.history.length} messages, archiving ${oldMessages.length}, keeping ${keepCount}`);\n\n // Format messages for LLM\n const lines: string[] = [];\n for (const m of oldMessages) {\n const content = typeof m.content === \"string\" ? m.content : \"\";\n if (!content) continue;\n const role = m.role.toUpperCase();\n const tools = m.toolsUsed ? ` [tools: ${m.toolsUsed.join(\", \")}]` : \"\";\n const ts = m.timestamp ? `[${m.timestamp.slice(0, 16)}] ` : \"\";\n lines.push(`${ts}${role}${tools}: ${content}`);\n }\n const conversation = lines.join(\"\\n\");\n const currentMemory = memory.readLongTerm();\n\n const prompt = `You are a memory consolidation agent. Process this conversation and return a JSON object with exactly two keys:\n\n1. \"history_entry\": A paragraph (2-5 sentences) summarizing the key events/decisions/topics. Start with a timestamp like [YYYY-MM-DD HH:MM]. Include enough detail to be useful when found by grep search later.\n\n2. \"memory_update\": The updated long-term memory content. Add any new facts: user location, preferences, personal info, habits, project context, technical decisions, tools/services used. If nothing new, return the existing content unchanged.\n\n## Current Long-term Memory\n${currentMemory || \"(empty)\"}\n\n## Conversation to Process\n${conversation}\n\nRespond with ONLY valid JSON, no markdown fences.`;\n\n try {\n const response = await this.provider.chat({\n messages: [\n { role: \"system\", content: \"You are a memory consolidation agent. Respond only with valid JSON.\" },\n { role: \"user\", content: prompt },\n ],\n model: this.consolidationModel,\n });\n\n let text = (response.content || \"\").trim();\n if (text.startsWith(\"```\")) {\n text = text.split(\"\\n\").slice(1).join(\"\\n\").split(\"```\")[0].trim();\n }\n\n const result = JSON.parse(text);\n\n if (result.history_entry) {\n const entry = typeof result.history_entry === \"string\"\n ? result.history_entry\n : JSON.stringify(result.history_entry, null, 2);\n memory.appendHistory(entry);\n }\n if (result.memory_update && result.memory_update !== currentMemory) {\n const update = typeof result.memory_update === \"string\"\n ? result.memory_update\n : JSON.stringify(result.memory_update, null, 2);\n memory.writeLongTerm(update);\n }\n } catch (err) {\n console.error(\"Memory consolidation failed:\", err);\n // Fallback: append raw conversation to history so it's not lost\n const fallbackEntry = `[${new Date().toISOString().slice(0, 16)}] Consolidation failed, archiving raw messages:\\n${conversation.slice(0, 2000)}`;\n memory.appendHistory(fallbackEntry);\n }\n\n // Always trim session to prevent unbounded growth\n session.trimHistory(keepCount);\n this.sessions.save(session);\n console.log(`Memory consolidation done, session trimmed to ${session.history.length} messages`);\n }\n}\n\n/** Check if an error is an abort/cancellation error. */\nfunction isAbortError(err: unknown): boolean {\n if (err instanceof DOMException && err.name === \"AbortError\") return true;\n if (err instanceof Error) {\n if (err.name === \"AbortError\") return true;\n // OpenAI SDK wraps abort as APIUserAbortError\n if (err.name === \"APIUserAbortError\") return true;\n if (err.message.includes(\"abort\")) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flex.d.mts","names":[],"sources":["../../../src/agent/tools/flex.ts"],"mappings":";;;;;AASA;;;;;cAAa,QAAA,SAAiB,IAAA;EAAA,SACnB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;EAAA,
|
|
1
|
+
{"version":3,"file":"flex.d.mts","names":[],"sources":["../../../src/agent/tools/flex.ts"],"mappings":";;;;;AASA;;;;;cAAa,QAAA,SAAiB,IAAA;EAAA,SACnB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;EAAA,QAwCtC,YAAA;EAAA,QAgEA,aAAA;EAAA,QA4BA,kBAAA;EAAA,QAwCA,YAAA;EAAA,QA+CA,mBAAA;EAAA,QAiDA,cAAA;EAAA,QAkCA,WAAA;AAAA"}
|
|
@@ -171,7 +171,7 @@ var FlexTool = class extends Tool {
|
|
|
171
171
|
break;
|
|
172
172
|
default: return `Error: unknown template '${template}'. Use: fortune, info_card, action_buttons, receipt, morning_summary, hydration, custom.`;
|
|
173
173
|
}
|
|
174
|
-
return JSON.stringify(flex)
|
|
174
|
+
return `${JSON.stringify(flex)}\n\n(Card sent automatically. Do NOT repeat the JSON. Just respond naturally.)`;
|
|
175
175
|
} catch (err) {
|
|
176
176
|
return `Error building flex message: ${err instanceof Error ? err.message : err}`;
|
|
177
177
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flex.mjs","names":[],"sources":["../../../src/agent/tools/flex.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\n\n/**\n * Tool for building LINE Flex Messages from structured data.\n *\n * The LLM calls this tool with a template name and data object,\n * and the tool returns valid Flex JSON that line.ts parseMessage()\n * will detect and send as a Flex Message.\n */\nexport class FlexTool extends Tool {\n readonly name = \"flex_message\";\n readonly description =\n \"Build a LINE Flex Message from a template. Returns JSON that will be rendered as a rich card in LINE. Use this instead of outputting raw JSON.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n template: {\n type: \"string\",\n description:\n \"Template name. Available: fortune, info_card, action_buttons, receipt, morning_summary, hydration, custom.\",\n },\n data: {\n type: \"object\",\n description: \"Template-specific data. See each template for fields.\",\n properties: {\n // fortune\n sign: { type: \"string\", description: \"Zodiac sign with emoji (e.g. '♍ 乙女座')\" },\n stars: { type: \"integer\", description: \"1-5 star rating\" },\n message: { type: \"string\", description: \"Fortune message\" },\n lucky_color: { type: \"string\", description: \"Lucky color name\" },\n lucky_item: { type: \"string\", description: \"Lucky item name\" },\n // info_card\n title: { type: \"string\", description: \"Card title\" },\n body: { type: \"string\", description: \"Card body text\" },\n // action_buttons\n prompt: { type: \"string\", description: \"Question or prompt text\" },\n buttons: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n label: { type: \"string\" },\n text: { type: \"string\" },\n style: { type: \"string\", enum: [\"primary\", \"secondary\", \"link\"] },\n },\n },\n description: \"Button definitions\",\n },\n // receipt\n items: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n value: { type: \"string\" },\n },\n },\n description: \"Line items\",\n },\n total: { type: \"string\", description: \"Total amount\" },\n // morning_summary\n greeting: { type: \"string\", description: \"Greeting text (e.g. 'おはよう!')\" },\n date: { type: \"string\", description: \"Date string (e.g. '2月13日 木曜日')\" },\n weather: { type: \"string\", description: \"Weather info (e.g. '東京 12°C 曇り')\" },\n advice: { type: \"string\", description: \"Short advice (e.g. 'コートでOK')\" },\n schedule: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Schedule/reminder lines\",\n },\n header_color: { type: \"string\", description: \"Header background color hex (default: #1DB446)\" },\n // hydration\n current: { type: \"integer\", description: \"Current count (e.g. glasses drunk)\" },\n goal: { type: \"integer\", description: \"Goal count\" },\n unit: { type: \"string\", description: \"Unit label (default: 杯)\" },\n button_label: { type: \"string\", description: \"Button label (default: '飲んだ!')\" },\n button_text: { type: \"string\", description: \"Button message text (default: '水飲んだ')\" },\n // custom\n contents: {\n type: \"array\",\n description: \"Raw Flex body contents array for custom template\",\n },\n },\n },\n },\n required: [\"template\", \"data\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const template = String(args.template);\n const data = (args.data ?? {}) as Record<string, unknown>;\n\n try {\n let flex: Record<string, unknown>;\n\n switch (template) {\n case \"fortune\":\n flex = this.buildFortune(data);\n break;\n case \"info_card\":\n flex = this.buildInfoCard(data);\n break;\n case \"action_buttons\":\n flex = this.buildActionButtons(data);\n break;\n case \"receipt\":\n flex = this.buildReceipt(data);\n break;\n case \"morning_summary\":\n flex = this.buildMorningSummary(data);\n break;\n case \"hydration\":\n flex = this.buildHydration(data);\n break;\n case \"custom\":\n flex = this.buildCustom(data);\n break;\n default:\n return `Error: unknown template '${template}'. Use: fortune, info_card, action_buttons, receipt, morning_summary, hydration, custom.`;\n }\n\n return JSON.stringify(flex);\n } catch (err) {\n return `Error building flex message: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private buildFortune(data: Record<string, unknown>): Record<string, unknown> {\n const sign = String(data.sign ?? \"♈ 牡羊座\");\n const stars = Math.max(1, Math.min(5, Number(data.stars ?? 3)));\n const message = String(data.message ?? \"\");\n const luckyColor = data.lucky_color ? String(data.lucky_color) : null;\n const luckyItem = data.lucky_item ? String(data.lucky_item) : null;\n\n const starText = \"★\".repeat(stars) + \"☆\".repeat(5 - stars);\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: sign, weight: \"bold\", size: \"xl\" },\n { type: \"text\", text: \"今日の運勢\", size: \"sm\", color: \"#666666\" },\n { type: \"text\", text: starText, size: \"xxl\", margin: \"md\" },\n ];\n\n if (message) {\n contents.push({\n type: \"text\",\n text: message,\n wrap: true,\n margin: \"sm\",\n });\n }\n\n if (luckyColor || luckyItem) {\n contents.push({ type: \"separator\", margin: \"md\" });\n }\n\n if (luckyColor) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: \"ラッキーカラー\", size: \"sm\", color: \"#228B22\", flex: 0 },\n { type: \"text\", text: luckyColor, size: \"sm\", align: \"end\" },\n ],\n });\n }\n\n if (luckyItem) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: luckyColor ? \"sm\" : \"md\",\n contents: [\n { type: \"text\", text: \"ラッキーアイテム\", size: \"sm\", color: \"#228B22\", flex: 0 },\n { type: \"text\", text: luckyItem, size: \"sm\", align: \"end\" },\n ],\n });\n }\n\n return {\n type: \"bubble\",\n size: \"kilo\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n paddingAll: \"lg\",\n contents,\n },\n };\n }\n\n private buildInfoCard(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"お知らせ\");\n const body = String(data.body ?? \"\");\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: title, weight: \"bold\", size: \"lg\" },\n { type: \"separator\", margin: \"md\" },\n ];\n\n if (body) {\n contents.push({\n type: \"text\",\n text: body,\n wrap: true,\n margin: \"md\",\n });\n }\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents,\n },\n };\n }\n\n private buildActionButtons(data: Record<string, unknown>): Record<string, unknown> {\n const prompt = String(data.prompt ?? \"選択してください\");\n const buttons = (data.buttons ?? []) as Array<Record<string, unknown>>;\n\n if (buttons.length === 0) {\n // Default yes/no\n buttons.push(\n { label: \"はい\", text: \"はい\", style: \"primary\" },\n { label: \"いいえ\", text: \"いいえ\", style: \"secondary\" },\n );\n }\n\n const buttonContents = buttons.map((btn) => ({\n type: \"button\",\n style: String(btn.style ?? \"primary\"),\n action: {\n type: \"message\",\n label: String(btn.label ?? \"\"),\n text: String(btn.text ?? btn.label ?? \"\"),\n },\n }));\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [\n { type: \"text\", text: prompt, weight: \"bold\", wrap: true },\n ],\n },\n footer: {\n type: \"box\",\n layout: \"vertical\",\n spacing: \"sm\",\n contents: buttonContents,\n },\n };\n }\n\n private buildReceipt(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"明細\");\n const items = (data.items ?? []) as Array<Record<string, unknown>>;\n const total = data.total ? String(data.total) : null;\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: `💰 ${title}`, weight: \"bold\" },\n { type: \"separator\", margin: \"lg\" },\n ];\n\n for (const item of items) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: String(item.name ?? \"\"), flex: 0 },\n { type: \"text\", text: String(item.value ?? \"\"), align: \"end\" },\n ],\n });\n }\n\n if (total) {\n contents.push(\n { type: \"separator\", margin: \"lg\" },\n {\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: \"合計\", weight: \"bold\" },\n { type: \"text\", text: total, weight: \"bold\", align: \"end\" },\n ],\n },\n );\n }\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents,\n },\n };\n }\n\n private buildMorningSummary(data: Record<string, unknown>): Record<string, unknown> {\n const greeting = String(data.greeting ?? \"おはよう!\");\n const date = String(data.date ?? \"\");\n const weather = data.weather ? String(data.weather) : null;\n const advice = data.advice ? String(data.advice) : null;\n const schedule = (data.schedule ?? []) as string[];\n const headerColor = String(data.header_color ?? \"#1DB446\");\n\n const headerContents: Record<string, unknown>[] = [\n { type: \"text\", text: greeting, color: \"#ffffff\", weight: \"bold\", size: \"lg\" },\n ];\n if (date) {\n headerContents.push({ type: \"text\", text: date, color: \"#ffffff\", size: \"sm\" });\n }\n\n const bodyContents: Record<string, unknown>[] = [];\n\n if (weather) {\n bodyContents.push({ type: \"text\", text: weather, weight: \"bold\" });\n }\n if (advice) {\n bodyContents.push({ type: \"text\", text: advice, size: \"sm\", color: \"#666666\" });\n }\n\n if (schedule.length > 0) {\n bodyContents.push({ type: \"separator\", margin: \"md\" });\n for (const line of schedule) {\n bodyContents.push({ type: \"text\", text: line, margin: \"md\", wrap: true });\n }\n }\n\n return {\n type: \"bubble\",\n header: {\n type: \"box\",\n layout: \"vertical\",\n backgroundColor: headerColor,\n paddingAll: \"lg\",\n contents: headerContents,\n },\n body: {\n type: \"box\",\n layout: \"vertical\",\n paddingAll: \"lg\",\n contents: bodyContents.length > 0 ? bodyContents : [{ type: \"text\", text: \"良い一日を!\" }],\n },\n };\n }\n\n private buildHydration(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"水飲んだ?\");\n const current = Number(data.current ?? 0);\n const goal = Number(data.goal ?? 8);\n const unit = String(data.unit ?? \"杯\");\n const buttonLabel = String(data.button_label ?? \"飲んだ!\");\n const buttonText = String(data.button_text ?? \"水飲んだ\");\n\n return {\n type: \"bubble\",\n size: \"kilo\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [\n { type: \"text\", text: title, size: \"xl\" },\n { type: \"text\", text: `今日: ${current}${unit} / ${goal}${unit}`, color: \"#666666\" },\n ],\n },\n footer: {\n type: \"box\",\n layout: \"horizontal\",\n contents: [\n {\n type: \"button\",\n style: \"primary\",\n color: \"#00B9ED\",\n action: { type: \"message\", label: buttonLabel, text: buttonText },\n },\n ],\n },\n };\n }\n\n private buildCustom(data: Record<string, unknown>): Record<string, unknown> {\n // If raw contents array is provided, use it directly\n const rawContents = data.contents as unknown[];\n if (Array.isArray(rawContents) && rawContents.length > 0) {\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: rawContents,\n },\n };\n }\n\n // Fallback: auto-build a card from common fields (title, body, buttons, header_color)\n const title = data.title ? String(data.title) : null;\n const body = data.body ? String(data.body) : null;\n const buttons = Array.isArray(data.buttons) ? data.buttons as Array<Record<string, unknown>> : [];\n const headerColor = data.header_color ? String(data.header_color) : null;\n\n if (!title && !body && buttons.length === 0) {\n throw new Error(\"custom template requires 'contents' array, or at least one of: title, body, buttons\");\n }\n\n const bodyContents: Record<string, unknown>[] = [];\n\n if (title) {\n bodyContents.push({ type: \"text\", text: title, weight: \"bold\", size: \"lg\" });\n }\n if (body) {\n if (title) bodyContents.push({ type: \"separator\", margin: \"md\" });\n bodyContents.push({ type: \"text\", text: body, wrap: true, margin: title ? \"md\" : undefined });\n }\n\n const bubble: Record<string, unknown> = {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: bodyContents.length > 0 ? bodyContents : [{ type: \"text\", text: \" \" }],\n },\n };\n\n // Add colored header if specified\n if (headerColor && title) {\n bubble.header = {\n type: \"box\",\n layout: \"vertical\",\n backgroundColor: headerColor,\n paddingAll: \"lg\",\n contents: [\n { type: \"text\", text: title, color: \"#ffffff\", weight: \"bold\", size: \"lg\" },\n ],\n };\n // Remove title from body since it's in the header now\n bodyContents.shift();\n }\n\n // Add buttons as footer\n if (buttons.length > 0) {\n bubble.footer = {\n type: \"box\",\n layout: \"vertical\",\n spacing: \"sm\",\n contents: buttons.map((btn) => ({\n type: \"button\",\n style: String(btn.style ?? \"primary\"),\n action: {\n type: \"message\",\n label: String(btn.label ?? \"\"),\n text: String(btn.text ?? btn.label ?? \"\"),\n },\n })),\n };\n }\n\n return bubble;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"flex.mjs","names":[],"sources":["../../../src/agent/tools/flex.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\n\n/**\n * Tool for building LINE Flex Messages from structured data.\n *\n * The LLM calls this tool with a template name and data object,\n * and the tool returns valid Flex JSON that line.ts parseMessage()\n * will detect and send as a Flex Message.\n */\nexport class FlexTool extends Tool {\n readonly name = \"flex_message\";\n readonly description =\n \"Build a LINE Flex Message from a template. Returns JSON that will be rendered as a rich card in LINE. Use this instead of outputting raw JSON.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n template: {\n type: \"string\",\n description:\n \"Template name. Available: fortune, info_card, action_buttons, receipt, morning_summary, hydration, custom.\",\n },\n data: {\n type: \"object\",\n description: \"Template-specific data. See each template for fields.\",\n properties: {\n // fortune\n sign: { type: \"string\", description: \"Zodiac sign with emoji (e.g. '♍ 乙女座')\" },\n stars: { type: \"integer\", description: \"1-5 star rating\" },\n message: { type: \"string\", description: \"Fortune message\" },\n lucky_color: { type: \"string\", description: \"Lucky color name\" },\n lucky_item: { type: \"string\", description: \"Lucky item name\" },\n // info_card\n title: { type: \"string\", description: \"Card title\" },\n body: { type: \"string\", description: \"Card body text\" },\n // action_buttons\n prompt: { type: \"string\", description: \"Question or prompt text\" },\n buttons: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n label: { type: \"string\" },\n text: { type: \"string\" },\n style: { type: \"string\", enum: [\"primary\", \"secondary\", \"link\"] },\n },\n },\n description: \"Button definitions\",\n },\n // receipt\n items: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n value: { type: \"string\" },\n },\n },\n description: \"Line items\",\n },\n total: { type: \"string\", description: \"Total amount\" },\n // morning_summary\n greeting: { type: \"string\", description: \"Greeting text (e.g. 'おはよう!')\" },\n date: { type: \"string\", description: \"Date string (e.g. '2月13日 木曜日')\" },\n weather: { type: \"string\", description: \"Weather info (e.g. '東京 12°C 曇り')\" },\n advice: { type: \"string\", description: \"Short advice (e.g. 'コートでOK')\" },\n schedule: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Schedule/reminder lines\",\n },\n header_color: { type: \"string\", description: \"Header background color hex (default: #1DB446)\" },\n // hydration\n current: { type: \"integer\", description: \"Current count (e.g. glasses drunk)\" },\n goal: { type: \"integer\", description: \"Goal count\" },\n unit: { type: \"string\", description: \"Unit label (default: 杯)\" },\n button_label: { type: \"string\", description: \"Button label (default: '飲んだ!')\" },\n button_text: { type: \"string\", description: \"Button message text (default: '水飲んだ')\" },\n // custom\n contents: {\n type: \"array\",\n description: \"Raw Flex body contents array for custom template\",\n },\n },\n },\n },\n required: [\"template\", \"data\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const template = String(args.template);\n const data = (args.data ?? {}) as Record<string, unknown>;\n\n try {\n let flex: Record<string, unknown>;\n\n switch (template) {\n case \"fortune\":\n flex = this.buildFortune(data);\n break;\n case \"info_card\":\n flex = this.buildInfoCard(data);\n break;\n case \"action_buttons\":\n flex = this.buildActionButtons(data);\n break;\n case \"receipt\":\n flex = this.buildReceipt(data);\n break;\n case \"morning_summary\":\n flex = this.buildMorningSummary(data);\n break;\n case \"hydration\":\n flex = this.buildHydration(data);\n break;\n case \"custom\":\n flex = this.buildCustom(data);\n break;\n default:\n return `Error: unknown template '${template}'. Use: fortune, info_card, action_buttons, receipt, morning_summary, hydration, custom.`;\n }\n\n const json = JSON.stringify(flex);\n return `${json}\\n\\n(Card sent automatically. Do NOT repeat the JSON. Just respond naturally.)`;\n } catch (err) {\n return `Error building flex message: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private buildFortune(data: Record<string, unknown>): Record<string, unknown> {\n const sign = String(data.sign ?? \"♈ 牡羊座\");\n const stars = Math.max(1, Math.min(5, Number(data.stars ?? 3)));\n const message = String(data.message ?? \"\");\n const luckyColor = data.lucky_color ? String(data.lucky_color) : null;\n const luckyItem = data.lucky_item ? String(data.lucky_item) : null;\n\n const starText = \"★\".repeat(stars) + \"☆\".repeat(5 - stars);\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: sign, weight: \"bold\", size: \"xl\" },\n { type: \"text\", text: \"今日の運勢\", size: \"sm\", color: \"#666666\" },\n { type: \"text\", text: starText, size: \"xxl\", margin: \"md\" },\n ];\n\n if (message) {\n contents.push({\n type: \"text\",\n text: message,\n wrap: true,\n margin: \"sm\",\n });\n }\n\n if (luckyColor || luckyItem) {\n contents.push({ type: \"separator\", margin: \"md\" });\n }\n\n if (luckyColor) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: \"ラッキーカラー\", size: \"sm\", color: \"#228B22\", flex: 0 },\n { type: \"text\", text: luckyColor, size: \"sm\", align: \"end\" },\n ],\n });\n }\n\n if (luckyItem) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: luckyColor ? \"sm\" : \"md\",\n contents: [\n { type: \"text\", text: \"ラッキーアイテム\", size: \"sm\", color: \"#228B22\", flex: 0 },\n { type: \"text\", text: luckyItem, size: \"sm\", align: \"end\" },\n ],\n });\n }\n\n return {\n type: \"bubble\",\n size: \"kilo\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n paddingAll: \"lg\",\n contents,\n },\n };\n }\n\n private buildInfoCard(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"お知らせ\");\n const body = String(data.body ?? \"\");\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: title, weight: \"bold\", size: \"lg\" },\n { type: \"separator\", margin: \"md\" },\n ];\n\n if (body) {\n contents.push({\n type: \"text\",\n text: body,\n wrap: true,\n margin: \"md\",\n });\n }\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents,\n },\n };\n }\n\n private buildActionButtons(data: Record<string, unknown>): Record<string, unknown> {\n const prompt = String(data.prompt ?? \"選択してください\");\n const buttons = (data.buttons ?? []) as Array<Record<string, unknown>>;\n\n if (buttons.length === 0) {\n // Default yes/no\n buttons.push(\n { label: \"はい\", text: \"はい\", style: \"primary\" },\n { label: \"いいえ\", text: \"いいえ\", style: \"secondary\" },\n );\n }\n\n const buttonContents = buttons.map((btn) => ({\n type: \"button\",\n style: String(btn.style ?? \"primary\"),\n action: {\n type: \"message\",\n label: String(btn.label ?? \"\"),\n text: String(btn.text ?? btn.label ?? \"\"),\n },\n }));\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [\n { type: \"text\", text: prompt, weight: \"bold\", wrap: true },\n ],\n },\n footer: {\n type: \"box\",\n layout: \"vertical\",\n spacing: \"sm\",\n contents: buttonContents,\n },\n };\n }\n\n private buildReceipt(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"明細\");\n const items = (data.items ?? []) as Array<Record<string, unknown>>;\n const total = data.total ? String(data.total) : null;\n\n const contents: Record<string, unknown>[] = [\n { type: \"text\", text: `💰 ${title}`, weight: \"bold\" },\n { type: \"separator\", margin: \"lg\" },\n ];\n\n for (const item of items) {\n contents.push({\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: String(item.name ?? \"\"), flex: 0 },\n { type: \"text\", text: String(item.value ?? \"\"), align: \"end\" },\n ],\n });\n }\n\n if (total) {\n contents.push(\n { type: \"separator\", margin: \"lg\" },\n {\n type: \"box\",\n layout: \"horizontal\",\n margin: \"md\",\n contents: [\n { type: \"text\", text: \"合計\", weight: \"bold\" },\n { type: \"text\", text: total, weight: \"bold\", align: \"end\" },\n ],\n },\n );\n }\n\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents,\n },\n };\n }\n\n private buildMorningSummary(data: Record<string, unknown>): Record<string, unknown> {\n const greeting = String(data.greeting ?? \"おはよう!\");\n const date = String(data.date ?? \"\");\n const weather = data.weather ? String(data.weather) : null;\n const advice = data.advice ? String(data.advice) : null;\n const schedule = (data.schedule ?? []) as string[];\n const headerColor = String(data.header_color ?? \"#1DB446\");\n\n const headerContents: Record<string, unknown>[] = [\n { type: \"text\", text: greeting, color: \"#ffffff\", weight: \"bold\", size: \"lg\" },\n ];\n if (date) {\n headerContents.push({ type: \"text\", text: date, color: \"#ffffff\", size: \"sm\" });\n }\n\n const bodyContents: Record<string, unknown>[] = [];\n\n if (weather) {\n bodyContents.push({ type: \"text\", text: weather, weight: \"bold\" });\n }\n if (advice) {\n bodyContents.push({ type: \"text\", text: advice, size: \"sm\", color: \"#666666\" });\n }\n\n if (schedule.length > 0) {\n bodyContents.push({ type: \"separator\", margin: \"md\" });\n for (const line of schedule) {\n bodyContents.push({ type: \"text\", text: line, margin: \"md\", wrap: true });\n }\n }\n\n return {\n type: \"bubble\",\n header: {\n type: \"box\",\n layout: \"vertical\",\n backgroundColor: headerColor,\n paddingAll: \"lg\",\n contents: headerContents,\n },\n body: {\n type: \"box\",\n layout: \"vertical\",\n paddingAll: \"lg\",\n contents: bodyContents.length > 0 ? bodyContents : [{ type: \"text\", text: \"良い一日を!\" }],\n },\n };\n }\n\n private buildHydration(data: Record<string, unknown>): Record<string, unknown> {\n const title = String(data.title ?? \"水飲んだ?\");\n const current = Number(data.current ?? 0);\n const goal = Number(data.goal ?? 8);\n const unit = String(data.unit ?? \"杯\");\n const buttonLabel = String(data.button_label ?? \"飲んだ!\");\n const buttonText = String(data.button_text ?? \"水飲んだ\");\n\n return {\n type: \"bubble\",\n size: \"kilo\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [\n { type: \"text\", text: title, size: \"xl\" },\n { type: \"text\", text: `今日: ${current}${unit} / ${goal}${unit}`, color: \"#666666\" },\n ],\n },\n footer: {\n type: \"box\",\n layout: \"horizontal\",\n contents: [\n {\n type: \"button\",\n style: \"primary\",\n color: \"#00B9ED\",\n action: { type: \"message\", label: buttonLabel, text: buttonText },\n },\n ],\n },\n };\n }\n\n private buildCustom(data: Record<string, unknown>): Record<string, unknown> {\n // If raw contents array is provided, use it directly\n const rawContents = data.contents as unknown[];\n if (Array.isArray(rawContents) && rawContents.length > 0) {\n return {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: rawContents,\n },\n };\n }\n\n // Fallback: auto-build a card from common fields (title, body, buttons, header_color)\n const title = data.title ? String(data.title) : null;\n const body = data.body ? String(data.body) : null;\n const buttons = Array.isArray(data.buttons) ? data.buttons as Array<Record<string, unknown>> : [];\n const headerColor = data.header_color ? String(data.header_color) : null;\n\n if (!title && !body && buttons.length === 0) {\n throw new Error(\"custom template requires 'contents' array, or at least one of: title, body, buttons\");\n }\n\n const bodyContents: Record<string, unknown>[] = [];\n\n if (title) {\n bodyContents.push({ type: \"text\", text: title, weight: \"bold\", size: \"lg\" });\n }\n if (body) {\n if (title) bodyContents.push({ type: \"separator\", margin: \"md\" });\n bodyContents.push({ type: \"text\", text: body, wrap: true, margin: title ? \"md\" : undefined });\n }\n\n const bubble: Record<string, unknown> = {\n type: \"bubble\",\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: bodyContents.length > 0 ? bodyContents : [{ type: \"text\", text: \" \" }],\n },\n };\n\n // Add colored header if specified\n if (headerColor && title) {\n bubble.header = {\n type: \"box\",\n layout: \"vertical\",\n backgroundColor: headerColor,\n paddingAll: \"lg\",\n contents: [\n { type: \"text\", text: title, color: \"#ffffff\", weight: \"bold\", size: \"lg\" },\n ],\n };\n // Remove title from body since it's in the header now\n bodyContents.shift();\n }\n\n // Add buttons as footer\n if (buttons.length > 0) {\n bubble.footer = {\n type: \"box\",\n layout: \"vertical\",\n spacing: \"sm\",\n contents: buttons.map((btn) => ({\n type: \"button\",\n style: String(btn.style ?? \"primary\"),\n action: {\n type: \"message\",\n label: String(btn.label ?? \"\"),\n text: String(btn.text ?? btn.label ?? \"\"),\n },\n })),\n };\n }\n\n return bubble;\n }\n}\n"],"mappings":"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"}
|
|
@@ -5,11 +5,11 @@ import { globalExpect } from "../../node_modules/vitest/dist/chunks/vi.DgezovHB.
|
|
|
5
5
|
//#region src/agent/tools/flex.test.ts
|
|
6
6
|
const tool = new FlexTool();
|
|
7
7
|
async function exec(template, data = {}) {
|
|
8
|
-
const
|
|
8
|
+
const jsonPart = (await tool.execute({
|
|
9
9
|
template,
|
|
10
10
|
data
|
|
11
|
-
});
|
|
12
|
-
return JSON.parse(
|
|
11
|
+
})).split("\n\n(")[0];
|
|
12
|
+
return JSON.parse(jsonPart);
|
|
13
13
|
}
|
|
14
14
|
async function execRaw(template, data = {}) {
|
|
15
15
|
return tool.execute({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flex.test.mjs","names":[],"sources":["../../../src/agent/tools/flex.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { FlexTool } from \"./flex.js\";\n\nconst tool = new FlexTool();\n\n// Helper: execute and parse result as JSON\nasync function exec(template: string, data: Record<string, unknown> = {}) {\n const result = await tool.execute({ template, data });\n return JSON.parse(result);\n}\n\n// Helper: execute and return raw string (for error cases)\nasync function execRaw(template: string, data: Record<string, unknown> = {}) {\n return tool.execute({ template, data });\n}\n\n// ---------------------------------------------------------------------------\n// fortune\n// ---------------------------------------------------------------------------\ndescribe(\"fortune\", () => {\n it(\"builds with defaults\", async () => {\n const flex = await exec(\"fortune\", { sign: \"♍ 乙女座\", stars: 4, message: \"良い日\" });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.size).toBe(\"kilo\");\n\n const body = flex.body;\n expect(body.type).toBe(\"box\");\n // sign, 今日の運勢, stars, message\n expect(body.contents.length).toBeGreaterThanOrEqual(4);\n expect(body.contents[0].text).toBe(\"♍ 乙女座\");\n expect(body.contents[2].text).toBe(\"★★★★☆\");\n expect(body.contents[3].text).toBe(\"良い日\");\n });\n\n it(\"clamps stars to 1-5\", async () => {\n const low = await exec(\"fortune\", { stars: -10 });\n expect(low.body.contents[2].text).toBe(\"★☆☆☆☆\");\n\n const high = await exec(\"fortune\", { stars: 99 });\n expect(high.body.contents[2].text).toBe(\"★★★★★\");\n });\n\n it(\"includes lucky color and item\", async () => {\n const flex = await exec(\"fortune\", {\n sign: \"♈ 牡羊座\",\n stars: 3,\n lucky_color: \"赤\",\n lucky_item: \"傘\",\n });\n const contents = flex.body.contents;\n // Should have separator + color row + item row\n const colorRow = contents.find(\n (c: Record<string, unknown>) =>\n c.type === \"box\" &&\n Array.isArray(c.contents) &&\n (c.contents as Record<string, unknown>[]).some((t) => t.text === \"ラッキーカラー\"),\n );\n expect(colorRow).toBeTruthy();\n\n const itemRow = contents.find(\n (c: Record<string, unknown>) =>\n c.type === \"box\" &&\n Array.isArray(c.contents) &&\n (c.contents as Record<string, unknown>[]).some((t) => t.text === \"ラッキーアイテム\"),\n );\n expect(itemRow).toBeTruthy();\n });\n\n it(\"omits lucky fields when not provided\", async () => {\n const flex = await exec(\"fortune\", { stars: 5 });\n const contents = flex.body.contents;\n const hasSeparator = contents.some((c: Record<string, unknown>) => c.type === \"separator\");\n expect(hasSeparator).toBe(false);\n });\n});\n\n// ---------------------------------------------------------------------------\n// info_card\n// ---------------------------------------------------------------------------\ndescribe(\"info_card\", () => {\n it(\"builds with title and body\", async () => {\n const flex = await exec(\"info_card\", { title: \"テスト\", body: \"本文テキスト\" });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n expect(contents[0].text).toBe(\"テスト\");\n expect(contents[1].type).toBe(\"separator\");\n expect(contents[2].text).toBe(\"本文テキスト\");\n });\n\n it(\"uses defaults when no data\", async () => {\n const flex = await exec(\"info_card\", {});\n expect(flex.body.contents[0].text).toBe(\"お知らせ\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// action_buttons\n// ---------------------------------------------------------------------------\ndescribe(\"action_buttons\", () => {\n it(\"builds with custom buttons\", async () => {\n const flex = await exec(\"action_buttons\", {\n prompt: \"好きな色は?\",\n buttons: [\n { label: \"赤\", text: \"赤が好き\", style: \"primary\" },\n { label: \"青\", text: \"青が好き\", style: \"secondary\" },\n ],\n });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.body.contents[0].text).toBe(\"好きな色は?\");\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"赤\");\n expect(flex.footer.contents[1].action.text).toBe(\"青が好き\");\n });\n\n it(\"adds default yes/no buttons when empty\", async () => {\n const flex = await exec(\"action_buttons\", { prompt: \"OK?\" });\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"はい\");\n expect(flex.footer.contents[1].action.label).toBe(\"いいえ\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// receipt\n// ---------------------------------------------------------------------------\ndescribe(\"receipt\", () => {\n it(\"builds with items and total\", async () => {\n const flex = await exec(\"receipt\", {\n title: \"ランチ\",\n items: [\n { name: \"ラーメン\", value: \"¥900\" },\n { name: \"餃子\", value: \"¥400\" },\n ],\n total: \"¥1,300\",\n });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n // title, separator, 2 items, separator, total\n expect(contents.length).toBe(6);\n expect(contents[0].text).toContain(\"ランチ\");\n // total row\n const totalRow = contents[5] as Record<string, unknown>;\n expect((totalRow.contents as Record<string, unknown>[])[1].text).toBe(\"¥1,300\");\n });\n\n it(\"omits total when not provided\", async () => {\n const flex = await exec(\"receipt\", {\n items: [{ name: \"Item\", value: \"100\" }],\n });\n // title, separator, 1 item = 3 contents (no total separator or total row)\n expect(flex.body.contents.length).toBe(3);\n });\n});\n\n// ---------------------------------------------------------------------------\n// morning_summary\n// ---------------------------------------------------------------------------\ndescribe(\"morning_summary\", () => {\n it(\"builds with all fields\", async () => {\n const flex = await exec(\"morning_summary\", {\n greeting: \"おはよう!\",\n date: \"2月13日 木曜日\",\n weather: \"東京 12°C 曇り\",\n advice: \"コートでOK\",\n schedule: [\"10:00 ミーティング\", \"14:00 歯医者\"],\n header_color: \"#FF6B6B\",\n });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.header.backgroundColor).toBe(\"#FF6B6B\");\n expect(flex.header.contents[0].text).toBe(\"おはよう!\");\n expect(flex.header.contents[1].text).toBe(\"2月13日 木曜日\");\n\n const body = flex.body.contents;\n expect(body[0].text).toBe(\"東京 12°C 曇り\");\n expect(body[1].text).toBe(\"コートでOK\");\n // separator + 2 schedule items\n expect(body.length).toBe(5);\n });\n\n it(\"uses default header color\", async () => {\n const flex = await exec(\"morning_summary\", {});\n expect(flex.header.backgroundColor).toBe(\"#1DB446\");\n });\n\n it(\"shows fallback body when no weather/schedule\", async () => {\n const flex = await exec(\"morning_summary\", { greeting: \"やぁ\" });\n expect(flex.body.contents[0].text).toBe(\"良い一日を!\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// hydration\n// ---------------------------------------------------------------------------\ndescribe(\"hydration\", () => {\n it(\"builds with current/goal\", async () => {\n const flex = await exec(\"hydration\", { current: 3, goal: 8 });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.size).toBe(\"kilo\");\n expect(flex.body.contents[0].text).toBe(\"水飲んだ?\");\n expect(flex.body.contents[1].text).toBe(\"今日: 3杯 / 8杯\");\n expect(flex.footer.contents[0].action.label).toBe(\"飲んだ!\");\n });\n\n it(\"uses custom unit and button text\", async () => {\n const flex = await exec(\"hydration\", {\n title: \"Coffee\",\n current: 2,\n goal: 4,\n unit: \"cups\",\n button_label: \"Had one!\",\n button_text: \"drank coffee\",\n });\n expect(flex.body.contents[0].text).toBe(\"Coffee\");\n expect(flex.body.contents[1].text).toBe(\"今日: 2cups / 4cups\");\n expect(flex.footer.contents[0].action.label).toBe(\"Had one!\");\n expect(flex.footer.contents[0].action.text).toBe(\"drank coffee\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// custom\n// ---------------------------------------------------------------------------\ndescribe(\"custom\", () => {\n it(\"uses raw contents array when provided\", async () => {\n const contents = [\n { type: \"text\", text: \"Hello\" },\n { type: \"text\", text: \"World\" },\n ];\n const flex = await exec(\"custom\", { contents });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.body.contents).toEqual(contents);\n });\n\n it(\"auto-builds card from title + body\", async () => {\n const flex = await exec(\"custom\", { title: \"Title\", body: \"Body text\" });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n expect(contents[0].text).toBe(\"Title\");\n expect(contents[1].type).toBe(\"separator\");\n expect(contents[2].text).toBe(\"Body text\");\n });\n\n it(\"auto-builds card with colored header\", async () => {\n const flex = await exec(\"custom\", {\n title: \"Workout\",\n body: \"Great session!\",\n header_color: \"#FF0000\",\n });\n expect(flex.header).toBeTruthy();\n expect(flex.header.backgroundColor).toBe(\"#FF0000\");\n expect(flex.header.contents[0].text).toBe(\"Workout\");\n // Title should be removed from body since it's in the header\n const bodyTexts = flex.body.contents.filter(\n (c: Record<string, unknown>) => c.type === \"text\",\n );\n expect(bodyTexts.every((t: Record<string, unknown>) => t.text !== \"Workout\")).toBe(true);\n });\n\n it(\"auto-builds card with buttons\", async () => {\n const flex = await exec(\"custom\", {\n title: \"Choose\",\n buttons: [\n { label: \"Option A\", text: \"A\", style: \"primary\" },\n { label: \"Option B\", text: \"B\", style: \"secondary\" },\n ],\n });\n expect(flex.footer).toBeTruthy();\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"Option A\");\n });\n\n it(\"errors when no contents, title, body, or buttons\", async () => {\n const result = await execRaw(\"custom\", {});\n expect(result).toContain(\"Error\");\n expect(result).toContain(\"custom template requires\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// unknown template\n// ---------------------------------------------------------------------------\ndescribe(\"unknown template\", () => {\n it(\"returns error for unknown template\", async () => {\n const result = await execRaw(\"nonexistent\", {});\n expect(result).toContain(\"Error: unknown template\");\n expect(result).toContain(\"nonexistent\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// tool metadata\n// ---------------------------------------------------------------------------\ndescribe(\"tool metadata\", () => {\n it(\"has correct name and description\", () => {\n expect(tool.name).toBe(\"flex_message\");\n expect(tool.description).toContain(\"Flex Message\");\n });\n\n it(\"generates valid tool definition\", () => {\n const def = tool.getDefinition();\n expect(def.type).toBe(\"function\");\n expect(def.function.name).toBe(\"flex_message\");\n expect(def.function.parameters.required).toContain(\"template\");\n expect(def.function.parameters.required).toContain(\"data\");\n });\n});\n"],"mappings":";;;;;AAGA,MAAM,OAAO,IAAI,UAAU;AAG3B,eAAe,KAAK,UAAkB,OAAgC,EAAE,EAAE;CACxE,MAAM,SAAS,MAAM,KAAK,QAAQ;EAAE;EAAU;EAAM,CAAC;AACrD,QAAO,KAAK,MAAM,OAAO;;AAI3B,eAAe,QAAQ,UAAkB,OAAgC,EAAE,EAAE;AAC3E,QAAO,KAAK,QAAQ;EAAE;EAAU;EAAM,CAAC;;AAMzC,SAAS,iBAAiB;AACxB,IAAG,wBAAwB,YAAY;EACrC,MAAM,OAAO,MAAM,KAAK,WAAW;GAAE,MAAM;GAAS,OAAO;GAAG,SAAS;GAAO,CAAC;AAC/E,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;AAChC,eAAO,KAAK,KAAK,CAAC,KAAK,OAAO;EAE9B,MAAM,OAAO,KAAK;AAClB,eAAO,KAAK,KAAK,CAAC,KAAK,MAAM;AAE7B,eAAO,KAAK,SAAS,OAAO,CAAC,uBAAuB,EAAE;AACtD,eAAO,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AAC3C,eAAO,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AAC3C,eAAO,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,MAAM;GACzC;AAEF,IAAG,uBAAuB,YAAY;AAEpC,gBADY,MAAM,KAAK,WAAW,EAAE,OAAO,KAAK,CAAC,EACtC,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AAG/C,gBADa,MAAM,KAAK,WAAW,EAAE,OAAO,IAAI,CAAC,EACrC,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;GAChD;AAEF,IAAG,iCAAiC,YAAY;EAO9C,MAAM,YANO,MAAM,KAAK,WAAW;GACjC,MAAM;GACN,OAAO;GACP,aAAa;GACb,YAAY;GACb,CAAC,EACoB,KAAK;AAQ3B,eANiB,SAAS,MACvB,MACC,EAAE,SAAS,SACX,MAAM,QAAQ,EAAE,SAAS,IACxB,EAAE,SAAuC,MAAM,MAAM,EAAE,SAAS,UAAU,CAC9E,CACe,CAAC,YAAY;AAQ7B,eANgB,SAAS,MACtB,MACC,EAAE,SAAS,SACX,MAAM,QAAQ,EAAE,SAAS,IACxB,EAAE,SAAuC,MAAM,MAAM,EAAE,SAAS,WAAW,CAC/E,CACc,CAAC,YAAY;GAC5B;AAEF,IAAG,wCAAwC,YAAY;AAIrD,gBAHa,MAAM,KAAK,WAAW,EAAE,OAAO,GAAG,CAAC,EAC1B,KAAK,SACG,MAAM,MAA+B,EAAE,SAAS,YAAY,CACtE,CAAC,KAAK,MAAM;GAChC;EACF;AAKF,SAAS,mBAAmB;AAC1B,IAAG,8BAA8B,YAAY;EAC3C,MAAM,OAAO,MAAM,KAAK,aAAa;GAAE,OAAO;GAAO,MAAM;GAAU,CAAC;AACtE,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;EAChC,MAAM,WAAW,KAAK,KAAK;AAC3B,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,MAAM;AACpC,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,YAAY;AAC1C,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS;GACvC;AAEF,IAAG,8BAA8B,YAAY;AAE3C,gBADa,MAAM,KAAK,aAAa,EAAE,CAAC,EAC5B,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,OAAO;GAC/C;EACF;AAKF,SAAS,wBAAwB;AAC/B,IAAG,8BAA8B,YAAY;EAC3C,MAAM,OAAO,MAAM,KAAK,kBAAkB;GACxC,QAAQ;GACR,SAAS,CACP;IAAE,OAAO;IAAK,MAAM;IAAQ,OAAO;IAAW,EAC9C;IAAE,OAAO;IAAK,MAAM;IAAQ,OAAO;IAAa,CACjD;GACF,CAAC;AACF,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;AAChC,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS;AACjD,eAAO,KAAK,OAAO,SAAS,CAAC,aAAa,EAAE;AAC5C,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,IAAI;AACtD,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,KAAK,CAAC,KAAK,OAAO;GACxD;AAEF,IAAG,0CAA0C,YAAY;EACvD,MAAM,OAAO,MAAM,KAAK,kBAAkB,EAAE,QAAQ,OAAO,CAAC;AAC5D,eAAO,KAAK,OAAO,SAAS,CAAC,aAAa,EAAE;AAC5C,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,KAAK;AACvD,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,MAAM;GACxD;EACF;AAKF,SAAS,iBAAiB;AACxB,IAAG,+BAA+B,YAAY;EAC5C,MAAM,OAAO,MAAM,KAAK,WAAW;GACjC,OAAO;GACP,OAAO,CACL;IAAE,MAAM;IAAQ,OAAO;IAAQ,EAC/B;IAAE,MAAM;IAAM,OAAO;IAAQ,CAC9B;GACD,OAAO;GACR,CAAC;AACF,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;EAChC,MAAM,WAAW,KAAK,KAAK;AAE3B,eAAO,SAAS,OAAO,CAAC,KAAK,EAAE;AAC/B,eAAO,SAAS,GAAG,KAAK,CAAC,UAAU,MAAM;EAEzC,MAAM,WAAW,SAAS;AAC1B,eAAQ,SAAS,SAAuC,GAAG,KAAK,CAAC,KAAK,SAAS;GAC/E;AAEF,IAAG,iCAAiC,YAAY;AAK9C,gBAJa,MAAM,KAAK,WAAW,EACjC,OAAO,CAAC;GAAE,MAAM;GAAQ,OAAO;GAAO,CAAC,EACxC,CAAC,EAEU,KAAK,SAAS,OAAO,CAAC,KAAK,EAAE;GACzC;EACF;AAKF,SAAS,yBAAyB;AAChC,IAAG,0BAA0B,YAAY;EACvC,MAAM,OAAO,MAAM,KAAK,mBAAmB;GACzC,UAAU;GACV,MAAM;GACN,SAAS;GACT,QAAQ;GACR,UAAU,CAAC,gBAAgB,YAAY;GACvC,cAAc;GACf,CAAC;AACF,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;AAChC,eAAO,KAAK,OAAO,gBAAgB,CAAC,KAAK,UAAU;AACnD,eAAO,KAAK,OAAO,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AAClD,eAAO,KAAK,OAAO,SAAS,GAAG,KAAK,CAAC,KAAK,YAAY;EAEtD,MAAM,OAAO,KAAK,KAAK;AACvB,eAAO,KAAK,GAAG,KAAK,CAAC,KAAK,aAAa;AACvC,eAAO,KAAK,GAAG,KAAK,CAAC,KAAK,SAAS;AAEnC,eAAO,KAAK,OAAO,CAAC,KAAK,EAAE;GAC3B;AAEF,IAAG,6BAA6B,YAAY;AAE1C,gBADa,MAAM,KAAK,mBAAmB,EAAE,CAAC,EAClC,OAAO,gBAAgB,CAAC,KAAK,UAAU;GACnD;AAEF,IAAG,gDAAgD,YAAY;AAE7D,gBADa,MAAM,KAAK,mBAAmB,EAAE,UAAU,MAAM,CAAC,EAClD,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS;GACjD;EACF;AAKF,SAAS,mBAAmB;AAC1B,IAAG,4BAA4B,YAAY;EACzC,MAAM,OAAO,MAAM,KAAK,aAAa;GAAE,SAAS;GAAG,MAAM;GAAG,CAAC;AAC7D,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;AAChC,eAAO,KAAK,KAAK,CAAC,KAAK,OAAO;AAC9B,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AAChD,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,cAAc;AACtD,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,OAAO;GACzD;AAEF,IAAG,oCAAoC,YAAY;EACjD,MAAM,OAAO,MAAM,KAAK,aAAa;GACnC,OAAO;GACP,SAAS;GACT,MAAM;GACN,MAAM;GACN,cAAc;GACd,aAAa;GACd,CAAC;AACF,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS;AACjD,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,oBAAoB;AAC5D,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,WAAW;AAC7D,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,KAAK,CAAC,KAAK,eAAe;GAChE;EACF;AAKF,SAAS,gBAAgB;AACvB,IAAG,yCAAyC,YAAY;EACtD,MAAM,WAAW,CACf;GAAE,MAAM;GAAQ,MAAM;GAAS,EAC/B;GAAE,MAAM;GAAQ,MAAM;GAAS,CAChC;EACD,MAAM,OAAO,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAC/C,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;AAChC,eAAO,KAAK,KAAK,SAAS,CAAC,QAAQ,SAAS;GAC5C;AAEF,IAAG,sCAAsC,YAAY;EACnD,MAAM,OAAO,MAAM,KAAK,UAAU;GAAE,OAAO;GAAS,MAAM;GAAa,CAAC;AACxE,eAAO,KAAK,KAAK,CAAC,KAAK,SAAS;EAChC,MAAM,WAAW,KAAK,KAAK;AAC3B,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,QAAQ;AACtC,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,YAAY;AAC1C,eAAO,SAAS,GAAG,KAAK,CAAC,KAAK,YAAY;GAC1C;AAEF,IAAG,wCAAwC,YAAY;EACrD,MAAM,OAAO,MAAM,KAAK,UAAU;GAChC,OAAO;GACP,MAAM;GACN,cAAc;GACf,CAAC;AACF,eAAO,KAAK,OAAO,CAAC,YAAY;AAChC,eAAO,KAAK,OAAO,gBAAgB,CAAC,KAAK,UAAU;AACnD,eAAO,KAAK,OAAO,SAAS,GAAG,KAAK,CAAC,KAAK,UAAU;AAKpD,eAHkB,KAAK,KAAK,SAAS,QAClC,MAA+B,EAAE,SAAS,OAC5C,CACgB,OAAO,MAA+B,EAAE,SAAS,UAAU,CAAC,CAAC,KAAK,KAAK;GACxF;AAEF,IAAG,iCAAiC,YAAY;EAC9C,MAAM,OAAO,MAAM,KAAK,UAAU;GAChC,OAAO;GACP,SAAS,CACP;IAAE,OAAO;IAAY,MAAM;IAAK,OAAO;IAAW,EAClD;IAAE,OAAO;IAAY,MAAM;IAAK,OAAO;IAAa,CACrD;GACF,CAAC;AACF,eAAO,KAAK,OAAO,CAAC,YAAY;AAChC,eAAO,KAAK,OAAO,SAAS,CAAC,aAAa,EAAE;AAC5C,eAAO,KAAK,OAAO,SAAS,GAAG,OAAO,MAAM,CAAC,KAAK,WAAW;GAC7D;AAEF,IAAG,oDAAoD,YAAY;EACjE,MAAM,SAAS,MAAM,QAAQ,UAAU,EAAE,CAAC;AAC1C,eAAO,OAAO,CAAC,UAAU,QAAQ;AACjC,eAAO,OAAO,CAAC,UAAU,2BAA2B;GACpD;EACF;AAKF,SAAS,0BAA0B;AACjC,IAAG,sCAAsC,YAAY;EACnD,MAAM,SAAS,MAAM,QAAQ,eAAe,EAAE,CAAC;AAC/C,eAAO,OAAO,CAAC,UAAU,0BAA0B;AACnD,eAAO,OAAO,CAAC,UAAU,cAAc;GACvC;EACF;AAKF,SAAS,uBAAuB;AAC9B,IAAG,0CAA0C;AAC3C,eAAO,KAAK,KAAK,CAAC,KAAK,eAAe;AACtC,eAAO,KAAK,YAAY,CAAC,UAAU,eAAe;GAClD;AAEF,IAAG,yCAAyC;EAC1C,MAAM,MAAM,KAAK,eAAe;AAChC,eAAO,IAAI,KAAK,CAAC,KAAK,WAAW;AACjC,eAAO,IAAI,SAAS,KAAK,CAAC,KAAK,eAAe;AAC9C,eAAO,IAAI,SAAS,WAAW,SAAS,CAAC,UAAU,WAAW;AAC9D,eAAO,IAAI,SAAS,WAAW,SAAS,CAAC,UAAU,OAAO;GAC1D;EACF"}
|
|
1
|
+
{"version":3,"file":"flex.test.mjs","names":[],"sources":["../../../src/agent/tools/flex.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { FlexTool } from \"./flex.js\";\n\nconst tool = new FlexTool();\n\n// Helper: execute and parse result as JSON (strip instruction suffix)\nasync function exec(template: string, data: Record<string, unknown> = {}) {\n const result = await tool.execute({ template, data });\n const jsonPart = result.split(\"\\n\\n(\")[0];\n return JSON.parse(jsonPart);\n}\n\n// Helper: execute and return raw string (for error cases)\nasync function execRaw(template: string, data: Record<string, unknown> = {}) {\n return tool.execute({ template, data });\n}\n\n// ---------------------------------------------------------------------------\n// fortune\n// ---------------------------------------------------------------------------\ndescribe(\"fortune\", () => {\n it(\"builds with defaults\", async () => {\n const flex = await exec(\"fortune\", { sign: \"♍ 乙女座\", stars: 4, message: \"良い日\" });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.size).toBe(\"kilo\");\n\n const body = flex.body;\n expect(body.type).toBe(\"box\");\n // sign, 今日の運勢, stars, message\n expect(body.contents.length).toBeGreaterThanOrEqual(4);\n expect(body.contents[0].text).toBe(\"♍ 乙女座\");\n expect(body.contents[2].text).toBe(\"★★★★☆\");\n expect(body.contents[3].text).toBe(\"良い日\");\n });\n\n it(\"clamps stars to 1-5\", async () => {\n const low = await exec(\"fortune\", { stars: -10 });\n expect(low.body.contents[2].text).toBe(\"★☆☆☆☆\");\n\n const high = await exec(\"fortune\", { stars: 99 });\n expect(high.body.contents[2].text).toBe(\"★★★★★\");\n });\n\n it(\"includes lucky color and item\", async () => {\n const flex = await exec(\"fortune\", {\n sign: \"♈ 牡羊座\",\n stars: 3,\n lucky_color: \"赤\",\n lucky_item: \"傘\",\n });\n const contents = flex.body.contents;\n // Should have separator + color row + item row\n const colorRow = contents.find(\n (c: Record<string, unknown>) =>\n c.type === \"box\" &&\n Array.isArray(c.contents) &&\n (c.contents as Record<string, unknown>[]).some((t) => t.text === \"ラッキーカラー\"),\n );\n expect(colorRow).toBeTruthy();\n\n const itemRow = contents.find(\n (c: Record<string, unknown>) =>\n c.type === \"box\" &&\n Array.isArray(c.contents) &&\n (c.contents as Record<string, unknown>[]).some((t) => t.text === \"ラッキーアイテム\"),\n );\n expect(itemRow).toBeTruthy();\n });\n\n it(\"omits lucky fields when not provided\", async () => {\n const flex = await exec(\"fortune\", { stars: 5 });\n const contents = flex.body.contents;\n const hasSeparator = contents.some((c: Record<string, unknown>) => c.type === \"separator\");\n expect(hasSeparator).toBe(false);\n });\n});\n\n// ---------------------------------------------------------------------------\n// info_card\n// ---------------------------------------------------------------------------\ndescribe(\"info_card\", () => {\n it(\"builds with title and body\", async () => {\n const flex = await exec(\"info_card\", { title: \"テスト\", body: \"本文テキスト\" });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n expect(contents[0].text).toBe(\"テスト\");\n expect(contents[1].type).toBe(\"separator\");\n expect(contents[2].text).toBe(\"本文テキスト\");\n });\n\n it(\"uses defaults when no data\", async () => {\n const flex = await exec(\"info_card\", {});\n expect(flex.body.contents[0].text).toBe(\"お知らせ\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// action_buttons\n// ---------------------------------------------------------------------------\ndescribe(\"action_buttons\", () => {\n it(\"builds with custom buttons\", async () => {\n const flex = await exec(\"action_buttons\", {\n prompt: \"好きな色は?\",\n buttons: [\n { label: \"赤\", text: \"赤が好き\", style: \"primary\" },\n { label: \"青\", text: \"青が好き\", style: \"secondary\" },\n ],\n });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.body.contents[0].text).toBe(\"好きな色は?\");\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"赤\");\n expect(flex.footer.contents[1].action.text).toBe(\"青が好き\");\n });\n\n it(\"adds default yes/no buttons when empty\", async () => {\n const flex = await exec(\"action_buttons\", { prompt: \"OK?\" });\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"はい\");\n expect(flex.footer.contents[1].action.label).toBe(\"いいえ\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// receipt\n// ---------------------------------------------------------------------------\ndescribe(\"receipt\", () => {\n it(\"builds with items and total\", async () => {\n const flex = await exec(\"receipt\", {\n title: \"ランチ\",\n items: [\n { name: \"ラーメン\", value: \"¥900\" },\n { name: \"餃子\", value: \"¥400\" },\n ],\n total: \"¥1,300\",\n });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n // title, separator, 2 items, separator, total\n expect(contents.length).toBe(6);\n expect(contents[0].text).toContain(\"ランチ\");\n // total row\n const totalRow = contents[5] as Record<string, unknown>;\n expect((totalRow.contents as Record<string, unknown>[])[1].text).toBe(\"¥1,300\");\n });\n\n it(\"omits total when not provided\", async () => {\n const flex = await exec(\"receipt\", {\n items: [{ name: \"Item\", value: \"100\" }],\n });\n // title, separator, 1 item = 3 contents (no total separator or total row)\n expect(flex.body.contents.length).toBe(3);\n });\n});\n\n// ---------------------------------------------------------------------------\n// morning_summary\n// ---------------------------------------------------------------------------\ndescribe(\"morning_summary\", () => {\n it(\"builds with all fields\", async () => {\n const flex = await exec(\"morning_summary\", {\n greeting: \"おはよう!\",\n date: \"2月13日 木曜日\",\n weather: \"東京 12°C 曇り\",\n advice: \"コートでOK\",\n schedule: [\"10:00 ミーティング\", \"14:00 歯医者\"],\n header_color: \"#FF6B6B\",\n });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.header.backgroundColor).toBe(\"#FF6B6B\");\n expect(flex.header.contents[0].text).toBe(\"おはよう!\");\n expect(flex.header.contents[1].text).toBe(\"2月13日 木曜日\");\n\n const body = flex.body.contents;\n expect(body[0].text).toBe(\"東京 12°C 曇り\");\n expect(body[1].text).toBe(\"コートでOK\");\n // separator + 2 schedule items\n expect(body.length).toBe(5);\n });\n\n it(\"uses default header color\", async () => {\n const flex = await exec(\"morning_summary\", {});\n expect(flex.header.backgroundColor).toBe(\"#1DB446\");\n });\n\n it(\"shows fallback body when no weather/schedule\", async () => {\n const flex = await exec(\"morning_summary\", { greeting: \"やぁ\" });\n expect(flex.body.contents[0].text).toBe(\"良い一日を!\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// hydration\n// ---------------------------------------------------------------------------\ndescribe(\"hydration\", () => {\n it(\"builds with current/goal\", async () => {\n const flex = await exec(\"hydration\", { current: 3, goal: 8 });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.size).toBe(\"kilo\");\n expect(flex.body.contents[0].text).toBe(\"水飲んだ?\");\n expect(flex.body.contents[1].text).toBe(\"今日: 3杯 / 8杯\");\n expect(flex.footer.contents[0].action.label).toBe(\"飲んだ!\");\n });\n\n it(\"uses custom unit and button text\", async () => {\n const flex = await exec(\"hydration\", {\n title: \"Coffee\",\n current: 2,\n goal: 4,\n unit: \"cups\",\n button_label: \"Had one!\",\n button_text: \"drank coffee\",\n });\n expect(flex.body.contents[0].text).toBe(\"Coffee\");\n expect(flex.body.contents[1].text).toBe(\"今日: 2cups / 4cups\");\n expect(flex.footer.contents[0].action.label).toBe(\"Had one!\");\n expect(flex.footer.contents[0].action.text).toBe(\"drank coffee\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// custom\n// ---------------------------------------------------------------------------\ndescribe(\"custom\", () => {\n it(\"uses raw contents array when provided\", async () => {\n const contents = [\n { type: \"text\", text: \"Hello\" },\n { type: \"text\", text: \"World\" },\n ];\n const flex = await exec(\"custom\", { contents });\n expect(flex.type).toBe(\"bubble\");\n expect(flex.body.contents).toEqual(contents);\n });\n\n it(\"auto-builds card from title + body\", async () => {\n const flex = await exec(\"custom\", { title: \"Title\", body: \"Body text\" });\n expect(flex.type).toBe(\"bubble\");\n const contents = flex.body.contents;\n expect(contents[0].text).toBe(\"Title\");\n expect(contents[1].type).toBe(\"separator\");\n expect(contents[2].text).toBe(\"Body text\");\n });\n\n it(\"auto-builds card with colored header\", async () => {\n const flex = await exec(\"custom\", {\n title: \"Workout\",\n body: \"Great session!\",\n header_color: \"#FF0000\",\n });\n expect(flex.header).toBeTruthy();\n expect(flex.header.backgroundColor).toBe(\"#FF0000\");\n expect(flex.header.contents[0].text).toBe(\"Workout\");\n // Title should be removed from body since it's in the header\n const bodyTexts = flex.body.contents.filter(\n (c: Record<string, unknown>) => c.type === \"text\",\n );\n expect(bodyTexts.every((t: Record<string, unknown>) => t.text !== \"Workout\")).toBe(true);\n });\n\n it(\"auto-builds card with buttons\", async () => {\n const flex = await exec(\"custom\", {\n title: \"Choose\",\n buttons: [\n { label: \"Option A\", text: \"A\", style: \"primary\" },\n { label: \"Option B\", text: \"B\", style: \"secondary\" },\n ],\n });\n expect(flex.footer).toBeTruthy();\n expect(flex.footer.contents).toHaveLength(2);\n expect(flex.footer.contents[0].action.label).toBe(\"Option A\");\n });\n\n it(\"errors when no contents, title, body, or buttons\", async () => {\n const result = await execRaw(\"custom\", {});\n expect(result).toContain(\"Error\");\n expect(result).toContain(\"custom template requires\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// unknown template\n// ---------------------------------------------------------------------------\ndescribe(\"unknown template\", () => {\n it(\"returns error for unknown template\", async () => {\n const result = await execRaw(\"nonexistent\", {});\n expect(result).toContain(\"Error: unknown template\");\n expect(result).toContain(\"nonexistent\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// tool metadata\n// ---------------------------------------------------------------------------\ndescribe(\"tool metadata\", () => {\n it(\"has correct name and description\", () => {\n expect(tool.name).toBe(\"flex_message\");\n expect(tool.description).toContain(\"Flex Message\");\n });\n\n it(\"generates valid tool definition\", () => {\n const def = tool.getDefinition();\n expect(def.type).toBe(\"function\");\n expect(def.function.name).toBe(\"flex_message\");\n expect(def.function.parameters.required).toContain(\"template\");\n expect(def.function.parameters.required).toContain(\"data\");\n });\n});\n"],"mappings":"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"}
|
package/dist/channels/line.d.mts
CHANGED
|
@@ -131,7 +131,7 @@ declare class LineChannel extends BaseChannel {
|
|
|
131
131
|
*/
|
|
132
132
|
verifySignature(rawBody: string, signature: string): boolean;
|
|
133
133
|
private returnFlexMessage;
|
|
134
|
-
/** Parse text into LINE message(s). If valid Flex JSON found, send as flex +
|
|
134
|
+
/** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */
|
|
135
135
|
private parseMessage;
|
|
136
136
|
/** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */
|
|
137
137
|
private findJsonEnd;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line.d.mts","names":[],"sources":["../../src/channels/line.ts"],"mappings":";;;;;;UAcU,eAAA;EACR,WAAA;EACA,MAAA,EAAQ,SAAA;AAAA;AAAA,UAGA,UAAA;EACR,IAAA;EACA,MAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGQ,SAAA;EACR,IAAA;EACA,SAAA;EACA,MAAA,EAAQ,UAAA;EACR,UAAA;EACA,IAAA;EACA,cAAA;EACA,eAAA;IAAmB,YAAA;EAAA;EACnB,OAAA,GAAU,WAAA;AAAA;AAAA,UAKF,SAAA;EACR,KAAA;EACA,MAAA;EACA,SAAA;EACA,OAAA;AAAA;AAAA,UAKQ,aAAA;EACR,KAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAA,UAGQ,WAAA;EACR,UAAA,EAAY,aAAA;AAAA;AAAA,UAKJ,mBAAA;EACR,IAAA;EACA,kBAAA;EACA,eAAA;AAAA;AAAA,UAKQ,YAAA;EACR,EAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAKQ,WAAA;EACR,IAAA;EACA,EAAA;EACA,UAAA;EACA,eAAA;EAGA,IAAA;EACA,MAAA,GAAS,SAAA;EACT,OAAA,GAAU,WAAA;EAGV,eAAA,GAAkB,mBAAA;EAClB,QAAA,GAAW,YAAA;EAGX,QAAA;EAGA,QAAA;EACA,QAAA;EAGA,KAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EAGA,SAAA;EACA,SAAA;EACA,mBAAA;EACA,QAAA;AAAA;AAAA,iBAOc,mBAAA,CACd,aAAA,UACA,OAAA,UACA,SAAA;;;AAvDe;;;;;cA8EJ,WAAA,SAAoB,WAAA;EAAA,SACtB,IAAA;EAAA,QACD,UAAA;EAAA,QACA,SAAA;cAEI,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,SAAA;EAO3C,KAAA,CAAA,GAAS,OAAA;EAKT,IAAA,CAAA,GAAQ,OAAA;EAMR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EA/ExB;EAuFJ,KAAA,CAAM,UAAA,UAAoB,IAAA,WAAe,OAAA;EAnFpC;EAwGL,WAAA,CAAY,EAAA,UAAY,IAAA,WAAe,OAAA;EAxGtB;;;;EAkIjB,aAAA,CAAc,IAAA,EAAM,eAAA,GAAkB,OAAA;EAAA,QA2B9B,cAAA;EAlKd;EAAA,QA8OQ,gBAAA;EA7OR;EAAA,QAgQc,kBAAA;EA7Pd;EAAA,QAsRQ,kBAAA;EArRR;EAAA,QA6RQ,kBAAA;EA1RR;EAAA,QAkSc,iBAAA;EA9Rd;EAAA,QA0SQ,qBAAA;EAtSR;EAAA,QAiTQ,oBAAA;EA/SR;;;;EAAA,QA2Uc,eAAA;EArUN;;AAOV;;;EAPU,QA+WM,mBAAA;EAvWd;;;;EAwZA,eAAA,CAAgB,OAAA,UAAiB,SAAA;EAAA,QAUzB,iBAAA;EAzYe;EAAA,QAoZf,YAAA;EA/YY;EAAA,
|
|
1
|
+
{"version":3,"file":"line.d.mts","names":[],"sources":["../../src/channels/line.ts"],"mappings":";;;;;;UAcU,eAAA;EACR,WAAA;EACA,MAAA,EAAQ,SAAA;AAAA;AAAA,UAGA,UAAA;EACR,IAAA;EACA,MAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGQ,SAAA;EACR,IAAA;EACA,SAAA;EACA,MAAA,EAAQ,UAAA;EACR,UAAA;EACA,IAAA;EACA,cAAA;EACA,eAAA;IAAmB,YAAA;EAAA;EACnB,OAAA,GAAU,WAAA;AAAA;AAAA,UAKF,SAAA;EACR,KAAA;EACA,MAAA;EACA,SAAA;EACA,OAAA;AAAA;AAAA,UAKQ,aAAA;EACR,KAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAA,UAGQ,WAAA;EACR,UAAA,EAAY,aAAA;AAAA;AAAA,UAKJ,mBAAA;EACR,IAAA;EACA,kBAAA;EACA,eAAA;AAAA;AAAA,UAKQ,YAAA;EACR,EAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAKQ,WAAA;EACR,IAAA;EACA,EAAA;EACA,UAAA;EACA,eAAA;EAGA,IAAA;EACA,MAAA,GAAS,SAAA;EACT,OAAA,GAAU,WAAA;EAGV,eAAA,GAAkB,mBAAA;EAClB,QAAA,GAAW,YAAA;EAGX,QAAA;EAGA,QAAA;EACA,QAAA;EAGA,KAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EAGA,SAAA;EACA,SAAA;EACA,mBAAA;EACA,QAAA;AAAA;AAAA,iBAOc,mBAAA,CACd,aAAA,UACA,OAAA,UACA,SAAA;;;AAvDe;;;;;cA8EJ,WAAA,SAAoB,WAAA;EAAA,SACtB,IAAA;EAAA,QACD,UAAA;EAAA,QACA,SAAA;cAEI,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,SAAA;EAO3C,KAAA,CAAA,GAAS,OAAA;EAKT,IAAA,CAAA,GAAQ,OAAA;EAMR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EA/ExB;EAuFJ,KAAA,CAAM,UAAA,UAAoB,IAAA,WAAe,OAAA;EAnFpC;EAwGL,WAAA,CAAY,EAAA,UAAY,IAAA,WAAe,OAAA;EAxGtB;;;;EAkIjB,aAAA,CAAc,IAAA,EAAM,eAAA,GAAkB,OAAA;EAAA,QA2B9B,cAAA;EAlKd;EAAA,QA8OQ,gBAAA;EA7OR;EAAA,QAgQc,kBAAA;EA7Pd;EAAA,QAsRQ,kBAAA;EArRR;EAAA,QA6RQ,kBAAA;EA1RR;EAAA,QAkSc,iBAAA;EA9Rd;EAAA,QA0SQ,qBAAA;EAtSR;EAAA,QAiTQ,oBAAA;EA/SR;;;;EAAA,QA2Uc,eAAA;EArUN;;AAOV;;;EAPU,QA+WM,mBAAA;EAvWd;;;;EAwZA,eAAA,CAAgB,OAAA,UAAiB,SAAA;EAAA,QAUzB,iBAAA;EAzYe;EAAA,QAoZf,YAAA;EA/YY;EAAA,QAqbZ,WAAA;EA9aO;EAAA,QAmdP,cAAA;EAxcQ;EAAA,QAseR,cAAA;AAAA"}
|
package/dist/channels/line.mjs
CHANGED
|
@@ -273,22 +273,29 @@ var LineChannel = class extends BaseChannel {
|
|
|
273
273
|
contents: parsed
|
|
274
274
|
}];
|
|
275
275
|
}
|
|
276
|
-
/** Parse text into LINE message(s). If valid Flex JSON found, send as flex +
|
|
276
|
+
/** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */
|
|
277
277
|
parseMessage(text) {
|
|
278
278
|
const trimmed = text.trim();
|
|
279
279
|
try {
|
|
280
280
|
return this.returnFlexMessage(trimmed);
|
|
281
281
|
} catch {}
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
const braceIdx = trimmed.indexOf("{");
|
|
283
|
+
if (braceIdx >= 0) {
|
|
284
|
+
const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));
|
|
284
285
|
if (jsonEnd > 0) {
|
|
285
|
-
const jsonPart = trimmed.slice(
|
|
286
|
-
const
|
|
286
|
+
const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);
|
|
287
|
+
const prefix = trimmed.slice(0, braceIdx).trim();
|
|
288
|
+
const suffix = trimmed.slice(braceIdx + jsonEnd).trim();
|
|
287
289
|
try {
|
|
288
|
-
const messages =
|
|
289
|
-
if (
|
|
290
|
+
const messages = [];
|
|
291
|
+
if (prefix) messages.push({
|
|
290
292
|
type: "text",
|
|
291
|
-
text:
|
|
293
|
+
text: prefix
|
|
294
|
+
});
|
|
295
|
+
messages.push(...this.returnFlexMessage(jsonPart));
|
|
296
|
+
if (suffix) messages.push({
|
|
297
|
+
type: "text",
|
|
298
|
+
text: suffix
|
|
292
299
|
});
|
|
293
300
|
return messages;
|
|
294
301
|
} catch {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + optional trailing text. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract JSON object from the start\n }\n\n // Look for a JSON object at the start of the string (possibly followed by text)\n if (trimmed.startsWith(\"{\")) {\n const jsonEnd = this.findJsonEnd(trimmed);\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(0, jsonEnd);\n const remainder = trimmed.slice(jsonEnd).trim();\n try {\n const messages = this.returnFlexMessage(jsonPart);\n if (remainder) {\n messages.push({ type: \"text\", text: remainder });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") return -1;\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) escape = true;\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) continue;\n\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i + 1;\n }\n }\n\n return -1;\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = extractText(item, depth + 1);\n if (result) return result;\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract a JSON object from anywhere in the string\n }\n\n // Find first '{' in the string and try to extract a JSON object\n const braceIdx = trimmed.indexOf(\"{\");\n if (braceIdx >= 0) {\n const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);\n const prefix = trimmed.slice(0, braceIdx).trim();\n const suffix = trimmed.slice(braceIdx + jsonEnd).trim();\n try {\n const messages: messagingApi.Message[] = [];\n if (prefix) {\n messages.push({ type: \"text\", text: prefix });\n }\n messages.push(...this.returnFlexMessage(jsonPart));\n if (suffix) {\n messages.push({ type: \"text\", text: suffix });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") return -1;\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) escape = true;\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) continue;\n\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i + 1;\n }\n }\n\n return -1;\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = extractText(item, depth + 1);\n if (result) return result;\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
|
|
@@ -174,6 +174,16 @@ describe("parseMessage", () => {
|
|
|
174
174
|
globalExpect(result).toHaveLength(1);
|
|
175
175
|
globalExpect(result[0].type).toBe("text");
|
|
176
176
|
});
|
|
177
|
+
it("splits flex JSON with prefix and suffix text", () => {
|
|
178
|
+
const result = ch.parseMessage(`完成!こんな感じ:{"type":"bubble","body":{"type":"box","layout":"vertical","contents":[{"type":"box","layout":"vertical","contents":[{"type": "text","text":"🌱 水やり記録","size": "xl" ,"weight":"bold"},{"type": "text","text":"前回: 今日\\n植物たち元気?", "color":"#666666","margin":"md"}]},{"type": "separator","margin":"lg"},{"type": "box","layout":"horizontal","margin":"md","spacing":"md","contents":[{"type": "button","style": "primary","color": "#4CAF50","action":{"type": "message","label": "水やりした!", "text": "水やりした"}},{"type": "button","style": "secondary","action":{"type": "message","label": "最後にいつ?", "text": "水やり確認"}}]}]}}\n\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`);
|
|
179
|
+
globalExpect(result).toHaveLength(3);
|
|
180
|
+
globalExpect(result[0].type).toBe("text");
|
|
181
|
+
globalExpect(result[0].text).toBe("完成!こんな感じ:");
|
|
182
|
+
globalExpect(result[1].type).toBe("flex");
|
|
183
|
+
globalExpect(result[1].altText).toBe("🌱 水やり記録");
|
|
184
|
+
globalExpect(result[2].type).toBe("text");
|
|
185
|
+
globalExpect(result[2].text).toBe("記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。");
|
|
186
|
+
});
|
|
177
187
|
it("splits real-world flex JSON with trailing Japanese text", () => {
|
|
178
188
|
const result = ch.parseMessage(`{"type":"bubble","body":{"type":"box","layout":"vertical","contents":[{"type":"box","layout":"vertical","contents":[{"type": "text","text":"🌱 水やり記録","size": "xl" ,"weight":"bold"},{"type": "text","text":"前回: 今日\\n植物たち元気?", "color":"#666666","margin":"md"}]},{"type": "separator","margin":"lg"},{"type": "box","layout":"horizontal","margin":"md","spacing":"md","contents":[{"type": "button","style": "primary","color": "#4CAF50","action":{"type": "message","label": "水やりした!", "text": "水やりした"}},{"type": "button","style": "secondary","action":{"type": "message","label": "最後にいつ?", "text": "水やり確認"}}]}]}}\n\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`);
|
|
179
189
|
globalExpect(result).toHaveLength(2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line.test.mjs","names":[],"sources":["../../src/channels/line.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { LineChannel } from \"./line.js\";\nimport { MessageBus } from \"../bus/queue.js\";\n\n// Construct a minimal LineChannel for testing private methods\nconst bus = new MessageBus();\nconst channel = new LineChannel(\n { enabled: false, channelSecret: \"test\", channelAccessToken: \"test\", allowFrom: [] },\n bus,\n);\n\n// Access private methods via any\nconst ch = channel as unknown as {\n parseMessage(text: string): Array<{ type: string; text?: string; altText?: string; contents?: unknown }>;\n findJsonEnd(str: string): number;\n extractAltText(contents: Record<string, unknown>): string;\n returnFlexMessage(message: string): Array<{ type: string; altText?: string; contents?: unknown }>;\n};\n\n// ---------------------------------------------------------------------------\n// findJsonEnd\n// ---------------------------------------------------------------------------\ndescribe(\"findJsonEnd\", () => {\n it(\"finds end of simple object\", () => {\n expect(ch.findJsonEnd('{\"a\":1}')).toBe(7);\n });\n\n it(\"finds end of nested object\", () => {\n const json = '{\"a\":{\"b\":{\"c\":1}}}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"finds end with trailing text\", () => {\n const str = '{\"type\":\"bubble\"} some trailing text';\n expect(ch.findJsonEnd(str)).toBe(17);\n });\n\n it(\"handles strings with escaped quotes\", () => {\n const json = '{\"text\":\"say \\\\\"hello\\\\\"\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles strings with braces inside\", () => {\n const json = '{\"text\":\"{ not a real brace }\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles escaped backslash before quote\", () => {\n // The string value is: path\\\\ (backslash is escaped, quote is not)\n const json = '{\"path\":\"C:\\\\\\\\\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"returns -1 for non-object string\", () => {\n expect(ch.findJsonEnd(\"hello\")).toBe(-1);\n });\n\n it(\"returns -1 for unclosed object\", () => {\n expect(ch.findJsonEnd('{\"a\":1')).toBe(-1);\n });\n\n it(\"handles empty object\", () => {\n expect(ch.findJsonEnd(\"{}\")).toBe(2);\n });\n\n it(\"handles arrays inside objects\", () => {\n const json = '{\"items\":[1,2,3]}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n});\n\n// ---------------------------------------------------------------------------\n// extractAltText\n// ---------------------------------------------------------------------------\ndescribe(\"extractAltText\", () => {\n it(\"extracts text from a simple bubble\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: \"Hello World\" }] },\n });\n expect(result).toBe(\"Hello World\");\n });\n\n it(\"extracts title when present\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n title: \"Card Title\",\n body: { type: \"box\", contents: [] },\n });\n expect(result).toBe(\"Card Title\");\n });\n\n it(\"falls back to 'Flex Message' when no text found\", () => {\n const result = ch.extractAltText({ type: \"bubble\" });\n expect(result).toBe(\"Flex Message\");\n });\n\n it(\"truncates long text to 100 chars\", () => {\n const longText = \"x\".repeat(200);\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: longText }] },\n });\n expect(result.length).toBe(100);\n });\n});\n\n// ---------------------------------------------------------------------------\n// parseMessage\n// ---------------------------------------------------------------------------\ndescribe(\"parseMessage\", () => {\n it(\"returns plain text for normal messages\", () => {\n const result = ch.parseMessage(\"Hello, world!\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"Hello, world!\");\n });\n\n it(\"returns (empty) for empty string\", () => {\n const result = ch.parseMessage(\"\");\n expect(result).toHaveLength(1);\n expect(result[0].text).toBe(\"(empty)\");\n });\n\n it(\"parses pure JSON flex message\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Test\" }] },\n });\n const result = ch.parseMessage(flex);\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n expect(result[0].altText).toBe(\"Test\");\n });\n\n it(\"handles JSON with trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Card\" }] },\n });\n const input = flex + \"\\n\\nHere is some extra text!\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"Here is some extra text!\");\n });\n\n it(\"handles JSON with Japanese trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"運勢\" }] },\n });\n const input = flex + \"\\n\\n記録できた!今日もがんばろう。\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日もがんばろう。\");\n });\n\n it(\"handles whitespace around JSON\", () => {\n const flex = JSON.stringify({ type: \"bubble\", body: { type: \"box\", contents: [{ type: \"text\", text: \"OK\" }] } });\n const result = ch.parseMessage(\" \" + flex + \" \");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n });\n\n it(\"falls back to text for invalid JSON starting with {\", () => {\n const result = ch.parseMessage(\"{not valid json at all}\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"{not valid json at all}\");\n });\n\n it(\"falls back to text for non-JSON curly brace text\", () => {\n const result = ch.parseMessage(\"{incomplete json\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n });\n\n it(\"splits real-world flex JSON with trailing Japanese text\", () => {\n const input = `{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// verifyLineSignature (exported function)\n// ---------------------------------------------------------------------------\nimport { verifyLineSignature } from \"./line.js\";\n\ndescribe(\"verifyLineSignature\", () => {\n it(\"returns true for valid signature\", () => {\n const secret = \"test-secret\";\n const body = '{\"events\":[]}';\n // Compute the expected signature\n const { createHmac } = require(\"node:crypto\");\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"base64\");\n expect(verifyLineSignature(secret, body, expected)).toBe(true);\n });\n\n it(\"returns false for invalid signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"bad-sig\")).toBe(false);\n });\n\n it(\"returns false for empty signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"\")).toBe(false);\n });\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"line.test.mjs","names":[],"sources":["../../src/channels/line.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { LineChannel } from \"./line.js\";\nimport { MessageBus } from \"../bus/queue.js\";\n\n// Construct a minimal LineChannel for testing private methods\nconst bus = new MessageBus();\nconst channel = new LineChannel(\n { enabled: false, channelSecret: \"test\", channelAccessToken: \"test\", allowFrom: [] },\n bus,\n);\n\n// Access private methods via any\nconst ch = channel as unknown as {\n parseMessage(text: string): Array<{ type: string; text?: string; altText?: string; contents?: unknown }>;\n findJsonEnd(str: string): number;\n extractAltText(contents: Record<string, unknown>): string;\n returnFlexMessage(message: string): Array<{ type: string; altText?: string; contents?: unknown }>;\n};\n\n// ---------------------------------------------------------------------------\n// findJsonEnd\n// ---------------------------------------------------------------------------\ndescribe(\"findJsonEnd\", () => {\n it(\"finds end of simple object\", () => {\n expect(ch.findJsonEnd('{\"a\":1}')).toBe(7);\n });\n\n it(\"finds end of nested object\", () => {\n const json = '{\"a\":{\"b\":{\"c\":1}}}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"finds end with trailing text\", () => {\n const str = '{\"type\":\"bubble\"} some trailing text';\n expect(ch.findJsonEnd(str)).toBe(17);\n });\n\n it(\"handles strings with escaped quotes\", () => {\n const json = '{\"text\":\"say \\\\\"hello\\\\\"\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles strings with braces inside\", () => {\n const json = '{\"text\":\"{ not a real brace }\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles escaped backslash before quote\", () => {\n // The string value is: path\\\\ (backslash is escaped, quote is not)\n const json = '{\"path\":\"C:\\\\\\\\\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"returns -1 for non-object string\", () => {\n expect(ch.findJsonEnd(\"hello\")).toBe(-1);\n });\n\n it(\"returns -1 for unclosed object\", () => {\n expect(ch.findJsonEnd('{\"a\":1')).toBe(-1);\n });\n\n it(\"handles empty object\", () => {\n expect(ch.findJsonEnd(\"{}\")).toBe(2);\n });\n\n it(\"handles arrays inside objects\", () => {\n const json = '{\"items\":[1,2,3]}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n});\n\n// ---------------------------------------------------------------------------\n// extractAltText\n// ---------------------------------------------------------------------------\ndescribe(\"extractAltText\", () => {\n it(\"extracts text from a simple bubble\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: \"Hello World\" }] },\n });\n expect(result).toBe(\"Hello World\");\n });\n\n it(\"extracts title when present\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n title: \"Card Title\",\n body: { type: \"box\", contents: [] },\n });\n expect(result).toBe(\"Card Title\");\n });\n\n it(\"falls back to 'Flex Message' when no text found\", () => {\n const result = ch.extractAltText({ type: \"bubble\" });\n expect(result).toBe(\"Flex Message\");\n });\n\n it(\"truncates long text to 100 chars\", () => {\n const longText = \"x\".repeat(200);\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: longText }] },\n });\n expect(result.length).toBe(100);\n });\n});\n\n// ---------------------------------------------------------------------------\n// parseMessage\n// ---------------------------------------------------------------------------\ndescribe(\"parseMessage\", () => {\n it(\"returns plain text for normal messages\", () => {\n const result = ch.parseMessage(\"Hello, world!\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"Hello, world!\");\n });\n\n it(\"returns (empty) for empty string\", () => {\n const result = ch.parseMessage(\"\");\n expect(result).toHaveLength(1);\n expect(result[0].text).toBe(\"(empty)\");\n });\n\n it(\"parses pure JSON flex message\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Test\" }] },\n });\n const result = ch.parseMessage(flex);\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n expect(result[0].altText).toBe(\"Test\");\n });\n\n it(\"handles JSON with trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Card\" }] },\n });\n const input = flex + \"\\n\\nHere is some extra text!\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"Here is some extra text!\");\n });\n\n it(\"handles JSON with Japanese trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"運勢\" }] },\n });\n const input = flex + \"\\n\\n記録できた!今日もがんばろう。\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日もがんばろう。\");\n });\n\n it(\"handles whitespace around JSON\", () => {\n const flex = JSON.stringify({ type: \"bubble\", body: { type: \"box\", contents: [{ type: \"text\", text: \"OK\" }] } });\n const result = ch.parseMessage(\" \" + flex + \" \");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n });\n\n it(\"falls back to text for invalid JSON starting with {\", () => {\n const result = ch.parseMessage(\"{not valid json at all}\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"{not valid json at all}\");\n });\n\n it(\"falls back to text for non-JSON curly brace text\", () => {\n const result = ch.parseMessage(\"{incomplete json\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n });\n\n it(\"splits flex JSON with prefix and suffix text\", () => {\n const input = `完成!こんな感じ:{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(3);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"完成!こんな感じ:\");\n expect(result[1].type).toBe(\"flex\");\n expect(result[1].altText).toBe(\"🌱 水やり記録\");\n expect(result[2].type).toBe(\"text\");\n expect(result[2].text).toBe(\"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\");\n });\n\n it(\"splits real-world flex JSON with trailing Japanese text\", () => {\n const input = `{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// verifyLineSignature (exported function)\n// ---------------------------------------------------------------------------\nimport { verifyLineSignature } from \"./line.js\";\n\ndescribe(\"verifyLineSignature\", () => {\n it(\"returns true for valid signature\", () => {\n const secret = \"test-secret\";\n const body = '{\"events\":[]}';\n // Compute the expected signature\n const { createHmac } = require(\"node:crypto\");\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"base64\");\n expect(verifyLineSignature(secret, body, expected)).toBe(true);\n });\n\n it(\"returns false for invalid signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"bad-sig\")).toBe(false);\n });\n\n it(\"returns false for empty signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"\")).toBe(false);\n });\n});\n"],"mappings":"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"}
|
package/dist/config/schema.d.mts
CHANGED
|
@@ -70,31 +70,31 @@ declare const ChannelsConfigSchema: z.ZodObject<{
|
|
|
70
70
|
channelAccessToken?: string | undefined;
|
|
71
71
|
}>>;
|
|
72
72
|
}, "strip", z.ZodTypeAny, {
|
|
73
|
-
line: {
|
|
74
|
-
enabled: boolean;
|
|
75
|
-
allowFrom: string[];
|
|
76
|
-
channelSecret: string;
|
|
77
|
-
channelAccessToken: string;
|
|
78
|
-
};
|
|
79
73
|
telegram: {
|
|
80
74
|
enabled: boolean;
|
|
81
75
|
token: string;
|
|
82
76
|
allowFrom: string[];
|
|
83
77
|
proxy?: string | null | undefined;
|
|
84
78
|
};
|
|
79
|
+
line: {
|
|
80
|
+
enabled: boolean;
|
|
81
|
+
allowFrom: string[];
|
|
82
|
+
channelSecret: string;
|
|
83
|
+
channelAccessToken: string;
|
|
84
|
+
};
|
|
85
85
|
}, {
|
|
86
|
-
line?: {
|
|
87
|
-
enabled?: boolean | undefined;
|
|
88
|
-
allowFrom?: string[] | undefined;
|
|
89
|
-
channelSecret?: string | undefined;
|
|
90
|
-
channelAccessToken?: string | undefined;
|
|
91
|
-
} | undefined;
|
|
92
86
|
telegram?: {
|
|
93
87
|
enabled?: boolean | undefined;
|
|
94
88
|
token?: string | undefined;
|
|
95
89
|
allowFrom?: string[] | undefined;
|
|
96
90
|
proxy?: string | null | undefined;
|
|
97
91
|
} | undefined;
|
|
92
|
+
line?: {
|
|
93
|
+
enabled?: boolean | undefined;
|
|
94
|
+
allowFrom?: string[] | undefined;
|
|
95
|
+
channelSecret?: string | undefined;
|
|
96
|
+
channelAccessToken?: string | undefined;
|
|
97
|
+
} | undefined;
|
|
98
98
|
}>;
|
|
99
99
|
type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
|
|
100
100
|
declare const AgentDefaultsSchema: z.ZodObject<{
|
|
@@ -556,15 +556,15 @@ declare const ToolsConfigSchema: z.ZodObject<{
|
|
|
556
556
|
export?: string | undefined;
|
|
557
557
|
}>, "many">>;
|
|
558
558
|
}, "strip", z.ZodTypeAny, {
|
|
559
|
+
exec: {
|
|
560
|
+
timeout: number;
|
|
561
|
+
};
|
|
559
562
|
web: {
|
|
560
563
|
search: {
|
|
561
564
|
apiKey: string;
|
|
562
565
|
maxResults: number;
|
|
563
566
|
};
|
|
564
567
|
};
|
|
565
|
-
exec: {
|
|
566
|
-
timeout: number;
|
|
567
|
-
};
|
|
568
568
|
restrictToWorkspace: boolean;
|
|
569
569
|
enabled?: string[] | undefined;
|
|
570
570
|
disabled?: string[] | undefined;
|
|
@@ -574,6 +574,9 @@ declare const ToolsConfigSchema: z.ZodObject<{
|
|
|
574
574
|
export?: string | undefined;
|
|
575
575
|
}[] | undefined;
|
|
576
576
|
}, {
|
|
577
|
+
exec?: {
|
|
578
|
+
timeout?: number | undefined;
|
|
579
|
+
} | undefined;
|
|
577
580
|
enabled?: string[] | undefined;
|
|
578
581
|
web?: {
|
|
579
582
|
search?: {
|
|
@@ -581,9 +584,6 @@ declare const ToolsConfigSchema: z.ZodObject<{
|
|
|
581
584
|
maxResults?: number | undefined;
|
|
582
585
|
} | undefined;
|
|
583
586
|
} | undefined;
|
|
584
|
-
exec?: {
|
|
585
|
-
timeout?: number | undefined;
|
|
586
|
-
} | undefined;
|
|
587
587
|
restrictToWorkspace?: boolean | undefined;
|
|
588
588
|
disabled?: string[] | undefined;
|
|
589
589
|
custom?: {
|
|
@@ -675,31 +675,31 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
675
675
|
channelAccessToken?: string | undefined;
|
|
676
676
|
}>>;
|
|
677
677
|
}, "strip", z.ZodTypeAny, {
|
|
678
|
-
line: {
|
|
679
|
-
enabled: boolean;
|
|
680
|
-
allowFrom: string[];
|
|
681
|
-
channelSecret: string;
|
|
682
|
-
channelAccessToken: string;
|
|
683
|
-
};
|
|
684
678
|
telegram: {
|
|
685
679
|
enabled: boolean;
|
|
686
680
|
token: string;
|
|
687
681
|
allowFrom: string[];
|
|
688
682
|
proxy?: string | null | undefined;
|
|
689
683
|
};
|
|
684
|
+
line: {
|
|
685
|
+
enabled: boolean;
|
|
686
|
+
allowFrom: string[];
|
|
687
|
+
channelSecret: string;
|
|
688
|
+
channelAccessToken: string;
|
|
689
|
+
};
|
|
690
690
|
}, {
|
|
691
|
-
line?: {
|
|
692
|
-
enabled?: boolean | undefined;
|
|
693
|
-
allowFrom?: string[] | undefined;
|
|
694
|
-
channelSecret?: string | undefined;
|
|
695
|
-
channelAccessToken?: string | undefined;
|
|
696
|
-
} | undefined;
|
|
697
691
|
telegram?: {
|
|
698
692
|
enabled?: boolean | undefined;
|
|
699
693
|
token?: string | undefined;
|
|
700
694
|
allowFrom?: string[] | undefined;
|
|
701
695
|
proxy?: string | null | undefined;
|
|
702
696
|
} | undefined;
|
|
697
|
+
line?: {
|
|
698
|
+
enabled?: boolean | undefined;
|
|
699
|
+
allowFrom?: string[] | undefined;
|
|
700
|
+
channelSecret?: string | undefined;
|
|
701
|
+
channelAccessToken?: string | undefined;
|
|
702
|
+
} | undefined;
|
|
703
703
|
}>>;
|
|
704
704
|
providers: z.ZodDefault<z.ZodObject<{
|
|
705
705
|
anthropic: z.ZodDefault<z.ZodObject<{
|
|
@@ -1015,15 +1015,15 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1015
1015
|
export?: string | undefined;
|
|
1016
1016
|
}>, "many">>;
|
|
1017
1017
|
}, "strip", z.ZodTypeAny, {
|
|
1018
|
+
exec: {
|
|
1019
|
+
timeout: number;
|
|
1020
|
+
};
|
|
1018
1021
|
web: {
|
|
1019
1022
|
search: {
|
|
1020
1023
|
apiKey: string;
|
|
1021
1024
|
maxResults: number;
|
|
1022
1025
|
};
|
|
1023
1026
|
};
|
|
1024
|
-
exec: {
|
|
1025
|
-
timeout: number;
|
|
1026
|
-
};
|
|
1027
1027
|
restrictToWorkspace: boolean;
|
|
1028
1028
|
enabled?: string[] | undefined;
|
|
1029
1029
|
disabled?: string[] | undefined;
|
|
@@ -1033,6 +1033,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1033
1033
|
export?: string | undefined;
|
|
1034
1034
|
}[] | undefined;
|
|
1035
1035
|
}, {
|
|
1036
|
+
exec?: {
|
|
1037
|
+
timeout?: number | undefined;
|
|
1038
|
+
} | undefined;
|
|
1036
1039
|
enabled?: string[] | undefined;
|
|
1037
1040
|
web?: {
|
|
1038
1041
|
search?: {
|
|
@@ -1040,9 +1043,6 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1040
1043
|
maxResults?: number | undefined;
|
|
1041
1044
|
} | undefined;
|
|
1042
1045
|
} | undefined;
|
|
1043
|
-
exec?: {
|
|
1044
|
-
timeout?: number | undefined;
|
|
1045
|
-
} | undefined;
|
|
1046
1046
|
restrictToWorkspace?: boolean | undefined;
|
|
1047
1047
|
disabled?: string[] | undefined;
|
|
1048
1048
|
custom?: {
|
|
@@ -1064,18 +1064,18 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1064
1064
|
};
|
|
1065
1065
|
};
|
|
1066
1066
|
channels: {
|
|
1067
|
-
line: {
|
|
1068
|
-
enabled: boolean;
|
|
1069
|
-
allowFrom: string[];
|
|
1070
|
-
channelSecret: string;
|
|
1071
|
-
channelAccessToken: string;
|
|
1072
|
-
};
|
|
1073
1067
|
telegram: {
|
|
1074
1068
|
enabled: boolean;
|
|
1075
1069
|
token: string;
|
|
1076
1070
|
allowFrom: string[];
|
|
1077
1071
|
proxy?: string | null | undefined;
|
|
1078
1072
|
};
|
|
1073
|
+
line: {
|
|
1074
|
+
enabled: boolean;
|
|
1075
|
+
allowFrom: string[];
|
|
1076
|
+
channelSecret: string;
|
|
1077
|
+
channelAccessToken: string;
|
|
1078
|
+
};
|
|
1079
1079
|
};
|
|
1080
1080
|
providers: {
|
|
1081
1081
|
anthropic: {
|
|
@@ -1139,15 +1139,15 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1139
1139
|
port: number;
|
|
1140
1140
|
};
|
|
1141
1141
|
tools: {
|
|
1142
|
+
exec: {
|
|
1143
|
+
timeout: number;
|
|
1144
|
+
};
|
|
1142
1145
|
web: {
|
|
1143
1146
|
search: {
|
|
1144
1147
|
apiKey: string;
|
|
1145
1148
|
maxResults: number;
|
|
1146
1149
|
};
|
|
1147
1150
|
};
|
|
1148
|
-
exec: {
|
|
1149
|
-
timeout: number;
|
|
1150
|
-
};
|
|
1151
1151
|
restrictToWorkspace: boolean;
|
|
1152
1152
|
enabled?: string[] | undefined;
|
|
1153
1153
|
disabled?: string[] | undefined;
|
|
@@ -1170,18 +1170,18 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1170
1170
|
} | undefined;
|
|
1171
1171
|
} | undefined;
|
|
1172
1172
|
channels?: {
|
|
1173
|
-
line?: {
|
|
1174
|
-
enabled?: boolean | undefined;
|
|
1175
|
-
allowFrom?: string[] | undefined;
|
|
1176
|
-
channelSecret?: string | undefined;
|
|
1177
|
-
channelAccessToken?: string | undefined;
|
|
1178
|
-
} | undefined;
|
|
1179
1173
|
telegram?: {
|
|
1180
1174
|
enabled?: boolean | undefined;
|
|
1181
1175
|
token?: string | undefined;
|
|
1182
1176
|
allowFrom?: string[] | undefined;
|
|
1183
1177
|
proxy?: string | null | undefined;
|
|
1184
1178
|
} | undefined;
|
|
1179
|
+
line?: {
|
|
1180
|
+
enabled?: boolean | undefined;
|
|
1181
|
+
allowFrom?: string[] | undefined;
|
|
1182
|
+
channelSecret?: string | undefined;
|
|
1183
|
+
channelAccessToken?: string | undefined;
|
|
1184
|
+
} | undefined;
|
|
1185
1185
|
} | undefined;
|
|
1186
1186
|
providers?: {
|
|
1187
1187
|
anthropic?: {
|
|
@@ -1245,6 +1245,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1245
1245
|
port?: number | undefined;
|
|
1246
1246
|
} | undefined;
|
|
1247
1247
|
tools?: {
|
|
1248
|
+
exec?: {
|
|
1249
|
+
timeout?: number | undefined;
|
|
1250
|
+
} | undefined;
|
|
1248
1251
|
enabled?: string[] | undefined;
|
|
1249
1252
|
web?: {
|
|
1250
1253
|
search?: {
|
|
@@ -1252,9 +1255,6 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1252
1255
|
maxResults?: number | undefined;
|
|
1253
1256
|
} | undefined;
|
|
1254
1257
|
} | undefined;
|
|
1255
|
-
exec?: {
|
|
1256
|
-
timeout?: number | undefined;
|
|
1257
|
-
} | undefined;
|
|
1258
1258
|
restrictToWorkspace?: boolean | undefined;
|
|
1259
1259
|
disabled?: string[] | undefined;
|
|
1260
1260
|
custom?: {
|