@adep/cli 0.4.1 → 0.4.5
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 +140 -11
- package/dist/index.js +34568 -27795
- package/dist/vite.js +279 -19
- package/dist/worker-entry.js +201 -2
- package/package.json +6 -5
- package/templates/app-desktop/README.md +67 -2
- package/templates/app-desktop/identity-store.mjs +309 -0
- package/templates/app-desktop/identity.mjs +517 -0
- package/templates/app-desktop/main.cjs +17 -0
- package/templates/app-desktop/main.mjs +218 -2
- package/templates/app-desktop/offline-runtime.mjs +72 -3
- package/templates/app-desktop/package.json +10 -6
- package/templates/app-desktop/recovery-ui.mjs +300 -0
- package/templates/app-desktop/sync-loop.mjs +137 -0
- package/templates/app-mobile/README.md +47 -9
- package/templates/app-mobile/package.json +2 -1
- package/templates/app-mobile/plugins/adep-bridge/Package.swift +52 -0
- package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +34 -3
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +12 -5
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +198 -12
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepIdentity.kt +342 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +56 -2
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqlite.kt +506 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqliteLogic.kt +235 -0
- package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +30 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +7 -1
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +221 -9
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepIdentity.swift +500 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +69 -17
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +25 -4
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepShellConfig.swift +54 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepSqlite.swift +614 -0
- package/templates/app-mobile/plugins/adep-bridge/package.json +10 -0
- package/templates/app-mobile/template.json +12 -3
- package/templates/app-mobile/web/adep-bridge.js +103 -9
- package/templates/app-mobile/web/offline-runtime.js +230 -0
- package/templates/app-mobile-xtool/AdepShell-Info.plist +24 -0
- package/templates/app-mobile-xtool/Package.swift +59 -0
- package/templates/app-mobile-xtool/README.md +81 -0
- package/templates/app-mobile-xtool/Sources/AdepShell/App.swift +36 -0
- package/templates/app-mobile-xtool/Sources/AdepShell/ShellViewController.swift +223 -0
- package/templates/app-mobile-xtool/adep.permissions.json +4 -0
- package/templates/app-mobile-xtool/app/site/.gitkeep +0 -0
- package/templates/app-mobile-xtool/template.json +15 -0
- package/templates/app-mobile-xtool/xtool.yml +35 -0
- package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +0 -10
|
@@ -19,31 +19,123 @@
|
|
|
19
19
|
*
|
|
20
20
|
* const result = await window.__ADEP_APP__.invokeNative('geolocation', {})
|
|
21
21
|
* // 未在 manifest permissions 声明 → 原生侧拒绝 { code: 'PERMISSION_DENIED' }
|
|
22
|
+
*
|
|
23
|
+
* 离线形态(APP-022 C 段):`adep app build --offline --toolchain xtool` 把离线树注入
|
|
24
|
+
* `app/site/offline/`,本文件探测到即装配 web/offline-runtime.js 并切换
|
|
25
|
+
* `window.__ADEP_APP__.mode = 'offline'`(invoke 走设备端执行器,与桌面 preload 同形)。
|
|
26
|
+
*
|
|
27
|
+
* 失败可见性(APP-030):装配链有四种结局,各自的字段**互斥**,出现任一即表示装配未完整完成:
|
|
28
|
+
*
|
|
29
|
+
* configError —— `./adep.app.json` 读不到 / 不是 JSON / 缺 platformUrl·projectSlug
|
|
30
|
+
* offlineError —— 离线树在(`./offline/manifest.json` 200)但装配抛错
|
|
31
|
+
* timeoutError —— 10s 内没装完(带 stage,说明卡在哪一步)
|
|
32
|
+
* (正常) —— platformUrl / projectSlug / appName / mode 齐备
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 为什么配置失败要单独一支(APP-030):`fetch` 对 404 **不 reject**(resolve 一个
|
|
35
|
+
* `ok: false` 的响应),`.json()` 才抛;而且装配 IIFE 顶部此前没有 try ⇒ 一次缺件会让整个
|
|
36
|
+
* IIFE 终止、`__ADEP_APP__` 永不出现,10s 后那条兜底文案还写着「大概率是原生桥调用悬空」——
|
|
37
|
+
* 按那句话去查 Swift,方向完全反了。缺配置是**构建期**问题(`adep.app.json` 没进 site/),
|
|
38
|
+
* 必须在页面里说出这件事。
|
|
22
39
|
*/
|
|
23
40
|
;(async () => {
|
|
41
|
+
// 装配阶段记账(APP-030):超时兜底要能说出「卡在哪一步」,而不是给一个猜测。
|
|
42
|
+
// config → offline-probe → offline-assemble → done
|
|
43
|
+
let stage = 'config'
|
|
44
|
+
|
|
45
|
+
/** 每个阶段卡住时的候选原因(超时文案按 stage 取)。 */
|
|
46
|
+
const STAGE_HINTS = {
|
|
47
|
+
config: '读 ./adep.app.json 没有返回——检查构建是否把壳配置注入 site/(重跑 adep app build)',
|
|
48
|
+
'offline-probe': '探测 ./offline/manifest.json 没有返回——离线树在包里,但静态服务不响应',
|
|
49
|
+
'offline-assemble':
|
|
50
|
+
'离线树已判定存在,但 offline-runtime.js 装配未完成(原生桥某条路径没 resolve / 读包失败)',
|
|
51
|
+
done: '装配已完成却仍没有 __ADEP_APP__(不应发生,报 bug)',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 装配卡死保护(2026-10-22 真机第五轮实测):原生桥某条路径没 resolve 时
|
|
55
|
+
// (CAPPluginCall 悬空),下面的 IIFE 停在 await 上,`__ADEP_APP__` 永远不出现——
|
|
56
|
+
// 页面只剩 MISSING,比报错毒(浮层连 ERR 行都没有)。超时先落一个可读的错误状态;
|
|
57
|
+
// 若装配随后完成,正常值直接覆盖(clearTimeout 后不再写)。
|
|
58
|
+
const assembleDeadline = setTimeout(() => {
|
|
59
|
+
if (window['__ADEP_APP__'] === undefined) {
|
|
60
|
+
window['__ADEP_APP__'] = {
|
|
61
|
+
mode: 'online',
|
|
62
|
+
timeoutError: `装配超时(10s 未完成,停在阶段 ${stage}):${STAGE_HINTS[stage] || '未知阶段'}`,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, 10000)
|
|
66
|
+
|
|
24
67
|
/** 壳配置(CLI build 时从壳工程 adep.app.json 复制进 site/;字段校验归 CLI schema)。 */
|
|
25
|
-
|
|
26
|
-
|
|
68
|
+
let config
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch('./adep.app.json')
|
|
71
|
+
// 404 不 reject:不显式判 ok 的话,`.json()` 会因 HTML 错误页抛 `Unexpected token '<'`——
|
|
72
|
+
// 报错文本把人指向「桥的代码坏了」,而真因是「文件没进包」。
|
|
73
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
74
|
+
config = await response.json()
|
|
75
|
+
// 与桌面 preload.cjs 的 fail-loud 同形(§3.2.8 ④):缺这两个字段的壳等于
|
|
76
|
+
// 「页面能开、数据全挂」,桌面侧直接 throw,移动侧不能悄悄透传 undefined。
|
|
77
|
+
if (typeof config.platformUrl !== 'string' || config.platformUrl === '') {
|
|
78
|
+
throw new Error('缺 platformUrl')
|
|
79
|
+
}
|
|
80
|
+
if (typeof config.projectSlug !== 'string' || config.projectSlug === '') {
|
|
81
|
+
throw new Error('缺 projectSlug')
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
clearTimeout(assembleDeadline)
|
|
85
|
+
window['__ADEP_APP__'] = {
|
|
86
|
+
mode: 'online',
|
|
87
|
+
configError:
|
|
88
|
+
`壳配置 ./adep.app.json 不可用(${String(error)}):页面拿不到 platformUrl / projectSlug。` +
|
|
89
|
+
'这是构建期问题——`adep.app.json` 没有进 site/ 或字段不全,重跑 adep app build;' +
|
|
90
|
+
'与原生桥无关(原生桥不可用时 invokeNative 会单独报 NATIVE_BRIDGE_UNAVAILABLE)。',
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
27
94
|
|
|
28
95
|
// 字符串 key 写法(与桌面 preload.cjs 同款):`__ADEP_APP__` 是壳与页面约定的全局名,
|
|
29
96
|
// 以标识符出现会触发 no-underscore-dangle(lint),字符串形式表达「这是契约名不是变量」。
|
|
97
|
+
// 在线/离线分流(APP-022 C 段):`offline/manifest.json` fetch 得到 → 站点内离线树
|
|
98
|
+
// 已注入(`adep app build --offline --toolchain xtool`),页面加载离线装配并切换 mode;
|
|
99
|
+
// 404 → 在线形态(在线构建不带离线树是**正常态**,一次 404 不是错误)。
|
|
100
|
+
// 判定与装配逻辑在 web/offline-runtime.js(本文件只负责暴露入口后的分流结果)。
|
|
101
|
+
let runtime = { mode: 'online' }
|
|
102
|
+
stage = 'offline-probe'
|
|
103
|
+
try {
|
|
104
|
+
const manifestProbe = await fetch('./offline/manifest.json')
|
|
105
|
+
if (manifestProbe.ok) {
|
|
106
|
+
stage = 'offline-assemble'
|
|
107
|
+
const offlineRuntime = await import('./offline-runtime.js')
|
|
108
|
+
const assembled = await offlineRuntime.createOfflineRuntime()
|
|
109
|
+
runtime = {
|
|
110
|
+
mode: 'offline',
|
|
111
|
+
invoke: (name, args) => assembled.executor.invoke(name, args),
|
|
112
|
+
version: assembled.version,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// fail loud 且不静默降级:离线树存在但装配失败(包损坏 / 原生桥缺失 / 函数源缺失)
|
|
117
|
+
// 与「没有离线树」是两回事——前者必须可见,不能静默回在线(数据面会静默漂到平台)。
|
|
118
|
+
console.error('[adep-app] 离线运行时装配失败,已回退在线形态;原始错误:', error)
|
|
119
|
+
runtime = { mode: 'online', offlineError: String(error) }
|
|
120
|
+
}
|
|
121
|
+
stage = 'done'
|
|
122
|
+
|
|
30
123
|
window['__ADEP_APP__'] = {
|
|
31
124
|
platformUrl: config.platformUrl,
|
|
32
125
|
projectSlug: config.projectSlug,
|
|
33
126
|
appName: config.appName || config.projectSlug,
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
mode
|
|
127
|
+
// mode 与 invoke 契约与桌面 preload.cjs 同形(§3.2.8 硬约束④):offline 时 invoke
|
|
128
|
+
// 走设备端执行器(函数在手机上跑),online 时页面用 @adep/client 打 platformUrl。
|
|
129
|
+
mode: runtime.mode,
|
|
130
|
+
...(runtime.mode === 'offline' ? { invoke: runtime.invoke, version: runtime.version } : {}),
|
|
131
|
+
...(runtime.offlineError !== undefined ? { offlineError: runtime.offlineError } : {}),
|
|
38
132
|
/**
|
|
39
133
|
* 原生能力入口:经 Capacitor 插件 `@adep/bridge`(plugins/adep-bridge)→ 原生侧门控。
|
|
40
134
|
* 门控做在原生侧:未声明权限即不可用(PERMISSION_DENIED)且不在运行时申请。
|
|
41
135
|
*/
|
|
42
136
|
invokeNative: (capability, args) => {
|
|
43
137
|
const plugin =
|
|
44
|
-
window.Capacitor &&
|
|
45
|
-
window.Capacitor.Plugins &&
|
|
46
|
-
window.Capacitor.Plugins.AdepBridge
|
|
138
|
+
window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AdepBridge
|
|
47
139
|
if (!plugin) {
|
|
48
140
|
return Promise.reject({
|
|
49
141
|
code: 'NATIVE_BRIDGE_UNAVAILABLE',
|
|
@@ -53,4 +145,6 @@
|
|
|
53
145
|
return plugin.invokeNative({ capability, args })
|
|
54
146
|
},
|
|
55
147
|
}
|
|
148
|
+
// 装配完成:取消卡死超时(本次 IIFE 结束前 __ADEP_APP__ 已就位,超时不会再写)。
|
|
149
|
+
clearTimeout(assembleDeadline)
|
|
56
150
|
})()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 移动壳页面侧离线运行时装配(APP-022 C 段 / APP-041 双壳单一真相源 / PRD §2.11.3)。
|
|
3
|
+
*
|
|
4
|
+
* **本文件同时服务两套移动壳**(单一真相源在 app-mobile 模板;xtool 壳由渲染期复制,
|
|
5
|
+
* 见 CLI `XTOOL_SHELL_REUSED_FROM_MOBILE`——AGENTS §9「同一算法两端各存一份拷贝」教训):
|
|
6
|
+
* - capacitor 移动壳的 Android WebView(APP-041:离线树落 site/offline 后经 cap sync 进 assets);
|
|
7
|
+
* - xtool 壳的 iOS WKWebView(APP-022 C 段)。
|
|
8
|
+
* 两个宿主对本文的差别只有 WebView 引擎(V8 / JSC)与 origin(`https://localhost` /
|
|
9
|
+
* `capacitor://localhost`)——相对路径 fetch 与 `window.Capacitor.Plugins` 在两侧同形。
|
|
10
|
+
*
|
|
11
|
+
* 与桌面壳 `app-desktop/offline-runtime.mjs` **同语义、不同宿主**:
|
|
12
|
+
* 桌面在 Electron 主进程跑(node:sqlite / node:fs / data URL import);
|
|
13
|
+
* 这里在移动 WebView 里跑(**没有 Node**)——三块能力的落点全部换形态:
|
|
14
|
+
*
|
|
15
|
+
* | 能力 | 桌面(main 进程) | 移动(本文件,页面内) |
|
|
16
|
+
* | --- | --- | --- |
|
|
17
|
+
* | 读离线树 | node:fs 读 `app/offline/` | `fetch('./offline/<path>')`(同 origin,落点见 CLI `SHELL_SITE_OFFLINE_DIR` 注释) |
|
|
18
|
+
* | SQLite | node:sqlite | `AdepBridge.sqliteRun/All/Get`(原生系统 SQLite,B 段;门控三 capability) |
|
|
19
|
+
* | executor 装载 | data URL dynamic import | data URL dynamic import(**同款**——JSC/V8 原生支持标准 ESM,bundle 是引擎无关纯 JS,ADR-0007 硬约束 1) |
|
|
20
|
+
*
|
|
21
|
+
* 检测与分流同构于桌面 main.mjs 的 `hasOfflineBundle`:`offline/manifest.json`
|
|
22
|
+
* fetch 得到 → offline 形态;404 → online 形态(在线路径零额外开销,一次 fetch 404)。
|
|
23
|
+
*
|
|
24
|
+
* 本文件**自包含**(不 import 任何 npm 包):executor bundle 由服务端在构建期打进
|
|
25
|
+
* `.adepack`(`runtime/executor.mjs`),本文件只负责按 manifest 装配桥并装载它。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 在线/离线分流决策(纯函数,TS 测试面)。
|
|
30
|
+
*
|
|
31
|
+
* 为什么抽出来:这是 C 段唯一值得单独判定的「决策」——设备端函数调用走本地执行器
|
|
32
|
+
* 还是平台 HTTP,判错了整壳的数据面就错。抽成无副作用纯函数后,host-side 直接测
|
|
33
|
+
* 两种形态 + 两个错误码,不需要 WebView。
|
|
34
|
+
*
|
|
35
|
+
* @param {boolean} manifestOk `fetch('./offline/manifest.json')` 是否成功(HTTP 2xx)
|
|
36
|
+
* @returns {Promise<object>} 形如 `{ mode: 'offline' | 'online', code?: string }`
|
|
37
|
+
* (模板文件的注释不写连续双花括号——壳模板渲染的占位符钝检会把它当残留变量)。
|
|
38
|
+
* offline / online;manifestOk=false 时恒 online(在线构建不带离线树是**正常态**,不是错误)。
|
|
39
|
+
*/
|
|
40
|
+
export function decideRuntimeMode(manifestOk) {
|
|
41
|
+
return manifestOk ? { mode: 'offline' } : { mode: 'online' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 手写最小实现:把 executor 源码按 data URL 装载成模块(与桌面壳同款语义)。 */
|
|
45
|
+
async function importExecutor(source) {
|
|
46
|
+
const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(source)))
|
|
47
|
+
return import(`data:text/javascript;base64,${base64}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 把 schema/baseline 的 SQL 文本切成单条语句(忽略空白/纯注释段)。
|
|
52
|
+
* 引号内('...'/"...")的分号不是语句边界;行注释 -- 与块注释 /* *\/ 内同理。
|
|
53
|
+
* @param {string} sql 包内 schema.sql / baseline.db.sql 全文
|
|
54
|
+
* @returns {string[]} 非空语句列表(已 trim;空串与纯注释段不产出元素)
|
|
55
|
+
*/
|
|
56
|
+
function splitSqlStatements(sql) {
|
|
57
|
+
const statements = []
|
|
58
|
+
let current = ''
|
|
59
|
+
let i = 0
|
|
60
|
+
while (i < sql.length) {
|
|
61
|
+
const ch = sql[i]
|
|
62
|
+
const next = sql[i + 1]
|
|
63
|
+
if (ch === '-' && next === '-') {
|
|
64
|
+
const end = sql.indexOf('\n', i)
|
|
65
|
+
const stop = end === -1 ? sql.length : end
|
|
66
|
+
current += ' '
|
|
67
|
+
i = stop
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (ch === '/' && next === '*') {
|
|
71
|
+
const end = sql.indexOf('*/', i + 2)
|
|
72
|
+
const stop = end === -1 ? sql.length : end + 2
|
|
73
|
+
current += ' '
|
|
74
|
+
i = stop
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (ch === "'" || ch === '"') {
|
|
78
|
+
const end = sql.indexOf(ch, i + 1)
|
|
79
|
+
const stop = end === -1 ? sql.length : end + 1
|
|
80
|
+
current += sql.slice(i, stop)
|
|
81
|
+
i = stop
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (ch === ';') {
|
|
85
|
+
const statement = current.trim()
|
|
86
|
+
if (statement !== '') statements.push(statement)
|
|
87
|
+
current = ''
|
|
88
|
+
i += 1
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
current += ch
|
|
92
|
+
i += 1
|
|
93
|
+
}
|
|
94
|
+
const tail = current.trim()
|
|
95
|
+
if (tail !== '') statements.push(tail)
|
|
96
|
+
return statements
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 装配离线运行时;任何一步失败都把错误**原样抛给调用方**(页面桥把它转成
|
|
101
|
+
* `__ADEP_APP__` 装配失败状态,页面看到的是可读的错误,不是白屏静默)。
|
|
102
|
+
*
|
|
103
|
+
* @param {import('./adep-bridge.js').AdepAppConfig} config 壳配置(adep.app.json 内容)
|
|
104
|
+
* @returns {Promise<{ executor: { invoke(name: string, args: object): Promise<object> }, manifest: object, version: string }>}
|
|
105
|
+
*/
|
|
106
|
+
export async function createOfflineRuntime() {
|
|
107
|
+
// 1. manifest(树完整性由 CLI 注入面保证;这里缺件即抛,不造半个运行时)
|
|
108
|
+
const manifestResponse = await fetch('./offline/manifest.json')
|
|
109
|
+
if (!manifestResponse.ok) {
|
|
110
|
+
throw new Error(`[adep-app] 离线清单读取失败(HTTP ${manifestResponse.status})`)
|
|
111
|
+
}
|
|
112
|
+
const manifest = await manifestResponse.json()
|
|
113
|
+
|
|
114
|
+
// 2. executor bundle(服务端构建期打进包的 @adep/app-runtime 编译产物)
|
|
115
|
+
const executorResponse = await fetch('./offline/runtime/executor.mjs')
|
|
116
|
+
if (!executorResponse.ok) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`[adep-app] 离线包含 runtime/executor.mjs 缺失(HTTP ${executorResponse.status}):包不完整,请重新执行 adep app build --offline`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
const executorModule = await importExecutor(await executorResponse.text())
|
|
122
|
+
|
|
123
|
+
// 3. 函数清单 + 逐个读源码(离线白名单内 offline:true 的才可本地执行,路由归 executor)
|
|
124
|
+
const indexResponse = await fetch('./offline/functions/index.json')
|
|
125
|
+
if (!indexResponse.ok) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`[adep-app] 离线包 functions/index.json 读取失败(HTTP ${indexResponse.status})`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
const index = await indexResponse.json()
|
|
131
|
+
const functions = []
|
|
132
|
+
/* oxlint-disable no-await-in-loop -- 串行读 + 失败报错点名具体函数(并发读会让「哪个函数
|
|
133
|
+
缺源码」淹没在聚合错误里;数量少、体积小,串行代价可忽略)。 */
|
|
134
|
+
for (const entry of index.functions ?? []) {
|
|
135
|
+
const sourceResponse = await fetch(`./offline/functions/${entry.name}.mjs`)
|
|
136
|
+
if (!sourceResponse.ok) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`[adep-app] 离线包函数源码 functions/${entry.name}.mjs 缺失(HTTP ${sourceResponse.status})`
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
const source = await sourceResponse.text()
|
|
142
|
+
functions.push({ name: entry.name, source, offline: entry.offline === true })
|
|
143
|
+
}
|
|
144
|
+
/* oxlint-enable no-await-in-loop */
|
|
145
|
+
|
|
146
|
+
// 4. DeviceBridge:SQLite 走原生桥(B 段;信封 { ok, code, ... },fail loud 转异常),
|
|
147
|
+
// 预置资源走 fetch(只读——storage/** 在包内,写入无意义也无路径)。
|
|
148
|
+
const plugin = window.Capacitor?.Plugins?.AdepBridge
|
|
149
|
+
if (!plugin) {
|
|
150
|
+
throw new Error('[adep-app] 原生桥不可用(AdepBridge 未注册):离线运行时无法装配')
|
|
151
|
+
}
|
|
152
|
+
/** @type {import('./adep-bridge.js').DeviceSqliteBridge} */
|
|
153
|
+
const sqliteBridge = {
|
|
154
|
+
run: async (sql, params) => {
|
|
155
|
+
const envelope = await plugin.sqliteRun({ sql, ...(params === undefined ? {} : { params }) })
|
|
156
|
+
if (envelope.ok !== true) throw new Error(`[${envelope.code}] ${envelope.message ?? ''}`)
|
|
157
|
+
return { changes: envelope.changes }
|
|
158
|
+
},
|
|
159
|
+
all: async (sql, params) => {
|
|
160
|
+
const envelope = await plugin.sqliteAll({ sql, ...(params === undefined ? {} : { params }) })
|
|
161
|
+
if (envelope.ok !== true) throw new Error(`[${envelope.code}] ${envelope.message ?? ''}`)
|
|
162
|
+
return envelope.rows
|
|
163
|
+
},
|
|
164
|
+
get: async (sql, params) => {
|
|
165
|
+
const envelope = await plugin.sqliteGet({ sql, ...(params === undefined ? {} : { params }) })
|
|
166
|
+
if (envelope.ok !== true) throw new Error(`[${envelope.code}] ${envelope.message ?? ''}`)
|
|
167
|
+
return envelope.row
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
const contents = Array.isArray(manifest.contents) ? manifest.contents : []
|
|
171
|
+
const filesBridge = {
|
|
172
|
+
writeFile: async (path) => {
|
|
173
|
+
throw new Error(`[adep-app] 离线包预置资源只读:storage/${path}`)
|
|
174
|
+
},
|
|
175
|
+
readFile: async (path) => {
|
|
176
|
+
const response = await fetch(`./offline/storage/${path}`)
|
|
177
|
+
if (!response.ok) throw new Error(`[adep-app] 离线包缺少预置资源 storage/${path}`)
|
|
178
|
+
return new Uint8Array(await response.arrayBuffer())
|
|
179
|
+
},
|
|
180
|
+
removeFile: async (path) => {
|
|
181
|
+
throw new Error(`[adep-app] 离线包预置资源只读:storage/${path}`)
|
|
182
|
+
},
|
|
183
|
+
listFiles: async (prefix = '') =>
|
|
184
|
+
contents
|
|
185
|
+
.filter(
|
|
186
|
+
(entry) => entry.path.startsWith('storage/') && entry.path.startsWith(`storage/${prefix}`)
|
|
187
|
+
)
|
|
188
|
+
.map((entry) => ({ path: entry.path.slice('storage/'.length), size: entry.size ?? 0 })),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. 本地库结构初始化(与桌面壳 offline-runtime.mjs 的 schema + baseline 步骤同语义)。
|
|
192
|
+
// 桌面在 main 进程用 node:sqlite 的 exec(原生多语句);这里语句要过原生桥,
|
|
193
|
+
// 而 B 段桥**按安全设计拒绝多语句**(pzTail 检测,APP-022 B 段)——所以按
|
|
194
|
+
// 「语句边界」受控切分后逐条 run。切分器只认 SQL 文本里的分号终结(引号/注释
|
|
195
|
+
// 内的分号不终结,与 CLI 侧 maskFencedBlocks 同思路)。
|
|
196
|
+
/* oxlint-disable no-await-in-loop -- 必须串行:DDL/DML 的执行顺序有语义(CREATE 先于
|
|
197
|
+
INSERT;baseline 逐行写入),并行会让「第几条语句失败」不可判读;初始化只发生一次、
|
|
198
|
+
语句数少,串行代价可忽略。函数源码读取段的同款豁免见上方步骤 3。 */
|
|
199
|
+
for (const dbFile of ['db/schema.sql', 'db/baseline.db.sql']) {
|
|
200
|
+
const response = await fetch(`./offline/${dbFile}`)
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new Error(`[adep-app] 离线包 ${dbFile} 读取失败(HTTP ${response.status})`)
|
|
203
|
+
}
|
|
204
|
+
const sqlText = await response.text()
|
|
205
|
+
for (const statement of splitSqlStatements(sqlText)) {
|
|
206
|
+
const envelope = await plugin.sqliteRun({ sql: statement })
|
|
207
|
+
if (envelope.ok !== true) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[adep-app] 本地库初始化失败(${dbFile}):[${envelope.code}] ${envelope.message ?? ''}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/* oxlint-enable no-await-in-loop */
|
|
215
|
+
|
|
216
|
+
// env:离线包永不下发 secret 值(§4.2);public 环境变量同样不进包(manifest app 段无 env 面)。
|
|
217
|
+
const env = { publicVars: {}, secretKeys: [] }
|
|
218
|
+
const executor = executorModule.createLocalExecutor({
|
|
219
|
+
functions,
|
|
220
|
+
bridge: { sqlite: sqliteBridge, files: filesBridge },
|
|
221
|
+
env,
|
|
222
|
+
})
|
|
223
|
+
const versionResponse = await fetch('./offline/runtime/version.txt')
|
|
224
|
+
if (!versionResponse.ok) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`[adep-app] 离线包 runtime/version.txt 读取失败(HTTP ${versionResponse.status})`
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return { executor, manifest, version: (await versionResponse.text()).trim() }
|
|
230
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<!--
|
|
4
|
+
AdepShell 的 Info.plist 覆盖层(APP-016)。
|
|
5
|
+
|
|
6
|
+
只放「要覆盖 xtool 默认值」的键,不放全量清单——xtool 在 Planner 里已经注入:
|
|
7
|
+
CFBundleIdentifier(来自 xtool.yml 的 bundleID)/ CFBundleVersion=1 /
|
|
8
|
+
CFBundleShortVersionString=1.0.0 / ShortVersion / MinimumOSVersion(取 Package.swift 的 iOS 版本)/
|
|
9
|
+
CFBundleName 与 CFBundleExecutable(= library 名 AdepShell)/ CFBundlePackageType /
|
|
10
|
+
UIDeviceFamily=[1,2] / UISupportedInterfaceOrientations(含 ~ipad) /
|
|
11
|
+
UILaunchScreen={}(纯色启动图,P0-5 判为「降级可用」)/ LSRequiresIPhoneOS /
|
|
12
|
+
UIRequiredDeviceCapabilities=[arm64] / CFBundleSupportedPlatforms
|
|
13
|
+
这些键**不在这里重复**:重复会形成两处真相源,且本文件优先级更高(merge 右侧胜)
|
|
14
|
+
⇒ 万一将来 xtool 改了默认值,这里会把旧值钉死。
|
|
15
|
+
|
|
16
|
+
本文件只钉一件事:显示名与产品名分离。产品名必须是 ASCII 稳定标识符(AdepShell),
|
|
17
|
+
显示名可以是中文 / 带空格({{appName}})。
|
|
18
|
+
-->
|
|
19
|
+
<plist version="1.0">
|
|
20
|
+
<dict>
|
|
21
|
+
<key>CFBundleDisplayName</key>
|
|
22
|
+
<string>{{appName}}</string>
|
|
23
|
+
</dict>
|
|
24
|
+
</plist>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
//
|
|
3
|
+
// AdepShell 的 Swift Package Manager 清单(APP-016,xtool 壳)。
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ **本包必须恰好有一个 library product,且 product 名 = target 名 = `AdepShell`**:
|
|
6
|
+
// xtool 打包时生成一个 `.executableTarget` 外层工程(stub.c 为空壳 + `@main` 由本库提供),
|
|
7
|
+
// 再用 `xtool.yml` 的 `product:` 在包内**按名字**选中要打成 .app 的那一个
|
|
8
|
+
// (PackLib/Planner.swift 的 `matching: schema.product`)。名字对不上不是编译错,
|
|
9
|
+
// 而是 `xtool dev build` 报「找不到 main product」。
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ **产品名不得吃 `{{appName}}`**:`appName` 是显示名(可含中文 / 空格 / 连字符),
|
|
12
|
+
// 而 SwiftPM 的 target 名、`CFBundleExecutable`、`CFBundleName` 都由它派生
|
|
13
|
+
// (Planner 用 `library.name` 填这三个键)⇒ 一旦渲染进中文名就是编译期就挂。
|
|
14
|
+
// 显示名走 `AdepShell-Info.plist` 的 `CFBundleDisplayName`(经 xtool.yml 的 `infoPath`
|
|
15
|
+
// 合并覆盖默认值),两者**刻意分离**。
|
|
16
|
+
//
|
|
17
|
+
// 依赖三项,缺一不可:
|
|
18
|
+
// - `capacitor-swift-pm`:Capacitor 的 **binaryTarget** 分发(xcframework 从 GitHub Releases
|
|
19
|
+
// 下载,非源码编译)⇒ 构建期**需要网络**;xtool 的 Packer 有 `.binaryTarget` 分支,
|
|
20
|
+
// 会把 `Capacitor.framework` 落到 .app 的 `Frameworks/` 并签名。
|
|
21
|
+
// - `Capacitor` **与** `Cordova` 两个 product:Capacitor 的 swiftinterface 里是
|
|
22
|
+
// `@_exported import Cordova`,只声明 Capacitor 会报 `module 'Cordova' not found`
|
|
23
|
+
// (与 `plugins/adep-bridge/Package.swift` 同一个坑,那里已有实测记录)。
|
|
24
|
+
// - 本地 `plugins/adep-bridge`:**源码单一来源**——本模板不复制插件源码,该目录在渲染期
|
|
25
|
+
// 由 CLI 注入(Phase 3 接线)。仓库内 `AdepBridgePlugin` 的实现只在
|
|
26
|
+
// `templates/app-mobile/plugins/adep-bridge/` 一处。
|
|
27
|
+
// ⇒ 未注入时本清单会因路径不存在而**立刻失败**(这是刻意的:宁可构建失败,
|
|
28
|
+
// 也不要产出一个没有原生桥的壳)。
|
|
29
|
+
// - `sqlcipher/SQLCipher.swift`(APP-025):经 adep-bridge 传递依赖(SQLCipher product),
|
|
30
|
+
// 本清单不直接声明——xtool Packer 的 `.binaryTarget` 分支会把它随 Capacitor 一起
|
|
31
|
+
// 落进 .app 的 Frameworks/ 并签名。
|
|
32
|
+
import PackageDescription
|
|
33
|
+
|
|
34
|
+
let package = Package(
|
|
35
|
+
name: "AdepShell",
|
|
36
|
+
platforms: [
|
|
37
|
+
// iOS 17 下限:WKWebView 的 loadFileURL / 自定义 scheme 行为在 16 上仍在变,
|
|
38
|
+
// 而 P0-1 已就位的是 iPhoneOS 27.1 SDK(计划 §12.16)⇒ 不向下兼容更老的运行时。
|
|
39
|
+
.iOS(.v17)
|
|
40
|
+
],
|
|
41
|
+
products: [
|
|
42
|
+
// xtool 要求「恰好一个」代表主 app 的 library product。
|
|
43
|
+
.library(name: "AdepShell", targets: ["AdepShell"])
|
|
44
|
+
],
|
|
45
|
+
dependencies: [
|
|
46
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"),
|
|
47
|
+
.package(path: "plugins/adep-bridge")
|
|
48
|
+
],
|
|
49
|
+
targets: [
|
|
50
|
+
.target(
|
|
51
|
+
name: "AdepShell",
|
|
52
|
+
dependencies: [
|
|
53
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
54
|
+
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
55
|
+
.product(name: "AdepBridge", package: "adep-bridge")
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# app-mobile-xtool(xtool 壳模板,APP-016)
|
|
2
|
+
|
|
3
|
+
用 [xtool](https://xtool.sh/) 把本工程直接编成**真 iOS app**(`.app` / `.ipa`),不需要
|
|
4
|
+
Xcode 工程文件、不需要 CocoaPods。产物是 unsigned 的(签名在安装时由 xtool 用本地
|
|
5
|
+
免费签名完成),与既有 `app-mobile`(Capacitor + Xcode 工程)**并存**,不替代它。
|
|
6
|
+
|
|
7
|
+
## 它与 `app-mobile` 的分工
|
|
8
|
+
|
|
9
|
+
| | `app-mobile` | `app-mobile-xtool`(本模板) |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| 工程形态 | Capacitor 标准工程(`npx cap add ios`) | SwiftPM 包 + `xtool.yml` |
|
|
12
|
+
| iOS 构建 | Xcode 工程(`.xcodeproj`)+ CocoaPods/SPM | `xtool dev build`(SwiftPM 直编) |
|
|
13
|
+
| Android | 有 | **无**(xtool 只做 iOS) |
|
|
14
|
+
| 原生桥 | `plugins/adep-bridge`(双形态) | 同一个目录,**源码单一来源**,渲染期注入 |
|
|
15
|
+
| 前端产物落点 | `app/site/` | `app/site/`(同契约) |
|
|
16
|
+
|
|
17
|
+
## 前置(缺一不可)
|
|
18
|
+
|
|
19
|
+
1. **Xcode**(提供 iPhoneOS SDK):`xcodebuild -showsdks | grep -i iphoneos` 要有输出。
|
|
20
|
+
没有 Xcode 时 `xtool dev build` 会报找不到 SDK。
|
|
21
|
+
2. **xtool** 且已登录:`xtool auth status` 显示 `Logged in.`。
|
|
22
|
+
3. **真机安装**另需:iPad/iPhone 已配对,且设备侧开启**开发者模式**
|
|
23
|
+
(设置 → 隐私与安全性 → 开发者模式 → 重启;iOS 16+ 强制门禁)。
|
|
24
|
+
4. 首次构建需要**网络**:Capacitor 以 xcframework 分发,从 GitHub Releases 下载
|
|
25
|
+
(约 6 MB)。离线环境需预置 SPM 缓存。
|
|
26
|
+
|
|
27
|
+
## 构建
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1) 把前端产物注入 app/site/(CLI 在真实流程里做;本地手测可直接拷)
|
|
31
|
+
# 站点根必须有 index.html
|
|
32
|
+
cp -R dist/* app/site/
|
|
33
|
+
|
|
34
|
+
# 2) 注入插件源码(源码单一来源;未注入时 SwiftPM 会因路径不存在直接报错)
|
|
35
|
+
mkdir -p plugins && cp -R ../app-mobile/plugins/adep-bridge plugins/
|
|
36
|
+
|
|
37
|
+
# 3) 只出 unsigned 产物(不碰真机、不签名)
|
|
38
|
+
xtool dev build --ipa
|
|
39
|
+
unzip -l xtool/AdepShell.ipa # 产物名 = 产品名 AdepShell
|
|
40
|
+
|
|
41
|
+
# 4) 装到已配对设备
|
|
42
|
+
xtool dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> 产物路径是 `<工程根>/xtool/<产品名>.ipa`(**随产品名**,不是固定的 `app.ipa`——
|
|
46
|
+
> 计划 §12.18 已订正这条)。
|
|
47
|
+
|
|
48
|
+
## 安装位置与页面来源
|
|
49
|
+
|
|
50
|
+
`xtool.yml` 的 `resources: [app/site]` 会把整个 `app/site/` 目录**递归**拷到 bundle 根
|
|
51
|
+
(`Payload/AdepShell.app/site/`,嵌套相对路径原样保留)。壳在
|
|
52
|
+
`ShellViewController.instanceDescriptor()` 里把 Capacitor 的 `appLocation` 指到这个目录,
|
|
53
|
+
于是页面从 `capacitor://localhost/` 加载——**不是** `file://`:
|
|
54
|
+
|
|
55
|
+
- 走 file:// 时页面 origin 是 `null`,`fetch()` 打平台地址会带 `Origin: null`,服务端要额外放行;
|
|
56
|
+
走 Capacitor 的本地 scheme 拿到的是正常源,MIME 与相对路径解析交给框架既有实现。
|
|
57
|
+
- `appLocation` 必须在 `loadView()` 之前定好(那一刻 `assetHandler` 的路径就固定了),
|
|
58
|
+
所以注入点在 `instanceDescriptor()`,不在 `capacitorDidLoad()`。
|
|
59
|
+
|
|
60
|
+
**站点缺失时不会白屏也不会崩**:壳会生成一个写明「站点资源未就绪 + 期望路径」的错误页
|
|
61
|
+
(Capacitor 原生行为是 `fatalError` 崩溃)。
|
|
62
|
+
|
|
63
|
+
## 两条不能"顺手优化"的约定
|
|
64
|
+
|
|
65
|
+
1. **产品名固定 `AdepShell`,不得吃 `{{appName}}`。**
|
|
66
|
+
`xtool.yml` 的 `product:`、`Package.swift` 的 target 名、`CFBundleExecutable` 都由它派生;
|
|
67
|
+
而 `appName` 是显示名(可含中文 / 空格)⇒ 一旦渲染进产品名就是编译期直接失败。
|
|
68
|
+
显示名走 `AdepShell-Info.plist` 的 `CFBundleDisplayName`。
|
|
69
|
+
2. **`xtool.yml` 的键名必须逐字来自 xtool 的 schema。**
|
|
70
|
+
Yams 解码**静默忽略未知键**(`PackSchemaBase`):写错 `bundleId` 不报错、只是不生效。
|
|
71
|
+
合法键:`version` / `orgID` / `bundleID` / `product` / `infoPath` / `entitlementsPath` /
|
|
72
|
+
`iconPath` / `resources` / `extensions` / `skipLSP`。
|
|
73
|
+
|
|
74
|
+
## 已知限制
|
|
75
|
+
|
|
76
|
+
- **免费签名 7 天**:设备上装的 app 7 天后失效,重装即可。只影响验收,不构成分发方案。
|
|
77
|
+
- **图标只收 1024 png**、启动图只有纯色(`UILaunchScreen = {}`)——P0-5 判为「降级可用」。
|
|
78
|
+
- **无 Android**:本模板不声明 `androidPackageId`。
|
|
79
|
+
- **插件注册未接线**:本模板只保证 `AdepBridge` 被编译并链接(`AppDelegate` 里有可观测的
|
|
80
|
+
引用与日志),把插件交给 Capacitor 插件表属 Phase 3 / APP-018。
|
|
81
|
+
- **未做离线包**:`app/offline/`(`.adepack`)属 Phase 4。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import AdepBridge
|
|
2
|
+
import Capacitor
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// AdepShell 的进程入口(APP-016)。
|
|
6
|
+
///
|
|
7
|
+
/// xtool 的打包方式是:用外层 `.executableTarget`(只有一个空的 `stub.c`)把本 library
|
|
8
|
+
/// 链进去,`main` 符号由本文件的 `@main` 提供(`PackLib/Packer.swift` 生成 shim,
|
|
9
|
+
/// P0-4 探针用同形态的 `@main` 已真机跑通,计划 §12.16)。
|
|
10
|
+
///
|
|
11
|
+
/// ⚠️ 这里**只搭窗口**:不要在这一层做站点加载。站点路径的解析在
|
|
12
|
+
/// `ShellViewController.instanceDescriptor()`——Capacitor 的 `assetHandler` 路径
|
|
13
|
+
/// 在 `loadView()` 里就按 `appLocation` 定好了,晚于那一刻再改会得到「配的是一处、
|
|
14
|
+
/// 加载的是另一处」的静默错位。
|
|
15
|
+
@main
|
|
16
|
+
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
17
|
+
var window: UIWindow?
|
|
18
|
+
|
|
19
|
+
func application(
|
|
20
|
+
_ application: UIApplication,
|
|
21
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
22
|
+
) -> Bool {
|
|
23
|
+
// 显式引用 AdepBridge 类型:SPM 静态库按需链接,**从未被引用**的模块会被整块丢掉,
|
|
24
|
+
// 运行时症状是插件调用报 "plugin is not implemented" 而编译毫无告警。
|
|
25
|
+
// 这一行同时是「N-3:Capacitor + 本地插件能否在 xtool 壳里链在一起」的可观测证据。
|
|
26
|
+
// 插件注册接线在 `ShellViewController.capacitorDidLoad()`(APP-018)——不在这里:
|
|
27
|
+
// 注册必须晚于 bridge 构造(`loadView()` 内),而本文件的 didFinishLaunching 早于它。
|
|
28
|
+
CAPLog.print("[adep-shell] boot; AdepBridge linked: \(String(describing: AdepBridge.self))")
|
|
29
|
+
|
|
30
|
+
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
31
|
+
window.rootViewController = ShellViewController()
|
|
32
|
+
window.makeKeyAndVisible()
|
|
33
|
+
self.window = window
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|