@adep/cli 0.1.17 → 0.2.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.
- package/README.md +96 -12
- package/dist/index.js +27103 -10486
- package/package.json +8 -6
- package/templates/app-desktop/README.md +51 -0
- package/templates/app-desktop/main.mjs +108 -0
- package/templates/app-desktop/offline-runtime.mjs +129 -0
- package/templates/app-desktop/package.json +49 -0
- package/templates/app-desktop/preload.cjs +45 -0
- package/templates/app-desktop/template.json +14 -0
- package/templates/app-desktop/window.mjs +53 -0
- package/templates/app-mobile/README.md +89 -0
- package/templates/app-mobile/adep.permissions.json +4 -0
- package/templates/app-mobile/bridge/gate.mjs +88 -0
- package/templates/app-mobile/capacitor.config.ts +23 -0
- package/templates/app-mobile/package.json +24 -0
- package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +22 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +9 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +86 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +81 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepPermissionGate.kt +81 -0
- package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +10 -0
- package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +44 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +14 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +69 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +58 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +82 -0
- package/templates/app-mobile/plugins/adep-bridge/package.json +14 -0
- package/templates/app-mobile/template.json +16 -0
- package/templates/app-mobile/web/adep-bridge.js +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adep/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "`adep` 命令行工具包位:登录 / 初始化 / 本地调试 / 部署 / 数据库维护 / 云存储 / 静态托管 / 微前端组件(widget),实现见任务单 CLI-001 ~ CLI-003 与 FE-002;子路径 `@adep/cli/vite` 提供云函数 vite 插件(CLI-014)",
|
|
6
6
|
"bin": {
|
|
@@ -15,18 +15,20 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"templates"
|
|
19
20
|
],
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"commander": "14.0.2",
|
|
22
23
|
"esbuild": "0.25.12",
|
|
23
|
-
"
|
|
24
|
+
"zod": "^4.4.3",
|
|
25
|
+
"@adep/runtime": "0.3.0",
|
|
26
|
+
"@adep/vite-plugin": "0.1.8",
|
|
24
27
|
"@adep/crypto": "0.1.0",
|
|
25
|
-
"@adep/
|
|
26
|
-
"@adep/runtime": "0.2.7"
|
|
28
|
+
"@adep/cf-compat": "0.1.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@adep/types": "0.
|
|
31
|
+
"@adep/types": "0.3.0"
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
34
|
"test": "vitest run",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# {{appName}}(AgentDeploy 桌面应用壳)
|
|
2
|
+
|
|
3
|
+
`adep app init` 生成的 **Electron 桌面壳工程**(PRD §2.11 路径 A:在线运行时 + 桌面壳):
|
|
4
|
+
GUI(前端产物)随包本地加载,云函数经 HTTP 在服务端执行——**改函数逻辑不需要重新发版**。
|
|
5
|
+
|
|
6
|
+
## 目录结构
|
|
7
|
+
|
|
8
|
+
- `main.mjs`:Electron 主进程——注册自定义 scheme `adep-app://` 并从 `app/site/` 提供
|
|
9
|
+
静态产物(`file://` 直接加载 ESM bundle 有 CORS 限制,必须走自定义协议);
|
|
10
|
+
- `window.mjs`:BrowserWindow 装配(沙箱 + contextIsolation 安全基线、外链交系统浏览器);
|
|
11
|
+
- `preload.cjs`:contextBridge 注入只读配置 `window.__ADEP_APP__`
|
|
12
|
+
(`platformUrl` / `projectSlug` / `appName`;⚠️ 沙箱 preload 不支持 ESM,故为 `.cjs`);
|
|
13
|
+
- `adep.app.json`:壳配置(schema 校验见 `@adep/cli`),init 时由 `--server` 等参数生成,
|
|
14
|
+
主进程启动时读取并经 `additionalArguments` 传给 preload;
|
|
15
|
+
- `package.json`:`electron` + `electron-builder` devDependencies 与三平台构建 target 占位。
|
|
16
|
+
|
|
17
|
+
## 开发
|
|
18
|
+
|
|
19
|
+
1. 把平台项目的前端构建产物放进 `app/site/`(`adep app build` 会自动拉取已发布产物);
|
|
20
|
+
2. `npm install`;
|
|
21
|
+
3. `npm start`(配置从 `adep.app.json` 读取,无需环境变量)。
|
|
22
|
+
|
|
23
|
+
## 页面侧调用云函数(在线运行时)
|
|
24
|
+
|
|
25
|
+
页面代码用 `@adep/client` 以 `platformUrl` 绝对地址 invoke:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createAdepClient } from '@adep/client/functions'
|
|
29
|
+
|
|
30
|
+
const client = createAdepClient({ baseUrl: window.__ADEP_APP__.platformUrl })
|
|
31
|
+
const result = await client.invokeFunction('hello', { name: 'world' })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 打安装包
|
|
35
|
+
|
|
36
|
+
推荐用 CLI(会自动拉取**已发布**的前端产物、回写产物版本、产出构建报告):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
adep app build # 当前平台、走 package.json build 段的 target(mac dmg / win nsis / linux AppImage)
|
|
40
|
+
adep app build --target dir # 只出未压缩目录(macOS 即 .app),本地快速验证
|
|
41
|
+
adep app build --prepare-only # 只拉产物 + 注入 + 出构建报告,不调 electron-builder
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
也可以只在这个目录里手动跑(`app/site/` 里已经是你自己放进来的产物时):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run dist # = electron-builder,target 见 package.json build 段
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
产物与构建报告都输出到 `dist/`(`dist/build-report.json` 记录本次打进包的产物版本、
|
|
51
|
+
云函数清单 `runtime: online` 与体积 Top 10)。平台不出具任何签名:签名与公证由开发者本地完成(PRD §2.11.5)。
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electron 主进程(app-desktop 模板,APP-001)。
|
|
3
|
+
*
|
|
4
|
+
* 职责(PRD §2.11 路径 A:在线运行时 + 桌面壳):
|
|
5
|
+
* 1. 注册自定义 scheme `adep-app://`——`file://` 直接加载 ESM bundle 会被 CORS 拦
|
|
6
|
+
* (`file://` origin 为 null,ES module 跨源请求一律被拒),必须走自定义协议;
|
|
7
|
+
* 2. `adep-app://bundle/<path>` 从工程内 `app/site/` 提供静态产物(带 MIME 与 SPA 回退);
|
|
8
|
+
* 3. 创建 BrowserWindow,经 additionalArguments 把 adep.app.json 配置传给沙箱 preload。
|
|
9
|
+
*
|
|
10
|
+
* 页面侧经 preload 注入的 `__ADEP_APP__` 拿到 platformUrl 后,用 `@adep/client`
|
|
11
|
+
* 绝对地址 invoke 云函数(在线运行时,函数逻辑迭代不需要重新发版)。
|
|
12
|
+
*
|
|
13
|
+
* 主进程是 .mjs(Electron 28+ 支持 ESM 主进程):启动流程用 top-level await
|
|
14
|
+
* 直写,比 .then() 链少一层回调;启动失败即响亮崩溃(unhandled rejection)。
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
19
|
+
import { app, ipcMain, net, protocol } from 'electron'
|
|
20
|
+
import { createWindow } from './window.mjs'
|
|
21
|
+
import { createOfflineRuntime, hasOfflineBundle } from './offline-runtime.mjs'
|
|
22
|
+
|
|
23
|
+
const APP_SCHEME = 'adep-app'
|
|
24
|
+
const SITE_DIR = 'app/site'
|
|
25
|
+
/**
|
|
26
|
+
* 壳工程根目录(= 本文件所在目录)。
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ ESM 里**没有** `__dirname`(那是 CJS 的全局量):主进程走 Node 的 ESM 加载器时它是
|
|
29
|
+
* `undefined`,拿去 join 会抛 TypeError(应用启动即崩)。必须自己从 `import.meta.url` 取。
|
|
30
|
+
*/
|
|
31
|
+
const APP_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
32
|
+
|
|
33
|
+
/** 从壳工程根读 adep.app.json(init 时生成;字段校验归 @adep/cli 的 schema)。 */
|
|
34
|
+
function loadAppConfig() {
|
|
35
|
+
const raw = JSON.parse(readFileSync(join(APP_ROOT, 'adep.app.json'), 'utf8'))
|
|
36
|
+
if (typeof raw.platformUrl !== 'string' || raw.platformUrl === '') {
|
|
37
|
+
throw new Error('[adep-app] adep.app.json 缺少 platformUrl(请重新执行 adep app init)')
|
|
38
|
+
}
|
|
39
|
+
if (typeof raw.projectSlug !== 'string' || raw.projectSlug === '') {
|
|
40
|
+
throw new Error('[adep-app] adep.app.json 缺少 projectSlug(请重新执行 adep app init)')
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
platformUrl: raw.platformUrl,
|
|
44
|
+
projectSlug: raw.projectSlug,
|
|
45
|
+
appName: typeof raw.appName === 'string' && raw.appName !== '' ? raw.appName : raw.projectSlug,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// registerSchemesAsPrivileged 必须在 app ready **之前**调用(ready 后注册会抛错):
|
|
50
|
+
// standard + secure 让 `adep-app://` 拥有标准 origin 语义(fetch / ESM 可用),
|
|
51
|
+
// supportFetchAPI + corsEnabled 让页面内 fetch 与 ES module 请求按标准 CORS 工作。
|
|
52
|
+
protocol.registerSchemesAsPrivileged([
|
|
53
|
+
{
|
|
54
|
+
scheme: APP_SCHEME,
|
|
55
|
+
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true },
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
await app.whenReady()
|
|
60
|
+
const config = loadAppConfig()
|
|
61
|
+
|
|
62
|
+
// 流式协议处理器:adep-app://bundle/<path> → app/site/<path>。
|
|
63
|
+
// 用 net.fetch(pathToFileURL(...)) 而非手拼 Response:前者自动带 Content-Type
|
|
64
|
+
//(ESM 对 MIME 严格校验,手写 Response 不给 MIME 会被静默拒载)。
|
|
65
|
+
protocol.handle(APP_SCHEME, async (request) => {
|
|
66
|
+
const { host, pathname } = new URL(request.url)
|
|
67
|
+
if (host !== 'bundle') {
|
|
68
|
+
return new Response(`未知 host "${host}"`, { status: 404 })
|
|
69
|
+
}
|
|
70
|
+
const siteRoot = join(APP_ROOT, SITE_DIR)
|
|
71
|
+
const relative = decodeURIComponent(pathname).replace(/^\/+/, '')
|
|
72
|
+
const rootUrl = pathToFileURL(siteRoot).href
|
|
73
|
+
const targetUrl = pathToFileURL(join(siteRoot, relative)).href
|
|
74
|
+
// 目录穿越防线:归一化后的目标必须仍在 app/site/ 内。
|
|
75
|
+
if (!targetUrl.startsWith(`${rootUrl}/`)) {
|
|
76
|
+
return new Response('路径越界', { status: 403 })
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await net.fetch(targetUrl)
|
|
80
|
+
} catch {
|
|
81
|
+
// SPA 回退:产物内路由未命中时回落 index.html(前端发布形态统一 SPA)。
|
|
82
|
+
if (relative !== 'index.html') {
|
|
83
|
+
return net.fetch(pathToFileURL(join(siteRoot, 'index.html')).href)
|
|
84
|
+
}
|
|
85
|
+
return new Response('未找到入口产物 app/site/index.html', { status: 404 })
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// 离线运行时(PRD §2.11.4):app/offline/ 存在即装配设备端执行器,页面函数调用走 ipc。
|
|
90
|
+
// 在线形态零开销(hasOfflineBundle 只做一次 existsSync);装配失败 = 启动即崩(fail loud)。
|
|
91
|
+
let offline = null
|
|
92
|
+
if (hasOfflineBundle()) {
|
|
93
|
+
offline = await createOfflineRuntime()
|
|
94
|
+
ipcMain.handle('adep-offline:invoke', (_event, name, args) => offline.executor.invoke(name, args))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const win = await createWindow(config, { offline: offline !== null })
|
|
98
|
+
await win.loadURL(`${APP_SCHEME}://bundle/index.html`)
|
|
99
|
+
|
|
100
|
+
app.on('activate', () => {
|
|
101
|
+
// macOS:点 Dock 图标且无窗口时重建(Electron 官方生命周期约定)。
|
|
102
|
+
if (app.getAllWindows().length === 0) void createWindow(config)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
app.on('window-all-closed', () => {
|
|
106
|
+
// Windows / Linux:关窗即退出;macOS 保持常驻(Dock 惯例)。
|
|
107
|
+
if (process.platform !== 'darwin') app.quit()
|
|
108
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桌面壳离线运行时装配(APP-008 范围 4 / PRD §2.11.4 运行时随包分发)。
|
|
3
|
+
*
|
|
4
|
+
* 消费 `.adepack` 内的 `runtime/executor.mjs`(@adep/app-runtime 的编译产物,服务端
|
|
5
|
+
* Bun.build 在构建期打进包):
|
|
6
|
+
*
|
|
7
|
+
* 读包(manifest / functions/index.json / functions/*.mjs / db/*.sql / storage/**)
|
|
8
|
+
* → 造 DeviceBridge(node:sqlite + 只读文件桥 + 空 env——离线包永不下发 secret)
|
|
9
|
+
* → executor.createLocalExecutor(白名单路由:offline:true 本地执行,false 返回
|
|
10
|
+
* FN_OFFLINE_ONLY 且提示在线调用方式)
|
|
11
|
+
* → 返回 { executor, manifest, sqlite, version } 供主进程 ipc 面使用。
|
|
12
|
+
*
|
|
13
|
+
* 壳工程是**独立 npm 工程**(装不了私有 workspace 包),运行时随包分发是本模板对
|
|
14
|
+
* 「接 @adep/app-runtime」的唯一可行落地;e2e 黄金测试(e2e/cli/app-offline.spec.ts)
|
|
15
|
+
* 用仓库内 @adep/app-runtime 真实现做 host-side harness,对本装配跑同一判定面,
|
|
16
|
+
* 把随包分发的语义分叉风险钉在 CI 里(AGENTS §9:拷贝必须点名分叉并双向对账)。
|
|
17
|
+
*/
|
|
18
|
+
import { mkdirSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync } from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
23
|
+
|
|
24
|
+
/** 壳工程根目录(= 本文件所在目录)。⚠️ ESM 没有 `__dirname`,须从 import.meta.url 取。 */
|
|
25
|
+
const APP_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
|
|
27
|
+
/** 离线产物目录(CLI `adep app build --offline` 注入的位置,与在线 `app/site/` 并列)。 */
|
|
28
|
+
const OFFLINE_DIR = join(APP_ROOT, 'app', 'offline')
|
|
29
|
+
|
|
30
|
+
/** 本地 SQLite 库文件(设备本地数据;首次装配时由 schema.sql + baseline.db.sql 幂等初始化)。 */
|
|
31
|
+
const LOCAL_DB_PATH = join(APP_ROOT, 'data', 'offline.sqlite')
|
|
32
|
+
|
|
33
|
+
/** 壳是否处于离线运行时形态(包内存在 manifest 即视为已注入离线产物)。 */
|
|
34
|
+
export function hasOfflineBundle() {
|
|
35
|
+
return existsSync(join(OFFLINE_DIR, 'manifest.json'))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 离线函数名 → 源码的懒读取(避免启动即读全部大函数;executor 需要时按名取)。 */
|
|
39
|
+
function readFunctionSource(name) {
|
|
40
|
+
const file = join(OFFLINE_DIR, 'functions', `${name}.mjs`)
|
|
41
|
+
if (!existsSync(file)) {
|
|
42
|
+
throw new Error(`[adep-app] 离线包缺少函数源码 functions/${name}.mjs(包损坏或注入不完整)`)
|
|
43
|
+
}
|
|
44
|
+
return readFileSync(file, 'utf8')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 装配离线运行时:装载 executor(data URL 动态 import,引擎无关标准 ESM 语义)、
|
|
49
|
+
* 初始化本地库、造桥。任何一步失败即 throw(壳启动即响亮崩溃,不留半离线坏状态)。
|
|
50
|
+
*/
|
|
51
|
+
export async function createOfflineRuntime() {
|
|
52
|
+
if (!hasOfflineBundle()) {
|
|
53
|
+
throw new Error('[adep-app] 未检测到离线产物 app/offline/manifest.json:请先执行 adep app build --offline')
|
|
54
|
+
}
|
|
55
|
+
const executorSource = readFileSync(join(OFFLINE_DIR, 'runtime', 'executor.mjs'), 'utf8')
|
|
56
|
+
// data URL import:包内字节不进磁盘副本,进程内直接装载(bundle 是纯 ESM,无 node: 运行时依赖)。
|
|
57
|
+
const executorModule = await import(
|
|
58
|
+
`data:text/javascript;base64,${Buffer.from(executorSource).toString('base64')}`
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const manifest = JSON.parse(readFileSync(join(OFFLINE_DIR, 'manifest.json'), 'utf8'))
|
|
62
|
+
const index = JSON.parse(
|
|
63
|
+
readFileSync(join(OFFLINE_DIR, 'functions', 'index.json'), 'utf8')
|
|
64
|
+
)
|
|
65
|
+
const functions =
|
|
66
|
+
index.functions === undefined
|
|
67
|
+
? []
|
|
68
|
+
: index.functions.map((fn) => ({ name: fn.name, source: readFunctionSource(fn.name), offline: fn.offline === true }))
|
|
69
|
+
|
|
70
|
+
// 本地库:schema.sql(owned + baseline 结构)+ baseline.db.sql(只读快照)幂等初始化。
|
|
71
|
+
mkdirSync(dirname(LOCAL_DB_PATH), { recursive: true })
|
|
72
|
+
const sqlite = new DatabaseSync(LOCAL_DB_PATH)
|
|
73
|
+
const schemaSql = readFileSync(join(OFFLINE_DIR, 'db', 'schema.sql'), 'utf8')
|
|
74
|
+
const baselineSql = readFileSync(join(OFFLINE_DIR, 'db', 'baseline.db.sql'), 'utf8')
|
|
75
|
+
if (schemaSql.trim() !== '') sqlite.exec(schemaSql)
|
|
76
|
+
if (baselineSql.trim() !== '') sqlite.exec(baselineSql)
|
|
77
|
+
|
|
78
|
+
// DeviceBridge:SQLite 桥(node:sqlite)+ 只读预置资源桥(app/offline/storage/**)+ 无原生能力。
|
|
79
|
+
const sqliteBridge = {
|
|
80
|
+
run: async (sql, params) => {
|
|
81
|
+
const stmt = sqlite.prepare(sql)
|
|
82
|
+
const result = params === undefined ? stmt.run() : stmt.run(...(params))
|
|
83
|
+
return { changes: Number(result.changes) }
|
|
84
|
+
},
|
|
85
|
+
all: async (sql, params) => {
|
|
86
|
+
const stmt = sqlite.prepare(sql)
|
|
87
|
+
return (params === undefined ? stmt.all() : stmt.all(...(params)))
|
|
88
|
+
},
|
|
89
|
+
get: async (sql, params) => {
|
|
90
|
+
const stmt = sqlite.prepare(sql)
|
|
91
|
+
const row = params === undefined ? stmt.get() : stmt.get(...(params))
|
|
92
|
+
return (row === undefined ? null : (row))
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
const storageRoot = join(OFFLINE_DIR, 'storage')
|
|
96
|
+
const filesBridge = {
|
|
97
|
+
writeFile: async (path, _data) => {
|
|
98
|
+
throw new Error(`[adep-app] 离线包预置资源只读:storage/${path}`)
|
|
99
|
+
},
|
|
100
|
+
readFile: async (path) => {
|
|
101
|
+
const file = join(storageRoot, path)
|
|
102
|
+
if (!existsSync(file)) throw new Error(`[adep-app] 离线包缺少预置资源 storage/${path}`)
|
|
103
|
+
return new Uint8Array(readFileSync(file))
|
|
104
|
+
},
|
|
105
|
+
removeFile: async (path) => {
|
|
106
|
+
throw new Error(`[adep-app] 离线包预置资源只读:storage/${path}`)
|
|
107
|
+
},
|
|
108
|
+
listFiles: async (prefix = '') => {
|
|
109
|
+
// 预置资源面在离线包里是固定清单(manifest.contents 里 storage/**),不落盘扫描。
|
|
110
|
+
const contents = manifest['contents']
|
|
111
|
+
return (contents ?? [])
|
|
112
|
+
.filter((entry) => entry.path.startsWith('storage/') && entry.path.startsWith(`storage/${prefix}`))
|
|
113
|
+
.map((entry) => ({ path: entry.path.slice('storage/'.length), size: entry.size ?? 0 }))
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
const bridge = {
|
|
117
|
+
sqlite: sqliteBridge,
|
|
118
|
+
files: filesBridge,
|
|
119
|
+
// 原生能力门控在原生侧(ADR-0007 硬约束 3);本壳未声明任何原生能力 → 调用即拒绝。
|
|
120
|
+
native: undefined,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// env:离线包永不下发 secret 值(§4.2);public 环境变量同样不进包(manifest app 段无 env 面)。
|
|
124
|
+
const env = { publicVars: {}, secretKeys: [] }
|
|
125
|
+
const local = executorModule.createLocalExecutor({ functions, bridge, env })
|
|
126
|
+
const version = readFileSync(join(OFFLINE_DIR, 'runtime', 'version.txt'), 'utf8').trim()
|
|
127
|
+
|
|
128
|
+
return { executor: local, manifest, sqlite, version }
|
|
129
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectSlug}}-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "{{appName}} —— AgentDeploy 桌面应用(Electron 壳,在线 + 离线运行时)",
|
|
6
|
+
"main": "main.mjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "electron .",
|
|
9
|
+
"dist": "electron-builder"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"electron": "^44.0.0",
|
|
13
|
+
"electron-builder": "^26.0.0"
|
|
14
|
+
},
|
|
15
|
+
"build": {
|
|
16
|
+
"appId": "{{appId}}",
|
|
17
|
+
"productName": "{{appName}}",
|
|
18
|
+
"asar": false,
|
|
19
|
+
"directories": {
|
|
20
|
+
"output": "dist"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"main.mjs",
|
|
24
|
+
"window.mjs",
|
|
25
|
+
"preload.cjs",
|
|
26
|
+
"adep.app.json",
|
|
27
|
+
"app/site/**/*",
|
|
28
|
+
"app/offline/**/*",
|
|
29
|
+
"data/**/*"
|
|
30
|
+
],
|
|
31
|
+
"mac": {
|
|
32
|
+
"target": [
|
|
33
|
+
"dmg"
|
|
34
|
+
],
|
|
35
|
+
"category": "public.app-category.productivity"
|
|
36
|
+
},
|
|
37
|
+
"win": {
|
|
38
|
+
"target": [
|
|
39
|
+
"nsis"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"linux": {
|
|
43
|
+
"target": [
|
|
44
|
+
"AppImage"
|
|
45
|
+
],
|
|
46
|
+
"category": "Utility"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload(app-desktop 模板,APP-001):contextBridge 注入只读配置 `__ADEP_APP__`。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 文件必须是 `.cjs`:Electron 沙箱 preload **不支持 ESM**(官方约束,sandbox
|
|
5
|
+
* 预加载脚本在隔离的 CJS 环境里执行),`.mjs` 会被静默跳过。
|
|
6
|
+
*
|
|
7
|
+
* 沙箱 preload 读不到文件、也读不到普通环境变量,配置由主进程经
|
|
8
|
+
* `webPreferences.additionalArguments` 以 `--adep-xxx=value` 形式传入(main.mjs 从
|
|
9
|
+
* adep.app.json 读取后注入)。页面侧 `@adep/client` 以 platformUrl 绝对地址 invoke
|
|
10
|
+
* 云函数(在线运行时):
|
|
11
|
+
*
|
|
12
|
+
* const client = createAdepClient({ baseUrl: window.__ADEP_APP__.platformUrl })
|
|
13
|
+
*/
|
|
14
|
+
const { contextBridge, ipcRenderer } = require('electron')
|
|
15
|
+
|
|
16
|
+
function argvValue(name) {
|
|
17
|
+
const prefix = `--${name}=`
|
|
18
|
+
const arg = process.argv.find((entry) => entry.startsWith(prefix))
|
|
19
|
+
return arg === undefined ? '' : arg.slice(prefix.length)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PLATFORM_URL = argvValue('adep-platform-url')
|
|
23
|
+
const PROJECT_SLUG = argvValue('adep-project-slug')
|
|
24
|
+
const APP_NAME = argvValue('adep-app-name')
|
|
25
|
+
const OFFLINE = argvValue('adep-offline') === '1'
|
|
26
|
+
|
|
27
|
+
if (PLATFORM_URL === '' || PROJECT_SLUG === '') {
|
|
28
|
+
// fail loud:缺配置的壳等于「页面能开、数据全挂」的静默坏状态。
|
|
29
|
+
throw new Error(
|
|
30
|
+
'[adep-app] 缺少运行配置 --adep-platform-url / --adep-project-slug(由主进程从 adep.app.json 注入)'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
contextBridge.exposeInMainWorld('__ADEP_APP__', {
|
|
35
|
+
platformUrl: PLATFORM_URL,
|
|
36
|
+
projectSlug: PROJECT_SLUG,
|
|
37
|
+
appName: APP_NAME,
|
|
38
|
+
// 离线形态(app/offline/ 已注入):函数调用经主进程 LocalFunctionExecutor 本地执行。
|
|
39
|
+
...(OFFLINE
|
|
40
|
+
? {
|
|
41
|
+
mode: 'offline',
|
|
42
|
+
invoke: (name, args) => ipcRenderer.invoke('adep-offline:invoke', name, args),
|
|
43
|
+
}
|
|
44
|
+
: { mode: 'online' }),
|
|
45
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "app-desktop",
|
|
3
|
+
"name": "桌面应用壳(Electron)",
|
|
4
|
+
"description": "Electron 桌面壳工程:BrowserWindow + adep-app:// 自定义协议加载 app/site/ 产物,在线运行时(云函数经 platformUrl 服务端执行)",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"variables": [
|
|
7
|
+
{
|
|
8
|
+
"name": "projectSlug",
|
|
9
|
+
"description": "平台项目 slug(写入包名 / adep.app.json / appId 推导)"
|
|
10
|
+
},
|
|
11
|
+
{ "name": "appName", "description": "应用显示名(窗口标题 / electron-builder productName)" },
|
|
12
|
+
{ "name": "appId", "description": "electron-builder appId(top.adep.<slug>)" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserWindow 装配(从 main.mjs 拆出,保持主流程可读)。
|
|
3
|
+
*
|
|
4
|
+
* 配置传递:沙箱 preload 读不到文件与环境变量,经 `additionalArguments` 以
|
|
5
|
+
* `--adep-xxx=value` 形式把 adep.app.json 的只读配置带进 preload(见 preload.cjs)。
|
|
6
|
+
*
|
|
7
|
+
* @typedef {Object} AppConfig
|
|
8
|
+
* @property {string} platformUrl 平台地址(页面侧 @adep/client 的 baseUrl)
|
|
9
|
+
* @property {string} projectSlug 平台项目 slug
|
|
10
|
+
* @property {string} appName 应用显示名
|
|
11
|
+
*/
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { BrowserWindow, shell } from 'electron'
|
|
15
|
+
|
|
16
|
+
/** 壳工程根目录(= 本文件所在目录)。⚠️ ESM 没有 `__dirname`,须从 import.meta.url 取。 */
|
|
17
|
+
const APP_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建主窗口并注入壳配置。
|
|
21
|
+
*
|
|
22
|
+
* @param {AppConfig} config adep.app.json 解析出的只读配置
|
|
23
|
+
* @returns {Promise<BrowserWindow>}
|
|
24
|
+
*/
|
|
25
|
+
export async function createWindow(config, options = {}) {
|
|
26
|
+
const win = new BrowserWindow({
|
|
27
|
+
width: 1280,
|
|
28
|
+
height: 800,
|
|
29
|
+
autoHideMenuBar: true,
|
|
30
|
+
webPreferences: {
|
|
31
|
+
preload: join(APP_ROOT, 'preload.cjs'),
|
|
32
|
+
// 沙箱 + contextIsolation 是 Electron 安全基线:preload 只经 contextBridge
|
|
33
|
+
// 暴露只读配置,页面拿不到 Node 能力(PRD §2.11 安全边界)。
|
|
34
|
+
sandbox: true,
|
|
35
|
+
contextIsolation: true,
|
|
36
|
+
nodeIntegration: false,
|
|
37
|
+
additionalArguments: [
|
|
38
|
+
`--adep-platform-url=${config.platformUrl}`,
|
|
39
|
+
`--adep-project-slug=${config.projectSlug}`,
|
|
40
|
+
`--adep-app-name=${config.appName}`,
|
|
41
|
+
`--adep-offline=${options.offline === true ? '1' : '0'}`,
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 外链交给系统浏览器,不让 app 壳变成浏览器(Electron 官方 setWindowOpenHandler 约定)。
|
|
47
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
48
|
+
void shell.openExternal(url)
|
|
49
|
+
return { action: 'deny' }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return win
|
|
53
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# {{appName}}(AgentDeploy 移动应用壳)
|
|
2
|
+
|
|
3
|
+
`adep app init --platform mobile` 生成的 **Capacitor 移动壳工程**(PRD §2.11 路径 A:在线
|
|
4
|
+
运行时 + 移动壳):GUI(前端产物)随包本地加载,云函数经 HTTP 在服务端执行——**改函数
|
|
5
|
+
逻辑不需要重新发版**(§2.11.4 在线运行时)。
|
|
6
|
+
|
|
7
|
+
移动端与桌面端能力对齐(§2.11.1),差异只在打包壳与分发口径:iOS / Android 有应用商店
|
|
8
|
+
审核约束,签名与上架由开发者自行完成(见下方「签名与上架」,完整指引
|
|
9
|
+
[`docs/mobile-shell-signing.md`](../../docs/mobile-shell-signing.md))。
|
|
10
|
+
|
|
11
|
+
## 目录结构
|
|
12
|
+
|
|
13
|
+
- `capacitor.config.ts`:Capacitor 配置(`appId` = iOS bundle id,`webDir` = `app/site`);
|
|
14
|
+
- `package.json`:`@capacitor/*` 依赖 + 脚本(`cap:add:ios` / `cap:add:android` /
|
|
15
|
+
`build:ios` / `build:android` 等);
|
|
16
|
+
- `web/adep-bridge.js`:页面侧桥(与桌面 `preload.cjs` 同形,§3.2.8 硬约束④)——CLI
|
|
17
|
+
`adep app build` 时复制进 `app/site/adep-bridge.js` 并注入 `index.html`,暴露
|
|
18
|
+
`window.__ADEP_APP__`(`platformUrl` / `projectSlug` / `appName` / `invokeNative`);
|
|
19
|
+
- `bridge/gate.mjs`:权限门控核心的 **host-side harness 镜像**(CI 判定面;原生侧同规则);
|
|
20
|
+
- `plugins/adep-bridge/`:**本地 Capacitor 插件**——原生桥(iOS Swift / Android Kotlin):
|
|
21
|
+
原生侧权限门控 + ownerKey 存储(Keychain / Keystore)+ 原生能力 invoke 路由;
|
|
22
|
+
- `adep.permissions.json`:权限清单占位(CLI build 时按服务端打包配置重写进 `app/site/`);
|
|
23
|
+
- `adep.app.json`:壳配置(CLI init 生成,build 时复制进 `app/site/` 供页面桥读取)。
|
|
24
|
+
|
|
25
|
+
## 首次构建(一次性)
|
|
26
|
+
|
|
27
|
+
原生工程由 Capacitor 生成,平台不维护原生工程(§3.2.8):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npx cap add ios # 生成 ios/ Xcode 工程
|
|
32
|
+
npx cap add android # 生成 android/ Gradle 工程
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> Android:`cap add` 生成的 `android/app/build.gradle` 里 `applicationId` 默认取
|
|
36
|
+
> `capacitor.config.ts` 的 `appId`;按 `{{androidPackageId}}` 修改(applicationId 不允许
|
|
37
|
+
> 连字符,iOS bundle id 不允许下划线——两端无法共用一个 id)。
|
|
38
|
+
|
|
39
|
+
## 打 unsigned 安装包
|
|
40
|
+
|
|
41
|
+
推荐用 CLI(自动拉取**已发布**前端产物、按服务端打包配置写入权限清单、回写版本、出报告):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
adep app build --platform ios --dir . # xcodebuild archive → .xcarchive(unsigned)
|
|
45
|
+
adep app build --platform android --dir . # gradlew assembleRelease → unsigned APK
|
|
46
|
+
adep app build --platform ios --prepare-only # 只注入产物 + 报告,不调工具链
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
产物与构建报告输出到 `dist/`。**平台不出具任何签名**:签名 / 公证 / 上架由开发者本地完成
|
|
50
|
+
(完整指引见 `docs/mobile-shell-signing.md`)。
|
|
51
|
+
|
|
52
|
+
## 页面侧调用云函数(在线运行时)
|
|
53
|
+
|
|
54
|
+
页面代码与桌面同款,用 `@adep/client` 以 `platformUrl` 绝对地址 invoke:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createAdepClient } from '@adep/client/functions'
|
|
58
|
+
|
|
59
|
+
const client = createAdepClient({ baseUrl: window.__ADEP_APP__.platformUrl })
|
|
60
|
+
const result = await client.invokeFunction('hello', { name: 'world' })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 原生能力与权限门控(§2.11.5)
|
|
64
|
+
|
|
65
|
+
- 在平台「应用打包」设置页声明的权限(`permissions`)经 CLI build 写入
|
|
66
|
+
`app/site/adep.permissions.json`,随 `npx cap sync` 同步进两端原生资源
|
|
67
|
+
(Android `assets/public/` / iOS `Public/`)。
|
|
68
|
+
- **门控做在原生侧**(`AdepPermissionGate.swift` / `AdepPermissionGate.kt`):未声明的能力
|
|
69
|
+
调用直接返回 `PERMISSION_DENIED` 并记设备侧审计日志(`adep-audit.log`),**绝不在运行时
|
|
70
|
+
申请**。JS 侧不持有放行决策(客户端产物等同公开,决策放 JS 侧等于没有门)。
|
|
71
|
+
- **系统权限映射**:声明了某个能力后,还需在原生工程配相应系统权限——iOS 在
|
|
72
|
+
`ios/App/App/Info.plist` 加用途描述(如定位 `NSLocationWhenInUseUsageDescription`)、
|
|
73
|
+
Android 在 `android/app/src/main/AndroidManifest.xml` 加 `uses-permission`
|
|
74
|
+
(如 `ACCESS_FINE_LOCATION`)。本插件**不**主动索取任何系统权限。
|
|
75
|
+
- 设备端函数(离线运行时)请求原生能力:`await window.__ADEP_APP__.invokeNative('geolocation', {})`。
|
|
76
|
+
|
|
77
|
+
## ownerKey 存储(§2.11.5 设备身份与恢复)
|
|
78
|
+
|
|
79
|
+
安装首启时由壳原生侧向服务端签发端点换取 ownerKey,**只存 Keychain(iOS)/ Keystore
|
|
80
|
+
(Android)**:`AdepKeychain.swift`(`kSecClassGenericPassword`)/
|
|
81
|
+
`AdepKeystore.kt`(Android Keystore + AES/GCM,密文落 SharedPreferences)。明文不落 JS
|
|
82
|
+
侧 / 明文文件;插件提供 `getOwnerKey` / `setOwnerKey` / `deleteOwnerKey`(恢复码认领时换键)。
|
|
83
|
+
|
|
84
|
+
## iOS 约束(§2.11.4 / §2.11.7)
|
|
85
|
+
|
|
86
|
+
- **仅 UI 变更可自行发版**:函数逻辑变更需重新提交 App Store 审核(天级)。
|
|
87
|
+
业务频繁迭代且用户主要联网时选**在线运行时**(默认,本模板即在线)。
|
|
88
|
+
- **本平台不做 iOS 运行时下发离线包**(App Store 2.5.2 灰区,ADR-0008 第 10 条);
|
|
89
|
+
Android 不受此限。随二进制提交的 JS 属静态产物、不触发 2.5.2。
|