Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
node: true,
es2022: true,
},
extends: ["eslint:recommended", "prettier"],
parserOptions: {
ecmaVersion: "latest",
},
ignorePatterns: ["node_modules/"],
overrides: [
{
files: ["web/**/*.js"],
env: {
browser: true,
},
},
],
rules: {
"no-console": "off",
},
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,6 @@ flomo

sendedIds.json
memo.json

AGENTS.md
.DS_Store
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@

最好使用 memo@0.10.4 以上版本,因为之前的版本不支持设置创建时间的功能。

# Web 控制台(可视化)

如果你不想手动执行命令行,可以使用内置 Web 控制台完成导入和删除。

## 启动

```bash
pnpm install
pnpm web
```

启动后访问:

```text
http://127.0.0.1:3131
```

## 页面操作步骤

1. 在 `Session` 区域填写:
- `API Host`(例如 `http://localhost:5230/api/v1` 或 `http://localhost:5230/api/v2`)
- `Access Token`
点击 `Save Session`。
2. 选择任务并执行:
- Flomo:优先在页面直接选择 flomo 导出文件夹(包含 `index.html` 和 `file/` 资源目录),然后点击 `执行 Flomo 导入`
- 微信读书:优先在页面直接选择 TXT 文件,然后点击 `执行微信读书导入`
- 如需兼容旧流程,可在“高级(可选)”里填写本地路径(例如 `./flomo/index.html`、`./weixin.txt`)
- 删除已导入数据:点击 `删除已导入 Memo`
3. 在 `Live Logs` 观察实时日志,在 `Status` 查看任务状态。

## 说明

- 当前仅支持单任务串行执行,运行中再次启动任务会返回冲突提示。
- 会话凭据只保存在服务进程内存中,页面刷新或服务重启后需要重新填写。
- 删除逻辑依赖 `sendedIds.json`,默认存放在系统临时目录 `memos-import-artifacts`(可通过 `MEMOS_ARTIFACT_DIR` 覆盖)。

# flomo

## 导出 flomo 数据
Expand Down Expand Up @@ -48,7 +84,7 @@ node ./src/main.js <your-api-host> <your-access-token> ./flomo/index.html

> 删除同步数据仅支持删除脚本创建的内容,创建的 tag 请手动删除,因为无法确认 tag 是否有被其他内容使用。

执行完同步数据后如果不符合预期,可以执行下面的命令删除同步的数据。删除会读取同步完成写入到 `sendedIds.json` 文件数据,所以需要保证这个文件存在
执行完同步数据后如果不符合预期,可以执行下面的命令删除同步的数据。删除会读取同步完成写入的 `sendedIds.json`,默认路径为系统临时目录 `memos-import-artifacts`(可通过 `MEMOS_ARTIFACT_DIR` 覆盖)

