This commit is contained in:
lychang
2026-05-21 01:57:25 +08:00
parent 7f5350db62
commit 10905c0acc
15 changed files with 7271 additions and 6387 deletions

View File

@@ -6,6 +6,7 @@ import (
"os"
"strings"
"time"
prompt2 "wasm-go/internal/prompt"
"wasm-go/internal/api"
"wasm-go/internal/excel"
@@ -73,13 +74,7 @@ func main() {
fmt.Println("\n✅ 所有工作表已转换为Markdown格式!")
// 读取quarterly1.txt的prompt
promptBytes, err := os.ReadFile("./quarterly1.txt")
if err != nil {
fmt.Printf("❌ 读取quarterly1.txt失败: %v\n", err)
return
}
prompt := string(promptBytes)
prompt := prompt2.Quarterly1Prompt
// 合并prompt和markdown表格
finalPrompt := fmt.Sprintf("%s\n\n%s", prompt, allMarkdownTables.String())
@@ -117,7 +112,7 @@ func main() {
// 创建税务健康服务客户端
// 注意:这里需要替换为实际的用户ID和私钥
taxHealthService := api.NewTaxHealthService("1560142251348287489", "")
taxHealthService := api.NewTaxHealthService("1554047490758508546", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 将解析的JSON解析为map结构
var companyData map[string]interface{}
@@ -125,13 +120,14 @@ func main() {
fmt.Printf("❌ 解析JSON数据失败: %v\n", err)
return
}
// 构建税务评估请求
request := api.TaxEvalRequest{
ReqNo: fmt.Sprintf("REQ%v", time.Now().Unix()), // 使用时间戳生成请求号
Detail: api.TaxEvalDetail{
CompanyName: "企业名称", // 可以从数据中提取公司名称
CompanyName: "郑州惠兴", // 可以从数据中提取公司名称
Data: companyData,
Period: "Q1",
Year: "2024",
},
}
@@ -145,6 +141,70 @@ func main() {
fmt.Println("\n========== 税务健康API反馈结果 ==========")
fmt.Printf("API响应: %+v\n", taxResponse)
fmt.Println("======================================")
// 检查是否成功获取订单号
if taxResponse.Code != 200 || taxResponse.Data.OrderNo == "" {
fmt.Printf("❌ 税务健康API调用失败或未返回订单号: %+v\n", taxResponse)
return
}
orderNo := taxResponse.Data.OrderNo
fmt.Printf("\n🔄 获取到订单号: %s,开始轮询查询结果...\n", orderNo)
// 轮询查询结果,最多等待5分钟(60次 * 5秒)
maxAttempts := 60
attempt := 0
for attempt < maxAttempts {
// 查询税务评估结果
queryResponse, err := taxHealthService.QueryTaxEvalResult(orderNo)
if err != nil {
fmt.Printf("❌ 查询税务评估结果失败: %v\n", err)
return
}
fmt.Printf("\n========== 查询结果响应 ==========\n")
fmt.Printf("状态码: %d\n", queryResponse.Code)
fmt.Printf("消息: %s\n", queryResponse.Message)
fmt.Printf("订单状态: %d\n", queryResponse.Data.Status)
fmt.Printf("================================\n")
// 根据状态码判断处理结果
switch queryResponse.Data.Status {
case 99: // 执行成功
fmt.Println("\n✅ 税务评估已完成!")
fmt.Printf("结果文件地址: %v\n", queryResponse.Data.Detail.Files)
// 如果有错误信息也显示
if queryResponse.Data.Detail.Error != "" {
fmt.Printf("警告信息: %s\n", queryResponse.Data.Detail.Error)
}
// 这里可以进一步处理结果文件,比如下载文件
for _, fileURL := range queryResponse.Data.Detail.Files {
fmt.Printf("文件URL: %s\n", fileURL)
// TODO: 实现文件下载逻辑
}
return // 完成任务,退出程序
case 0: // 正在执行
fmt.Printf("⏳ 任务仍在处理中... (%d/%d)\n", attempt+1, maxAttempts)
time.Sleep(5 * time.Second) // 等待5秒后再次查询
attempt++
case -1: // 无效订单
fmt.Printf("❌ 订单无效: %s\n", queryResponse.Data.Detail.Error)
return
case 98: // 执行失败
fmt.Printf("❌ 任务执行失败: %s\n", queryResponse.Data.Detail.Error)
return
default:
fmt.Printf("⚠️ 未知状态码: %d\n", queryResponse.Data.Status)
time.Sleep(5 * time.Second)
attempt++
}
}
fmt.Println("\n⏰ 查询超时,建议稍后手动查询结果")
}
// extractJSONFromResponse 从LLM响应中提取JSON内容

View File

@@ -1,55 +0,0 @@
package main
import (
"fmt"
"log"
"wasm-go/internal/api"
)
func main() {
fmt.Println("=== 企业行业分类示例 ===")
// 创建LLM客户端 - 这里使用Ollama作为示例
// 如果Ollama不可用,也可以使用OpenAI
var llmClient *api.LLMClient
// 尝试连接到Ollama
fmt.Println("尝试连接到Ollama服务...")
llmClient = api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
// 如果需要使用OpenAI,取消下面的注释并替换API KEY
/*
llmClient = api.NewOpenAIClient("your-openai-api-key", "gpt-3.5-turbo")
*/
// 创建企业行业分类器
classifier := api.NewCompanyIndustryClassifier(llmClient)
// 示例公司名称 - 可以从命令行参数获取
companyName := "深圳市致远速联信息技术有限公司"
fmt.Printf("正在分析公司: %s\n", companyName)
// 分析企业行业
result, err := classifier.ClassifyCompany(companyName)
if err != nil {
log.Printf("分析失败: %v", err)
return
}
// 输出结果
fmt.Printf("\n✅ 企业行业分析完成!\n")
fmt.Printf("公司名称: %s\n", result.CompanyName)
fmt.Printf("主要行业: %s - %s\n", result.Primary.Code, result.Primary.Name)
fmt.Printf("判断依据: %s\n", result.Primary.Reason)
if len(result.Secondary) > 0 {
fmt.Printf("次要行业:\n")
for _, sec := range result.Secondary {
fmt.Printf(" %s - %s\n", sec.Code, sec.Name)
fmt.Printf(" 判断依据: %s\n", sec.Reason)
}
} else {
fmt.Printf("次要行业: 无\n")
}
}

View File

@@ -3,17 +3,21 @@ package main
import (
"encoding/json"
"fmt"
"strings"
"syscall/js"
"wasm-go/internal/api"
"wasm-go/internal/excel"
"wasm-go/internal/prompt"
)
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))
// 保持程序运行
select {} // 无限循环,保持程序运行
@@ -21,8 +25,9 @@ func main() {
// analyzeCompanyIndustryAsync 异步版本的函数,返回Promise
func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
// 获取公司名称参数
// 获取公司名称参数和配置参数
companyName := args[0].String()
configJSON := args[1].String() // 配置参数,可选
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
@@ -30,8 +35,26 @@ func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
// 在goroutine中执行异步操作
go func() {
// 创建LLM客户端
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
var config LLMConfig
// 如果提供了配置参数,则解析它;否则使用默认配置
if configJSON != "" {
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
errorChan <- fmt.Errorf("解析LLM配置失败: %v", err)
return
}
} else {
// 使用默认配置
config = LLMConfig{
Provider: "ollama",
BaseURL: "http://localhost:11434",
Model: "qwen3.5:9b",
APIKey: "",
}
}
// 创建可配置的LLM客户端
llmClient := createConfigurableLLMClient(config)
// 创建企业行业分类器
classifier := api.NewCompanyIndustryClassifier(llmClient)
@@ -42,8 +65,12 @@ func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
// 发送错误到错误通道
errorChan <- err
} else {
// 发送结果到结果通道
resultChan <- result
// 发送结果到结果通道,包含使用的配置信息
resultWithConfig := map[string]interface{}{
"data": result,
"config": config,
}
resultChan <- resultWithConfig
}
}()
@@ -185,10 +212,37 @@ func parseExcelAsync(this js.Value, args []js.Value) interface{} {
return promise
}
// callLLMAsync 调用大模型的异步函数
func callLLMAsync(this js.Value, args []js.Value) interface{} {
// 获取提示词参数
prompt := args[0].String()
// 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.Quarterly2Prompt
case "Q2", "第二季度":
pmt = prompt.Quarterly2Prompt
case "Q3", "第三季度":
pmt = prompt.Quarterly2Prompt
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)
@@ -196,18 +250,263 @@ func callLLMAsync(this js.Value, args []js.Value) interface{} {
// 在goroutine中执行异步操作
go func() {
// 创建LLM客户端(可以根据需要选择不同的模型)
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
// 调用大模型
response, err := llmClient.SimpleChat(prompt)
// 创建Excel读取器
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
if err != nil {
errorChan <- err
return
}
result := map[string]string{
"response": response,
// 读取所有工作表
sheetNames, err := reader.GetSheetNames()
if err != nil {
errorChan <- err
return
}
// 创建一个map来存储所有工作表的数据
sheets := make(map[string][][]string)
// 读取每个工作表的数据
for _, sheetName := range sheetNames {
sheetData, err := reader.GetSheetData(sheetName)
if err != nil {
// 记录错误但继续处理其他工作表
continue
}
sheets[sheetName] = sheetData
}
// 将Excel数据转换为markdown格式,便于LLM理解
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())
var config LLMConfig
// 如果提供了配置参数,则解析它;否则使用默认配置
if configJSON != "" {
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
errorChan <- fmt.Errorf("解析LLM配置失败: %v", err)
return
}
} else {
// 使用默认配置
config = LLMConfig{
Provider: "ollama",
BaseURL: "http://localhost:11434",
Model: "qwen3.5:9b",
APIKey: "",
}
}
// 创建可配置的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(),
"config": config,
}
resultChan <- result
} else {
// 成功解析为JSON对象
result := map[string]interface{}{
"json_data": jsonData,
"raw_response": response.Choices[0].Message.Content,
"conversion": "success",
"config": config,
}
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:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": 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"`
}
// 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() {
var config LLMConfig
// 如果提供了配置参数,则解析它;否则使用默认配置
if configJSON != "" {
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
errorChan <- fmt.Errorf("解析LLM配置失败: %v", err)
return
}
} else {
// 使用默认配置
config = LLMConfig{
Provider: "ollama",
BaseURL: "http://localhost:11434",
Model: "qwen3.5:9b",
APIKey: "",
}
}
// 创建可配置的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,
"config": config,
}
resultChan <- result
@@ -255,7 +554,7 @@ func callLLMAsync(this js.Value, args []js.Value) interface{} {
}
// callTaxHealthAsync 调用税务健康API的异步函数
// 注意:由于WASM环境的CORS限制,我们在这里提供模拟实现
// 注意:由于WASM环境的CORS限制,我们需要通过后端代理调用真实API
func callTaxHealthAsync(this js.Value, args []js.Value) interface{} {
// 获取公司信息参数
companyInfoJSON := args[0].String()
@@ -266,30 +565,177 @@ func callTaxHealthAsync(this js.Value, args []js.Value) interface{} {
// 在goroutine中执行异步操作
go func() {
// 由于WASM环境无法直接调用外部API(CORS限制)
// 我们返回一个模拟的响应
simulatedResponse := map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"req_no": "REQ" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
"order_no": "ORDER" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
},
"message": "模拟的税务健康评估结果",
}
// 解析输入的JSON以获取一些信息用于模拟
var companyInfo map[string]interface{}
err := json.Unmarshal([]byte(companyInfoJSON), &companyInfo)
// 解析输入的JSON参数
var requestData map[string]interface{}
err := json.Unmarshal([]byte(companyInfoJSON), &requestData)
if err != nil {
errorChan <- err
return
}
// 根据输入数据添加一些模拟的评估结果
simulatedResponse["company_data"] = companyInfo
// 从输入数据中提取各个字段
reqNo, _ := requestData["req_no"].(string)
callback, _ := requestData["callback"].(string)
companyName, _ := requestData["company_name"].(string)
period, _ := requestData["period"].(string)
year, _ := requestData["year"].(string)
isVerify, _ := requestData["is_verify"].(bool)
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)
resultChan <- simulatedResponse
// 检查是否提供了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("1560142251348287489", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 构建税务评估请求
request := api.TaxEvalRequest{
ReqNo: reqNo,
Callback: callback,
Detail: api.TaxEvalDetail{
CompanyName: companyName,
Period: period,
Year: year,
IsVerify: isVerify,
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:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": 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("1560142251348287489", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 查询税务评估结果
queryResponse, err := taxHealthService.QueryTaxEvalResult(orderNo)
if err != nil {
errorChan <- err
return
}
result := map[string]interface{}{
"query_response": queryResponse,
"message": "查询税务评估结果成功",
}
resultChan <- result
}()
// 创建Promise

7
go.mod
View File

@@ -2,6 +2,9 @@ module wasm-go
go 1.22
require github.com/tjfoc/gmsm v1.4.1
require (
github.com/tjfoc/gmsm v1.4.1
)
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
)

2
go.sum
View File

@@ -26,6 +26,7 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -44,6 +45,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@@ -14,7 +14,7 @@ import (
"time"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/sm3"
"github.com/tjfoc/gmsm/x509"
)
// CompanyInfoService 公司信息查询服务
@@ -122,7 +122,7 @@ func NewTaxHealthService(userID, privateKey string) *TaxHealthService {
}
client := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
Timeout: 60 * time.Second,
}
httpClient := &HTTPClient{Client: client}
@@ -138,8 +138,8 @@ func NewTaxHealthService(userID, privateKey string) *TaxHealthService {
func (s *CompanyInfoService) GetCompanyInfo(companyName string, apis []string) (*CompanyInfoResponse, error) {
// 生成随机数和时间戳
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10)
// 构建查询参数
params := url.Values{}
@@ -183,11 +183,12 @@ func (s *CompanyInfoService) GetCompanyInfo(companyName string, apis []string) (
func (s *TaxHealthService) EvaluateTaxHealth(request TaxEvalRequest) (*TaxEvalResponse, error) {
// 生成随机数和时间戳
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10)
fmt.Println(nonce)
fmt.Println(timestamp)
apiURL := "https://api.cmdp.cn/v1/eval/tax-risk"
fmt.Println(apiURL)
// 序列化请求体
jsonData, err := json.Marshal(request)
fmt.Println(string(jsonData))
@@ -198,6 +199,7 @@ func (s *TaxHealthService) EvaluateTaxHealth(request TaxEvalRequest) (*TaxEvalRe
// 生成签名
signData := s.userID + timestamp + nonce + apiURL + string(jsonData)
signature, err := s.generateSignature([]byte(signData))
fmt.Println(signature)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
@@ -268,55 +270,47 @@ func (s *TaxHealthService) QueryTaxEvalResult(orderNo string) (*TaxEvalQueryResp
// generateSignature 生成签名
func (s *CompanyInfoService) generateSignature(plainText []byte) (string, error) {
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
return sign(plainText, s.privateKey)
}
func sign(data []byte, privateKeyHex string) (string, error) {
prvKeyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
return "", fmt.Errorf("failed to decode private key hex: %v", err)
}
digest := sm3.Sm3Sum(plainText)
signature, err := privateKey.Sign(rand.Reader, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
var prvKey *sm2.PrivateKey
if pkcs8Key, err := x509.ParsePKCS8PrivateKey(prvKeyBytes, nil); err == nil {
prvKey = pkcs8Key
} else {
prvKey = &sm2.PrivateKey{}
prvKey.Curve = sm2.P256Sm2()
prvKey.D = new(big.Int).SetBytes(prvKeyBytes)
if prvKey.D.Sign() == 0 {
return "", fmt.Errorf("failed to parse private key")
}
prvKey.PublicKey.Curve = sm2.P256Sm2()
// 使用ScalarBaseMult计算公钥坐标,这是SM2曲线标准做法
prvKey.PublicKey.X, prvKey.PublicKey.Y = sm2.P256Sm2().ScalarBaseMult(prvKey.D.Bytes())
}
return base64.StdEncoding.EncodeToString(signature), nil
r, s, err := sm2.Sm2Sign(prvKey, data, nil, rand.Reader)
if err != nil {
return "", fmt.Errorf("sign failed: %v", err)
}
sig, err := sm2.SignDigitToSignData(r, s)
if err != nil {
return "", fmt.Errorf("marshal signature failed: %v", err)
}
return base64.StdEncoding.EncodeToString(sig), nil
}
// generateSignature 生成签名
func (s *TaxHealthService) generateSignature(plainText []byte) (string, error) {
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
}
digest := sm3.Sm3Sum(plainText)
signature, err := privateKey.Sign(rand.Reader, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return sign(plainText, s.privateKey)
return base64.StdEncoding.EncodeToString(signature), nil
}
// parsePrivateKeyFromHex 从十六进制字符串解析私钥
func parsePrivateKeyFromHex(privateKeyHex string) (*sm2.PrivateKey, error) {
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("解码私钥失败: %v", err)
}
d := new(big.Int).SetBytes(privateKeyBytes)
privateKey := &sm2.PrivateKey{
PublicKey: sm2.PublicKey{
Curve: sm2.P256Sm2(),
},
D: d,
}
privateKey.PublicKey.X, privateKey.PublicKey.Y = privateKey.PublicKey.Curve.ScalarBaseMult(d.Bytes())
return privateKey, nil
}
// joinAPIParams 连接API参数

View File

@@ -87,7 +87,8 @@ func (c *HTTPClient) Post(url string, data []byte, contentType string, headers m
}(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("POST请求失败,状态码: %d", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("POST请求失败,状态码: %d, 响应体: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)

View File

@@ -37,7 +37,10 @@ type ChatRequest struct {
TopP float64 `json:"top_p,omitempty"` // 采样参数
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` // 频率惩罚
PresencePenalty float64 `json:"presence_penalty,omitempty"` // 存在惩罚
Stream bool `json:"stream,omitempty"` // 是否流式输出
Stream bool `json:"stream"` // 是否流式输出
Thinking struct {
Type string `json:"type"`
} `json:"thinking"`
}
// OllamaMessage Ollama消息格式
@@ -113,10 +116,10 @@ func NewLLMClient(config LLMConfig) *LLMClient {
}
// NewOpenAIClient 创建OpenAI客户端
func NewOpenAIClient(apiKey, model string) *LLMClient {
func NewOpenAIClient(apiKey, model, baseURL string) *LLMClient {
config := LLMConfig{
APIKey: apiKey,
BaseURL: "https://api.openai.com/v1",
BaseURL: baseURL,
Model: model,
Provider: "openai",
Temperature: 0.1,
@@ -149,6 +152,12 @@ func (c *LLMClient) Chat(messages []ChatMessage) (*ChatResponse, error) {
Messages: messages,
Temperature: c.config.Temperature,
MaxTokens: c.config.MaxTokens,
Stream: false,
Thinking: struct {
Type string `json:"type"`
}{
Type: "disabled",
},
}
return c.sendChatRequest(request)

View File

@@ -325,7 +325,7 @@ func TestCreateTaxEvalRequest(t *testing.T) {
}`
// 创建税务健康服务客户端
// 注意:这里需要替换为实际的用户ID和私钥
taxHealthService := NewTaxHealthService("1560142251348287489", "")
taxHealthService := NewTaxHealthService("1554047490758508546", "00d502fcac7a2b0e227751cd6d8b0c725c55f71adcaccc6a50c4d63c73db6c8add")
// 将解析的JSON解析为map结构
var companyData map[string]interface{}
@@ -340,6 +340,8 @@ func TestCreateTaxEvalRequest(t *testing.T) {
Detail: TaxEvalDetail{
CompanyName: "郑州惠兴", // 可以从数据中提取公司名称
Data: companyData,
Period: "Q1",
Year: "2024",
},
}

6188
internal/prompt/prompt.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -218,44 +218,124 @@
background-color: white;
}
.config-btn {
background: inherit;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px 15px;
cursor: pointer;
font-size: 16px;
margin-left: 10px;
box-shadow: none;
}
.config-btn:hover {
background-color: #ddd;
}
.config-panel {
display: none;
background-color: white;
border: 1px solid #ccc;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.config-panel.active {
display: block;
}
footer {
text-align: center;
margin-top: 40px;
color: #95a5a6;
font-size: 0.9em;
}
.json-output {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
border-radius: 5px;
font-family: monospace;
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
}
.config-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.config-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.config-panel {
display: none;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.config-panel.active {
display: block;
}
</style>
</head>
<body>
<header>
<h1>🏭 WASM企业分析工具套件</h1>
<h1>🚀 Excel转JSON工具</h1>
<p class="subtitle">基于WebAssembly的多功能企业数据分析平台</p>
</header>
<div class="container">
<div class="features">
<div class="feature-card">
<span class="feature-icon">📊</span>
<h3 class="feature-title">行业分析</h3>
<p class="feature-desc">基于企业信息的国民经济行业分类</p>
</div>
<div class="feature-card">
<span class="feature-icon">📄</span>
<h3 class="feature-title">Excel解析</h3>
<p class="feature-desc">在浏览器中直接解析Excel文件</p>
</div>
<div class="feature-card">
<span class="feature-icon">🤖</span>
<h3 class="feature-title">AI分析</h3>
<p class="feature-desc">利用大模型进行智能数据分析</p>
</div>
<div class="feature-card">
<span class="feature-icon">💰</span>
<h3 class="feature-title">税务健康</h3>
<p class="feature-desc">企业税务健康状况评估</p>
<!-- 顶部导航栏 -->
<div class="navbar">
<h1>🚀 Excel转JSON工具</h1>
<!-- LLM配置按钮 -->
<button id="llmConfigBtn" class="config-btn" onclick="toggleLLMConfig()">⚙️ LLM配置</button>
</div>
<!-- LLM配置面板 -->
<div id="llmConfigPanel" class="config-panel">
<div class="section">
<h3>🤖 LLM配置</h3>
<label for="llmProviderSelect">Provider:</label>
<select id="llmProviderSelect">
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
</select>
<label for="llmBaseUrlInput">Base URL:</label>
<input type="text" id="llmBaseUrlInput" placeholder="输入API基础URL" value="http://localhost:11434">
<label for="llmModelInput">Model:</label>
<input type="text" id="llmModelInput" placeholder="输入模型名称" value="qwen3.5:9b">
<label for="llmApiKeyInput">API Key:</label>
<input type="password" id="llmApiKeyInput" placeholder="输入API密钥(如果需要)">
<button id="saveLlmConfigBtn" class="btn">💾 保存配置</button>
<button id="closeLlmConfigBtn" class="btn cancel-btn" onclick="toggleLLMConfig()">❌ 关闭</button>
</div>
</div>
<!-- 标签页导航 -->
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'company')">企业行业分析</button>
<button class="tablinks" onclick="openTab(event, 'excel')">Excel解析</button>
@@ -293,9 +373,60 @@
<!-- 税务健康标签页 -->
<div id="tax" class="tabcontent">
<div class="section">
<label for="taxJson">💼 请输入企业JSON数据:</label>
<textarea id="taxJson" rows="6" placeholder='请输入企业数据,例如: {"revenue": 1000000, "expenses": 800000}'>{"revenue": 1000000, "expenses": 800000, "tax_paid": 50000}</textarea>
<button id="callTaxBtn" disabled>评估税务健康</button>
<h3>📊 税务健康评估参数</h3>
<label for="companyNameInput">🏢 企业名称:</label>
<input type="text" id="companyNameInput" placeholder="请输入企业全称" value="企业名称">
<label for="periodInput">📅 测评周期:</label>
<select id="periodInput">
<option value="Q1">第一季度</option>
<option value="Q2">第二季度</option>
<option value="Q3">第三季度</option>
<option value="Q4">第四季度</option>
<option value="年度">年度</option>
</select>
<label for="yearInput">📅 报告期:</label>
<input type="number" id="yearInput" placeholder="年份" value="2025" min="1900" max="2100">
<label for="citInput">💰 企业所得税应纳税额合计 (万元):</label>
<input type="number" id="citInput" placeholder="单位:万元" value="100.0" step="0.01">
<label for="vatInput">💰 增值税应纳税额合计 (万元):</label>
<input type="number" id="vatInput" placeholder="单位:万元" value="50.0" step="0.01">
<label for="inputVatInput">💰 增值税进项税额 (万元):</label>
<input type="number" id="inputVatInput" placeholder="单位:万元" value="30.0" step="0.01">
<label for="outputVatInput">💰 增值税销项税额 (万元):</label>
<input type="number" id="outputVatInput" placeholder="单位:万元" value="80.0" step="0.01">
<label for="preCitInput">💰 上一期企业所得税应纳税额合计 (万元):</label>
<input type="number" id="preCitInput" placeholder="单位:万元" value="90.0" step="0.01">
<label for="preVatInput">💰 上一期增值税应纳税额合计 (万元):</label>
<input type="number" id="preVatInput" placeholder="单位:万元" value="45.0" step="0.01">
<label for="preInputVatInput">💰 上一期增值税进项税额 (万元):</label>
<input type="number" id="preInputVatInput" placeholder="单位:万元" value="25.0" step="0.01">
<label for="preOutputVatInput">💰 上一期增值税销项税额 (万元):</label>
<input type="number" id="preOutputVatInput" placeholder="单位:万元" value="70.0" step="0.01">
<label for="excelFileInput">📁 上传Excel财务报表 (可选):</label>
<input type="file" id="excelFileInput" accept=".xlsx,.xls">
<label for="taxJsonInput">📄 或者直接粘贴JSON数据:</label>
<textarea id="taxJsonInput" rows="4" placeholder='粘贴财务报表JSON数据'>{"LC0000003E_T0I0331": 741213833.14, "LC0000003E_T0I0101": 578064138.06}</textarea>
<button id="submitTaxBtn" disabled>提交税务评估</button>
<button id="queryResultBtn" disabled style="background: linear-gradient(45deg, #4CAF50, #8BC34A);">查询评估结果</button>
<div style="margin-top: 15px;">
<label for="orderNoInput">📋 订单号 (用于查询):</label>
<input type="text" id="orderNoInput" placeholder="输入订单号查询结果">
</div>
</div>
</div>
@@ -326,7 +457,8 @@
document.getElementById('analyzeCompanyBtn').disabled = false;
document.getElementById('parseExcelBtn').disabled = false;
document.getElementById('callLLMBtn').disabled = false;
document.getElementById('callTaxBtn').disabled = false;
document.getElementById('submitTaxBtn').disabled = false;
document.getElementById('queryResultBtn').disabled = false;
console.log('WASM模块加载完成');
}).catch((err) => {
console.error('WASM加载失败:', err);
@@ -337,11 +469,9 @@
const analyzeCompanyBtn = document.getElementById('analyzeCompanyBtn');
const parseExcelBtn = document.getElementById('parseExcelBtn');
const callLLMBtn = document.getElementById('callLLMBtn');
const callTaxBtn = document.getElementById('callTaxBtn');
const companyNameInput = document.getElementById('companyName');
const excelFileInput = document.getElementById('excelFile');
const llmPromptInput = document.getElementById('llmPrompt');
const taxJsonInput = document.getElementById('taxJson');
const resultSection = document.getElementById('resultSection');
const resultContent = document.getElementById('resultContent');
const loadingDiv = document.getElementById('loading');
@@ -361,7 +491,10 @@
resultSection.classList.remove('show');
try {
const result = await window.analyzeCompanyIndustryAsync(companyName);
// 获取当前LLM配置
const llmConfig = getCurrentLLMConfig();
const result = await window.analyzeCompanyIndustryAsync(companyName, JSON.stringify(llmConfig));
if (result && result.error) {
showError(result.error);
@@ -421,12 +554,17 @@
resultSection.classList.remove('show');
try {
const result = await window.callLLMAsync(prompt);
// 获取当前LLM配置
const llmConfig = getCurrentLLMConfig();
// 调用大模型
const result = await window.callLLMAsync(prompt, JSON.stringify(llmConfig));
if (result && result.error) {
showError(result.error);
showError('调用大模型失败: ' + result.error);
} else {
displayResults('AI分析结果', result);
// 显示结果
displayResults('大模型调用结果', result);
}
} catch (error) {
showError('AI分析过程中发生错误: ' + error.message);
@@ -435,12 +573,46 @@
}
});
// 税务健康按钮事件
callTaxBtn.addEventListener('click', async () => {
// 税务健康提交按钮事件
const submitTaxBtn = document.getElementById('submitTaxBtn');
const queryResultBtn = document.getElementById('queryResultBtn');
const taxCompanyNameInput = document.getElementById('companyNameInput');
const periodInput = document.getElementById('periodInput');
const yearInput = document.getElementById('yearInput');
const citInput = document.getElementById('citInput');
const vatInput = document.getElementById('vatInput');
const inputVatInput = document.getElementById('inputVatInput');
const outputVatInput = document.getElementById('outputVatInput');
const preCitInput = document.getElementById('preCitInput');
const preVatInput = document.getElementById('preVatInput');
const preInputVatInput = document.getElementById('preInputVatInput');
const preOutputVatInput = document.getElementById('preOutputVatInput');
const taxExcelFileInput = document.getElementById('excelFileInput');
const taxJsonInput = document.getElementById('taxJsonInput');
const orderNoInput = document.getElementById('orderNoInput');
submitTaxBtn.addEventListener('click', async () => {
// 收集所有输入的参数
const companyName = taxCompanyNameInput.value.trim();
const period = periodInput.value;
const year = yearInput.value;
const cit = parseFloat(citInput.value) || 0;
const vat = parseFloat(vatInput.value) || 0;
const inputVat = parseFloat(inputVatInput.value) || 0;
const outputVat = parseFloat(outputVatInput.value) || 0;
const preCit = parseFloat(preCitInput.value) || 0;
const preVat = parseFloat(preVatInput.value) || 0;
const preInputVat = parseFloat(preInputVatInput.value) || 0;
const preOutputVat = parseFloat(preOutputVatInput.value) || 0;
const jsonData = taxJsonInput.value.trim();
if (!jsonData) {
showError('请输入企业JSON数据');
if (!companyName) {
showError('请输入企业名称');
return;
}
if (!year || isNaN(year)) {
showError('请输入有效的年份');
return;
}
@@ -449,15 +621,182 @@
resultSection.classList.remove('show');
try {
const result = await window.callTaxHealthAsync(jsonData);
// 构建请求数据
let requestData = {
req_no: 'REQ' + Date.now().toString(),
company_name: companyName,
period: period,
year: year.toString(),
cit: cit,
vat: vat,
input_vat: inputVat,
output_vat: outputVat,
pre_cit: preCit,
pre_vat: preVat,
pre_input_vat: preInputVat,
pre_output_vat: preOutputVat
};
// 处理Excel文件或JSON数据
if (taxExcelFileInput.files.length > 0) {
// 如果用户选择了Excel文件,需要先解析它并通过LLM转换为JSON
const file = taxExcelFileInput.files[0];
const arrayBuffer = await file.arrayBuffer();
// 获取期间参数
const period = periodInput.value;
// 获取当前LLM配置
const llmConfig = getCurrentLLMConfig();
// 使用新的函数将Excel数据通过LLM转换为JSON格式,传入期间参数和配置
const conversionResult = await window.convertExcelToJsonViaLLM(arrayBuffer, period, JSON.stringify(llmConfig));
// 检查转换结果是否包含错误
if (conversionResult && conversionResult.error) {
showError('Excel转JSON过程中发生错误: ' + conversionResult.error);
return;
}
if (conversionResult && conversionResult.error) {
showError('Excel转JSON过程中发生错误: ' + conversionResult.error);
return;
}
// 优先使用转换后的JSON数据
if (conversionResult.json_data) {
requestData.data = conversionResult.json_data;
} else if (conversionResult.parsed_json) {
// 如果有解析后的JSON字符串,尝试解析它
try {
requestData.data = JSON.parse(conversionResult.parsed_json);
} catch (e) {
console.warn('无法解析LLM返回的JSON,使用原始响应:', e);
// 如果解析失败,使用原始响应作为数据
requestData.data = conversionResult;
}
} else {
showError('Excel转JSON失败,未获得有效数据');
return;
}
} else if (jsonData) {
// 如果提供了JSON数据
try {
const parsedData = JSON.parse(jsonData);
requestData.data = parsedData;
} catch (e) {
showError('JSON数据格式不正确: ' + e.message);
return;
}
} else {
showError('请上传Excel文件或提供JSON数据');
return;
}
// 首先调用税务健康API获取订单号
const result = await window.callTaxHealthAsync(JSON.stringify(requestData));
if (result && result.error) {
showError(result.error);
return;
}
// 显示初始结果并开始轮询
displayResults('税务健康评估提交结果', result);
// 自动填充订单号到查询框
if (result.order_no) {
orderNoInput.value = result.order_no;
// 开始轮询查询结果
console.log('开始轮询查询结果...');
let attempts = 0;
const maxAttempts = 60; // 最多轮询60次
const pollForResult = async () => {
if (attempts >= maxAttempts) {
console.log('轮询超时');
return;
}
try {
const pollResult = await window.queryTaxResultAsync(result.order_no);
if (pollResult && pollResult.error) {
showError('查询结果时发生错误: ' + pollResult.error);
return;
}
const queryResponse = pollResult.query_response;
console.log(`轮询结果 - 状态码: ${queryResponse.code}, 订单状态: ${queryResponse.data.status}`);
// 根据状态码判断处理结果
switch (queryResponse.data.status) {
case 99: // 执行成功
console.log('税务评估已完成!');
displayResults('税务评估最终结果', pollResult);
// 显示结果文件地址
if (queryResponse.data.detail && queryResponse.data.detail.files) {
console.log('结果文件地址:', queryResponse.data.detail.files);
}
return;
case 0: // 正在执行
console.log(`任务仍在处理中... (${attempts + 1}/${maxAttempts})`);
attempts++;
setTimeout(pollForResult, 5000); // 5秒后再次查询
break;
case -1: // 无效订单
showError(`订单无效: ${queryResponse.data.detail?.error || '未知错误'}`);
return;
case 98: // 执行失败
showError(`任务执行失败: ${queryResponse.data.detail?.error || '未知错误'}`);
return;
default:
console.log(`未知状态码: ${queryResponse.data.status}`);
attempts++;
setTimeout(pollForResult, 5000);
break;
}
} catch (error) {
showError('轮询过程中发生错误: ' + error.message);
}
};
// 开始轮询
setTimeout(pollForResult, 5000);
}
} catch (error) {
showError('税务健康评估过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
});
// 查询税务评估结果按钮事件
queryResultBtn.addEventListener('click', async () => {
const orderNo = orderNoInput.value.trim();
if (!orderNo) {
showError('请输入订单号');
return;
}
showLoading(true);
hideError();
resultSection.classList.remove('show');
try {
const result = await window.queryTaxResultAsync(orderNo);
if (result && result.error) {
showError(result.error);
} else {
displayResults('税务健康评估结果', result);
displayResults('税务评估查询结果', result);
}
} catch (error) {
showError('税务健康评估过程中发生错误: ' + error.message);
showError('查询税务评估结果过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
@@ -471,7 +810,8 @@
analyzeCompanyBtn.disabled = true;
parseExcelBtn.disabled = true;
callLLMBtn.disabled = true;
callTaxBtn.disabled = true;
submitTaxBtn.disabled = true;
queryResultBtn.disabled = true;
} else {
loadingDiv.classList.remove('show');
// 重新启用按钮(但保持初始状态由WASM加载决定)
@@ -480,7 +820,8 @@
analyzeCompanyBtn.disabled = false;
parseExcelBtn.disabled = false;
callLLMBtn.disabled = false;
callTaxBtn.disabled = false;
submitTaxBtn.disabled = false;
queryResultBtn.disabled = false;
}, 100);
}
}
@@ -513,6 +854,91 @@
resultSection.classList.add('show');
}
// 切换LLM配置面板显示/隐藏
function toggleLLMConfig() {
const panel = document.getElementById('llmConfigPanel');
const isActive = panel.classList.contains('active');
if (isActive) {
panel.classList.remove('active');
} else {
panel.classList.add('active');
}
}
// 保存LLM配置
document.addEventListener('DOMContentLoaded', function() {
const saveLlmConfigBtn = document.getElementById('saveLlmConfigBtn');
if (saveLlmConfigBtn) {
saveLlmConfigBtn.addEventListener('click', function() {
const provider = document.getElementById('llmProviderSelect').value;
const baseUrl = document.getElementById('llmBaseUrlInput').value;
const model = document.getElementById('llmModelInput').value;
const apiKey = document.getElementById('llmApiKeyInput').value;
// 保存配置到localStorage
const llmConfig = {
provider: provider,
base_url: baseUrl,
model: model,
api_key: apiKey
};
localStorage.setItem('llmConfig', JSON.stringify(llmConfig));
alert('LLM配置已保存!');
});
}
});
// 从localStorage加载LLM配置
function loadLLMConfig() {
const savedConfig = localStorage.getItem('llmConfig');
if (savedConfig) {
try {
const config = JSON.parse(savedConfig);
if (document.getElementById('llmProviderSelect')) {
document.getElementById('llmProviderSelect').value = config.provider || 'ollama';
}
if (document.getElementById('llmBaseUrlInput')) {
document.getElementById('llmBaseUrlInput').value = config.base_url || 'http://localhost:11434';
}
if (document.getElementById('llmModelInput')) {
document.getElementById('llmModelInput').value = config.model || 'qwen3.5:9b';
}
if (document.getElementById('llmApiKeyInput')) {
document.getElementById('llmApiKeyInput').value = config.api_key || '';
}
} catch (e) {
console.error('加载LLM配置失败:', e);
}
}
}
// 在页面加载完成后初始化LLM配置
document.addEventListener('DOMContentLoaded', function() {
loadLLMConfig();
});
// 获取当前LLM配置
function getCurrentLLMConfig() {
const savedConfig = localStorage.getItem('llmConfig');
if (savedConfig) {
try {
return JSON.parse(savedConfig);
} catch (e) {
console.error('解析LLM配置失败:', e);
}
}
// 返回默认配置
return {
provider: 'ollama',
base_url: 'http://localhost:11434',
model: 'qwen3.5:9b',
api_key: ''
};
}
// 标签页功能
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
@@ -537,7 +963,7 @@
} else if (activeTab.id === 'llm') {
callLLMBtn.click();
} else if (activeTab.id === 'tax') {
callTaxBtn.click();
submitTaxBtn.click();
}
}
});