Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a01d218
feat(android): 安卓离线版首个可运行版本存档
Fly143 Aug 1, 2026
f339815
docs(android): 记录漫画工作台在安卓端暂不可用的根因
Fly143 Aug 1, 2026
a160ebb
feat(android): 漫画工作台原生启用(NDK 编译 bionic sharp + Termux libvips)
Fly143 Aug 2, 2026
a41671b
docs(android): 新增资产构建脚本 + 安卓端构建说明
Fly143 Aug 2, 2026
a62dd87
fix(db): 补充 ComicCharacter.gender 列迁移
Fly143 Aug 2, 2026
4935acc
fix(db): 补齐漫画模块缺失的建表与列迁移
Fly143 Aug 2, 2026
6beab7f
chore(android): 发布版签名配置 + 版本号 0.4.7.1
Fly143 Aug 3, 2026
4b162b1
revert: @ai-novel/shared 依赖还原为 workspace:*
Fly143 Aug 3, 2026
a1aa6aa
style: package.json 末尾补换行
Fly143 Aug 3, 2026
151a4ab
fix(onboarding): 修复首次启动模型配置弹窗
Fly143 Aug 3, 2026
e95094f
perf(android): APK 体积减半(433MB→222MB)
Fly143 Aug 3, 2026
99a037e
feat(android): 强制横屏 + 统一桌面布局
Fly143 Aug 3, 2026
a19c7eb
feat(android): WebView 下载导出到公共 Downloads/AINovel
Fly143 Aug 3, 2026
d9dda2a
fix(android): sharp 改用 WASM 版,node-project.zip 改为固定资产
Fly143 Aug 3, 2026
6d775ed
build(android): 安卓构建必需资产入库(fork 可直接构建 APK)
Fly143 Aug 3, 2026
a5542f6
docs: 更新安卓构建文档(资产已入库、fork 两步构建)+ shared 构建步骤
Fly143 Aug 3, 2026
f5fd2ab
feat(android): 解锁短剧工作台入口 + 横竖屏方向布局
Fly143 Aug 3, 2026
2574e04
docs: release notes 记录 0.4.7.2(短剧解锁 + 方向布局)
Fly143 Aug 3, 2026
ea847fd
fix(android): 方向改回 fullSensor(sensorPortrait 不允许横屏)+ 版本号 0.4.7.2
Fly143 Aug 3, 2026
bf70d33
chore(android): 更新 .env.android 参考配置(漫画已用 WASM sharp,移除过时禁用项)
Fly143 Aug 3, 2026
cc805ed
docs: 措辞统一——安卓版为'本地运行'(模型调用需联网),非'离线运行'
Fly143 Aug 4, 2026
d17f780
Merge remote-tracking branch 'origin/main' into merge_test
Fly143 Aug 9, 2026
3a94499
merge: 解决与 upstream 的冲突(README/release-notes/AppLayout)
Fly143 Aug 9, 2026
29daaf0
Merge branch 'ExplosiveCoderflome:main' into main
Fly143 Aug 9, 2026
1338c14
build(android): 版本 0.4.9 + 应用名改为英文 AI Novel Studio
Fly143 Aug 9, 2026
064742a
Merge remote-tracking branch 'origin/main'
Fly143 Aug 9, 2026
7259246
fix(android): 修复 APK 内前端版本不更新(www.zip 未重新生成)+ WebView 缓存
Fly143 Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 41 additions & 35 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
# 依赖与构建产物
node_modules/
dist/
.turbo/
.DS_Store
.env
.env.local
client/node_modules/
server/node_modules/
desktop/node_modules/
shared/dist/
server/dist/
client/dist/
desktop/dist/
site/dist/

# 数据库(本地运行产生的)
dev.db
client/.env
client/.env.local
server/.env
server/.env.local
dev.db-shm
dev.db-wal
*.db-journal
server/dev.db
server/dev.db-shm
server/dev.db-wal
server/tmp_*.js
server/tmp/**/*.db
server/tmp/**/*.db*
server/tmp/**/*.db-*
server/tmp/**/*.sqlite
server/tmp/**/*.sqlite*
server/tmp/**/*.sqlite-*
server/storage/
server/src/prisma/dev.db
server/src/prisma/dev.db-journal
.history/
.histyory/
.logs/
.tmp/
.codegraph/
.trae/
server/.tmp/
desktop/build/
desktop/runtime-migration-smoke/
server/prisma-diff-migrations-to-schema.sql

