코드 제로, Dify와 Laf를 사용하여 2분 만에 기업용 WeChat AI 로봇에 연결

원본 링크: https://docs.dify.ai/v/zh-hans/use-cases/integrate-with-wecom-using-dify

Dify는 AI 애플리케이션 생성을 허용하고 보조 개발 기능을 제공합니다. 여기서는 "Zhifa"라는 법률 질문 및 답변 도우미를 위한 AI 애플리케이션 생성을 보여 드리겠습니다. 이 튜토리얼에서는 "Zhifa"에 대한 기업 WeChat에 연결하는 방법을 안내합니다.

준비

  • Enterprise WeChat에 대한 관리자 권한
  • Diify 계정 _
  • Laf 클라우드 계정
  • (선택 사항) OpenAI API 키. 그렇지 않은 경우 Dify에서 무료로 제공하는 200개의 통화 기회를 테스트용으로 사용할 수 있습니다.
  • (선택 사항) 컴퓨터에 새 env.txt 파일을 만들고 다음 콘텐츠를 env.txt에 복사합니다. 다음 튜토리얼에서는 이 파일에 관련 정보를 단계별로 입력하겠습니다. 정보 저장이 필요한 단계는 강조 표시됩니다.
WXWORK_TOKEN=""
WXWORK_AESKEY=""
WXWORK_CORPID=""
WXWORK_AGENTID=""
WXWORK_CORPSECRET=""
DIFY_APPTOKEN=""

Diify에서 앱 만들기

이 장에서는 법률 지식 데이터 세트를 생성하고 데이터 세트를 애플리케이션과 연결하는 방법을 소개합니다.

법률 지식 데이터 세트 구축

데이터세트 구축에 대한 추가 작업은 언제든지 문서를 확인하세요. [데이터세트 관리]

'법을 알라'가 더 많은 맥락을 이해하기 위해서는 법률지식의 데이터베이스를 구축해야 합니다.

  • **문서 가져오기:**컴퓨터에서 법률 지식 PDF 문서를 가져옵니다.
  • 텍스트 분할 및 정리 : 업로드된 텍스트는 대규모 언어 모델에서 이해되기 전에 2차 처리를 거쳐야 합니다. 여기서는 특정 구현 논리에 주의할 필요가 없습니다. 자동 분할을 선택한 다음 "저장 및 처리"를 클릭하면 됩니다.
  • **텍스트 임베딩:** 약 30초 안에 데이터 세트가 성공적으로 생성됩니다. 언제든지 돌아와서 데이터베이스에 더 많은 파일을 추가할 수 있습니다.

구축된 애플리케이션

애플리케이션 생성에 대한 자세한 내용은 언제든지 설명서를 확인하세요. [애플리케이션 생성]

  • **애플리케이션 만들기:** 그림의 지침에 따라 대화형 애플리케이션을 만들고 이름을 "Zhifa"로 지정합니다.
  • **연관된 데이터 세트:** "프롬프트 단어 정렬" 페이지에서 "컨텍스트" 모듈에서 방금 생성한 데이터 세트를 추가하고 선택합니다.
  • **모델 게시:** 관련 데이터 세트를 완료한 후 페이지 오른쪽 상단에 있는 '게시'를 클릭하여 모델을 적용하세요.
  • **API 액세스 키를 받으세요. **"Access API" 페이지에서 API 키를 생성하고 복사하여 DIFY_APPTOKEN. 재산상의 손해를 방지하기 위해 열쇠를 타인에게 유출하지 않도록 주의하시기 바랍니다.

기업용 WeChat 애플리케이션 만들기

  • **기업 정보 기록: **기업 WeChat 관리 배경 - My Enterprise를 입력하고 여기에 기업 ID를 다음과 같이 기록합니다.WXWORK_CORPID
  • **기업용 WeChat 애플리케이션 생성:** 애플리케이션 관리 페이지에 들어가서 [애플리케이션 생성]을 클릭하여 생성 페이지로 들어가서 애플리케이션 정보를 입력한 후 [애플리케이션 생성]을 클릭합니다. 이미 기존 애플리케이션이 있는 경우 이 단계를 건너뛸 수 있습니다.
  • **기업 WeChat 애플리케이션 정보 기록:** 애플리케이션 관리 페이지에서 방금 생성된 애플리케이션을 클릭하여 애플리케이션 세부정보 페이지로 들어갑니다. 각각 WXWORK_AGENTID 및 WXWORK_CORPSECRET인 AgentId 및 Secret을 여기에 기록합니다(회사 WeChat 채팅 창에서 가져오려면 가져오기 버튼을 클릭해야 함).
  • **기업 WeChat 애플리케이션이 정보를 수신합니다:** 애플리케이션 세부정보 페이지의 메시지 수신 영역에서 [API 수신 설정]을 클릭합니다.

