├── .gitignore ├── package-lock.json ├── package.json ├── readme.md └── src ├── AndroidPublishHelper.ts ├── AndroidPublishHelperWithCookieCheck.ts ├── CookieCheckable.ts ├── HuaweiPublishHelper.ts ├── MeizuPublishHelper.ts ├── MiPublishHelper.ts ├── OppoPublishHelper.ts ├── QQPublishHelper.ts ├── QihuPublishHelper.ts ├── SogouPublishHelper.ts ├── VivoPublishHelper.ts ├── cookie.txt └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "android_market_publish", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.7.4", 9 | "resolved": "https://registry.npm.taobao.org/@babel/runtime/download/@babel/runtime-7.7.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.7.4.tgz", 10 | "integrity": "sha1-sjqFZ1HkvwmSYvhndniJwOP+F1s=", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.2" 13 | } 14 | }, 15 | "@types/node": { 16 | "version": "12.12.14", 17 | "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-12.12.14.tgz?cache=0&sync_timestamp=1574723520942&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-12.12.14.tgz", 18 | "integrity": "sha1-HB1uPHXbpGbgMmlI1W6L1yoZA9I=" 19 | }, 20 | "asn1": { 21 | "version": "0.2.4", 22 | "resolved": "https://registry.npm.taobao.org/asn1/download/asn1-0.2.4.tgz", 23 | "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", 24 | "requires": { 25 | "safer-buffer": "~2.1.0" 26 | } 27 | }, 28 | "boolbase": { 29 | "version": "1.0.0", 30 | "resolved": "https://registry.npm.taobao.org/boolbase/download/boolbase-1.0.0.tgz", 31 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 32 | }, 33 | "cheerio": { 34 | "version": "1.0.0-rc.3", 35 | "resolved": "https://registry.npm.taobao.org/cheerio/download/cheerio-1.0.0-rc.3.tgz", 36 | "integrity": "sha1-CUY21CWy6cD065GkbAVjDJoai/Y=", 37 | "requires": { 38 | "css-select": "~1.2.0", 39 | "dom-serializer": "~0.1.1", 40 | "entities": "~1.1.1", 41 | "htmlparser2": "^3.9.1", 42 | "lodash": "^4.15.0", 43 | "parse5": "^3.0.1" 44 | } 45 | }, 46 | "crypto": { 47 | "version": "1.0.1", 48 | "resolved": "https://registry.npm.taobao.org/crypto/download/crypto-1.0.1.tgz", 49 | "integrity": "sha1-KvG3ytgXXSTIobB3glV5SiGAMDc=" 50 | }, 51 | "css-select": { 52 | "version": "1.2.0", 53 | "resolved": "https://registry.npm.taobao.org/css-select/download/css-select-1.2.0.tgz", 54 | "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", 55 | "requires": { 56 | "boolbase": "~1.0.0", 57 | "css-what": "2.1", 58 | "domutils": "1.5.1", 59 | "nth-check": "~1.0.1" 60 | } 61 | }, 62 | "css-what": { 63 | "version": "2.1.3", 64 | "resolved": "https://registry.npm.taobao.org/css-what/download/css-what-2.1.3.tgz", 65 | "integrity": "sha1-ptdgRXM2X+dGhsPzEcVlE9iChfI=" 66 | }, 67 | "dom-serializer": { 68 | "version": "0.1.1", 69 | "resolved": "https://registry.npm.taobao.org/dom-serializer/download/dom-serializer-0.1.1.tgz", 70 | "integrity": "sha1-HsQFnihLq+027sKUHUqXChic58A=", 71 | "requires": { 72 | "domelementtype": "^1.3.0", 73 | "entities": "^1.1.1" 74 | } 75 | }, 76 | "domelementtype": { 77 | "version": "1.3.1", 78 | "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-1.3.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomelementtype%2Fdownload%2Fdomelementtype-1.3.1.tgz", 79 | "integrity": "sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=" 80 | }, 81 | "domhandler": { 82 | "version": "2.4.2", 83 | "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-2.4.2.tgz?cache=0&sync_timestamp=1564708887907&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-2.4.2.tgz", 84 | "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=", 85 | "requires": { 86 | "domelementtype": "1" 87 | } 88 | }, 89 | "domutils": { 90 | "version": "1.5.1", 91 | "resolved": "https://registry.npm.taobao.org/domutils/download/domutils-1.5.1.tgz", 92 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 93 | "requires": { 94 | "dom-serializer": "0", 95 | "domelementtype": "1" 96 | } 97 | }, 98 | "entities": { 99 | "version": "1.1.2", 100 | "resolved": "https://registry.npm.taobao.org/entities/download/entities-1.1.2.tgz?cache=0&sync_timestamp=1563403318326&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fentities%2Fdownload%2Fentities-1.1.2.tgz", 101 | "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" 102 | }, 103 | "htmlparser2": { 104 | "version": "3.10.1", 105 | "resolved": "https://registry.npm.taobao.org/htmlparser2/download/htmlparser2-3.10.1.tgz", 106 | "integrity": "sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=", 107 | "requires": { 108 | "domelementtype": "^1.3.1", 109 | "domhandler": "^2.3.0", 110 | "domutils": "^1.5.1", 111 | "entities": "^1.1.1", 112 | "inherits": "^2.0.1", 113 | "readable-stream": "^3.1.1" 114 | } 115 | }, 116 | "inherits": { 117 | "version": "2.0.4", 118 | "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz?cache=0&sync_timestamp=1560975547815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finherits%2Fdownload%2Finherits-2.0.4.tgz", 119 | "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" 120 | }, 121 | "lodash": { 122 | "version": "4.17.15", 123 | "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.15.tgz", 124 | "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=" 125 | }, 126 | "moment": { 127 | "version": "2.24.0", 128 | "resolved": "https://registry.npm.taobao.org/moment/download/moment-2.24.0.tgz", 129 | "integrity": "sha1-DQVdU/UFKqZTyfbraLtdEr9cK1s=" 130 | }, 131 | "node-rsa": { 132 | "version": "1.0.7", 133 | "resolved": "https://registry.npm.taobao.org/node-rsa/download/node-rsa-1.0.7.tgz", 134 | "integrity": "sha1-hbem1vqO5iS+ZAKmtBvkknLVgFU=", 135 | "requires": { 136 | "asn1": "^0.2.4" 137 | } 138 | }, 139 | "nth-check": { 140 | "version": "1.0.2", 141 | "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz", 142 | "integrity": "sha1-sr0pXDfj3VijvwcAN2Zjuk2c8Fw=", 143 | "requires": { 144 | "boolbase": "~1.0.0" 145 | } 146 | }, 147 | "parse5": { 148 | "version": "3.0.3", 149 | "resolved": "https://registry.npm.taobao.org/parse5/download/parse5-3.0.3.tgz", 150 | "integrity": "sha1-BC95L/3TaFFVHPTp4Gazh0q0W1w=", 151 | "requires": { 152 | "@types/node": "*" 153 | } 154 | }, 155 | "readable-stream": { 156 | "version": "3.4.0", 157 | "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.4.0.tgz", 158 | "integrity": "sha1-pRwmdUZY4KPCHb9ZFjvUW6b0R/w=", 159 | "requires": { 160 | "inherits": "^2.0.3", 161 | "string_decoder": "^1.1.1", 162 | "util-deprecate": "^1.0.1" 163 | } 164 | }, 165 | "regenerator-runtime": { 166 | "version": "0.13.3", 167 | "resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.13.3.tgz", 168 | "integrity": "sha1-fPanfY9cb2Drc8X8GVWyzrAea/U=" 169 | }, 170 | "safe-buffer": { 171 | "version": "5.2.0", 172 | "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.2.0.tgz", 173 | "integrity": "sha1-t02uxJsRSPiMZLaNSbHoFcHy9Rk=" 174 | }, 175 | "safer-buffer": { 176 | "version": "2.1.2", 177 | "resolved": "https://registry.npm.taobao.org/safer-buffer/download/safer-buffer-2.1.2.tgz", 178 | "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" 179 | }, 180 | "string_decoder": { 181 | "version": "1.3.0", 182 | "resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.3.0.tgz?cache=0&sync_timestamp=1565170823020&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstring_decoder%2Fdownload%2Fstring_decoder-1.3.0.tgz", 183 | "integrity": "sha1-QvEUWUpGzxqOMLCoT1bHjD7awh4=", 184 | "requires": { 185 | "safe-buffer": "~5.2.0" 186 | } 187 | }, 188 | "typescript": { 189 | "version": "3.7.3", 190 | "resolved": "https://registry.npm.taobao.org/typescript/download/typescript-3.7.3.tgz?cache=0&sync_timestamp=1575530522587&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-3.7.3.tgz", 191 | "integrity": "sha1-s2hAZooWRYpwJbnqv60Rtmq4XGk=", 192 | "dev": true 193 | }, 194 | "util-deprecate": { 195 | "version": "1.0.2", 196 | "resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", 197 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 198 | }, 199 | "yaml": { 200 | "version": "1.7.2", 201 | "resolved": "https://registry.npm.taobao.org/yaml/download/yaml-1.7.2.tgz", 202 | "integrity": "sha1-8mqr9zhZCrYe+spQI1jkjcnzSLI=", 203 | "requires": { 204 | "@babel/runtime": "^7.6.3" 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "android_market_publish", 3 | "version": "1.0.0", 4 | "description": "automatic publish app to every android markets.", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "automatic", 11 | "publish", 12 | "android", 13 | "markets" 14 | ], 15 | "author": "hackingwu", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "typescript": "^3.7.3" 19 | }, 20 | "dependencies": { 21 | "cheerio": "^1.0.0-rc.3", 22 | "crypto": "^1.0.1", 23 | "moment": "^2.24.0", 24 | "node-rsa": "^1.0.7", 25 | "yaml": "^1.7.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # 目的 3 | 自动发布到各个安卓市场。现在有一个开源的工具fastlane 可以做到自动发布到App Store 和 Google Play。但是国内有各种各样的市场,例如华为,小米,Vivo, Oppo等等,现在还没有一个工具可以帮助我们自动发布。本项目就是应运而生,希望能让安卓更加便捷。 4 | # 使用 5 | 目前华为和小米市场都有提供API来做发布,其他市场目前还没有,我们仅仅只能通过模拟请求的方式来完成。而模拟请求有一个登录的问题,除了搜狗市场,其他我还以一直没有解决,目前是通过维护cookie的方式来做,就是对一个已登录状态的cookie 不对去请求,让其一直保持alive 6 | 本项目现在支持/半支持的市场有: 7 | 1. 华为 8 | 2. 小米 9 | 3. 魅族 10 | 4. Oppo 11 | 5. 奇虎360 12 | 6. QQ应用宝 13 | 7. Vivo 14 | 8. Sogou 15 | 16 | 需要维护Cookie的请看cookie.txt, 我已经最小化了每个市场所需的cookie值。可以通过cookie.txt看各个市场所需cookie的key是啥,不用带上所有的cookie项。 17 | 因为我平时的使用cookie以及一些配置是放在Zookeeper内,这里就先不移除了。本项目也是给大家一个参考,比如如何调用小米和华为的API,还有各个市场模拟请求(上传,发布)的格式。让大家不用再踩一次坑,或者缩短在坑里的时间。而是花更多的时间去解决已知的缺陷。 18 | # 缺陷 19 | 20 | 1. 解决登录问题 21 | 2. 补充更多的市场 -------------------------------------------------------------------------------- /src/AndroidPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import { Zookeeper } from "ZooKeeper"; 2 | 3 | const request = require('request') 4 | const util = require('util'); 5 | export default abstract class AndroidPublishHelper { 6 | zk: Zookeeper; 7 | cookie: string; 8 | getAsync; 9 | postAsync; 10 | putAsync; 11 | constructor(zookeeper: Zookeeper) { 12 | this.zk = zookeeper; 13 | this.cookie = ""; 14 | this.postAsync = util.promisify(request.post); 15 | this.getAsync = util.promisify(request.get); 16 | this.putAsync = util.promisify(request.put); 17 | } 18 | 19 | protected getCookiePathInZk(){ 20 | return `android_market.${this.getName()}.cookie`; 21 | } 22 | protected async refreshCookieFromZk(): Promise{ 23 | this.cookie = await this.zk.getString(this.getCookiePathInZk()); 24 | return this.cookie; 25 | } 26 | // WARN: cookie需要事先被设置好 27 | protected async doRequest(req, config){ 28 | return await req({ 29 | ...config, 30 | headers: {"Cookie": this.cookie} 31 | }) 32 | } 33 | 34 | /** 35 | * 36 | * @param cn_name 应用的中文名称 37 | * @param en_name 应用的英文名称 38 | * @param package_name 应用的包名 39 | * @param project_version 应用本次发布的版本号 40 | * @param desc 应用本次发布的描述 41 | * @param extra 42 | */ 43 | abstract async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any): Promise; 44 | 45 | abstract getName(): string; 46 | 47 | /** 48 | * 49 | * @param en_name 应用的英文名称 50 | * @param project_version 应用的版本号 51 | */ 52 | async getApkPath(en_name: string, project_version: string): Promise{ 53 | return await this.zk.getString(`android_market.apk_path`) + `/${this.getName()}/${en_name}_${project_version}.apk` 54 | } 55 | 56 | setData(key, value, data) { 57 | //数组的处理 58 | if (data[key]) { 59 | if(!Array.isArray(data[key])) { 60 | data[key] = [data[key]] 61 | } 62 | data[key].push(value) 63 | } else { 64 | data[key]=value 65 | } 66 | } 67 | 68 | async fillDataFromInput($: any, data: any){ 69 | const inputArr = $('input'); //[type="hidden"] 70 | for (let i=0; i < inputArr.length; i++){ 71 | const input = inputArr[i]; 72 | const attribs = input.attribs; 73 | if (!attribs.name || attribs.name === "undefined") continue 74 | if (attribs.type==='radio' && !("checked" in attribs)) { 75 | continue 76 | } 77 | 78 | this.setData(attribs.name, attribs.value, data); 79 | } 80 | } 81 | 82 | 83 | async fillDataFromTextarea($: any, data: any) { 84 | const textareaArr = $('textarea'); 85 | for (let i = 0; i < textareaArr.length; i++) { 86 | const textarea = textareaArr[i]; 87 | const attribs = textarea.attribs; 88 | if (!attribs.name || attribs.name === "undefined") continue 89 | 90 | let value = '' 91 | if (textarea.childNodes.length>0) { 92 | value = textarea.childNodes[0].data 93 | } 94 | this.setData(attribs.name, value, data) 95 | } 96 | } 97 | 98 | async fillDataFromSelect($: any, data: any) { 99 | const selectArr = $('select') 100 | for (let i = 0; i < selectArr.length; i++) { 101 | const select = selectArr[i]; 102 | if (!select.attribs.name || select.attribs.name === "undefined") continue 103 | const selectedOptions = select.childNodes.filter(node=>node.name==="option" && node.attribs.selected==="selected"); 104 | 105 | let value; 106 | if (selectedOptions.length === 0) { 107 | value = select.attribs.value || select.attribs["data-value"] 108 | if (value === undefined) continue 109 | } else { 110 | const selectedOption = selectedOptions[0]; 111 | value = selectedOption.attribs.value 112 | } 113 | 114 | 115 | this.setData(select.attribs.name, value, data) 116 | } 117 | } 118 | 119 | 120 | async getCookieMap(){ 121 | const cookieMap = {}; 122 | const arr1=this.cookie.split(/\s*;\s*/); 123 | arr1.forEach(it=>{ 124 | const arr2=it.split(/\s*=\s*/) 125 | if (arr2.length < 2) return 126 | cookieMap[arr2[0]]=arr2[1] 127 | }) 128 | return cookieMap; 129 | } 130 | } -------------------------------------------------------------------------------- /src/AndroidPublishHelperWithCookieCheck.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import CookieCheckable from "./CookieCheckable"; 3 | 4 | export default abstract class AndroidPublishHelperWithCookieCheck extends AndroidPublishHelper implements CookieCheckable{ 5 | 6 | 7 | abstract checkCookieAlive(): Promise ; 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/CookieCheckable.ts: -------------------------------------------------------------------------------- 1 | export default interface CookieCheckable{ 2 | checkCookieAlive(): Promise; 3 | getName(): string; 4 | // setCookie(); 5 | } -------------------------------------------------------------------------------- /src/HuaweiPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | const NodeRSA = require("node-rsa"); 3 | const fs = require('fs'); 4 | const xmlParser = require("fast-xml-parser"); 5 | const path = require("path") 6 | 7 | export default class HuaweiPublishHelper extends AndroidPublishHelper { 8 | 9 | getName(): string { 10 | return "huawei"; 11 | } 12 | ZK_PREFIX = "android_market.huawei"; 13 | 14 | private client_id; 15 | private token; 16 | 17 | protected async doRequest(req, config){ 18 | return await req({ 19 | ...config, 20 | headers: { 21 | "Authorization": "Bearer " + this.token, 22 | "client_id": this.client_id 23 | } 24 | }) 25 | } 26 | 27 | private checkError(result: any, msg: string) { 28 | if (!result.body) { 29 | throw new Error(msg); 30 | } 31 | if (result.body.errorCode) { 32 | throw new Error(`${msg}: ${result.body.errorMsg}`) 33 | } 34 | } 35 | 36 | private async initToken() { 37 | const domain = "https://connect-api.cloud.huawei.com/api" 38 | this.client_id = await this.zk.getString(`${this.ZK_PREFIX}.client_id`); 39 | const client_secret = await this.zk.getString(`${this.ZK_PREFIX}.client_secret`); 40 | const result = await this.postAsync({ 41 | url: domain+"/oauth2/v1/token", 42 | json: { 43 | "client_id": this.client_id, 44 | "client_secret": client_secret, 45 | "grant_type": "client_credentials" 46 | } 47 | }); 48 | this.token = result.body.access_token 49 | } 50 | 51 | /** 52 | * Deprecate 53 | * 华为的cookie不是存储在zk里,因此不要调用initCookie 54 | * @param app 55 | */ 56 | private async connect(){ 57 | const clientId = await this.zk.getString(`${this.ZK_PREFIX}.clientId`); 58 | const priKey = await this.zk.getString(`${this.ZK_PREFIX}.priKey`) 59 | const currentTimestamp = Date.now(); 60 | const content = clientId+currentTimestamp 61 | 62 | const key = new NodeRSA(); 63 | key.importKey(Buffer.from(priKey, 'base64'), 'pkcs8-der'); 64 | const privateKey = key.exportKey(); 65 | const sign = new NodeRSA(privateKey, {signingSchema: 'sha256'}).sign(content).toString('base64'); 66 | 67 | const result = await this.doRequest(this.postAsync,{ 68 | url: "https://connect-api.cloud.huawei.com/api/common/v1/connect", 69 | json: { 70 | "key_string": { 71 | "clientId": clientId, 72 | "time": currentTimestamp, 73 | "sign": sign 74 | } 75 | } 76 | }); 77 | this.checkError(result, "登录失败") 78 | 79 | this.cookie = result.headers["set-cookie"][0] 80 | 81 | } 82 | 83 | /** 84 | * 85 | * @param package_name 应用包名 86 | */ 87 | private async getAppId(package_name: String){ 88 | const appIdReq = await this.doRequest(this.getAsync, { 89 | url: "https://connect-api.cloud.huawei.com/api/publish/v2/appid-list", 90 | qs: { 91 | "packageName": package_name 92 | } 93 | }); 94 | const appIdBody = JSON.parse(appIdReq.body); 95 | if (appIdBody.ret.code !== 0) { 96 | throw new Error("获取APPID失败:"+appIdBody.ret.msg) 97 | } 98 | const appId = appIdBody.appids[0].value 99 | return appId 100 | } 101 | 102 | 103 | public async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any){ 104 | //1. 登录 105 | await this.initToken(); 106 | 107 | //4. 获取appId 108 | const appId = await this.getAppId(package_name); 109 | 110 | //2. 获取上传文件认证码 111 | const uploadReq = await this.doRequest(this.getAsync, { 112 | url: "https://connect-api.cloud.huawei.com/api/publish/v2/upload-url", 113 | qs: {"suffix": "apk", "appId": appId} 114 | }); 115 | const uploadReqBody = JSON.parse(uploadReq.body) 116 | if (uploadReqBody.ret.code !== 0){ 117 | throw new Error("获取文件上传信息失败: " + uploadReqBody.ret.msg); 118 | } 119 | const uploadAuthCode = uploadReqBody.authCode; 120 | const uplaodUrl = uploadReqBody.uploadUrl; 121 | 122 | //3. 上传文件 123 | const apkPath = await this.getApkPath(en_name, project_version); 124 | const uploadFileReq = await this.doRequest(this.postAsync, { 125 | url: uplaodUrl, 126 | formData: { 127 | authCode: uploadAuthCode, 128 | fileCount: 1, 129 | file: fs.createReadStream(apkPath) 130 | } 131 | }); 132 | const uploadFileBoby = JSON.parse(uploadFileReq.body).result.UploadFileRsp; 133 | 134 | if (!uploadFileBoby.ifSuccess) throw new Error("文件上传失败"); 135 | const fileInfoList = uploadFileBoby.fileInfoList; 136 | fileInfoList[0]["fileName"] = path.basename(apkPath) 137 | fileInfoList[0]["fileDestUrl"] = fileInfoList[0]["fileDestUlr"] //??是接口的typo???? 138 | 139 | //4. 更新应用语言描述信息 140 | const lang = "zh-CN"; 141 | const updateAppReleaseInfoResult = await this.doRequest(this.putAsync, { 142 | url: 'https://connect-api.cloud.huawei.com/api/publish/v2/app-language-info', 143 | qs: { 144 | "appId": appId, 145 | }, 146 | json: { 147 | 148 | "lang": lang, 149 | "newFeatures": desc 150 | 151 | 152 | } 153 | }); 154 | const updateAppReleaseInfoBody = updateAppReleaseInfoResult.body; 155 | if (updateAppReleaseInfoBody.ret.code !== 0) { 156 | throw new Error("更新描述失败: "+updateAppReleaseInfoBody.ret.msg); 157 | } 158 | 159 | //5.更新文件信息 160 | 161 | const updateAppFileResult = await this.doRequest(this.putAsync, { 162 | url: "https://connect-api.cloud.huawei.com/api/publish/v2/app-file-info", 163 | qs: { 164 | "appId": appId 165 | }, 166 | json: { 167 | lang: lang, 168 | fileType: 5, 169 | files: fileInfoList 170 | } 171 | }) 172 | const updateAppFileBody = updateAppFileResult.body 173 | if (updateAppFileBody.ret.code !== 0) { 174 | throw new Error("更新文件失败: "+updateAppFileBody.ret.msg); 175 | } 176 | 177 | 178 | //6. 提交审核 179 | const submitResult = await this.doRequest(this.postAsync, { 180 | url: "https://connect-api.cloud.huawei.com/api/publish/v2/app-submit", 181 | qs: {"appId": appId} 182 | }) 183 | const submitBody = JSON.parse(submitResult.body) 184 | if (submitBody.ret.code !== 0) { 185 | throw new Error("提交审核失败: "+submitBody.ret.msg); 186 | } 187 | 188 | return true; 189 | } 190 | 191 | 192 | } -------------------------------------------------------------------------------- /src/MeizuPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import CookieCheckable from "./CookieCheckable"; 3 | const fs = require('fs'); 4 | const moment = require('moment') 5 | export default class MeizuPublishHelper extends AndroidPublishHelper implements CookieCheckable { 6 | 7 | appMap = new Map>(); 8 | 9 | 10 | 11 | //cookie有效期是session,我们保持心跳请求,保证服务器session不过期就可以一直存活。 12 | //除非对方服务器重启,并且session没有序列化。或者我们太久没有请求. 13 | async checkCookieAlive(): Promise { 14 | const cookie = await this.refreshCookieFromZk(); 15 | // console.log(this.getName(), cookie); 16 | const getResponse = await this.doRequest(this.getAsync, { 17 | url: "http://developer.meizu.com/console/apps/app/list/data", 18 | qs: { 19 | //懒得去抓取所有数据了,预计一个公司的app不会的需求不会超过10条,除外包公司以外。 20 | "start": 0, 21 | "limit": 10 22 | } 23 | }) 24 | let alive ; 25 | let result; 26 | try { 27 | result = JSON.parse(getResponse.body); 28 | alive = result.code === 200; 29 | }catch(e){ 30 | alive=false; 31 | } 32 | 33 | if (alive) { 34 | const data = result.value.data; 35 | data.forEach(it=>{this.appMap.set(it["name"], it)}) 36 | } 37 | return alive; 38 | } 39 | 40 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 41 | if (!await this.checkCookieAlive()) { 42 | throw new Error("请先登录"); 43 | } 44 | //1. 上传apk包 45 | const uploadResult = await this.doRequest(this.postAsync, { 46 | url: "http://developer.meizu.com/console/apps/upload/chain", 47 | formData: { 48 | "Filedata": fs.createReadStream(await this.getApkPath(en_name, project_version)) 49 | } 50 | }) 51 | const uploadResultBody = JSON.parse(uploadResult.body); 52 | if (uploadResultBody.code !== 200) { 53 | throw new Error("上传失败: "+ uploadResultBody.message); 54 | } 55 | const uploadResultData = uploadResultBody.value[0]; 56 | const meizuBean = new MeizuBean(); 57 | 58 | 59 | //2. 获取app信息 60 | const appInfoReq = await this.doRequest(this.getAsync,{ 61 | url: "http://developer.meizu.com/console/apps/appinfo.json", 62 | qs: { 63 | package_name: package_name 64 | } 65 | }) 66 | const appInfoBoby = JSON.parse(appInfoReq.body); 67 | if (appInfoBoby.code !== 200) throw new Error("获取APP信息失败: " + appInfoBoby.message) 68 | const appInfo = appInfoBoby.value; 69 | 70 | const keyPairs={"appName": "name", "appDesc": "appDescription", "verDesc": "verDescription", 71 | "authorName": "publisher", "catid": "categoryId", "cat2id": "category2Id" } 72 | 73 | for (let key of Object.keys(meizuBean)) { 74 | let value; 75 | if (key in keyPairs) { 76 | value = appInfo[keyPairs[key]] 77 | } else { 78 | value = appInfo[key]; 79 | } 80 | if (typeof value === 'boolean') { 81 | value = value ? 1 : 0; 82 | } 83 | 84 | if (value === undefined || value === null) value = '' 85 | else if (key === "saleTime" || key === "warnTime" || key === "betaTime" || key === "betaEndTime") { 86 | value = moment(new Date(value)).format('YYYY-MM-DD HH:mm:ss') 87 | } 88 | meizuBean[key] = value; 89 | 90 | } 91 | meizuBean.packageUrl = "/upload/"+uploadResultData.url 92 | const verDesc = desc; 93 | if (verDesc && verDesc.length > 5) meizuBean.verDesc = verDesc; 94 | meizuBean.screenShots=appInfo.images.map(it=>it.image) 95 | meizuBean.submitType = "publish"; 96 | meizuBean.unionVersion=0;//联合运营 97 | 98 | //3. 提交审核 99 | const submitResult = await this.doRequest(this.postAsync, { 100 | url: 'http://developer.meizu.com/console/apps/save.json', 101 | formData: meizuBean 102 | }) 103 | const submitResultBody = JSON.parse(submitResult.body); 104 | if (submitResultBody.code !== 200) { 105 | throw new Error("提交审核出错: " + submitResultBody.message) 106 | } 107 | return true; 108 | } 109 | 110 | getName(): string { 111 | return "meizu"; 112 | } 113 | 114 | 115 | 116 | } 117 | 118 | class MeizuBean{ 119 | id = undefined; 120 | verisonId = undefined; 121 | packageUrl = undefined; 122 | appName = undefined; 123 | unionVersion = undefined; 124 | appDesc = undefined; 125 | verDesc = undefined; 126 | recommendDesc = undefined; 127 | catid = undefined; 128 | cat2id = undefined; 129 | tagId = undefined; 130 | keyword = undefined; 131 | price = undefined; 132 | notifyUrl = undefined; 133 | authorName = undefined; 134 | testAccount = undefined; 135 | testPassword = undefined; 136 | icon = undefined; 137 | screenShots = undefined; 138 | enableSaleTime = undefined; 139 | saleTime = undefined; 140 | enableWarnTime = undefined; 141 | warnTime = undefined; 142 | enableBetaTime = undefined; 143 | betaTime = undefined; 144 | enableBetaEndTime = undefined; 145 | betaEndTime = undefined; 146 | locale = undefined; 147 | enablePurchase = 0 ; 148 | appNotifyUrl = "" ; 149 | submitType = "publish"; 150 | } -------------------------------------------------------------------------------- /src/MiPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | 3 | import { Zookeeper } from "ZooKeeper"; 4 | 5 | const fs = require('fs'); 6 | const crypto = require('crypto'); 7 | 8 | export default class MiPublishHelper extends AndroidPublishHelper { 9 | getName(): string { 10 | return "mi"; 11 | } 12 | 13 | ZK_PREFIX = "android_market.mi"; 14 | encryptGroupSize=(1024/11)-11; 15 | publicKey:string ; 16 | MI_DOMAIN = "http://api.developer.xiaomi.com/devupload"; 17 | 18 | async encryptContent(content: string){ 19 | if (!this.publicKey) { 20 | this.publicKey = await this.zk.getString(`${this.ZK_PREFIX}.publicKey`) 21 | } 22 | let sig = '' 23 | for (let i = 0; i < content.length; ){ 24 | const remain = content.length - i; 25 | const segSize = remain > this.encryptGroupSize ? this.encryptGroupSize : remain; 26 | const segment = content.substring(i, i+segSize) 27 | const r1 = crypto.publicEncrypt({key: this.publicKey, padding: crypto.constants.RSA_PKCS1_PADDING}, Buffer.from(segment)).toString('hex'); 28 | sig+=r1; 29 | i = i + segSize; 30 | } 31 | return sig; 32 | } 33 | 34 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 35 | const userName = await this.zk.getString(`${this.ZK_PREFIX}.username`) 36 | const privateKey = await this.zk.getString(`${this.ZK_PREFIX}.private_key`) 37 | const requestData = { 38 | "userName": userName, 39 | "synchroType": 1, 40 | "appInfo": { 41 | "appName": cn_name, 42 | "packageName": package_name, 43 | "updateDesc": desc 44 | } 45 | } 46 | const requestDataStr = JSON.stringify(requestData); 47 | const paramsMd5Arr = []; 48 | const apkPath = await this.getApkPath(en_name, project_version); 49 | const buffer = fs.readFileSync(apkPath); 50 | const fsHash = crypto.createHash('md5').update(buffer).digest('hex') 51 | const fileStream = fs.createReadStream(apkPath) 52 | paramsMd5Arr.push({"name": "RequestData", "hash": crypto.createHash('md5').update(requestDataStr).digest('hex')}); 53 | paramsMd5Arr.push({"name": "apk", "hash": fsHash}); 54 | const result = await this.postAsync({ 55 | "url": this.MI_DOMAIN + "/dev/push", 56 | "formData": { 57 | "RequestData": requestDataStr, 58 | "apk": fileStream, 59 | "SIG": await this.encryptContent(JSON.stringify({"sig": paramsMd5Arr, "password": privateKey})) 60 | } 61 | }); 62 | 63 | if (result.body.result) { 64 | throw new Error(`发布失败: ${result.body.message}`) 65 | } 66 | 67 | 68 | return true; 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/OppoPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import CookieCheckable from "./CookieCheckable"; 3 | const fs = require('fs'); 4 | const path = require("path"); 5 | const cheerio = require('cheerio'); 6 | export default class OppoPublishHelper extends AndroidPublishHelper implements CookieCheckable { 7 | async checkCookieAlive(): Promise { 8 | await this.refreshCookieFromZk(); 9 | const result = await this.doRequest(this.getAsync, { 10 | url: "https://open.oppomobile.com/user/index/check-login.json" 11 | }); 12 | let alive = false; 13 | try { 14 | const body = JSON.parse(result.body); 15 | if (body.data && body.errno === 0) alive = true; 16 | } catch(e){} 17 | return alive; 18 | } 19 | 20 | appMap = new Map>(); 21 | 22 | async listApp() { 23 | const result = await this.doRequest(this.postAsync, { 24 | url: "https://open.oppomobile.com/resource/list/index.json", 25 | formData: { 26 | type: 0, 27 | limit: 20, 28 | offset: 0, 29 | app_name: "", 30 | state: "" 31 | } 32 | }); 33 | const body = JSON.parse(result.body); 34 | body.data["app_list"].data.rows.forEach(it => this.appMap.set(it.pkg_name, it)) 35 | } 36 | 37 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 38 | if (!await this.checkCookieAlive()) { 39 | throw new Error("请先登录"); 40 | } 41 | //1. 先获取app_id 42 | //appMap[pkg_name]["app_id"] 43 | const verDesc = desc; 44 | await this.listApp(); 45 | const appId=this.appMap.get(package_name)["app_id"]; 46 | if (!appId) throw new Error("找不到该应用"); 47 | const apkPath = await this.getApkPath(en_name, project_version); 48 | const fileName = path.basename(apkPath); 49 | //2. 获取app信息 50 | const data = {}; 51 | const appInfo = await this.doRequest(this.getAsync, { 52 | url: "https://open.oppomobile.com/resource/publish", 53 | qs: { 54 | app_id: appId 55 | } 56 | }); 57 | const $ = cheerio.load(appInfo.body) 58 | this.fillDataFromInput($, data); 59 | this.fillDataFromTextarea($, data); 60 | this.fillDataFromSelect($, data); 61 | if (Object.keys(data).length === 0) throw new Error("不存在该应用基础信息"); 62 | if (verDesc && verDesc.length > 0) data["update_desc"] = verDesc; 63 | 64 | //3.上传app 65 | const uploadResult = await this.doRequest(this.postAsync, { 66 | url: "https://api.open.oppomobile.com/api/utility/upload", 67 | qs: { 68 | id: 0, 69 | filename: fileName 70 | }, 71 | formData: { 72 | file: fs.createReadStream(apkPath), 73 | type: 'apk', 74 | id: 0 75 | } 76 | }); 77 | const uploadBody = JSON.parse(uploadResult.body); 78 | if (uploadBody.errno !== 0) throw new Error("上传失败"); 79 | const uploadData = uploadBody.data; 80 | data["apk_md5"] = uploadData["md5"] 81 | data["apk_url"] = uploadData["url"]; 82 | 83 | //4. 获取apk信息 84 | const appCheckReq = await this.doRequest(this.postAsync, { 85 | url: "https://open.oppomobile.com/resource/publish/checkapp", 86 | formData: { 87 | apk_url: data["apk_url"], 88 | version_operation_type: data["version_operation_type"], 89 | app_id: appId, 90 | } 91 | }) 92 | const appCheckBody = JSON.parse(appCheckReq.body); 93 | if (appCheckBody.errno) throw new Error("检查APK包失败:"+appCheckBody.data.message) 94 | const appCheckData = appCheckBody.data; 95 | const replacedKey = ["app_name", "pkg_name", "apk_size", "min_sdk_version", "target_sdk_version","version_name", "version_code", "header_md5", "apk_md5", "sign"]; 96 | replacedKey.forEach(it=> { 97 | data[it] = appCheckData[it] 98 | }); 99 | 100 | ["package_permission", "package_permission_desc"].forEach(it=>{ 101 | data[it]=appCheckData[it].join(",") 102 | }) 103 | data["version_id"] = "" 104 | 105 | //提交审核 106 | const submitResult = await this.doRequest(this.postAsync, { 107 | url: "https://open.oppomobile.com/resource/update/index", 108 | formData: data 109 | }); 110 | 111 | const submitResultBody = JSON.parse(submitResult.body); 112 | if (submitResultBody.errno !== 0) { 113 | throw new Error("审核失败" + submitResultBody.data.message) 114 | } 115 | return true; 116 | } 117 | 118 | 119 | getName(): string { 120 | return "oppo"; 121 | } 122 | 123 | 124 | } -------------------------------------------------------------------------------- /src/QQPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import CookieCheckable from "./CookieCheckable"; 3 | const fs = require('fs'); 4 | const path = require("path"); 5 | export default class QQPublishHelper extends AndroidPublishHelper implements CookieCheckable{ 6 | 7 | 8 | async checkCookieAlive(): Promise { 9 | await this.refreshCookieFromZk(); 10 | const result = await this.doRequest(this.getAsync, { 11 | url: "http://op.open.qq.com/manage_centerv2/get_ad_config", 12 | qs: { 13 | ad_type: "manage_center" 14 | }, 15 | }); 16 | let alive = false; 17 | try { 18 | const body = JSON.parse(result.body); 19 | if (body.code === 0) alive = true; 20 | }catch{ 21 | alive = false; 22 | } 23 | return alive; 24 | } 25 | 26 | 27 | getToken(skey: string) { 28 | for (var e = skey || "", n = 5381, t = 0, i = e.length; i > t; ++t) 29 | n += (n << 5) + e.charCodeAt(t); 30 | return 2147483647 & n 31 | } 32 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 33 | if (!await this.checkCookieAlive()) { 34 | throw new Error("请先登录"); 35 | } 36 | const uin = await this.zk.getString("username"); 37 | const result = await this.doRequest(this.getAsync, { 38 | url: "http://op.open.qq.com/manage_centerv2/android", 39 | qs: { 40 | owner: uin, 41 | uin: uin, 42 | }, 43 | }) 44 | const content = result.body; 45 | let dataStrArr = content.match(/G_DATA=.*?}(,|;)/) 46 | if (dataStrArr.length === 0) { 47 | throw new Error("没有找到应用列表") 48 | } 49 | let dataStr = dataStrArr[0].substring("G_DATA=".length, dataStrArr[0].length-1) 50 | const appList = JSON.parse(dataStr); 51 | const appMap = {}; 52 | for (let key of Object.keys(appList)){ 53 | appList[key].forEach(it => { 54 | it.status = key; 55 | appMap[it.app_alias] = it; 56 | }) 57 | } 58 | const appId = appMap[cn_name].appid; 59 | const cookieMap = this.getCookieMap(); 60 | const skey = cookieMap["skey"] 61 | const apkPath = this.getApkPath(en_name, project_version); 62 | const fileName = path.basename(apkPath); 63 | const fileStat = fs.statSync(apkPath); 64 | //更新安装包 65 | const uploadReq = await this.doRequest(this.postAsync, { 66 | url: "http://op.open.qq.com/mobile_api/apkupload", 67 | formData: { 68 | uin: uin, 69 | skey: skey, 70 | token: this.getToken(skey), 71 | appid: appId, 72 | name: fileName, 73 | type: "application/vnd.android.package-archive", 74 | lastModifiedDate: new Date(fileStat.mtimeMs).toString(), 75 | size: fileStat.size, 76 | file: fs.createReadStream(apkPath), 77 | } 78 | }) 79 | return true; 80 | } 81 | 82 | getName(): string { 83 | return "qq"; 84 | // throw new Error("Method not implemented."); 85 | } 86 | 87 | 88 | } -------------------------------------------------------------------------------- /src/QihuPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import AppPublish from "Zookeeper"; 3 | import CookieCheckable from "./CookieCheckable"; 4 | const cheerio = require('cheerio'); 5 | const fs = require('fs'); 6 | const path = require("path"); 7 | export default class QihuPublishHelper extends AndroidPublishHelper implements CookieCheckable{ 8 | 9 | 10 | ZK_PREFIX="android_market.qihu"; 11 | qid="2354327295"; //不知含义,模拟请求一次checkCookieAlive里url,看一下各自的qid是啥 12 | appMap=new Map>(); 13 | constructor(zk: Zookeeper) { 14 | super(zk); 15 | } 16 | 17 | 18 | 19 | async checkCookieAlive(): Promise { 20 | await this.refreshCookieFromZk(); 21 | const result = await this.doRequest(this.getAsync, { 22 | url: 'http://dev.360.cn/mod3/mobile/Newgetappinfopage', 23 | qs: { 24 | "qid": this.qid, 25 | "page": "1", 26 | "page_size": "12", 27 | } 28 | }) 29 | let alive, data; 30 | try { 31 | data = JSON.parse(result.body).data as Array>; 32 | alive = data!= null; 33 | }catch(e){ 34 | alive=false; 35 | } 36 | 37 | if (alive){ 38 | data.list.forEach(r=> this.appMap.set(r["pname"], r)); 39 | } 40 | 41 | return alive; 42 | } 43 | 44 | async getAppKey(appId: string) { 45 | const result = await this.doRequest(this.getAsync, { 46 | url: "http://dev.360.cn/mod3/home/", 47 | qs: { 48 | "qid": this.qid, 49 | "appid": appId 50 | } 51 | }); 52 | const body = result.body; 53 | const lines = body.split("\n"); 54 | for (let i=0; i < lines.length; i++){ 55 | if (lines[i].indexOf("appkey")>-1) { 56 | const reuslt = lines[i+1].match(/[a-z0-9A-Z]{32}/) 57 | if (result.length>0) return result[0]; 58 | } 59 | } 60 | return null; 61 | } 62 | 63 | getTagString(datas){ 64 | return datas.map(data=>{ 65 | if (Array.isArray(data)) return data.join(",") 66 | return data; 67 | }).join("|"); 68 | } 69 | 70 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 71 | if (!await this.checkCookieAlive()) { 72 | throw new Error("请先登录"); 73 | } 74 | const packageName = package_name; 75 | if (!this.appMap.has(packageName)) await this.checkCookieAlive(); 76 | const appInfo = this.appMap.get(packageName); 77 | if (!appInfo) throw new Error("在市场上找不到该应用"); 78 | const appId = appInfo["appid"] 79 | const appKey = this.getAppKey(appId); 80 | if (!appKey) throw new Error("获取不到APPKey"); 81 | //软件分类硬编码待完善,分类仅限软件(apptype=soft, tag=1) 82 | const result = await this.doRequest(this.getAsync, { 83 | url: 'http://dev.360.cn/mod3/createmobile/app', 84 | qs: { 85 | id: appId 86 | } 87 | }); 88 | const $ = cheerio.load(result.body); 89 | const data = {}; 90 | this.fillDataFromInput($, data); 91 | this.fillDataFromTextarea($, data); 92 | this.fillDataFromSelect($, data); 93 | data["timed_pub"] = 0 //立即发布 这个radio 没有被checked 94 | //还有一部分信息在js中 95 | const lines = result.body.split("\n"); 96 | const informationInJS = 2; 97 | let tag=1; //1. 软件 2. 游戏 3. 电子书 98 | let tag1; 99 | let tag2; 100 | for (let i = 0, j = 0; i < lines.length && j < informationInJS; i++) { 101 | if (lines[i].indexOf("for_free = ") > -1) { 102 | const result = lines[i].match(/\d+/); 103 | if (result.length>0) data["is_free"] = result[0] 104 | j++; 105 | } else if (lines[i].indexOf('var key = ')>-1) { 106 | const result = lines[i].match(/"\d+":\d+,"\d+":\d+/); 107 | if (result.length>0) { 108 | const arr = result[0].split(","); 109 | const tags = result[0].replace(/[{}"]/g, '').split(/[,:]/); 110 | tag1=tags[2]; 111 | tag2=tags[3]; 112 | data['tag1']=tags[0]+","+tags[1]; 113 | data['tag2']=tags[2]+","+tags[3]; 114 | } 115 | j++; 116 | } 117 | } 118 | 119 | data["id"] = appId; 120 | data["apptype"] = "soft"; //不会有游戏吧 121 | if (desc && desc.length>0) 122 | data["edition_brief"] = desc//需要赋值 123 | const getFeatureTagResult = await this.doRequest(this.getAsync, { 124 | url: "http://dev.360.cn/mod/createmobile/GetFeaturetag", 125 | qs: { 126 | 'appid': appId 127 | } 128 | }) 129 | const featureTagBody = JSON.parse(getFeatureTagResult.body); 130 | if (featureTagBody.errno !== "0"){ 131 | throw new Error("获取特性标签失败"); 132 | } 133 | const featureOthers = featureTagBody.data.featuretag_selected.feature_other; 134 | const featureTags = featureTagBody.data.featuretag_selected.feature_tag; 135 | data["feature_other"] = this.getTagString(featureOthers); 136 | data["feature_tag"] = this.getTagString(featureTags); 137 | //获取AppTag比较麻烦,先不处理, tag从html里获取( 找selected),tag1,tag2从js中获取 //Util.getTag('/createmobile/tagapi',2); 在js中设置selected 138 | const getAppTagResult = await this.doRequest(this.getAsync, { 139 | url: "http://dev.360.cn/mod/createmobile/GetApptag", 140 | qs: { 141 | 'tag': tag, 142 | 'tag1': tag1, 143 | 'tag2': tag2, 144 | appid: appId 145 | } 146 | }) 147 | const appTagBody = JSON.parse(getAppTagResult.body) 148 | if (appTagBody.errno !== "0") { 149 | throw new Error("获取应用标签失败"); 150 | } 151 | data["common_tag"] = appTagBody.data.apptag_selected.common_tag.join(",") 152 | data["common_other"] = appTagBody.data.apptag_selected.common_other.join(",") 153 | const apkPath = await this.getApkPath(en_name, project_version); 154 | const fileName = path.basename(apkPath); 155 | const fileStat = fs.statSync(apkPath); 156 | const uploadResult = await this.doRequest(this.postAsync, { 157 | url: "http://upload.dev.360.cn/mod/upload/apk/", 158 | qs: { 159 | apptype: "soft", 160 | apkType: "Mobilecase", 161 | appid: appId, 162 | appkey: appKey, 163 | qid: this.qid 164 | }, 165 | formData: { 166 | name: fileName, 167 | type: "application/vnd.android.package-archive", 168 | lastModifiedDate: new Date(fileStat.mtimeMs).toString(), 169 | size: fileStat.size, 170 | Filedata: fs.createReadStream(apkPath) 171 | } 172 | }) 173 | 174 | const uploadResultBody = JSON.parse(uploadResult.body); 175 | if (uploadResultBody.status !== 0) { 176 | throw new Error("上传失败: " + uploadResultBody.error) 177 | } 178 | 179 | //新的上传文件信息 180 | for (let key in uploadResultBody.data) { 181 | if (key in data) { 182 | const value = uploadResultBody.data[key] 183 | if (!value) continue 184 | if (key === "sensitive_permission") { 185 | data[key] = Object.keys(value).join(",") 186 | } else { 187 | data[key] = value; 188 | } 189 | } 190 | } 191 | const submitResult = await this.doRequest(this.postAsync, { 192 | url: "http://dev.360.cn/mod3/createmobile/submit", 193 | formData: data 194 | }) 195 | const submitResultBody = JSON.parse(submitResult.body); 196 | if (submitResultBody.errno !== '0') { 197 | throw new Error("审核失败: "+submitResultBody.erro) 198 | } 199 | return true; 200 | } 201 | 202 | getName(): string { 203 | return "qihu"; 204 | } 205 | 206 | 207 | } -------------------------------------------------------------------------------- /src/SogouPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const cheerio = require('cheerio'); 5 | export default class SogouPublishHelper extends AndroidPublishHelper { 6 | 7 | 8 | getToken() { 9 | function e() { 10 | return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1) 11 | } 12 | return e() + e() + e() + e() + e() + e() + e() + e() 13 | } 14 | ZK_PREFIX = "android_market.sogou"; 15 | async login(){ 16 | const result = await this.postAsync({ 17 | url: "https://account.sogou.com/web/login", 18 | formData: { 19 | username: await this.zk.getString(`${this.ZK_PREFIX}.username`), 20 | password: await this.zk.getString(`${this.ZK_PREFIX}.password`), 21 | autoLogin: 1, 22 | xd: 'http://zhushou.sogou.com/open/jump.html', 23 | client_id: 1299, //模拟一次请求看一下你们的是啥 24 | token: this.getToken() 25 | } 26 | }); 27 | const cookies = result.headers["set-cookie"]; 28 | if (cookies && cookies.length>0) { 29 | this.cookie=cookies.map(it=> it.split(";")[0]).join(";") 30 | } else { 31 | throw new Error("登录失败"); 32 | } 33 | } 34 | 35 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 36 | const verDesc = desc; 37 | const apkPath = await this.getApkPath(en_name, project_version); 38 | const fileName = path.basename(apkPath); 39 | await this.login(); //测试中不要频繁登录 40 | //1.拉取应用列表,找到appId 41 | const appList = await this.doRequest(this.getAsync, { 42 | url: "http://zhushou.sogou.com/open/user/app/index.html" 43 | }); 44 | let $ = cheerio.load(appList.body) 45 | const appNameNodes = $('div[class=info-con]').find('span[class=name]'); 46 | const appIdNodes = $('div[class=info-con]').find('span[class=appid]'); 47 | const appNames = []; 48 | const appIds = []; 49 | for (let i = 0; i < appNameNodes.length; i++) { 50 | appNames.push(appNameNodes[i].firstChild.data); 51 | } 52 | for (let i = 0; i < appIdNodes.length; i++) { 53 | appIds.push(appIdNodes[i].firstChild.data.split(/\s+/)[1]) 54 | } 55 | const appName = cn_name; 56 | const idx = appNames.findIndex(it=>it===appName); 57 | if (idx === -1 || idx >= appIds.length) throw new Error("没有找到该应用") 58 | const appId = appIds[idx]; 59 | //解析完获取到appId 60 | //2. 获取到app基础信息 61 | const appInfoReq = await this.doRequest(this.getAsync, { 62 | url: "http://zhushou.sogou.com/open/app/update.html", 63 | qs: { 64 | id: appId 65 | } 66 | }); 67 | const data = {} 68 | $ = cheerio.load(appInfoReq.body); 69 | this.fillDataFromInput($, data) 70 | this.fillDataFromSelect($, data) 71 | this.fillDataFromTextarea($, data) 72 | if (Object.keys(data).length === 0) { 73 | let msg = appInfoReq.body.indexOf("审核中") > -1 ? "应用正在审核中" : "获取应用基础信息失败"; 74 | throw new Error(msg); 75 | } 76 | data["changelog"]=(verDesc&&verDesc.length>0) ? verDesc : data["update_info"]; 77 | data["file_icon"]=$('div.icon').find('img')[0].attribs.src; 78 | data["label"] = $('div.labels').find('input[type="hidden"]')[0].attribs.value 79 | const tagInputs = $('div.tags').find('input[type="hidden"]'); 80 | const tags=[]; 81 | for (let i =0; i< tagInputs.length;i++){ 82 | tags.push(tagInputs[i].attribs.value) 83 | } 84 | data["tags[]"] = tags 85 | delete data["versioncode"] 86 | if (!("qualification[]" in data )) { 87 | data["qualification[]"] = "" 88 | } 89 | data["type"] = 0;//这个type应该是文件相关的,不知道是不是指的文件类型,都先统一为0 90 | //3. 上传文件 91 | const fileStat = fs.statSync(apkPath); 92 | const uploadReq = await this.doRequest(this.postAsync, { 93 | url: "http://zhushou.sogou.com/open/", 94 | qs: { 95 | route: "upload.app", 96 | rand: new Date() 97 | }, 98 | formData: { 99 | name: fileName, 100 | type: "application/vnd.android.package-archive", 101 | lastModifiedDate: new Date(fileStat.mtimeMs).toString(), 102 | size: fileStat.size, 103 | Filedata: fs.createReadStream(apkPath), 104 | auth_sid: data["auth_sid"], 105 | uid: data["user_id"], 106 | token: data["token"], 107 | } 108 | }) 109 | const uploadReqBody = JSON.parse(uploadReq.body); 110 | if (!uploadReqBody.file_id) throw new Error("上传失败:"+uploadReqBody.msg); 111 | data["file_id"] = uploadReqBody.file_id 112 | //4. 提交 113 | const submitReq = await this.doRequest(this.postAsync, { 114 | url: "http://zhushou.sogou.com/open/app/update.html", 115 | qs: { 116 | id: appId 117 | }, 118 | formData: data 119 | }) 120 | if (submitReq.status != 200) { 121 | throw new Error("提交审核失败"); 122 | } 123 | return true; 124 | } 125 | 126 | getName(): string { 127 | return "sogou"; 128 | } 129 | 130 | 131 | } -------------------------------------------------------------------------------- /src/VivoPublishHelper.ts: -------------------------------------------------------------------------------- 1 | import AndroidPublishHelper from "./AndroidPublishHelper"; 2 | import CookieCheckable from "./CookieCheckable"; 3 | const fs = require('fs'); 4 | const cheerio = require('cheerio'); 5 | export default class VivoPublishHelper extends AndroidPublishHelper implements CookieCheckable{ 6 | appMap = new Map(); 7 | //cookie有效期是session,我们保持心跳请求,保证服务器session不过期就可以一直存活。 8 | //除非对方服务器重启,并且session没有序列化。或者我们太久没有请求. 9 | async checkCookieAlive(): Promise { 10 | await this.refreshCookieFromZk(); 11 | const result = await this.doRequest(this.getAsync,{ 12 | url: "https://dev.vivo.com.cn/webapi/app/page-list", 13 | qs: { 14 | currentPageNum: 1, 15 | cnName:"", //以下两个参数可以为空值,但不能不填 16 | appType:"" 17 | } 18 | }); 19 | let alive, body; 20 | try { 21 | body = JSON.parse(result.body); 22 | alive = body.data && body.code === 0 ? true : false; 23 | }catch{ 24 | alive = false; 25 | } 26 | if (alive) { 27 | const data = body.data.data; 28 | data.forEach(it=>{this.appMap.set(it["packageName"], it)}) 29 | } 30 | return alive; 31 | } 32 | 33 | async publish(cn_name: string, en_name: string, package_name: string, project_version: string, desc: string, extra: any) { 34 | if (!await this.checkCookieAlive()) { 35 | throw new Error("请先登录"); 36 | } 37 | // 1. 获取app信息 38 | const vivoApp = this.appMap.get(package_name); 39 | if (!vivoApp){ 40 | throw new Error(`没有找到到包名为${package_name}的应用`); 41 | } 42 | const appId = vivoApp["id"]; 43 | const apkPath = await this.getApkPath(en_name, project_version); 44 | const verDesc = desc; 45 | if(!appId) throw new Error("没有找到到该应用"); 46 | const appInfo = await this.doRequest(this.getAsync, { 47 | url: "https://developer.vivo.com.cn/application/manage/editApplicationPage", 48 | qs: {"appInfo.id": appId} 49 | }) 50 | 51 | const data:any = {} 52 | const $ = cheerio.load(appInfo.body); 53 | this.fillDataFromInput($, data); 54 | this.fillDataFromSelect($, data) 55 | this.fillDataFromTextarea($, data) 56 | 57 | data.operateType = data.operateType === 4 ? 3 : data.operateType; 58 | //2. 上传文件 59 | data.uploadify = fs.createReadStream(apkPath); 60 | Object.keys(data).forEach(key=>{ 61 | if (!data[key]) data[key]="" 62 | }) 63 | const uploadReq = await this.doRequest(this.postAsync, { 64 | url: 'https://developer.vivo.com.cn/upload/apk/application', 65 | qs: { 66 | appId: appId, 67 | operateType: data.operateType, 68 | tokenVerify: data.tokenVerify 69 | }, 70 | formData: data 71 | }) 72 | const uploadResult = JSON.parse(uploadReq.body); 73 | if (uploadResult.code != 1){ //"1" == 1 true, "1" === 1 false 74 | throw new Error("上传失败: "+data.errorCodeMsg.errorMsg) 75 | }; 76 | const uploadResultObj = uploadResult.object; 77 | data.uuid = uploadResultObj["uuid"] 78 | data["appInfo.sensitivePermissionList"] = JSON.stringify(uploadResultObj.sensitivePermissionList) 79 | data["appInfo.sensitivePermissionParam"] = data["appInfo.sensitivePermissionList"] 80 | if (verDesc && verDesc.length > 0) data["appInfo.updateDes"] = verDesc; 81 | delete data["uploadify"] 82 | const submitReq = await this.doRequest(this.postAsync, { 83 | url: "https://developer.vivo.com.cn/application/manage/editApplication", 84 | formData: data 85 | }); 86 | 87 | const submitResult = JSON.parse(submitReq.body); 88 | if (submitResult.code != 1) { 89 | throw new Error("提交审核失败: " + data.errorCodeMsg.errorMsg) 90 | } 91 | return true; 92 | } 93 | 94 | getName(): string { 95 | return "vivo"; 96 | } 97 | 98 | 99 | } -------------------------------------------------------------------------------- /src/cookie.txt: -------------------------------------------------------------------------------- 1 | cookie的格式是Key1=Value1;Key2=Value2; 2 | 以下的Cookie均不可以使用,只是告知最小cookie的key value值 3 | 360(需要Q, T): 4 | Q=u%3D360H2754327195%26n%3D%126le%3DqTIwnPH0ZUO1pUIgAioD%3D%3D%26m%3DZGZlWGWOWGWOWGWOWGWOWGWOZwtj%26qid%3D2754327195%26im%3D1_t011655040b3ed000bf%26src%3Dpcw_open_app%26t%3D1; T=s%3D28a061243e88facca140ff54e75b2ec4%2273868966%26lm%3D%26lf%3D4%26sk%3D986395778fdf8b8caebe3b9c89c257cd%26mt%3D1573868966%26rc%3D%26v%3D2.0%26a%3D1; 5 | meizu(需要JSESSIONID): 6 | JSESSIONID=m331os58w8dtqj11jk95k 7 | oppo(需要opkey): 8 | opkey=eyJpdiI6I12356BScGF3RUlBclNNZHc9PSIsInZhbHVlIjoibEdla05cL0pwYU5aSXZ5YlMzcmVrM01uQUV4MWdKam5MMW40TENYbFJ0d0grdnNSTzRpSGU2YXpjeldobDVEYmNUWWlOMUNPdENLcmxNakE3Y3E0dDV1UWNzblZWVGduNlwvdGNhZENlV2ZTM05NTUFnSklVUEtkNGhOd3NNZ2dZViIsIm1hYyI6IiJ9 9 | qq(需要uin,skey) 10 | uin=o2891674648; skey=@NHcpLEZFe 11 | vivo(需要b_account_username, b_account_aid, b_account_token, b_account_salt) 12 | b_account_username=iWDZpib6bk7WwMJLrb%cBG%2BA%3D%3D;b_account_aid=STcZ4ibRvAA%3D;b_account_token=1764aecc79981dc924cf6149988242df.1575694217352;b_account_salt=l8A3V1hiiqn4fbJzhXKirg%3D%3D.1575694217352 -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import HuaweiPublishHelper from "./HuaweiPublishHelper"; 2 | import MeizuPublishHelper from "./MeizuPublishHelper"; 3 | import MiPublishHelper from "./MiPublishHelper"; 4 | import OppoPublishHelper from "./OppoPublishHelper"; 5 | import VivoPublishHelper from "./VivoPublishHelper"; 6 | import SogouPublishHelper from "./SogouPublishHelper"; 7 | import QQPublishHelper from "./QQPublishHelper"; 8 | import QihuPublishHelper from "./QihuPublishHelper"; 9 | let zk; 10 | const androidPublishHelpers=[ 11 | new HuaweiPublishHelper(zk), 12 | new MiPublishHelper(zk), 13 | new MeizuPublishHelper(zk), 14 | new OppoPublishHelper(zk), 15 | new VivoPublishHelper(zk), 16 | new SogouPublishHelper(zk), 17 | new QQPublishHelper(zk), 18 | new QihuPublishHelper(zk) 19 | ] 20 | Promise.all( 21 | androidPublishHelpers.map(it=>it.publish("测试", "test", "cn.test", "2.6.7", "1. 更新xxx\n2.修复xxx", null)) 22 | ) 23 | --------------------------------------------------------------------------------