```bash
node ./src/delete.js <your-api-host> <your-access-token>
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"description": "",
"main": "index.js",
"scripts": {
"run": "node ./src/weixin.js http://localhost:5230/api/memo?openId=8deb3079-055b-48a1-a1e4-499ce493fa0c ./weixin.txt",
"lint": "eslint ."
"run": "node ./src/weixin.js http://localhost:5230/api/v1 YOUR_ACCESS_TOKEN ./weixin.txt",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint .",
"test": "node --test tests/*.test.js",
"web": "node ./src/server/index.js"
},
"keywords": [],
"author": "",
Expand All @@ -18,12 +20,13 @@
"fs-extra": "^11.1.0",
"mime": "^3.0.0",
"turndown": "^7.1.2",
"url-parse": "^1.5.10"
"url-parse": "^1.5.10",
"busboy": "^1.6.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/eslint-parser": "^7.19.1",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0"
}
}
}
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions src/core/apiClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const fs = require("fs-extra");
const path = require("path");
const mime = require("mime");
const axios = require("axios");
const parse = require("url-parse");

const DEFAULT_SLEEP_MS = 1000;

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function getVersion(openApi) {
if ((openApi || "").includes("/v2")) return "/v2";
return "/v1";
}

function getRequestUrl(openApi, requestPath) {
const { origin } = parse(openApi || "");
return `${origin}${requestPath}`;
}

function extractMemoName(responseData) {
return responseData?.data?.name || responseData?.data?.data?.name;
}

function createApiClient(options) {
const { openApi, accessToken, sleepMs = DEFAULT_SLEEP_MS } = options;

if (!openApi) {
throw new Error("openApi is required");
}
if (!accessToken) {
throw new Error("accessToken is required");
}

const version = getVersion(openApi);
const headers = {
Authorization: `Bearer ${accessToken}`,
};

return {
async uploadFile(filePath) {
const readFile = fs.readFileSync(filePath);
const response = await axios({
method: "post",
url: getRequestUrl(openApi, `/api${version}/resources`),
data: {
content: readFile.toString("base64"),
filename: path.basename(filePath),
type: mime.getType(filePath) || undefined,
},
headers,
});

return response.data;
},

async sendMemo(memo) {
const response = await axios({
method: "post",
url: getRequestUrl(openApi, `/api${version}/memos`),
data: memo,
headers: {
...headers,
"Content-Type": "application/json; charset=UTF-8",
},
});

await sleep(sleepMs);
return response;
},

async updateMemo(memoName, createTime) {
return axios({
method: "patch",
url: getRequestUrl(openApi, `/api${version}/${memoName}`),
data: { createTime },
headers: {
...headers,
"Content-Type": "application/json; charset=UTF-8",
},
});
},

async setMemoResources(memoName, resources) {
return axios({
method: "patch",
url: getRequestUrl(openApi, `/api${version}/${memoName}/resources`),
data: {
resources,
},
headers: {
...headers,
"Content-Type": "application/json; charset=UTF-8",
},
});
},

async deleteMemo(memoName) {
return axios({
method: "delete",
url: getRequestUrl(openApi, `/api${version}/${memoName}`),
headers: {
...headers,
"Content-Type": "application/json; charset=UTF-8",
},
});
},

extractMemoName,
};
}

module.exports = {
createApiClient,
extractMemoName,
};
18 changes: 18 additions & 0 deletions src/core/artifactStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const os = require("os");
const path = require("path");
const fs = require("fs-extra");

const ARTIFACT_DIR_ENV = "MEMOS_ARTIFACT_DIR";

function getDefaultArtifactDir() {
const fromEnv = process.env[ARTIFACT_DIR_ENV];
const dir = fromEnv ? path.resolve(fromEnv) : path.join(os.tmpdir(), "memos-import-artifacts");
fs.ensureDirSync(dir);
return dir;
}

module.exports = {
ARTIFACT_DIR_ENV,
getDefaultArtifactDir,
};

42 changes: 42 additions & 0 deletions src/core/deleteImported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const path = require("path");
const fs = require("fs-extra");
const { createApiClient } = require("./apiClient");
const { getDefaultArtifactDir } = require("./artifactStore");

function noop() {}

async function deleteImported(options) {
const { openApi, accessToken, artifactDir = getDefaultArtifactDir(), onEvent = noop } = options;

const client = createApiClient({ openApi, accessToken });
const idsFilePath = path.join(artifactDir, "sendedIds.json");

if (!fs.existsSync(idsFilePath)) {
throw new Error(`sendedIds.json not found: ${idsFilePath}`);
}

const ids = fs.readJSONSync(idsFilePath);

onEvent({ type: "started", message: "Delete started", data: { total: ids.length } });

let current = 0;
for (const id of ids) {
current += 1;
onEvent({ type: "progress", message: `Deleting ${current}/${ids.length}`, data: { current, total: ids.length } });
await client.deleteMemo(id);
onEvent({ type: "success", message: `Deleted: ${id}` });
}

const result = {
total: ids.length,
success: ids.length,
failed: 0,
};

onEvent({ type: "finished", message: "Delete finished", data: result });
return result;
}

module.exports = {
deleteImported,
};
Loading