API 메시지 수신 페이지에서 두 개의 [Randomly Get] 버튼을 클릭하면 각각 WXWORK_TOKEN 및 WXWORK_AESKEY로 기록되는 토큰 및 EncodingAESKey가 자동으로 생성됩니다. 참고로 이 페이지를 닫지 마세요. Laf 측이 구성된 후 URL을 입력하겠습니다.

Laf Cloud에서 클라우드 기능 만들기

  • **새 Laf 클라우드 애플리케이션 생성:** Laf를 입력한 후 New를 클릭하여 클라우드 애플리케이션을 생성합니다. 여기에서 무료 플랜을 선택하세요.
  • @wecom/crypto**종속성 추가:** 기업 WeChat 애플리케이션은 두 가지 종속성을 추가해야 합니다 xml2js. 추가되면 종속성 목록이 아래와 같아야 합니다.
  • **환경 변수 추가:** 두 번째 줄부터 위 단계에서 수집한 모든 내용을 여기에 붙여넣고 업데이트를 클릭합니다.
  • **클라우드 기능 생성: **클라우드 기능을 생성하려면 클릭하세요. "요청 방법"이 선택되어 있는지 확인 POST하고 GET확인을 클릭하세요.

클라우드 함수를 생성한 후 기본 코드를 삭제하고 여기에 글 마지막에 있는 "부록"에 있는 코드를 모두 붙여넣으세요.

  • **클라우드 기능 게시:** 게시를 클릭하면 클라우드 기능이 적용됩니다.

이제 Enterprise WeChat 백엔드의 [API 수신 설정] 페이지 바로 왼쪽 빈 공간에 URL을 붙여넣은 후 저장을 클릭하세요.

  • **IP 화이트리스트 구성:** WeChat Enterprise에서 방금 생성한 애플리케이션을 찾아 메시지를 보내세요. 예상대로 메시지가 수신되지 않았습니다. 이는 Enterprise WeChat이 기본적으로 Laf Cloud의 IP를 차단하기 때문입니다.

로그를 클릭하면 ' not allow to access from your ip' 와 같은 오류가 표시됩니다.

이 로그의 세부 정보를 보고 로그에 제공된 Laf 클라우드 IP를 기록하려면 클릭하세요.

Enterprise WeChat의 관리 배경으로 돌아가서 방금 생성한 애플리케이션을 클릭하고 애플리케이션에 대해 실행 가능한 IP를 구성합니다.

방금 로그에 기록된 IP를 입력하시면 됩니다.

효과 확인

  1. **채팅 테스트:** WeChat Enterprise에서 방금 만든 애플리케이션을 찾아 메시지를 보내세요. 이제 푸시 메시지를 받을 수 있습니다.

인용하다

이 심층적인 참고자료는 다음 글을 참고하였으며, 원저자의 노고에 감사의 말씀을 전하고 싶습니다. https://forum.laf.run/d/556/3

부록

기업 WeChat 애플리케이션 코드 - (의사 스트리밍 응답)

import cloud from '@lafjs/cloud'
import { decrypt, getSignature } from '@wecom/crypto'
import xml2js from 'xml2js'

function genConversationKey(userName) {
  return `${process.env.WXWORK_AGENTID}:${userName}`
}

function genWxAppAccessTokenKey() {
  return `${process.env.WXWORK_AGENTID}:access-token`
}

async function getToken() {
  console.log('[getToken] called')

  const cache = cloud.shared.get(genWxAppAccessTokenKey())
  if (cache && cache.expires >= Date.now()) return cache.token

  const res = await cloud.fetch({
    url: 'https://qyapi.weixin.qq.com/cgi-bin/gettoken',
    method: 'GET',
    params: {
      corpid: process.env.WXWORK_CORPID,
      corpsecret: process.env.WXWORK_CORPSECRET,
    }
  })

  const token = res.data.access_token
  cloud.shared.set(genWxAppAccessTokenKey(), { token, expires: Date.now() + res.data.expires_in * 1000 })
  return token
}

