1 1 місяць тому
батько
коміт
2584ee718b

+ 23 - 0
common/common.go

@@ -0,0 +1,23 @@
+package common
+
+import (
+	"git.listensoft.net/tool/jspkit/common/lxhttp"
+	"git.listensoft.net/tool/jspkit/common/variable"
+	"git.listensoft.net/tool/jspkit/logger"
+)
+
+// 手机号锁 time 锁定多长时间 传-1解锁
+func AddTelLockerX2(tel string, secend string) {
+	c := lxhttp.NewHttpClient()
+	bytes, _ := lxhttp.Get(c, variable.TaxTaskURL+"/api/v1/saveTelLock?tel="+tel+"&time="+secend)
+	logger.Info("TelLock: " + string(bytes) + secend + "s")
+}
+func AddTelLockerX2Req(tel string, secend string, reqs string) {
+	c := lxhttp.NewHttpClient()
+	uri := variable.TaxTaskURL + "/api/v1/saveTelLock?tel=" + tel + "&time=" + secend
+	if reqs != "" {
+		uri += "&reqNos=" + reqs
+	}
+	bytes, _ := lxhttp.Get(c, uri)
+	logger.Info(tel + " TelLock: " + string(bytes) + " " + secend + "s")
+}

+ 321 - 0
common/lxhttp/http.go

@@ -0,0 +1,321 @@
+package lxhttp
+
+import (
+	"bytes"
+	"compress/gzip"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/json"
+	"git.listensoft.net/tool/jspkit/taxerr"
+	"golang.org/x/net/publicsuffix"
+	"io"
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// POSTForm 发送Form表单请求
+func POSTForm(c *http.Client, requrl string, params map[string]string, headers map[string]string) ([]byte, error) {
+	form := url.Values{}
+	for k, v := range params {
+		form.Add(k, v)
+	}
+	var bys []byte
+	req, err := http.NewRequest("POST", requrl, strings.NewReader(form.Encode()))
+	if err != nil {
+		return bys, err
+	}
+	for k, v := range headers {
+		req.Header.Add(k, v)
+	}
+	resp, err := c.Do(req)
+	if err != nil {
+		return bys, err
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}
+
+// GET 发送get请求
+func GET(c *http.Client, requrl string, params map[string]string, headers map[string]string) ([]byte, error) {
+	form := url.Values{}
+	for k, v := range params {
+		form.Add(k, v)
+	}
+	var bys []byte
+	req, err := http.NewRequest("GET", requrl, strings.NewReader(form.Encode()))
+	if err != nil {
+		return bys, err
+	}
+	for k, v := range headers {
+		req.Header.Add(k, v)
+	}
+	resp, err := c.Do(req)
+	if err != nil {
+		return bys, err
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}
+
+func GZIPDecode(in []byte) ([]byte, error) {
+	reader, err := gzip.NewReader(bytes.NewReader(in))
+	if err != nil {
+		var out []byte
+		return out, err
+	}
+	defer reader.Close()
+	return io.ReadAll(reader)
+}
+
+// POSTJson 发送json请求
+func POSTJson1(c *http.Client, requrl string, params map[string]interface{}, headers map[string]string) ([]byte, error) {
+	bytesData, _ := json.Marshal(params)
+	request, err := http.NewRequest("POST", requrl, bytes.NewReader(bytesData))
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range headers {
+		request.Header.Add(k, v)
+	}
+	request.Header.Set("Content-Type", "application/json;charset=UTF-8")
+	request.Header.Set("Accept-Encoding", "gzip, deflate, br")
+	resp, err := c.Do(request)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	bytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if strings.Contains(resp.Header.Get(`Content-Encoding`), "gzip") {
+		return GZIPDecode(bytes)
+	}
+	return bytes, nil
+}
+
+// POSTJson 发送json请求
+func POSTJsonAny(c *http.Client, requrl string, params any, headers map[string]string) ([]byte, error) {
+	bytesData, _ := json.Marshal(params)
+	request, err := http.NewRequest("POST", requrl, bytes.NewReader(bytesData))
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range headers {
+		request.Header.Add(k, v)
+	}
+	request.Header.Set("Content-Type", "application/json;charset=UTF-8")
+	request.Header.Set("Accept-Encoding", "gzip, deflate, br")
+	resp, err := c.Do(request)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	bytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if strings.Contains(resp.Header.Get(`Content-Encoding`), "gzip") {
+		return GZIPDecode(bytes)
+	}
+	return bytes, nil
+}
+
+// POSTJson 发送json请求
+func POSTStrReader(c *http.Client, requrl, params string, headers map[string]string) ([]byte, error) {
+	request, err := http.NewRequest("POST", requrl, strings.NewReader(params))
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range headers {
+		request.Header.Add(k, v)
+	}
+	request.Header.Set("Accept-Encoding", "gzip, deflate, br")
+	resp, err := c.Do(request)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	bytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if strings.Contains(resp.Header.Get(`Content-Encoding`), "gzip") {
+		return GZIPDecode(bytes)
+	}
+	return bytes, nil
+}
+
+// POSTJson 发送json请求
+func POSTJson(c *http.Client, requrl string, params map[string]string, headers map[string]string) ([]byte, error) {
+	bytesData, _ := json.Marshal(params)
+	request, err := http.NewRequest("POST", requrl, bytes.NewReader(bytesData))
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range headers {
+		request.Header.Add(k, v)
+	}
+	request.Header.Set("Content-Type", "application/json;charset=UTF-8")
+	request.Header.Set("Accept-Encoding", "gzip, deflate, br")
+	resp, err := c.Do(request)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode == 504 && strings.Contains(requrl, "keepalive") { // 超时
+		return nil, taxerr.KeepTimeOut
+	}
+	defer resp.Body.Close()
+	bytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if strings.Contains(resp.Header.Get(`Content-Encoding`), "gzip") {
+		return GZIPDecode(bytes)
+	}
+	return bytes, nil
+}
+
+// Get http get请求
+func Get(c *http.Client, url string) ([]byte, error) {
+	resp, err := c.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}
+
+// NewHttpClient 生成一个htppclient
+func NewHttpClient() *http.Client {
+	tr := &http.Transport{
+		// Proxy:           http.ProxyFromEnvironment,             // 从环境变量中获取代理
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 忽略证书校验错误
+	}
+	var c = &http.Client{
+		Timeout:   time.Duration(60 * time.Second),
+		Transport: tr,
+	}
+	options := cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	}
+	curCookieJar, _ := cookiejar.New(&options)
+	c.Jar = curCookieJar
+	return c
+}
+
+// NewHttpClient 生成一个htppclient 请求等待时间加长
+func NewLongHttpClient() *http.Client {
+	tr := &http.Transport{
+		// Proxy:           http.ProxyFromEnvironment,             // 从环境变量中获取代理
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 忽略证书校验错误
+	}
+	var c = &http.Client{
+		Timeout:   time.Duration(600 * time.Second),
+		Transport: tr,
+	}
+	options := cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	}
+	curCookieJar, _ := cookiejar.New(&options)
+	c.Jar = curCookieJar
+	return c
+}
+
+// NewHttpClient 生成一个代理htppclient
+func NewHttpClientForProxy(proxy string) *http.Client {
+	var tr *http.Transport
+	if proxy == "" {
+		tr = &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略证书校验
+		}
+	} else {
+		p, _ := url.Parse(proxy)
+		tr = &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略证书校验
+			Proxy:           http.ProxyURL(p),
+		}
+	}
+	var c = &http.Client{
+		Timeout:   time.Duration(20 * time.Second),
+		Transport: tr,
+	}
+	options := cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	}
+	curCookieJar, _ := cookiejar.New(&options)
+	c.Jar = curCookieJar
+	return c
+}
+
+// NewHttpClient 生成一个待认证的htppclient (仅适用西藏)
+func NewHttpClientForXizang(ip, auth string) *http.Client {
+	p, _ := url.Parse(ip)
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略证书校验
+		Proxy:           http.ProxyURL(p),
+	}
+	if auth != "" {
+		tr.ProxyConnectHeader = http.Header{
+			"Proxy-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(auth))},
+		}
+	}
+	var c = &http.Client{
+		Timeout:   time.Duration(20 * time.Second),
+		Transport: tr,
+	}
+	options := cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	}
+	curCookieJar, _ := cookiejar.New(&options)
+	c.Jar = curCookieJar
+	return c
+}
+
+// NewHttpClientForLocalProxy 针对本地代理 需要账号认证的 不认证的也可以用,后面全用这个
+func NewHttpClientForLocalProxy(ip, auth string) *http.Client {
+	if ip == "" {
+		return NewHttpClient()
+	}
+	p, _ := url.Parse(ip)
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略证书校验
+		Proxy:           http.ProxyURL(p),
+	}
+	if auth != "" {
+		tr.ProxyConnectHeader = http.Header{
+			"Proxy-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(auth))},
+		}
+	}
+	var c = &http.Client{
+		Timeout:   time.Duration(60 * time.Second),
+		Transport: tr,
+	}
+	options := cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	}
+	curCookieJar, _ := cookiejar.New(&options)
+	c.Jar = curCookieJar
+	return c
+}
+
+// CreateFormReader 将 map 转换成 header
+func CreateFormReader(data map[string]string) io.Reader {
+	form := url.Values{}
+	for k, v := range data {
+		form.Add(k, v)
+	}
+	return strings.NewReader(form.Encode())
+}
+
+func HttpClientPost(client *http.Client, URL, contentType string, body io.Reader) (res []byte, err error) {
+	resp, err := client.Post(URL, contentType, body)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}

+ 38 - 0
common/lxrod/bank.go

@@ -0,0 +1,38 @@
+package lxrod
+
+import (
+	"context"
+
+	"github.com/go-rod/rod"
+	"github.com/go-rod/rod/lib/devices"
+	"github.com/go-rod/rod/lib/launcher"
+)
+
+// NewLauncher 创建一个新的 rod 浏览器启动器
+func NewLauncher(ctx context.Context) *launcher.Launcher {
+	return launcher.New().Context(ctx).Set("window-size", "1600,900").Headless(false)
+}
+
+// NewEdgeBrowser 创建一个由 rod chromium 模拟的 Edge 浏览器
+func NewEdgeBrowser(ctx context.Context, u string) *rod.Browser {
+	return rod.New().Context(ctx).DefaultDevice(EdgeLandscape).ControlURL(u)
+}
+
+// EdgeLandscape device.
+var EdgeLandscape = devices.Device{
+	Title:          "Edge Laptop with HiDPI screen",
+	Capabilities:   []string{},
+	UserAgent:      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
+	AcceptLanguage: "en",
+	Screen: devices.Screen{
+		DevicePixelRatio: 2,
+		Horizontal: devices.ScreenSize{
+			Width:  1600,
+			Height: 900,
+		},
+		Vertical: devices.ScreenSize{
+			Width:  900,
+			Height: 1600,
+		},
+	},
+}.Landscape()

+ 61 - 0
common/lxrod/cookie.go

@@ -0,0 +1,61 @@
+package lxrod
+
+import (
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+	"time"
+
+	"github.com/go-rod/rod"
+	"github.com/go-rod/rod/lib/proto"
+)
+
+// 将 p 中所有 cookie 提取到 jar 中。cookie 的 domain 设置为 u。
+// TODO: 双向绑定
+func SetCookieFor(p *rod.Page, u *url.URL) (http.CookieJar, error) {
+	jar, _ := cookiejar.New(nil)
+	cookies, err := p.Cookies(nil)
+	if err != nil {
+		return nil, err
+	}
+	cs := make([]*http.Cookie, len(cookies))
+	for i, c := range cookies {
+		cs[i] = CookieFrom(c)
+	}
+	jar.SetCookies(u, cs)
+	return jar, nil
+}
+
+// 将 rod 的 cookie 格式转换为 go http 的 cookie。
+// go 1.19 以后的新字段不支持。
+func CookieFrom(c *proto.NetworkCookie) *http.Cookie {
+	var t time.Time
+	if c.Expires != -1 {
+		t = c.Expires.Time()
+	}
+
+	return &http.Cookie{
+		Name:     c.Name,
+		Value:    c.Value,
+		Path:     c.Path,
+		Domain:   c.Domain,
+		Expires:  t,
+		Secure:   c.Secure,
+		HttpOnly: c.HTTPOnly,
+		SameSite: SameSite(c.SameSite),
+	}
+}
+
+// 从 rod cookie 中提取创建 go cookie 所用的 SameSite 数据
+func SameSite(s proto.NetworkCookieSameSite) http.SameSite {
+	switch s {
+	case proto.NetworkCookieSameSiteStrict:
+		return http.SameSiteStrictMode
+	case proto.NetworkCookieSameSiteLax:
+		return http.SameSiteLaxMode
+	case proto.NetworkCookieSameSiteNone:
+		return http.SameSiteNoneMode
+	default:
+		return http.SameSiteDefaultMode
+	}
+}

+ 23 - 0
common/lxrod/event.go

@@ -0,0 +1,23 @@
+package lxrod
+
+import (
+	"github.com/go-rod/rod"
+	"github.com/go-rod/rod/lib/proto"
+)
+
+// vue 页面导航完成结果。如果提前停止等待则返回 false。
+type NavigateResultFunc = func() (proto.PageNavigatedWithinDocument, bool)
+
+// 等待 vue hash 路由页面导航完成。
+// vue router 有一个 hash 模式。页面的 url 会变成 xx.com/#/yy 的格式。
+// 此时页面的“跳转”是用 url 的 fragment 模拟的,并不会触发真正的页面导航。
+// 该函数会等待这种页面的跳转事件,并且返回导航的信息。如果监听提前停止则会返回 false。
+// 可以给 p 的 ctx 添加 timeout 或者 cancel 来达到提前停止的目的。
+func WaitVuePageNavigated(p *rod.Page) NavigateResultFunc {
+	var e proto.PageNavigatedWithinDocument
+	wait := p.WaitEvent(&e)
+	return func() (proto.PageNavigatedWithinDocument, bool) {
+		wait()
+		return e, e != proto.PageNavigatedWithinDocument{}
+	}
+}

