files 文件系统
files 文件系统
files 提供的是 JSXHook 脚本运行环境里的文件系统能力。它的整体风格很像 Auto.js,但有几个很容易踩坑的地方,必须先讲透:
- 相对路径不是相对“当前这个
.js文件所在目录”,而是相对files.cwd() - 空字符串路径不是报错,而是会解析成当前工作目录
- 很多 API 是否把路径当成“目录”,要看你有没有在末尾写
/或\ - 读文件失败时,很多不是返回空,而是直接抛异常
copy()/move()/removeDir()有些行为比名字看起来更“硬”
如果你打算把文档写给第一次接触 JSXHook 的人,这页其实是最该讲细的之一,因为文件 API 一旦理解偏了,后面所有配置、缓存、导出、备份、清理都会跟着错。
先记住这 12 条
- 相对路径一律基于
files.cwd()解析,不是基于当前脚本文件自身位置。 files.path("")、files.path(" ")、很多 API 里省略路径参数,最终都可能落到当前工作目录。files.create("./cache")默认会把它当成“文件路径”;要创建目录,应该写成files.create("./cache/")。files.create()不会自动补父目录;files.createWithDirs()会。files.ensureDir("./cache")在cache还不存在时,并不会创建cache这个目录;它只会确保cache的父目录存在。要创建cache目录,应写files.ensureDir("./cache/")。files.read()/files.readBytes()读不到文件时会抛异常,不是默默返回空字符串或空数组。files.read(path, encoding?)/write(path, text, encoding?)/append(path, text, encoding?)的编码参数默认是utf-8,但如果你写了非法编码名,会直接抛异常。files.writeBytes(path, bytes)如果第二个参数是字符串,会按 UTF-8 转字节再写入。files.appendBytes(path, "abc", "utf-8")是按文本编码追加,不是把"abc"当成数字字节数组。files.rename(path, newName)只会在原目录内改名;就算你传../other.txt,也只会取最后的文件名other.txt。files.move(src, dst)如果目标已存在,会先尝试把目标删掉;所以它不是“保守移动”。files.removeDir("")这种写法非常危险,因为空路径会落到files.cwd(),而removeDir()是递归删除。
这一节建议先吃透,再去看后面的单个 API。
`files.cwd()` 到底是什么
源码里,当前工作目录会根据脚本运行场景切换:
| 运行场景 | files.cwd() 的含义 |
|---|---|
| 项目脚本 | 当前项目目录 |
| App Script | 当前包名对应的脚本目录 |
[GLOBAL] 脚本 | 工作区根目录 |
| 其他兜底情况 | 也是工作区根目录 |
也就是说,这句:
files.read("./config.json");
真正的意思更接近:
files.read(files.join(files.cwd(), "config.json"));
相对路径、绝对路径、空路径的真实规则
所有路径最终都会经过规范化处理,也就是:
- 会去掉多余的
.、.. - 会统一成规范化后的绝对路径字符串
规则表如下:
| 输入 | 结果 |
|---|---|
"" 或只含空白 | 返回当前 files.cwd() |
| 绝对路径 | 直接使用这个绝对路径,再做 normalize |
| 相对路径 | 拼到 files.cwd() 后面,再做 normalize |
最常见的调试写法
log(`cwd = ${files.cwd()}`);
log(`main = ${files.path("./main.js")}`);
log(`parent = ${files.path("../shared/config.json")}`);
很多“为什么读不到文件”的问题,打出这三行基本就能定位。
`files.path(path)` 的几个典型例子
| 调用 | 结果说明 |
|---|---|
files.path("") | 当前工作目录的绝对路径 |
files.path("./a/../b.txt") | 规范化后的 b.txt 绝对路径 |
files.path("/sdcard/demo.txt") | 仍然是这个绝对路径,只是做规范化 |
`files.join(parent, child)` 的几个典型例子
| 调用 | 结果说明 |
|---|---|
files.join("", "a.txt") | 把 files.cwd() 当成 parent |
files.join("./logs", "") | 返回规范化后的 logs 路径 |
files.join("./logs", "../cache/out.json") | 返回规范化后的 cache/out.json |
files.join("./logs", "/sdcard/out.txt") | 因为 child 是绝对路径,所以最终以 child 为准 |
一定要注意空路径的副作用
空路径不会报错,而是会落到当前工作目录。这对读路径函数来说很方便,但对修改函数来说可能很危险。
比如:
files.path(""); // 当前 cwd
files.exists(""); // 判断 cwd 是否存在,通常是 true
files.remove(""); // 尝试删除 cwd;非空目录通常删不掉
files.removeDir(""); // 递归删除 cwd,危险
所以只要是会改文件系统的 API,最好都别让路径来源“可能为空”。
这个规则对 create()、createWithDirs()、ensureDir()、copy()、move() 都很重要。
源码里判断“看起来像目录”的标准
只有一个非常直接的规则:
- 原始路径字符串去掉首尾空白后,如果以
/或\结尾,就认为它“看起来像目录”
也就是说:
| 路径字符串 | 会不会被当成目录风格 |
|---|---|
"./cache" | 不会 |
"./cache/" | 会 |
"./cache\\\\" | 会 |
" ./cache/ " | 会,前后空白会先被 trim 掉 |
这会造成什么区别
files.create("./cache"); // 创建的是名为 cache 的文件
files.create("./cache/"); // 创建的是名为 cache 的目录
同样地:
files.createWithDirs("./data/out.json"); // 自动补父目录,再创建文件
files.createWithDirs("./data/"); // 创建目录
`ensureDir()` 的规则更容易误解
ensureDir() 的逻辑是:
- 如果你传入的路径“看起来像目录”,或者目标本身已经是目录,就确保这个目录存在
- 否则,就确保“它的父目录”存在
所以:
files.ensureDir("./cache"); // 确保的是当前目录存在,不会新建 cache 目录
files.ensureDir("./cache/"); // 才会确保 cache 目录存在
这个点非常重要,很多人第一次用都会误以为 ensureDir("./cache") 能创建 cache 文件夹,实际上不是。
files.isFile(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 相对路径基于 files.cwd() 解析 |
返回值
boolean真实行为
路径存在且是普通文件时返回
true不存在、是目录、或解析后不是普通文件时都返回
false示例
log(files.isFile("./main.js"));
files.isDir(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 相对路径基于 files.cwd() 解析 |
返回值
boolean真实行为
路径存在且是目录时返回
true示例
log(files.isDir("./hooks"));
files.isEmptyDir(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 相对路径基于 files.cwd() 解析 |
返回值
boolean真实行为
只有同时满足下面 3 个条件才返回 true:
- 路径存在
- 它是目录
- 目录里没有任何子项
其余情况一律是 false,包括:
路径不存在
它是文件
它是非空目录
示例
log(files.isEmptyDir("./tmp/"));
files.join(parent, child)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
parent | string | 路径字符串 | 为空时会退回 files.cwd() |
child | string | 路径字符串 | 为空时会返回规范化后的 parent |
返回值
string真实行为
| 情况 | 结果 |
|---|---|
parent 为空 | 用 files.cwd() 作为基准 |
child 为空 | 返回规范化后的 parent |
child 是绝对路径 | 以 child 为准 |
含 . / .. | 最终都会被规范化 |
示例
const full = files.join(files.cwd(), "logs/runtime.txt");
log(full);
log(files.join("./logs", "../cache/result.json"));
files.create(path)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
path | string | 文件路径或目录风格路径 | 目录风格要以 / 或 \ 结尾 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
| 目标已存在 | false |
路径以 / 或 \ 结尾 | 按目录创建,调用 mkdirs() |
| 路径看起来是文件 | 只会创建文件本身,不会补父目录 |
| 文件路径的父目录不存在 | false |
一个必须记住的例子
files.create("./cache"); // 文件
files.create("./cache/"); // 目录
示例
files.create("./empty-dir/");
files.create("./cache.txt");
常见误区
下面这种写法通常会失败,因为 logs/2026 不存在,而 create() 不会帮你补:
files.create("./logs/2026/runtime.txt");
应该改成:
files.createWithDirs("./logs/2026/runtime.txt");
files.createWithDirs(path)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
path | string | 文件路径或目录风格路径 | 目录风格要以 / 或 \ 结尾 |
返回值
boolean真实行为
如果目标已存在,返回
false如果看起来是目录路径,直接
mkdirs()如果看起来是文件路径,会先
parentFile?.mkdirs(),再创建文件示例
files.createWithDirs("./logs/2026/boot.txt");
files.createWithDirs("./cache/image/");
和 `create()` 的本质区别
| API | 会不会自动补父目录 |
|---|---|
files.create() | 不会 |
files.createWithDirs() | 会 |
files.exists(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 路径字符串 |
返回值
boolean真实行为
只判断“是否存在”
不区分它是文件还是目录
示例
if (!files.exists("./config.json")) {
log("missing config");
}
files.ensureDir(path)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
path | string | 文件路径或目录风格路径 | 是否写末尾 / 很关键 |
返回值
boolean真实行为
| 传入方式 | 实际确保存在的是谁 |
|---|---|
files.ensureDir("./logs/") | logs 目录本身 |
files.ensureDir("./logs/runtime.txt") | logs 目录 |
files.ensureDir("./cache") 且 cache 不存在 | 当前目录,也就是 cache 的父目录,不会新建 cache |
files.ensureDir("./cache") 且 cache 已经是目录 | cache 目录本身 |
结论
如果你的目标就是“创建某个目录本身”,最稳的写法永远是带末尾斜杠:
files.ensureDir("./cache/");
示例
files.ensureDir("./logs/");
files.ensureDir("./logs/runtime.txt");
编码参数的真实规则
这 3 个 API 都使用同一套编码处理:
files.read(path, encoding?)files.write(path, text, encoding?)files.append(path, text, encoding?)
规则如下:
| 情况 | 结果 |
|---|---|
不传 encoding | 默认 utf-8 |
encoding 是合法字符集名 | 按该字符集处理 |
encoding 是非法字符集名 | 直接抛异常,不会自动回退到 UTF-8 |
所以这些都通常可用:
files.read("./a.txt", "utf-8");
files.read("./a.txt", "utf-16");
files.read("./a.txt", "gbk");
但如果你写了一个系统不认识的编码名,例如:
files.read("./a.txt", "not-a-charset");
那不是回退默认值,而是会抛异常。
files.read(path, encoding?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 文件路径 | 无 | 必须存在且必须是普通文件 |
encoding | string | 任意 Java Charset.forName() 可识别的编码名 | utf-8 | 非法编码名会抛异常 |
返回值
string失败行为
| 情况 | 结果 |
|---|---|
| 文件不存在 | 抛 FileNotFoundException |
| 路径存在但不是普通文件 | 抛 FileNotFoundException |
| 编码名非法 | 抛字符集相关异常 |
示例
const text = files.read("./config.json", "utf-8");
log(text);
更稳的写法
if (files.isFile("./config.json")) {
const text = files.read("./config.json");
log(text);
}
files.readBytes(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 必须存在且必须是普通文件 |
返回值
ByteArray失败行为
| 情况 | 结果 |
|---|---|
| 文件不存在 | 抛 FileNotFoundException |
| 路径存在但不是普通文件 | 抛 FileNotFoundException |
示例
const bytes = files.readBytes("./icon.png");
log(bytes.length);
files.write(path, text, encoding?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 文件路径 | 无 | 覆盖写入 |
text | string | 任意文本 | "" | 第二参数省略时会写空字符串 |
encoding | string | 任意合法字符集名 | utf-8 | 非法编码名会抛异常 |
返回值
boolean真实行为
覆盖写入,原内容会被整个替换
父目录不存在时会尝试自动
mkdirs()文件不存在时会新建
示例
files.write("./logs/runtime.txt", "ready\n", "utf-8");
常见用法
files.write("./data/user.json", JSON.stringify({
name: "demo",
enabled: true
}, null, 2));
这一组的细节很多,尤其是第二个参数到底会怎么转字节。
files.writeBytes(path, bytes)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 文件路径 | 无 | 覆盖写入 |
bytes | ByteArray | number[] | string | Collection | Array | 多种 | 空字节数组 | 不支持的类型会退成空字节数组 |
返回值
boolean第二参数的真实转换规则
| 传入值 | 最终写入内容 |
|---|---|
ByteArray | 原样写入 |
"hello" | 按 UTF-8 转成字节后写入 |
[1, 2, 3] | 每个元素转成 1 个字节 |
["65", "66"] | 字符串先转整数,再转字节 |
["abc"] | 无法转整数,写成 0 |
{} / 不支持的对象 | 变成空字节数组 |
null / 省略 | 变成空字节数组 |
数字数组还有一个进阶细节
数组元素最终走的是 int.toByte(),也就是只保留低 8 位。
这意味着:
| 输入数字 | 最终字节效果 |
|---|---|
255 | 0xFF |
256 | 0x00 |
300 | 0x2C |
-1 | 0xFF |
所以如果你本来就处理的是原始二进制,最好自己保证元素范围在 0..255。
示例 1:写二进制数组
files.writeBytes("./out.bin", [1, 2, 3, 4]);
示例 2:直接把字符串写成 UTF-8 字节
files.writeBytes("./hello.txt", "hello");
示例 3:意外写出空文件的情况
files.writeBytes("./empty.bin", {});
这句通常不会报错,而是创建或覆盖成一个 0 字节文件。
files.append(path, text, encoding?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 文件路径 | 无 | 追加写入 |
text | string | 任意文本 | "" | 第二参数省略时会追加空字符串 |
encoding | string | 任意合法字符集名 | utf-8 | 非法编码名会抛异常 |
返回值
boolean真实行为
追加写入
父目录不存在时会自动创建
文件不存在时会先创建文件再追加
示例
files.append("./logs/runtime.txt", `${Date.now()} boot\n`);
典型日志写法
function appendLog(message) {
files.append("./logs/run.log", `[${Date.now()}] ${message}\n`);
}
files.appendBytes(path, data, encoding?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 文件路径 | 无 | 追加写入 |
data | ByteArray | number[] | string | Collection | Array | 多种 | 空字节数组或空文本 | string 和“数组/字节”是两条不同分支 |
encoding | string | 合法字符集名 | utf-8 | 只在 data 是字符串时按文本编码使用 |
返回值
boolean这几个分支一定要分清
data 类型 | 实际行为 |
|---|---|
String | 走文本追加,按 encoding 编码 |
ByteArray / number[] / Collection / Array | 走字节追加 |
| 不支持的对象 | 退成空字节数组追加 |
这也是为什么下面两句完全不是一回事
files.appendBytes("./demo.bin", "1,2,3");
files.appendBytes("./demo.bin", [1, 2, 3]);
第一句追加的是文本 "1,2,3" 的编码结果。
第二句追加的才是 3 个原始字节。
示例 1:追加原始字节
files.appendBytes("./logs/raw.bin", [0, 1, 2]);
示例 2:按文本追加
files.appendBytes("./logs/raw.txt", "tail\n", "utf-8");
这一组的重点不是“能不能用”,而是“目标路径到底会被解释成什么”。
`copy()` / `move()` 的目标路径判定表
源码会先根据“源是文件还是目录”来决定目标最终长什么样。
| 源类型 | 目标情况 | 最终目标 |
|---|---|---|
| 文件 | 目标已存在且是目录 | 目标目录/源文件名 |
| 文件 | 目标字符串看起来像目录(以 / 或 \ 结尾) | 目标目录/源文件名 |
| 文件 | 其他情况 | 目标本身就是最终文件路径 |
| 目录 | 目标已存在且是目录 | 目标目录/源目录名 |
| 目录 | 目标字符串看起来像目录 | 目标本身就是最终目录路径 |
| 目录 | 其他情况 | 目标本身也会被当成目录路径使用 |
两个非常重要的例子
files.copy("./a.txt", "./backup/");
最终目标是:
./backup/a.txt
而这句:
files.copy("./assets", "./backup.txt");
因为源是目录,所以最终会创建一个名叫 backup.txt 的目录,而不是一个文件。
files.copy(src, dst)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
src | string | 源路径,文件和目录都支持 |
dst | string | 目标路径,最终解释规则见上表 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
| 源不存在 | false |
| 源是文件 | 复制文件,必要时自动补目标父目录 |
| 源是目录 | 递归复制整个目录树 |
| 源目录和目标目录是同一路径 | true,不重复复制 |
一个很重要的覆盖细节
当复制目录时,如果最终目标目录已经存在,源码会先对这个目标做 deleteRecursively(),删除成功后再重新复制。
也就是说,目录复制更接近“替换式拷贝”,不是“把源目录内容并入目标目录”。
示例
files.copy("./config.json", "./backup/config.json");
files.copy("./assets/", "./backup/assets/");
files.move(src, dst)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
src | string | 源路径 |
dst | string | 目标路径,最终解释规则与 copy() 相同 |
返回值
boolean真实行为
源不存在时,返回
false先计算最终目标路径
如果源和目标绝对路径完全相同,直接返回
true如果目标已存在,先尝试递归删除目标
优先尝试
renameTo()如果直接重命名失败,再退化成“复制 + 删除源”
这意味着什么
move()会覆盖已有目标,而且覆盖前会先删除目标它不是“如果目标已存在就自动失败”的保守行为
示例
files.move("./tmp/result.json", "./archive/result.json");
files.move("./tmp/images/", "./archive/images/");
什么情况下别用 `rename()`
只要你的目标已经换目录了,就应该用 move():
files.move("./a/test.txt", "./b/test.txt");
files.rename(path, newName)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
path | string | 原路径 | 源必须存在 |
newName | string | 新名字 | 最终只保留纯文件名部分 |
返回值
booleannewName会怎样被处理
源码会做:
File(newName).name
所以:
传入的 newName | 最终实际使用 |
|---|---|
"new.txt" | "new.txt" |
"../new.txt" | "new.txt" |
"a/b/c.txt" | "c.txt" |
真实行为
只在原目录内改名
不会跨目录移动
如果目标名字已存在,返回
false示例
files.rename("./logs/today.txt", "yesterday.txt");
files.rename("./logs/today.txt", "../yesterday.txt"); // 实际还是改成同目录下的 yesterday.txt
files.renameWithoutExtension(path, newName)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 原路径 |
newName | string | 新基础名,只保留纯文件名部分 |
返回值
boolean真实行为
改名时尽量保留原扩展名
这里的“扩展名”只认最后一个点后面的部分
例子一定要看
| 原文件名 | newName | 结果 |
|---|---|---|
runtime.txt | runtime-old | runtime-old.txt |
archive.tar.gz | backup | backup.gz |
.gitignore | backup | backup |
.gitignore 这种前导点文件,源码认为它“没有扩展名”。
示例
files.renameWithoutExtension("./logs/runtime.txt", "runtime-old");
files.getName(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 文件或目录路径都可以 |
返回值
string真实行为
直接返回最后一段名称
包含扩展名
示例
log(files.getName("./logs/runtime.txt"));
// runtime.txt
files.getNameWithoutExtension(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 文件或目录路径都可以 |
返回值
string真实行为
只去掉最后一个点后的扩展名:
| 输入名 | 返回值 |
|---|---|
runtime.txt | runtime |
archive.tar.gz | archive.tar |
.gitignore | .gitignore |
noext | noext |
示例
log(files.getNameWithoutExtension("./logs/runtime.txt"));
files.getExtension(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 文件或目录路径都可以 |
返回值
string真实行为
只返回最后一个点后面的扩展名
不包含点号
.
| 输入名 | 返回值 |
|---|---|
runtime.txt | txt |
archive.tar.gz | gz |
.gitignore | "" |
noext | "" |
示例
log(files.getExtension("./logs/runtime.txt"));
// txt
files.remove(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 文件或目录路径 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
| 路径不存在 | false |
| 是普通文件 | 尝试删除该文件 |
| 是空目录 | 可能删除成功 |
| 是非空目录 | 因为不是递归删除,通常删不掉 |
示例
files.remove("./logs/runtime.txt");
files.remove("./empty-dir/");
正确理解它的名字
remove() 更像“普通 delete”,不是“递归清空”。
files.removeDir(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 目标路径 |
返回值
boolean真实行为
路径不存在时返回
false底层调用的是
deleteRecursively()目录树会被整棵删掉
虽然名字叫
removeDir(),但如果你传的是文件路径,底层通常也会把这个文件删掉最高优先级警告
files.removeDir("");
因为空路径会解析成 files.cwd(),这句有可能把当前工作目录整棵递归删掉。
所以任何“清理目录”脚本里,都别让路径参数可能为空。
示例
files.removeDir("./tmp/");
files.getSdcardPath()
参数
无
返回值
string真实行为
优先取系统外部存储根目录
拿不到时回退到
"/sdcard"示例
log(files.getSdcardPath());
files.cwd()
参数
无
返回值
string真实意义
当前脚本工作目录
所有相对路径最终都以它为基准
示例
log(files.cwd());
很推荐的用法
const output = files.join(files.cwd(), "output/result.json");
files.write(output, "{}");
files.path(path)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 相对路径、绝对路径、空字符串都支持 |
返回值
string真实行为
| 输入 | 结果 |
|---|---|
| 相对路径 | 解析成规范化后的绝对路径 |
| 绝对路径 | 规范化后原样返回 |
| 空字符串 | 返回当前 files.cwd() 的绝对路径 |
示例
log(files.path("./main.js"));
log(files.path("../shared/config.json"));
files.listDir(path, filter?)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
path | string | 目录路径 | 必须存在且必须是目录 |
filter | (name: string) => boolean | 可选回调 | 回调只收到文件名,不收到完整路径 |
返回值
string[]真实行为
| 规则 | 说明 |
|---|---|
| 非目录 | 直接抛 IllegalArgumentException("Not a directory: ...") |
| 返回内容 | 只是名称列表,不是绝对路径列表 |
| 排序 | 按不区分大小写排序 |
| 过滤回调参数 | 只有 name |
| 回调返回值 | 会按 Rhino 的 truthy / falsy 规则转布尔,不强制必须返回真正的 true/false |
示例 1:列出全部
const names = files.listDir("./");
log(JSON.stringify(names));
示例 2:只筛 `.js`
const jsFiles = files.listDir("./", name => name.endsWith(".js"));
log(JSON.stringify(jsFiles));
示例 3:再补成完整路径
const fullPaths = files
.listDir("./images", name => name.endsWith(".png"))
.map(name => files.join("./images", name));
log(JSON.stringify(fullPaths, null, 2));
1. 读配置,不存在就生成默认文件
const configPath = "./config/config.json";
if (!files.exists(configPath)) {
files.write(configPath, JSON.stringify({
debug: true,
retry: 3
}, null, 2));
}
const config = JSON.parse(files.read(configPath));
log(JSON.stringify(config, null, 2));
2. 生成日志目录并持续追加
files.ensureDir("./logs/");
function appendLog(message) {
files.append("./logs/runtime.log", `[${Date.now()}] ${message}\n`);
}
appendLog("script start");
appendLog("hook ready");
3. 备份目录再清缓存
if (files.exists("./cache")) {
files.copy("./cache", "./backup/cache");
files.removeDir("./cache");
}
4. 正确创建目录,而不是误建同名文件
files.createWithDirs("./exports/");
files.createWithDirs("./exports/result.json");
5. 处理 `listDir()` 只返回名称的问题
const jsonFiles = files
.listDir("./data", name => name.endsWith(".json"))
.map(name => ({
name,
path: files.join("./data", name)
}));
log(JSON.stringify(jsonFiles, null, 2));
最后压成一句话
files最核心的不是“会不会增删改查”,而是“路径到底按什么规则解释”。- 你只要把
cwd()、空路径、末尾斜杠、编码参数、目标路径覆盖规则这几件事搞明白,后面大部分文件脚本都会顺很多。 - 真要记一个最危险的坑,那就是:空路径不是空操作,
removeDir("")这种写法可能真的删东西。
