feat: support opencode

This commit is contained in:
weishu
2026-01-29 10:34:57 +08:00
parent 66ab96bf38
commit 70b5c22c8f
47 changed files with 2737 additions and 28 deletions
+135
View File
@@ -0,0 +1,135 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const PLUGIN_FILENAME = 'hapi-hook.ts';
function buildPluginSource(hookUrl: string, token: string): string {
const escapedUrl = JSON.stringify(hookUrl);
const escapedToken = JSON.stringify(token);
return [
'// Generated by HAPI. Do not edit manually.',
'',
`const DEFAULT_HOOK_URL = ${escapedUrl};`,
`const DEFAULT_HOOK_TOKEN = ${escapedToken};`,
'const HOOK_URL = process.env.HAPI_OPENCODE_HOOK_URL || DEFAULT_HOOK_URL;',
'const HOOK_TOKEN = process.env.HAPI_OPENCODE_HOOK_TOKEN || DEFAULT_HOOK_TOKEN;',
'',
'const EVENT_NAMES = new Set([',
" 'message.updated',",
" 'message.part.updated',",
" 'permission.updated',",
" 'permission.asked',",
" 'permission.replied',",
" 'session.created',",
" 'session.updated',",
" 'tool.execute.before',",
" 'tool.execute.after',",
']);',
'',
'function pickString(value) {',
" return typeof value === 'string' && value.length > 0 ? value : null;",
'}',
'',
'function extractSessionId(value) {',
" if (!value || typeof value !== 'object') return null;",
' const record = value;',
' const direct = (',
' pickString(record.sessionId)',
' || pickString(record.sessionID)',
' || pickString(record.session_id)',
' || (record.session && pickString(record.session.id))',
' );',
' if (direct) return direct;',
' if (record.part && typeof record.part === \'object\') {',
' const nested = extractSessionId(record.part);',
' if (nested) return nested;',
' }',
' if (record.info && typeof record.info === \'object\') {',
' const nested = extractSessionId(record.info);',
' if (nested) return nested;',
' }',
' return null;',
'}',
'',
'function extractSessionIdFromEvent(eventName, payload) {',
' const direct = extractSessionId(payload);',
' if (direct) return direct;',
' if (!payload || typeof payload !== \'object\') return null;',
' const record = payload;',
' if (record.info && typeof record.info === \'object\') {',
' const fromInfo = extractSessionId(record.info);',
' if (fromInfo) return fromInfo;',
' if (eventName.startsWith(\'session.\')) {',
' return pickString(record.info.id);',
' }',
' }',
' return null;',
'}',
'',
'async function sendHook(eventName, payload, sessionId) {',
' if (!HOOK_URL || !HOOK_TOKEN) {',
' return;',
' }',
'',
' const body = JSON.stringify({',
' event: eventName,',
' payload,',
' sessionId',
' });',
'',
' try {',
' await fetch(HOOK_URL, {',
" method: 'POST',",
' headers: {',
" 'Content-Type': 'application/json',",
" 'x-hapi-hook-token': HOOK_TOKEN",
' },',
' body',
' });',
' } catch {',
' // Ignore hook errors to avoid disrupting OpenCode',
' }',
'}',
'',
'export const HapiHookPlugin = async () => {',
' return {',
' event: async ({ event }) => {',
' if (!event || typeof event.type !== \'string\') {',
' return;',
' }',
' if (EVENT_NAMES.size > 0 && !EVENT_NAMES.has(event.type)) {',
' return;',
' }',
' const sessionId = extractSessionIdFromEvent(event.type, event.properties);',
' await sendHook(event.type, event.properties, sessionId);',
' }',
' };',
'};',
''
].join('\\n');
}
function resolvePluginDir(rootPath: string): string {
return join(rootPath, 'plugins');
}
export function ensureOpencodeHookPlugin(rootPath: string, hookUrl: string, token: string): string {
const pluginDir = resolvePluginDir(rootPath);
mkdirSync(pluginDir, { recursive: true });
const pluginPath = join(pluginDir, PLUGIN_FILENAME);
const nextSource = buildPluginSource(hookUrl, token);
try {
const current = readFileSync(pluginPath, 'utf-8');
if (current === nextSource) {
return pluginPath;
}
} catch {
// Ignore missing or unreadable file.
}
writeFileSync(pluginPath, nextSource, 'utf-8');
return pluginPath;
}