它是怎么搭起来的

一个不依赖任何第三方库的原生安卓应用:程序化 UI、SSE 流式生成、分层记忆压缩、 结构化人物档案、可核对的费用账本,以及一层把 15 家模型接进来的适配层。 这一页把关键取舍和实现讲清楚。

0第三方依赖
0KB 安装包
0家内置提供方
0% 数据存本机

技术选型与取舍

做减法:能不引依赖就不引,能不联网的服务端就不搭

☕

纯原生 Java,零依赖

只用 Android SDK 自带 API:HttpURLConnection 发请求、org.json 解析、 自绘 Canvas 画图表。没有 Gradle 之外的构建插件,APK 才 638KB 左右。

🧱

全程序化 UI,无 XML 布局

界面在 Java 里搭(LinearLayout + GradientDrawable),样式统一走一套主题调色板, 深浅色切换只需重新装载色板 + recreate()。好处是换肤、动效、圆角都能在一处统一改。

🗂️

一本书 = 一个 JSON 文件

没有数据库:filesDir/novels/novel_<时间戳>.json,写入用 临时文件 + renameTo 原子替换,进程被杀也不会写出半个坏文件。

🌊

SSE 流式,边写边看

请求带 stream:true,逐行解析 data: 帧,正文增量往界面上刷; UI 侧做节流,避免每个 token 都触发布局。

🧠

提示词与业务分层

PromptBuilder 负责"读什么、按什么顺序读",NovelStore 负责存档, DeepSeek 只管收发与方言,互不越界。

📦

没有服务端

官网是纯静态页面(Netlify),App 直接带着你自己的 Key 访问模型厂商。 没有中转服务器,也就没有"你的稿子存在别人机器上"这件事。

整体架构

四层:界面只管显示,业务层管"该给 AI 看什么"和"钱花在哪",网络层管协议,存储层管落盘

界面层
Activity + 自绘 View

书架、创作设置、生成页、重写、续写、记忆档案、模型管理、花费页、对话页;图表与开关都是自绘。

BlueHomeActivity 书架 GenerateActivity 生成/查看 CostActivity / CostDetailActivity 花费 ModelListActivity / ProviderEditActivity 模型 BlueTheme 主题与动效
业务层
记忆组装 / 存档 / 记账 / 模型配置

决定每章"读哪些前文、带哪些档案",维护章节与版本,记录每一笔花费,解析当前该用哪家模型。

PromptBuilder 上下文与提示词 NovelStore 章节/档案/导出 CostBook 费用账本 Providers / ProviderPresets 多提供方 UpdateChecker 版本检查
网络层
DeepSeek.java:一个文件装下所有厂商差异

SSE 解析、停止令牌、错误分类、思考参数"方言"、用量与思考 token 解析、余额与模型列表查询。

streamChat / streamChatMessages updateCharacters(结构化 JSON) costPartsPriced 计价 queryBalance / fetchModels
存储层
文件 + SharedPreferences

书稿、人物档案、章节版本、费用账本都在书自己的 JSON 里;设置与对话历史在 prefs。