async function sendWxMessage(message, user) {
  console.log('[sendWxMessage] called', user, message)

  const res = await cloud.fetch({
    url: 'https://qyapi.weixin.qq.com/cgi-bin/message/send',
    method: 'POST',
    params: {
      access_token: await getToken()
    },
    data: {
      "touser": user,
      "msgtype": "text",
      "agentid": process.env.WXWORK_AGENTID,
      "text": {
        "content": message
      },
      "safe": 0,
      "enable_id_trans": 0,
      "enable_duplicate_check": 0,
      "duplicate_check_interval": 1800
    },
  })
  console.log('[sendWxMessage] received', res.data)
}

async function sendDifyMessage(message, userName, onMessage) {
  console.log('[sendDifyMessage] called', message, userName)

  const conversationId = cloud.shared.get(genConversationKey(userName)) || null
  let newConversationId = ''
  let responseText = ''

  try {
    const response = await cloud.fetch({
      url: 'https://api.dify.ai/v1/chat-messages',
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.DIFY_APPTOKEN}`
      },
      data: {
        inputs: {},
        response_mode: "streaming",
        query: message,
        user: userName,
        conversation_id: conversationId
      },
      responseType: "stream"
    })

    let firstHalfMessage = ''
    response.data.on('data', (data) => {
      let message = data.toString()
      try {
        if (firstHalfMessage) {
          message += firstHalfMessage
          firstHalfMessage = ''
        }

        // 检查是不是sse协议
        if (!message.startsWith('data: ')) return

        const parsedChunk: Record<string, any> = JSON.parse(message.substring(6))

        if (!newConversationId) {
          newConversationId = parsedChunk.conversation_id
          cloud.shared.set(genConversationKey(userName), newConversationId)
        }
        const { answer } = parsedChunk
        responseText += answer

        // 伪流式响应
        if (answer.endsWith('\n\n') || (responseText.length > 120 && /[?。;!]$/.test(responseText))) {
          onMessage(responseText.replace('\n\n', ''))
          console.log('[sendDifyMessage] received', responseText, newConversationId)
          responseText = ''
        }
      } catch (e) {
        firstHalfMessage = message
        console.error('[sendDifyMessage] error', message)
      }

    })

    // stream结束时把剩下的消息全部发出去
    response.data.on('end', () => {
      onMessage(responseText.replace('\n\n', ''))
    })
  } catch (e) {
    console.error("[sendDifyMessage] error", e)
  }
}

async function asyncSendMessage(xml) {
  console.log('[asyncSendMessage] called', xml)

  if (xml.MsgType[0] !== 'text') return

  const message = xml.Content[0]
  const userName = xml.FromUserName[0]

  if (message === '/new') {
    // 重置conversation id
    cloud.shared.set(genConversationKey(userName), null)
    sendWxMessage('新建成功,开始新的对话吧~~', userName)
    return
  }

  sendWxMessage('AI思考中, 请耐心等待~~', userName)

  try {
    sendDifyMessage(message, userName, (message) => {
      sendWxMessage(message, userName)
    })
  }
  catch (e) {
    console.error('[sendDifyMessage] error', e)
    sendWxMessage('接口请求失败,请联系管理员查看错误信息', userName)
  }
}

export default async function (ctx: FunctionContext) {
  const { query } = ctx
  const { msg_signature, timestamp, nonce, echostr } = query
  const token = process.env.WXWORK_TOKEN
  const key = process.env.WXWORK_AESKEY
  console.log('[main] called', ctx.method, ctx.request.url)

  // 签名验证专用
  if (ctx.method === 'GET') {
    const signature = getSignature(token, timestamp, nonce, echostr)
    if (signature !== msg_signature) {
      return { message: '签名验证失败', code: 401 }
    }
    const { message } = decrypt(key, echostr)
    return message
  }

  const payload = ctx.body.xml
  const encrypt = payload.encrypt[0]
  const signature = getSignature(token, timestamp, nonce, encrypt)
  if (signature !== msg_signature) {
    return { message: '签名验证失败', code: 401 }
  }

  const { message } = decrypt(key, encrypt)
  const {
    xml
  } = await xml2js.parseStringPromise(message)
  // 由于GPT API耗时较久,这里提前返回,防止企业微信超时重试,后续再手动调用发消息接口
  ctx.response.sendStatus(200)

  await asyncSendMessage(xml)

  return { message: true, code: 0 }
}

추천

출처blog.csdn.net/alex_yangchuansheng/article/details/132560912