From 2cf260eb05114cd0670f64a3fa41580b8a29e97d Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Wed, 23 Sep 2026 20:24:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=EF=BC=9A=E9=9F=B3?= =?UTF-8?q?=E4=B9=90=E6=A8=A1=E5=9D=97=E4=B8=8E=E8=85=BE=E8=AE=AF=E4=BA=91?= =?UTF-8?q?=20COS=20=E6=95=B4=E4=BD=93=E4=B8=8B=E7=BA=BF=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=88=86=E5=8F=91=E6=A1=B6=EF=BC=88App=20=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=8C=85=E4=B8=8E=E5=BD=95=E9=9F=B3/=E5=9B=BA?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E5=BC=80=E6=94=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 音乐:sys/music、sys/musicobj、modules/music、mcp/tool_music.go 全部删除,home 的 模块列表与 musicobj 初始化、两份 yaml 模板里的 musicobj 段一并去掉。起因是它的 ApiBaseUrl 指向上一代品牌域名;客户端的音乐模块本来就早已删除。 ⚠️ 咪咕音乐(tool_migu_music + migu 服务)没有动,别一起当成音乐链路删了。 腾讯云 COS:sys/tencentyun 整个删除,home/api 的 cos.OnInit 与配置段、 comm.svcRuntimeSections 的 cos 托管一并去掉。头像上传改走阿里云 OSS(与 user_getuploadurl、console 后台直传同一个桶)。modules/api/api_getcostoken.go 是 孤儿(后台调的其实是 console 那个同名路由,客户端从不调),删除。后台两处的 腾讯云 COS 卡片/模板与巡检探针也删——服务端读存储配置的唯一入口只认 provider=aliyun_oss,留着是「配得上却不生效」的陷阱。存量 tencent_cos 行不动。 ⚠️ 老客户端仍读业务库 config 表的 COS_* 直传腾讯云,那 5 个键别删;历史录音的 audiourl 是 COS 绝对地址,不会自动搬家。 分发桶:comm.AliyunOSSConf 加 DistBucket/DistDomain(留空用 ymai-dl / dl.ymaikj.com),凭据与主桶共用。后台「上传 APK」走 api_getcostoken 带 target:"dist":key 不加 EAIMAR 根前缀(分发桶直接挂在 dl.ymaikj.com 上)、目录走 白名单、按应用分子目录(一个桶放多个应用,不分目录会静默互相覆盖)。 ⚠️ Content-Type 由服务端按扩展名强制并回带给前端:浏览器对 .apk 给出的 File.type 往往是空串,前端照它发 PUT 会与预签名不符、OSS 回 403,而报错里完全看不出是类型 对不上。⚠️ 分发桶必须单独配 CORS(来源 console.ymaikj.com、方法 PUT), 浏览器直传打的是桶的 OSS endpoint 而不是自定义域名。 另含下载落地页 deploy/dl(含 OSS 部署教程)与各 deploy 脚本/README 的同步更新。 Co-Authored-By: Claude Opus 5 (1M context) --- apps/services/comm/ossconf.go | 46 +- apps/services/comm/svcruntime.go | 8 +- apps/services/modules/agents/api_test.go | 2 +- apps/services/modules/api/api_getcostoken.go | 31 - apps/services/modules/api/model_test.go | 2 +- .../modules/console/api_svctemplate.go | 11 +- .../modules/console/api_upload_test.go | 54 ++ .../services/modules/console/inspect_probe.go | 24 - .../modules/console/model_environment.go | 2 +- apps/services/modules/mcp/module.go | 6 - apps/services/modules/mcp/tool_music.go | 115 ---- apps/services/modules/music/api.go | 27 - apps/services/modules/music/api_search.go | 55 -- apps/services/modules/music/api_url.go | 38 -- apps/services/modules/music/core.go | 26 - apps/services/modules/music/model.go | 20 - apps/services/modules/music/model_test.go | 1 - apps/services/modules/music/module.go | 49 -- apps/services/modules/music/options.go | 27 - apps/services/modules/user/api_setting.go | 47 +- apps/services/modules/user/options.go | 1 - apps/services/services/api/main.go | 8 +- apps/services/services/home/main.go | 14 - .../services/sys/aliyun/filetrans/sys_test.go | 4 +- .../sys/bytedance/audomodel/sys_test.go | 2 +- apps/services/sys/music/core.go | 623 ------------------ apps/services/sys/music/music.go | 187 ------ apps/services/sys/music/options.go | 40 -- apps/services/sys/music/sys_test.go | 83 --- apps/services/sys/musicobj/core.go | 66 -- apps/services/sys/musicobj/music.go | 117 ---- apps/services/sys/musicobj/options.go | 40 -- apps/services/sys/musicobj/sys_test.go | 29 - apps/services/sys/pay/alipay/sys_test.go | 2 +- apps/services/sys/pay/appleiap/sys_test.go | 2 +- apps/services/sys/pay/appleiap/token_test.go | 2 +- apps/services/sys/pay/googleiap/sys_test.go | 2 +- apps/services/sys/pay/wechat/sys_test.go | 2 +- apps/services/sys/tencentyun/cos/core.go | 55 -- apps/services/sys/tencentyun/cos/cos.go | 100 --- apps/services/sys/tencentyun/cos/options.go | 80 --- apps/services/sys/tencentyun/cos/sys_test.go | 193 ------ build.sh | 2 +- deploy/README.md | 10 +- deploy/admin/README.md | 5 +- deploy/admin/dev-deploy.sh | 8 +- deploy/admin/docker-compose.yml | 4 +- deploy/admin/prod-build.sh | 4 +- deploy/admin/prod-deploy.sh | 6 +- deploy/app/README.md | 3 +- deploy/app/confs/api.yaml.example | 9 +- deploy/app/confs/home.yaml.example | 18 +- deploy/app/confs/mcp.yaml.example | 2 - deploy/app/confs/timer.yaml.example | 2 +- deploy/app/dev-deploy.sh | 6 +- deploy/app/docker-compose.yml | 2 +- deploy/app/prod-build.sh | 2 +- deploy/app/prod-deploy.sh | 10 +- deploy/console/README.md | 3 +- deploy/console/dev-deploy.sh | 6 +- deploy/console/docker-compose.yml | 4 +- deploy/console/prod-build.sh | 2 +- deploy/console/prod-deploy.sh | 6 +- deploy/dev-deploy-all.sh | 4 +- deploy/dl/README.md | 209 ++++++ deploy/dl/build.sh | 31 + deploy/dl/index.html | 380 +++++++++++ deploy/dl/index.template.html | 380 +++++++++++ deploy/registry-profiles.sh | 37 +- 69 files changed, 1229 insertions(+), 2169 deletions(-) delete mode 100644 apps/services/modules/api/api_getcostoken.go create mode 100644 apps/services/modules/console/api_upload_test.go delete mode 100644 apps/services/modules/mcp/tool_music.go delete mode 100644 apps/services/modules/music/api.go delete mode 100644 apps/services/modules/music/api_search.go delete mode 100644 apps/services/modules/music/api_url.go delete mode 100644 apps/services/modules/music/core.go delete mode 100644 apps/services/modules/music/model.go delete mode 100644 apps/services/modules/music/model_test.go delete mode 100644 apps/services/modules/music/module.go delete mode 100644 apps/services/modules/music/options.go delete mode 100644 apps/services/sys/music/core.go delete mode 100644 apps/services/sys/music/music.go delete mode 100644 apps/services/sys/music/options.go delete mode 100644 apps/services/sys/music/sys_test.go delete mode 100644 apps/services/sys/musicobj/core.go delete mode 100644 apps/services/sys/musicobj/music.go delete mode 100644 apps/services/sys/musicobj/options.go delete mode 100644 apps/services/sys/musicobj/sys_test.go delete mode 100644 apps/services/sys/tencentyun/cos/core.go delete mode 100644 apps/services/sys/tencentyun/cos/cos.go delete mode 100644 apps/services/sys/tencentyun/cos/options.go delete mode 100644 apps/services/sys/tencentyun/cos/sys_test.go create mode 100644 deploy/dl/README.md create mode 100755 deploy/dl/build.sh create mode 100644 deploy/dl/index.html create mode 100644 deploy/dl/index.template.html diff --git a/apps/services/comm/ossconf.go b/apps/services/comm/ossconf.go index ec16d441..f3267824 100644 --- a/apps/services/comm/ossconf.go +++ b/apps/services/comm/ossconf.go @@ -15,6 +15,44 @@ type AliyunOSSConf struct { AccessSecret string Bucket string Domain string // 可选:自定义 CDN 域名,配了则访问 URL 走该域名 + + // 「分发桶」:放对外公开下载的东西(App 安装包),与放用户录音/固件的主桶分开。 + // 分开的理由不是洁癖:安装包域名容易被微信/浏览器判风险,混在主桶会连累 + // 录音和后台直传;而且几十 MB × 几千次下载的流量账单要能单独看。 + // 同一个 OSS 账号,所以 Endpoint/AK/SK 复用主桶那份,只有桶名和域名不同。 + DistBucket string // 留空用 DefaultDistBucket + DistDomain string // 留空用 DefaultDistDomain +} + +// 分发桶的默认值。运营在【第三方服务配置 → 存储】填了 dist_bucket / dist_domain 就以配置为准。 +// +// ⚠️ DefaultDistDomain 必须与 deploy/dl 那个下载落地页所在的桶一致—— +// 页面和安装包同源才没有跨域与防盗链问题。 +const ( + DefaultDistBucket = "ymai-dl" + DefaultDistDomain = "https://dl.ymaikj.com" +) + +// DistUploadDirs 分发桶里允许上传到的顶层目录白名单。 +// +// ⚠️ 不做白名单的话,前端传什么 path 就在桶里造什么目录——这个桶是公共读、 +// 直接挂在 dl.ymaikj.com 上的,等于把它变成任人摆放的公开网盘。 +var DistUploadDirs = map[string]bool{ + "android": true, // App 安装包(apk) +} + +// ForDist 返回一份指向「分发桶」的配置副本。凭据与主桶共用。 +func (oc *AliyunOSSConf) ForDist() *AliyunOSSConf { + d := *oc + d.Bucket = oc.DistBucket + if d.Bucket == "" { + d.Bucket = DefaultDistBucket + } + d.Domain = oc.DistDomain + if d.Domain == "" { + d.Domain = DefaultDistDomain + } + return &d } // AliyunOSSProvider svc_config / sys_service_config 中阿里云对象存储的 provider 值。 @@ -94,6 +132,8 @@ func LoadAliyunOSSConf(appName, encKey string) (*AliyunOSSConf, error) { AccessSecret: get("access_key_secret"), Bucket: get("bucket"), Domain: get("domain"), + DistBucket: get("dist_bucket"), + DistDomain: get("dist_domain"), } if err := oc.normalize(); err != nil { return nil, err @@ -121,6 +161,8 @@ func loadOSSFromSysConfig(encKey string) *AliyunOSSConf { AccessSecret: strings.TrimSpace(cfg.SysFieldPlain("access_key_secret", encKey)), Bucket: strings.TrimSpace(cfg.SysFieldPlain("bucket", encKey)), Domain: strings.TrimSpace(cfg.SysFieldPlain("domain", encKey)), + DistBucket: strings.TrimSpace(cfg.SysFieldPlain("dist_bucket", encKey)), + DistDomain: strings.TrimSpace(cfg.SysFieldPlain("dist_domain", encKey)), } if oc.normalize() != nil { return nil @@ -150,8 +192,8 @@ func (oc *AliyunOSSConf) PublicURL(signedURL, key string) string { if d := strings.TrimSpace(oc.Domain); d != "" { d = strings.TrimPrefix(strings.TrimPrefix(d, "https://"), "http://") d = strings.Trim(d, "/") - // 只取第一段做 host:运营常把域名连着路径一起填(cdn.ymaikj.com/), - // 那样直接当 Host 用会拼出 https://cdn.ymaikj.com//EAIMAR/... 这种取不到的地址。 + // 只取第一段做 host:运营常把域名连着路径一起填(oss.ymaikj.com/), + // 那样直接当 Host 用会拼出 https://oss.ymaikj.com//EAIMAR/... 这种取不到的地址。 if i := strings.Index(d, "/"); i >= 0 { d = d[:i] } diff --git a/apps/services/comm/svcruntime.go b/apps/services/comm/svcruntime.go index 716e409e..4d932506 100644 --- a/apps/services/comm/svcruntime.go +++ b/apps/services/comm/svcruntime.go @@ -15,16 +15,16 @@ import ( // // 目标:业务服务启动时—— // 1. 先用本地 yaml/env 连上 console 共享库(postgres); -// 2. 若库中没有本(应用,区域,服务)的配置,则把当前文件里的基础设施配置(DSN/Redis/NATS/COS…) seed 进库(source=file), +// 2. 若库中没有本(应用,区域,服务)的配置,则把当前文件里的基础设施配置(DSN/Redis/NATS…) seed 进库(source=file), // 这样 console「应用的服务配置」页立刻能看到默认值; -// 3. 若库中已有,则以库为准,覆盖内存里的 Sys 配置(后台改过的值生效),再继续初始化 mysql/redis/cos 等。 +// 3. 若库中已有,则以库为准,覆盖内存里的 Sys 配置(后台改过的值生效),再继续初始化 mysql/redis 等。 // // 注意:postgres 自身是 bootstrap(要先连它才能读配置),故它不纳入托管;其覆盖也不会在本次启动生效。 // 密钥字段(DSN/密码/secret/token)以 AES-256-GCM 密文落库(comm.Encrypt,密钥 ${FIELD_ENCRYPT_KEY}), // console 与业务服务必须配同一把 key。 // svcRuntimeSections 纳入托管的 Sys 子系统(基础设施/运行配置)。postgres/log/lgid 等 bootstrap 不含。 -var svcRuntimeSections = []string{"mysql", "redis", "nats", "cos", "email", "translate"} +var svcRuntimeSections = []string{"mysql", "redis", "nats", "email", "translate"} // AppServiceConfig 一行 = 某应用×区域×服务 的一个配置项(扁平化点号无关,直接存 section+key)。 // 存 console 共享库(postgres.GetSys()),与 global_config 同库;console 前端读写、业务服务启动 seed/读取。 @@ -33,7 +33,7 @@ type AppServiceConfig struct { AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_svcconf" json:"app_name"` // 应用名(app_registry.app_name) Region string `gorm:"column:region;size:32;uniqueIndex:uidx_svcconf" json:"region"` // 区域(app_registry.region),可空 Service string `gorm:"column:service;size:32;uniqueIndex:uidx_svcconf" json:"service"` // 服务类型 api/gateway/home… - Section string `gorm:"column:section;size:32;uniqueIndex:uidx_svcconf" json:"section"` // 配置分区 mysql/redis/nats/cos… + Section string `gorm:"column:section;size:32;uniqueIndex:uidx_svcconf" json:"section"` // 配置分区 mysql/redis/nats… Key string `gorm:"column:key;size:64;uniqueIndex:uidx_svcconf" json:"key"` // 配置键 Dsn/Addr/Password… Value string `gorm:"column:value;type:text" json:"value"` // 值(密钥字段存 AES-GCM 密文;非标量存 JSON) Encrypted bool `gorm:"column:encrypted" json:"encrypted"` // 是否密钥字段(前端脱敏、落库加密) diff --git a/apps/services/modules/agents/api_test.go b/apps/services/modules/agents/api_test.go index 4ecee4aa..52167b99 100644 --- a/apps/services/modules/agents/api_test.go +++ b/apps/services/modules/agents/api_test.go @@ -9,7 +9,7 @@ import ( ) const ( - baseURL = "https://voitrans.ideapsound.com" + baseURL = "https://ym-dev.ymaikj.com" appID = "app_346bbeb6dc3bd37c" appSecret = "sk_RJHRhzpost64qus5SCBlzECbPdMaiG_fcv7eJ-3u" ) diff --git a/apps/services/modules/api/api_getcostoken.go b/apps/services/modules/api/api_getcostoken.go deleted file mode 100644 index c72b19c5..00000000 --- a/apps/services/modules/api/api_getcostoken.go +++ /dev/null @@ -1,31 +0,0 @@ -package api - -import ( - "yunyan/comm" - "yunyan/pb" - "yunyan/sys/tencentyun/cos" -) - -// 获取COS临时上传凭证(前端直传用) -func (this *apiComp) GetCosToken(session comm.IUserSession, req *pb.ApiGetCosTokenReq) (resp *pb.ApiGetCosTokenResp, errdata *pb.ErrorData) { - result, err := cos.Sts() - if err != nil { - errdata = &pb.ErrorData{ - Code: pb.ErrorCode_SystemError, - Message: "获取上传凭证失败: " + err.Error(), - } - return - } - config := cos.GetConfig() - resp = &pb.ApiGetCosTokenResp{ - Bucket: config.BucketName, - Region: config.Region, - BucketUrl: config.BucketURL, - TmpSecretId: result.Credentials.TmpSecretID, - TmpSecretKey: result.Credentials.TmpSecretKey, - SessionToken: result.Credentials.SessionToken, - Expiration: result.Expiration, - Domain: config.DomainName, - } - return -} diff --git a/apps/services/modules/api/model_test.go b/apps/services/modules/api/model_test.go index 4c696967..c3746b0c 100644 --- a/apps/services/modules/api/model_test.go +++ b/apps/services/modules/api/model_test.go @@ -183,7 +183,7 @@ func Test_TavilyApi(t *testing.T) { //  测试api返回效率 func Test_API_AI(t *testing.T) { // 测试参数 - apiURL := "http://ideapsound.com/ai/funcchat" // 替换为你的实际地址 + apiURL := "http://127.0.0.1:8080/ai/funcchat" // 替换为你的实际地址 testMessage := &AIChatReq{ Msg: "今天的天气如何", Contexts: []string{ diff --git a/apps/services/modules/console/api_svctemplate.go b/apps/services/modules/console/api_svctemplate.go index 7fd2a78d..dfdc3c18 100644 --- a/apps/services/modules/console/api_svctemplate.go +++ b/apps/services/modules/console/api_svctemplate.go @@ -352,9 +352,14 @@ func builtinSvcTemplates() []ThirdSvcTemplate { // 必须仍是 OSS 源站,签名是按源站 host 算的,换域名会被判签名不符。 tpl("storage_aliyun_oss", "aliyun_oss", "阿里云 OSS", svcCatStorage, 0, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true), sf("bucket", "存储桶", false), sf("endpoint", "地域节点 Endpoint", false), - sf("domain", "自定义域名(可选,如 cdn.ymaikj.com;留空用 OSS 源站地址)", false)), - tpl("storage_tencent_cos", "tencent_cos", "腾讯云 COS", svcCatStorage, 1, - sf("secret_id", "SecretId", true), sf("secret_key", "SecretKey", true), sf("bucket", "存储桶", false), sf("region", "地域,如 ap-guangzhou", false)), + sf("domain", "自定义域名(可选,如 oss.ymaikj.com;留空用 OSS 源站地址)", false), + // 分发桶:放对外公开下载的 App 安装包,与放录音/固件的主桶分开。凭据复用上面那套。 + // 留空则用 comm.DefaultDistBucket / DefaultDistDomain(ymai-dl / dl.ymaikj.com)。 + sf("dist_bucket", "分发桶(可选,放App安装包;留空用 ymai-dl)", false), + sf("dist_domain", "分发桶域名(可选;留空用 https://dl.ymaikj.com)", false)), + // 2026-09-23:腾讯云 COS 模板已删除。服务端读存储配置的唯一入口是 comm.LoadAliyunOSSConf, + // 它只认 provider=aliyun_oss —— 留着 COS 这一项只会让运营配完发现上传照样走 OSS, + // 是个配得上、却不生效的陷阱。存量 tencent_cos 行不会被自动删除,但也不再有任何读取方。 // MCP 服务(模型上下文协议)。字段均非加密:url 服务地址,type 传输类型(0=HTTP/1=SSE),tools 启用工具白名单。 // 各区域的实际 url/tools 通过「区域覆盖分叉」维护,运行时按客户端区域解析。 diff --git a/apps/services/modules/console/api_upload_test.go b/apps/services/modules/console/api_upload_test.go new file mode 100644 index 00000000..cebc5340 --- /dev/null +++ b/apps/services/modules/console/api_upload_test.go @@ -0,0 +1,54 @@ +package console + +import "testing" + +// 分发桶是公共读、直接挂在 dl.ymaikj.com 上的,key 拼错就等于把它变成公开网盘, +// 所以每条边界都钉在测试里。 +func TestBuildDistObjectKey(t *testing.T) { + ok := map[string][3]string{ + "正常": {"EAIMAR", "android", "eaimar-release-1.0.2-20260923.apk"}, + "目录带斜杠": {"EAIMAR", "/android/", "a.apk"}, + "文件名夹带目录": {"EAIMAR", "android", "../../etc/passwd"}, + "另一个应用": {"deepGlass", "android", "a.apk"}, + "应用名带斜杠": {"a/../b", "android", "a.apk"}, + } + want := map[string]string{ + "正常": "android/EAIMAR/eaimar-release-1.0.2-20260923.apk", + "目录带斜杠": "android/EAIMAR/a.apk", + "文件名夹带目录": "android/EAIMAR/passwd", + "另一个应用": "android/deepGlass/a.apk", + "应用名带斜杠": "android/a____b/a.apk", + } + for name, in := range ok { + got, err := buildDistObjectKey(in[0], in[1], in[2]) + if err != nil { + t.Errorf("%s: 本该通过,却报错 %v", name, err) + continue + } + if got != want[name] { + t.Errorf("%s: 期望 %q,得到 %q", name, want[name], got) + } + } + + bad := map[string][3]string{ + "没选应用": {"", "android", "a.apk"}, + "应用名全是非法字符": {"//", "android", "a.apk"}, + "目录为空": {"EAIMAR", "", "a.apk"}, + "目录不在白名单": {"EAIMAR", "anything", "a.apk"}, + "用 .. 跳出去": {"EAIMAR", "android/../secret", "a.apk"}, + "绝对路径跳根": {"EAIMAR", "/../../", "a.apk"}, + "文件名为空": {"EAIMAR", "android", " "}, + } + for name, in := range bad { + if got, err := buildDistObjectKey(in[0], in[1], in[2]); err == nil { + t.Errorf("%s: 本该被拒,却得到 %q", name, got) + } + } +} + +// apk 的 Content-Type 必须由服务端强制:它计入预签名,签错了要等真机装包才发现。 +func TestDistContentTypeForced(t *testing.T) { + if distContentTypes[".apk"] != "application/vnd.android.package-archive" { + t.Fatalf("apk 的 Content-Type 不对: %q", distContentTypes[".apk"]) + } +} diff --git a/apps/services/modules/console/inspect_probe.go b/apps/services/modules/console/inspect_probe.go index 01ba487f..ac16781d 100644 --- a/apps/services/modules/console/inspect_probe.go +++ b/apps/services/modules/console/inspect_probe.go @@ -22,7 +22,6 @@ import ( "crypto/sha1" "encoding/base64" "encoding/json" - "fmt" "io" "net/http" "net/url" @@ -32,7 +31,6 @@ import ( "time" ossSdk "github.com/aliyun/aliyun-oss-go-sdk/oss" - cosSdk "github.com/tencentyun/cos-go-sdk-v5" "golang.org/x/oauth2/google" "yunyan/comm" @@ -82,8 +80,6 @@ func probeService(ctx context.Context, provider string, cats map[int32]bool, fie return probeGoogle(ctx, fields) case "aliyun_oss": return probeAliyunOSS(ctx, fields) - case "tencent_cos": - return probeTencentCOS(ctx, fields) case "alibaba": return probeAlibaba(ctx, fields) case "doubao": @@ -431,26 +427,6 @@ func probeAliyunOSS(ctx context.Context, fields map[string]string) probeResult { return credOK("阿里云 OSS 凭据有效,存储桶 " + bucket + " 可访问") } -// probeTencentCOS HEAD Bucket:只读、免费,验证 SecretId/Key + 桶存在 + 有权限。 -func probeTencentCOS(ctx context.Context, fields map[string]string) probeResult { - id, key := pick(fields, "secret_id"), pick(fields, "secret_key") - bucket, region := pick(fields, "bucket"), pick(fields, "region") - if id == "" || key == "" || bucket == "" || region == "" { - return credFail("腾讯云 COS 缺少 SecretId/SecretKey/bucket/region,无法验证") - } - u, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region)) - if err != nil { - return credFail("腾讯云 COS 地址拼装失败: " + err.Error()) - } - client := cosSdk.NewClient(&cosSdk.BaseURL{BucketURL: u}, &http.Client{ - Transport: &cosSdk.AuthorizationTransport{SecretID: id, SecretKey: key}, - }) - if _, err = client.Bucket.Head(ctx); err != nil { - return credFail("腾讯云 COS 访问存储桶失败: " + err.Error()) - } - return credOK("腾讯云 COS 凭据有效,存储桶 " + bucket + " 可访问") -} - // ============================== 火山引擎 / 字节跳动 ============================== // probeDoubao 豆包有两套凭据:方舟大模型走 api_key(OpenAI 兼容),翻译走火山 AK/SK 签名。 diff --git a/apps/services/modules/console/model_environment.go b/apps/services/modules/console/model_environment.go index 73c9a4ea..0da63bc3 100644 --- a/apps/services/modules/console/model_environment.go +++ b/apps/services/modules/console/model_environment.go @@ -28,7 +28,7 @@ type AppEnvironment struct { SshPrivateKey string `gorm:"column:ssh_private_key;type:text" json:"ssh_private_key"` // DEPLOY_KEY 私钥内容(PEM)【加密】 // ── 私有镜像仓库 + Docker 网络(节点级共享;镜像名/tag 是应用维度,放部署行)── - Registry string `gorm:"column:registry;size:255" json:"registry"` // 如 docker-registry.ideapsound.com + Registry string `gorm:"column:registry;size:255" json:"registry"` // 如 registry.ymaikj.com RegistryUser string `gorm:"column:registry_user;size:64" json:"registry_user"` // RegistryPass string `gorm:"column:registry_pass;size:255" json:"registry_pass"` // 【加密】 DockerNetwork string `gorm:"column:docker_network;size:64" json:"docker_network"` // 如 1panel-network diff --git a/apps/services/modules/mcp/module.go b/apps/services/modules/mcp/module.go index c01bb44f..ba9521c5 100644 --- a/apps/services/modules/mcp/module.go +++ b/apps/services/modules/mcp/module.go @@ -81,13 +81,7 @@ func (this *Mcp) OnInstallComp() { this.RegisterComp(new(tool_tavily_search)) this.RegisterComp(new(tool_hefeng_weather)) - this.RegisterComp(new(tool_music)) this.RegisterComp(new(tool_migu_music)) - // this.RegisterComp(new(tool_music_pause)) - // this.RegisterComp(new(tool_music_resume)) - // this.RegisterComp(new(tool_music_prev)) - // this.RegisterComp(new(tool_music_next)) - // this.RegisterComp(new(tool_music_close)) // 记忆中心(拾忆):只读工具。写操作留在 home 的 memory 模块, // 在这里复制一份写路径的校验/幂等/提醒重算必然漂移。 diff --git a/apps/services/modules/mcp/tool_music.go b/apps/services/modules/mcp/tool_music.go deleted file mode 100644 index 7196afae..00000000 --- a/apps/services/modules/mcp/tool_music.go +++ /dev/null @@ -1,115 +0,0 @@ -package mcp - -import ( - "context" - "yunyan/lego/core" - "yunyan/lego/core/cbase" - "yunyan/lego/sys/log" - "yunyan/sys/musicobj" - "yunyan/utils" - "fmt" - - "github.com/mark3labs/mcp-go/mcp" -) - -// 音乐工具(搜索+播放) -type tool_music struct { - cbase.ModuleCompBase - module *Mcp -} - -// 组件初始化接口 -func (this *tool_music) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { - this.ModuleCompBase.Init(service, module, comp, opt) - this.module = module.(*Mcp) - return -} - -func (this *tool_music) Start() (err error) { - err = this.ModuleCompBase.Start() - if this.module.AddTool(ToolGroup_CHINA, this.Tool(), this.Handl) { - if err := musicobj.OnInit(this.module.service.GetSettings().Sys["musicobj"]); err != nil { - panic(fmt.Sprintf("init sys.musicobj err: %s", err.Error())) - } else { - log.Infof("init sys.musicobj success!") - } - } - return -} - -func (this *tool_music) Tool() mcp.Tool { - return mcp.NewTool("music_search_play", - mcp.WithDescription("搜索音乐并返回带播放链接的音乐列表"), - mcp.WithString("query", - mcp.Description("搜索的音乐关键字 例如:小苹果, 周杰伦, 张学友, 流行音乐等"), - mcp.Required(), - mcp.DefaultString("热门"), - ), - mcp.WithNumber("limit", - mcp.Description("返回结果数量 (1-20, default 10)"), - mcp.DefaultNumber(30), - ), - mcp.WithNumber("offset", - mcp.Description("搜索的音乐偏移量 范围在0到9之间 (默认0)"), - mcp.DefaultNumber(0), - ), - ) -} - -type MusicItem struct { - Id int64 `json:"id"` - Name string `json:"name"` - Singer string `json:"singer"` - Image string `json:"image"` - Url string `json:"url"` -} - -func (this *tool_music) Handl(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) { - var ( - query string - limit, offset int - songslist *musicobj.SearchResultResponse - ) - if query, err = request.RequireString("query"); err != nil { - err = fmt.Errorf("query is required") - return - } - limit = request.GetInt("limit", 30) - offset = request.GetInt("offset", 0) - if songslist, err = musicobj.SearchMusic(query, limit, offset); err != nil { - this.module.Errorln(err) - return - } - if len(songslist.Data.Musics) == 0 { - err = fmt.Errorf("no music found") - return - } - - musics := make([]*MusicItem, 0, len(songslist.Data.Musics)) - for _, v := range songslist.Data.Musics { - item := &MusicItem{ - Id: v.Id, - Name: v.Name, - Singer: v.Singer, - Image: v.Image, - } - if urlResp, urlErr := musicobj.MusicUrl(v.Id); urlErr == nil { - item.Url = urlResp.Data.URL - } - musics = append(musics, item) - } - - text := utils.ToString(map[string]interface{}{ - "data": musics, - "result": "success", - }) - result = &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: text, - }, - }, - } - return -} diff --git a/apps/services/modules/music/api.go b/apps/services/modules/music/api.go deleted file mode 100644 index a97a3f96..00000000 --- a/apps/services/modules/music/api.go +++ /dev/null @@ -1,27 +0,0 @@ -package music - -import ( - "yunyan/lego/base" - "yunyan/lego/core" - "yunyan/modules" -) - -type apiComp struct { - modules.MCompHttpGate - service base.IRPCXService - module *Music - options *Options -} - -func (this *apiComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, options core.IModuleOptions) (err error) { - this.MCompHttpGate.Init(service, module, comp, options) - this.service = service.(base.IRPCXService) - this.module = module.(*Music) - this.options = options.(*Options) - return -} - -func (this *apiComp) Start() (err error) { - err = this.MCompHttpGate.Start() - return -} diff --git a/apps/services/modules/music/api_search.go b/apps/services/modules/music/api_search.go deleted file mode 100644 index 098f9064..00000000 --- a/apps/services/modules/music/api_search.go +++ /dev/null @@ -1,55 +0,0 @@ -package music - -import ( - "yunyan/comm" - "yunyan/pb" - "yunyan/sys/musicobj" - "fmt" -) - -// @Summary 获取音乐播放地址 -// @Description 获取音乐播放地址 -// @Tags Music -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param user body pb.MusicSearchReq true "请求数据" -// @Success 200 {object} comm.HttpResult{data=pb.MusicSearchResp} "响应数据" -// @Router /api/home/music_search [post] -func (this *apiComp) Search(session comm.IUserSession, req *pb.MusicSearchReq) (resp *pb.MusicSearchResp, errdata *pb.ErrorData) { - var ( - musics []*pb.DBMusic - songslist *musicobj.SearchResultResponse - err error - ) - - if songslist, err = musicobj.SearchMusic(req.Keywords, int(req.Limit), int(req.Offset)); err != nil { - errdata = &pb.ErrorData{ - Code: pb.ErrorCode_MusicSearchError, - Message: err.Error(), - } - return - } else { - if len(songslist.Data.Musics) == 0 { - errdata = &pb.ErrorData{ - Code: pb.ErrorCode_MusicNotFound, - Message: fmt.Sprintf("未找到关键字:%s 相关歌单", req.Keywords), - } - return - } - } - - musics = make([]*pb.DBMusic, 0) - for _, v := range songslist.Data.Musics { - musiccard := &pb.DBMusic{Id: v.Id} - musiccard.Name = v.Name - musiccard.Singer = v.Singer - musiccard.Image = v.Image - musics = append(musics, musiccard) - } - - resp = &pb.MusicSearchResp{ - Musics: musics, - } - return -} diff --git a/apps/services/modules/music/api_url.go b/apps/services/modules/music/api_url.go deleted file mode 100644 index 5ff35f89..00000000 --- a/apps/services/modules/music/api_url.go +++ /dev/null @@ -1,38 +0,0 @@ -package music - -import ( - "yunyan/comm" - "yunyan/lego/sys/log" - "yunyan/pb" - "yunyan/sys/musicobj" -) - -// @Summary 获取音乐播放地址 -// @Description 获取音乐播放地址 -// @Tags Music -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param user body pb.MusicUrlReq true "请求数据" -// @Success 200 {object} comm.HttpResult{data=pb.MusicUrlResp} "响应数据" -// @Router /api/home/music_url [post] -func (this *apiComp) Url(session comm.IUserSession, req *pb.MusicUrlReq) (resp *pb.MusicUrlResp, errdata *pb.ErrorData) { - var ( - result *musicobj.UrlResultResponse - err error - ) - if result, err = musicobj.MusicUrl(req.Id); err != nil { - errdata = &pb.ErrorData{ - Code: pb.ErrorCode_MusicUrlError, - Message: err.Error(), - } - this.module.Error("GetMusicUrl Fail!", log.Field{Key: "uid", Value: session.GetUserId()}, log.Field{Key: "req", Value: req.String()}, log.Field{Key: "err", Value: err.Error()}) - return - } - resp = &pb.MusicUrlResp{ - Id: req.Id, - Url: result.Data.URL, - Ts: result.Data.Ts, - } - return -} diff --git a/apps/services/modules/music/core.go b/apps/services/modules/music/core.go deleted file mode 100644 index 458a5389..00000000 --- a/apps/services/modules/music/core.go +++ /dev/null @@ -1,26 +0,0 @@ -package music - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// 设置跨域 -func cors() gin.HandlerFunc { - return func(c *gin.Context) { - method := c.Request.Method - origin := c.Request.Header.Get("Origin") - if origin != "" { - c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名 - c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") - c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") - c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type") - c.Header("Access-Control-Allow-Credentials", "true") - } - if method == "OPTIONS" { - c.AbortWithStatus(http.StatusNoContent) - } - c.Next() - } -} diff --git a/apps/services/modules/music/model.go b/apps/services/modules/music/model.go deleted file mode 100644 index 2c4cd0db..00000000 --- a/apps/services/modules/music/model.go +++ /dev/null @@ -1,20 +0,0 @@ -package music - -import ( - "yunyan/lego/core" - "yunyan/lego/core/cbase" -) - -// 代理模型 -type modelComp struct { - cbase.ModuleCompBase - module *Music -} - -// 组件初始化接口 -func (this *modelComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { - this.ModuleCompBase.Init(service, module, comp, opt) - this.module = module.(*Music) - - return -} diff --git a/apps/services/modules/music/model_test.go b/apps/services/modules/music/model_test.go deleted file mode 100644 index 70572a21..00000000 --- a/apps/services/modules/music/model_test.go +++ /dev/null @@ -1 +0,0 @@ -package music_test diff --git a/apps/services/modules/music/module.go b/apps/services/modules/music/module.go deleted file mode 100644 index cf010668..00000000 --- a/apps/services/modules/music/module.go +++ /dev/null @@ -1,49 +0,0 @@ -package music - -import ( - "yunyan/comm" - "yunyan/modules" - - "yunyan/lego/core" -) - -func NewModule() core.IModule { - m := new(Music) - return m -} - -type Music struct { - modules.ModuleBase - api *apiComp - model *modelComp - options *Options -} - -func (this *Music) GetType() core.M_Modules { - return comm.ModuleMusic -} - -func (this *Music) NewOptions() (options core.IModuleOptions) { - return new(Options) -} - -func (this *Music) Init(service core.IService, module core.IModule, options core.IModuleOptions) (err error) { - this.options = options.(*Options) - if err = this.ModuleBase.Init(service, module, options); err != nil { - return - } - return -} - -func (this *Music) Start() (err error) { - if err = this.ModuleBase.Start(); err != nil { - return - } - return -} - -func (this *Music) OnInstallComp() { - this.ModuleBase.OnInstallComp() - this.api = this.RegisterComp(new(apiComp)).(*apiComp) - this.model = this.RegisterComp(new(modelComp)).(*modelComp) -} diff --git a/apps/services/modules/music/options.go b/apps/services/modules/music/options.go deleted file mode 100644 index 82436bf7..00000000 --- a/apps/services/modules/music/options.go +++ /dev/null @@ -1,27 +0,0 @@ -package music - -import ( - "yunyan/modules" - - "yunyan/lego/utils/mapstructure" -) - -type ( - Options struct { - modules.Options - GinDebug bool //web引擎日志开关 - ListenPort int //websocket 监听端口 - } -) - -func (this *Options) LoadConfig(settings map[string]interface{}) (err error) { - if settings != nil { - if err = this.Options.LoadConfig(settings); err != nil { - return - } - if err = mapstructure.Decode(settings, this); err != nil { - return - } - } - return -} diff --git a/apps/services/modules/user/api_setting.go b/apps/services/modules/user/api_setting.go index a01dabcd..80bd96e1 100644 --- a/apps/services/modules/user/api_setting.go +++ b/apps/services/modules/user/api_setting.go @@ -1,14 +1,21 @@ package user import ( + "fmt" + "io" + "path" + "strings" + "yunyan/comm" "yunyan/lego/sys/mysql" "yunyan/lego/sys/wordfilter" "yunyan/pb" - "yunyan/sys/tencentyun/cos" - "yunyan/utils" + "yunyan/sys/aliyun/oss" + + ossSDK "github.com/aliyun/aliyun-oss-go-sdk/oss" "os" "path/filepath" + "yunyan/utils" ) // @Summary 用户设置接口 @@ -72,7 +79,7 @@ func (this *apiComp) Setting(session comm.IUserSession, req *pb.UserSettingReq) // 获取文件扩展名 ext := filepath.Ext(filename) // 上传 - 使用用户ID+扩展名作为文件名避免冲突 - publicURL, err = cos.Put("/userAvatar/"+session.GetUserId()+ext, file) + publicURL, err = putAvatar(session.GetUserId(), ext, file) if err != nil { errdata = &pb.ErrorData{ Code: pb.ErrorCode_StorageError, @@ -98,3 +105,37 @@ func (this *apiComp) Setting(session comm.IUserSession, req *pb.UserSettingReq) } return } + +// putAvatar 把头像传到阿里云 OSS,返回可直接访问的地址。 +// +// 原实现走腾讯云 COS(sys/tencentyun/cos),2026-09-23 随 COS 整体下线改到 OSS, +// 与 user_getuploadurl / console 后台直传共用 comm.LoadAliyunOSSConf 那一份配置—— +// 分家会让运维要盯两套存储。 +// +// object key 用 uid 拼死且**不带随机段**,即同一用户换头像覆盖同一个对象: +// 与 COS 时期的行为一致(不留历史文件),代价是 URL 不变、客户端可能吃到缓存。 +func putAvatar(uid, ext string, r io.Reader) (string, error) { + // 扩展名白名单,且必须是图片:这个入口收的是头像,放开任意扩展名等于多一个任人上传的通道。 + ct := uploadExtWhitelist[strings.ToLower(ext)] + if !strings.HasPrefix(ct, "image/") { + return "", fmt.Errorf("不支持的头像格式: %s", ext) + } + conf, err := comm.LoadAliyunOSSConf(comm.AppName(), os.Getenv("FIELD_ENCRYPT_KEY")) + if err != nil { + return "", err + } + client, err := oss.NewPresigner(conf.Endpoint, conf.AccessKeyId, conf.AccessSecret, conf.Bucket) + if err != nil { + return "", fmt.Errorf("初始化阿里云 OSS 失败: %w", err) + } + key := path.Join(comm.OSSUploadRoot, "User", uid, "Avatar", uid+strings.ToLower(ext)) + if err = client.UploadObject(key, r, ossSDK.ContentType(ct)); err != nil { + return "", err + } + // 没配自定义域名时 PublicURL 要从一个签名 URL 上摘 host,签名是纯本地运算、不发网络请求。 + signed, err := client.GetURL(key, 60) + if err != nil { + return "", err + } + return conf.PublicURL(signed, key), nil +} diff --git a/apps/services/modules/user/options.go b/apps/services/modules/user/options.go index 4cd275f0..b1f88084 100644 --- a/apps/services/modules/user/options.go +++ b/apps/services/modules/user/options.go @@ -19,7 +19,6 @@ type ( // user_getmcptoken 直接报错,不会退化成用 TokenKey 签。 McpTokenKey string CBCKey string - AvatarUrl string //头像地址 UserGroup int //用户分组 OpenTestAccount bool //是否开启测试账号 TestAccounts []string //是否测试账号 diff --git a/apps/services/services/api/main.go b/apps/services/services/api/main.go index b48aa099..c479884e 100644 --- a/apps/services/services/api/main.go +++ b/apps/services/services/api/main.go @@ -9,7 +9,6 @@ import ( "yunyan/services" "yunyan/sys/email" "yunyan/sys/microsoft/translate" - "yunyan/sys/tencentyun/cos" "flag" "fmt" "os" @@ -69,7 +68,7 @@ func (this *Service) InitSys() { // 服务配置托管:库优先、无则读文件回写(seed)。应用标识取部署身份(comm.AppName/AppRegion, // 由 .env 的 ANALYZE_APP_NAME/ANALYZE_REGION 声明),FIELD_ENCRYPT_KEY 需与 console 一致。 // 未声明应用名则跳过、保持纯文件配置(不产生孤儿数据)。 - // 命中库配置时就地覆盖 Sys(mysql/redis/cos…),随后各 OnInit 使用被覆盖后的值。 + // 命中库配置时就地覆盖 Sys(mysql/redis/nats…),随后各 OnInit 使用被覆盖后的值。 appName, region, encKey := comm.AppName(), comm.AppRegion(), os.Getenv("FIELD_ENCRYPT_KEY") if seeded, err := comm.LoadOrSeedServiceConfig(encKey, appName, region, "api", this.GetSettings().Sys); err != nil { log.Warnf("init service config 托管失败(降级为纯文件配置): %s", err.Error()) @@ -100,11 +99,6 @@ func (this *Service) InitSys() { } else { log.Infof("init sys.redis success!") } - if err := cos.OnInit(this.GetSettings().Sys["cos"]); err != nil { - panic(fmt.Sprintf("init sys.cos err: %s", err.Error())) - } else { - log.Infof("init sys.cos success!") - } // 邮件服务:可选能力,缺配置降级(未初始化时 SendMail* 返回错误),console 配好后可热重载生效。 // 不能 panic —— api 在 5-in-1 容器里一死,entrypoint 会连坐杀掉其余服务(测试服常不配发信凭据)。 diff --git a/apps/services/services/home/main.go b/apps/services/services/home/main.go index b69e5691..507db6e0 100644 --- a/apps/services/services/home/main.go +++ b/apps/services/services/home/main.go @@ -9,7 +9,6 @@ import ( "yunyan/modules/analyze" "yunyan/modules/echomeet" "yunyan/modules/memory" - musicmodule "yunyan/modules/music" "yunyan/modules/pay" "yunyan/modules/svccfg" "yunyan/modules/user" @@ -23,7 +22,6 @@ import ( "yunyan/sys/email" "yunyan/sys/google" "yunyan/sys/ipinfo" - "yunyan/sys/musicobj" "yunyan/sys/nats" "yunyan/sys/pay/alipay" "yunyan/sys/pay/appleiap" @@ -31,7 +29,6 @@ import ( "yunyan/sys/pay/paypal" "yunyan/sys/pay/wechat" "yunyan/sys/sms" - "yunyan/sys/tencentyun/cos" "yunyan/sys/websearch/bochasearch" "flag" "fmt" @@ -79,7 +76,6 @@ func main() { agents.NewModule(), pay.NewModule(), echomeet.NewModule(), - musicmodule.NewModule(), allhelp.NewModule(), memory.NewModule(), //记忆中心(拾忆):闹钟/待办/灵感/花销 + 周报月报 // bailianmodule.NewModule(), @@ -146,11 +142,6 @@ func (this *Service) InitSys() { } else { log.Infof("init sys.nats success!") } - if err := cos.OnInit(this.GetSettings().Sys["cos"]); err != nil { - panic(fmt.Sprintf("init sys.cos err: %s", err.Error())) - } else { - log.Infof("init sys.cos success!") - } // 短信服务:可选能力,缺配置降级(未初始化时 SendCaptcha 返回错误),console 配好后可热重载生效。 // 不能 panic —— home 在 5-in-1 容器里一死,entrypoint 会连坐杀掉其余服务(测试服常不配短信凭据)。 if err := sms.OnInit(this.GetSettings().Sys["sms"]); err != nil { @@ -200,11 +191,6 @@ func (this *Service) InitSys() { log.Infof("init sys.wechat_auth success!") } - if err := musicobj.OnInit(this.GetSettings().Sys["musicobj"]); err != nil { - log.Warnf("init sys.musicobj err: %s (音乐对象存储未配置,相关功能不可用)", err.Error()) - } else { - log.Infof("init sys.musicobj success!") - } // 邮件服务:同 sms,可选能力,缺配置降级(未初始化时 SendMail* 返回错误)。 if err := email.OnInit(this.GetSettings().Sys["email"]); err != nil { diff --git a/apps/services/sys/aliyun/filetrans/sys_test.go b/apps/services/sys/aliyun/filetrans/sys_test.go index ca0dd940..6be172aa 100644 --- a/apps/services/sys/aliyun/filetrans/sys_test.go +++ b/apps/services/sys/aliyun/filetrans/sys_test.go @@ -16,7 +16,7 @@ const ( testWorkspaceKey = "sk-ws-H.ERXDMMY.8sEM.MEUCIDeZPGn6VJdlGEoWwhLG-w_2c4OT2IZSmmucug1sFPtmAiEA79Se7JiZ7CXw5AB33r3zXvSHPqwtcL7fThJSrq1e-gM" ) -const testVietnameseAudio = "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com/User/1942916924698525696/LocalAudio/2026/02/27/jLEIQzYIAnqy.wav" +const testVietnameseAudio = "https://oss.ymaikj.com/EAIMAR/User/1942916924698525696/LocalAudio/2026/02/27/jLEIQzYIAnqy.wav" // 注意 language 传的是 **短码**(language_hints 格式,即 toDashScopeLang 的输出),不是 BCP-47。 // 传 "vi-VN" 会被 CreateTask 的语种校验直接拒掉。 @@ -121,7 +121,7 @@ func TestCreateAndPollTask(t *testing.T) { } taskID, err := sys.CreateTask( - "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com/User/2014183956768882688/LocalAudio/2026/01/22/kUqCZgiVkspH.wav", + "https://oss.ymaikj.com/EAIMAR/User/2014183956768882688/LocalAudio/2026/01/22/kUqCZgiVkspH.wav", "zh", true, "", diff --git a/apps/services/sys/bytedance/audomodel/sys_test.go b/apps/services/sys/bytedance/audomodel/sys_test.go index df0d4b78..cabfa47a 100644 --- a/apps/services/sys/bytedance/audomodel/sys_test.go +++ b/apps/services/sys/bytedance/audomodel/sys_test.go @@ -86,7 +86,7 @@ func TestCreateTask(t *testing.T) { if err != nil { t.Fatalf("初始化失败: %v", err) } - taskID, logID, err := sys.CreateTask("2019585175758831616", "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com/User/2019585175758831616/LocalAudio/2026/02/27/rLLkZPargyKZ.wav", false, "en-US", "https://zens.yunyanservice.xyz/api/home/echomeet_backcall", fmt.Sprintf("%d", 49)) + taskID, logID, err := sys.CreateTask("2019585175758831616", "https://oss.ymaikj.com/EAIMAR/User/2019585175758831616/LocalAudio/2026/02/27/rLLkZPargyKZ.wav", false, "en-US", "https://ym-dev.ymaikj.com/api/home/echomeet_backcall", fmt.Sprintf("%d", 49)) if err != nil { fmt.Printf("创建任务失败: %v (taskID=%s, logID=%s)", err, taskID, logID) } diff --git a/apps/services/sys/music/core.go b/apps/services/sys/music/core.go deleted file mode 100644 index 97ae417b..00000000 --- a/apps/services/sys/music/core.go +++ /dev/null @@ -1,623 +0,0 @@ -package music - -type ( - - // MusicResponse 顶层响应结构 - SearchResultResponse struct { - Result struct { - SearchQcReminder interface{} `json:"searchQcReminder"` - Songs []struct { - Name string `json:"name"` - Id int64 `json:"id"` - Pst int `json:"pst"` - T int `json:"t"` - Ar []struct { // 内嵌艺术家结构 - Id int64 `json:"id"` - Name string `json:"name"` - Tns []string `json:"tns"` - Alias []string `json:"alias"` - Alia []string `json:"alia"` - } `json:"ar"` - Alia []string `json:"alia"` - Pop int `json:"pop"` - St int `json:"st"` - Rt string `json:"rt"` - Fee int `json:"fee"` - V int `json:"v"` - Crbt interface{} `json:"crbt"` - Cf string `json:"cf"` - Al struct { // 内嵌专辑结构 - Id int64 `json:"id"` - Name string `json:"name"` - PicUrl string `json:"picUrl"` - Tns []string `json:"tns"` - PicStr string `json:"pic_str"` - Pic int64 `json:"pic"` - } `json:"al"` - Dt int `json:"dt"` - H struct { // 内嵌高品质音频信息 - Br int `json:"br"` - Fid int `json:"fid"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - } `json:"h"` - M struct { // 内嵌中品质音频信息 - Br int `json:"br"` - Fid int `json:"fid"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - } `json:"m"` - L struct { // 内嵌低品质音频信息 - Br int `json:"br"` - Fid int `json:"fid"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - } `json:"l"` - Sq struct { // 内嵌无损品质音频信息 - Br int `json:"br"` - Fid int `json:"fid"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - } `json:"sq"` - Hr struct { // 内嵌高清品质音频信息 - Br int `json:"br"` - Fid int `json:"fid"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - } `json:"hr"` - A interface{} `json:"a"` - Cd string `json:"cd"` - No int `json:"no"` - RtUrl interface{} `json:"rtUrl"` - Ftype int `json:"ftype"` - RtUrls []interface{} `json:"rtUrls"` - DjId int `json:"djId"` - Copyright int `json:"copyright"` - SId int `json:"s_id"` - Mark int64 `json:"mark"` - OriginCoverType int `json:"originCoverType"` - OriginSongSimpleData interface{} `json:"originSongSimpleData"` - TagPicList interface{} `json:"tagPicList"` - ResourceState bool `json:"resourceState"` - Version int `json:"version"` - SongJumpInfo interface{} `json:"songJumpInfo"` - EntertainmentTags interface{} `json:"entertainmentTags"` - Single int `json:"single"` - NoCopyrightRcmd interface{} `json:"noCopyrightRcmd"` - Rtype int `json:"rtype"` - Rurl interface{} `json:"rurl"` - Mst int `json:"mst"` - Cp int `json:"cp"` - Mv int `json:"mv"` - PublishTime int `json:"publishTime"` - Privilege struct { // 内嵌权限信息结构 - Id int64 `json:"id"` - Fee int `json:"fee"` - Payed int `json:"payed"` - St int `json:"st"` - Pl int `json:"pl"` - Dl int `json:"dl"` - Sp int `json:"sp"` - Cp int `json:"cp"` - Subp int `json:"subp"` - Cs bool `json:"cs"` - Maxbr int `json:"maxbr"` - Fl int `json:"fl"` - Toast bool `json:"toast"` - Flag int `json:"flag"` - PreSell bool `json:"preSell"` - PlayMaxbr int `json:"playMaxbr"` - DownloadMaxbr int `json:"downloadMaxbr"` - MaxBrLevel string `json:"maxBrLevel"` - PlayMaxBrLevel string `json:"playMaxBrLevel"` - DownloadMaxBrLevel string `json:"downloadMaxBrLevel"` - PlLevel string `json:"plLevel"` - DlLevel string `json:"dlLevel"` - FlLevel string `json:"flLevel"` - Rscl interface{} `json:"rscl"` - FreeTrialPrivilege struct { // 内嵌免费试用权限 - ResConsumable bool `json:"resConsumable"` - UserConsumable bool `json:"userConsumable"` - ListenType interface{} `json:"listenType"` - CannotListenReason interface{} `json:"cannotListenReason"` - PlayReason interface{} `json:"playReason"` - FreeLimitTagType interface{} `json:"freeLimitTagType"` - } `json:"freeTrialPrivilege"` - RightSource int `json:"rightSource"` - ChargeInfoList []struct { // 内嵌收费信息列表 - Rate int `json:"rate"` - ChargeUrl interface{} `json:"chargeUrl"` - ChargeMessage interface{} `json:"chargeMessage"` - ChargeType int `json:"chargeType"` - } `json:"chargeInfoList"` - Code int `json:"code"` - Message interface{} `json:"message"` - PlLevels interface{} `json:"plLevels"` - DlLevels interface{} `json:"dlLevels"` - IgnoreCache interface{} `json:"ignoreCache"` - Bd interface{} `json:"bd"` - } `json:"privilege"` - } `json:"songs"` - SongCount int `json:"songCount"` - } `json:"result"` - Code int `json:"code"` - } - - FreeTrialInfo struct { - ResConsumable bool `json:"resConsumable"` - UserConsumable bool `json:"userConsumable"` - ListenType interface{} `json:"listenType"` - CannotListenReason interface{} `json:"cannotListenReason"` - PlayReason interface{} `json:"playReason"` - FreeLimitTagType interface{} `json:"freeLimitTagType"` - } - - FreeTimeTrialPrivilege struct { - ResConsumable bool `json:"resConsumable"` - UserConsumable bool `json:"userConsumable"` - Type int `json:"type"` - RemainTime int `json:"remainTime"` - } - - UrlResult struct { - Data struct { - Size int `json:"size"` - Br int `json:"br"` - URL string `json:"url"` - MD5 string `json:"md5"` - Source string `json:"source"` - } `json:"data"` - Code int `json:"code"` - } - //歌单搜索 - PlaylistSearchResponse struct { - Code int `json:"code"` // 响应状态码 - - Result struct { - PlaylistCount int `json:"playlistCount"` // 歌单总数 - HasMore bool `json:"hasMore"` // 是否有更多数据 - HlWords []string `json:"hlWords"` // 高亮词(通常为搜索关键词) - SearchQcReminder interface{} `json:"searchQcReminder"` // 提示信息(暂无结构) - - Playlists []struct { - ID int64 `json:"id"` // 歌单 ID - Name string `json:"name"` // 歌单名称 - CoverImgUrl string `json:"coverImgUrl"` // 歌单封面图 - Subscribed bool `json:"subscribed"` // 是否已收藏 - TrackCount int `json:"trackCount"` // 歌曲数量 - UserId int64 `json:"userId"` // 创建者用户 ID - PlayCount int64 `json:"playCount"` // 播放次数 - BookCount int64 `json:"bookCount"` // 收藏次数 - SpecialType int `json:"specialType"` // 特殊标记(如官方歌单) - Action string `json:"action"` // 播放链接 - ActionType string `json:"actionType"` // 动作类型 - RecommendText interface{} `json:"recommendText"` // 推荐文案(可能为空) - Score string `json:"score"` // 推荐分数(字符串) - Description string `json:"description"` // 歌单描述 - HighQuality bool `json:"highQuality"` // 是否高品质歌单 - OfficialTags []string `json:"officialTags"` // 官方标签 - - Creator struct { - Nickname string `json:"nickname"` // 创建者昵称 - UserId int64 `json:"userId"` // 用户 ID - UserType int `json:"userType"` // 用户类型 - AvatarUrl string `json:"avatarUrl"` // 用户头像 - AuthStatus int `json:"authStatus"` // 认证状态 - ExpertTags interface{} `json:"expertTags"` // 专家标签(空或未知结构) - Experts interface{} `json:"experts"` // 专家信息(空或未知结构) - } `json:"creator"` // 创建者信息 - - Track struct { - Name string `json:"name"` // 歌曲名称 - ID int64 `json:"id"` // 歌曲 ID - Position int `json:"position"` // 歌曲在歌单中的位置 - Alias []string `json:"alias"` // 别名 - Status int `json:"status"` // 状态 - Fee int `json:"fee"` // 费用 - CopyrightId int `json:"copyrightId"` // 版权 ID - Disc string `json:"disc"` // 所属碟片 - No int `json:"no"` // 曲目编号 - - Artists []struct { - Name string `json:"name"` // 歌手名 - ID int64 `json:"id"` // 歌手 ID - PicId int64 `json:"picId"` - Img1v1Id int64 `json:"img1v1Id"` - BriefDesc string `json:"briefDesc"` - PicUrl string `json:"picUrl"` - Img1v1Url string `json:"img1v1Url"` - AlbumSize int `json:"albumSize"` - Alias []string `json:"alias"` - Trans string `json:"trans"` - MusicSize int `json:"musicSize"` - TopicPerson int `json:"topicPerson"` - } `json:"artists"` // 歌手列表 - - Album struct { - Name string `json:"name"` // 专辑名称 - ID int64 `json:"id"` // 专辑 ID - IdStr *string `json:"idStr"` // ID 字符串 - Type string `json:"type"` // 类型 - Size int `json:"size"` // 歌曲数 - PicId int64 `json:"picId"` // 封面图 ID - BlurPicUrl string `json:"blurPicUrl"` // 模糊封面图 - CompanyId int `json:"companyId"` - Pic int64 `json:"pic"` - PicUrl string `json:"picUrl"` - PublishTime int64 `json:"publishTime"` // 发布时间 - Description string `json:"description"` // 描述 - Tags string `json:"tags"` // 标签 - Company string `json:"company"` // 出版公司 - BriefDesc string `json:"briefDesc"` // 简要描述 - - Artist struct { - Name string `json:"name"` - ID int64 `json:"id"` - PicId int64 `json:"picId"` - Img1v1Id int64 `json:"img1v1Id"` - BriefDesc string `json:"briefDesc"` - PicUrl string `json:"picUrl"` - Img1v1Url string `json:"img1v1Url"` - AlbumSize int `json:"albumSize"` - Alias []string `json:"alias"` - Trans string `json:"trans"` - MusicSize int `json:"musicSize"` - TopicPerson int `json:"topicPerson"` - } `json:"artist"` - - Songs []interface{} `json:"songs"` // 预留字段 - Alias []string `json:"alias"` - Status int `json:"status"` - CopyrightId int `json:"copyrightId"` - CommentThreadId string `json:"commentThreadId"` - Artists []struct { - Name string `json:"name"` - ID int64 `json:"id"` - PicId int64 `json:"picId"` - Img1v1Id int64 `json:"img1v1Id"` - BriefDesc string `json:"briefDesc"` - PicUrl string `json:"picUrl"` - Img1v1Url string `json:"img1v1Url"` - AlbumSize int `json:"albumSize"` - Alias []string `json:"alias"` - Trans string `json:"trans"` - MusicSize int `json:"musicSize"` - TopicPerson int `json:"topicPerson"` - } `json:"artists"` - - OnSale bool `json:"onSale"` - PicIdStr string `json:"picId_str"` - } `json:"album"` - - Starred bool `json:"starred"` - Popularity int `json:"popularity"` - Score int `json:"score"` - StarredNum int `json:"starredNum"` - Duration int `json:"duration"` // 时长(毫秒) - PlayedNum int `json:"playedNum"` - DayPlays int `json:"dayPlays"` - HearTime int `json:"hearTime"` - Ringtone string `json:"ringtone"` - CRBT interface{} `json:"crbt"` - Audition interface{} `json:"audition"` - CopyFrom string `json:"copyFrom"` - CommentThreadId string `json:"commentThreadId"` - RtUrl interface{} `json:"rtUrl"` - Ftype int `json:"ftype"` - RtUrls []string `json:"rtUrls"` - Copyright int `json:"copyright"` - - HMusic struct { - Name *string `json:"name"` - ID int64 `json:"id"` - Size int `json:"size"` - Extension string `json:"extension"` - Sr int `json:"sr"` - DfsId int64 `json:"dfsId"` - Bitrate int `json:"bitrate"` - PlayTime int `json:"playTime"` - VolumeDelta int `json:"volumeDelta"` - } `json:"hMusic"` - - MMusic struct { - Name *string `json:"name"` - ID int64 `json:"id"` - Size int `json:"size"` - Extension string `json:"extension"` - Sr int `json:"sr"` - DfsId int64 `json:"dfsId"` - Bitrate int `json:"bitrate"` - PlayTime int `json:"playTime"` - VolumeDelta int `json:"volumeDelta"` - } `json:"mMusic"` - - LMusic struct { - Name *string `json:"name"` - ID int64 `json:"id"` - Size int `json:"size"` - Extension string `json:"extension"` - Sr int `json:"sr"` - DfsId int64 `json:"dfsId"` - Bitrate int `json:"bitrate"` - PlayTime int `json:"playTime"` - VolumeDelta int `json:"volumeDelta"` - } `json:"lMusic"` - - BMusic struct { - Name *string `json:"name"` - ID int64 `json:"id"` - Size int `json:"size"` - Extension string `json:"extension"` - Sr int `json:"sr"` - DfsId int64 `json:"dfsId"` - Bitrate int `json:"bitrate"` - PlayTime int `json:"playTime"` - VolumeDelta int `json:"volumeDelta"` - } `json:"bMusic"` - - Mvid int `json:"mvid"` - Rtype int `json:"rtype"` - Rurl interface{} `json:"rurl"` - Mp3Url interface{} `json:"mp3Url"` - } `json:"track"` - - Alg string `json:"alg"` // 推荐算法字段 - } `json:"playlists"` - } `json:"result"` - } - - SongResponse struct { - Songs []struct { - Name string `json:"name"` // 歌曲名称 - MainTitle *string `json:"mainTitle"` // 主标题 - AdditionalTitle *string `json:"additionalTitle"` // 附加标题 - ID int `json:"id"` // 歌曲 ID - Pst int `json:"pst"` // 排序字段 - T int `json:"t"` // 类型字段 - Ar []struct { - ID int `json:"id"` // 歌手 ID - Name string `json:"name"` // 歌手名 - Tns []string `json:"tns"` // 翻译名 - Alias []string `json:"alias "` // 别名 - } `json:"ar"` - Alia []string `json:"alia"` // 歌曲别名 - Pop int `json:"pop"` // 热度 - St int `json:"st"` // 状态码 - Rt string `json:"rt"` // 资源标识 - Fee int `json:"费用"` // 是否收费 - V int `json:"v "` // 版本 - Crbt *string `json:"crbt"` // 彩铃信息 - Cf string `json:"cf"` // 未知字段 - Al struct { - ID int `json:"id"` // 专辑 ID - Name string `json:"name"` // 专辑名 - PicURL string `json:"picUrl"` // 专辑封面 - Tns []string `json:"tns"` // 翻译名 - PicStr string `json:"pic_str"` // 图片字符串 ID - Pic int64 `json:"pic"` // 图片 ID - } `json:"al"` - Dt int `json:"dt"` // 时长(毫秒) - - H struct{ Br, Fid, Size, Vd, Sr int } `json:"h"` // 高音质 - M struct{ Br, Fid, Size, Vd, Sr int } `json:"m"` // 中音质 - L struct{ Br, Fid, Size, Vd, Sr int } `json:"l"` // 低音质 - Sq struct{ Br, Fid, Size, Vd, Sr int } `json:"sq"` // 无损音质 - - Hr interface{} `json:"hr"` - A *string `json:"a"` - Cd string `json:"cd"` - No int `json:"no"` - RtUrl *string `json:"rtUrl"` - Ftype int `json:"ftype"` - RtUrls []string `json:"rtUrls"` - DjId int `json:"djId"` - Copyright int `json:"copyright"` - SId int `json:"s_id"` - Mark int64 `json:"mark"` - OriginCoverType int `json:"originCoverType"` - Tns []string `json:"tns"` - OriginSongSimpleData interface{} `json:"originSongSimpleData"` - TagPicList *string `json:"tagPicList"` - ResourceState bool `json:"resourceState"` - Version int `json:"version"` - SongJumpInfo *string `json:"songJumpInfo"` - EntertainmentTags *string `json:"entertainmentTags"` - AwardTags *string `json:"awardTags"` - DisplayTags *string `json:"displayTags"` - Single int `json:"single"` - NoCopyrightRcmd *string `json:"noCopyrightRcmd"` - Mv int `json:"mv"` - Rtype int `json:"rtype"` - Rurl *string `json:"rurl"` - Mst int `json:"mst"` - Cp int `json:"cp"` - PublishTime int64 `json:"publishTime"` - } `json:"songs"` - - Privileges []struct { - ID int `json:"id"` - Fee int `json:"fee"` - Payed int `json:"payed"` - St int `json:"st"` - Pl int `json:"pl"` - Dl int `json:"dl"` - Sp int `json:"sp"` - Cp int `json:"cp"` - Subp int `json:"subp"` - Cs bool `json:"cs"` - Maxbr int `json:"maxbr"` - Fl int `json:"fl"` - Toast bool `json:"toast"` - Flag int `json:"flag"` - PreSell bool `json:"preSell"` - PlayMaxbr int `json:"playMaxbr"` - DownloadMaxbr int `json:"downloadMaxbr"` - MaxBrLevel string `json:"maxBrLevel"` - PlayMaxBrLevel string `json:"playMaxBrLevel"` - DownloadMaxBrLevel string `json:"downloadMaxBrLevel"` - PlLevel string `json:"plLevel"` - DlLevel string `json:"dlLevel"` - FlLevel string `json:"flLevel"` - Rscl interface{} `json:"rscl"` - FreeTrialPrivilege struct { - ResConsumable bool `json:"resConsumable"` - UserConsumable bool `json:"userConsumable"` - ListenType interface{} `json:"listenType"` - CannotListenReason interface{} `json:"cannotListenReason"` - PlayReason interface{} `json:"playReason"` - FreeLimitTagType interface{} `json:"freeLimitTagType"` - } `json:"freeTrialPrivilege"` - RightSource int `json:"rightSource"` - ChargeInfoList []struct { - Rate int `json:"rate"` - ChargeUrl interface{} `json:"chargeUrl"` - ChargeMessage interface{} `json:"chargeMessage"` - ChargeType int `json:"chargeType"` - } `json:"chargeInfoList"` - Code int `json:"code"` - Message *string `json:"message"` - } `json:"privileges"` - - Code int `json:"code"` // 响应状态码 - } - //播放列表详情回复 - MusicListDetailResponse struct { - Code int `json:"code"` // 响应状态码 - - Songs []struct { - Name string `json:"name"` // 歌曲名称 - MainTitle *string `json:"mainTitle"` // 主标题(可选) - AdditionalTitle *string `json:"additionalTitle"` // 副标题(可选) - ID int64 `json:"id"` // 歌曲 ID - Pst int `json:"pst"` - T int `json:"t"` - - Ar []struct { - ID int64 `json:"id"` // 歌手 ID - Name string `json:"name"` // 歌手名称 - Tns []string `json:"tns"` // 翻译名 - Alias []string `json:"alias"` // 别名 - } `json:"ar"` // 歌手列表 - - Al struct { - ID int64 `json:"id"` // 专辑 ID - Name string `json:"name"` // 专辑名称 - PicURL string `json:"picUrl"` // 专辑封面 URL - PicStr string `json:"pic_str"` // 封面图字符串 - Pic int64 `json:"pic"` // 封面图 ID - Tns []string `json:"tns"` // 专辑翻译名 - } `json:"al"` // 专辑信息 - - Pop int `json:"pop"` // 热度 - - Fee int `json:"fee"` // 版权费用标记 - - DT int `json:"dt"` // 时长(单位:毫秒) - - H struct { - Br int `json:"br"` // 码率 - Size int `json:"size"` // 文件大小(字节) - Vd int `json:"vd"` // 音量动态 - Sr int `json:"sr"` // 采样率 - Fid int `json:"fid"` // 文件 ID - } `json:"h"` // 高音质 - - M struct { - Br int `json:"br"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - Fid int `json:"fid"` - } `json:"m"` // 中音质 - - L struct { - Br int `json:"br"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - Fid int `json:"fid"` - } `json:"l"` // 低音质 - - SQ *struct { - Br int `json:"br"` - Size int `json:"size"` - Vd int `json:"vd"` - Sr int `json:"sr"` - Fid int `json:"fid"` - } `json:"sq,omitempty"` // 超高音质(可选) - - MV int `json:"mv"` // MV ID - PublishTime int64 `json:"publishTime"` // 发布时间(时间戳) - Tns []string `json:"tns"` // 歌曲翻译名 - } `json:"songs"` - - Privileges []struct { - ID int64 `json:"id"` // 歌曲 ID - Fee int `json:"fee"` // 收费类型 - Payed int `json:"payed"` // 是否已付费 - ST int `json:"st"` // 状态码 - Pl int `json:"pl"` // 播放权限 - Dl int `json:"dl"` // 下载权限 - Sp int `json:"sp"` // 播放标记 - Cp int `json:"cp"` // 复制权限 - Subp int `json:"subp"` // 订阅权限 - Maxbr int `json:"maxbr"` // 最大码率 - Flag int `json:"flag"` // 标记位 - Fl int `json:"fl"` // 免费播放标记 - - ChargeInfoList []struct { - Rate int `json:"rate"` // 码率 - ChargeType int `json:"chargeType"` // 收费类型 - ChargeUrl string `json:"chargeUrl"` // 收费链接(一般为空) - ChargeMsg string `json:"chargeMessage"` // 提示信息(一般为空) - } `json:"chargeInfoList"` // 收费信息列表 - - FreeTrialPrivilege struct { - ResConsumable bool `json:"resConsumable"` // 资源是否可消费 - UserConsumable bool `json:"userConsumable"` // 用户是否可试听 - ListenType interface{} `json:"listenType"` // 听歌类型(可空) - CannotListenReason interface{} `json:"cannotListenReason"` // 无法播放原因(可空) - } `json:"freeTrialPrivilege"` // 试听权限设置 - } `json:"privileges"` - } - - ISys interface { - SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) - SearchMusicList(keywords string, limit, offset int) (result *PlaylistSearchResponse, err error) - MusicListDetail(id int64, limit, offset int) (result *MusicListDetailResponse, err error) - MusicUrl(id int64) (result *UrlResult, err error) - } -) - -var defsys ISys - -func OnInit(config map[string]interface{}, option ...Option) (err error) { - defsys, err = newSys(newOptions(config, option...)) - return -} - -func NewSys(option ...Option) (sys ISys, err error) { - sys, err = newSys(newOptionsByOption(option...)) - return -} - -func SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) { - return defsys.SearchMusic(keywords, limit, offset) -} - -func SearchMusicList(keywords string, limit, offset int) (result *PlaylistSearchResponse, err error) { - return defsys.SearchMusicList(keywords, limit, offset) -} - -func MusicListDetail(id int64, limit, offset int) (result *MusicListDetailResponse, err error) { - return defsys.MusicListDetail(id, limit, offset) -} - -func MusicUrl(id int64) (result *UrlResult, err error) { - return defsys.MusicUrl(id) -} diff --git a/apps/services/sys/music/music.go b/apps/services/sys/music/music.go deleted file mode 100644 index a2a5f208..00000000 --- a/apps/services/sys/music/music.go +++ /dev/null @@ -1,187 +0,0 @@ -package music - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" -) - -func newSys(options Options) (sys *Music, err error) { - sys = &Music{ - options: options, - } - return -} - -type Music struct { - options Options -} - -// 搜索歌曲 -func (this *Music) SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - keywords = url.QueryEscape(keywords) - url := fmt.Sprintf("%s/cloudsearch?keywords=%s&limit=%d&offset=%d", this.options.ApiBaseUrl, keywords, limit, offset) - - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return - } - - // 执行请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - } - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - // 输出响应内容 - // fmt.Println(string(body)) - result = &SearchResultResponse{} - err = json.Unmarshal(body, result) - return -} - -// 搜索歌单 -func (this *Music) SearchMusicList(keywords string, limit, offset int) (result *PlaylistSearchResponse, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - keywords = url.QueryEscape(keywords) - url := fmt.Sprintf("%s/search?type=1000&keywords=%s&limit=%d&offset=%d", this.options.ApiBaseUrl, keywords, limit, offset) - - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return - } - - // 设置请求头 - req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") - req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Connection", "keep-alive") - req.Header.Set("Pragma", "no-cache") - req.Header.Set("Sec-Fetch-Dest", "document") - req.Header.Set("Sec-Fetch-Mode", "navigate") - req.Header.Set("Sec-Fetch-Site", "none") - req.Header.Set("Sec-Fetch-User", "?1") - req.Header.Set("Upgrade-Insecure-Requests", "1") - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36") - req.Header.Set("sec-ch-ua", `"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"`) - req.Header.Set("sec-ch-ua-mobile", "?0") - req.Header.Set("sec-ch-ua-platform", `"macOS"`) - - // 设置 Cookie - req.Header.Set("Cookie", this.options.Cookie) - - // 执行请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - } - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - // 输出响应内容 - // fmt.Println(string(body)) - result = &PlaylistSearchResponse{} - err = json.Unmarshal(body, result) - return -} - -// 歌单详情 -func (this *Music) MusicListDetail(id int64, limit, offset int) (result *MusicListDetailResponse, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - url := fmt.Sprintf("%s/playlist/track/all?id=%d&limit=%d&offset=%d", this.options.ApiBaseUrl, id, limit, offset) - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return - } - - // 发起请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - } - // 打印结果 - fmt.Println(string(body)) - result = &MusicListDetailResponse{} - err = json.Unmarshal(body, result) - return -} - -// 音乐连接获取 -func (this *Music) MusicUrl(id int64) (result *UrlResult, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - - url := fmt.Sprintf("%s/song/url/unblock?id=%d", this.options.ApiBaseUrl, id) - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return - } - - // 发起请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - return - } - // 打印结果 - // fmt.Println(string(body)) - result = &UrlResult{} - err = json.Unmarshal(body, result) - return -} diff --git a/apps/services/sys/music/options.go b/apps/services/sys/music/options.go deleted file mode 100644 index d622c66c..00000000 --- a/apps/services/sys/music/options.go +++ /dev/null @@ -1,40 +0,0 @@ -package music - -import ( - "yunyan/lego/utils/mapstructure" -) - -type Option func(*Options) -type Options struct { - ApiBaseUrl string - Cookie string -} - -func SetApiBaseUrl(v string) Option { - return func(o *Options) { - o.ApiBaseUrl = v - } -} -func SetCookie(v string) Option { - return func(o *Options) { - o.Cookie = v - } -} -func newOptions(config map[string]interface{}, opts ...Option) Options { - options := Options{} - if config != nil { - mapstructure.Decode(config, &options) - } - for _, o := range opts { - o(&options) - } - return options -} - -func newOptionsByOption(opts ...Option) Options { - options := Options{} - for _, o := range opts { - o(&options) - } - return options -} diff --git a/apps/services/sys/music/sys_test.go b/apps/services/sys/music/sys_test.go deleted file mode 100644 index d2f83876..00000000 --- a/apps/services/sys/music/sys_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package music_test - -import ( - "yunyan/sys/music" - "fmt" - "io" - "net/http" - "net/url" - "os" - "testing" - "time" -) - -// Test_SearchMusic 调用外部音乐搜索接口做最小联通性验证。 -// 参数: -// - t: Go 测试对象 -// -// 返回值: -// - 无 -// -// 异常: -// - 当未启用集成测试时跳过 -// - 当外部接口不可用或返回异常时,测试失败 -func Test_SearchMusic(t *testing.T) { - if os.Getenv("MUSIC_INTEGRATION_TEST") == "" { - t.Skip("跳过测试:未设置环境变量 MUSIC_INTEGRATION_TEST") - } - keywords := url.QueryEscape("泡沫") - url := fmt.Sprintf("http://api.ideapsound.com:3000/cloudsearch?keywords=%s&limit=1&offset=0", keywords) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Fatalf("构造请求失败: %v", err) - } - // 执行请求 - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - t.Fatalf("请求失败: %v", err) - } - defer resp.Body.Close() - - // 读取响应内容 - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("读取响应失败: %v", err) - } - - // 输出响应内容 - fmt.Println(string(body)) -} - -// Test_Sys_SearchMusic 使用系统封装进行音乐 URL 获取的最小集成测试。 -// 参数: -// - t: Go 测试对象 -// -// 返回值: -// - 无 -// -// 异常: -// - 当未启用集成测试时跳过 -// - 当请求失败时,测试失败 -func Test_Sys_SearchMusic(t *testing.T) { - if os.Getenv("MUSIC_INTEGRATION_TEST") == "" { - t.Skip("跳过测试:未设置环境变量 MUSIC_INTEGRATION_TEST") - } - if sys, err := music.NewSys( - music.SetApiBaseUrl("https://music.voitrans.net"), - ); err != nil { - t.Fatalf("Sys Init err:%v", err) - } else { - var ( - searchResultResponse *music.UrlResult - ) - if searchResultResponse, err = sys.MusicUrl(441491828); err == nil { - - } - if err != nil { - t.Fatalf("播放音乐失败: %v", err) - } - t.Logf("播放音乐成功 searchResultResponse:%+v", searchResultResponse) - } -} diff --git a/apps/services/sys/musicobj/core.go b/apps/services/sys/musicobj/core.go deleted file mode 100644 index b5d16460..00000000 --- a/apps/services/sys/musicobj/core.go +++ /dev/null @@ -1,66 +0,0 @@ -package musicobj - -import "errors" - -type ( - SearchResultResponse struct { - Msg string `json:"msg"` - Data struct { - Musics []struct { - Id int64 `json:"id"` - Name string `json:"name"` - Image string `json:"image"` - Singer string `json:"singer"` - } `json:"musics"` - } `json:"data"` - Code int `json:"code"` - } - UrlResultResponse struct { - Msg string `json:"msg"` - Data struct { - Id int64 `json:"id"` - URL string `json:"url"` - Ts int64 `json:"ts"` - } `json:"data"` - Code int `json:"code"` - } - - ISys interface { - SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) - MusicUrl(id int64) (result *UrlResultResponse, err error) - } -) - -var defsys ISys - -// ErrNotInited 音乐对象存储未初始化。OnInit 失败时调用方可选择降级;此时包级函数返回本错误,而不是 panic。 -var ErrNotInited = errors.New("musicobj 未初始化:配置缺失") - -func OnInit(config map[string]interface{}, option ...Option) error { - sys, err := newSys(newOptions(config, option...)) - if err != nil { - // 出错时不要给 defsys 赋值,避免「非 nil 接口 + nil 指针」让守卫失效。 - return err - } - defsys = sys - return nil -} - -func NewSys(option ...Option) (sys ISys, err error) { - sys, err = newSys(newOptionsByOption(option...)) - return -} - -func SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) { - if defsys == nil { - return nil, ErrNotInited - } - return defsys.SearchMusic(keywords, limit, offset) -} - -func MusicUrl(id int64) (result *UrlResultResponse, err error) { - if defsys == nil { - return nil, ErrNotInited - } - return defsys.MusicUrl(id) -} diff --git a/apps/services/sys/musicobj/music.go b/apps/services/sys/musicobj/music.go deleted file mode 100644 index 9b1c2fb6..00000000 --- a/apps/services/sys/musicobj/music.go +++ /dev/null @@ -1,117 +0,0 @@ -package musicobj - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" -) - -func newSys(options Options) (sys *Music, err error) { - sys = &Music{ - options: options, - } - return -} - -type Music struct { - options Options -} - -// 搜索歌曲 -func (this *Music) SearchMusic(keywords string, limit, offset int) (result *SearchResultResponse, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - // keywords = url.QueryEscape(keywords) - - // 创建JSON请求体 - requestData := map[string]interface{}{ - "keywords": keywords, - "limit": limit, - "offset": offset, - } - url := fmt.Sprintf("%s/api/music/music_search", this.options.ApiBaseUrl) - - jsonData, err := json.Marshal(requestData) - if err != nil { - return - } - - req, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - return - } - // 设置请求头 - req.Header.Set("Content-Type", "application/json") - // 执行请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - } - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - // 输出响应内容 - // fmt.Println(string(body)) - result = &SearchResultResponse{} - err = json.Unmarshal(body, result) - return -} - -// 音乐连接获取 -func (this *Music) MusicUrl(id int64) (result *UrlResultResponse, err error) { - var ( - req *http.Request - resp *http.Response - body []byte - ) - // 创建JSON请求体 - requestData := map[string]interface{}{ - "id": id, - } - jsonData, err := json.Marshal(requestData) - if err != nil { - return - } - url := fmt.Sprintf("%s/api/music/music_url", this.options.ApiBaseUrl) - req, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - return - } - - // 设置请求头 - req.Header.Set("Content-Type", "application/json") - // 发起请求 - client := &http.Client{} - resp, err = client.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - - // 读取响应内容 - body, err = io.ReadAll(resp.Body) - if err != nil { - return - } - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("StatusCode:%d", resp.StatusCode) - return - } - // 打印结果 - fmt.Println(string(body)) - result = &UrlResultResponse{} - err = json.Unmarshal(body, result) - return -} diff --git a/apps/services/sys/musicobj/options.go b/apps/services/sys/musicobj/options.go deleted file mode 100644 index 51cba7b0..00000000 --- a/apps/services/sys/musicobj/options.go +++ /dev/null @@ -1,40 +0,0 @@ -package musicobj - -import ( - "yunyan/lego/utils/mapstructure" -) - -type Option func(*Options) -type Options struct { - ApiBaseUrl string - Cookie string -} - -func SetApiBaseUrl(v string) Option { - return func(o *Options) { - o.ApiBaseUrl = v - } -} -func SetCookie(v string) Option { - return func(o *Options) { - o.Cookie = v - } -} -func newOptions(config map[string]interface{}, opts ...Option) Options { - options := Options{} - if config != nil { - mapstructure.Decode(config, &options) - } - for _, o := range opts { - o(&options) - } - return options -} - -func newOptionsByOption(opts ...Option) Options { - options := Options{} - for _, o := range opts { - o(&options) - } - return options -} diff --git a/apps/services/sys/musicobj/sys_test.go b/apps/services/sys/musicobj/sys_test.go deleted file mode 100644 index f4d44de8..00000000 --- a/apps/services/sys/musicobj/sys_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package musicobj_test - -import ( - music "yunyan/sys/musicobj" - "fmt" - "testing" -) - -// http://www.deapsound.com:3000/search?keywords=%E7%AC%A8%E5%B0%8F%E5%AD%A9 -func Test_Sys_SearchMusic(t *testing.T) { - if sys, err := music.NewSys( - music.SetApiBaseUrl("https://hw.music.voitrans.net"), - ); err != nil { - fmt.Printf("Sys Init err:%v", err) - } else { - var ( - searchResultResponse *music.SearchResultResponse - ) - if searchResultResponse, err = sys.SearchMusic("童年", 10, 0); err == nil { - - } - if err != nil { - fmt.Printf("播放音乐失败, err:%v", err) - return - } else { - fmt.Printf("播放音乐成功 searchResultResponse:%+v", searchResultResponse) - } - } -} diff --git a/apps/services/sys/pay/alipay/sys_test.go b/apps/services/sys/pay/alipay/sys_test.go index 66c4fb6e..90353337 100644 --- a/apps/services/sys/pay/alipay/sys_test.go +++ b/apps/services/sys/pay/alipay/sys_test.go @@ -33,7 +33,7 @@ func Test_Sys(t *testing.T) { } else { orderid := sys.GenerateOrderNo("APP") fmt.Println(orderid) - results, err := sys.CreateAppOrder(context.TODO(), orderid, 100, "vip购买", "https://api.ideapsound.com/api/heom/pay_wechatnotify") + results, err := sys.CreateAppOrder(context.TODO(), orderid, 100, "vip购买", "https://pay.ymaikj.com/api/home/pay_alipaynotify") if err != nil { t.Fatalf("CreateAppOrder 失败: %v", err) } diff --git a/apps/services/sys/pay/appleiap/sys_test.go b/apps/services/sys/pay/appleiap/sys_test.go index d4df8ff8..d9418ff1 100644 --- a/apps/services/sys/pay/appleiap/sys_test.go +++ b/apps/services/sys/pay/appleiap/sys_test.go @@ -8,7 +8,7 @@ import ( func TestNewSys(t *testing.T) { sys, err := appleiap.NewSys( - appleiap.SetAppStoreBundleID("com.saitong.voitrans"), + appleiap.SetAppStoreBundleID("com.yimai.eaimar"), appleiap.SetAppStoreIssuerID("96c4b3eb-5e5c-4719-8b07-db9cc4dc1f89"), appleiap.SetAppStoreKeyID("VS538D3ZR5"), appleiap.SetAppStorePrivateKeyPEM(`-----BEGIN PRIVATE KEY----- diff --git a/apps/services/sys/pay/appleiap/token_test.go b/apps/services/sys/pay/appleiap/token_test.go index a2d2038e..c938db42 100644 --- a/apps/services/sys/pay/appleiap/token_test.go +++ b/apps/services/sys/pay/appleiap/token_test.go @@ -29,7 +29,7 @@ func TestServerAPICredentials(t *testing.T) { iss := "96c4b3eb-5e5c-4719-8b07-db9cc4dc1f89" kid := "VS538D3ZR5" // StoreKit 要求 JWT 载荷包含 bid(bundleId),与交易所属 App 一致 - bundleID := "com.saitong.voitrans" + bundleID := "com.yimai.eaimar" // 方式A:把 .p8 全文粘贴到下面的变量(包含完整头尾与换行) pem := `` diff --git a/apps/services/sys/pay/googleiap/sys_test.go b/apps/services/sys/pay/googleiap/sys_test.go index c15e3a9b..d99ad5c1 100644 --- a/apps/services/sys/pay/googleiap/sys_test.go +++ b/apps/services/sys/pay/googleiap/sys_test.go @@ -26,7 +26,7 @@ func TestNewSys(t *testing.T) { t.Errorf("NewSys() error = %v", err) return } - ok, err := sys.VerifyProductPurchase(context.Background(), "com.saitong.voitrans", "ai_001", "hghmaneppaommckcmhaacfee.AO-J1OwkyeKZug3-alqW6GUnAJf0fNrrnxYcCJFdZ_0YtG_iUY4U5I50cRc-M3VqvoMnYw_qVO8A51kcrwML0mrgzqGtYlo5lQ") + ok, err := sys.VerifyProductPurchase(context.Background(), "com.yimai.eaimar", "ai_001", "hghmaneppaommckcmhaacfee.AO-J1OwkyeKZug3-alqW6GUnAJf0fNrrnxYcCJFdZ_0YtG_iUY4U5I50cRc-M3VqvoMnYw_qVO8A51kcrwML0mrgzqGtYlo5lQ") if err != nil { t.Errorf("VerifyProductPurchase() error = %v", err) return diff --git a/apps/services/sys/pay/wechat/sys_test.go b/apps/services/sys/pay/wechat/sys_test.go index aec2ba50..d563168d 100644 --- a/apps/services/sys/pay/wechat/sys_test.go +++ b/apps/services/sys/pay/wechat/sys_test.go @@ -20,7 +20,7 @@ func Test_Sys(t *testing.T) { } else { orderid := sys.GenerateOrderNo("APP") fmt.Println(orderid) - results, err := sys.CreateAppOrder(context.TODO(), orderid, 100, "vip购买", "https://api.ideapsound.com/api/heom/pay_wechatnotify") + results, err := sys.CreateAppOrder(context.TODO(), orderid, 100, "vip购买", "https://pay.ymaikj.com/api/home/pay_wechatnotify") fmt.Println(results, err) } } diff --git a/apps/services/sys/tencentyun/cos/core.go b/apps/services/sys/tencentyun/cos/core.go deleted file mode 100644 index 27f22c63..00000000 --- a/apps/services/sys/tencentyun/cos/core.go +++ /dev/null @@ -1,55 +0,0 @@ -package cos - -import ( - "io" - - "github.com/tencentyun/cos-go-sdk-v5" - sts "github.com/tencentyun/qcloud-cos-sts-sdk/go" -) - -type ( - ISys interface { - Sts() (result *sts.CredentialResult, err error) - Get(name string) (resp *cos.Response, err error) - Put(name string, r io.Reader) (publicURL string, err error) - Delete(name string) (err error) - GetFileUrl(name string) (publicURL string) - } -) - -var defsys ISys -var defopt Options - -func OnInit(config map[string]interface{}, option ...Option) (err error) { - defopt = newOptions(config, option...) - defsys, err = newSys(defopt) - return -} - -func GetConfig() Options { - return defopt -} - -func NewSys(option ...Option) (sys ISys, err error) { - sys, err = newSys(newOptionsByOption(option...)) - return -} - -func Sts() (result *sts.CredentialResult, err error) { - return defsys.Sts() -} - -func Get(name string) (resp *cos.Response, err error) { - return defsys.Get(name) -} -func Put(name string, r io.Reader) (publicURL string, err error) { - return defsys.Put(name, r) -} - -func Delete(name string) (err error) { - return defsys.Delete(name) -} - -func GetFileUrl(name string) (publicURL string) { - return defsys.GetFileUrl(name) -} diff --git a/apps/services/sys/tencentyun/cos/cos.go b/apps/services/sys/tencentyun/cos/cos.go deleted file mode 100644 index 2fb62762..00000000 --- a/apps/services/sys/tencentyun/cos/cos.go +++ /dev/null @@ -1,100 +0,0 @@ -package cos - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/tencentyun/cos-go-sdk-v5" - sts "github.com/tencentyun/qcloud-cos-sts-sdk/go" -) - -func newSys(options Options) (sys *COS, err error) { - sys = &COS{ - options: options, - } - bucketURL, _ := url.Parse(options.BucketURL) - baseURL := &cos.BaseURL{BucketURL: bucketURL} - // 创建客户端,使用永久密钥 - sys.client = cos.NewClient(baseURL, &http.Client{ - Transport: &cos.AuthorizationTransport{ - SecretID: options.SecretID, // 腾讯云密钥 SecretId - SecretKey: options.SecretKey, // 腾讯云密钥 SecretKey - }, - }) - return -} - -type COS struct { - options Options - client *cos.Client -} - -// 临时访问 -func (this *COS) Sts() (result *sts.CredentialResult, err error) { - // STS 配置 - client := sts.NewClient(this.options.SecretID, this.options.SecretKey, nil) - - // 定义权限策略 - policy := &sts.CredentialPolicy{ - Version: "2.0", - Statement: []sts.CredentialPolicyStatement{ - { - Effect: "allow", - Action: []string{ - "name/cos:GetObject", - "name/cos:PutObject", - "name/cos:ListBucket", - }, - Resource: []string{ - fmt.Sprintf("qcs::cos:%s:uid/%s:%s", this.options.Region, this.options.AppId, this.options.BucketName), - fmt.Sprintf("qcs::cos:%s:uid/%s:%s/*", this.options.Region, this.options.AppId, this.options.BucketName), - }, - }, - }, - } - // 设置临时密钥参数 - opt := &sts.CredentialOptions{ - DurationSeconds: int64(time.Hour.Seconds()), // 有效期:1小时 - Region: this.options.Region, // 替换为你的存储桶所在地域 - Policy: policy, - } - // 获取临时密钥 - result, err = client.GetCredential(opt) - if err != nil { - return - } - return -} - -func (this *COS) Get(name string) (resp *cos.Response, err error) { - resp, err = this.client.Object.Get(context.Background(), name, nil) - return -} -func (this *COS) GetFileUrl(name string) (publicURL string) { - if this.options.DomainName == "" { - publicURL = fmt.Sprintf("https://%s.cos.%s.myqcloud.com/%s", this.options.BucketName, this.options.Region, name) - } else { - publicURL = fmt.Sprintf("https://%s/%s", this.options.DomainName, name) - } - - return -} - -func (this *COS) Put(name string, r io.Reader) (publicURL string, err error) { - _, err = this.client.Object.Put(context.Background(), name, r, nil) - if this.options.DomainName == "" { - publicURL = fmt.Sprintf("https://%s.cos.%s.myqcloud.com/%s", this.options.BucketName, this.options.Region, name) - } else { - publicURL = fmt.Sprintf("https://%s/%s", this.options.DomainName, name) - } - return -} - -func (this *COS) Delete(name string) (err error) { - _, err = this.client.Object.Delete(context.Background(), name, nil) - return -} diff --git a/apps/services/sys/tencentyun/cos/options.go b/apps/services/sys/tencentyun/cos/options.go deleted file mode 100644 index 01aa7c77..00000000 --- a/apps/services/sys/tencentyun/cos/options.go +++ /dev/null @@ -1,80 +0,0 @@ -package cos - -import ( - "yunyan/lego/sys/log" - "yunyan/lego/utils/mapstructure" -) - -type Option func(*Options) -type Options struct { - Debug bool //日志是否开启 - Log log.ILogger - SecretID string - SecretKey string - Region string - AppId string - BucketName string - BucketURL string - DomainName string // 自定义域名 -} - -func SetSecretID(v string) Option { - return func(o *Options) { - o.SecretID = v - } -} -func SetSecretKey(v string) Option { - return func(o *Options) { - o.SecretKey = v - } -} -func SetRegion(v string) Option { - return func(o *Options) { - o.Region = v - } -} -func SetAppId(v string) Option { - return func(o *Options) { - o.AppId = v - } -} -func SetBucketName(v string) Option { - return func(o *Options) { - o.BucketName = v - } -} -func SetBucketURL(v string) Option { - return func(o *Options) { - o.BucketURL = v - } -} -func SetDomainName(v string) Option { - return func(o *Options) { - o.DomainName = v - } -} - -func newOptions(config map[string]interface{}, opts ...Option) Options { - options := Options{} - if config != nil { - mapstructure.Decode(config, &options) - } - for _, o := range opts { - o(&options) - } - if options.Log == nil { - options.Log = log.NewTurnlog(options.Debug, log.Clone("sys.tavily", 3)) - } - return options -} - -func newOptionsByOption(opts ...Option) Options { - options := Options{} - for _, o := range opts { - o(&options) - } - if options.Log == nil { - options.Log = log.NewTurnlog(options.Debug, log.Clone("sys.tavily", 3)) - } - return options -} diff --git a/apps/services/sys/tencentyun/cos/sys_test.go b/apps/services/sys/tencentyun/cos/sys_test.go deleted file mode 100644 index c60ae3a1..00000000 --- a/apps/services/sys/tencentyun/cos/sys_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package cos_test - -import ( - "context" - lgcos "yunyan/sys/tencentyun/cos" - "fmt" - "io" - "log" - "net/http" - "net/url" - "strings" - "testing" - "time" - - "github.com/tencentyun/cos-go-sdk-v5" - sts "github.com/tencentyun/qcloud-cos-sts-sdk/go" -) - -func Test_Sys(t *testing.T) { - if err := lgcos.OnInit(nil, - lgcos.SetSecretID("AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA"), - lgcos.SetSecretKey("OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc"), - lgcos.SetAppId("1253517901"), - lgcos.SetRegion("ap-guangzhou"), - lgcos.SetBucketName("deepsound-1253517901"), - lgcos.SetBucketURL("https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com"), - lgcos.SetDomainName("app.deepsoud.com"), - ); err != nil { - return - } else { - // var file *os.File - // defer file.Close() - // if file, err = os.Open("./DeapSound_0619.apk"); err != nil { - // fmt.Println("打开文件失败!", err) - // return - // } - results := lgcos.GetFileUrl("app/DeapSound_0619.apk") - fmt.Printf("results:%s", results) - } -} - -func Test_Sys_Sts(t *testing.T) { - if err := lgcos.OnInit(nil, - lgcos.SetSecretID("AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA"), - lgcos.SetSecretKey("OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc"), - lgcos.SetAppId("1253517901"), - lgcos.SetRegion("ap-guangzhou"), - lgcos.SetBucketName("deepsound-1253517901"), - lgcos.SetBucketURL("https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com"), - ); err != nil { - return - } else { - results, err := lgcos.Sts() - // fmt.Printf("TmpSecretId:%s TmpSecretKey:%s SessionToken:%s err:%v", results.Credentials.TmpSecretID, results.Credentials.TmpSecretKey, results.Credentials.SessionToken, err) - - // 你的 COS 存储桶信息 - bucketURL := "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com" - - // 通过 STS 获取的临时凭证 - tmpSecretID := results.Credentials.TmpSecretID - tmpSecretKey := results.Credentials.TmpSecretKey - sessionToken := results.Credentials.SessionToken - - // 初始化 COS 客户端 - u, _ := url.Parse(bucketURL) - b := &cos.BaseURL{BucketURL: u} - - client := cos.NewClient(b, &http.Client{ - Transport: &cos.AuthorizationTransport{ - SecretID: tmpSecretID, - SecretKey: tmpSecretKey, - SessionToken: sessionToken, // 关键部分,必须加上 SessionToken - }, - }) - // objectKey := "test.txt" - // content := "Hello, COS with STS!" - // _, err = client.Object.Put(context.Background(), objectKey, - // strings.NewReader(content), nil) - // if err != nil { - // log.Fatalf("上传对象失败: %v", err) - // } - // fmt.Printf("成功上传对象: %s\n", objectKey) - - // 测试下载对象 - // resp, err := client.Object.Get(context.Background(), objectKey, nil) - // if err != nil { - // log.Fatalf("下载对象失败: %v", err) - // } - // defer resp.Body.Close() - // body, err := io.ReadAll(resp.Body) - // if err != nil { - // log.Fatalf("读取对象内容失败: %v", err) - // } - // fmt.Printf("下载对象内容: %s\n", string(body)) - // 示例:列出存储桶中的对象 - result, _, err := client.Bucket.Get(context.Background(), nil) - if err != nil { - fmt.Println("请求失败:", err) - return - } - - for _, v := range result.Contents { - fmt.Println("文件名:", v.Key) - } - } -} - -func Test_Sys_Sts1(t *testing.T) { - // 替换为您的永久密钥 - secretID := "AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA" - secretKey := "OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc" - - // 配置存储桶信息 - appID := "1253517901" - bucket := "deepsound-1253517901" - region := "ap-guangzhou" - bucketURL := fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region) - - // 创建 STS 客户端 - client := sts.NewClient(secretID, secretKey, nil) - - // 配置策略 - policy := &sts.CredentialPolicy{ - Version: "2.0", - Statement: []sts.CredentialPolicyStatement{ - { - Effect: "allow", - Action: []string{ - "name/cos:GetObject", - "name/cos:PutObject", - "name/cos:ListBucket", - }, - Resource: []string{ - fmt.Sprintf("qcs::cos:%s:uid/%s:%s", region, appID, bucket), - fmt.Sprintf("qcs::cos:%s:uid/%s:%s/*", region, appID, bucket), - }, - }, - }, - } - - // 配置临时密钥参数 - opt := &sts.CredentialOptions{ - DurationSeconds: int64(time.Hour.Seconds()), // 有效期:1小时 - Region: region, - Policy: policy, - } - - // 获取临时密钥 - result, err := client.GetCredential(opt) - if err != nil { - log.Fatalf("获取临时密钥失败: %v", err) - } - - fmt.Printf("临时密钥信息:\n") - fmt.Printf("TmpSecretId: %s\n", result.Credentials.TmpSecretID) - fmt.Printf("TmpSecretKey: %s\n", result.Credentials.TmpSecretKey) - fmt.Printf("SessionToken: %s\n", result.Credentials.SessionToken) - fmt.Printf("开始时间: %d\n", result.StartTime) - fmt.Printf("过期时间: %d\n", result.ExpiredTime) - - // 使用临时密钥配置 COS 客户端 - u, _ := url.Parse(bucketURL) - b := &cos.BaseURL{BucketURL: u} - cosClient := cos.NewClient(b, &http.Client{ - Transport: &cos.AuthorizationTransport{ - SecretID: result.Credentials.TmpSecretID, - SecretKey: result.Credentials.TmpSecretKey, - SessionToken: result.Credentials.SessionToken, - }, - }) - - // 测试上传对象 - objectKey := "test.txt" - content := "Hello, COS with STS!" - _, err = cosClient.Object.Put(context.Background(), objectKey, - strings.NewReader(content), nil) - if err != nil { - log.Fatalf("上传对象失败: %v", err) - } - fmt.Printf("成功上传对象: %s\n", objectKey) - - // 测试下载对象 - resp, err := cosClient.Object.Get(context.Background(), objectKey, nil) - if err != nil { - log.Fatalf("下载对象失败: %v", err) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Fatalf("读取对象内容失败: %v", err) - } - fmt.Printf("下载对象内容: %s\n", string(body)) -} diff --git a/build.sh b/build.sh index 4785b9ef..627a82d7 100755 --- a/build.sh +++ b/build.sh @@ -12,7 +12,7 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SERVICE="${1:-}" [ -n "$SERVICE" ] || { echo "用法: ./build.sh "; exit 1; } -REGISTRY="${REGISTRY:-registry.voitrans.net}" +REGISTRY="${REGISTRY:-registry.ymaikj.com}" TAG="${TAG:-latest}" PLATFORM="${PLATFORM:-linux/amd64}" PUSH="${PUSH:-1}" diff --git a/deploy/README.md b/deploy/README.md index ceecc5ce..be2a6dde 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -14,11 +14,11 @@ cd deploy 环境:目前只有 `along`(阿龙),与各服务 `dev-deploy.sh` 的 `ENVS` 同名,脚本启动时会交叉校验, 一边加了新环境另一边忘了加会直接报错。 -> ⚠️ **2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本仓库的脚本中移除。** -> 移除的只是「往哪台服务器部署」——这些环境的 SSH 私钥路径指向的是另一台开发机,本机没有钥匙, -> 留着只会在菜单里制造误选。**镜像仓库没有动**:`prod-build.sh` 仍可选 `yunyan` / `lingpu` -> 把镜像推到 `registry.voitrans.net` / `registry.lingpu.net`。 -> 要恢复部署目标,从 git 历史里取回对应的 `env_profile` / `region_profile` 段即可(含 IP 与私钥路径)。 +> ⚠️ **本仓库只剩阿龙一家,服务器与镜像仓库都是。** +> 2026-09-13 先移除了云雁/灵谱的部署目标(SSH 私钥指向另一台开发机,本机没有钥匙, +> 留着只会在菜单里制造误选);2026-09-23 又删掉了它们的镜像仓库档案 +> (`registry.voitrans.net` / `registry.lingpu.net`),`prod-build.sh` 现在只能推阿龙仓库。 +> 要恢复,从 git 历史里取回 `env_profile` / `region_profile` 段与 `registry_profile` 的对应 case。 它只是按顺序调各服务自己的 `deploy/<服务>/dev-deploy.sh <环境>` 并汇总结果,部署规则仍然 只有一处;单独发某个服务照旧进服务目录跑 `./dev-deploy.sh <环境>`。服务器上还没有真实 `.env` diff --git a/deploy/admin/README.md b/deploy/admin/README.md index 0abf14b7..4b480a92 100644 --- a/deploy/admin/README.md +++ b/deploy/admin/README.md @@ -46,9 +46,8 @@ docker compose up -d # 本机起 starpivot-admin ```bash cd deploy/admin ./dev-deploy.sh along # 开发:发布到阿龙测试环境(构建 dev-latest → 推送 → 远端拉取重启) -./dev-deploy.sh yunyan # 开发:发布到云雁测试环境 ./dev-deploy.sh # 不带参数 → 交互选择测试环境 -./prod-build.sh 0.1.1 yunyan # 生产①:构建并推送 starpivot-admin:0.1.1 (+latest);第2参数选公司 +./prod-build.sh 0.1.1 along # 生产①:构建并推送 starpivot-admin:0.1.1 (+latest);公司只剩 along,可省略 ./prod-deploy.sh 0.2.1 along # 生产②:部署 0.2.1 到生产环境(目前只有 along,省略即用它) ./prod-deploy.sh rollback along # 生产回滚到上一版本(与当前版本来回切换) ``` @@ -60,7 +59,7 @@ cd deploy/admin - **部署目标与凭据**:写死在各脚本顶部。`dev-deploy.sh` 的 `env_profile()`(每个测试环境一套 `REGISTRY/DEV_HOST/DEV_KEY/DEV_DIR`,**目前只有 `along`**;新增环境加一段 case 并写进 `ENVS`)、`prod-deploy.sh` 的 `env_profile()`(每个生产环境一套 `REGISTRY/IMAGE_NAME/DEPLOY_HOST/DEPLOY_KEY/DEPLOY_DIR`,**目前只有 `along`**;新增环境加一段 case 并写进 `PROD_ENVS`); 仓库账号 `admin` / 密码已写死;`DEPLOY_KEY` 默认指向阿龙那把钥匙,可用环境变量覆盖:`DEPLOY_KEY=~/Documents/keys/loginscre.pem ./prod-deploy.sh …`。 - - 2026-09-13 移除了云雁/灵谱的部署目标(私钥在另一台开发机上,本机连不上);**镜像仓库未动**,`prod-build.sh` 仍可选这两家推镜像。 + - 2026-09-13 移除了云雁/灵谱的部署目标(私钥在另一台开发机上,本机连不上);2026-09-23 又删掉了它们的镜像仓库档案,现在只能推阿龙仓库。 - ⚠️ 本目录脚本纳入 git,**公开仓库前务必清空** `*_HOST` 与 `REGISTRY_PASS`。 - **后端地址**:`.env` 的 `CONSOLE_BACKEND`,须指向同网络的 console 容器,默认 `http://starpivot-console:8080`。 diff --git a/deploy/admin/dev-deploy.sh b/deploy/admin/dev-deploy.sh index 8e404e1a..b652cc47 100755 --- a/deploy/admin/dev-deploy.sh +++ b/deploy/admin/dev-deploy.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # admin 开发环境:构建开发镜像 → 推送 → 远端拉取重启。 # -# Nuxt 管理前端打成镜像 yunyan-admin,推到私有仓库后远端拉取重启。 +# Nuxt 管理前端打成镜像 starpivot-admin,推到私有仓库后远端拉取重启。 # 仅做镜像部署:服务器上的 docker-compose.yml / .env 由你自行维护 # (照着仓库里的 env/env.example 在服务器上放好真实 .env),本脚本不上传任何真实配置。 # @@ -11,8 +11,8 @@ # # 开发版镜像始终使用固定 tag dev-latest,每次构建覆盖同一 tag。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/admin @@ -35,7 +35,7 @@ IMAGE_NAME="starpivot-admin" # 业务镜像名 # ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║ # ║ 新增环境:在 env_profile 加一段 case,并把环境名加进 ENVS 数组。 ║ # ╚══════════════════════════════════════════════════════════════════════╝ -ENVS=(along) # 可选测试环境(目前只剩阿龙;云雁/灵谱已于 2026-09-13 移除,见文件头说明) +ENVS=(along) # 可选测试环境(目前只剩阿龙,见文件头说明) DEFAULT_ENV="along" # 无参数且无 TTY(CI/管道)时的默认环境 env_profile() { diff --git a/deploy/admin/docker-compose.yml b/deploy/admin/docker-compose.yml index e3736727..f41e49d1 100644 --- a/deploy/admin/docker-compose.yml +++ b/deploy/admin/docker-compose.yml @@ -1,13 +1,13 @@ # admin 服务部署(单服务) # -# 只部署 yunyan-admin(Nuxt 管理前端)。它通过 CONSOLE_BACKEND 连接同网络的 console 服务。 +# 只部署 starpivot-admin(Nuxt 管理前端)。它通过 CONSOLE_BACKEND 连接同网络的 console 服务。 # 依赖服务由目标环境共享提供,不在此 compose。 # # 不直接 docker compose 调用,统一通过 dev-deploy.sh / prod-deploy.sh(与 console 一致)。 services: starpivot-admin: - image: ${REGISTRY:-registry.voitrans.net}/starpivot-admin:${TAG:-latest} + image: ${REGISTRY:-registry.ymaikj.com}/starpivot-admin:${TAG:-latest} container_name: starpivot-admin restart: unless-stopped platform: ${PLATFORM:-linux/amd64} diff --git a/deploy/admin/prod-build.sh b/deploy/admin/prod-build.sh index de446399..e2b4ac56 100755 --- a/deploy/admin/prod-build.sh +++ b/deploy/admin/prod-build.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # admin 生产环境:仅「构建带版本号的镜像并推送」,不部署。 # -# Nuxt 管理前端打成镜像 yunyan-admin,同时推送 : 与 :latest 两个 tag。 +# Nuxt 管理前端打成镜像 starpivot-admin,同时推送 : 与 :latest 两个 tag。 # 部署用 ./prod-deploy.sh 。 # # 用法(在 deploy/admin/ 下): # ./prod-build.sh # version 必须形如 X.Y.Z,如 0.1.1 -# ./prod-build.sh [公司] # 公司 = yunyan | lingpu | along(省略则交互选择;无 TTY 用默认 yunyan) +# ./prod-build.sh [公司] # 公司目前只剩 along,省略即用它 set -euo pipefail SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/deploy/admin/prod-deploy.sh b/deploy/admin/prod-deploy.sh index 72db6623..2e0bac25 100755 --- a/deploy/admin/prod-deploy.sh +++ b/deploy/admin/prod-deploy.sh @@ -11,8 +11,8 @@ # 环境 = along(阿龙 47.116.104.181);省略 → 直接用它。 # ⚠️ 环境必须与 prod-build.sh 构建时所选公司一致,否则拉不到镜像。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -35,7 +35,7 @@ DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务 # 容器名:必须与 docker-compose.yml 的 container_name 一字不差(本脚本会上传覆盖那份 compose)。 # 仅用于 up 冲突时清理同名遗留容器;不随环境变化。 CONTAINER_NAME="starpivot-admin" -PROD_ENVS=(along) # 可选生产环境(目前只剩阿龙;云雁/灵谱已于 2026-09-13 移除,见文件头说明) +PROD_ENVS=(along) # 可选生产环境(目前只剩阿龙,见文件头说明) DEFAULT_PROD_ENV="along" # 无参数且无 TTY(CI/管道)时的默认环境 env_profile() { diff --git a/deploy/app/README.md b/deploy/app/README.md index d08f030f..f11210b3 100644 --- a/deploy/app/README.md +++ b/deploy/app/README.md @@ -38,7 +38,7 @@ dev 部署目录默认 `/home/work/starpivot/app`;prod 按**应用**取自 `prod-deploy.sh` 的 `APPS` 表(如 `ym` → `/home/work/ym-a11`)。 -> 2026-09-13:prod 的可选区域只剩 `along`(云雁的 shanghai/japan/singapore 已移除,私钥在另一台开发机上)。 +> prod 的可选区域只剩 `along`(其余几地已于 2026-09-13 移除,私钥在另一台开发机上)。 ### 二、部署 @@ -47,7 +47,6 @@ dev 部署目录默认 `/home/work/starpivot/app`;prod 按**应用**取自 `pr ```bash cd deploy/voitrans ./dev-deploy.sh along # 发布到阿龙测试环境(构建 dev-latest → 推送 → 远端拉取重启) -./dev-deploy.sh yunyan # 发布到云雁测试环境 ./dev-deploy.sh # 不带参数 → 交互选择测试环境 ``` diff --git a/deploy/app/confs/api.yaml.example b/deploy/app/confs/api.yaml.example index 151a87b9..5c896fb3 100644 --- a/deploy/app/confs/api.yaml.example +++ b/deploy/app/confs/api.yaml.example @@ -23,14 +23,7 @@ sys: Addr: ["${REDIS_ADDR:-redis:6379}"] Password: ${REDIS_PASSWORD:-} DB: ${REDIS_DB:-1} - KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans} - cos: - SecretID: "" - SecretKey: "" - AppId: "1253517901" - Region: ap-guangzhou - BucketName: "deepsound-1253517901" - BucketURL: "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com" + KeyPrefix: ${REDIS_KEY_PREFIX:-EAIMAR} translate: #Azure Translator,按需填写 Key: "${AZURE_TRANSLATE_KEY:-}" Region: "eastasia" diff --git a/deploy/app/confs/home.yaml.example b/deploy/app/confs/home.yaml.example index 457f152a..0fa9cfaa 100644 --- a/deploy/app/confs/home.yaml.example +++ b/deploy/app/confs/home.yaml.example @@ -52,7 +52,7 @@ sys: Addr: ["${REDIS_ADDR:-redis:6379}"] Password: ${REDIS_PASSWORD:-} DB: ${REDIS_DB:-1} - KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans} #key 应用分组前缀 + KeyPrefix: ${REDIS_KEY_PREFIX:-EAIMAR} #key 应用分组前缀 sms: #短信服务(服务商 tencent | aliyun,留空按 tencent;也可在 console 后台「业务功能配置」里改,改完热生效) Provider: "tencent" #腾讯云 @@ -89,15 +89,6 @@ sys: wechat_auth: AppID: wx4b5ec8957ad5e425 AppSecret: "" - musicobj: - ApiBaseUrl: https://hw.music.voitrans.net - cos: - SecretID: "" - SecretKey: "" - AppId: "1253517901" - Region: ap-guangzhou - BucketName: "deepsound-1253517901" - BucketURL: "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com" wechatpay: #微信支付,需放置 ./apiclient_key.pem # 键名必须与 comm.ModuleCatalog 里 wechatpay 的 Key 逐字符一致(AppID/MchID,结尾大写 ID), # 否则 seedModuleRows 的 sec[f.Key] 查不到,这两个字段永远不入库、后台显示为空。 @@ -119,7 +110,11 @@ sys: AppStoreIssuerID: "" AppStoreKeyID: "" AppStorePrivateKeyPEM: "" - AppStoreBundleID: com.saitong.voitrans + # ⚠️ 必须与 iOS 工程的 PRODUCT_BUNDLE_IDENTIFIER 逐字相同:appleiap 拿它做两件事—— + # 签 App Store Server API 的 JWT(载荷 bid)、以及校验交易归属(info.BundleID 不等就拒)。 + # 填错的表现是**所有 iOS 内购校验都失败**,报「交易归属的 bundleId 不匹配」。 + # 2026-09-23:原值是上一代品牌的 bundle id,与 iOS 工程对不上,已改正。 + AppStoreBundleID: com.yimai.eaimar UseSandbox: true googleiap: ServiceAccountJSON: "" @@ -143,7 +138,6 @@ modules: Timezone: "Asia/Shanghai" user: Debug: ${LOG_DEBUG:-false} - AvatarUrl: "https://api.ideapsound.com/avatar/" TokenKey: "${GATEWAY_TOKEN_KEY:-}" # ⚠️ 签发 MCP 会话令牌用,必须与 mcp.yaml 的 McpTokenKey 同值 # (两边都读 .env 的 MCP_TOKEN_KEY)。留空则 user_getmcptoken 直接报错, diff --git a/deploy/app/confs/mcp.yaml.example b/deploy/app/confs/mcp.yaml.example index 7eab63a3..415754ef 100644 --- a/deploy/app/confs/mcp.yaml.example +++ b/deploy/app/confs/mcp.yaml.example @@ -25,8 +25,6 @@ sys: ClientID: "" ClientSecret: "" RedirectURI: "http://localhost:8080/callback" - musicobj: - ApiBaseUrl: https://hw.music.ideapsound.com bravesearch: ApiKey: "" diff --git a/deploy/app/confs/timer.yaml.example b/deploy/app/confs/timer.yaml.example index 9abd97b2..2ec2afe2 100644 --- a/deploy/app/confs/timer.yaml.example +++ b/deploy/app/confs/timer.yaml.example @@ -18,7 +18,7 @@ sys: Addr: ["${REDIS_ADDR:-redis:6379}"] Password: ${REDIS_PASSWORD:-} DB: ${REDIS_DB:-1} - KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans} + KeyPrefix: ${REDIS_KEY_PREFIX:-EAIMAR} modules: timer: diff --git a/deploy/app/dev-deploy.sh b/deploy/app/dev-deploy.sh index 30b47096..9f54b892 100755 --- a/deploy/app/dev-deploy.sh +++ b/deploy/app/dev-deploy.sh @@ -11,8 +11,8 @@ # # 参数只用于选环境;开发版镜像始终使用固定 tag dev-latest,每次构建覆盖同一 tag。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/app @@ -35,7 +35,7 @@ IMAGE_NAME="starpivot-app" # 业务单镜像名 # ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║ # ║ 新增环境:在 env_profile 加一段 case,并把环境名加进 ENVS 数组。 ║ # ╚══════════════════════════════════════════════════════════════════════╝ -ENVS=(along) # 可选测试环境(目前只剩阿龙;云雁/灵谱已于 2026-09-13 移除,见文件头说明) +ENVS=(along) # 可选测试环境(目前只剩阿龙,见文件头说明) DEFAULT_ENV="along" # 无参数且无 TTY(CI/管道)时的默认环境 env_profile() { diff --git a/deploy/app/docker-compose.yml b/deploy/app/docker-compose.yml index 1fe1070a..ffd8a74f 100644 --- a/deploy/app/docker-compose.yml +++ b/deploy/app/docker-compose.yml @@ -14,7 +14,7 @@ services: app: # 镜像名由部署脚本注入(IMAGE_NAME,来自 deploy/registry-profiles.sh 的 prod_image_name), # 这里的默认值只在手工 docker compose 时兜底——别在此写死,写死过一次就和脚本漂移了。 - image: ${REGISTRY:-registry.voitrans.net}/${IMAGE_NAME:-starpivot-app}:${TAG:-latest} + image: ${REGISTRY:-registry.ymaikj.com}/${IMAGE_NAME:-starpivot-app}:${TAG:-latest} # 容器名按「应用」区分:prod-deploy.sh 注入 CONTAINER_NAME=<应用>-a11(ym → ym-a11), # 并写回 .env,手工 docker compose 时也用同一个名字。默认值只给 dev / 未注入时兜底。 container_name: ${CONTAINER_NAME:-starpivot-app} diff --git a/deploy/app/prod-build.sh b/deploy/app/prod-build.sh index 46990543..64417fa2 100755 --- a/deploy/app/prod-build.sh +++ b/deploy/app/prod-build.sh @@ -6,7 +6,7 @@ # # 用法(在 deploy/voitrans/ 下): # ./prod-build.sh # 只要版本号(应用共用同一镜像,构建不分应用);version 必须形如 X.Y.Z,如 0.1.1 -# ./prod-build.sh [公司] # 公司 = yunyan | lingpu | along(省略则交互选择;无 TTY 用默认 yunyan) +# ./prod-build.sh [公司] # 公司目前只剩 along,省略即用它 set -euo pipefail SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/deploy/app/prod-deploy.sh b/deploy/app/prod-deploy.sh index 0cddeda3..b7b6be03 100755 --- a/deploy/app/prod-deploy.sh +++ b/deploy/app/prod-deploy.sh @@ -20,8 +20,8 @@ # 应用: 见 APPS(默认 voitrans) 区域: along # 镜像仓库跟区域走:along → 阿龙仓库 registry.ymaikj.com;需与 prod-build.sh 所选公司一致。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -37,7 +37,7 @@ yellow(){ printf "\033[33m%s\033[0m\n" "$*"; } # ── 私有仓库凭据(写死;公开仓库前清空 REGISTRY_PASS)── # 镜像仓库按「区域所属公司」自动选(见 region_profile 里的 REGION_COMPANY:along 走自己的 -# registry.ymaikj.com),也可用环境变量强制指定:COMPANY=lingpu ./prod-deploy.sh … +# registry.ymaikj.com)。COMPANY 环境变量仍可强制指定,但目前只有 along 一个合法值。 # 所选公司必须与 prod-build.sh 构建时所选公司一致,否则拉不到镜像。 COMPANY_OVERRIDE="${COMPANY:-}" # 环境变量显式指定的公司;留空 = 跟区域走 # 共用镜像:所有应用都用这一个。名字走 prod_image_name(),与 prod-build.sh 同源,防漂移。 @@ -82,7 +82,7 @@ DEFAULT_APP="${APPS[0]%%|*}" # 省略应用时的默认(首个) # ║ 写死在此(本文件纳入 git)。公开仓库前清空 HOST/KEY。 ║ # ║ 新增区域:在 region_profile 加一段 case,并把区域名加进 REGIONS 数组。 ║ # ╚══════════════════════════════════════════════════════════════════════╝ -REGIONS=(along) # 可选环境/区域(目前只剩阿龙;云雁三地已于 2026-09-13 移除,见文件头说明) +REGIONS=(along) # 可选环境/区域(目前只剩阿龙,见文件头说明) region_profile() { # 远端部署目录/容器名由「应用」决定(见 APPS),此处只管各区域服务器连接信息 REGION_COMPANY="$DEFAULT_COMPANY" # 该区域镜像仓库所属公司(可被环境变量 COMPANY 覆盖;along 段下面会改写成 along) @@ -146,7 +146,7 @@ for a in "$@"; do elif is_removed_region "$a"; then # ⚠️ 变量名必须写成 ${a} 而不是 $a:紧跟其后的是多字节的「」,在非 UTF-8 locale 下 # bash 会把它的首字节并进变量名,报出 `a?: unbound variable`(set -u)。 - red "✗ 区域「${a}」已于 2026-09-13 从本脚本移除(云雁三地的 SSH 私钥在另一台开发机上,本机连不上)。" + red "✗ 区域「${a}」已从本脚本移除(那几台的 SSH 私钥在另一台开发机上,本机连不上)。" red " 现可用区域: ${REGIONS[*]}。需要恢复请从 git 历史取回 region_profile 对应段。" exit 1 elif is_region "$a"; then diff --git a/deploy/console/README.md b/deploy/console/README.md index a4713fd5..34c6db37 100644 --- a/deploy/console/README.md +++ b/deploy/console/README.md @@ -52,9 +52,8 @@ docker compose --env-file .env up -d # 本机起 starpivot-console ```bash cd deploy/console ./dev-deploy.sh along # 开发:发布到阿龙测试环境(构建 dev-latest → 推送 → 远端拉取重启) -./dev-deploy.sh yunyan # 开发:发布到云雁测试环境 ./dev-deploy.sh # 不带参数 → 交互选择测试环境 -./prod-build.sh 0.1.1 yunyan # 生产①:构建并推送 starpivot-console:0.1.1 (+latest);第2参数选公司 +./prod-build.sh 0.1.1 along # 生产①:构建并推送 starpivot-console:0.1.1 (+latest);公司只剩 along,可省略 ./prod-deploy.sh 0.2.1 along # 生产②:部署 0.2.1 到生产环境(目前只有 along,省略即用它) ./prod-deploy.sh rollback along # 生产回滚到上一版本(与当前版本来回切换) ``` diff --git a/deploy/console/dev-deploy.sh b/deploy/console/dev-deploy.sh index 45840d35..cb54c561 100755 --- a/deploy/console/dev-deploy.sh +++ b/deploy/console/dev-deploy.sh @@ -11,8 +11,8 @@ # # 开发版镜像始终使用固定 tag dev-latest,每次构建覆盖同一 tag。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/console @@ -35,7 +35,7 @@ IMAGE_NAME="starpivot-console" # 业务镜像名 # ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║ # ║ 新增环境:在 env_profile 加一段 case,并把环境名加进 ENVS 数组。 ║ # ╚══════════════════════════════════════════════════════════════════════╝ -ENVS=(along) # 可选测试环境(目前只剩阿龙;云雁/灵谱已于 2026-09-13 移除,见文件头说明) +ENVS=(along) # 可选测试环境(目前只剩阿龙,见文件头说明) DEFAULT_ENV="along" # 无参数且无 TTY(CI/管道)时的默认环境 env_profile() { diff --git a/deploy/console/docker-compose.yml b/deploy/console/docker-compose.yml index 7f52d529..9a84f353 100644 --- a/deploy/console/docker-compose.yml +++ b/deploy/console/docker-compose.yml @@ -1,6 +1,6 @@ # console 服务部署(单服务) # -# 只部署 yunyan-console。依赖服务(PostgreSQL / Redis / NATS)由目标环境共享提供, +# 只部署 starpivot-console。依赖服务(PostgreSQL / Redis / NATS)由目标环境共享提供, # 不在此 compose 内搭建——在 .env 里用地址/容器名连接它们。 # # 不直接 docker compose 调用,统一通过 deploy.sh, @@ -8,7 +8,7 @@ services: starpivot-console: - image: ${REGISTRY:-registry.voitrans.net}/starpivot-console:${TAG:-latest} + image: ${REGISTRY:-registry.ymaikj.com}/starpivot-console:${TAG:-latest} container_name: starpivot-console restart: unless-stopped platform: ${PLATFORM:-linux/amd64} diff --git a/deploy/console/prod-build.sh b/deploy/console/prod-build.sh index c5ca5d28..4633d954 100755 --- a/deploy/console/prod-build.sh +++ b/deploy/console/prod-build.sh @@ -6,7 +6,7 @@ # # 用法(在 deploy/console/ 下): # ./prod-build.sh # version 必须形如 X.Y.Z,如 0.1.1 -# ./prod-build.sh [公司] # 公司 = yunyan | lingpu | along(省略则交互选择;无 TTY 用默认 yunyan) +# ./prod-build.sh [公司] # 公司目前只剩 along,省略即用它 set -euo pipefail SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/deploy/console/prod-deploy.sh b/deploy/console/prod-deploy.sh index 3d65e534..d1883132 100755 --- a/deploy/console/prod-deploy.sh +++ b/deploy/console/prod-deploy.sh @@ -11,8 +11,8 @@ # 环境 = along(阿龙 47.116.104.181);省略 → 直接用它。 # ⚠️ 环境必须与 prod-build.sh 构建时所选公司一致,否则拉不到镜像。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -35,7 +35,7 @@ DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务 # 容器名:必须与 docker-compose.yml 的 container_name 一字不差(本脚本会上传覆盖那份 compose)。 # 仅用于 up 冲突时清理同名遗留容器;不随环境变化。 CONTAINER_NAME="starpivot-console" -PROD_ENVS=(along) # 可选生产环境(目前只剩阿龙;云雁/灵谱已于 2026-09-13 移除,见文件头说明) +PROD_ENVS=(along) # 可选生产环境(目前只剩阿龙,见文件头说明) DEFAULT_PROD_ENV="along" # 无参数且无 TTY(CI/管道)时的默认环境 env_profile() { diff --git a/deploy/dev-deploy-all.sh b/deploy/dev-deploy-all.sh index 0b7b9a2e..764a5f12 100755 --- a/deploy/dev-deploy-all.sh +++ b/deploy/dev-deploy-all.sh @@ -16,8 +16,8 @@ # ⚠️ macOS 自带 bash 3.2 不认多字节字符边界,`"$rc)"` 会被当成变量名 `rc)` 而在 set -u 下 # 报 unbound variable。所以本文件里凡是变量后面紧跟中文标点的,一律写成 ${rc}。 set -euo pipefail -# ⚠️ 2026-09-13:云雁(yunyan)/灵谱(lingpu) 的部署目标已从本脚本移除——本仓库的部署目标只剩阿龙。 -# 移除的是「往哪台服务器部署」,**镜像仓库没动**:prod-build.sh 仍可选 yunyan/lingpu 推镜像。 +# ⚠️ 本仓库的部署目标只剩阿龙一家:2026-09-13 移除了云雁/灵谱的服务器,2026-09-23 连它们的 +# 镜像仓库档案(registry.voitrans.net / registry.lingpu.net)也一并删除。需要找回查 git 历史。 # 要恢复:git log 里找回对应的 env_profile/region_profile 段即可(含 IP 与各自的 SSH 私钥路径)。 DEPLOY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 仓库的 deploy/ diff --git a/deploy/dl/README.md b/deploy/dl/README.md new file mode 100644 index 00000000..8b6c2dc7 --- /dev/null +++ b/deploy/dl/README.md @@ -0,0 +1,209 @@ +# 下载落地页(dl.ymaikj.com) + +一个纯静态页,托管在阿里云 OSS 上,**不占服务器、不经 NPM**。 +安装包也放同一个桶里,页面按 UA 决定跳商店还是给 APK 直链。 + +``` +deploy/dl/ +├── index.template.html ← 源文件,改页面改这个 +├── build.sh ← 把 App 图标内联进去,生成 index.html +└── index.html ← 生成物,**上传的是它**(已入库,方便直接传) +``` + +**版本号、更新日志、各商店链接都不在页面里维护**,实时取自管理后台(见下文「三」)。 +图标取自 `apps/client/assets/images/app_icon.png`,与 App 用的是同一份素材。 + +--- + +## 一、为什么放 OSS 而不是放业务服务器 + +下载页一般要挂 APK 直链。微信、各家浏览器对「发 apk 的域名」判定很粗暴, +一旦被拦截或标记为风险,**同域名上的接口请求也会一起遭殃**。所以它必须与 +`ym.ymaikj.com`(业务网关)分开。 + +放 OSS 还有两个顺带好处:页面和安装包同源(没有跨域、没有防盗链问题), +以及不占业务机的带宽——一个版本几十 MB,几千次下载就是几百 GB。 + +--- + +## 二、部署步骤(首次) + +> ✅ 2026-09-23:`ymai-dl` 桶、`dl.ymaikj.com` 域名与证书**均已建好并实测可用** +> (证书有效期至 2027-03-23,404 路由正确,与 `ym.ymaikj.com` 的跨域也已实测通过)。 +> 下面三步留档,重建时照做。 + +### 1. 准备存储桶 + +**建议新建一个专用桶**,例如 `ymai-dl`(上海,与 `ymaioss` 同地域): + +- 读写权限:**公共读**(安装包本来就要公开下载) +- 不要复用 `ymaioss`。那个桶里是用户录音,把公开分发的东西混进去会让 + 权限、生命周期规则、流量账单都分不清谁是谁。 + +> 想省事复用 `ymaioss` 也能跑通,但页面要放在 `EAIMAR/dist/index.html` +> 这种子路径下,且 `oss.ymaikj.com` 已经绑给它了,你还得再绑一个 `dl.ymaikj.com`。 +> 收益不大,不推荐。 + +### 2. 开启静态网站托管 + +桶 → **数据管理 → 静态页面**: + +| 项 | 填 | +|---|---| +| 默认首页 | `index.html` | +| 默认 404 页 | `index.html`(可选,让任何路径都落到下载页) | +| 子目录首页 | 开启 | + +不开这一项,访问 `https://dl.ymaikj.com/` 会返回桶的 XML 列表或 403,而不是页面。 + +### 3. 绑定域名 `dl.ymaikj.com` + +桶 → **传输管理 → 域名管理 → 绑定域名**: + +1. 填 `dl.ymaikj.com`,勾选「自动添加 CNAME 记录」(域名在同一个阿里云账号下时可用; + 否则去 DNS 那边手工加一条 CNAME 指向桶的外网 Endpoint) +2. ⚠️ **中国大陆地域的桶绑自定义域名,域名必须已备案**。`ymaikj.com` 应该已备案, + 确认一下子域名是否需要单独报备。 +3. 证书:同一页的「证书托管」里传一张 `dl.ymaikj.com` 的证书。 + 阿里云「数字证书管理服务」可以申请免费的 DV 证书,签完直接在这里选。 + **不配证书 = 只能 http 访问**,微信和现在的浏览器都会拦。 + +### 4. 上传页面与安装包 + +用 ossutil(推荐,能重复执行): + +```bash +# 首次配置:ossutil config -e oss-cn-shanghai.aliyuncs.com -i -k + +# 页面(放桶根) +ossutil cp deploy/dl/index.html oss://ymai-dl/index.html \ + --meta "Content-Type:text/html; charset=utf-8#Cache-Control:no-cache, must-revalidate" + +# 安装包 +ossutil cp eaimar-release-1.0.2-20260923.apk \ + oss://ymai-dl/android/EAIMAR/eaimar-release-1.0.2-20260923.apk \ + --meta "Content-Type:application/vnd.android.package-archive#Content-Disposition:attachment" +``` + +也可以直接在 OSS 控制台点「上传文件」,然后在文件的「设置 HTTP 头」里改这两项。 + +⚠️ **页面必须设 `Cache-Control: no-cache`**。默认缓存会让你改了页面之后老用户 +还拿着旧版本,而这种问题根本没人会来报。 + +⚠️ **APK 建议设 `Content-Disposition: attachment`**。不设的话部分浏览器会尝试 +「预览」这个文件,表现为点了没反应或者打开一堆乱码。 + +--- + +## 三、与管理后台联动(版本号、商店链接、更新日志) + +页面打开时会调一次 `POST https://ym.ymaikj.com/api/home/user_getchannelapps` +(该接口在网关白名单里,未登录可调),把后台配的东西取回来: + +| 页面上的 | 来自后台哪里 | 落到哪个字段 | +|---|---|---| +| 版本号 | 服务配置 → **版本与分发** → 发版配置 → 线上版本 | `app_release.version` | +| 「本次更新」 | 版本与分发 → 发版配置 → 更新日志 | `app_release.changelog` | +| App Store 链接 | 版本与分发 → **渠道分发** → 全球-苹果(101) 的下载地址 | `channel_app.address` | +| 各安卓商店链接 | 渠道分发 → 华为(1) / 小米(2) / OPPO(3) / VIVO(4) / 荣耀(5) | 同上 | +| APK 直链 | 渠道分发 → **官方渠道(100)** 的下载地址 | 同上 | + +**后台改完,页面刷新即生效,不需要重新上传。** + +渠道地址留空时后端会自动回退到「发版配置」里的默认下载地址(`AppRelease.DownloadAddress`), +所以只想维护一套地址的话,只填那一处就够——填成 `https://dl.ymaikj.com` 即可兜住所有渠道。 + +⚠️ **APK 直链约定挂在「官方渠道(100)」下**。这是本页面单方面的约定,后台界面上没有任何提示, +所以要么按这个约定配,要么去改 `index.template.html` 里 `CFG.channels` 的映射。 + +⚠️ 接口打不通时(断网 / 网关挂了 / 跨域被拦)页面用 `CFG.fallback` 的兜底值渲染,**不报错**—— +用户是来下载的,给他看一个报错页没有任何用。代价是兜底值会比后台旧, +所以**别把 fallback 当配置入口去维护**。 + +--- + +## 四、发版时要做什么 + +1. 打包,产物名按仓库约定:`eaimar-release-<版本号>-<时间>.apk` + (**不要留 `app-release.apk`**) +2. 传安装包 —— 两条路任选: + - **后台传(推荐)**:服务配置 → **版本与分发** → 「默认下载地址」旁边的 **上传 APK** 按钮。 + 传到分发桶的 `android/<应用>/` 目录(按应用分目录,先选应用才能传),带进度条, + 传完**自动写进「渠道分发 → 官方渠道(100)」**的下载地址,不动「默认下载地址」那一格。 + - 手工传:`oss://ymai-dl/android/`,见上文「二、4」 +3. 到后台「版本与分发」改线上版本与更新日志(用「上传 APK」传的话,官方渠道地址已自动写好) + +### ⚠️ 后台上传的前置:分发桶要配跨域(CORS) + +浏览器是**直传到 OSS**(预签名 PUT 打的是 `ymai-dl.oss-cn-shanghai.aliyuncs.com`, +不是 `dl.ymaikj.com`),所以桶上必须有 CORS 规则,否则浏览器直接拦掉、 +XHR 只报一句没有细节的「网络错误」。 + +OSS 控制台 → `ymai-dl` → **权限管理 → 跨域设置 → 创建规则**: + +| 项 | 填 | +|---|---| +| 来源 | `https://console.ymaikj.com`
`https://console-dev.ymaikj.com` | +| 允许 Methods | `PUT`(顺带勾 GET、HEAD) | +| 允许 Headers | `content-type`(或 `*`) | + +对照:主桶 `ymaioss` 早就配好了(所以产品固件上传一直能用), +2026-09-23 实测 `ymai-dl` 的预检返回 **403**,需要照着补一条。 + +页面本身不用动。只有改页面文案/样式,或者 **App 图标换了**,才需要: + +```bash +cd deploy/dl && ./build.sh # 重新内联图标,生成 index.html +# 再把 index.html 传一次 +``` + +--- + +## 五、页面行为 + +| 环境 | 行为 | +|---|---| +| iOS | 跳 App Store | +| Android(认出品牌) | 主按钮跳对应商店,副按钮给 APK 直链 | +| Android(认不出品牌) | 直接给 APK 直链 | +| 桌面浏览器 | 两个按钮都给,提示用手机打开 | +| **微信 / QQ 内置浏览器** | **一进页面就弹遮罩**,引导「右上角 ··· → 在浏览器中打开」 | + +⚠️ 微信那一层不是可选项。微信会拦截 apk 下载而且**静默失败**——点了什么都不发生, +用户只会认为这个页面坏了。所以是进页面就提示,而不是等他点了才说。 + +品牌判断的口径与 App 内 `getAndroidStoreInfoByBrand` 一致:**判的是手机品牌, +不是从哪家商店下载的**(国内五家共用一个包)。认不出就走 APK,这个兜底方向是安全的。 + +商店地址与 APK 地址相同时(后台该渠道没单独配、回退到了默认地址)只显示一个按钮—— +两个一模一样的按钮只会让人犹豫点哪个。 + +--- + +## 六、待办 + +**首选是去后台「渠道分发」配**(见上文「三」),配了就不用动代码。 + +`index.template.html` 里 `CFG.fallback` 的商店链接目前是占位地址 +`https://emai.ymaikj.com/1223`,搜 `TODO(store-links)` 能定位,它只在接口打不通时才会被用到。 +各商店上架后可以顺手更新: + +| key | 从哪拿 | +|---|---| +| `appStore` | App Store Connect →「App 信息 → Apple ID」,一串纯数字,拼成 `https://apps.apple.com/cn/app/id<数字>` | +| `huawei` / `honor` | 各自开发者后台的应用详情页链接,形如 `https://appgallery.huawei.com/app/C<数字>` | +| `xiaomi` | `https://app.mi.com/details?id=com.yimai.eaimar` | +| `oppo` / `vivo` | 各自开发者后台的详情页链接 | + +⚠️ 包名是 **`com.yimai.eaimar`**(iOS bundle id 同名)。 + +--- + +## 七、注意 + +- **别在这个页面做 Universal Link 唤起**:UL 域名是 `yimai.games`,不是 `ymaikj.com`, + 在 ymaikj 的任何子域上配都不会生效。 +- 桶是公共读,APK 谁都能下——这对安装包是对的。但**别把别的东西往这个桶里放**。 +- 下载量上来之后再在 OSS 前面挂 CDN,页面不用改(域名不变)。 +- 图标在页面里只内联一份(``),favicon 与 apple-touch-icon 由 JS 复用同一个 + data URI。在 `` 里再写两个 href 会把几十 KB 的 base64 存三份,页面体积直接翻三倍。 diff --git a/deploy/dl/build.sh b/deploy/dl/build.sh new file mode 100755 index 00000000..0936b858 --- /dev/null +++ b/deploy/dl/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# 由 index.template.html 生成可上传的 index.html:把 App 图标内联成 data URI。 +# +# 为什么内联而不是放一个 icon.png 在旁边:这页只有一个文件要上传, +# 少一个文件就少一种「漏传了导致图标裂开」的失败形态,也少一次请求。 +# 代价是 index.html 会大几十 KB —— 对一个要发 60MB 安装包的页面无所谓。 +# +# App 图标换了就重跑本脚本,再把 index.html 传一次。 +set -euo pipefail + +cd "$(dirname "$0")" +SRC="../../apps/client/assets/images/app_icon.png" # App 图标的唯一来源,与客户端同一份素材 +SIZE=192 # 展示 92px,@2x 够用;再大只是徒增体积 + +[ -f "$SRC" ] || { echo "✗ 找不到 App 图标: $SRC" >&2; exit 1; } + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +sips -Z "$SIZE" "$SRC" --out "$TMP/icon.png" >/dev/null +B64="$(base64 -i "$TMP/icon.png" | tr -d '\n')" + +# 用 python 替换:图标 base64 有几万字符,sed 在这个量级上会报 "RE error" +python3 - "$B64" <<'PY' +import sys +b64 = sys.argv[1] +src = open('index.template.html').read() +out = src.replace('__ICON_DATA_URI__', 'data:image/png;base64,' + b64) +assert '__ICON_DATA_URI__' not in out, '占位符没替换干净' +open('index.html', 'w').write(out) +PY + +printf "✓ 已生成 index.html(%s KB)\n" "$(( $(stat -f%z index.html) / 1024 ))" diff --git a/deploy/dl/index.html b/deploy/dl/index.html new file mode 100644 index 00000000..48af8ccd --- /dev/null +++ b/deploy/dl/index.html @@ -0,0 +1,380 @@ + + + + + +EAIMAR · 下载 + + + + + + + + + + + +
+
EAIMAR
+

EAIMAR

+

实时记录会议纪要 · 多语言翻译

+

+ +
+ + +
    +
  • 会议纪要录音自动转写,AI 生成结构化纪要
  • +
  • 同声传译通话 / 面对面实时翻译,支持数十种语言
  • +
  • 智能硬件配套耳机、支架、车载设备开箱即用
  • +
+ +
+ 官网 +
+
+ + + + diff --git a/deploy/dl/index.template.html b/deploy/dl/index.template.html new file mode 100644 index 00000000..853b53db --- /dev/null +++ b/deploy/dl/index.template.html @@ -0,0 +1,380 @@ + + + + + +EAIMAR · 下载 + + + + + + + + + + + +
+
EAIMAR
+

EAIMAR

+

实时记录会议纪要 · 多语言翻译

+

+ +
+ + +
    +
  • 会议纪要录音自动转写,AI 生成结构化纪要
  • +
  • 同声传译通话 / 面对面实时翻译,支持数十种语言
  • +
  • 智能硬件配套耳机、支架、车载设备开箱即用
  • +
+ +
+ 官网 +
+
+ + + + diff --git a/deploy/registry-profiles.sh b/deploy/registry-profiles.sh index 8d79544b..8d8eba65 100644 --- a/deploy/registry-profiles.sh +++ b/deploy/registry-profiles.sh @@ -6,36 +6,27 @@ # # 用法(在脚本里): # source "$REPO_ROOT/deploy/registry-profiles.sh" -# registry_profile yunyan # → 设好 REGISTRY / REGISTRY_USER / REGISTRY_PASS / COMPANY_DESC -# COMPANY="$(pick_company)" # → 交互选公司(无 TTY 时用 DEFAULT_COMPANY) +# registry_profile along # → 设好 REGISTRY / REGISTRY_USER / REGISTRY_PASS / COMPANY_DESC +# COMPANY="$(pick_company)" # → 选公司(目前只剩 along,直接返回它) # # ⚠️ 本文件纳入 git,下面的仓库密码会随仓库提交(私有仓库内可接受)。 # 公开仓库前务必清空 REGISTRY_PASS。 set -euo pipefail -COMPANIES=(yunyan lingpu along) # 可选公司(交互菜单按此顺序展示;along 2026-08-16 起也有正式环境,故进生产菜单) -DEFAULT_COMPANY="yunyan" # 不指定且无 TTY(CI/管道)时用哪个 +# 2026-09-23:云雁(yunyan → registry.voitrans.net)与灵谱(lingpu → registry.lingpu.net)两个档案已删除。 +# 它们的部署目标早在 2026-09-13 就从本仓库清掉了,仓库档案再留着只会让人误推镜像到没人拉的地方。 +# 需要找回:git log -- deploy/registry-profiles.sh +COMPANIES=(along) # 可选公司(目前只剩阿龙一家) +DEFAULT_COMPANY="along" # 不指定且无 TTY(CI/管道)时用哪个 # 按公司设置仓库三件套。失败返回非 0,由调用方决定是否 exit。 registry_profile() { case "${1:-}" in - yunyan) - COMPANY_DESC="云雁" - REGISTRY="registry.voitrans.net" # 云雁自有仓库(1Panel OpenResty 反代 → docker-registry:5000) - REGISTRY_USER="liwei" - REGISTRY_PASS="li13451234" - ;; - lingpu) - COMPANY_DESC="灵谱" - REGISTRY="registry.lingpu.net" # 灵谱自有仓库 - REGISTRY_USER="liwei" - REGISTRY_PASS="li13451234" - ;; along) COMPANY_DESC="阿龙" REGISTRY="registry.ymaikj.com" # 仓库实体就在阿龙测试机 8.133.166.29 上(NPM 反代 → docker-registry:5000) # 正式机 47.116.104.181 也从这里拉镜像(阿龙只此一个仓库) - REGISTRY_USER="admin" # 注意:账号是 admin,与云雁/灵谱的 liwei 不同 + REGISTRY_USER="admin" # 注意:账号是 admin REGISTRY_PASS="li13451234" ;; *) @@ -48,14 +39,10 @@ registry_profile() { # 生产镜像名按公司区分。prod-build.sh 与 prod-deploy.sh 都调它,避免两处各写一份映射后漂移 # ——构建推的名字和部署拉的名字对不上,报错要到远端 docker pull 才暴露。 # -# admin/console:各公司都用 starpivot-* 新命名,与 docker-compose.yml 里写死的 image/container_name -# 一致。云雁的 admin/console 生产目标已于 2026-08-05 迁到上海机(那台没有历史容器), -# app:2026-08-10 起统一为 starpivot-app。旧名 voitrans-a11 已废弃——仓库里的旧 tag 不再更新, -# 首次用新名部署需先 ./prod-build.sh 推一版(旧 tag 不会自动搬过来)。 -# -# 现在三个服务、各家公司都是 starpivot-<服务>,函数保留公司参数只为兼容调用方和将来再分叉。 +# 三个服务一律 starpivot-<服务>,与 docker-compose.yml 里的 image/container_name 一致。 +# 函数保留公司参数只为兼容调用方和将来再分叉。 # -# 用法:IMAGE_NAME="$(prod_image_name lingpu admin)" +# 用法:IMAGE_NAME="$(prod_image_name along admin)" prod_image_name() { # $1=公司 $2=服务(admin|console|app) case "${2:-}" in admin|console|app) printf 'starpivot-%s' "$2" ;; @@ -67,7 +54,9 @@ prod_image_name() { # $1=公司 $2=服务(admin|console|app) } # 交互选择公司;无 TTY 时回落到 DEFAULT_COMPANY。选中的 key 从 stdout 返回(菜单走 stderr,不污染返回值)。 +# 只剩一家时不弹菜单——让人对着独苗菜单按回车纯属噪音。 pick_company() { + if [ "${#COMPANIES[@]}" -le 1 ]; then printf '%s' "${COMPANIES[0]}"; return 0; fi if [ ! -t 0 ]; then printf '%s' "$DEFAULT_COMPANY"; return 0; fi printf "\033[34m选择镜像仓库所属公司:\033[0m\n" >&2 local i=1 c