+ 959 - 0
common/lxrod/rod.go

@@ -0,0 +1,959 @@
+package lxrod
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"git.listensoft.net/tool/jspkit/common/lxhttp"
+	"git.listensoft.net/tool/jspkit/common/variable"
+	"git.listensoft.net/tool/jspkit/taxerr"
+	"github.com/go-rod/stealth"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-rod/rod"
+	"github.com/go-rod/rod/lib/launcher"
+	"github.com/go-rod/rod/lib/proto"
+	"github.com/go-rod/rod/lib/utils"
+)
+
+var (
+	LastAlertMsg = make(chan string, 100)
+)
+
+// 只允许task.go 初始化一次
+type Lxrod struct {
+	Area                  variable.Area
+	Browser               *rod.Browser
+	Launcher              *launcher.Launcher
+	NoByPass              bool //添加反反爬虫代码 原理就是执行一段js去掉window属性中关于自动化的代码
+	Headless              bool //headless 默认显示浏览器 为true是不显示浏览器
+	MonitorDialog         bool //默认监控alert然后去点击true 如果要自己处理 设置为true不添加监控
+	PageEvent             bool //默认每个页面的监听事件 设置为true不添加监控
+	UseProxy              bool //代理
+	Leakless              bool
+	NoDefaultDevice       bool
+	Proxy                 string
+	ProxyAuth             string
+	WsURL                 string // rod的ws地址
+	NoEachEventForCreated bool
+	LoginErrImg           string
+}
+
+// 关闭浏览器 统一控制
+func (lr *Lxrod) CloseBrowser() (err error) {
+	if lr.Browser != nil {
+		return rod.Try(func() {
+			if lr.Browser != nil {
+				lr.Browser.Close()
+				go rod.Try(func() {
+
+					lr.Launcher.Cleanup()
+					lr.Launcher.Kill()
+
+				})
+			}
+		})
+	}
+	return nil
+}
+
+// 关闭浏览器 统一控制
+func (lr *Lxrod) CloseBrowserNew() {
+	if lr.Browser != nil {
+		_ = lr.Browser.Close()
+
+		lr.Launcher.Cleanup()
+
+	}
+}
+
+func GetAlertMsg() string {
+
+	select {
+	case v := <-LastAlertMsg:
+
+		return v
+	case <-time.After(time.Second * 8):
+
+		return ""
+	}
+}
+
+func ClickDialog(p *rod.Page, h func(bool, string)) { //默认添加点击alert事件
+	go func() {
+		defer func() {
+			if err := recover(); err != nil {
+
+				rod.Try(func() { h(true, "确定") })
+			}
+		}()
+		h(true, "确定") //dialog 浏览器语言中文默认是确定 alert浏览器语言英文默认是ok
+	}()
+}
+
+// 监听 alert confirm 等事件打开
+func DialogWatch(ctx context.Context, page *rod.Page) {
+
+	defer func() {
+		if r := recover(); r != nil {
+
+		}
+	}()
+	_, h := page.MustHandleDialog()
+	go page.EachEvent(func(e *proto.PageJavascriptDialogOpening) {
+		defer func() {
+
+			if r := recover(); r != nil {
+
+			}
+		}()
+		if strings.Contains(page.MustInfo().URL, `henan`) {
+			if strings.Contains(e.Message, `请重新登录`) || strings.Contains(e.Message, `访问异常`) {
+				page.Browser().MustSetCookies()
+
+				page.MustNavigate("")
+
+			}
+		}
+		if strings.Contains(page.MustInfo().URL, `jiangxi`) {
+			if strings.Contains(e.Message, "纳税人识别号未注册电子税务局") {
+
+				//LastAlertMsgJiangXi <- e.Message
+			}
+		}
+
+		LastAlertMsg <- e.Message
+
+		ClickDialog(page, h)
+		utils.Sleep(5)
+		if len(LastAlertMsg) > 0 {
+			_ = <-LastAlertMsg
+		} else {
+
+		}
+
+	})()
+}
+
+// 新建浏览器对象-青岛
+func (lxrod *Lxrod) Newqd(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	utils.Sleep(1)
+	lxrod.Launcher = launcher.New()
+	lxrod.Launcher.Set("window-size", "1600,900").
+		Set("no-sandbox").
+		Set("ignore-certificate-errors").
+		Set("ignore-certificate-errors-spki-list").
+		Set("ignore-ssl-errors").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Headless(lxrod.Headless)
+	//lxrod.Launcher.UserDataDir("user-data")
+	//if viper.GetString("env") == string(variable.Development) {
+	//	lxrod.Launcher.Leakless(false) //开发环境不使用用leakless
+	//}
+	lxrod.Launcher.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	u, err := lxrod.Launcher.Launch()
+	if err != nil {
+
+		err = taxerr.New("调用浏览器失败,请稍后重试!")
+		return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+	}
+	b = rod.New().ControlURL(u).MustConnect().Context(ctx).NoDefaultDevice() //.Trace(true) Trace调试打开
+	utils.Sleep(1)
+	b.MustSetCookies()
+	p = b.MustPage("").MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	lxrod.Browser = b
+	return lxrod.Browser, p, err
+}
+
+// 新建浏览器对象
+func (lxrod *Lxrod) New(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	utils.Sleep(1)
+	// if viper.GetString("env") == string(variable.Development) { //直接启动浏览器
+	lxrod.Launcher = launcher.New()
+	// } else { //容器内启动
+	// serviceURL := os.Getenv("serviceURL")
+	// lxrod.Launcher, err = launcher.NewManaged(serviceURL)
+	// if err != nil {
+	//
+	// err = taxerr.New("调用浏览器失败,请稍后重试!")
+	// return lxrod.Browser, p, err
+	// }
+	// lxrod.Launcher.XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16")
+	// }
+	lxrod.Launcher.Set("window-size", "1600,900").
+		Set("no-sandbox").
+		Set("ignore-certificate-errors").
+		Set("ignore-certificate-errors-spki-list").
+		Set("ignore-ssl-errors").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		//UserDataDir("user-data").
+		Headless(lxrod.Headless)
+	//if lxrod.Area == "hebei" {
+	//	lxrod.Launcher.UserDataDir("user-data")
+	//}
+	//if lxrod.Area == "hebei" {
+	lxrod.Launcher.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	//}
+	//if viper.GetString("env") == string(variable.Development) {
+	//	//lxrod.Launcher.Leakless(false) //开发环境不使用用leakless todo 进程老是结束不掉
+	//}
+	// Leakless(lxrod.Leakless)
+	// chromePath, ok := launcher.LookPath()
+	// fmt.Println(ok)
+	// fmt.Println(chromePath)
+	// if ok {
+	// 	lxrod.Launcher.Bin(chromePath)
+	// }
+	if lxrod.UseProxy {
+		lxrod.Proxy, lxrod.ProxyAuth = GetLocalProxy(string(lxrod.Area))
+		lxrod.Launcher.Proxy(lxrod.Proxy)
+	}
+	u, err := lxrod.Launcher.Launch()
+	lxrod.WsURL = u
+	if err != nil {
+		err = taxerr.New("调用浏览器失败,请稍后重试!")
+		return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+	}
+	// lxrod.Browser = rod.New().Client(lxrod.Launcher.MustClient()).MustConnect().Context(ctx).MustSetCookies() //.Trace(true) Trace调试打开
+	lxrod.Browser = rod.New().ControlURL(u).MustConnect().Context(ctx) //.Trace(true) Trace调试打开
+	utils.Sleep(1)
+	lxrod.Browser.MustSetCookies()
+	if lxrod.NoDefaultDevice {
+		lxrod.Browser.NoDefaultDevice()
+	}
+	if !lxrod.NoByPass {
+		//是否需要反反爬虫
+		p = stealth.MustPage(lxrod.Browser)
+	} else {
+		p, err = lxrod.Browser.Page(proto.TargetCreateTarget{})
+		if err != nil {
+			return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+		}
+	}
+	// launcher.Open(lxrod.Browser.ServeMonitor(""))
+	p.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	if lxrod.MonitorDialog {
+		DialogWatch(ctx, p) //为新打开的页面添加一个dialog监控
+	}
+	if !lxrod.PageEvent {
+		lxrod.EachPageCreatedEvent(ctx, lxrod.Browser)
+	}
+	return lxrod.Browser, p, err
+}
+
+func (lxrod *Lxrod) NewSd(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	utils.Sleep(1)
+	// if viper.GetString("env") == string(variable.Development) { //直接启动浏览器
+	lxrod.Launcher = launcher.New()
+	// } else { //容器内启动
+	// serviceURL := os.Getenv("serviceURL")
+	// lxrod.Launcher, err = launcher.NewManaged(serviceURL)
+	// if err != nil {
+	//
+	// err = taxerr.New("调用浏览器失败,请稍后重试!")
+	// return lxrod.Browser, p, err
+	// }
+	// lxrod.Launcher.XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16")
+	// }
+	lxrod.Launcher.Set("window-size", "1600,900").
+		Set("no-sandbox").
+		Set("ignore-certificate-errors").
+		Set("ignore-certificate-errors-spki-list").
+		Set("ignore-ssl-errors").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		//UserDataDir("user-data").
+		Headless(lxrod.Headless)
+	//if viper.GetString("env") == string(variable.Development) {
+	//	lxrod.Launcher.Leakless(false) //开发环境不使用用leakless
+	//}
+	// Leakless(lxrod.Leakless)
+	// chromePath, ok := launcher.LookPath()
+	// fmt.Println(ok)
+	// fmt.Println(chromePath)
+	// if ok {
+	// 	lxrod.Launcher.Bin(chromePath)
+	// }
+	if lxrod.UseProxy {
+		lxrod.Proxy, lxrod.ProxyAuth = GetLocalProxy(string(lxrod.Area))
+		lxrod.Launcher.Proxy(lxrod.Proxy)
+	}
+	u, err := lxrod.Launcher.Launch()
+	if err != nil {
+
+		err = taxerr.New("调用浏览器失败,请稍后重试!")
+		return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+	}
+	// lxrod.Browser = rod.New().Client(lxrod.Launcher.MustClient()).MustConnect().Context(ctx).MustSetCookies() //.Trace(true) Trace调试打开
+	lxrod.Browser = rod.New().ControlURL(u).MustConnect().Context(ctx) //.Trace(true) Trace调试打开
+	utils.Sleep(1)
+	lxrod.Browser.MustSetCookies()
+	if lxrod.NoDefaultDevice {
+		lxrod.Browser.NoDefaultDevice()
+	}
+	if !lxrod.NoByPass {
+		//是否需要反反爬虫
+		p = stealth.MustPage(lxrod.Browser)
+	} else {
+		p, err = lxrod.Browser.Page(proto.TargetCreateTarget{})
+		if err != nil {
+			return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+		}
+	}
+	// launcher.Open(lxrod.Browser.ServeMonitor(""))
+	p.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	if lxrod.MonitorDialog {
+		DialogWatch(ctx, p) //为新打开的页面添加一个dialog监控
+	}
+	lxrod.EachPageCreatedEvent(ctx, lxrod.Browser)
+	return lxrod.Browser, p, err
+}
+
+// 新建浏览器对象 易代账移除EachPageCreatedEvent否则新标签页打不开 by fengxianwei
+func (lxrod *Lxrod) NewByydz(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	utils.Sleep(1)
+	// if viper.GetString("env") == string(variable.Development) { //直接启动浏览器
+	lxrod.Launcher = launcher.New()
+	// } else { //容器内启动
+	// serviceURL := os.Getenv("serviceURL")
+	// lxrod.Launcher, err = launcher.NewManaged(serviceURL)
+	// if err != nil {
+	//
+	// err = taxerr.New("调用浏览器失败,请稍后重试!")
+	// return lxrod.Browser, p, err
+	// }
+	// lxrod.Launcher.XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16")
+	// }
+	lxrod.Launcher.Set("window-size", "1600,900").
+		Set("no-sandbox").
+		Set("ignore-certificate-errors").
+		Set("ignore-certificate-errors-spki-list").
+		Set("ignore-ssl-errors").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		//UserDataDir("user-data").
+		Headless(lxrod.Headless)
+	//if viper.GetString("env") == string(variable.Development) {
+	//	lxrod.Launcher.Leakless(false) //开发环境不使用用leakless
+	//}
+	if lxrod.UseProxy {
+		lxrod.Proxy, lxrod.ProxyAuth = GetLocalProxy(string(lxrod.Area))
+		lxrod.Launcher.Proxy(lxrod.Proxy)
+	}
+	u, err := lxrod.Launcher.Launch()
+	if err != nil {
+
+		err = taxerr.New("调用浏览器失败,请稍后重试!")
+		return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+	}
+	lxrod.Browser = rod.New().ControlURL(u).MustConnect().Context(ctx) //.Trace(true) Trace调试打开
+	utils.Sleep(1)
+	lxrod.Browser.MustSetCookies()
+	if lxrod.NoDefaultDevice {
+		lxrod.Browser.NoDefaultDevice()
+	}
+	if !lxrod.NoByPass {
+		//是否需要反反爬虫
+		p = stealth.MustPage(lxrod.Browser)
+	} else {
+		p, err = lxrod.Browser.Page(proto.TargetCreateTarget{})
+		if err != nil {
+			return lxrod.Browser, p, taxerr.NewWebStuckTitle(true)
+		}
+	}
+	p.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	if lxrod.MonitorDialog {
+		DialogWatch(ctx, p) //为新打开的页面添加一个dialog监控
+	}
+	return lxrod.Browser, p, err
+}
+
+// 跳过反爬虫检查
+func (lxrod *Lxrod) EachPageCreatedEvent(ctx context.Context, browser *rod.Browser) {
+	if !lxrod.MonitorDialog && lxrod.NoByPass {
+		return
+	}
+	go browser.EachEvent(func(e *proto.TargetTargetCreated) {
+		defer func() {
+			if r := recover(); r != nil {
+
+			}
+		}()
+		if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+			return
+		}
+		page := browser.MustPageFromTargetID(e.TargetInfo.TargetID)
+		if !lxrod.NoByPass {
+			_, err := page.EvalOnNewDocument(stealth.JS)
+			if err != nil {
+				return
+			}
+			page.MustEvalOnNewDocument(stealth.JS)
+			page.MustSetUserAgent(nil)
+			utils.E(proto.RuntimeRunIfWaitingForDebugger{}.Call(page))
+		}
+		page.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+		if lxrod.MonitorDialog {
+			DialogWatch(ctx, page) //为新打开的页面添加一个dialog监控
+		}
+	})()
+	utils.E(proto.TargetSetAutoAttach{
+		AutoAttach:             true,
+		WaitForDebuggerOnStart: true,
+		Flatten:                true,
+	}.Call(browser))
+
+}
+
+func PageToClient(page *rod.Page, BaseURL string) (*http.Client, error) {
+	c := lxhttp.NewHttpClient()
+	e := rod.Try(func() {
+		cookies := page.MustCookies()
+		var cookiesArr []*http.Cookie
+		for _, v := range cookies {
+			var cookie http.Cookie
+			cookie.Name = v.Name
+			cookie.Value = v.Value
+			cookiesArr = append(cookiesArr, &cookie)
+		}
+		clientURL, _ := url.Parse(BaseURL)
+		c.Jar.SetCookies(clientURL, cookiesArr)
+	})
+	return c, e
+}
+
+func BrowserToClient(b *rod.Browser, BaseURL string) (*http.Client, error) {
+	domainMap := map[string][]*http.Cookie{}
+	c := lxhttp.NewHttpClient()
+	e := rod.Try(func() {
+		cookies := b.MustGetCookies()
+		for _, v := range cookies {
+			var cookie http.Cookie
+			cookie.Name = v.Name
+			cookie.Value = v.Value
+			cookie.Path = v.Path
+			cookie.Domain = v.Domain
+			domainMap[v.Domain] = append(domainMap[v.Domain], &cookie)
+		}
+		for _, cookie := range domainMap {
+			clientURL, _ := url.Parse(BaseURL)
+			c.Jar.SetCookies(clientURL, cookie)
+		}
+	})
+	return c, e
+}
+
+func BrowserToClientYunnan(b *rod.Browser, BaseURL string) (*http.Client, error) {
+	domainMap := map[string][]*http.Cookie{}
+	c := lxhttp.NewHttpClient()
+	e := rod.Try(func() {
+		cookies := b.MustGetCookies()
+		for _, v := range cookies {
+			var cookie http.Cookie
+			cookie.Name = v.Name
+			cookie.Value = v.Value
+			cookie.Path = v.Path
+			cookie.Domain = v.Domain
+			domainMap[v.Domain] = append(domainMap[v.Domain], &cookie)
+		}
+		for _, cookie := range domainMap {
+			clientURL, _ := url.Parse(BaseURL)
+			c.Jar.SetCookies(clientURL, cookie)
+		}
+		u, auth := GetLocalProxy("yunnan")
+		uu, _ := url.Parse(u)
+		c.Transport = &http.Transport{Proxy: http.ProxyURL(uu), ProxyConnectHeader: http.Header{
+			"Proxy-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(auth))},
+		}}
+	})
+	return c, e
+}
+
+func BrowserToClientByProxy(b *rod.Browser, BaseURL string) (*http.Client, error) {
+	area := ""
+	{
+		u, _ := url.Parse(BaseURL)
+		area = strings.Split(u.Host, ".")[1]
+	}
+	if area == "" {
+		return lxhttp.NewHttpClient(), taxerr.TimeOut
+	}
+	domainMap := map[string][]*http.Cookie{}
+	c := lxhttp.NewHttpClient()
+	proxy, auth := GetLocalProxy(area)
+	uu, _ := url.Parse(proxy)
+	tr := &http.Transport{Proxy: http.ProxyURL(uu)}
+	if auth != "" {
+		tr.ProxyConnectHeader = http.Header{
+			"Proxy-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(auth))},
+		}
+	}
+	c.Transport = tr
+	e := rod.Try(func() {
+		cookies := b.MustGetCookies()
+		for _, v := range cookies {
+			var cookie http.Cookie
+			cookie.Name = v.Name
+			cookie.Value = v.Value
+			cookie.Path = v.Path
+			cookie.Domain = v.Domain
+			domainMap[v.Domain] = append(domainMap[v.Domain], &cookie)
+		}
+		for _, cookie := range domainMap {
+			clientURL, _ := url.Parse(BaseURL)
+			c.Jar.SetCookies(clientURL, cookie)
+		}
+	})
+	return c, e
+}
+
+func (lxrod *Lxrod) NewByHjj(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	_ = rod.Try(func() {
+		_ = lxrod.CloseBrowser()
+	})
+	lxrod.Launcher = launcher.New()
+	lxrod.Launcher.Set("window-size", "1600,900").
+		Set("no-sandbox").
+		Set("ignore-certificate-errors").
+		Set("ignore-certificate-errors-spki-list").
+		Set("ignore-ssl-errors").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Headless(lxrod.Headless)
+	//lxrod.Launcher.UserDataDir("user-data")
+	lxrod.Launcher.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	u, err := lxrod.Launcher.Launch()
+	if err != nil {
+
+		err = taxerr.New("调用浏览器失败,请稍后重试!")
+		return lxrod.Browser, p, err
+	}
+	err = rod.Try(func() {
+		lxrod.Browser = rod.New().ControlURL(u).MustConnect().Context(ctx)
+		utils.Sleep(1)
+		lxrod.Browser.MustSetCookies()
+	})
+	if err != nil {
+		return nil, nil, taxerr.NewWebStuckTitle(true)
+	}
+	lxrod.Browser.NoDefaultDevice()
+	p = stealth.MustPage(lxrod.Browser)
+	if lxrod.MonitorDialog {
+		//为新打开的页面添加一个dialog监控
+		DialogWatch(ctx, p)
+		go lxrod.Browser.EachEvent(func(e *proto.TargetTargetCreated) {
+			defer func() {
+				if r := recover(); r != nil {
+
+				}
+			}()
+			if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+				return
+			}
+			page := lxrod.Browser.MustPageFromTargetID(e.TargetInfo.TargetID)
+			DialogWatch(ctx, page)
+		})()
+	}
+	p = p.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	return lxrod.Browser, p, err
+}
+
+// 新建浏览器对象
+func (lxrod *Lxrod) NewBeijing(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+		os.RemoveAll("tmp/")
+	})
+
+	lxrod.Launcher = launcher.NewUserMode()
+	u, err := lxrod.Launcher.
+		// New().
+		Leakless(true).
+		UserDataDir("tmp/t").
+		Set("disable-default-apps").
+		Set("no-first-run").
+		//Set("disable-plugins").
+		Set("disable-popup-blocking").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Headless(lxrod.Headless).
+		Launch()
+	if err != nil {
+
+		return nil, nil, taxerr.NewUser("浏览器创建失败请稍后重试1")
+	}
+
+	utils.Sleep(3)
+
+	err = rod.Try(func() {
+		b = rod.New().ControlURL(u).Timeout(time.Second * 60).MustConnect().Context(ctx).NoDefaultDevice()
+	})
+	if err != nil {
+
+		return nil, nil, taxerr.NewUser("浏览器创建失败请稍后重试2")
+	}
+
+	b.MustIncognito()
+	p = b.MustPage("")
+	lxrod.Browser = b
+	go b.EachEvent(func(e *proto.TargetTargetCreated) {
+		defer func() {
+			if r := recover(); r != nil {
+
+			}
+		}()
+		if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+			return
+		}
+		page := b.MustPageFromTargetID(e.TargetInfo.TargetID)
+		page.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	})()
+	return b, p, err
+}
+
+func (lxrod *Lxrod) NewHuBei(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	l := launcher.
+		New().
+		Set("window-size", "1600,900").
+		Leakless(true).
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Headless(false)
+	lxrod.Launcher = l
+	//lxrod.Launcher.UserDataDir("user-data")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	//if viper.GetString("env") == string(variable.Development) {
+	//	l.Leakless(false) //开发环境不使用用leakless
+	//}
+	u := l.MustLaunch()
+
+	b = rod.New().ControlURL(u).MustConnect().NoDefaultDevice()
+	p = stealth.MustPage(b)
+	lxrod.Browser = b
+	go b.EachEvent(func(e *proto.TargetTargetCreated) {
+		defer func() {
+			if r := recover(); r != nil {
+
+			}
+		}()
+		if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+			return
+		}
+		page := b.MustPageFromTargetID(e.TargetInfo.TargetID)
+
+		_, err := page.EvalOnNewDocument(stealth.JS)
+		if err != nil {
+			return
+		}
+		page.MustEvalOnNewDocument(stealth.JS)
+		page.MustSetUserAgent(nil)
+		utils.E(proto.RuntimeRunIfWaitingForDebugger{}.Call(page))
+		page.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	})()
+	utils.E(proto.TargetSetAutoAttach{
+		AutoAttach:             true,
+		WaitForDebuggerOnStart: true,
+		Flatten:                true,
+	}.Call(b))
+	return b, p, err
+}
+func (lxrod *Lxrod) NewNoParam() (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	l := launcher.
+		New().
+		Set("window-size", "1600,900").
+		Leakless(true).
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Headless(false)
+	lxrod.Launcher = l
+	//lxrod.Launcher.UserDataDir("user-data")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	//if viper.GetString("env") == string(variable.Development) {
+	//	l.Leakless(false) //开发环境不使用用leakless
+	//}
+	u := l.MustLaunch()
+
+	b = rod.New().ControlURL(u).MustConnect().NoDefaultDevice()
+	p = stealth.MustPage(b)
+	lxrod.Browser = b
+	go b.EachEvent(func(e *proto.TargetTargetCreated) {
+		defer func() {
+			if r := recover(); r != nil {
+
+			}
+		}()
+		if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+			return
+		}
+		page := b.MustPageFromTargetID(e.TargetInfo.TargetID)
+
+		_, err := page.EvalOnNewDocument(stealth.JS)
+		if err != nil {
+			return
+		}
+		page.MustEvalOnNewDocument(stealth.JS)
+		page.MustSetUserAgent(nil)
+		utils.E(proto.RuntimeRunIfWaitingForDebugger{}.Call(page))
+		page.MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	})()
+	utils.E(proto.TargetSetAutoAttach{
+		AutoAttach:             true,
+		WaitForDebuggerOnStart: true,
+		Flatten:                true,
+	}.Call(b))
+	return b, p, err
+}
+
+// 最简单的浏览器创建
+func (lxrod *Lxrod) NewLowBrowser(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		if lxrod.Browser != nil {
+			lxrod.CloseBrowser()
+			utils.Sleep(5)
+		}
+	})
+	l := launcher.New().
+		Headless(false).
+		Set("high-dpi-support", "1").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Set("force-device-scale-factor", "1")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	lxrod.Launcher = l
+	//lxrod.Launcher.UserDataDir("user-data")
+	//if lxrod.Area == "hebei" {
+	//	lxrod.Launcher.UserDataDir("user-data")
+	//}
+	if lxrod.UseProxy {
+		lxrod.Proxy, lxrod.ProxyAuth = GetLocalProxy(string(lxrod.Area))
+		lxrod.Launcher.Proxy(lxrod.Proxy)
+	}
+	wsURL, err := l.Launch()
+
+	lxrod.WsURL = wsURL
+	if err != nil {
+		return nil, nil, taxerr.NewWebStuckTitle(true)
+	}
+	b = rod.New().ControlURL(wsURL).Context(ctx).MustConnect()
+	utils.Sleep(1)
+	b = b.MustSetCookies()
+
+	if lxrod.UseProxy && lxrod.ProxyAuth != "" {
+		auths := strings.Split(lxrod.ProxyAuth, ":")
+		go b.HandleAuth(auths[0], auths[1])()
+	}
+	p = b.MustPage().MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+
+	if lxrod.MonitorDialog && lxrod.NoEachEventForCreated {
+		//为新打开的页面添加一个dialog监控
+		DialogWatch(ctx, p)
+		go b.EachEvent(func(e *proto.TargetTargetCreated) {
+			defer func() {
+				if r := recover(); r != nil {
+
+				}
+			}()
+			if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+				return
+			}
+			page := b.MustPageFromTargetID(e.TargetInfo.TargetID)
+			DialogWatch(ctx, page)
+		})()
+	}
+	lxrod.Browser = b
+	return b, p, nil
+}
+
+// NewBrowserBank360 打开 360 急速浏览器。适用于既要 ocx 密码输入又要 chrome 采集的银行任务,
+// 如承德银行。运行前需要先安装 360 急速浏览器,然后将 360 急速浏览器的安装目录添加到环境变量中。
+// 对于银行采集服务器,这个目录一般是:
+//
+//	"C:\\Users\\taxrobot\\Desktop\\360Chrome1\\"
+func (lxrod *Lxrod) NewBrowserBank360(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+
+	bin, err := exec.LookPath("360chromeX.exe")
+	if err != nil {
+		return nil, nil, taxerr.NewUser("请先安装360极速浏览器(" + err.Error() + ")")
+	}
+
+	l := launcher.New().
+		Headless(false).
+		Bin(bin).
+		//Bin("C:\\Program Files (x86)\\税局小助手\\360Chrome\\360chromeX.exe").
+		Set("high-dpi-support", "1").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Set("force-device-scale-factor", "1")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+
+	lxrod.Launcher = l
+	wsURL, err := l.Launch()
+	lxrod.WsURL = wsURL
+	if err != nil {
+
+		return nil, nil, taxerr.NewWebStuckTitle(true)
+	}
+
+	b = rod.New().ControlURL(wsURL).Context(ctx).MustConnect()
+	p = b.MustPage().MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+
+	if lxrod.MonitorDialog {
+		//为新打开的页面添加一个dialog监控
+		DialogWatch(ctx, p)
+		go b.EachEvent(func(e *proto.TargetTargetCreated) {
+			defer func() {
+				if r := recover(); r != nil {
+
+				}
+			}()
+			if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
+				return
+			}
+			page := b.MustPageFromTargetID(e.TargetInfo.TargetID)
+			DialogWatch(ctx, page)
+		})()
+	}
+	lxrod.Browser = b
+
+	return b, p, nil
+}
+
+// 最简单的浏览器创建2
+func (lxrod *Lxrod) NewLowBrowser2(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	l := launcher.New().
+		Headless(false).
+		Set("high-dpi-support", "1").
+		Set("disable-features", "CalculateNativeWinOcclusion").
+		Set("force-device-scale-factor", "1")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	lxrod.Launcher = l
+	//lxrod.Launcher.UserDataDir("user-data")
+	wsURL, err := l.Launch()
+	if err != nil {
+
+		return nil, nil, taxerr.NewWebStuckTitle(true)
+	}
+	b = rod.New().ControlURL(wsURL).Context(ctx).MustConnect()
+	p = b.MustPage().MustSetWindow(0, 0, 1600, 900).MustSetViewport(1600, 900, 1, false)
+	lxrod.Browser = b
+	if lxrod.MonitorDialog {
+		DialogWatch(ctx, p) //为新打开的页面添加一个dialog监控
+	}
+	if lxrod.UseProxy {
+		lxrod.Proxy, lxrod.ProxyAuth = GetLocalProxy(string(lxrod.Area))
+		lxrod.Launcher.Proxy(lxrod.Proxy)
+	}
+	return b, p, nil
+}
+
+// 自定义鼠标操作 方便升降版本
+type Mouse struct {
+	Mouse *rod.Mouse
+}
+
+func (m *Mouse) MustMove(x, y float64) *Mouse {
+	m.Mouse.MustMoveTo(x, y)
+	return m
+}
+
+func (m *Mouse) MustDown(button proto.InputMouseButton) *Mouse {
+	m.Mouse.MustDown(button)
+	return m
+}
+
+func (m *Mouse) MustUp(button proto.InputMouseButton) *Mouse {
+	m.Mouse.MustUp(button)
+	return m
+}
+
+func (m *Mouse) MustClick(button proto.InputMouseButton) *Mouse {
+	m.Mouse.MustClick(button)
+	return m
+}
+
+func ShenzhenProxy() string {
+	// 获取私密代理IP API
+	api := "https://dps.kdlapi.com/api/getdps"
+
+	// 请求参数
+	params := url.Values{}
+	params.Set("secret_id", "odr5kxfzzky69wbtuhzh")
+	params.Set("signature", "qwndesfg2ouix111a8rfzyixou7q9nr3")
+	params.Set("num", strconv.Itoa(1)) // 提取数量
+
+	// 构建完整的URL,包括参数
+	fullURL := api + "?" + params.Encode()
+
+	// 发送GET请求
+	response, err := http.Get(fullURL)
+	if err != nil {
+		fmt.Println("Error:", err)
+		return ""
+	}
+	defer response.Body.Close()
+
+	// 读取响应内容
+	body, err := io.ReadAll(response.Body)
+	if err != nil {
+		fmt.Println("Error reading response body:", err)
+		return ""
+	}
+
+	// 输出响应结果
+	return "http://" + string(body)
+}
+
+// 备用
+func (lxrod *Lxrod) BeijingNew(ctx context.Context) (b *rod.Browser, p *rod.Page, err error) {
+	_ = rod.Try(func() {
+		lxrod.CloseBrowser()
+	})
+	l := launcher.New().Set("window-size", "1600,900").
+		Delete("enable-automation").
+		Leakless(true).
+		Headless(false).Set("disable-features", "CalculateNativeWinOcclusion")
+	l.Flags["disable-blink-features"] = []string{"AutomationControlled"}
+	u := l.MustLaunch()
+	b = rod.New().ControlURL(u).MustConnect()
+	p = b.MustPage("")
+	lxrod.Browser = b
+	return b, p, nil
+}
+
+// GetLocalProxy 获取所有地区代理 部分地区是使用的我们自己的代理池
+func GetLocalProxy(Area string) (string, string) {
+	// 都换成5879
+	if Area == "xizang" /*|| Area == "yunnan"*/ || Area == "hebei" || Area == "liaoning" || Area == "chongqing" || Area == "sichuan" {
+		return `http://8.141.85.129:5879`, "lxuser:Aa123456"
+	}
+	if Area == "yunnan" {
+		return "http://106.58.178.172:27237", "13280888700:nPDZ0RYX"
+	}
+	if Area == "shenzhen" {
+		return ShenzhenProxy(), "d2879234360:ikdnhzbk"
+	}
+	return `http://8.141.85.129:5879`, "lxuser:Aa123456"
+}