novels/*.json 原子写 *.记忆.txt 可读档案 chat-costs.json 对话账本 prefs: providers / 主题 / 字号 / 思考开关

数据与存储

全部落在应用私有目录,卸载即消失;导出即是一份完整快照

# 应用私有目录(无需任何存储权限)
files/
├─ novels/
│   ├─ novel_1730000000000.json   # 一本书的全部内容(章节 / 档案 / 版本 / token / 费用账本)
│   ├─ novel_1730000000000.记忆.txt # 同内容的可读版:人物档案 + 每章摘要
│   └─ ...
└─ chat-costs.json                # 对话页的花费(不属于任何一本书)

# SharedPreferences "settings"
providers        # 服务方列表(含密钥、模型目录、思考写法、可选计价)
write_provider / write_model / char_provider / char_model
think_write / think_char / think_effort / cache_opt
theme / font_size / tts_* / export_dir_uri ...

一本书的 JSON 里有什么

字段用途
title / characters / background / globalPrompt创作设定(AI 每次都会读到)
memoryMode / memoryChapters记忆模式(传统 / modern)与"最近几章给原文"
chapters[]每章:正文、摘要、人物记录、token 与花费、生成时间、versions[] 历史版本
charactersJson[]结构化人物档案:姓名 / 年龄 / 性别 / 身份 / 性格 / 当前状态 / 其他
memoryText我的笔记 + 章节摘要区(用户可编辑,AI 只读)
costs[]费用账本:时间 / 用途 / 金额 / 输入命中·未命中·输出·思考 token / 三段费用 / 模型
原子写:先写 xxx.json.tmp,再 renameTo 覆盖正式文件。 写一半被杀进程只会留下一个临时文件,正式稿永远完整。

一次章节生成的全过程

从"点生成"到"章节落盘",中间发生的事

组装上下文

PromptBuilder 按这本书的记忆模式拼出 system + user:传统模式=人物档案 → 全部摘要 + 最近 N 章原文;modern 记忆引擎=本地检索出的事实卡片 + 未闭合线索 + 上一章全文 → 本章要求 → 最后贴"写作纪律"(放在最末尾,靠近因效应压住跑题)。

检索记忆(仅 modern)

本地多路检索:本章要求 / 每章通用要求 / 未闭合线索 / 上一章结尾各算一路,取每张卡的最高分, 再按图谱 2 跳扩展、按重要度与时间衰减加权。未闭合线索与用户点名的物品强制带上, 全程不额外调用模型。传统模式没有这一步。

(可选)缓存优化

开启后把请求改成"接着上一轮继续"的多轮形式:[system] + (user 要求, assistant 正文)×N + [本章要求], 并把每章实际发出的 user 消息原样存档,下次请求前缀与上一轮完全一致 → 整段命中缓存(命中价约为未命中的 1/50)。

SSE 流式接收

逐行读 data: 帧:choices[0].delta.content 是正文, reasoning_content 是思考内容(单独进"思考"面板);末尾的 usage 块给出真实用量。

抽取记忆 + 更新档案(仅 modern)

写完把本章正文交给一次非流式请求:同一个 JSON 里同时返回人物档案、物品档案与 本章新事实(记忆卡片),本地按"槽位"合并落库 —— 同一件事发生变化就把旧卡作废、新卡记下取代关系。 这一步**搭在原有请求里,不多花一次调用**。

标记剥离与存档

模型会在正文后追加 【本章摘要】(传统模式还会追加 【人物记录】)。 显示层一旦遇到标记就停止输出,标记后的内容只进存档——用户看到的正文始终是干净的。

记账

按 usage 的三段 token 计价(没有就按字数估算),写入这本书的 costs[];同时刷新查看页顶部的余额条。

两种记忆模式

作者写长篇最大的敌人是"注意力涣散":越写越长,模型越容易忘设定、重复情节

传统模式modern 模式(记忆引擎)
给什么 全部已批准章节的摘要 + 最近 N 章(1~50,可自定义)的完整原文 检索出来的事实卡片(约 1500 字)+ 未闭合线索 + 上一章全文(保持文风与衔接)
额外机制— 多路检索 + 图谱扩展;写完把本章新事实抽成卡片存回库;每 10 章把过旧的低重要度卡片折叠成「卷摘要」
上下文随篇幅线性增长(摘要越堆越多) 基本恒定:写 20 章和写 200 章,每次要看的差不多
适合短篇;希望模型贴着前文文风细节续写长篇;希望越写越不容易前后矛盾、且花费不随篇幅膨胀

① 检索记忆

每章开写前,本地从记忆库里挑出与本章相关的事实卡片(多路查询取最高分,图谱扩展两跳), 不额外调用模型、不额外花钱。

② 生成正文

模型只依据卡片写作:卡片里没有的细节可以发挥,但不得与卡片冲突;矛盾项会被标成「⚠待确认」, 按保守方式处理。

③ 抽取记忆

写完把本章新事实抽成卡片存回库:谁做了什么、状态/关系变化、承诺与伏笔。旧卡作废但留痕, 重写旧章节时只读"章号早于本章"的卡片。

模式随书锁定:新建时选定,之后不可更改——半个故事用一种读法、半个故事用另一种,连贯性反而更差。

人物档案 / 物品档案

写一百章也不会"谁受了伤、谁藏了秘密、东西在谁手上"记乱

🧩

人物:结构化 + 增量更新

档案是 JSON 数组而不是一段文本。每章写完单独发一次请求,只让模型输出新出场或信息有变化的角色, 再按字段合并进现有档案:同名覆盖非空字段、新名追加、永不删除角色。输出量小,角色再多也不会漏。

🕰️

人物的版次快照

每章会存下"写这一章之前"的档案快照(charsBefore)。以后重写第 N 章时,模型读到的是 "没有写过这一章"的档案,避免它认为那些剧情已经发生过——重写不再自相矛盾。

💠

物品:只记你点名的那些

物品档案比人物更简单,只有"状态"一个字段,而且只记你在「要记的物品」里写下的类别: 填「灵魂碎片」,它就记下正文里出现过的「黑色碎片」「蓝色碎片」及其状态,没点名的类别一律不记。

🔁

两者同一次请求一起更新

人物与物品在同一次档案更新请求里返回并合并,不多花一次调用;物品同样会进记忆档案和每次生成的上下文, 所以"东西在谁手上、还剩几枚"这类信息不会漂。

费用账本:每一分钱都算得清

"花了多少"不该是个黑盒 —— 按用途、按计费项两个口径分开看

三段计价

计费项含义DeepSeek 空闲价(元/百万 tokens)
输入 · 缓存命中命中缓存的前文,约为未命中的 1/50Flash 0.02 / Pro 0.15
输入 · 未命中这次新读进去的前文与要求Flash 1 / Pro 4.5
输出(含思考)正文与思考过程,按输出价计费Flash 4 / Pro 13.5
思考 token 不单独计价:服务端的 completion_tokens 里已经含了 reasoning token, 所以"思考"是从输出费用里按 token 占比切出来的——两个数加起来正好等于输出总费用,不会重复计。 服务端没返回 reasoning_tokens 时,按流式收到的思考正文估算(取不到就如实标注)。

账本怎么记

🧾

记在书里,跟着书走

每一笔都写进所属书籍 JSON 的 costs[]:时间、用途(正文 / 重写 / 人物档案 / 摘要 / 定位)、 金额、四类 token、三段费用、模型名。所以导出原文件、整包备份都把花费带走,换设备后账目仍在。

🧮

老书也能看到历史

升级前的老书写的是"每章一共花了多少"。第一次记账时会把这些按章记录合成为"正文生成 / 未分项", 一两百章也不会算两遍;记录超过 900 条会按用途压缩,文件不会无限膨胀。

多提供方适配层

写小说用什么模型由你定;各家接口的"方言差异"由这一层吃掉

方言思考参数写法典型厂商
deepseekthinking:{type} + reasoning_effortDeepSeek 官方
glm只发 thinking:{type}智谱 GLM、火山方舟(豆包)
qwen顶层 enable_thinking阿里云百炼、Kimi、硅基流动
minimaxthinking:{type:"adaptive"}MiniMax
standard只发 reasoning_effortOpenAI、Gemini、xAI、混元
openrouterreasoning:{effort}OpenRouter
none什么都不发千帆、星火、本地 Ollama
自动判断 + 失败回退:先按模型名、再按域名匹配方言;如果网关因为不认这些参数返回 400, 程序会去掉思考参数原样重试一次——所以配错方言最多是"没有思考",不会写不成。

15 家内置提供方

基元律动 TokenRhythm、OpenRouter、智谱 GLM、阿里云百炼、Kimi、MiniMax、硅基流动、火山方舟、 腾讯混元、百度千帆、讯飞星火、OpenAI、Gemini、xAI Grok、本地 Ollama。

预置模型 + 实时拉取

每家都带几个代表性模型,点一下就把地址、模型、思考写法填好;编辑页里还能 "从接口获取模型列表"实时拉取你账号下的全部模型。

单价可填,花费可算

第三方没有统一价目表,所以在提供方(或单个模型)上留了"计费价格"三项: 输入命中 / 未命中 / 输出。填了就能算钱,没填就如实显示"只记 token 与次数"。

更新与发布

一个静态 version.json 撑起整条更新链

官网放一份 version.json

内容只有版本号、更新说明、APK 地址。App 启动时拉一次,用 versionCode 与本机比对 (同一新版本只提醒一次)。

应用内下载安装(零依赖)

自己实现了一个 FileProvider 暴露私有目录里的 APK,下载完直接拉起系统安装器; 下载过程有环形进度动画。

地址可改

更新检查地址写在设置里,换域名不用重新打包——发版只改 version.json 一个文件。

踩过的坑(挑三个说)

都是真机上暴露出来的,记在这里免得再犯

🕳️

非流式请求没有用量

人物档案走的是非流式接口,代码里从来没人解析过它的 usage —— 于是"人物档案"这一项的花费一直是 0。 修完才看得到钱花在哪。

🕳️

思考 token 不能重复计价

一开始想做成"输入 / 思考 / 输出"三段独立计费,等于把思考算了两遍。正确做法是: 思考是输出的一部分,只是从输出费用里按占比切出来展示。

🕳️

选中一片,其他片"消失"了

高亮选中的扇形时把其余片压到了 36% 不透明度,深色模式下看着像被删了。 现在改成完全不压暗,靠"弹出 + 变粗 + 柔光"表达选中。

隐私与数据归属

没有服务器、没有账号。API Key 只存在本机 SharedPreferences,请求直接从手机发往你选定的模型厂商; 书稿、人物档案、费用记录全部在应用私有目录,卸载即清除。 唯一的联网行为是:调用你配置的模型接口、检查官网上的版本号。
↑