refactor: remove V1 code, plugin is V2-only now

- Remove isV2Context() detection and all V1 branches
- Remove sdkClient parameter and V1 SDK polling logic
- Remove extractContent/formatMessage helpers (V1-only)
- Remove sentMessageIds dedup map (V1-only)
- Remove v1Server() entry point and server() export
- Remove encodeSessionId() from session-manager
- Remove crypto dependency
- Simplify help text (no mode info)
- Session ID now uses roomId:threadRootId directly
This commit is contained in:
Бородин Роман 2026-09-25 23:24:12 +03:00
parent 2f17001f2a
commit 51b98e02a5
2 changed files with 40 additions and 268 deletions

244
index.ts
View File

@ -1,32 +1,23 @@
import fs from "fs"
import { MatrixBotClient } from "./matrix-client.js"
import { SessionManager, encodeSessionId } from "./session-manager.js"
import { SessionManager } from "./session-manager.js"
import { loadConfig } from "./config-loader.js"
import { log } from "./logger.js"
log("Plugin module loaded")
// =============================================================================
// V2 Support Detection
// Plugin Setup
// =============================================================================
function isV2Context(ctx: any): boolean {
return !!(ctx?.session?.prompt && ctx?.event?.subscribe && ctx?.storage)
}
// =============================================================================
// Shared initialization logic
// =============================================================================
async function runPlugin(ctx: any, options: any, sdkClient?: any) {
const isV2 = isV2Context(ctx)
async function runPlugin(ctx: any, options: any) {
const eventController = new AbortController()
let cleanup: (() => void) | undefined
try {
const directory = ctx?.location?.directory || process.cwd()
fs.appendFileSync("/tmp/opencode-matrix-plugin/setup-started.log", `[${new Date().toISOString()}] setup() called, dir=${directory}, v2=${isV2}, sdk=${!!sdkClient}\n`)
log(`Plugin setup started, directory=${directory}, v2=${isV2}, sdk=${!!sdkClient}`)
fs.appendFileSync("/tmp/opencode-matrix-plugin/setup-started.log", `[${new Date().toISOString()}] setup() called, dir=${directory}\n`)
log(`Plugin setup started, directory=${directory}`)
if (!options.homeserver) {
log("Plugin disabled (no homeserver configured)")
@ -38,60 +29,10 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
const matrix = new MatrixBotClient(options)
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
if (isV2) {
void (async () => {
try {
for await (const event of ctx.event.subscribe({ signal: eventController.signal })) {
@ -112,7 +53,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
}
}
})()
}
// Message handler
matrix.on(async (data: any) => {
@ -147,18 +87,10 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
if (!opencodeSessionId) {
log(`Creating new OpenCode session for ${roomId}:${threadRootId}`)
try {
if (isV2 && ctx?.session?.create) {
// V2: can pass custom ID
const encodedId = encodeSessionId(roomId, threadRootId)
const encodedId = `${roomId}:${threadRootId}`
const createResult = await ctx.session.create({ id: encodedId, title: `Matrix: ${roomId.slice(0, 30)}...` })
opencodeSessionId = createResult?.id || createResult?.data?.id || encodedId
log(`V2: Created OpenCode session ${opencodeSessionId} (custom ID: ${encodedId})`)
} else if (sdkClient) {
// V1: cannot pass custom ID, need to save mapping
const createResult = await sdkClient.session.create({ body: { title: `Matrix: ${roomId.slice(0, 30)}...` } })
opencodeSessionId = createResult?.data?.id || createResult?.id || ""
log(`V1: Created OpenCode session ${opencodeSessionId}`)
}
log(`Created OpenCode session ${opencodeSessionId} (ID: ${encodedId})`)
session.opencodeSessionId = opencodeSessionId
sessionManager.saveSession(roomId, threadRootId, opencodeSessionId)
sessionContextMap.set(opencodeSessionId, { roomId, threadRootId, context })
@ -173,8 +105,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
}
try {
if (isV2) {
// V2: use ctx.session.prompt()
const result = await ctx.session.prompt({
sessionID: opencodeSessionId,
text: query,
@ -182,103 +112,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
const responseText = result ? String(result) : "No response"
session.outputChars += responseText.length
await matrix.sendReply(context, responseText)
} else if (sdkClient) {
// V1: use SDK client.session.prompt()
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 {
const result = await sdkClient.session.prompt({
path: { id: opencodeSessionId },
body: { parts: [{ type: "text", text: query }] },
})
// Stop polling
clearInterval(pollInterval)
// Send any remaining messages
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 sending final messages: ${String(err)}`)
}
} catch (promptErr: any) {
log(`V1 prompt error: ${String(promptErr)}`)
const errMsg = promptErr?.response?.error?.message || promptErr?.error?.message || String(promptErr)
await matrix.sendNotice(context, `Error: ${errMsg.slice(0, 200)}`)
}
} else {
// No SDK client available
log(`V1 mode without SDK client: Would send prompt to session ${opencodeSessionId}: ${query}`)
const responseText = "Matrix plugin running in V1 mode — prompt forwarding requires OpenCode SDK client"
session.outputChars += responseText.length
await matrix.sendReply(context, responseText)
}
} catch (err: any) {
log(`Error processing message: ${String(err)}`)
const errMsg = err?.response?.error?.message || String(err)
@ -322,7 +155,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
case "h": {
const trigger = options.triggerPatterns?.[0] || "!oc "
const botName = options.botName || "opencode"
const mode = isV2 ? "OpenCode V2 (full)" : sdkClient ? "OpenCode V1 (full via SDK)" : "OpenCode V1 (limited — requires SDK client)"
const msg = [
`${botName} - OpenCode Matrix Plugin`,
"",
@ -334,8 +166,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
`Usage: ${trigger} <your question>`,
`Or @${botName} <your question>`,
`Or reply in a thread`,
"",
`Mode: ${mode}`,
].join("\n")
await sendFn(msg)
break
@ -368,8 +198,8 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
}
})
// V2 Storage - defensive access
if (isV2 && ctx?.storage) {
// V2 Storage
if (ctx?.storage) {
await ctx.storage.set("matrix/plugin_version", "1.0.0")
await ctx.storage.set("matrix/started_at", new Date().toISOString())
}
@ -377,7 +207,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
// Start Matrix client
await matrix.start()
log(`Plugin setup completed successfully (V2=${isV2}, sdk=${!!sdkClient})`)
log(`Plugin setup completed successfully`)
cleanup = () => {
log("Plugin cleaning up...")
@ -395,47 +225,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) {
}
}
// =============================================================================
// V1 Server Entry Point — receives PluginInput with SDK client
// =============================================================================
async function v1Server(input: any, options?: any) {
try {
log("V1 server entry point called")
const { client, directory, worktree, project } = input
const sdkClient = client
if (!sdkClient) {
log("V1: No SDK client in PluginInput")
return {}
}
log(`V1: SDK client available from PluginInput`)
// Load config using loadConfig (reads from matrix.json or env vars)
const pluginOptions = options || {}
const { options: configOptions } = await loadConfig(pluginOptions)
// Create a context that runPlugin can use
const v1Ctx = {
location: { directory: directory || process.cwd(), project: project || { id: "v1" } },
storage: {
set: async (_key: string, _value: any) => { /* V1 storage not available */ },
get: async (_key: string) => null,
},
session: {}, // Not V2 context
event: {}, // Not V2 context
}
await runPlugin(v1Ctx as any, configOptions, sdkClient)
return {}
} catch (err) {
log(`V1 server error: ${String(err)}`)
return {}
}
}
// =============================================================================
// V2 Setup Function
// =============================================================================
@ -452,24 +241,13 @@ async function v2Setup(ctx: any) {
}
// =============================================================================
// Dual V1/V2 Export
// Export
// =============================================================================
// OpenCode 1.18.x calls server(PluginInput, options) where PluginInput contains SDK client.
// V2 reads id + setup() and ignores server().
export default {
id: "matrix-plugin",
async setup(ctx: any) {
const isV2 = isV2Context(ctx)
if (isV2) {
log("Using V2 path (full context)")
return await v2Setup(ctx)
}
log("Using V1 path (server() will be called by OpenCode)")
return await v1Server({}, {})
},
async server(input: any, options?: any) {
log("V1 server() called by OpenCode")
return await v1Server(input, options)
},
}

View File

@ -1,4 +1,3 @@
import crypto from "crypto"
import fs from "fs"
import path from "path"
@ -15,11 +14,6 @@ interface Session {
lastEventIds: Map<string, string>
}
export function encodeSessionId(matrixRoomId: string, threadRootId: string): string {
const hash = crypto.createHash("md5").update(`${matrixRoomId}:${threadRootId}`).digest("hex").slice(0, 16)
return `ses_${hash}`
}
const SESSION_MAP_FILE = path.join(process.env.HOME || "/tmp", ".opencode-matrix-sessions.json")
console.log(`[SessionManager] SESSION_MAP_FILE=${SESSION_MAP_FILE}`)