+ 54 - 0
common/lxrod/screenshot.go

@@ -0,0 +1,54 @@
+package lxrod
+
+import (
+	"context"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
+	"git.listensoft.net/lx/taxrobot/common/clients"
+	"git.listensoft.net/lx/taxrobot/common/path"
+	"git.listensoft.net/lx/taxrobot/model"
+	"github.com/go-rod/rod"
+	"go.uber.org/zap"
+)
+
+// 截图并上传
+func PostScreenshot(l *zap.Logger, p *rod.Page, task *model.TaxTask) error {
+	l.Debug("尝试保存截图")
+	imgPath := path.GetErrImgPath(task.TaxNo, task.Period)
+	bin, err := p.Timeout(10*time.Second).Screenshot(false, nil)
+	if err != nil {
+		l.Error("截图创建失败", zap.Error(err))
+		return err
+	}
+	err = os.WriteFile(imgPath, bin, 0666)
+	if err != nil {
+		l.Error("截图保存失败", zap.Error(err))
+		return err
+	}
+	l.Info("截图保存成功", zap.String("path", imgPath))
+
+	l.Debug("尝试上传截图")
+	ctx, cancel := context.WithTimeout(p.GetContext(), 30*time.Second)
+	defer cancel()
+
+	fc := clients.FileClient{
+		Client: http.DefaultClient,
+		Logger: l,
+	}
+	businessImg, err := fc.PostScreenShotBytes(ctx, bin, clients.PostScreenShotReq{
+		Name:   filepath.Base(imgPath),
+		TaxNo:  task.TaxNo,
+		Period: task.Period,
+	})
+	if err != nil {
+		l.Error("截图上传失败", zap.Error(err))
+		return err
+	}
+
+	l.Info("截图上传成功", zap.String("businessImg", businessImg))
+	task.Result.BusinessImg = businessImg
+	return nil
+}

