@huoye/app-ship 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ # 本机发布凭据配置
2
+
3
+ 本功能由火叶工作室自行实现。Flu-CLI/Core 和现有 VS Code 插件接入华为 Android、鸿蒙、蒲公英、荣耀、魅族、三星、小米、OPPO、vivo、应用宝、App Store、Google Play 全部十二个平台的本机凭据流程;MCP 仍只开放原有三平台。UI 沿用已有布局与组件,上传保持现有人工确认边界。
4
+
5
+ ## 使用条件
6
+
7
+ - 安全存储首批支持 macOS;MCP/Native Host 使用 Node.js 20 及以上,AI Studio 沿用现有 Node.js 24。
8
+ - 浏览器助手使用 Chromium Manifest V3、`activeTab`、`scripting`、`nativeMessaging`,没有全站访问、Cookie、网页存储或历史权限。
9
+ - 独立 CLI 要求项目已有 `flu_release.yaml` / `flu_release.yml`,对应渠道填写真实 `packageName`,不要求启用上传。插件可从商店配置已保存的包名创建公开目标;保留已有 `enable`,新目标默认 `false`。获取凭据和在线体检都不要求在打包上传中勾选渠道,也不改变打包模式或选择。App ID 和 `credentialProfile` 可以缺省;不猜包名,不配置 SDK 或签名。
10
+ - 体检访问固定 HTTPS 接口,华为首批为默认中国区。只读鉴权与应用访问成功不能证明实际上传、送审或上架权限。
11
+
12
+ ## 插件完整流程(2026-10-19)
13
+
14
+ 1. 打开“发布中心 → 商店配置 → 当前项目”,选择上述十二个平台,保存真实包名(Apple 为 Bundle ID)及必要的公开参数。项目 YAML 中有旧明文凭据时,先用原安全迁移入口处理;未保存的表单不能开始采集。
15
+ 2. 首次点击“浏览器助手设置 → 安装浏览器助手”。插件复制并展示随包目录;在 Chrome / Edge 扩展管理页启用开发者模式并加载该目录,复制浏览器分配的扩展 ID。
16
+ 3. 再点“浏览器助手设置 → 连接到 Flu”,选择已加载助手的浏览器并粘贴扩展 ID。核对原生确认框中的实际注册文件、启动器路径后点击“连接到 Flu”;取消不会写入注册。复用当前 IDE 的运行时,无需额外下载 Node 或依赖。
17
+ 4. 点击“自动获取凭据”,插件将官方控制台交给已注册的 Chrome / Edge。用户自行登录,打开已有凭据页面并点击浏览器中的“Flu 发布凭据助手”;只有明确可见字段可读取。华为/鸿蒙/Google JSON、三星 PEM、Apple P8 和小米公钥/证书通过助手自身文件选择器导入;缺文件时停止,不能恢复一次性私钥。
18
+ 5. 编辑器每两秒跟进等待中的任务;材料采集完成后自动核验一次。蒲公英要求只读鉴权通过,其余渠道另要求明确目标应用匹配,成功才保存公开 Profile 引用及适用的 App ID,并同步已有表单快照。认证失败或目标冲突停止自动核验,保留“重试核验”供显式重试。浏览器未能打开时取消该次等待任务。
19
+ 6. 插件只展示一个获取入口:尚无 Profile 时为“自动获取凭据”,已有 Profile 时为“重新获取凭据”。后者创建新 Profile,成功后只替换当前项目引用,旧 Profile 保留;只想核验当前账号时使用“在线体检”,按钮显示处理中并在旁边返回成功或失败原因。独立 CLI 未指定 `--refresh-existing` 时仍复用已有 Profile。已有任务草稿继续保留,需要刷新发布配置或点击“填入打包表单”后使用,不会启动任务。
20
+
21
+ “当前项目”只修改当前项目记录;“全局默认”供本机其他未选择独立账号的项目复用。全局记录目前也包含包名、App ID 等参数,使用前须按应用核对。蒲公英 API Key 可由同一账号的多个项目使用,体检只证明该 Key 的只读鉴权,不证明特定应用或上传权限。已有蒲公英 Profile 时常态显示安全保存状态,手动字段收在“手动配置”中;手填 API Key 与 Profile 只选一种来源。
22
+
23
+ 手填账号入口继续保留。插件不创建或重置平台密钥,不绕过登录、页面授权或上传确认;自动配置与在线体检只适用于当前项目。材料不进入 Webview 返回值或项目 YAML,上传时由可信宿主解析 Profile;引用失败不退回旧账号。
24
+
25
+ 十二渠道共用流程使用模拟供应商响应、合成材料和隔离目录验证,并检查独立 Native Host 及当前 IDE 运行时兼容性。此前已按用户授权,在 Chrome“之夜”和当前 IDE 完成华为/鸿蒙的真实采集、只读鉴权与应用匹配,以及蒲公英 API Key 的真实采集与只读鉴权;其余九家的真实页面/账号仍待验证。该结果不代表实际上传权限已通过,未真实上传。可用根仓 `npm run smoke:credentials -w flu-cli-vscode` 重跑模拟闭环(先构建扩展资源)。
26
+
27
+ ## 十二渠道材料与只读边界(2026-10-19)
28
+
29
+ | 渠道 | 已有材料 | 只读核验 |
30
+ | ------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
31
+ | 华为 Android | 开发者级 Client ID/Secret,或 Service Account JSON | 中国区 appid-list,packageTypes=1,唯一 App ID |
32
+ | 鸿蒙 | 独立任务采集同类开发者凭据或 JSON | packageTypes=7,唯一 App ID |
33
+ | 蒲公英 | API Key,不读 User Key | listMy 鉴权,不匹配 App ID |
34
+ | 荣耀 | Client ID/Secret | 鉴权、按包名匹配应用 |
35
+ | 魅族 | Client ID/Secret | 鉴权、分页唯一应用匹配 |
36
+ | 三星 | Service Account ID + 已下载 RSA 私钥 | publishing 鉴权、应用列表及 binaryList 包名匹配 |
37
+ | 小米 | 开发者邮箱 + API 访问密码 + 平台公钥/证书 | POST dev/query,RequestData MD5、RSA PKCS1 加密 SIG;核对 packageInfo.packageName |
38
+ | OPPO | 我的 API 中 client_id/client_secret | GET token、HMAC-SHA256 app/info;核对 pkg_name/app_id |
39
+ | vivo | 个人信息中 access_key/accessSecret | HMAC-SHA256 app.query.details;检查 code/subCode、packageName |
40
+ | 应用宝 | 主账号 user_id、app_id、access_secret | HMAC-SHA256 query_app_detail;请求 App ID 与返回 pkg_name 绑定 |
41
+ | App Store | 团队 .p8 + Issuer ID/Key ID;已有 Apple ID/App 专用密码供当前 CLI 上传 | ES256,GET apps 按 Bundle ID 精确过滤,唯一数字 App ID;不验证 App 专用密码 |
42
+ | Google Play | 已下载服务账号 JSON | RS256 换 OAuth token,GET applications/{packageName}/deviceTierConfigs?pageSize=1;不创建 edit |
43
+
44
+ Apple、Google、三星及华为服务账号的私钥不能从后台恢复。助手不生成、重置密钥或扩权;文件只经 Native Messaging 进入现有安全库。小米“私钥”是 API 访问密码,不能用登录密码替代;已有公钥支持 PEM 与 DER 证书。Apple 保留当前 CLI 账号上传方式,API Key 核验不证明 CLI 密码或上传权限有效;仅有 CLI 账号时体检返回 unsupported_credential_method,不把账号误判为无效。个人 API Key 未纳入此助手。
45
+
46
+ 小米、OPPO、vivo、应用宝的字段协议已按官方文档核对,实际登录页 DOM 尚无本批真实联调证据;仅在对应官方站点的明确可见标签唯一时读取。页面变化、遮盖、多个候选均停止,入口不保证绕过供应商登录后导航。三星、荣耀、魅族也仍待真实联调。vivo 总规范写 HMAC-SHA256,详情页仍有旧 hmac 文案;本轮沿用总规范,不自动试多套算法,真实鉴权待验证。
47
+
48
+ Google 的应用匹配依据是包名限定 GET 成功,空配置集合合法;不回传供应商原文。2026-10-19 的真实核验观察到 OAuth token 返回 200,固定 `deviceTierConfigs.list` GET 返回 204、无正文。按 [RFC 9110 §15.3.5](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.5) 的成功无正文语义,仅该固定查询将 204 归一为空对象;这是对真实响应的兼容,官方接口文档没有单独承诺 204 形状。token 仍必须有有效 `access_token`,其他接口不接受空正文;401/403/404、畸形/超大响应仍失败,不添加新权限或更换端点。定向回归分别覆盖 JSON 空集合、Google GET 204 完成公开绑定、token 204 和其他平台空响应拒绝。
49
+
50
+ 权限不足、应用不存在、业务/HTTP 错误、畸形/超大响应、身份冲突和取消均停止,不切换账号或调用写接口。Apple 无结果或多结果均停止。全部只读核验保持 `uploadPermission: not_verified`。
51
+
52
+ 本地证据:`smoke:all-store-credentials` 验证新增六家的实际签名、安全库及失败门禁;`smoke:browser-credentials` 使用合成 DOM/Chrome 与正式 popup 验证标签、检查零材料及文件导入;根仓 `smoke:credentials` 覆盖十二渠道宿主跟进、复用与重新采集。供应商响应和系统密钥库为隔离替身,不能替代真实账号或用户验收。
53
+
54
+ 官方依据(2026-10-19):[小米自动发布接口](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1134)、[OPPO 认证](https://open.oppomobile.com/documentation/page/info?id=10998)、[OPPO 详情](https://open.oppomobile.com/documentation/page/info?id=11004)、[vivo 接入](https://dev.vivo.com.cn/documentCenter/doc/327)、[vivo 查询详情](https://dev.vivo.com.cn/documentCenter/doc/346)、[应用宝主账号 API](https://wikinew.open.qq.com/index.html#/iwiki/4015262492)、[Apple Key 下载限制](https://developer.apple.com/help/app-store-connect/get-started/app-store-connect-api/)、[Apple 应用查询](https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps)、[Google 密钥文件](https://docs.cloud.google.com/iam/docs/keys-create-delete)、[Google 包名限定只读接口](https://developers.google.com/android-publisher/api-ref/rest/v3/applications.deviceTierConfigs/list)。OPPO、vivo、应用宝动态正文另通过官方页面实际渲染读取;这是公开文档证据,非真实账号调用。
55
+
56
+ ## 华为新版凭据页适配(2026-10-18)
57
+
58
+ - 官方 Connect API 的“获取服务端授权”说明要求使用**开发者级**服务账号或 API 客户端。实际登录后的入口是“开发者管理 → ID、证书和 Profile → 凭证 → 开发者级凭证”;项目级、应用级页签不能据此当作发布 API 凭据。
59
+ - 默认打开 `https://developer.huawei.com/consumer/cn/hdc-console/index.html#/home/101653523124950083`。Native Host 仅为此精确凭据路由补充支持,继续拒绝任意 HDC 页面、登录回调和仿冒域名;旧 AGC 页面仍兼容。
60
+ - 当前页面使用可见的同源 `/consumer/cn/consoleIframe/api/credentials/1` 框架,以及 `nz-table` 内拆分的 Ant Design 表头/正文。助手按“凭证名称 / 凭证ID / 密钥 / 角色”定位开发者级 API 客户端表格,并按完整一行采集,不跨行组合账号。
61
+ - 助手打开时只检查公开名称和创建时间。只有一条客户端时自动选中;多条时由用户明确选择。点击读取后,ID 和密钥只经 Native Messaging 进入本机安全库,页面切换或选择信息变化时停止。被遮盖、隐藏、密码框或无法唯一识别的值均不采集;请由用户在华为页面显示已有密钥,再重新打开助手。
62
+ - Service Account 私钥由华为在创建时下载并交给用户保管,网页不能恢复;继续使用助手的已有 JSON 导入入口。无需为验证采集而创建或重置账号。
63
+ - 只有对中国区 Connect API 的鉴权与按真实包名匹配 App ID 都通过,才替换项目公开 Profile 引用。只读核验不宣称真实上传权限已经验证。本批继续禁止真实上传、送审或发布。
64
+
65
+ 真实纵向验证:Chrome“之夜”助手已从用户已有的 `FluCli-upload` API 客户端采集材料,IDE 自动只读核验并为 Hereveil Android 保存公开 Profile 引用,App ID/包名保持一致。只记录成功状态与公开标识,未记录密钥值;未创建或重置平台凭据,未上传安装包。其他真实账号/平台不能由本条结果推断。
66
+
67
+ 官方依据:[获取服务端授权](https://developer.huawei.com/consumer/cn/doc/doccenter-connect-api/agc-help-connect-api-obtain-server-auth-0000002271134661)、[应用级 OAuth 说明](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/open-platform-oauth-0000001053629189)。本地定向检查为 `npm run smoke:browser-credentials --workspaces=false`,使用合成 DOM、合成密钥和模拟 Chrome 边界;真实材料与接口结果另按实际证据记录。
68
+
69
+ ## 蒲公英新版 API 信息页适配(2026-10-18)
70
+
71
+ - 当前 Chrome“之夜”的官方 `/account/api` 入口登录后转到 `/dashboard/?account=api&agKey=<应用组标识>`。仅接受 `pgyer.com` / `www.pgyer.com` 的 HTTPS API 信息路由;`agKey` 可省略,存在时须为单个 32 位十六进制值。拒绝其它账号页签、重复参数、登录回调参数、片段和仿冒域名,保留旧精确入口兼容。
72
+ - 真实页面把 API Key、User Key 分放在独立卡片;只按 `复制API Key` 按钮定位同级 `API Key` 标签与唯一可见、无子元素的 `code`。不点击复制或重置,不读取 User Key、Cookie、网页存储或页面接口。打开助手仅核对结构,明确点击读取后才传材料;隐藏、遮盖、额外候选或重复卡片会停止。
73
+ - 现有只读鉴权使用固定 `POST https://www.pgyer.com/apiv2/app/listMy`,只有 `code=0` 才保存公开 Profile。官方接口返回应用列表,但结果不回传插件/MCP;本步骤不匹配特定应用,也不能证明上传权限。
74
+ - `smoke:browser-credentials` 使用合成 DOM 与正式弹框验证新版路由、API/User Key 隔离、公开检查零材料及拒绝分支;原三渠道配置 smoke 与六渠道插件 smoke 继续通过。复用现有 Node、Chrome、IDE 和随包 Native Host,未增加依赖、浏览器权限或新的网络调用。
75
+ - 真实纵向流程已在当前 IDE 完成:助手读取已有 API Key,经本机管道进入蒲公英专用 Profile,IDE 自动只读鉴权并显示“凭据已核验并配置”。只保存公开引用,材料未进入模型、项目 YAML 或日志;未创建/重置远端密钥或真实上传。该结果只证明此账号材料的只读鉴权,其他账号及上传仍须各自验证。
76
+
77
+ 官方依据:[API 2.0 鉴权](https://www.pgyer.com/doc/en/view/api)、[查询已上传应用](https://www.pgyer.com/doc/en/view/api_app)。页面结构为本轮实际浏览器观察,非官方稳定 DOM 契约;后续结构变化应停止采集并重新核对。
78
+
79
+ ## 独立 CLI 首次准备
80
+
81
+ 1. 在浏览器扩展管理页加载本包的 `browser-extension` 目录,取得实际扩展 ID。
82
+ 2. 在 App Ship 包目录运行以下命令预览本机桥接注册;核对后追加 `--apply` 才会注册。Edge 使用 `--browser edge`。
83
+
84
+ ```bash
85
+ node scripts/register-browser-host.mjs --extension-id <实际扩展ID> --browser chrome
86
+ ```
87
+
88
+ 脚本默认使用 App Ship 构建入口;插件随包注册时通过 `--native-entry` 指定独立 Native Host,并记录对应浏览器。脚本不安装扩展、不读取商店凭据,拒绝覆盖不同扩展身份。当前注册仅绑定一个明确扩展 ID。本次开发不会自动修改浏览器或注册系统桥接。
89
+
90
+ 注册文件为 `~/.app-ship/browser-connector.v1.json`、`~/.app-ship/browser-native-host` 和浏览器的 `NativeMessagingHosts/cn.huoye.app_ship.credentials.json`。卸载时先停用扩展,再核对并仅处理这些注册文件;不要删除凭据 vault 或 Keychain Profile。
91
+
92
+ ## Flu-CLI 新增渠道(2026-10-17)
93
+
94
+ | 渠道 | 已有凭据 | 应用匹配与上传终点 |
95
+ | -------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------- |
96
+ | 荣耀 `honor` | 页面明确可见的 Client ID / Client Secret | 按包名匹配唯一 App ID,APK 绑定草稿,不送审 |
97
+ | 魅族 `meizu` | 页面明确可见的 Client ID / Client Secret | 分页查询唯一应用,签名上传 APK 文件;尚未绑定版本,不发布 |
98
+ | 三星 `samsung` | Service Account ID + 已下载 RSA 私钥(PEM/key) | 账号需 publishing 权限;查询既有应用的 binaryList,创建/更新草稿并添加 APK,不送审 |
99
+
100
+ 三星私钥只在创建时可下载;助手不会恢复丢失的密钥、生成新密钥或重置账号。用户在扩展自身页面选择已有文件,材料由本机 AES-256-GCM vault 保存,不进入网页、项目 YAML 或 AI。多安装包三星应用必须明确填写 `binarySeq`,`gms` 留空继承该安装包配置;应用审核中或无法唯一继承时停止。
101
+
102
+ 已有 Flutter 工程先在原打包上传页面勾选渠道、填写包名并保存;也可直接编辑公开配置。三家首批均仅接收一个明确的 APK;魅族拒绝包含 32 位原生库的 APK。渠道应用须先在对应商店后台建立。插件单次 Android 构建目标沿用原实现:新增渠道与 Google Play 的 AAB 构建需分批执行,或在仅上传模式提供 APK 与 AAB;Flu CLI 可按渠道同时生成两种格式。
103
+
104
+ ```yaml
105
+ publish:
106
+ honor:
107
+ enable: true
108
+ packageName: dev.example.app
109
+ # 自动完成后写入 appId 和 credentialProfile,不写入明文秘密
110
+ ```
111
+
112
+ 使用 Node.js 20+,首次浏览器桥接准备见上文。命令为本地调用,不启动或依赖 MCP。
113
+
114
+ ```bash
115
+ flu-cli credentials setup -c flu_release.yaml --platform honor
116
+ flu-cli credentials status --setup-id <任务ID>
117
+ flu-cli credentials complete --setup-id <任务ID>
118
+ flu-cli credentials preflight -c flu_release.yaml --platform honor
119
+ flu-cli credentials cancel --setup-id <任务ID>
120
+ # 明确更新已有账号引用;原 Profile 保留供其他项目使用
121
+ flu-cli credentials setup -c flu_release.yaml --platform honor --refresh-existing
122
+ ```
123
+
124
+ `meizu`、`samsung` 替换平台名即可。独立 `app-ship credentials` 提供同名命令。插件十二渠道完整操作见上文;独立 CLI 采集后仍需显式执行 complete。自动配置和体检首批针对项目范围,全局/临时账号仍可手动配置并在上传时核验。只有明确标签且真实可见的字段可以读取;遮盖、多候选或未知页面结构会停止,保留手动输入入口。
125
+
126
+ 上传可用 `flu-cli upload --only-upload --platform honor --file /path/app.apk`。确认窗口/命令提示绑定平台、动作、App ID、包名、实际 APK 版本、大小、SHA-256 与公开设备配置。拒绝确认时不读取 Profile 或开始商店调用;确认后复核产物、凭据与磁盘配置,再精确匹配远端应用,失败不自动重试。荣耀和三星显示草稿结果,魅族显示“文件已上传,尚未绑定版本”。只读体检返回 `uploadPermission: not_verified`,不把鉴权成功当作实际上传权限已验证。
127
+
128
+ 新增渠道尚未进行真实账号/页面和上传联调。隔离验证使用合成响应、临时 APK/aapt 和内存系统密钥替身;已运行真实本机签名、加密文件读写、命令进程、执行器与回执,并非真实商店成功证据。MCP 是否开放新增渠道,待渠道验收后另行决定。
129
+
130
+ 官方协议依据(2026-10-17 核对):
131
+
132
+ - [荣耀应用市场 API](https://developer.honor.com/cn/doc/guides/101359):Client Credentials、按包名查应用、上传与绑定;不调用 `submit-audit`。
133
+ - [魅族开放接口](https://open.flyme.cn/docs?id=333):Token、traceId/clientId/timestamp/uri 排序 SHA-256 签名、分页应用列表及 APK 上传;不调用 `app/publish`。
134
+ - [三星 Access Token](https://developer.samsung.com/galaxy-store/galaxy-store-developer-api/create-an-access-token.html):RS256、publishing scope、20 分钟 JWT。
135
+ - [三星应用信息](https://developer.samsung.com/galaxy-store/galaxy-store-developer-api/content-publish-api/view-sellers-app-details.html)、[更新草稿](https://developer.samsung.com/galaxy-store/galaxy-store-developer-api/content-publish-api/modify-app-data.html)、[添加安装包](https://developer.samsung.com/galaxy-store/galaxy-store-developer-api/content-publish-api/add-new-binary.html):保留应用现有语言、价格、发布方式,沿用已有安装包设备范围;不调用 `contentSubmit`。
136
+
137
+ 验证入口:App Ship `npm run smoke:additional-stores --workspaces=false`;根仓 `npm run smoke:additional-stores -w flu-cli-vscode`(先构建 App Ship/Core)。
138
+
139
+ ## 火叶 MCP 流程
140
+
141
+ 1. `huoye_credentials_setup({projectPath, platform})`:平台为 `huawei` / `harmony` / `pgyer`。创建十分钟采集任务;已有 Profile 则复用。不要求完整构建环境或 SDK。
142
+ 2. 完成商店登录后点击助手并选择任务。已有 API 客户端和蒲公英 API Key 只在字段明确标注且可见时读取;被遮盖、多候选或页面不支持时停止。
143
+ 3. 已有华为/鸿蒙 Service Account JSON 从扩展自身的文件选择器导入,不交给商店页面或 AI。用户自行创建及下载,助手不新建、重置或扩权。
144
+ 4. `huoye_credentials_status({setupId})` 查询本会话状态;`huoye_credentials_complete({setupId})` 在线核验并保存公开配置。
145
+ 5. `huoye_publish_preflight({projectPath, platform})` 重新体检,只读,不保存或批准上传。
146
+
147
+ 凭据错误时可显式设置 `huoye_credentials_setup({projectPath, platform, refreshExisting: true})`。它创建新 Profile,核验后只更换当前项目引用;旧 Profile 保留,不覆盖其他项目共用的凭据。
148
+
149
+ 华为使用 `packageTypes=1` 匹配 Android,鸿蒙使用 `packageTypes=7` 匹配 APP 应用/元服务。不存在、多结果或已有 App ID 冲突均停止,不取第一条猜目标。蒲公英不需要提前注册 App ID,体检只证明 API Key 鉴权。
150
+
151
+ 配置完成只写公开凭据引用、App ID 与已有表单快照中的包名;配置变化、取消、关闭或过期均阻止继续完成。标量秘密由系统安全库保护,Service Account 文件由既有 AES-256-GCM vault 加密。磁盘采集任务只含元数据。多进程安全库写入采用排他锁,冲突明确失败,不覆盖或自动重试;异常退出的遗留锁需核对没有在途写入后处理,不自动强行解锁。
152
+
153
+ 本批不扩展火叶 MCP 的鸿蒙构建流程。真正上传仍使用现有具体计划与人的当次确认。
154
+
155
+ ## 在插件中查看本机配置
156
+
157
+ 商店配置的“查看已保存凭据”可只读打开当前商店、当前保存范围的账号和文件内容;“保存位置”说明实际使用的项目配置、加密库、钥匙串条目或外部文件,并提供文件管理器定位。导入的 JSON/私钥已加密入库时,显示正在使用的凭据库,不伪造未保留的原始导入路径。
158
+
159
+ 普通 `local_display` 回显仍不解密文件。只有用户明确点击查看后,可信本机宿主才使用 `local_view` 读取该档案,正文仅交给当前只读弹窗;关闭、切换范围或离开编辑器后清空,迟到回复丢弃。查看不生成明文文件、不改账号、不访问平台;导出用途仍拒绝,MCP 目录与返回值不增加秘密或本机路径。网页预览仅展示标注过的虚拟位置和合成内容,不读取本机安全库。
160
+
161
+ ## 协议与验证
162
+
163
+ 核对日期:2026-10-16。只参考功能,未引入 apkgo 或其助手代码。
164
+
165
+ - [华为授权](https://developer.huawei.com/consumer/cn/doc/doccenter-connect-api/agc-help-connect-api-obtain-server-auth-0000002271134661):Service Account 使用 PS256、固定 audience、3600 秒 JWT,同时兼容 API 客户端。
166
+ - [鸿蒙应用 ID 查询](https://developer.huawei.com/consumer/cn/doc/doccenter-connect-api/agc-help-publish-api-appid-list-0000002271000617):固定 Publishing API 和软件包类型过滤。
167
+ - [Android 应用 ID 查询](https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-References/agcapi-appid-list-0000001111845086):`Pair.key` 是应用名称;按单包名查询后只接受唯一 App ID,不拿名称冒充包名。
168
+ - [蒲公英 API](https://www.pgyer.com/doc/en/view/api_app):`listMy` 用于只读鉴权,接口的账号应用列表不回传插件/MCP。
169
+ - [Native Messaging](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging)、[activeTab](https://developer.chrome.com/docs/extensions/develop/concepts/activeTab):扩展身份、长度前缀帧与用户手势授权。
170
+
171
+ 定向检查使用隔离供应商响应和内存密钥库,验证 PS256、真实加密、MCP/Native Messaging 及失败门禁。真实浏览器加载、商店页面字段兼容和真实账号权限须分别验证。
172
+
173
+ ```bash
174
+ npm run smoke:publishing-setup --workspaces=false
175
+ node scripts/smoke-mcp.mjs
176
+ node scripts/smoke-mcp.mjs --modern
177
+ node scripts/smoke-mcp-upload.mjs
178
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huoye/app-ship",
3
- "version": "0.2.0",
4
- "description": "多平台应用商店上传引擎 — CLI 命令行工具 & npm 库,支持 APK / IPA / AAB / HAP 一键分发",
3
+ "version": "0.3.0",
4
+ "description": "多平台应用发布与分发引擎 — CLI 命令行工具 & npm 库,支持 APK / IPA / AAB / HAP 测试分发与商店发布",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,6 +11,9 @@
11
11
  "dist/*.cjs",
12
12
  "dist/*.d.ts",
13
13
  "dist/*.d.cts",
14
+ "browser-extension",
15
+ "scripts/register-browser-host.mjs",
16
+ "docs/browser-credentials.md",
14
17
  "README.md",
15
18
  "LICENSE"
16
19
  ],
@@ -27,15 +30,29 @@
27
30
  }
28
31
  },
29
32
  "bin": {
30
- "app-ship": "./dist/cli.js"
33
+ "app-ship": "./dist/cli.js",
34
+ "app-ship-mcp": "./dist/mcp.js"
31
35
  },
32
36
  "scripts": {
33
37
  "build": "tsup",
34
38
  "dev": "tsup --watch",
35
- "smoke:google-play": "npm run build && node scripts/smoke-google-play.mjs",
36
- "smoke:release-receipt": "npm run build && node scripts/smoke-release-receipt.mjs",
39
+ "smoke:additional-stores": "npm run build --workspaces=false && node scripts/smoke-additional-stores.mjs",
40
+ "smoke:google-play": "npm run build --workspaces=false && node scripts/smoke-google-play.mjs",
41
+ "smoke:release-receipt": "npm run build --workspaces=false && node scripts/smoke-release-receipt.mjs",
42
+ "smoke:safety-contract": "npm run build --workspaces=false && node scripts/smoke-safety-contract.mjs",
43
+ "smoke:credential-store": "npm run build --workspaces=false && node scripts/smoke-credential-store.mjs",
44
+ "smoke:macos-keychain": "npm run build --workspaces=false && node scripts/smoke-macos-keychain.mjs",
45
+ "smoke:cli-plan-gate": "npm run build --workspaces=false && node scripts/smoke-cli-plan-gate.mjs",
46
+ "smoke:artifact-identity": "npm run build --workspaces=false && node scripts/smoke-artifact-identity.mjs",
47
+ "smoke:release-executor": "npm run build --workspaces=false && node scripts/smoke-release-executor.mjs",
48
+ "smoke:mcp": "npm run build --workspaces=false && node scripts/smoke-mcp.mjs && node scripts/smoke-mcp.mjs --modern",
49
+ "smoke:mcp-upload": "npm run build --workspaces=false && node scripts/smoke-mcp-upload.mjs",
50
+ "smoke:publishing-setup": "npm run build --workspaces=false && node scripts/smoke-publishing-setup.mjs",
37
51
  "prepublishOnly": "npm run build",
38
- "release": "node scripts/release.js"
52
+ "release": "node scripts/release.js",
53
+ "smoke:store-upload-regressions": "npm run build --workspaces=false && node scripts/smoke-store-upload-regressions.mjs",
54
+ "smoke:all-store-credentials": "npm run build --workspaces=false && node scripts/smoke-all-store-credentials.mjs",
55
+ "smoke:browser-credentials": "node scripts/smoke-browser-credentials.mjs"
39
56
  },
40
57
  "keywords": [
41
58
  "app-ship",
@@ -51,7 +68,6 @@
51
68
  "oppo",
52
69
  "vivo",
53
70
  "tencent",
54
- "testflight",
55
71
  "harmony",
56
72
  "apk",
57
73
  "ipa",
@@ -109,6 +125,7 @@
109
125
  "js-yaml": "^4.1.1"
110
126
  },
111
127
  "devDependencies": {
128
+ "@modelcontextprotocol/server": "2.0.0",
112
129
  "@types/fs-extra": "^11.0.4",
113
130
  "@types/js-yaml": "^4.0.9",
114
131
  "@types/node": "^20.19.25",
@@ -116,6 +133,7 @@
116
133
  "inquirer": "^13.3.2",
117
134
  "semver": "^7.7.4",
118
135
  "tsup": "^8.0.1",
119
- "typescript": "^5.3.3"
136
+ "typescript": "^5.3.3",
137
+ "zod": "^4.6.1"
120
138
  }
121
139
  }
@@ -0,0 +1,110 @@
1
+ // 本文件准备或按明确 --apply 注册 macOS 本用户的浏览器桥接;默认只显示计划,不安装扩展或读取商店凭据。
2
+ import { lstat, mkdir, readFile, realpath, writeFile } from 'node:fs/promises'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { homedir } from 'node:os'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const args = process.argv.slice(2)
8
+ /**
9
+ * 读取当前命令的命名参数,省略时由对应字段选择默认值。
10
+ * @param name - 需要读取的完整参数名。
11
+ */
12
+ const value = (name) => {
13
+ const index = args.indexOf(name)
14
+ return index < 0 ? undefined : args[index + 1]
15
+ }
16
+ const extensionId = value('--extension-id')
17
+ const browser = value('--browser') ?? 'chrome'
18
+ if (
19
+ process.platform !== 'darwin' ||
20
+ !/^[a-p]{32}$/u.test(extensionId ?? '') ||
21
+ !['chrome', 'edge'].includes(browser)
22
+ ) {
23
+ console.error(
24
+ '用法:node scripts/register-browser-host.mjs --extension-id <32位扩展ID> [--browser chrome|edge] [--apply];当前只支持 macOS。',
25
+ )
26
+ process.exitCode = 1
27
+ } else {
28
+ const directory = resolve(process.env.APP_SHIP_HOME ?? join(homedir(), '.app-ship'))
29
+ const entry = resolve(
30
+ value('--native-entry') ??
31
+ resolve(dirname(fileURLToPath(import.meta.url)), '../dist/browser-native.js'),
32
+ )
33
+ const launcher = join(directory, 'browser-native-host')
34
+ const config = join(directory, 'browser-connector.v1.json')
35
+ const vendor = browser === 'chrome' ? 'Google/Chrome' : 'Microsoft Edge'
36
+ const manifestDirectory = join(
37
+ homedir(),
38
+ 'Library/Application Support',
39
+ vendor,
40
+ 'NativeMessagingHosts',
41
+ )
42
+ const manifestPath = join(manifestDirectory, 'cn.huoye.app_ship.credentials.json')
43
+ const manifest = {
44
+ name: 'cn.huoye.app_ship.credentials',
45
+ description: 'Flu 本机发布凭据桥接',
46
+ path: launcher,
47
+ type: 'stdio',
48
+ allowed_origins: [`chrome-extension://${extensionId}/`],
49
+ }
50
+ /**
51
+ * 将可信本地路径转为单个 shell 字符串,避免空格或引号改变启动参数。
52
+ * @param text - 注册器已经解析成绝对路径的本机值。
53
+ */
54
+ const quote = (text) => `'${text.replaceAll("'", "'\"'\"'")}'`
55
+ const script = `#!/bin/sh\nexport APP_SHIP_HOME=${quote(directory)}\n${process.versions.electron ? 'export ELECTRON_RUN_AS_NODE=1\n' : ''}exec ${quote(process.execPath)} ${quote(entry)} "$@"\n`
56
+ const plan = {
57
+ browser,
58
+ extensionId,
59
+ entry,
60
+ launcher,
61
+ config,
62
+ manifestPath,
63
+ applied: args.includes('--apply'),
64
+ }
65
+ if (args.includes('--apply')) {
66
+ const entryInfo = await lstat(entry)
67
+ if (!entryInfo.isFile() || entryInfo.isSymbolicLink()) throw new Error('请先构建 App Ship')
68
+ await mkdir(directory, { recursive: true, mode: 0o700 })
69
+ const info = await lstat(directory)
70
+ if (
71
+ !info.isDirectory() ||
72
+ info.isSymbolicLink() ||
73
+ info.uid !== process.getuid() ||
74
+ info.mode & 0o077 ||
75
+ (await realpath(directory)) !== directory
76
+ )
77
+ throw new Error('桥接目录必须是本用户私有目录')
78
+ // 不把已有安装静默切换到另一个扩展身份。
79
+ try {
80
+ const previous = await lstat(config)
81
+ if (
82
+ !previous.isFile() ||
83
+ previous.isSymbolicLink() ||
84
+ previous.uid !== process.getuid() ||
85
+ previous.mode & 0o077
86
+ )
87
+ throw new Error('已有桥接配置不安全')
88
+ if (JSON.parse(await readFile(config, 'utf8')).extensionId !== extensionId)
89
+ throw new Error('已注册其他扩展,请先明确处理旧注册')
90
+ } catch (error) {
91
+ if (error.code !== 'ENOENT') throw error
92
+ }
93
+ for (const file of [launcher, manifestPath]) {
94
+ try {
95
+ const previous = await lstat(file)
96
+ if (!previous.isFile() || previous.isSymbolicLink() || previous.uid !== process.getuid())
97
+ throw new Error('桥接目标不安全')
98
+ } catch (error) {
99
+ if (error.code !== 'ENOENT') throw error
100
+ }
101
+ }
102
+ await mkdir(manifestDirectory, { recursive: true })
103
+ await writeFile(config, JSON.stringify({ schemaVersion: 1, extensionId, browser }), {
104
+ mode: 0o600,
105
+ })
106
+ await writeFile(launcher, script, { mode: 0o700 })
107
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600 })
108
+ }
109
+ console.log(JSON.stringify(plan, null, 2))
110
+ }