index.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import type { Plugin } from "@opencode-ai/plugin"
  2. interface WebhookConfig {
  3. url: string
  4. secret?: string
  5. enabled: boolean
  6. }
  7. export const StatusLightPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
  8. // 默认配置,可以通过环境变量或配置文件覆盖
  9. const config: WebhookConfig = {
  10. url: process.env.OPENCODE_WEBHOOK_URL || "http://localhost:8080/api/webhook",
  11. secret: process.env.OPENCODE_WEBHOOK_SECRET || "",
  12. enabled: process.env.OPENCODE_WEBHOOK_ENABLED !== "false",
  13. }
  14. console.log(`[StatusLight] Plugin initialized`)
  15. console.log(`[StatusLight] Webhook URL: ${config.url}`)
  16. console.log(`[StatusLight] Webhook enabled: ${config.enabled}`)
  17. // 发送webhook请求
  18. async function sendWebhook(eventType: string, data: any) {
  19. if (!config.enabled) {
  20. return
  21. }
  22. try {
  23. const payload = {
  24. type: eventType,
  25. timestamp: new Date().toISOString(),
  26. project: project?.name || "unknown",
  27. directory,
  28. worktree,
  29. data,
  30. }
  31. const headers: Record<string, string> = {
  32. "Content-Type": "application/json",
  33. }
  34. // 如果配置了密钥,添加签名头
  35. if (config.secret) {
  36. const encoder = new TextEncoder()
  37. const key = await crypto.subtle.importKey(
  38. "raw",
  39. encoder.encode(config.secret),
  40. { name: "HMAC", hash: "SHA-256" },
  41. false,
  42. ["sign"]
  43. )
  44. const signature = await crypto.subtle.sign(
  45. "HMAC",
  46. key,
  47. encoder.encode(JSON.stringify(payload))
  48. )
  49. const signatureArray = Array.from(new Uint8Array(signature))
  50. const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, "0")).join("")
  51. headers["X-Webhook-Signature"] = `sha256=${signatureHex}`
  52. }
  53. const response = await fetch(config.url, {
  54. method: "POST",
  55. headers,
  56. body: JSON.stringify(payload),
  57. })
  58. if (!response.ok) {
  59. console.error(`[StatusLight] Webhook failed: ${response.status} ${response.statusText}`)
  60. }
  61. } catch (error) {
  62. console.error(`[StatusLight] Webhook error:`, error)
  63. }
  64. }
  65. return {
  66. // 工具执行前
  67. "tool.execute.before": async (input, output) => {
  68. console.log(`[StatusLight] Tool executing: ${input.tool}`)
  69. await sendWebhook("tool.execute.before", {
  70. tool: input.tool,
  71. args: output.args,
  72. status: "running",
  73. })
  74. },
  75. // 工具执行后
  76. "tool.execute.after": async (input, output) => {
  77. console.log(`[StatusLight] Tool completed: ${input.tool}`)
  78. await sendWebhook("tool.execute.after", {
  79. tool: input.tool,
  80. args: output.args,
  81. result: output.result,
  82. status: "completed",
  83. })
  84. },
  85. // 会话状态变化
  86. "session.status": async (input, output) => {
  87. console.log(`[StatusLight] Session status: ${JSON.stringify(input)}`)
  88. await sendWebhook("session.status", {
  89. status: input,
  90. })
  91. },
  92. // 会话空闲
  93. "session.idle": async (input, output) => {
  94. console.log(`[StatusLight] Session idle`)
  95. await sendWebhook("session.idle", {
  96. status: "idle",
  97. })
  98. },
  99. // 会话错误
  100. "session.error": async (input, output) => {
  101. console.log(`[StatusLight] Session error: ${JSON.stringify(input)}`)
  102. await sendWebhook("session.error", {
  103. error: input,
  104. })
  105. },
  106. // 权限请求
  107. "permission.asked": async (input, output) => {
  108. console.log(`[StatusLight] Permission requested: ${JSON.stringify(input)}`)
  109. await sendWebhook("permission.asked", {
  110. permission: input,
  111. })
  112. },
  113. // 消息更新(包含工具状态)
  114. "message.part.updated": async (input, output) => {
  115. const part = input as any
  116. if (part.type === "tool") {
  117. console.log(`[StatusLight] Tool part updated: ${part.tool} - ${part.state?.status}`)
  118. await sendWebhook("message.part.updated", {
  119. part: {
  120. type: part.type,
  121. tool: part.tool,
  122. state: part.state,
  123. },
  124. })
  125. } else if (part.type === "reasoning") {
  126. console.log(`[StatusLight] Reasoning part updated`)
  127. await sendWebhook("message.part.updated", {
  128. part: {
  129. type: part.type,
  130. status: "reasoning",
  131. },
  132. })
  133. }
  134. },
  135. }
  136. }