+ 53 - 0
common/models/info.go

@@ -0,0 +1,53 @@
+package models
+
+import (
+	"git.listensoft.net/tool/jspkit/common/variable"
+)
+
+type CompanyInfo struct {
+	ComName             string           `json:"comName,omitempty"`       //公司名称
+	TaxNo               string           `json:"taxNo,omitempty"`         //纳税人识别号
+	Dlfs                string           `json:"dlfs,omitempty"`          //登录方式
+	Xzsf                string           `json:"xzsf,omitempty"`          //办税身份
+	IdNo                string           `json:"id_no,omitempty"`         //账号
+	Password            string           `json:"password,omitempty"`      //密码
+	Zzrxm               string           `json:"zzrxm,omitempty"`         //办税人姓名
+	Zzridno             string           `json:"zzridno,omitempty"`       //办税人身份证号码
+	Zzrmm               string           `json:"zzrmm,omitempty"`         //办税人密码
+	Tel                 string           `json:"tel,omitempty"`           //手机号
+	Area                string           `json:"area,omitempty"`          //所属区域
+	Period              string           `json:"period,omitempty"`        //账期
+	VatBc               bool             `json:"vatBc,omitempty"`         //补充申报表是否申报
+	Sffgs               bool             `json:"sffgs,omitempty"`         //是否分公司
+	Lslf                bool             `json:"lslf,omitempty"`          //六税两费
+	AccountSystem       variable.Kjze    `json:"accountSystem,omitempty"` //会计准则
+	Gslx                variable.Gslx    `json:"gslx,omitempty"`          //公司类型
+	Sfxy                string           `json:"sfxy,omitempty"`          //三方协议
+	Nsrlx               variable.Nsrlx   `json:"nsrlx,omitempty"`         //纳税人类型
+	Qysdslx             variable.Qysdslx `json:"qysdslx,omitempty"`       //企业所得税类型
+	XjllbSb             bool             `json:"xjllb_sb,omitempty"`      //是申报现金流量表
+	Bsbcwbb             bool             `json:"bsbcwbb,omitempty"`       //是否需要申报财务报表
+	Sfsxbdc             bool             `json:"sfxsbdc,omitempty"`       //增值税是否销售不动产
+	Zgswjg              string           `json:"zgswjg"`                  //主管税务机关
+	HqDkFp              bool             `json:"hqDkFp,omitempty"`        //是否获取代开发票
+	Cscsts              bool             `json:"cscsts,omitempty"`        //超时是否提示重试
+	comURL              string           // 公司URL
+	Ckts                bool             `json:"ckts,omitempty"`    //是否采集出口退税票
+	GxqrAll             bool             `json:"gxqrAll,omitempty"` //是否全部勾选确认
+	NewTaxBsQx          bool             //新版税局办税权限(身份里有办税员、财务负责人、法人时为true
+	LoginChsCom         string           //登陆选择时的企业名
+	Jcwq                bool             `json:"jcwq"`     //申报检查-是否检查往期 如果true检查往期,检查往期是否有未交款的 有往期未交款 提示有遗漏状态
+	IsNewApp            bool             `json:"isNewApp"` // 保活用 区分新旧版服务 刷新状态
+	GsMm                string           `json:"gsMm"`     //个税密码
+	GsDlfs              string           `json:"gsDlfs"`   //个税登录方式
+	NoTxfFp             bool             `json:"noTxfFp"`  //是否不采集通行费发票
+	Wqsb                bool             // 社保采集申报扣款往期
+	Dldl                bool             `json:"dldl"`                //是否需要先代理登录
+	AgTaxNo             string           `json:"agTaxNo"`             //agencyTaxNo代理机构税号
+	AgTel               string           `json:"agTel"`               //代理机构登录账号
+	AgPwd               string           `json:"agPwd"`               //代理机构登录密码
+	SheBaoPassword      string           `json:"shebaoPassword"`      // 社保支付密码
+	SheBaoPasswordLogin string           `json:"shebaoPasswordLogin"` // 单独社保登录密码
+	ConvertKj           string           // 自定义会计制度
+	Qzsbcwbb            bool             `json:"qzsbcwbb"` //强制申报财务报表,如果已申报就去更正
+}

+ 66 - 0
common/models/task.go

@@ -0,0 +1,66 @@
+package models
+
+import (
+	"context"
+	variable2 "git.listensoft.net/tool/jspkit/common/variable"
+	"github.com/go-rod/rod"
+	"net/http"
+)
+
+type Result struct {
+	NodeName       string              `json:"nodeName"`       //机器名
+	ReqNo          string              `json:"reqNo"`          //唯一识别号
+	Status         variable2.TaxStatus `json:"status"`         //任务状态 默认成功 不准动这个状态
+	ErrLog         string              `json:"errLog"`         //不用管这个log
+	BusinessImg    string              `json:"businessImg"`    //截图
+	BusinessTime   string              `json:"businessTime"`   //截图时间
+	BusinessStatus variable2.TaxStatus `json:"businessStatus"` //业务状态
+	BusinessLog    string              `json:"b·usinessLog"`   //业务有错误时返回的提示语
+	Amount         float64             `json:"amount"`         //申报金额
+	AmountPaid     float64             `json:"amountPaid"`     //已扣款金额
+	Data           interface{}         `json:"data"`           //采集的数据
+	RobotName      string              `json:"robotName"`      // 执行的机器人名称
+	CwbbDb         bool                // 财报代办已申报过
+}
+
+type TaxTask struct {
+	OrgId            uint               `json:"orgId"`
+	ComName          string             `json:"comName"`
+	Period           string             `json:"period"` // 账期
+	ReqNo            string             `json:"reqNo"`  // 任务唯一标识。用于结束任务
+	TaskName         variable2.TaskName `json:"taskName"`
+	LoginType        string             `json:"loginType"`
+	TaxNo            string             `json:"tax_no"`
+	Address          string             `json:"address"`
+	IdType           string             `json:"idType"`
+	IdNo             string             `json:"id_no"`
+	Password         string             `json:"password"`
+	Xzsf             string             `json:"xzsf"`
+	Zzrxm            string             `json:"zzrxm"`
+	Zzridno          string             `json:"zzridno"`
+	Zzrmm            string             `json:"zzrmm"`
+	ManagerName      string             `json:"managerName"`
+	ManagerTel       string             `json:"managerTel"`
+	ManagerIdNo      string             `json:"managerIdNo"`
+	LinkName         string             `json:"linkName"`
+	LinkTel          string             `json:"linkTel"`
+	LinkIdNo         string             `json:"linkIdNo"`
+	TaxCollectorName string             `json:"taxCollectorName"`
+	TaxCollectorTel  string             `json:"taxCollectorTel"`
+	TaxCollectorIdNo string             `json:"taxCollectorIdNo"`
+	Tel              string             `json:"tel"`
+	Data             string             `json:"data"`
+	Result           Result             `json:"result"`
+	Leave            int                // 申报优先级
+}
+
+type SbParams struct {
+	BaseUrl   string
+	Ctx       context.Context
+	Page      *rod.Page
+	Info      *CompanyInfo
+	Task      *TaxTask
+	C         *http.Client
+	Browser   *rod.Browser
+	OtherTask *TaxTask //存放企业所得税使用的财报task 小规模则存放定期定额task
+}

