правильная организация директории плагина
This commit is contained in:
parent
26e28361cc
commit
5fdb2510b4
|
|
@ -21,7 +21,6 @@ function isV2Context(ctx: any): boolean {
|
||||||
async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
const isV2 = isV2Context(ctx)
|
const isV2 = isV2Context(ctx)
|
||||||
const eventController = new AbortController()
|
const eventController = new AbortController()
|
||||||
let expiryInterval: ReturnType<typeof setInterval> | undefined
|
|
||||||
let cleanup: (() => void) | undefined
|
let cleanup: (() => void) | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -39,6 +38,58 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
const matrix = new MatrixBotClient(options)
|
const matrix = new MatrixBotClient(options)
|
||||||
const sessionManager = new SessionManager(options.rateLimitSeconds || 5)
|
const sessionManager = new SessionManager(options.rateLimitSeconds || 5)
|
||||||
|
|
||||||
|
// Track pending tool calls per session: sessionID -> Map<callID, {roomId, threadRootId, context}>
|
||||||
|
const pendingToolCalls = new Map<string, Map<string, { roomId: string; threadRootId: string; context: any }>>()
|
||||||
|
|
||||||
|
// Track sent messages per session to avoid duplicates
|
||||||
|
const sentMessageIds = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
// Track Matrix context per OpenCode session
|
||||||
|
const sessionContextMap = new Map<string, { roomId: string; threadRootId: string; context: any }>()
|
||||||
|
|
||||||
|
// Helper: send a message to Matrix
|
||||||
|
async function sendMatrixMessage(roomId: string, threadRootId: string, context: any, text: string): Promise<void> {
|
||||||
|
if (!text.trim()) return
|
||||||
|
await matrix.sendReply(context, text)
|
||||||
|
log(`Matrix: Sent to ${roomId}:${threadRootId} (${text.length} chars)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: extract content from parts
|
||||||
|
function extractContent(parts: any[]): { reasoningText: string; mainText: string; toolCallText: string } {
|
||||||
|
let reasoningText = ""
|
||||||
|
let mainText = ""
|
||||||
|
let toolCallText = ""
|
||||||
|
const reasoningTypes = new Set(["reasoning", "thinking", "redwood", "reason"])
|
||||||
|
|
||||||
|
for (const p of parts) {
|
||||||
|
const content = p?.content || p?.text || ""
|
||||||
|
const pType = p?.type || ""
|
||||||
|
if (reasoningTypes.has(pType)) {
|
||||||
|
reasoningText += content
|
||||||
|
} else if (pType === "tool_call" || pType === "tool") {
|
||||||
|
toolCallText += content
|
||||||
|
} else {
|
||||||
|
mainText += content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { reasoningText, mainText, toolCallText }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: format message text
|
||||||
|
function formatMessage(toolCallText: string, reasoningText: string, mainText: string): string {
|
||||||
|
if (toolCallText.trim()) {
|
||||||
|
return `Tool call: ${toolCallText.trim()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseText = mainText
|
||||||
|
if (reasoningText.trim()) {
|
||||||
|
responseText = `<details><summary>Thoughts</summary>\n\n${reasoningText.trim()}\n\n</details>\n\n---\n\n${mainText}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
|
|
||||||
// V2 Event subscription for response streaming
|
// V2 Event subscription for response streaming
|
||||||
if (isV2) {
|
if (isV2) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
|
@ -50,6 +101,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
if (!sessionId) continue
|
if (!sessionId) continue
|
||||||
const update = (event as { update?: { type: string; content?: string; toolName?: string } }).update
|
const update = (event as { update?: { type: string; content?: string; toolName?: string } }).update
|
||||||
if (!update) continue
|
if (!update) continue
|
||||||
|
log(`V2 event: session=${sessionId}, update.type=${update.type}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -62,30 +114,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
// V1 Event subscription for response streaming
|
|
||||||
if (!isV2 && sdkClient) {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const events = await sdkClient.event.subscribe()
|
|
||||||
for await (const event of events.stream) {
|
|
||||||
const eventType = event.type || ""
|
|
||||||
if (eventType === "session.updated") {
|
|
||||||
const sessionId = event.properties?.sessionID
|
|
||||||
if (!sessionId) continue
|
|
||||||
const update = event.properties?.update
|
|
||||||
if (!update) continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (String(err).includes("AbortError") || (err as { name?: string }).name === "AbortError") {
|
|
||||||
// expected during cleanup
|
|
||||||
} else {
|
|
||||||
log(`V1 event subscription error: ${String(err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message handler
|
// Message handler
|
||||||
matrix.on(async (data: any) => {
|
matrix.on(async (data: any) => {
|
||||||
const { context, query, sender, roomId, eventId, timestamp } = data
|
const { context, query, sender, roomId, eventId, timestamp } = data
|
||||||
|
|
@ -133,6 +161,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
}
|
}
|
||||||
session.opencodeSessionId = opencodeSessionId
|
session.opencodeSessionId = opencodeSessionId
|
||||||
sessionManager.saveSession(roomId, threadRootId, opencodeSessionId)
|
sessionManager.saveSession(roomId, threadRootId, opencodeSessionId)
|
||||||
|
sessionContextMap.set(opencodeSessionId, { roomId, threadRootId, context })
|
||||||
} catch (createErr: any) {
|
} catch (createErr: any) {
|
||||||
log(`Failed to create session: ${String(createErr)}`)
|
log(`Failed to create session: ${String(createErr)}`)
|
||||||
session.isActive = false
|
session.isActive = false
|
||||||
|
|
@ -157,66 +186,87 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
// V1: use SDK client.session.prompt()
|
// V1: use SDK client.session.prompt()
|
||||||
log(`V1: Sending prompt to session ${opencodeSessionId}: ${query}`)
|
log(`V1: Sending prompt to session ${opencodeSessionId}: ${query}`)
|
||||||
|
|
||||||
|
// Start polling for new messages
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
if (!session.isActive) {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingIds = sentMessageIds.get(opencodeSessionId) || new Set()
|
||||||
|
const messagesResult = await sdkClient.session.messages({
|
||||||
|
path: { id: opencodeSessionId },
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = messagesResult?.data || []
|
||||||
|
if (Array.isArray(messages)) {
|
||||||
|
for (const msg of messages) {
|
||||||
|
const msgId = msg?.info?.id || msg?.id
|
||||||
|
if (!msgId || existingIds.has(msgId)) continue
|
||||||
|
|
||||||
|
const role = msg?.info?.role || ""
|
||||||
|
if (role !== "assistant" && role !== "tool_call") continue
|
||||||
|
|
||||||
|
const parts = msg?.parts || msg?.info?.parts || []
|
||||||
|
const { reasoningText, mainText, toolCallText } = extractContent(parts)
|
||||||
|
const responseText = formatMessage(toolCallText, reasoningText, mainText)
|
||||||
|
|
||||||
|
if (responseText.trim()) {
|
||||||
|
await matrix.sendReply(context, responseText)
|
||||||
|
log(`V1: Sent message ${msgId} (role=${role}, ${responseText.length} chars)`)
|
||||||
|
existingIds.add(msgId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sentMessageIds.set(opencodeSessionId, existingIds)
|
||||||
|
} catch (err) {
|
||||||
|
log(`V1: Error during polling: ${String(err)}`)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sdkClient.session.prompt({
|
const result = await sdkClient.session.prompt({
|
||||||
path: { id: opencodeSessionId },
|
path: { id: opencodeSessionId },
|
||||||
body: { parts: [{ type: "text", text: query }] },
|
body: { parts: [{ type: "text", text: query }] },
|
||||||
})
|
})
|
||||||
log(`V1: prompt result keys=${Object.keys(result || {})}`)
|
|
||||||
log(`V1: prompt result.data.keys=${result?.data ? Object.keys(result.data).join(",") : "null"}`)
|
|
||||||
if (result?.data?.parts) {
|
|
||||||
log(`V1: data.parts count=${result.data.parts.length}`)
|
|
||||||
log(`V1: data.parts.types=${result.data.parts.map((p: any) => p.type).join(",")}`)
|
|
||||||
log(`V1: data.parts[0]=${JSON.stringify(result.data.parts[0]).slice(0, 200)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract content from various locations, separating reasoning from main text
|
// Stop polling
|
||||||
let reasoningText = ""
|
clearInterval(pollInterval)
|
||||||
let mainText = ""
|
|
||||||
const reasoningTypes = new Set(["reasoning", "thinking", "redwood", "reason"])
|
// Send any remaining messages
|
||||||
if (result?.data?.parts) {
|
try {
|
||||||
for (const p of result.data.parts) {
|
const existingIds = sentMessageIds.get(opencodeSessionId) || new Set()
|
||||||
const content = p.content || p.text || ""
|
const messagesResult = await sdkClient.session.messages({
|
||||||
if (reasoningTypes.has(p.type)) {
|
path: { id: opencodeSessionId },
|
||||||
reasoningText += content
|
})
|
||||||
} else {
|
|
||||||
mainText += content
|
const messages = messagesResult?.data || []
|
||||||
|
if (Array.isArray(messages)) {
|
||||||
|
for (const msg of messages) {
|
||||||
|
const msgId = msg?.info?.id || msg?.id
|
||||||
|
if (!msgId || existingIds.has(msgId)) continue
|
||||||
|
|
||||||
|
const role = msg?.info?.role || ""
|
||||||
|
if (role !== "assistant" && role !== "tool_call") continue
|
||||||
|
|
||||||
|
const parts = msg?.parts || msg?.info?.parts || []
|
||||||
|
const { reasoningText, mainText, toolCallText } = extractContent(parts)
|
||||||
|
const responseText = formatMessage(toolCallText, reasoningText, mainText)
|
||||||
|
|
||||||
|
if (responseText.trim()) {
|
||||||
|
await matrix.sendReply(context, responseText)
|
||||||
|
log(`V1: Sent message ${msgId} (role=${role}, ${responseText.length} chars)`)
|
||||||
|
existingIds.add(msgId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (result?.data?.info?.parts) {
|
|
||||||
for (const p of result.data.info.parts) {
|
|
||||||
const content = p.content || p.text || ""
|
|
||||||
if (reasoningTypes.has(p.type)) {
|
|
||||||
reasoningText += content
|
|
||||||
} else {
|
|
||||||
mainText += content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (result?.data?.message?.parts) {
|
|
||||||
for (const p of result.data.message.parts) {
|
|
||||||
const content = p.content || p.text || ""
|
|
||||||
if (reasoningTypes.has(p.type)) {
|
|
||||||
reasoningText += content
|
|
||||||
} else {
|
|
||||||
mainText += content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (result?.data?.content) {
|
|
||||||
mainText = result.data.content
|
|
||||||
} else if (result?.response?.content) {
|
|
||||||
mainText = result.response.content
|
|
||||||
} else if (result?.content) {
|
|
||||||
mainText = result.content
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseText = mainText
|
sentMessageIds.set(opencodeSessionId, existingIds)
|
||||||
if (reasoningText.trim()) {
|
} catch (err) {
|
||||||
responseText = `<details><summary>Thoughts</summary>\n\n${reasoningText.trim()}\n\n</details>\n\n---\n\n${mainText}`
|
log(`V1: Error sending final messages: ${String(err)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.outputChars += responseText.length
|
|
||||||
await matrix.sendReply(context, responseText)
|
|
||||||
log(`V1: Response sent (${responseText.length} chars)`)
|
|
||||||
} catch (promptErr: any) {
|
} catch (promptErr: any) {
|
||||||
log(`V1 prompt error: ${String(promptErr)}`)
|
log(`V1 prompt error: ${String(promptErr)}`)
|
||||||
const errMsg = promptErr?.response?.error?.message || promptErr?.error?.message || String(promptErr)
|
const errMsg = promptErr?.response?.error?.message || promptErr?.error?.message || String(promptErr)
|
||||||
|
|
@ -236,6 +286,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
} finally {
|
} finally {
|
||||||
session.isActive = false
|
session.isActive = false
|
||||||
session.lastActivity = Date.now()
|
session.lastActivity = Date.now()
|
||||||
|
sessionContextMap.delete(opencodeSessionId)
|
||||||
releaseQuery()
|
releaseQuery()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -317,7 +368,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start Matrix client
|
// V2 Storage - defensive access
|
||||||
if (isV2 && ctx?.storage) {
|
if (isV2 && ctx?.storage) {
|
||||||
await ctx.storage.set("matrix/plugin_version", "1.0.0")
|
await ctx.storage.set("matrix/plugin_version", "1.0.0")
|
||||||
await ctx.storage.set("matrix/started_at", new Date().toISOString())
|
await ctx.storage.set("matrix/started_at", new Date().toISOString())
|
||||||
Loading…
Reference in New Issue