可信源码风险提示:保存或下载前请阅读源码和权限声明。
// author:紫初辰 QQ:2027439658
const baseDir = 'internal://files/../../app/'
// 响应式状态
const status = ui.signal('准备就绪')
const srcPkg = ui.signal('')
const dstPkg = ui.signal('')
function joinPath(dir, name) {
return dir + (dir.endsWith('/') ? '' : '/') + name
}
function getDirName(p) {
var i = p.lastIndexOf('/')
return i >= 0 ? p.substring(0, i) : ''
}
// system.files → Promise 包装
function fs(method, params) {
return new Promise(function(resolve) {
system.files[method](Object.assign({}, params, {
success: function(d) { resolve({ ok: true, data: d }) },
fail: function(_, code) { resolve({ ok: false, code: code }) }
}))
})
}
// ===== 递归复制(get + subFiles) =====
async function copyDir(src, dst) {
var r = await fs('get', { uri: src })
if (!r.ok) { console.log('get失败: ' + src + ' code=' + r.code); return }
var subs = (r.data.subFiles || [])
console.log('get ' + src + ' -> ' + subs.length + ' 项')
for (var i = 0; i < subs.length; i++) {
var f = subs[i]
var name = f.name
if (!name) {
var u = f.uri || ''
name = u.substring(u.lastIndexOf('/') + 1)
}
if (!name) continue
var sc = joinPath(src, name)
var dc = joinPath(dst, name)
if (f.type === 'dir') {
await fs('mkdir', { uri: dc, recursive: true })
await copyDir(sc, dc)
} else {
var cr = await fs('copy', { srcUri: sc, dstUri: dc })
if (cr.ok) console.log('[FILE] ' + dc)
else console.log('[FILE] 失败: ' + dc + ' code=' + cr.code)
}
}
}
// ===== 选择源包 =====
async function pickSource() {
status.set('读取应用列表...')
var ar = await fs('readText', { uri: 'internal://files/../../apps.json' })
if (!ar.ok) { script.toast('读取apps.json失败'); status.set('就绪'); return }
var apps = []
try { apps = JSON.parse(ar.data.text).InstalledApps || [] } catch(e) {}
if (apps.length === 0) { script.toast('没有已安装的应用'); status.set('就绪'); return }
var items = apps.map(function(a) {
return { label: (a.names && a.names[0] && a.names[0].value) || a.package, value: a.package }
})
var choice = await dialog.select({
title: '选择要克隆的应用',
items: items,
multiple: false
})
if (choice.action === 'confirm' && choice.value) {
srcPkg.set(choice.value)
dstPkg.set(choice.value + '.clone')
status.set('已选择: ' + choice.value)
}
}
// ===== 修改目标包名 =====
async function renameTarget() {
var r = await dialog.text({
title: '目标包名',
message: '输入新应用的包名',
value: dstPkg.get() || 'com.test.app',
required: true,
maxLength: 64
})
if (r.action === 'confirm' && r.value && r.value.trim()) {
dstPkg.set(r.value.trim())
status.set('目标包名已更新')
}
}
// ===== 执行克隆 =====
async function doClone() {
if (!srcPkg.get()) { script.toast('请先选择源包'); return }
if (!dstPkg.get()) { script.toast('请设置目标包名'); return }
status.set('克隆中...')
var srcDir = joinPath(baseDir, srcPkg.get())
var dstDir = joinPath(baseDir, dstPkg.get())
// 1. 创建目标目录
await fs('mkdir', { uri: dstDir, recursive: true })
// 2. 递归复制
await copyDir(srcDir, dstDir)
// 3. 读源 manifest 拿图标路径
var srcIcon = ''
var srcMr = await fs('readText', { uri: joinPath(srcDir, 'manifest.json') })
if (srcMr.ok) {
try { srcIcon = JSON.parse(srcMr.data.text).icon || '' } catch(e) {}
}
// 4. 改目标 manifest 包名 + 图标
var dstMr = await fs('readText', { uri: joinPath(dstDir, 'manifest.json') })
if (dstMr.ok) {
try {
var m = JSON.parse(dstMr.data.text)
m.package = dstPkg.get()
if (srcIcon) m.icon = srcIcon
await fs('writeText', { uri: joinPath(dstDir, 'manifest.json'), text: JSON.stringify(m, null, 2) })
} catch(e) { console.log('manifest解析失败') }
}
// 5. 复制图标文件
if (srcIcon) {
var cleanPath = srcIcon.replace(/^\/+/, '')
var srcIconFull = joinPath(srcDir, cleanPath)
var dstIconFull = joinPath(dstDir, cleanPath)
await fs('mkdir', { uri: getDirName(dstIconFull), recursive: true })
await fs('copy', { srcUri: srcIconFull, dstUri: dstIconFull })
}
// 6. 注册到 apps.json
var ar = await fs('readText', { uri: 'internal://files/../../apps.json' })
if (ar.ok) {
try {
var j = JSON.parse(ar.data.text)
if (!j.InstalledApps) j.InstalledApps = []
var exist = false
for (var i = 0; i < j.InstalledApps.length; i++) {
if (j.InstalledApps[i].package === dstPkg.get()) { exist = true; break }
}
if (!exist) {
j.InstalledApps.push({
package: dstPkg.get(),
versionName: '1.0.0',
versionCode: 1,
icon: srcIcon || 'common/icon.png',
minAPILevel: 1,
minPlatformVersion: 1000,
fingerprint: '',
names: [{ value: 'Cloned App', lang: 'defaults' }]
})
await fs('writeText', { uri: 'internal://files/../../apps.json', text: JSON.stringify(j, null, 4) })
}
} catch(e) { console.log('apps.json处理失败') }
}
status.set('克隆完成')
script.toast('克隆完成,重启后生效')
}
// ===== UI 布局 =====
ui.showHeader(false)
ui.render(function() {
return ui.column([
ui.heading('应用克隆工具', { size: 26 }),
ui.text('状态: ' + status.get(), { size: 17, color: '#b9c8cf' }),
ui.text('源包: ' + (srcPkg.get() || '未选择'), { size: 16, color: '#c1cbd1' }),
ui.text('目标: ' + (dstPkg.get() || '未设置'), { size: 16, color: '#c1cbd1' }),
ui.row([
ui.button('选择源包', pickSource, { background: '#1976a3', height: 48, lines: 1 }),
ui.button('改包名', renameTarget, { background: '#1976a3', height: 48, lines: 1 }),
], { gap: 8 }),
ui.button('开始克隆', doClone, { background: '#2e7d32', height: 54, lines: 1 }),
ui.button('退出', function() { script.exit() }, { background: '#34373d', height: 44, lines: 1 })
], { gap: 12, padding: 16 })
})