+ 51 - 0
common/variable/area.go

@@ -0,0 +1,51 @@
+package variable
+
+type Area string
+
+// 国家各地区电子税务局 共37个电子税务局
+const (
+
+	//4个直辖市
+	Beijing   Area = "beijing"
+	Tianjin   Area = "tianjin"
+	Shanghai  Area = "shanghai"
+	Chongqing Area = "chongqing"
+
+	//5个单列计划市
+	Dalian   Area = "dalian"
+	Qingdao  Area = "qingdao"
+	Ningbo   Area = "ningbo"
+	Xiamen   Area = "xiamen"
+	Shenzhen Area = "shenzhen"
+
+	//5个自治区
+	Neimenggu Area = "neimenggu"
+	Guangxi   Area = "guangxi"
+	Xizang    Area = "xizang"
+	Ningxia   Area = "ningxia"
+	Xinjiang  Area = "xinjiang"
+
+	//23个省
+	Hebei        Area = "hebei"
+	Shanxi       Area = "shanxi"
+	Liaoning     Area = "liaoning"
+	Jilin        Area = "jilin"
+	Heilongjiang Area = "heilongjiang"
+	Jiangsu      Area = "jiangsu"
+	Zhejiang     Area = "zhejiang"
+	Anhui        Area = "anhui"
+	Fujian       Area = "fujian"
+	Jiangxi      Area = "jiangxi"
+	Shandong     Area = "shandong"
+	Henan        Area = "henan"
+	Hubei        Area = "hubei"
+	Hunan        Area = "hunan"
+	Guangdong    Area = "guangdong"
+	Hainan       Area = "hainan"
+	Sichuan      Area = "sichuan"
+	Guizhou      Area = "guizhou"
+	Yunnan       Area = "yunnan"
+	Shaanxi      Area = "shaanxi"
+	Gansu        Area = "gansu"
+	Qinghai      Area = "qinghai"
+)

+ 121 - 0
common/variable/const.go

@@ -0,0 +1,121 @@
+package variable
+
+const TaxTaskURL string = "https://task.listensoft.net" //获取任务的URL
+
+type LXContextVar string // ctx传递存储的信息KEY
+type Environment string  //环境变量
+type Kjze string         //会计准则
+
+func (k Kjze) Check() bool {
+	if k == KjXqy2013 || k == KjYbqyWzx || k == KjQykjzd || k == KjYbqyYzx || k == KjMbf || k == Nyhzs {
+		return true
+	}
+	return false
+}
+
+const (
+	KjXqy2013 Kjze = "小企业会计准则2013版"
+	KjYbqyWzx Kjze = "一般企业会计准则" // (未执行新金融准则)
+	KjQykjzd  Kjze = "企业会计制度"
+	KjYbqyYzx Kjze = "一般企业会计准则(已执行新金融准则)"
+	KjMbf     Kjze = "民办非"
+	Nyhzs     Kjze = "农业合作社"
+)
+
+type Gslx string //公司类型
+func (k Gslx) Check() bool {
+	if k == Yszrgs || k == Gtgsh || k == Grdzqy || k == Hhqr || k == Nchzs || k == Mbf {
+		return true
+	}
+	return false
+}
+
+const (
+	Yszrgs Gslx = "有限责任公司"
+	Gtgsh  Gslx = "个体工商户"
+	Grdzqy Gslx = "个人独资企业"
+	Hhqr   Gslx = "合伙企业"
+	Nchzs  Gslx = "农村合作社"
+	Mbf    Gslx = "民办非企业单位"
+)
+
+type Nsrlx string //纳税人类型
+
+func (q Nsrlx) Check() bool {
+	if q == Ybnsr || q == Xgmnsr {
+		return true
+	}
+	return false
+}
+
+const (
+	Ybnsr  Nsrlx = "一般纳税人"
+	Xgmnsr Nsrlx = "小规模纳税人"
+)
+
+type Qysdslx string
+
+func (q Qysdslx) Check() bool {
+	if q == QysdsA || q == QysdsB {
+		return true
+	}
+	return false
+}
+
+const (
+	QysdsA Qysdslx = "A"
+	QysdsB Qysdslx = "B"
+)
+
+type TaxStatus int //业务状态
+
+const (
+	_                     TaxStatus = iota
+	TaxDoing                             //通用:
+	TaxSuccess                           //通用: 成功  	    申报:申报成功无需扣款  税局扣款:成功  	申报检查:成功     发票采集: 成功
+	TaxFail                              //通用: 失败 	    申报:申报失败		  税局扣款:失败    申报检查: 失败	   发票采集: 失败
+	TaxTiJiaoJinSan                      //通用: 			申报:提交金三						   					发票采集: 预约采集处理中
+	TaxSuccessNeedPay                    //通用: 			申报:申报成功,待缴款									 发票采集: 成功(未勾选发票)
+	TaxSuccessPaid                       //通用: 			申报:申报成功,已缴款
+	TaxSuccessd           TaxStatus = 20 //通用:			申报: 已申报过,无需扣款
+	TaxSuccessdNeedPay    TaxStatus = 21 //通用: 			申报:已申报过,待缴款
+	TaxSuccessdPaid       TaxStatus = 22 //通用: 			申报:已申报过,已缴款
+	TaxSuccessdInequality TaxStatus = 23 //通用:            申报:已申报过,但税额不符
+	TaxSuccessdOmit       TaxStatus = 30 //通用:													申报检查:有遗漏
+	TaxNoNeed             TaxStatus = 40 //通用:			申报:无需申报(未核定财务报表用这个状态)    申报检查:无需申报(未核定财务报表用这个状态)
+	TaxAbnormal           TaxStatus = 50 //														申报检查:已申报,有异常
+	TaxSuccessNeedRefund  TaxStatus = 60 // 汇算清缴专用 申报成功,待退税
+)
+
+func (tax TaxStatus) Check() bool { //检验状态
+	// TaxDoing 永远不用
+	if tax != TaxSuccess &&
+		tax != TaxFail &&
+		tax != TaxTiJiaoJinSan &&
+		tax != TaxSuccessNeedPay &&
+		tax != TaxSuccessPaid &&
+		tax != TaxSuccessd &&
+		tax != TaxSuccessdNeedPay &&
+		tax != TaxSuccessdPaid &&
+		tax != TaxSuccessdOmit &&
+		tax != TaxNoNeed &&
+		tax != TaxSuccessdInequality &&
+		tax != TaxAbnormal {
+		return false
+	}
+	return true
+}
+
+func (tax TaxStatus) Success() bool { //检验是否成功状态
+	// TaxDoing 永远不用
+	if tax == TaxSuccess ||
+		tax == TaxTiJiaoJinSan ||
+		tax == TaxSuccessNeedPay ||
+		tax == TaxSuccessPaid ||
+		tax == TaxNoNeed {
+		return true
+	}
+	return false
+}
+
+type TaxErrorText string

+ 154 - 0
common/variable/task.go

@@ -0,0 +1,154 @@
+package variable
+
+type TaskName string
+
+const (
+	//采集
+	TaxCjJianzhang   TaskName = "tax-cj-jianzhang"   //建账采集(企业信息和税种税表)
+	TaxCjJump        TaskName = "tax-cj-jump"        //跳过采集
+	TaxCjTaxCategory TaskName = "tax-cj-taxCategory" //更新税种
+	TaxCjResetTaxs   TaskName = "tax-cj-resetTaxs"   //重新采集税表
+	TaxCjYyInvoice   TaskName = "tax-cj-invoice"     //预约采集
+	TaxCjOutinvoice  TaskName = "tax-cj-outInvoice"  //销项发票
+	TaxCjInInvoice   TaskName = "tax-cj-inInvoice"   //进项发票
+	TaxCjInvoicePdf  TaskName = "cj-invoice-pdf"     //全电发票pdf
+	TaxCjInvoice     TaskName = "tax-cj-invoice"     //进项销项发票
+	TaxCjPdf         TaskName = "tax-cj-pdf"         //重发为了生成发票影像
+	TaxCjShebao      TaskName = "tax-cj-shebao"      //社保采集
+	TaxCjCwbb        TaskName = "tax-cj-cwbb"        //社保采集
+	//申报
+	TaxSbSmallVat   TaskName = "tax-sb-smallVat"   //小规模_增值税纳税
+	TaxSbVat        TaskName = "tax-sb-vat"        //增值税一般纳税人
+	TaxSbTaxQuarter TaskName = "tax-sb-taxQuarter" //企业所得税申报表
+	TaxSbDeed       TaskName = "tax-sb-deed"       //行为税
+	TaxSbDeedYhs    TaskName = "tax-sb-deed-yhs"   //印花税
+	TaxSbDeedZys    TaskName = "tax-sb-deed-zys"   //资源税
+	TaxSbDeedFcs    TaskName = "tax-sb-deed-fcs"   //房产税
+	TaxSbDeedTds    TaskName = "tax-sb-deed-tds"   //土地税
+	TaxSbQtsr       TaskName = "tax-sb-qtsr"       //其他收入
+	TaxSbSl         TaskName = "tax-sb-sl"         //水利建设专项收入
+	TaxSbWhsyjsf    TaskName = "tax-sb-whsyjsf"    //文化事业建设费
+	TaxSbCbj        TaskName = "tax-sb-cbj"        //残疾人保障金g
+	TaxSbXfs        TaskName = "tax-sb-xfs"        //消费税
+	TaxSbLjcl       TaskName = "tax-sb-ljcl"       //城市生活垃圾处置费
+	TaxSbShebao     TaskName = "tax-sb-shebao"     //社保申报
+	TaxSbCwbb       TaskName = "tax-sb-cwbb"       //财务报表
+	TaxSbFcs        TaskName = "tax-sb-fcs"        //房产税
+	TaxSbDqde       TaskName = "tax-sb-dqde"       //定期定额
+	//扣款
+	TaxKkSmallVat   TaskName = "tax-kk-smallVat"   //小规模_增值税纳税
+	TaxKkVat        TaskName = "tax-kk-vat"        //增值税一般纳税人
+	TaxKkTaxQuarter TaskName = "tax-kk-taxQuarter" //企业所得税申报表
+	TaxKkDeed       TaskName = "tax-kk-deed"       //行为税
+	TaxKkQtsr       TaskName = "tax-kk-qtsr"       //其他收入
+	TaxKkSl         TaskName = "tax-kk-sl"         //水利建设专项收入
+	TaxKkWhsyjsf    TaskName = "tax-kk-whsyjsf"    //文化事业建设费
+	TaxKkCbj        TaskName = "tax-kk-cbj"        //残疾人保障金
+	TaxKkXfs        TaskName = "tax-kk-xfs"        //消费税
+	TaxKkLjcl       TaskName = "tax-kk-ljcl"       //城市生活垃圾处置费
+	TaxKkFjs        TaskName = "tax-kk-fjs"        //附加税
+	TaxKkShebao     TaskName = "tax-kk-shebao"     //社保申报
+	TaxKkDqde       TaskName = "tax-kk-dqde"       // 扣款定期定额
+	//检查
+	TaxJcShenBao TaskName = "tax-jc-shenbao" //检查
+	TaxJcShebao  TaskName = "tax-jc-shebao"  //社保检查
+	//云端历史数据采集
+	HisCjHistoryCollect TaskName = "his-cj-historyCollect" //历史数据采集
+	HisCjHistoryImport  TaskName = "his-cj-historyImport"  //历史数据导入
+	HisCjHistoryMatch   TaskName = "his-cj-historyMatch"   //历史数据匹配
+	OtherCjJianzhang    TaskName = "other-cj-jianzhang"    //建账采集  jz-collect
+	TaxCjComInfo        TaskName = "tax-cj-comInfo"        //采集企业信息
+	TaxCjHisData        TaskName = "tax-cj-hisData"        //历史迁移数据
+	//年度
+	TaxYearCj   TaskName = "tax-year-cj"   //年报表采集
+	TaxYearSb   TaskName = "tax-year-sb"   //汇算清缴 年报表申报
+	TaxYearKk   TaskName = "tax-year-kk"   //汇算清缴 年报表扣款
+	TaxCwYearSb TaskName = "tax-cwyear-sb" //财务报表年报
+
+	TaxCjNdks TaskName = "tax-cj-ndks" //年报表采集
+
+	TaxYbjc TaskName = "tax-ybjc" //一表集成采集
+
+	CjBank            TaskName = "cj-bank"             // 银行明细账采集
+	CheckBankPassword TaskName = "check-bank-password" // 银行密码校验
+
+	//金蝶对接所需接口
+	TaxCjMmyz TaskName = "tax-cj-mmyz" //账号密码验证- 需要手机号的到发验证码那一步 不要点击发送验证码 需要截图
+	TaxCjQyxx TaskName = "tax-cj-qyxx" //采集企业信息+会计制度+纳税人性质(一般还是小规模)+ 每个企业信息的截图
+	//申报暂存功能 填表后点击暂存 不要申报 有问题提示 成功或者失败都要截图
+	TaxSbSmallVatZc   TaskName = "tax-sb-smallVatZc"   //小规模_增值税纳税
+	TaxSbVatZc        TaskName = "tax-sb-vatZc"        //增值税一般纳税人
+	TaxSbTaxQuarterZc TaskName = "tax-sb-taxQuarterZc" //企业所得税申报表
+	TaxSbDeedZc       TaskName = "tax-sb-deedZc"       //行为税
+	TaxSbQtsrZc       TaskName = "tax-sb-qtsrZc"       //其他收入
+	TaxSbSlZc         TaskName = "tax-sb-slZc"         //水利建设专项收入
+	TaxSbWhsyjsfZc    TaskName = "tax-sb-whsyjsfZc"    //文化事业建设费
+	TaxSbCbjZc        TaskName = "tax-sb-cbjZc"        //残疾人保障金g
+	TaxSbXfsZc        TaskName = "tax-sb-xfsZc"        //消费税
+	TaxSbLjclZc       TaskName = "tax-sb-ljclZc"       //城市生活垃圾处置费
+	TaxSbShebaoZc     TaskName = "tax-sb-shebaoZc"     //社保申报
+	TaxSbCwbbZc       TaskName = "tax-sb-cwbbZc"       //财务报表
+	TaxSbFcsZc        TaskName = "tax-sb-fcsZc"        //房产税
+	//财报更正功能
+	TaxSbCwbbGz TaskName = "tax-sb-cwbbGz" //财务报表更正
+
+	//涉水机构相关
+	SsjgPeopleSync TaskName = "ssjg-people-sync" //涉税机构人员同步
+	SsjgXySync     TaskName = "ssjg-xy-sync"     //涉税机构协议同步
+	Yjkp           TaskName = "open-invoice"     //一键开票
+
+	//登录上线 保活tpass任务
+	TaxTpassLogin TaskName = "tax-tpass-login"
+
+	//抵扣确认
+	TaxGxrz TaskName = "tax-gxrz"
+
+	//顶呱呱对接
+	TaxCjSbqc   TaskName = "tax-cj-sbqc"   //采集申报清册
+	TaxCjSwbb   TaskName = "tax-cj-swbb"   //下载税务报表
+	TaxCjQcybjc TaskName = "tax-cj-qcybjc" //集成申报清册的数据 企业所得税没报财报进不去的提示err:"请先申报财报后在提取数据"
+	TaxCjGsnb   TaskName = "gsnb-cj"       //工商年报采集
+	TaxSbGsnb   TaskName = "gsnb-sb"       //工商年报申报
+
+	//完税证明采集
+	TaxCjWszm TaskName = "tax-cj-wszm" //完税证明采集
+
+	TaxSbUpdate  TaskName = "tax-sb-update"  //申报更正
+	TaxSbInvalid TaskName = "tax-sb-invalid" //申报作废
+	// 作废
+	TaxZfSmallVat   TaskName = "tax-zf-smallVat"   //小规模_增值税纳税
+	TaxZfVat        TaskName = "tax-zf-vat"        //增值税一般纳税人
+	TaxZfTaxQuarter TaskName = "tax-zf-taxQuarter" //企业所得税申报表
+	TaxZfDeed       TaskName = "tax-zf-deed"       //行为税
+	TaxZfQtsr       TaskName = "tax-zf-qtsr"       //其他收入
+	TaxZfSl         TaskName = "tax-zf-sl"         //水利建设专项收入
+	TaxZfWhsyjsf    TaskName = "tax-zf-whsyjsf"    //文化事业建设费
+	TaxZfCbj        TaskName = "tax-zf-cbj"        //残疾人保障金g
+	TaxZfXfs        TaskName = "tax-zf-xfs"        //消费税
+	TaxZfLjcl       TaskName = "tax-zf-ljcl"       //城市生活垃圾处置费
+	TaxZfShebao     TaskName = "tax-zf-shebao"     //社保申报
+	TaxZfCwbb       TaskName = "tax-zf-cwbb"       //财务报表
+	TaxZfFcs        TaskName = "tax-zf-fcs"        //房产税
+	TaxZfDqde       TaskName = "tax-zf-dqde"       //定期定额
+	// 更正
+	TaxGzSmallVat   TaskName = "tax-gz-smallVat"   //小规模_增值税纳税
+	TaxGzVat        TaskName = "tax-gz-vat"        //增值税一般纳税人
+	TaxGzTaxQuarter TaskName = "tax-gz-taxQuarter" //企业所得税申报表
+	TaxGzDeed       TaskName = "tax-gz-deed"       //行为税
+	TaxGzQtsr       TaskName = "tax-gz-qtsr"       //其他收入
+	TaxGzSl         TaskName = "tax-gz-sl"         //水利建设专项收入
+	TaxGzWhsyjsf    TaskName = "tax-gz-whsyjsf"    //文化事业建设费
+	TaxGzCbj        TaskName = "tax-gz-cbj"        //残疾人保障金g
+	TaxGzXfs        TaskName = "tax-gz-xfs"        //消费税
+	TaxGzLjcl       TaskName = "tax-gz-ljcl"       //城市生活垃圾处置费
+	TaxGzCwbb       TaskName = "tax-gz-cwbb"       //财务报表
+	TaxGzFcs        TaskName = "tax-gz-fcs"        //房产税
+	TaxGzDqde       TaskName = "tax-gz-dqde"       //定期定额
+)
+
+func (t TaskName) Check() bool {
+	if t == TaxCjJianzhang {
+		return true
+	}
+	return false
+}

