Files
wasm-go/cmd/wasm/main.go
lychang 8a15a1bdf4 update
2026-05-28 22:19:21 +08:00

793 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"fmt"
"strings"
"syscall/js"
"wasm-go/internal/api"
"wasm-go/internal/excel"
"wasm-go/internal/prompt"
)
func createErrorJSValue(msg string) js.Value {
errorObj := map[string]string{"error": msg}
jsonBytes, _ := json.Marshal(errorObj)
return js.Global().Get("JSON").Call("parse", string(jsonBytes))
}
var DefaultLLMConfig = LLMConfig{
Provider: "openai",
BaseURL: "https://api.deepseek.com",
Model: "deepseek-v4-flash",
APIKey: "sk-c2f04b4d635b4df0aace0432f3c6d6f4",
}
func main() {
// 注册各种功能函数
js.Global().Set("analyzeCompanyIndustryAsync", js.FuncOf(analyzeCompanyIndustryAsync))
js.Global().Set("parseExcelAsync", js.FuncOf(parseExcelAsync))
js.Global().Set("convertExcelToJsonViaLLM", js.FuncOf(convertExcelToJsonViaLLM))
js.Global().Set("callLLMAsync", js.FuncOf(callLLMAsync))
js.Global().Set("callTaxHealthAsync", js.FuncOf(callTaxHealthAsync))
js.Global().Set("queryTaxResultAsync", js.FuncOf(queryTaxResultAsync))
js.Global().Set("getPrompt", js.FuncOf(getPrompt))
// 保持程序运行
select {} // 无限循环,保持程序运行
}
// analyzeCompanyIndustryAsync 异步版本的函数,返回Promise
func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
// 获取公司名称参数和配置参数
companyName := args[0].String()
configJSON := args[1].String() // 配置参数,可选
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1) // 使用error类型而不是interface{}
// 在goroutine中执行异步操作
go func() {
config := parseLLMConfig(configJSON)
// 创建可配置的LLM客户端
llmClient := createConfigurableLLMClient(config)
// 创建企业行业分类器
classifier := api.NewCompanyIndustryClassifier(llmClient)
// 分析企业行业
result, err := classifier.ClassifyCompany(companyName)
if err != nil {
// 发送错误到错误通道
errorChan <- err
} else {
// 发送结果到结果通道,包含使用的配置信息
resultWithConfig := map[string]interface{}{
"data": result,
}
resultChan <- resultWithConfig
}
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// parseExcelAsync 解析Excel文件的异步函数
func parseExcelAsync(this js.Value, args []js.Value) interface{} {
// 获取Excel文件的ArrayBuffer
arrayBuffer := args[0]
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
// 创建字节切片
length := uint8Array.Get("length").Int()
excelBytes := make([]byte, length)
// 将数据复制到Go字节切片
for i := 0; i < length; i++ {
excelBytes[i] = byte(uint8Array.Index(i).Int())
}
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 创建Excel读取器
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
if err != nil {
errorChan <- err
return
}
// 读取所有工作表
sheetNames, err := reader.GetSheetNames()
if err != nil {
errorChan <- err
return
}
sheets := make(map[string][][]string)
sheetErrors := make(map[string]string)
for _, sheetName := range sheetNames {
sheetData, err := reader.GetSheetData(sheetName)
if err != nil {
sheetErrors[sheetName] = err.Error()
continue
}
sheets[sheetName] = sheetData
}
result := map[string]interface{}{
"sheets": sheets,
}
if len(sheetErrors) > 0 {
result["errors"] = sheetErrors
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// getPrompt 获取提示词
func getPrompt(this js.Value, args []js.Value) interface{} {
period := args[0].String()
// 根据period映射相应的prompt
var pmt string
switch period {
case "Q1", "第一季度":
pmt = prompt.Quarterly2Prompt
case "Q2", "第二季度":
pmt = prompt.Quarterly2Prompt
case "Q3", "第三季度":
pmt = prompt.Quarterly3Prompt
case "年度":
pmt = prompt.AnnualPrompt
default:
pmt = `你是一个专业的财务数据分析师。请分析以下财务报表数据,并将其转换为适合税务健康评估的JSON格式。只需要返回JSON数据,不要包含其他解释文字。返回格式应该是一个包含财务指标及其数值的对象,例如:{"LC0000003E_T0I0331": 741213833.14, "LC0000003E_T0I0101": 578064138.06}`
}
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
go func() {
allows := []string{"Q1", "第一季度", "Q2", "第二季度", "Q3", "第三季度", "年度"}
isValid := false
for _, allowed := range allows {
if allowed == period {
isValid = true
break
}
}
if isValid {
result := map[string]interface{}{
"prompt": pmt,
"period": period,
}
resultChan <- result
} else {
errorChan <- fmt.Errorf("所选周期错误,支持的周期: Q1, 第一季度, Q2, 第二季度, Q3, 第三季度, 年度")
return
}
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// convertExcelToJsonViaLLM 将Excel数据通过LLM转换为JSON格式的函数
func convertExcelToJsonViaLLM(this js.Value, args []js.Value) interface{} {
// 获取Excel文件的ArrayBuffer和参数
arrayBuffer := args[0]
period := args[1].String() // 期间参数 (Q1, Q2, Q3, Q4, 年度等)
configJSON := args[2].String() // 配置参数,可选
// 根据period映射相应的prompt
var pmt string
switch period {
case "Q1", "第一季度":
pmt = prompt.Quarterly1Prompt
case "Q2", "第二季度":
pmt = prompt.Quarterly2Prompt
case "Q3", "第三季度":
pmt = prompt.Quarterly3Prompt
case "年度":
pmt = prompt.AnnualPrompt
default:
pmt = `你是一个专业的财务数据分析师。请分析以下财务报表数据,并将其转换为适合税务健康评估的JSON格式。只需要返回JSON数据,不要包含其他解释文字。返回格式应该是一个包含财务指标及其数值的对象,例如:{"LC0000003E_T0I0331": 741213833.14, "LC0000003E_T0I0101": 578064138.06}`
}
// 创建字节切片
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
length := uint8Array.Get("length").Int()
excelBytes := make([]byte, length)
// 将数据复制到Go字节切片
for i := 0; i < length; i++ {
excelBytes[i] = byte(uint8Array.Index(i).Int())
}
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 创建Excel读取器
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
if err != nil {
errorChan <- err
return
}
// 读取所有工作表
sheetNames, err := reader.GetSheetNames()
if err != nil {
errorChan <- err
return
}
sheets := make(map[string][][]string)
sheetErrors := make(map[string]string)
for _, sheetName := range sheetNames {
sheetData, err := reader.GetSheetData(sheetName)
if err != nil {
sheetErrors[sheetName] = err.Error()
continue
}
sheets[sheetName] = sheetData
}
if len(sheetErrors) > 0 {
errorChan <- fmt.Errorf("部分工作表读取失败: %v", sheetErrors)
return
}
var allMarkdownTables strings.Builder
allMarkdownTables.WriteString("以下是Excel表格的数据:\n\n")
for sheetName, data := range sheets {
if len(data) == 0 {
continue
}
allMarkdownTables.WriteString(fmt.Sprintf("## 工作表: %s\n", sheetName))
markdown := excel.ConvertToMarkdown(data)
allMarkdownTables.WriteString(markdown)
allMarkdownTables.WriteString("\n\n")
}
// 合并提示词和表格数据
finalPrompt := fmt.Sprintf("%s\n\n%s", pmt, allMarkdownTables.String())
config := parseLLMConfig(configJSON)
// 创建可配置的LLM客户端
llmClient := createConfigurableLLMClient(config)
// 调用大模型
response, err := llmClient.Chat([]api.ChatMessage{
{Role: "user", Content: finalPrompt},
})
if err != nil {
errorChan <- err
return
}
if response == nil {
errorChan <- fmt.Errorf("LLM响应为空")
return
}
// 解析LLM返回的响应,提取JSON内容
parsedJSON := extractJSONFromResponse(response.Choices[0].Message.Content)
// 尝试将解析的JSON字符串转换为map结构
var jsonData map[string]interface{}
if err := json.Unmarshal([]byte(parsedJSON), &jsonData); err != nil {
// 如果解析失败,返回原始字符串
result := map[string]interface{}{
"raw_response": response.Choices[0].Message.Content,
"parsed_json": parsedJSON,
"conversion": "success",
"parse_error": err.Error(),
}
resultChan <- result
} else {
// 成功解析为JSON对象
result := map[string]interface{}{
"json_data": jsonData,
"raw_response": response.Choices[0].Message.Content,
"conversion": "success",
}
resultChan <- result
}
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// LLMConfig 定义大模型客户端配置
type LLMConfig struct {
Provider string `json:"provider"` // "ollama" 或 "openai"
BaseURL string `json:"base_url"`
Model string `json:"model"`
APIKey string `json:"api_key,omitempty"`
}
func parseLLMConfig(configJSON string) LLMConfig {
var config LLMConfig
if configJSON != "" {
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
fmt.Printf("解析LLM配置失败: %v\n", err)
config = DefaultLLMConfig
}
}
if config.Provider == "" || config.Provider == "default" {
config = DefaultLLMConfig
}
return config
}
// createConfigurableLLMClient 创建可配置的大模型客户端
func createConfigurableLLMClient(config LLMConfig) *api.LLMClient {
switch strings.ToLower(config.Provider) {
case "ollama":
return api.NewOllamaClient(config.BaseURL, config.Model)
case "openai":
return api.NewOpenAIClient(config.APIKey, config.Model, config.BaseURL)
default:
// 默认使用Ollama
return api.NewOllamaClient(config.BaseURL, config.Model)
}
}
// extractJSONFromResponse 从LLM响应中提取JSON内容
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
func extractJSONFromResponse(response string) string {
// 首先尝试匹配 ```json 开头的代码块
start := strings.Index(response, "```json")
if start != -1 {
start += 7 // 跳过 "```json"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
return jsonContent
}
}
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
start = strings.Index(response, "```")
if start != -1 {
start += 3 // 跳过 "```"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
// 检查是否看起来像JSON
jsonContent = strings.TrimSpace(jsonContent)
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
return jsonContent
}
}
}
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
response = strings.TrimSpace(response)
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
return response
}
// 如果以上都失败,返回原内容
return response
}
// callLLMAsync 调用大模型的异步函数
func callLLMAsync(this js.Value, args []js.Value) interface{} {
// 获取提示词参数和配置参数
prompt := args[0].String()
configJSON := args[1].String() // 配置参数,可选
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
config := parseLLMConfig(configJSON)
// 创建可配置的LLM客户端
llmClient := createConfigurableLLMClient(config)
// 调用大模型
response, err := llmClient.Chat([]api.ChatMessage{
{Role: "user", Content: prompt},
})
if response == nil {
errorChan <- fmt.Errorf("LLM响应为空")
return
}
if err != nil {
errorChan <- err
return
}
result := map[string]interface{}{
"response": response.Choices[0].Message.Content,
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// callTaxHealthAsync 调用税务健康API的异步函数
// 注意:由于WASM环境的CORS限制,我们需要通过后端代理调用真实API
func callTaxHealthAsync(this js.Value, args []js.Value) interface{} {
// 获取公司信息参数
companyInfoJSON := args[0].String()
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 解析输入的JSON参数
var requestData map[string]interface{}
err := json.Unmarshal([]byte(companyInfoJSON), &requestData)
if err != nil {
errorChan <- err
return
}
// 从输入数据中提取各个字段
reqNo, _ := requestData["req_no"].(string)
callback, _ := requestData["callback"].(string)
companyName, _ := requestData["company_name"].(string)
period, _ := requestData["period"].(string)
year, _ := requestData["year"].(string)
cit, _ := requestData["cit"].(float64)
vat, _ := requestData["vat"].(float64)
inputVat, _ := requestData["input_vat"].(float64)
outputVat, _ := requestData["output_vat"].(float64)
preCIT, _ := requestData["pre_cit"].(float64)
preVAT, _ := requestData["pre_vat"].(float64)
preInputVat, _ := requestData["pre_input_vat"].(float64)
preOutputVat, _ := requestData["pre_output_vat"].(float64)
// 检查是否提供了Excel文件URL
fileUrl, hasFileUrl := requestData["file_url"].(string)
// 从requestData中获取data字段
data, hasData := requestData["data"].(map[string]interface{})
if !hasData {
// 尝试将整个 requestData 的 data 部分作为map
if dataSlice, ok := requestData["data"].([]interface{}); ok {
// 转换slice为map
data = make(map[string]interface{})
for _, item := range dataSlice {
if itemMap, ok := item.(map[string]interface{}); ok {
for k, v := range itemMap {
data[k] = v
}
}
}
hasData = true
}
}
// 创建税务健康服务客户端
// 注意:在真实的实现中,这些应该是安全存储的
taxHealthService := api.NewTaxHealthService("1554047490758508546", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 构建税务评估请求
request := api.TaxEvalRequest{
ReqNo: reqNo,
Callback: callback,
Detail: api.TaxEvalDetail{
CompanyName: companyName,
Period: period,
Year: year,
CIT: cit,
VAT: vat,
InputVAT: inputVat,
OutputVAT: outputVat,
PreCIT: preCIT,
PreVAT: preVAT,
PreInputVAT: preInputVat,
PreOutputVAT: preOutputVat,
},
}
// 如果有文件URL,则使用文件URL;否则使用数据
if hasFileUrl && fileUrl != "" {
request.Detail.FileURL = fileUrl
} else if hasData {
request.Detail.Data = data
}
// 调用税务健康API
taxResponse, err := taxHealthService.EvaluateTaxHealth(request)
if err != nil {
errorChan <- err
return
}
// 检查是否成功获取订单号
if taxResponse.Code != 200 || taxResponse.Data.OrderNo == "" {
errorChan <- fmt.Errorf("税务健康API调用失败或未返回订单号: %+v", taxResponse)
return
}
orderNo := taxResponse.Data.OrderNo
// 轮询查询结果,最多等待5分钟(60次 * 5秒)
// 注意:在WASM环境中,我们不能使用长时间阻塞操作
// 因此这里只是返回订单号,轮询应该在前端JavaScript中处理
result := map[string]interface{}{
"initial_response": taxResponse,
"order_no": orderNo,
"message": "税务评估已提交,正在处理中,请使用订单号查询结果",
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}
// queryTaxResultAsync 查询税务评估结果的异步函数
func queryTaxResultAsync(this js.Value, args []js.Value) interface{} {
// 获取订单号参数
orderNo := args[0].String()
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 创建税务健康服务客户端
taxHealthService := api.NewTaxHealthService("1554047490758508546", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 查询税务评估结果
queryResponse, err := taxHealthService.QueryTaxEvalResult(orderNo)
if err != nil {
errorChan <- err
return
}
result := map[string]interface{}{
"query_response": queryResponse,
"message": "查询税务评估结果成功",
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
jsonResult, err := json.Marshal(result)
if err != nil {
reject.Invoke(createErrorJSValue("JSON序列化失败: " + err.Error()))
return
}
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
reject.Invoke(createErrorJSValue(err.Error()))
}
}()
return nil
}))
return promise
}