# AI tool artifacts
.claude/plan/
.claude/memory/
.spec-workflow/
.codex-backups/
.cursor/
# Android 构建产物
android-app/.gradle/
android-app/build/
android-app/app/build/
android-app/app/.cxx/
android-app/local.properties
android-app/C:Androidbuild_done.flag
# node 资产(重二进制/可重建产物;保留手写源码 app.js / init-db.cjs)
android-app/app/src/main/assets/nodejs/node-project.zip
android-app/app/src/main/assets/nodejs/bundle.cjs
android-app/app/src/main/assets/nodejs/node_modules/
android-app/app/src/main/assets/nodejs/dev.db
android-app/app/src/main/assets/nodejs/.extracted
android-app/app/src/main/assets/www/
android-app/app/src/main/assets/www.zip
android-app/app/src/main/assets/*.zip
*.iml

# 测试残留
server/selftest.js
server/test-android.db
server/test-android.db-shm
server/test-android.db-wal

# 其他
ARM64/
android-app/release.keystore
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,49 @@
- 章节批次完成后,后台资源索引等收尾事件不会覆盖主任务的完成状态,页面会继续显示真实进度和章节范围。
- README 提供支付宝捐赠二维码,方便支持项目持续开发与维护。

### 2026-08-03

- 安卓端构建资产全部入库,fork 仓库后按文档两步即可构建出可运行的 APK。
- 漫画工作台渲染改用 WASM sharp,免原生库依赖,真机渲染稳定,APK 体积降至约 163MB。
- 修复首次启动模型配置弹窗、安卓网络连接失败等问题;导出文件写入公共下载目录。
- 手机端统一为桌面布局(横屏固定),操作入口与电脑端一致。

> 查看完整更新历史:[docs/releases/release-notes.md](./docs/releases/release-notes.md)

## 安卓端构建(fork 后构建必读)

安卓 APK 需要的全部内嵌资产已**入库**(`node-project.zip` 40MB / `noderuntime/` 48MB / `migrations.sqlite/`),
fork 后 clone 下来即可直接构建,无需从 Release 下载或从 APK 提取:

```bash
git clone https://github.com/<你的 fork>/AI-Novel-Writing-Assistant.git
cd AI-Novel-Writing-Assistant
pnpm install # 安装依赖(esbuild / vite / prisma 等)
bash android/build-android-assets.sh # 生成 bundle.cjs + www.zip(node-project.zip 为固定资产,跳过重建)
cd android-app
# 首次需创建 local.properties 指向你的 Android SDK:sdk.dir=C:/Android/sdk
gradle clean assembleDebug
```

产物:`android-app/app/build/outputs/apk/debug/app-debug.apk`(约 166MB,含内嵌 Node 运行时)。

> ⚠️ **每次构建 APK 前都必须重跑 `build-android-assets.sh`**:NodeService 运行时解压的是
> `www.zip`(不是 `assets/www/` 目录)。只改前端代码/只同步 `www/` 目录而不重新打 `www.zip`,
> 打包进 APK 的仍是旧前端(版本号、功能不更新)。前端、后端代码有任何改动,都要先跑
> `bash android/build-android-assets.sh` 再 `gradle assembleDebug`。
>
> 脚本会:① 构建 `@ai-novel/shared`(workspace 包,tsc 生成 dist);② esbuild 完整打包
> `server/src/app.ts` → `bundle.cjs`(express 等打进包内,仅 sharp/better-sqlite3/@prisma/client
> 走原生加载);③ 打包前端 `www/` → `www.zip`;④ `node-project.zip` 是固定资产(依赖锁定,
> 含安卓 ELF better-sqlite3 + WASM sharp),存在则跳过重建。
>
> 漫画工作台 sharp 使用 **WASM 版**(@img/sharp-wasm32 + @emnapi/runtime,免原生库):
> 原生 bionic 版依赖 libvips 全家,libstdc++ ABI 在真机不兼容(dlopen 失败),故已弃用。
> 旧文档见 [android/sharp-android/README.md](./android/sharp-android/README.md)(历史参考)。
>
> 构建环境需配置:Android SDK(compileSdk 35)、JDK 17、Gradle 8.9(或使用 wrapper)。
> 构建脚本在 MSYS/Git-Bash / Linux / macOS 下均可运行(路径已兼容)。

## 功能预览
### 功能概览中的95%以上编写都是AI完成

Expand Down
20 changes: 20 additions & 0 deletions android-app/app/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.4.1)

add_library(nodejs-mobile-native-lib
SHARED
src/main/cpp/native-lib.cpp
src/main/cpp/rn-bridge.cpp)

include_directories(src/main/cpp/libnode/include/node/)
include_directories(src/main/cpp/)

# nodejs-mobile 预编译运行时
add_library(libnode SHARED IMPORTED)
set_target_properties(libnode PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libnode.so)

find_library(log-lib log)

target_link_libraries(nodejs-mobile-native-lib
libnode
${log-lib})
71 changes: 71 additions & 0 deletions android-app/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
plugins {
id 'com.android.application'
}

android {
namespace 'com.ainovel.assistant'
compileSdk 35

defaultConfig {
applicationId "com.ainovel.assistant"
minSdk 31
targetSdk 35
versionCode 9
versionName "0.4.9"
ndk {
abiFilters 'arm64-v8a'
}
}

signingConfigs {
release {
storeFile file('../release.keystore')
storePassword 'ainovel123'
keyAlias 'ainovel'
keyPassword 'ainovel123'
}
}

buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

// 排除 nodejs/node_modules 冗余目录(已在 node-project.zip 内,仅作打包源用;
// 见 build-android-assets.sh 步骤 3。运行时只解压 zip)
sourceSets {
main {
assets {
srcDirs 'src/main/assets'
exclude 'nodejs/node_modules/**'
}
}
}

aaptOptions {
noCompress 'node', 'js', 'so', 'zip'
}

buildFeatures {
viewBinding true
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'com.google.android.material:material:1.12.0'
}
12 changes: 12 additions & 0 deletions android-app/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 保留 nodejs-mobile JNI 绑定类
-keep class com.nodejs.mobile.** { *; }
-keep class io.nodejs.mobile.** { *; }
-keep class nodejs.** { *; }

# 保留 better-sqlite3 等 native 模块名
-keep class better_sqlite3 { *; }

# WebView / 反射调用不混淆
-keepattributes *Annotation*
-keep class * extends android.app.Service
-keep class * extends android.app.Activity
37 changes: 37 additions & 0 deletions android-app/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:label="AI Novel Studio"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:extractNativeLibs="true"
android:theme="@style/AppTheme">

<service
android:name=".NodeService"
android:exported="false"
android:foregroundServiceType="dataSync" />

<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullSensor"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|density|uiMode"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
14 changes: 14 additions & 0 deletions android-app/app/src/main/assets/nodejs/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 安卓端入口:设置运行环境后启动打包后的 server bundle
process.env.NODE_PATH = __dirname;
process.env.AI_NOVEL_RUNTIME = process.env.AI_NOVEL_RUNTIME || 'web';
process.env.AI_NOVEL_DATABASE_MODE = process.env.AI_NOVEL_DATABASE_MODE || 'sqlite';
process.env.PORT = process.env.PORT || '3000';
process.env.HOST = process.env.HOST || '127.0.0.1';
process.env.AI_NOVEL_APP_DATA_DIR = __dirname;

const server = require('./bundle.cjs');
// 安卓/web 模式由 NodeService 提前用 init-db.cjs 建表,这里直接启动监听
server.startServer().catch((error) => {
console.error('[server] bootstrap failed.', error);
process.exit(1);
});
61 changes: 61 additions & 0 deletions android-app/app/src/main/assets/nodejs/init-db.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 安卓专用:用 better-sqlite3 直接执行 Prisma 的 SQL 迁移建表(不依赖 prisma CLI)
const fs = require('node:fs');
const path = require('node:path');
const Database = require('better-sqlite3');

const dbPath = path.join(__dirname, 'dev.db');
const migrationsDir = path.join(__dirname, 'migrations.sqlite');

const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

// 创建 prisma 迁移追踪表(与 prisma 兼容)
db.exec(`
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"id" TEXT PRIMARY KEY NOT NULL,
"checksum" TEXT NOT NULL,
"finished_at" DATETIME,
"migration_name" TEXT NOT NULL,
"logs" TEXT,
"rolled_back_at" DATETIME,
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
);
`);

if (!fs.existsSync(migrationsDir)) {
console.log('[init-db] no migrations dir, skip');
process.exit(0);
}

const names = fs.readdirSync(migrationsDir)
.filter((n) => fs.statSync(path.join(migrationsDir, n)).isDirectory())
.sort();

const applied = new Set(
db.prepare(`SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL`).all().map((r) => r.migration_name)
);

let count = 0;
for (const name of names) {
if (applied.has(name)) continue;
const sqlPath = path.join(migrationsDir, name, 'migration.sql');
if (!fs.existsSync(sqlPath)) continue;
const sql = fs.readFileSync(sqlPath, 'utf8');
const checksum = require('node:crypto').createHash('sha256').update(sql).digest('hex');
const tx = db.transaction(() => {
db.prepare(`PRAGMA defer_foreign_keys = ON`).run();
db.exec(sql);
db.prepare(
`INSERT INTO "_prisma_migrations" (id, checksum, migration_name, finished_at, applied_steps_count)
VALUES (?, ?, ?, current_timestamp, 1)`
).run(require('node:crypto').randomUUID(), checksum, name);
});
tx();
count++;
console.log('[init-db] applied', name);
}

console.log(`[init-db] done, applied ${count} new migration(s)`);
process.exit(0);
Loading