+ 94 - 0
task/task.go

@@ -0,0 +1,94 @@
+package task
+
+import (
+	"context"
+	"encoding/json"
+	"git.listensoft.net/tool/jspkit/common"
+	"git.listensoft.net/tool/jspkit/common/lxhttp"
+	"git.listensoft.net/tool/jspkit/common/models"
+	"git.listensoft.net/tool/jspkit/common/variable"
+	"github.com/go-kratos/kratos/v2/log"
+	"github.com/spf13/viper"
+	"github.com/tidwall/gjson"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+const taxMaxTime = 20 * time.Minute //超时默认20分钟
+
+// GetContent 根据地区定制Content 设置超时时间
+func GetContent(childCtx context.Context, address string, pdfTel string, ticker *time.Ticker) (childCtx1 context.Context) {
+	if address == "shandong" || address == "neimenggu" || address == "jiangsu" { // 针对20分钟不够用的地区
+		childCtx1, _ = context.WithTimeout(childCtx, 30*time.Minute)
+	} else if address == "sichuan" || address == "chongqing" || address == "shanxi" || (address == "anhui") {
+		childCtx1, _ = context.WithTimeout(childCtx, 40*time.Minute)
+	} else if address == "fujian" || address == "beijing" || address == "qinghai" || address == "xiamen" {
+		childCtx1, _ = context.WithTimeout(childCtx, 40*time.Minute)
+	} else {
+		childCtx1, _ = context.WithTimeout(childCtx, taxMaxTime)
+	}
+	if pdfTel != "" {
+		//childCtx1, _ = context.WithTimeout(childCtx, 120*time.Minute) //四川的pdf采集 一个小时不够
+		ticker = time.NewTicker(4 * time.Minute) // 每四分钟加五分钟的锁
+		go func() {
+			for t := range ticker.C { // 阻塞等待接收ticker发出的时间信号
+				common.AddTelLockerX2(pdfTel, "300")
+				log.Info("Tick at:", t)
+			}
+		}()
+	}
+	return
+}
+
+// GetTaxTask 获取任务
+func GetTaxTask(robotName string) (list []models.TaxTask, err error) {
+	client := lxhttp.NewHttpClient()
+	defer client.CloseIdleConnections()
+	param := map[string]string{
+		"robotName": robotName,
+	}
+	bys, err2 := lxhttp.POSTJson(client, variable.TaxTaskURL+"/api/v1/getRobotQueue", param, map[string]string{})
+	if err2 != nil {
+		return
+	}
+	data := gjson.GetBytes(bys, "data").String()
+	list = []models.TaxTask{}
+	err = json.Unmarshal([]byte(data), &list)
+	return
+}
+
+// EndQueue 释放机器人
+func EndQueue(robotName string) {
+	client := lxhttp.NewHttpClient()
+	defer client.CloseIdleConnections()
+	param := map[string]string{
+		"robotName": robotName,
+	}
+	bys, err2 := lxhttp.POSTJson(client, variable.TaxTaskURL+"/api/v1/endQueue", param, map[string]string{}) // https://task.listensoft.net
+	if err2 != nil {
+		return
+	}
+	log.Info("endQueue - ", string(bys))
+}
+
+// GetRobotName 获取当前机器人名称
+func GetRobotName() string {
+	//环境变量中读取名称
+	envRobotName := os.Getenv("robotName")
+	log.Info("envRobotName:", envRobotName)
+	if envRobotName != "" {
+		return envRobotName
+	}
+	//config中读取文件
+	confRobotName := viper.GetString("robotName")
+	log.Info("confRobotName:", confRobotName)
+	if confRobotName != "" { //如果是测试环境 固定一个 robotName 方便调试 一直获取一个任务
+		return confRobotName
+	}
+	//读取不到默认文件夹名称
+	dir, _ := os.Getwd()
+	_, file := filepath.Split(dir)
+	log.Info("fileDIrRobotName:", file)
+	return file
+}

+ 32 - 0
taxerr/bank.go

@@ -0,0 +1,32 @@
+package taxerr
+
+import "fmt"
+
+// NewBankSystemError 创建银行任务系统异常
+func NewBankSystemError(bank, reason, operation string) error {
+	return &SystemErr{
+		Msg: fmt.Sprintf("[异常]: [%s] %s [操作]: %s", bank, reason, operation),
+	}
+}
+
+// NewBankUserError 创建银行任务用户错误
+func NewBankUserError(bank, reason, operation string) error {
+	return &UserErr{
+		Msg: fmt.Sprintf("[错误]: [%s] %s [操作]: %s", bank, reason, operation),
+	}
+}
+
+// 创建没有银行账户错误
+func NewErrNoAccount(bank string) error {
+	return NewBankUserError(bank, "网银无银行账户", "请手动登录网银确认账户是否正常")
+}
+
+// 创建银行主账号不存在错误
+func NewErrAccountNotFound(bank string) error {
+	return NewBankUserError(bank, "指定的银行主账号不存在", "请核实后再发起采集")
+}
+
+// 创建银行账户匹配失败错误
+func NewErrAccMismatch(bank string) error {
+	return NewBankUserError(bank, "银行账户匹配失败", "请联系运维人员处理")
+}

+ 152 - 0
taxerr/err.go

@@ -0,0 +1,152 @@
+package taxerr
+
+import (
+	"errors"
+	"strings"
+
+	"git.listensoft.net/lx/taxrobot/common/zaplog"
+	"github.com/go-rod/rod"
+)
+
+// FormatLoginError 格式化错误信息
+func FormatLoginError(str string) error {
+	zaplog.LoggerS.Info("原始错误信息 - ", str)
+	if strings.Contains(str, "短信发送次数超限") {
+		return TelSmsOverrun
+	}
+	if strings.Contains(str, "手机号不在线") {
+		return SmsUnOnline
+	}
+	if strings.Contains(str, "请调用发送验证码") || strings.Contains(str, "您的登录已掉线,请重新授权后再次发起任务") {
+		return TelOffline
+	}
+	if strings.Contains(str, "手机验证码接收失败") || strings.Contains(str, "服务未获取到短信验证码") {
+		return SmsCodeNotReceived
+	}
+	if strings.Contains(str, "连续认证错误次数过多") || strings.Contains(str, "次输入个人用户密码错误") || strings.Contains(str, "次输入密码错误") {
+		return PasswdLockError
+	}
+	if strings.Contains(str, "输入的短信验证码错误") {
+		return TelSmsError
+	}
+	if strings.Contains(str, "输入的个人账号或密码错误") {
+		return PasswdError
+	}
+	if strings.Contains(str, "税局服务繁忙") {
+		return TaxAbnormalError(true)
+	}
+	if strings.Contains(str, "输入的统一社会信用代码或纳税人识别号错误") {
+		return TaxNoError
+	}
+	if strings.Contains(str, "未查询到您与该企业的关联关系") {
+		return ComIrrelevantRelationError
+	}
+	if strings.Contains(str, "未查询到您与该代理企业的关联关系信息") {
+		return NewUserV3("税局提示:未查询到您与该代理企业的关联关系信息", "请确认录入的信息是否正确")
+	}
+	if strings.Contains(str, "办税人登录信息与企业无关联关系") {
+		return ComIrrelevantRelationError
+	}
+	if strings.Contains(str, "税局服务繁忙") || strings.Contains(str, `登录失败~`) || strings.Contains(str, "内部异常:") ||
+		strings.Contains(str, "应用令牌无效") || str == "税局服务异常,请稍后重试" ||
+		strings.Contains(str, "渠道认证") {
+		// {"code":-1,"data":{},"message":"内部异常:error parse true"}
+		// {"code":-1,"data":{},"message":"登录失败~syntax error, pos 1, json : Read timed out"}
+		return TaxAbnormalError(true)
+	}
+	if strings.Contains(str, "手机号码不存在") {
+		return NewUserV3("税局提示:手机号码不存在", "您可以尝试使用证件号码登录")
+	}
+	if strings.Contains(str, "您与该企业的绑定关系已过期") {
+		return NewUserV3("税局提示:您与该企业的绑定关系已过期", "请重新绑定版税身份后重试")
+	}
+	if strings.Contains(str, "请在自然人业务入口进行用户注册") {
+		return UnRegisteredError
+	}
+	if strings.Contains(str, "登记企业信息不存在") {
+		return NewSystemV3("税局提示:登记企业信息不存在", "请核实")
+	}
+	if strings.Contains(str, "查询到您与该企业的关联关系信息仍未确认,请确认是否扫脸或稍后再试") {
+		return NewUserV3(`税局提示:查询到您与该企业的关联关系信息仍未确认,请确认是否扫脸或稍后再试`, "请核实该企业与办税员绑定是否正常")
+	}
+	if strings.Contains(str, "请先完成扫脸认证") || strings.Contains(str, "请前往税务APP扫脸认证身份") {
+		return NewUser(str)
+	}
+	if strings.Contains(str, "输入的个人账号、密码或报税手机号错误,请重新输入!") {
+		return TaxNotMatch
+	}
+	if strings.Contains(str, `确认成为该企业办税人失败,请在网站自行扫脸验证确认后使用`) {
+		return FaceVerifyError
+	}
+	if strings.Contains(str, "请核对账套内填写的手机号") || strings.Contains(str, "请自行确认为该企业内的身份2") {
+		return NewUser(str)
+	}
+	// 扫码失败或者login一分钟超时后等等待一分钟扫码失败
+	if strings.Contains(str, "请登陆后再操作") || strings.Contains(str, "请先完成登录,再执行此项业务") {
+		return TaxAbnormalError(true)
+	}
+	if strings.Contains(str, "不存在用户") || strings.Contains(str, "返回当前用户") {
+		return NewUser("代理登录未绑定该企业,请先去税局绑定")
+	}
+	if strings.Contains(str, "税号错误") {
+		return NewUserV3(`输入的纳税人识别号与公司名不匹配`, `请在“企业信息”修改后重试`)
+	}
+	if strings.Contains(str, "您使用短信验证码过于频繁") || strings.Contains(str, "短信发送申请过于频繁") {
+		return NewUser("税局提示:您使用短信验证码过于频繁")
+	}
+	if strings.Contains(str, "请先完成渠道认证") {
+		return NewUserV3(`请先完成渠道认证`, `再执行此项业务`)
+	}
+	return New(str)
+}
+
+// TaskErrMsg 尝试从错误中提取任务错误信息
+func TaskErrMsg(err error) (string, bool) {
+	if se := new(SystemErr); errors.As(err, &se) {
+		return se.Msg, true
+	}
+	if ue := new(UserErr); errors.As(err, &ue) {
+		return ue.Msg, true
+	}
+	return "", false
+}
+
+// AsTaskErr 尝试将错误强转为 tax 错误
+func AsTaskErr(err error) (error, bool) {
+	if se := new(SystemErr); errors.As(err, &se) {
+		return se, true
+	}
+	if ue := new(UserErr); errors.As(err, &ue) {
+		return ue, true
+	}
+	return nil, false
+}
+
+func M(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+func E[T any](t T, err error) T {
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
+
+// U 尝试解析 rod.Try 返回 err 的底层 error
+func U(err error) error {
+	if err == nil {
+		return nil
+	}
+	te := new(rod.TryError)
+	if !errors.As(err, &te) {
+		return err
+	}
+	ue := errors.Unwrap(te)
+	if ue == nil {
+		return te
+	}
+	return ue
+}

+ 35 - 0
taxerr/err_test.go

@@ -0,0 +1,35 @@
+package taxerr
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/go-rod/rod"
+)
+
+func TestU(t *testing.T) {
+	var e1 error = nil
+	r1 := U(e1)
+	if r1 != nil {
+		t.Errorf("Expected nil, but got %v", r1)
+	}
+
+	e2 := rod.Try(func() { panic("s") })
+	r2 := U(e2)
+	if r2 == nil || r2.Error() != "s" {
+		t.Errorf("Expected try err, but got %v", r2)
+	}
+
+	e := errors.New("e")
+	e3 := rod.Try(func() { panic(e) })
+	r3 := U(e3)
+	if r3 != e {
+		t.Errorf("Expected e, but got %v", r3)
+	}
+
+	e4 := errors.New("normal error")
+	r4 := U(e4)
+	if r4 != e4 {
+		t.Errorf("Expected normal error, but got %v", r4)
+	}
+}

+ 1 - 0
taxerr/invoice.go

@@ -0,0 +1 @@
+package taxerr

+ 33 - 0
taxerr/system_err_test.go

@@ -0,0 +1,33 @@
+package taxerr
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestNew(t *testing.T) {
+	e1 := New("e")
+	e2 := New("e")
+
+	if !errors.Is(e1, e2) {
+		t.Fatalf("a1 is not e2")
+	}
+}
+
+func TestNewUser(t *testing.T) {
+	e1 := NewUser("e")
+	e2 := NewUser("e")
+
+	if !errors.Is(e1, e2) {
+		t.Fatalf("e1 is not e2")
+	}
+}
+
+func TestNewUserV3(t *testing.T) {
+	e1 := NewUserV3("a", "b")
+	e2 := NewUserV3("a", "b")
+
+	if !errors.Is(e1, e2) {
+		t.Fatalf("e1 is not e2")
+	}
+}

+ 82 - 0
taxerr/systemerr.go

@@ -0,0 +1,82 @@
+package taxerr
+
+type SystemErr struct {
+	Msg string
+}
+
+func (t *SystemErr) Error() string {
+	return t.Msg
+}
+
+func (t *SystemErr) Is(err error) bool {
+	e, ok := err.(*SystemErr)
+	return ok && e.Msg == t.Msg
+}
+
+func New(msg string) *SystemErr {
+	return &SystemErr{
+		Msg: msg,
+	}
+}
+
+func NewSystemV3(errmsg, prompt string) *SystemErr {
+	return &SystemErr{
+		Msg: `<span>[异常]:` + errmsg + `</span><br />[操作]:` + prompt,
+	}
+}
+
+/*错误规范
+ *1.结尾不带符号
+ *2.表明确错误信息,做到让不懂会计的也能看懂,报表问题按税局提示即可
+ *3.第一句提示错误原因,之后提示办法 例如 网页请求超时,请稍后重试
+ *4.通用错误例如超时,找不到报表等,优先使用定义好的错误
+ *5.需要明确提示错误信息,用New() NewUser()方法自定义 例如 NewUser("手机号不一致,税局为xxx,系统为xxx")
+ */
+
+var TimeOut = New("请求超时,请稍后重试")
+
+var LaunchBrowserFail = New("打开浏览器失败,请联系运维人员处理")
+
+type WebsiteType string
+
+const (
+	Tax           WebsiteType = "税局" //税局和发票平台都用这个
+	Bank          WebsiteType = "银行" //银行采集
+	Business      WebsiteType = "工商" //工商年报
+	OtherSoftware WebsiteType = "其他软件"
+)
+
+// 税局卡顿统一提示
+func NewWebStuckTitle(cscsts bool) *SystemErr {
+	if cscsts {
+		return NewSystemV3("电子税局网页卡顿", "系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewSystemV3("电子税局网页卡顿", "请稍后重试(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+// 税局卡顿统一提示
+func NewWebStuckTitleForErr(err string, cscsts bool) *SystemErr {
+	if cscsts {
+		return NewSystemV3(err, "系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewSystemV3(err, "请稍后重试(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+var ContactDevHanding = NewWebStuckTitle(false)
+
+var InvoiceAuth = NewUserV3("当前企业办税员无电票平台登录权限!", "请切换有电票平台登录权限的办税员或法定代表人后重试。")
+
+func ErrorCompanyName(ErrName string) *SystemErr {
+	return NewSystemV3(`无法登录电子税局,公司名称与税局不同,税局为`+ErrName+`。`, `[操作]:请在“企业信息”修改后重试!`)
+}
+
+func ErrorCompanyNameDppt(ErrName, comName string) *UserErr {
+	//return NewUserV3(`无法登录全电发票平台,公司名称不一致,平台为`+ErrName+`。`, `[操作]:请在“企业信息”修改后重试!`)
+	return NewUserV3(`企业名称与税局不符,税局内名称为:`+ErrName+`,系统内为`+comName, `请在“企业信息”里修改企业名称"`)
+}
+
+var PilotTaxpayer = NewUserV3(`您不是电子发票服务平台用票试点纳税人`, `请前往增值税发票综合服务平台办理相关业务`)
+
+var TickAuthenticationDppt = NewUserV3(`税局未勾选确认进项发票!`, `请在税局勾选认证进项发票后重新发起采集!`)

+ 186 - 0
taxerr/template.go

@@ -0,0 +1,186 @@
+package taxerr
+
+import (
+	"fmt"
+
+	"git.listensoft.net/lx/taxrobot/common/variable"
+)
+
+// ================================================ 错误类 ==============================================================
+
+// NoResultError 本次申报未查询到结果
+func NoResultError(retry bool) *UserErr {
+	if retry {
+		return NewUserV3("本次提交申报后,未获取到税局页面返回的申报结果", "请您稍后发起任务核实!系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewUserV3("本次提交申报后,未获取到税局页面返回的申报结果", "请您稍后发起任务核实!(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+// TaxServiceUnavailable 税局--您当前访问的服务暂时不可用
+func TaxServiceUnavailable(retry bool) *UserErr {
+	if retry {
+		return NewUserV3("[税局提示]您当前访问的服务暂时不可用", "请稍后再试!系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewUserV3("[税局提示]您当前访问的服务暂时不可用", "请稍后再试!(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+// ================================================ 财报类 ==============================================================
+
+// AccountingStandardsError 会计准则错误 systemAcc: 系统准则 bureauAcc: 税局准则
+func AccountingStandardsError(systemAcc, bureauAcc string) *UserErr {
+	return NewUserV3(`会计准则不一致,系统准则为`+systemAcc+`,税局准则为`+bureauAcc, `请在“企业信息”修改后重试!`)
+}
+
+// AccountingStandardsNotSupport 不支持的会计准则
+func AccountingStandardsNotSupport(systemAcc variable.Kjze) *UserErr {
+	return NewUserV3(fmt.Sprintf("暂不支持【%s】的财报申报", systemAcc), `请检查会计准则是否正确或联系运维人员处理!`)
+}
+
+// FinancialStatementsError 财务报表错误
+func FinancialStatementsError(accountingStandard variable.Kjze, sheetName string) *UserErr {
+	return NewUserV3(fmt.Sprintf(`【%s】【%s】有误`, accountingStandard, sheetName), `请重新计提!`)
+}
+
+// ================================================ 异常类 ==============================================================
+
+// LoadError 未正常加载
+func LoadError(element string, retry bool) *SystemErr {
+	if retry {
+		return New(fmt.Sprintf(`【%s】因数据卡顿未正常加载, 系统将于30分钟后重试(可在"通用设置"关闭)`, element))
+	} else {
+		return New(fmt.Sprintf(`【%s】因数据卡顿未正常加载, (可在"通用设置"配置自动重试)`, element))
+	}
+}
+
+// ================================================ 发票类 ==============================================================
+
+func LoginHasBeenBlockedAndTakenOffline(Cscsts bool) *UserErr {
+	if Cscsts {
+		return NewUserV3("检测到他人登录电子税局,账号被顶下线导致任务中断", "任务进行时请勿登录电子税局!系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewUserV3("检测到他人登录电子税局,账号被顶下线导致任务中断", "任务进行时请勿登录电子税局!(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+// ================================================= 登陆类 -扫码==========================================================
+
+// PasswdLockError 账号或密码错误,输入次数过多,税局已锁定
+var PasswdLockError = NewUserV3("账号或密码错误,输入次数过多,税局已锁定!", "请在税局修改密码后重新登录或在“企业信息”修改为正确信息后次日发起任务!")
+
+// PasswdError 输入的个人账号、密码或报税手机号错误,请重新输入
+var PasswdError = NewUserV3("登录税局个人密码错误!", "请在“企业信息”修改后重试!")
+
+// TelUnMatch 税局手机号和系统报税手机号不一致
+var TelUnMatch = NewUserV3(`报税手机号填写错误!`, `请核实“企业信息”填写的报税手机号是否正确!`)
+
+var TelSmsOverrun = NewUser("短信发送次数超限")
+
+var TelSmsError = NewUser("输入的短信验证码错误")
+
+// PasswdEmpty 密码为空
+var PasswdEmpty = NewUserV3(`未填写密码`, `请在“企业信息”填写后重新发起`)
+
+// TelEmpty 手机号为空
+var TelEmpty = NewUserV3(`未填写报税手机号`, `请在“企业信息”填写后重新发起`)
+
+// TelError TelEmpty 手机号有误或格式不对
+var TelError = NewUserV3(`报税手机号有误或格式不对`, `请在“企业信息”修改后重新发起`)
+
+// TaxNoError 税号错误
+var TaxNoError = NewUserV3(`输入的纳税人识别号错误`, `请在“企业信息”修改后重试`)
+
+// CompanyNameChanged 公司名变了
+func CompanyNameChanged(Name string) *UserErr {
+	return NewUserV3(`申报过程公司名称发生变化,税局内名称为:`+Name, `请在手动核实`)
+}
+
+// DlTaxNoError 税号错误
+var DlTaxNoError = NewUserV3(`代理机构号为空`, `请在“授权管理”修改后重试`)
+
+// InputSmsError 输入的验证码错误
+var InputSmsError = NewUserV3("输入的短信验证码错误", "请稍后重试")
+
+// InputSmsExpiredError InputSmsExpired 短信验证码过期或失效
+var InputSmsExpiredError = NewUserV3("验证码已失效", "请重新授权")
+
+// SmsCodeNotReceivedError 没接收到验证码
+var SmsCodeNotReceivedError = NewUserV3(`手机验证码接收失败`, `请检查短信转发设备是否正常,可在“订单管理”购买设备。`)
+
+// SmsUnOnline 手机号没心跳不在线
+var SmsUnOnline = NewUserV3(`手机号不在线`, `请检查短信转发设备或短信助手app是否正常`)
+
+// SmsTransfinite 短信发送次数超限
+var SmsTransfinite = NewUserV3(`短信发送次数超限。`, `请次日再重新发起任务`)
+
+// ComIrrelevantRelationError 未查询到您与该企业的关联关系信息
+var ComIrrelevantRelationError = NewUserV3("办税人登录信息与企业无关联关系。", "建议使用法定代表人身份登录或在电子税局添加该办税人与该企业关联关系!")
+
+// UnRegisteredError 用户未注册,请在自然人业务入口进行用户注册
+var UnRegisteredError = NewUserV3("该用户未注册。", "在电子税局自然人业务入口进行用户注册后重试。")
+
+// TelOffline 请调用发送验证码|您的登录已掉线
+var TelOffline = NewUserV3("您的登录已掉线!", "请在“授权登录”页重新点击【授权】后再次发起任务!")
+
+var NonsupportOldLogin = NewUserV3("该地区电子税局已不再支持旧版登录。", `请在“企业信息”切换新版登录方式。`)
+
+// TelNotFundInTax 手机号码不存在,您可以尝试使用证件号码登录
+var TelNotFundInTax = NewUserV3(`登录税局手机号码不存在`, `请在“企业信息”里修改正确后重试!`)
+
+// TaxNotMatch 接口返回:输入的个人账号、密码或报税手机号错误,请重新输入
+var TaxNotMatch = NewUserV3(`输入的个人账号、密码或报税手机号错误,请重新输入!`, `请在“企业信息”里修改正确后重试!`)
+
+// FaceVerifyError 确认成为该企业办税人失败,请在网站自行扫脸验证确认后使用
+var FaceVerifyError = NewUserV3(`确认成为该企业办税人失败`, `请在网站自行扫脸验证确认后使用`)
+
+var CompanyNotFund = NewUserV3("未在当前代理手机号下找到该公司", "请确认是否绑定办税员")
+
+// PasswdAlwaysError 密码输入连续错误x次
+func PasswdAlwaysError(msg string) error {
+	return NewUserV3(msg, "请在“企业信息”修改后重试!")
+}
+
+func CompanyNameError(name string) error {
+	return NewUserV3(`企业名称与税局不符,税局内名称为:`+name, `请在“企业信息”里修改企业名称`)
+}
+
+// TaxAbnormalError 登陆服务异常
+func TaxAbnormalError(Cscsts bool) error {
+	if Cscsts {
+		return NewSystemV3("税局登录服务异常。", "系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return NewSystemV3("税局登录服务异常。", "请稍后重试(可在\"通用设置\"配置自动重试)")
+	}
+}
+
+var KeepTimeOut = New("网络超时,请稍后再试")
+
+var KeepTimeOutArea = map[string]bool{
+	"chongqing": true,
+}
+
+// ================================================ 社保类 ==============================================================
+
+// SheBaoNotFoundErr 全部位置都查不到社保信息(已申报和待申报)
+var SheBaoNotFoundErr = NewUserV3("待申报和已申报页面均未查询到社保信息", "请确认参保信息后重试。")
+
+// SheBaoUnreported 存在往期未申报
+var SheBaoUnreported = NewUserV3("您当前存在历史属期的费款未申报", "可在设置中选择申报往期社保或手动处理")
+
+// SheBaoPaymentInfoNotFound 未查询到待缴款信息
+var SheBaoPaymentInfoNotFound = NewUserV3("未查询到待缴款信息", "请确认申报状态。")
+
+// SheBaoUnbindAgreement 未绑定三方协议
+var SheBaoUnbindAgreement = NewUserV3("该企业未绑定三方协议", "请前往电子税局绑定三方协议后重试。")
+
+// ================================================ 银行类 ==============================================================
+
+// BankWebStuckErr 银行页面卡顿
+func BankWebStuckErr(retryFlag bool) *SystemErr {
+	if retryFlag {
+		return New("银行页面卡顿,系统将于30分钟后重试(可在\"通用设置\"关闭)")
+	} else {
+		return New("银行页面卡顿,系统将于30分钟后重试(可在\"通用设置\"配置自动重试)")
+	}
+}

+ 126 - 0
taxerr/usererr.go

@@ -0,0 +1,126 @@
+package taxerr
+
+import (
+	"fmt"
+	"strings"
+)
+
+type UserErr struct {
+	Msg string
+}
+
+func (u *UserErr) Error() string {
+	return u.Msg
+}
+
+func (u *UserErr) Is(err error) bool {
+	e, ok := err.(*UserErr)
+	return ok && e.Msg == u.Msg
+}
+
+// 用户错误 比如报表错误提示
+func NewUser(msg string) *UserErr {
+	return &UserErr{
+		Msg: msg,
+	}
+}
+
+// v3新版报错提示
+// @errmsg 错误原因
+// @prompt 操作提示
+func NewUserV3(errmsg, prompt string) *UserErr {
+	return &UserErr{
+		Msg: `<span>[错误]:` + errmsg + `</span><br />[操作]:` + prompt,
+	}
+}
+
+/*错误规范
+ *1.结尾不带符号
+ *2.表明确错误信息,做到让不懂会计的也能看懂,报表问题按税局提示即可
+ *3.第一句提示错误原因,之后提示办法 例如 网页请求超时,请稍后重试
+ *4.通用错误例如超时,找不到报表等,优先使用定义好的错误
+ *5.需要明确提示错误信息,用New() NewUser()方法自定义 例如 NewUser(`手机号不一致,税局为xxx,系统为xxx`)
+ */
+//手机号
+var SmsCodeNotReceived = NewUser(`<span>[错误]:手机验证码接收失败</span><br />[操作]:请检查短信APP或短信转发设备是否正常`)
+var SmsCodeReceivedIncorrect = NewUser(`<span>[错误]:本次接收到的手机验证码不正确</span><br />[操作]:请重试`)
+
+// 企业相关
+var Sz = NewUser(`<span>[错误]:电子税务局未查询到税种信息</span><br />[操作]:请确认企业是否核定税种`)
+var KjzeXz = NewUser(`<span>[错误]:会计准则选择有误</span><br />[操作]:请修改正确后重试`)
+
+var YbnsrZg = NewUser(`<span>[错误]:纳税人资格选择错误,税局为(一般纳税人)</span><br />[操作]:请修改后重试`)
+var XgmnsrZg = NewUser(`<span>[错误]:纳税人资格选择错误,税局为(小规模纳税人)</span><br />[操作]:请修改后重试`)
+
+// 采集相关
+var StartPeriod = NewUser(`<span>[错误]:初始账期有误</span><br />[操作]:请删除后重新添加此企业`)
+var EtaxPassword = NewUser(`<span>[错误]:电子税局企业密码错误</span><br />[操作]:请改正后重试`)
+var EtaxPersonalPassword = NewUser(`<span>[错误]:电子税局个人密码错误</span><br />[操作]:请改正后重试`)
+
+var ZzsNotFound = NewUser(`<span>[错误]:电子税务局未查询到增值税报表`)
+var QysdsNotFound = NewUser(`<span>[错误]:电子税务局未查询到企业所得税报表`)
+var QysdsYearNotFound = NewUser(`<span>[错误]:电子税务局未查询到上年企业所得税报表年报`)
+var CwbbNotFound = NewUser(`<span>[错误]:电子税务局未查询到财务报表`)
+var CwbbErr = NewUser(`<span>[错误]:财务报表 会计准则有误,请核实后重新发起`)
+
+// 账期
+func NotFound(period, tableName string) error {
+	p := period
+	if len(p) > 6 {
+		p = strings.ReplaceAll(period, "-", "")[:6]
+	}
+	return NewUser(fmt.Sprintf(`<span>[错误]:电子税务局未查询到%s账期%s`, p, tableName))
+}
+
+func NotFoundBeginAndEnd(begin, end, tableName string) error {
+	return NewUser(fmt.Sprintf(`<span>[错误]:电子税务局未查询到%s-%s账期%s`, strings.ReplaceAll(begin, "-", "")[:6], strings.ReplaceAll(end, "-", "")[:6], tableName))
+}
+
+// 申报相关
+var CwbbEmpty = NewUser(`<span>[错误]:财务报表数据为空</span><br />[操作]:请重新取数后重试`)
+var KjZcfzEmpty = NewUser(`<span>[错误]:资产负债表数据为空</span><br />[操作]:请重新取数后重试`)
+var KjLrbEmpty = NewUser(`<span>[错误]:利润表数据为空</span><br />[操作]:请重新取数后重试`)
+var KjXjllbEmpty = NewUser(`<span>[错误]:现金流量表数据为空</span><br />[操作]:请重新取数后重试`)
+var NoBsqx = NewUser(`<span>[错误]:当前登录人员未找到有办税权限的身份</span><br />[操作]:请核实`)
+
+var DqdeZbEmpty = NewUser(`<span>[错误]:定期定额数据为空</span><br />[操作]:请重新取数后重试`)
+var SmallVatZbEmpty = NewUser(`<span>[错误]:主表数据为空</span><br />[操作]:请重新取数后重试`)
+var SmallVatFb1Empty = NewUser(`<span>[错误]:附表1数据为空</span><br />[操作]:请重新取数后重试`)
+var SmallVatFb2Empty = NewUser(`<span>[错误]:附表2数据为空</span><br />[操作]:请重新取数后重试`)
+var SmallVatFb5Empty = NewUser(`<span>[错误]:附表5数据为空</span><br />[操作]:请重新取数后重试`)
+var SmallVatJmmxEmpty = NewUser(`<span>[错误]:减免明细表数据为空</span><br />[操作]:请重新取数后重试`)
+var GsSmallVatFb4 = NewUser(`<span>[错误]:小规模增值税开票信息/销售额 服务为空</span><br />[操作]:请重新取数后重试`)
+var GsSmallVatFb5 = NewUser(`<span>[错误]:小规模增值税开票信息/销售额 货物为空</span><br />[操作]:请重新取数后重试`)
+var GsSmallVatFb6 = NewUser(`<span>[错误]:小规模增值税税额 服务为空</span><br />[操作]:请重新取数后重试`)
+var GsSmallVatFb7 = NewUser(`<span>[错误]:小规模增值税税额 货物为空</span><br />[操作]:请重新取数后重试`)
+
+var VatZbEmpty = NewUser(`<span>[错误]:主表数据为空</span><br />[操作]:请重新取数后重试`)
+var VatFb1Empty = NewUser(`<span>[错误]:附表1数据为空</span><br />[操作]:请重新取数后重试`)
+var VatFb2Empty = NewUser(`<span>[错误]:附表2数据为空</span><br />[操作]:请重新取数后重试`)
+var VatFb3Empty = NewUser(`<span>[错误]:附表3数据为空</span><br />[操作]:请重新取数后重试`) //附表3 不是必须申报
+var VatFb4Empty = NewUser(`<span>[错误]:附表4数据为空</span><br />[操作]:请重新取数后重试`)
+var VatFb5Empty = NewUser(`<span>[错误]:附表5数据为空</span><br />[操作]:请重新取数后重试`)
+var VatJmmxEmpty = NewUser(`<span>[错误]:减免明细表数据为空</span><br />[操作]:请重新取数后重试`)
+var VatNcpEmpty = NewUser(`<span>[错误]:《购进农产品直接销售核定农产品增值税进项税额计算表》数据为空</span><br />[操作]:请重新取数后重试`)
+
+// 减免性质代码 自己提示
+var QysdsEmpty = NewUser(`<span>[错误]:企业所得税数据为空</span><br />[操作]:请重新取数后重试`)
+var Gsa201020Empty = NewUser(`<span>[错误]:Gsa201020表数据为空</span><br />[操作]:请重新取数后重试`)
+var QysdsFb1Empty = NewUser(`<span>[错误]:企业所得税附表1数据为空</span><br />[操作]:请重新取数后重试`)
+var QysdsFb2Empty = NewUser(`<span>[错误]:企业所得税附表2数据为空</span><br />[操作]:请重新取数后重试`)
+var QysdsFb3Empty = NewUser(`<span>[错误]:企业所得税附表3数据为空</span><br />[操作]:请重新取数后重试`)
+
+var QtsrEmpty = NewUser(`<span>[错误]:其他收入数据为空</span><br />[操作]:请重新取数后重试`)
+var SljsEmpty = NewUser(`<span>[错误]:水利建设数据为空</span><br />[操作]:请重新取数后重试`)
+var WhsyjsfEmpty = NewUser(`<span>[错误]:文化事业建设费数据为空</span><br />[操作]:请重新取数后重试`)
+var XwsEmpty = NewUser(`<span>[错误]:财产行为税数据为空</span><br />[操作]:请重新取数后重试`)
+var CbjEmpty = NewUser(`<span>[错误]:残疾人保障金数据为空</span><br />[操作]:请重新取数后重试`)
+var LjclEmpty = NewUser(`<span>[错误]:垃圾处理费数据为空</span><br />[操作]:请重新取数后重试`)
+
+// 登录相关
+var LoginTelEmpty = NewUser(`<span>[错误]:报税手机号为空</span><br />[操作]:请填写后重试`)
+var LoginZrridnoEmpty = NewUser(`<span>[错误]:办税人/自然人身份证号为空</span><br />[操作]:请填写后重试`)
+var LoginZrrmmEmpty = NewUser(`<span>[错误]:办税人/自然人密码为空</span><br />[操作]:请填写后重试`)
+var LoginXzsfEmpty = NewUser(`<span>[错误]:办税人的身份为空</span><br />[操作]:请填写后重试`)
+var LoginPasswdError = NewUserV3("账号或密码错误!", "请在“企业信息”修改为正确信息后重试!")
+var LoginRelationError = NewUserV3("办税人登录信息与企业无关联关系", "建议使用法定代表人身份登录或在电子税局添加该办税人与该企业关联关系")