update at 2026-04-21 17:55:41
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
a7f871680b2d92288a71295813e8841953d40da9 {"key":"make-fetch-happen:request-cache:https://registry.npmjs.org/@anthropic-ai%2fclaude-code","integrity":"sha512-aK3b2K7aQN3sMXKsd8hbqNU6TR5P6a0WxR05ZAAmqDnjCDufv2O3YIte2xUmRS/7SKkQ8GL0MWHv0EXhQFCwQg==","time":1776738447571,"size":1011381,"metadata":{"time":1776738447570,"url":"https://registry.npmjs.org/@anthropic-ai%2fclaude-code","reqHeaders":{"accept":"application/json"},"resHeaders":{"cache-control":"public, max-age=300","date":"Tue, 21 Apr 2026 02:27:27 GMT","etag":"\"6f1f0c29ef4bf9d95c4ab5838494d210\"","last-modified":"Mon, 20 Apr 2026 22:17:51 GMT","vary":"Accept-Encoding","content-encoding":"gzip","content-type":"application/json"},"options":{"compress":true}}}
|
||||||
|
6e9682891832857a170342c1f84ecdef7ed433cd {"key":"make-fetch-happen:request-cache:https://registry.npmjs.org/@anthropic-ai%2fclaude-code","integrity":"sha512-aK3b2K7aQN3sMXKsd8hbqNU6TR5P6a0WxR05ZAAmqDnjCDufv2O3YIte2xUmRS/7SKkQ8GL0MWHv0EXhQFCwQg==","time":1776738452173,"size":1011381,"metadata":{"time":1776738452172,"url":"https://registry.npmjs.org/@anthropic-ai%2fclaude-code","reqHeaders":{"accept":"application/json"},"resHeaders":{"cache-control":"public, max-age=300","date":"Tue, 21 Apr 2026 02:27:32 GMT","etag":"\"6f1f0c29ef4bf9d95c4ab5838494d210\"","last-modified":"Mon, 20 Apr 2026 22:17:51 GMT","vary":"Accept-Encoding","content-encoding":"gzip","content-type":"application/json"},"options":{"compress":true}}}
|
||||||
25
.npm-cache/_logs/2026-04-21T02_27_21_705Z-debug-0.log
Normal file
25
.npm-cache/_logs/2026-04-21T02_27_21_705Z-debug-0.log
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
0 verbose cli /usr/local/bin/node /usr/local/bin/npm
|
||||||
|
1 info using npm@10.9.3
|
||||||
|
2 info using node@v22.20.0
|
||||||
|
3 silly config load:file:/usr/local/lib/node_modules/npm/npmrc
|
||||||
|
4 silly config load:file:/Users/gavin/obsidian-plugins/note2any/.npmrc
|
||||||
|
5 silly config load:file:/Users/gavin/.npmrc
|
||||||
|
6 silly config load:file:/usr/local/etc/npmrc
|
||||||
|
7 verbose title npm view @anthropic-ai/claude-code name version description dist-tags.latest
|
||||||
|
8 verbose argv "view" "@anthropic-ai/claude-code" "name" "version" "description" "dist-tags.latest"
|
||||||
|
9 verbose logfile logs-max:10 dir:/Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_21_705Z-
|
||||||
|
10 verbose logfile /Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_21_705Z-debug-0.log
|
||||||
|
11 silly logfile done cleaning log files
|
||||||
|
12 http fetch GET 200 https://registry.npmjs.org/npm 531ms
|
||||||
|
13 http fetch GET 200 https://registry.npmjs.org/@anthropic-ai%2fclaude-code 524ms (cache miss)
|
||||||
|
14 verbose cwd /Users/gavin/obsidian-plugins/note2any
|
||||||
|
15 verbose os Darwin 24.6.0
|
||||||
|
16 verbose node v22.20.0
|
||||||
|
17 verbose npm v10.9.3
|
||||||
|
18 notice
|
||||||
|
18 notice New major version of npm available! 10.9.3 -> 11.12.1
|
||||||
|
18 notice Changelog: https://github.com/npm/cli/releases/tag/v11.12.1
|
||||||
|
18 notice To update run: npm install -g npm@11.12.1
|
||||||
|
18 notice { force: true, [Symbol(proc-log.meta)]: true }
|
||||||
|
19 verbose exit 0
|
||||||
|
20 info ok
|
||||||
19
.npm-cache/_logs/2026-04-21T02_27_27_088Z-debug-0.log
Normal file
19
.npm-cache/_logs/2026-04-21T02_27_27_088Z-debug-0.log
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
0 verbose cli /usr/local/bin/node /usr/local/bin/npm
|
||||||
|
1 info using npm@10.9.3
|
||||||
|
2 info using node@v22.20.0
|
||||||
|
3 silly config load:file:/usr/local/lib/node_modules/npm/npmrc
|
||||||
|
4 silly config load:file:/Users/gavin/obsidian-plugins/note2any/.npmrc
|
||||||
|
5 silly config load:file:/Users/gavin/.npmrc
|
||||||
|
6 silly config load:file:/usr/local/etc/npmrc
|
||||||
|
7 verbose title npm view @anthropic-ai/claude-code engines bin
|
||||||
|
8 verbose argv "view" "@anthropic-ai/claude-code" "engines" "bin"
|
||||||
|
9 verbose logfile logs-max:10 dir:/Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_27_088Z-
|
||||||
|
10 verbose logfile /Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_27_088Z-debug-0.log
|
||||||
|
11 silly logfile done cleaning log files
|
||||||
|
12 http fetch GET 200 https://registry.npmjs.org/@anthropic-ai%2fclaude-code 368ms (cache revalidated)
|
||||||
|
13 verbose cwd /Users/gavin/obsidian-plugins/note2any
|
||||||
|
14 verbose os Darwin 24.6.0
|
||||||
|
15 verbose node v22.20.0
|
||||||
|
16 verbose npm v10.9.3
|
||||||
|
17 verbose exit 0
|
||||||
|
18 info ok
|
||||||
19
.npm-cache/_logs/2026-04-21T02_27_31_431Z-debug-0.log
Normal file
19
.npm-cache/_logs/2026-04-21T02_27_31_431Z-debug-0.log
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
0 verbose cli /usr/local/bin/node /usr/local/bin/npm
|
||||||
|
1 info using npm@10.9.3
|
||||||
|
2 info using node@v22.20.0
|
||||||
|
3 silly config load:file:/usr/local/lib/node_modules/npm/npmrc
|
||||||
|
4 silly config load:file:/Users/gavin/obsidian-plugins/note2any/.npmrc
|
||||||
|
5 silly config load:file:/Users/gavin/.npmrc
|
||||||
|
6 silly config load:file:/usr/local/etc/npmrc
|
||||||
|
7 verbose title npm view @anthropic-ai/claude-code readme
|
||||||
|
8 verbose argv "view" "@anthropic-ai/claude-code" "readme"
|
||||||
|
9 verbose logfile logs-max:10 dir:/Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_31_431Z-
|
||||||
|
10 verbose logfile /Users/gavin/obsidian-plugins/note2any/.npm-cache/_logs/2026-04-21T02_27_31_431Z-debug-0.log
|
||||||
|
11 silly logfile done cleaning log files
|
||||||
|
12 http fetch GET 200 https://registry.npmjs.org/@anthropic-ai%2fclaude-code 618ms (cache revalidated)
|
||||||
|
13 verbose cwd /Users/gavin/obsidian-plugins/note2any
|
||||||
|
14 verbose os Darwin 24.6.0
|
||||||
|
15 verbose node v22.20.0
|
||||||
|
16 verbose npm v10.9.3
|
||||||
|
17 verbose exit 0
|
||||||
|
18 info ok
|
||||||
0
.npm-cache/_update-notifier-last-checked
Normal file
0
.npm-cache/_update-notifier-last-checked
Normal file
15
build.sh
15
build.sh
@@ -1,6 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e # 出错立即退出
|
set -e # 出错立即退出
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# 1. 构建
|
# 1. 构建
|
||||||
MODE="$1"
|
MODE="$1"
|
||||||
BUILD_CMD=(npm run build)
|
BUILD_CMD=(npm run build)
|
||||||
@@ -55,5 +58,17 @@ else
|
|||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 5. server 如有变动则同步远端
|
||||||
|
SERVER_STATUS="$(git status --porcelain --untracked-files=all -- server 2>/dev/null || true)"
|
||||||
|
if [[ -n "$SERVER_STATUS" ]]; then
|
||||||
|
echo "🚀 检测到 server 目录有变动,开始更新远端..."
|
||||||
|
bash "$SCRIPT_DIR/server/deploy.sh"
|
||||||
|
echo "✅ server 远端更新完成"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "ℹ️ server 无变动,跳过远端更新"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
echo "✅ 部署完成!"
|
echo "✅ 部署完成!"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"build": "npm run build:bundle --",
|
"build": "npm run build:bundle --",
|
||||||
"build:obf": "OBFUSCATE=1 npm run build:bundle --",
|
"build:obf": "OBFUSCATE=1 npm run build:bundle --",
|
||||||
"build:bundle": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build:bundle": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"build:server": "tsc -p server/tsconfig.json",
|
||||||
"download": "node tools/download.mjs",
|
"download": "node tools/download.mjs",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
},
|
},
|
||||||
|
|||||||
20
run.sh
20
run.sh
@@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 自动添加所有修改
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# 如果没有提交信息,默认用时间戳
|
|
||||||
msg="update at $(date '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
|
|
||||||
# 支持自定义提交信息:./run.sh "your message"
|
|
||||||
if [ $# -gt 0 ]; then
|
|
||||||
msg="$*"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 提交
|
|
||||||
git commit -m "$msg"
|
|
||||||
|
|
||||||
# 推送到远程 main 分支
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
28
server/README.md
Normal file
28
server/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# note2any-wx-server
|
||||||
|
|
||||||
|
仓库内置的 TypeScript 版微信代理服务。
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
- `GET /`
|
||||||
|
- `POST /v1/wx/token`
|
||||||
|
- `POST /v1/wx/encrypt`
|
||||||
|
- `GET /v1/wx/info/:authkey`
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
- `PORT`,默认 `3001`
|
||||||
|
- `ENCRYPT_KEY`,长度必须为 `32`
|
||||||
|
- `USERS_FILE`,默认 `./data/users.json`
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node server/dist/index.js
|
||||||
|
```
|
||||||
39
server/deploy.sh
Normal file
39
server/deploy.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REMOTE="gavin@biboer.cn"
|
||||||
|
PORT="21174"
|
||||||
|
REMOTE_DIR="/home/gavin/note2any-wx-server"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
npm run build:server
|
||||||
|
|
||||||
|
ssh -p "$PORT" "$REMOTE" bash -s <<EOF
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$REMOTE_DIR/backups/$STAMP"
|
||||||
|
cp -f "$REMOTE_DIR/app.js" "$REMOTE_DIR/backups/$STAMP/app.js.bak" 2>/dev/null || true
|
||||||
|
cp -f "$REMOTE_DIR/package.json" "$REMOTE_DIR/backups/$STAMP/package.json.bak" 2>/dev/null || true
|
||||||
|
cp -f "$REMOTE_DIR/ecosystem.config.js" "$REMOTE_DIR/backups/$STAMP/ecosystem.config.js.bak" 2>/dev/null || true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
scp -P "$PORT" -r \
|
||||||
|
"server/dist" \
|
||||||
|
"server/package.json" \
|
||||||
|
"server/README.md" \
|
||||||
|
"$REMOTE:$REMOTE_DIR/"
|
||||||
|
|
||||||
|
ssh -p "$PORT" "$REMOTE" bash -s <<EOF
|
||||||
|
set -euo pipefail
|
||||||
|
cat > "$REMOTE_DIR/app.js" <<'APP'
|
||||||
|
require("./dist/index.js");
|
||||||
|
APP
|
||||||
|
pkill -f "node $REMOTE_DIR/app.js" 2>/dev/null || true
|
||||||
|
pkill -f "node $REMOTE_DIR/dist/index.js" 2>/dev/null || true
|
||||||
|
cd "$REMOTE_DIR"
|
||||||
|
set -a
|
||||||
|
[ -f ./.env ] && . ./.env
|
||||||
|
set +a
|
||||||
|
nohup node app.js > server.log 2>&1 &
|
||||||
|
sleep 1
|
||||||
|
ps -ef | grep -E "node .*note2any-wx-server/(app|dist/index)\\.js" | grep -v grep
|
||||||
|
EOF
|
||||||
63
server/dist/config.js
vendored
Normal file
63
server/dist/config.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.config = void 0;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
function readDotEnv(dotEnvPath) {
|
||||||
|
if (!fs.existsSync(dotEnvPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(dotEnvPath, "utf8");
|
||||||
|
const parsed = {};
|
||||||
|
for (const rawLine of content.split(/\r?\n/)) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (line.length === 0 || line.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const index = line.indexOf("=");
|
||||||
|
if (index <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = line.slice(0, index).trim();
|
||||||
|
const value = line.slice(index + 1).trim();
|
||||||
|
if (key.length > 0 && !(key in parsed)) {
|
||||||
|
parsed[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const dotEnv = readDotEnv(path.join(cwd, ".env"));
|
||||||
|
function getEnv(name, fallback = "") {
|
||||||
|
var _a, _b;
|
||||||
|
const value = (_b = (_a = process.env[name]) !== null && _a !== void 0 ? _a : dotEnv[name]) !== null && _b !== void 0 ? _b : fallback;
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
exports.config = {
|
||||||
|
port: parseInt(getEnv("PORT", "3001"), 10),
|
||||||
|
encryptKey: getEnv("ENCRYPT_KEY"),
|
||||||
|
usersFile: path.resolve(cwd, getEnv("USERS_FILE", "./data/users.json")),
|
||||||
|
};
|
||||||
53
server/dist/crypto.js
vendored
Normal file
53
server/dist/crypto.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.decrypt = exports.encrypt = void 0;
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
|
const config_1 = require("./config");
|
||||||
|
const ALGORITHM = "aes-256-cbc";
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
function getKey() {
|
||||||
|
if (config_1.config.encryptKey.length !== 32) {
|
||||||
|
throw new Error("ENCRYPT_KEY must be a 32-character string");
|
||||||
|
}
|
||||||
|
return Buffer.from(config_1.config.encryptKey, "utf8");
|
||||||
|
}
|
||||||
|
function encrypt(text) {
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv);
|
||||||
|
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
|
||||||
|
return iv.toString("hex") + encrypted.toString("hex");
|
||||||
|
}
|
||||||
|
exports.encrypt = encrypt;
|
||||||
|
function decrypt(encrypted) {
|
||||||
|
const ivHex = encrypted.slice(0, IV_LENGTH * 2);
|
||||||
|
const contentHex = encrypted.slice(IV_LENGTH * 2);
|
||||||
|
const iv = Buffer.from(ivHex, "hex");
|
||||||
|
const content = Buffer.from(contentHex, "hex");
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv);
|
||||||
|
const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);
|
||||||
|
return decrypted.toString("utf8");
|
||||||
|
}
|
||||||
|
exports.decrypt = decrypt;
|
||||||
31
server/dist/http.js
vendored
Normal file
31
server/dist/http.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.readJsonBody = exports.sendJson = void 0;
|
||||||
|
function sendJson(res, statusCode, payload) {
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Length": Buffer.byteLength(body),
|
||||||
|
});
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
exports.sendJson = sendJson;
|
||||||
|
async function readJsonBody(req, limitBytes = 10 * 1024 * 1024) {
|
||||||
|
const chunks = [];
|
||||||
|
let size = 0;
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
size += buffer.length;
|
||||||
|
if (size > limitBytes) {
|
||||||
|
throw new Error("Request body too large");
|
||||||
|
}
|
||||||
|
chunks.push(buffer);
|
||||||
|
}
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||||
|
if (raw.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
exports.readJsonBody = readJsonBody;
|
||||||
127
server/dist/index.js
vendored
Normal file
127
server/dist/index.js
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const http = __importStar(require("http"));
|
||||||
|
const url_1 = require("url");
|
||||||
|
const config_1 = require("./config");
|
||||||
|
const crypto_1 = require("./crypto");
|
||||||
|
const http_1 = require("./http");
|
||||||
|
const wechat_1 = require("./wechat");
|
||||||
|
const tokenCache = {};
|
||||||
|
function isNonEmptyString(value) {
|
||||||
|
return typeof value === "string" && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
async function handleToken(req, res) {
|
||||||
|
var _a;
|
||||||
|
const body = await (0, http_1.readJsonBody)(req);
|
||||||
|
const appid = typeof body.appid === "string" ? body.appid.trim() : "";
|
||||||
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
||||||
|
if (!appid || !secret) {
|
||||||
|
(0, http_1.sendJson)(res, 400, { code: 400, msg: "Missing required fields" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cached = tokenCache[appid];
|
||||||
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
(0, http_1.sendJson)(res, 200, {
|
||||||
|
access_token: cached.token,
|
||||||
|
expires_in: Math.floor((cached.expiresAt - Date.now()) / 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const realSecret = secret.startsWith("SECRET") ? (0, crypto_1.decrypt)(secret.slice(6)) : secret;
|
||||||
|
const wxUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appid)}&secret=${encodeURIComponent(realSecret)}`;
|
||||||
|
const wxRes = await (0, wechat_1.httpsGetJson)(wxUrl);
|
||||||
|
if (typeof wxRes.errcode === "number") {
|
||||||
|
(0, http_1.sendJson)(res, 200, { code: wxRes.errcode, msg: (_a = wxRes.errmsg) !== null && _a !== void 0 ? _a : "" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isNonEmptyString(wxRes.access_token) || typeof wxRes.expires_in !== "number") {
|
||||||
|
throw new Error("Invalid token response from WeChat API");
|
||||||
|
}
|
||||||
|
tokenCache[appid] = {
|
||||||
|
token: wxRes.access_token,
|
||||||
|
expiresAt: Date.now() + 7000 * 1000,
|
||||||
|
};
|
||||||
|
(0, http_1.sendJson)(res, 200, { ...wxRes });
|
||||||
|
}
|
||||||
|
async function handleEncrypt(req, res) {
|
||||||
|
const body = await (0, http_1.readJsonBody)(req);
|
||||||
|
const wechat = Array.isArray(body.wechat) ? body.wechat : [];
|
||||||
|
if (wechat.length === 0) {
|
||||||
|
(0, http_1.sendJson)(res, 400, { code: 400, msg: "Missing required fields" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const encrypted = wechat.map((item) => ({
|
||||||
|
name: typeof item.name === "string" ? item.name : "",
|
||||||
|
appid: typeof item.appid === "string" ? item.appid : "",
|
||||||
|
secret: "SECRET" + (0, crypto_1.encrypt)(typeof item.secret === "string" ? item.secret : ""),
|
||||||
|
}));
|
||||||
|
(0, http_1.sendJson)(res, 200, { code: 0, wechat: encrypted });
|
||||||
|
}
|
||||||
|
function handleInfo(authkey, res) {
|
||||||
|
(0, http_1.sendJson)(res, 200, {
|
||||||
|
code: 0,
|
||||||
|
name: authkey || "note2any",
|
||||||
|
status: "active",
|
||||||
|
created: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function requestHandler(req, res) {
|
||||||
|
var _a, _b;
|
||||||
|
const method = (_a = req.method) !== null && _a !== void 0 ? _a : "GET";
|
||||||
|
const url = new url_1.URL((_b = req.url) !== null && _b !== void 0 ? _b : "/", "http://127.0.0.1");
|
||||||
|
const pathname = url.pathname;
|
||||||
|
try {
|
||||||
|
if (method === "GET" && pathname === "/") {
|
||||||
|
(0, http_1.sendJson)(res, 200, { status: "ok", service: "note2any-wx-server" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method === "POST" && pathname === "/v1/wx/token") {
|
||||||
|
await handleToken(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method === "POST" && pathname === "/v1/wx/encrypt") {
|
||||||
|
await handleEncrypt(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method === "GET" && pathname.startsWith("/v1/wx/info/")) {
|
||||||
|
const authkey = decodeURIComponent(pathname.slice("/v1/wx/info/".length));
|
||||||
|
handleInfo(authkey, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(0, http_1.sendJson)(res, 404, { code: 404, msg: "Not Found" });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown server error";
|
||||||
|
console.error(`[server] ${method} ${pathname} failed: ${message}`);
|
||||||
|
(0, http_1.sendJson)(res, 500, { code: 500, msg: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
void requestHandler(req, res);
|
||||||
|
});
|
||||||
|
server.listen(config_1.config.port, () => {
|
||||||
|
console.log(`note2any-wx-server running on port ${config_1.config.port}`);
|
||||||
|
});
|
||||||
44
server/dist/users.js
vendored
Normal file
44
server/dist/users.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.validateAuthKey = void 0;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const config_1 = require("./config");
|
||||||
|
function readUsersFile() {
|
||||||
|
const content = fs.readFileSync(config_1.config.usersFile, "utf8");
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
function validateAuthKey(authkey) {
|
||||||
|
const users = readUsersFile().users;
|
||||||
|
const user = users[authkey];
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (user.status !== "active") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
exports.validateAuthKey = validateAuthKey;
|
||||||
46
server/dist/wechat.js
vendored
Normal file
46
server/dist/wechat.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.httpsGetJson = void 0;
|
||||||
|
const https = __importStar(require("https"));
|
||||||
|
function httpsGetJson(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https.get(url, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
}
|
||||||
|
catch (_error) {
|
||||||
|
reject(new Error("Invalid JSON from WeChat API"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.httpsGetJson = httpsGetJson;
|
||||||
10
server/package.json
Normal file
10
server/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "note2any-wx-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "TypeScript implementation of the note2any WeChat proxy server",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
server/src/config.ts
Normal file
45
server/src/config.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
function readDotEnv(dotEnvPath: string): Record<string, string> {
|
||||||
|
if (!fs.existsSync(dotEnvPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(dotEnvPath, "utf8");
|
||||||
|
const parsed: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const rawLine of content.split(/\r?\n/)) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (line.length === 0 || line.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = line.indexOf("=");
|
||||||
|
if (index <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = line.slice(0, index).trim();
|
||||||
|
const value = line.slice(index + 1).trim();
|
||||||
|
if (key.length > 0 && !(key in parsed)) {
|
||||||
|
parsed[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const dotEnv = readDotEnv(path.join(cwd, ".env"));
|
||||||
|
|
||||||
|
function getEnv(name: string, fallback = ""): string {
|
||||||
|
const value = process.env[name] ?? dotEnv[name] ?? fallback;
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
port: parseInt(getEnv("PORT", "3001"), 10),
|
||||||
|
encryptKey: getEnv("ENCRYPT_KEY"),
|
||||||
|
usersFile: path.resolve(cwd, getEnv("USERS_FILE", "./data/users.json")),
|
||||||
|
};
|
||||||
30
server/src/crypto.ts
Normal file
30
server/src/crypto.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
|
const ALGORITHM = "aes-256-cbc";
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
|
||||||
|
function getKey(): Buffer {
|
||||||
|
if (config.encryptKey.length !== 32) {
|
||||||
|
throw new Error("ENCRYPT_KEY must be a 32-character string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(config.encryptKey, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encrypt(text: string): string {
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv);
|
||||||
|
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
|
||||||
|
return iv.toString("hex") + encrypted.toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(encrypted: string): string {
|
||||||
|
const ivHex = encrypted.slice(0, IV_LENGTH * 2);
|
||||||
|
const contentHex = encrypted.slice(IV_LENGTH * 2);
|
||||||
|
const iv = Buffer.from(ivHex, "hex");
|
||||||
|
const content = Buffer.from(contentHex, "hex");
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv);
|
||||||
|
const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);
|
||||||
|
return decrypted.toString("utf8");
|
||||||
|
}
|
||||||
36
server/src/http.ts
Normal file
36
server/src/http.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as http from "http";
|
||||||
|
|
||||||
|
export interface JsonResponse {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendJson(res: http.ServerResponse, statusCode: number, payload: JsonResponse): void {
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Length": Buffer.byteLength(body),
|
||||||
|
});
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readJsonBody(req: http.IncomingMessage, limitBytes = 10 * 1024 * 1024): Promise<unknown> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
size += buffer.length;
|
||||||
|
if (size > limitBytes) {
|
||||||
|
throw new Error("Request body too large");
|
||||||
|
}
|
||||||
|
chunks.push(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||||
|
if (raw.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
}
|
||||||
139
server/src/index.ts
Normal file
139
server/src/index.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import * as http from "http";
|
||||||
|
import { URL } from "url";
|
||||||
|
import { config } from "./config";
|
||||||
|
import { decrypt, encrypt } from "./crypto";
|
||||||
|
import { readJsonBody, sendJson } from "./http";
|
||||||
|
import { httpsGetJson } from "./wechat";
|
||||||
|
|
||||||
|
interface TokenRequestBody {
|
||||||
|
authkey?: string;
|
||||||
|
appid?: string;
|
||||||
|
secret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncryptWechatItem {
|
||||||
|
name?: string;
|
||||||
|
appid?: string;
|
||||||
|
secret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncryptRequestBody {
|
||||||
|
authkey?: string;
|
||||||
|
wechat?: EncryptWechatItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenCache: Record<string, { token: string; expiresAt: number }> = {};
|
||||||
|
|
||||||
|
function isNonEmptyString(value: unknown): value is string {
|
||||||
|
return typeof value === "string" && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToken(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||||
|
const body = await readJsonBody(req) as TokenRequestBody;
|
||||||
|
const appid = typeof body.appid === "string" ? body.appid.trim() : "";
|
||||||
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
||||||
|
|
||||||
|
if (!appid || !secret) {
|
||||||
|
sendJson(res, 400, { code: 400, msg: "Missing required fields" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = tokenCache[appid];
|
||||||
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
sendJson(res, 200, {
|
||||||
|
access_token: cached.token,
|
||||||
|
expires_in: Math.floor((cached.expiresAt - Date.now()) / 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realSecret = secret.startsWith("SECRET") ? decrypt(secret.slice(6)) : secret;
|
||||||
|
const wxUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appid)}&secret=${encodeURIComponent(realSecret)}`;
|
||||||
|
const wxRes = await httpsGetJson(wxUrl);
|
||||||
|
|
||||||
|
if (typeof wxRes.errcode === "number") {
|
||||||
|
sendJson(res, 200, { code: wxRes.errcode, msg: wxRes.errmsg ?? "" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNonEmptyString(wxRes.access_token) || typeof wxRes.expires_in !== "number") {
|
||||||
|
throw new Error("Invalid token response from WeChat API");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCache[appid] = {
|
||||||
|
token: wxRes.access_token,
|
||||||
|
expiresAt: Date.now() + 7000 * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendJson(res, 200, { ...wxRes });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEncrypt(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||||
|
const body = await readJsonBody(req) as EncryptRequestBody;
|
||||||
|
const wechat = Array.isArray(body.wechat) ? body.wechat : [];
|
||||||
|
|
||||||
|
if (wechat.length === 0) {
|
||||||
|
sendJson(res, 400, { code: 400, msg: "Missing required fields" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = wechat.map((item) => ({
|
||||||
|
name: typeof item.name === "string" ? item.name : "",
|
||||||
|
appid: typeof item.appid === "string" ? item.appid : "",
|
||||||
|
secret: "SECRET" + encrypt(typeof item.secret === "string" ? item.secret : ""),
|
||||||
|
}));
|
||||||
|
|
||||||
|
sendJson(res, 200, { code: 0, wechat: encrypted });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInfo(authkey: string, res: http.ServerResponse): void {
|
||||||
|
sendJson(res, 200, {
|
||||||
|
code: 0,
|
||||||
|
name: authkey || "note2any",
|
||||||
|
status: "active",
|
||||||
|
created: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestHandler(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||||
|
const method = req.method ?? "GET";
|
||||||
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (method === "GET" && pathname === "/") {
|
||||||
|
sendJson(res, 200, { status: "ok", service: "note2any-wx-server" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "POST" && pathname === "/v1/wx/token") {
|
||||||
|
await handleToken(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "POST" && pathname === "/v1/wx/encrypt") {
|
||||||
|
await handleEncrypt(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "GET" && pathname.startsWith("/v1/wx/info/")) {
|
||||||
|
const authkey = decodeURIComponent(pathname.slice("/v1/wx/info/".length));
|
||||||
|
handleInfo(authkey, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJson(res, 404, { code: 404, msg: "Not Found" });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown server error";
|
||||||
|
console.error(`[server] ${method} ${pathname} failed: ${message}`);
|
||||||
|
sendJson(res, 500, { code: 500, msg: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
|
void requestHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.port, () => {
|
||||||
|
console.log(`note2any-wx-server running on port ${config.port}`);
|
||||||
|
});
|
||||||
31
server/src/users.ts
Normal file
31
server/src/users.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
|
export interface UserRecord {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsersFile {
|
||||||
|
users: Record<string, UserRecord>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readUsersFile(): UsersFile {
|
||||||
|
const content = fs.readFileSync(config.usersFile, "utf8");
|
||||||
|
return JSON.parse(content) as UsersFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateAuthKey(authkey: string): UserRecord | null {
|
||||||
|
const users = readUsersFile().users;
|
||||||
|
const user = users[authkey];
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status !== "active") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
27
server/src/wechat.ts
Normal file
27
server/src/wechat.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { IncomingMessage } from "http";
|
||||||
|
import * as https from "https";
|
||||||
|
|
||||||
|
export interface WechatTokenResponse {
|
||||||
|
access_token?: string;
|
||||||
|
expires_in?: number;
|
||||||
|
errcode?: number;
|
||||||
|
errmsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function httpsGetJson(url: string): Promise<WechatTokenResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https.get(url, (res: IncomingMessage) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk: Buffer) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data) as WechatTokenResponse);
|
||||||
|
} catch (_error) {
|
||||||
|
reject(new Error("Invalid JSON from WeChat API"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
5
src/api-host.ts
Normal file
5
src/api-host.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* 运行时 API Host。
|
||||||
|
* 公众号鉴权、数学公式渲染、Excalidraw 转换等后端接口统一走这里。
|
||||||
|
*/
|
||||||
|
export const API_HOST = 'https://api.biboer.cn';
|
||||||
@@ -9,7 +9,7 @@ import { UploadImageToWx } from './imagelib';
|
|||||||
import { NMPSettings } from './settings';
|
import { NMPSettings } from './settings';
|
||||||
import AssetsManager from './assets';
|
import AssetsManager from './assets';
|
||||||
import InlineCSS from './inline-css';
|
import InlineCSS from './inline-css';
|
||||||
import { wxGetToken, wxAddDraft, wxBatchGetMaterial, DraftArticle, DraftImageMediaId, DraftImages, wxAddDraftImages } from './wechat/weixin-api';
|
import { wxGetTokenWithFallback, wxAddDraft, wxBatchGetMaterial, DraftArticle, DraftImageMediaId, DraftImages, wxAddDraftImages } from './wechat/weixin-api';
|
||||||
import { MDRendererCallback } from './markdown/extension';
|
import { MDRendererCallback } from './markdown/extension';
|
||||||
import { MarkedParser } from './markdown/parser';
|
import { MarkedParser } from './markdown/parser';
|
||||||
import { LocalImageManager, LocalFile } from './markdown/local-file';
|
import { LocalImageManager, LocalFile } from './markdown/local-file';
|
||||||
@@ -535,23 +535,19 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
|
|
||||||
async getToken(appid: string) {
|
async getToken(appid: string) {
|
||||||
const secret = this.getSecret(appid);
|
const secret = this.getSecret(appid);
|
||||||
const res = await wxGetToken(this.settings.authKey, appid, secret);
|
const res = await wxGetTokenWithFallback('', appid, secret);
|
||||||
|
const message = typeof res.json?.message === 'string' ? res.json.message : '未知错误';
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
const data = res.json;
|
throw new Error('获取token失败: ' + message);
|
||||||
throw new Error('获取token失败: ' + data.message);
|
|
||||||
}
|
}
|
||||||
const token = res.json.token;
|
const token = typeof res.json?.token === 'string' ? res.json.token : '';
|
||||||
if (token === '') {
|
if (token === '') {
|
||||||
throw new Error('获取token失败: ' + res.json.message);
|
throw new Error('获取token失败: ' + message);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImages(appid: string) {
|
async uploadImages(appid: string) {
|
||||||
if (!this.settings.authKey) {
|
|
||||||
throw new Error('请先设置注册码(AuthKey)');
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = this.getMetadata();
|
let metadata = this.getMetadata();
|
||||||
if (metadata.appid) {
|
if (metadata.appid) {
|
||||||
appid = metadata.appid;
|
appid = metadata.appid;
|
||||||
@@ -590,17 +586,13 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
getSecret(appid: string) {
|
getSecret(appid: string) {
|
||||||
for (const wx of this.settings.wxInfo) {
|
for (const wx of this.settings.wxInfo) {
|
||||||
if (wx.appid === appid) {
|
if (wx.appid === appid) {
|
||||||
return wx.secret.replace('SECRET', '');
|
return wx.secret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async postArticle(appid:string, localCover: File | null = null) {
|
async postArticle(appid:string, localCover: File | null = null) {
|
||||||
if (!this.settings.authKey) {
|
|
||||||
throw new Error('请先设置注册码(AuthKey)');
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = this.getMetadata();
|
let metadata = this.getMetadata();
|
||||||
if (metadata.appid) {
|
if (metadata.appid) {
|
||||||
appid = metadata.appid;
|
appid = metadata.appid;
|
||||||
@@ -675,10 +667,6 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async postImages(appid: string) {
|
async postImages(appid: string) {
|
||||||
if (!this.settings.authKey) {
|
|
||||||
throw new Error('请先设置注册码(AuthKey)');
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = this.getMetadata();
|
let metadata = this.getMetadata();
|
||||||
if (metadata.appid) {
|
if (metadata.appid) {
|
||||||
appid = metadata.appid;
|
appid = metadata.appid;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export interface PlatformConfig {
|
|||||||
|
|
||||||
export interface WechatConfig extends PlatformConfig {
|
export interface WechatConfig extends PlatformConfig {
|
||||||
wxInfo: Array<{name: string, appid: string, secret: string}>;
|
wxInfo: Array<{name: string, appid: string, secret: string}>;
|
||||||
authKey: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface XhsConfig extends PlatformConfig {
|
export interface XhsConfig extends PlatformConfig {
|
||||||
@@ -61,7 +60,6 @@ export class ConfigManager {
|
|||||||
name: 'WeChat',
|
name: 'WeChat',
|
||||||
enabled: this.settings.wxInfo.length > 0,
|
enabled: this.settings.wxInfo.length > 0,
|
||||||
wxInfo: this.settings.wxInfo,
|
wxInfo: this.settings.wxInfo,
|
||||||
authKey: this.settings.authKey,
|
|
||||||
validate: () => this.validateWechatConfig()
|
validate: () => this.validateWechatConfig()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -94,7 +92,6 @@ export class ConfigManager {
|
|||||||
|
|
||||||
private validateWechatConfig(): boolean {
|
private validateWechatConfig(): boolean {
|
||||||
try {
|
try {
|
||||||
ErrorHandler.validateRequired(this.settings.authKey, 'WeChat 授权密钥');
|
|
||||||
if (this.settings.wxInfo.length === 0) {
|
if (this.settings.wxInfo.length === 0) {
|
||||||
throw new ValidationError('WeChat 配置信息不能为空');
|
throw new ValidationError('WeChat 配置信息不能为空');
|
||||||
}
|
}
|
||||||
@@ -131,7 +128,7 @@ export class ConfigManager {
|
|||||||
|
|
||||||
isTokenValid(platform: 'wechat'): boolean {
|
isTokenValid(platform: 'wechat'): boolean {
|
||||||
if (platform === 'wechat') {
|
if (platform === 'wechat') {
|
||||||
return this.settings.isAuthKeyVaild();
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -145,4 +142,4 @@ export class ConfigManager {
|
|||||||
// 当前设置结构中使用 authKey 系统,这里可以扩展
|
// 当前设置结构中使用 authKey 系统,这里可以扩展
|
||||||
console.log('WeChat token cleared (using authKey system)');
|
console.log('WeChat token cleared (using authKey system)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Token, Tokens, MarkedExtension } from "marked";
|
import { Token, Tokens, MarkedExtension } from "marked";
|
||||||
import { Notice, TAbstractFile, TFile, Vault, MarkdownView, requestUrl, Platform } from "obsidian";
|
import { Notice, TAbstractFile, TFile, Vault, MarkdownView, requestUrl, Platform } from "obsidian";
|
||||||
import { Extension } from "./extension";
|
import { Extension } from "./extension";
|
||||||
|
import { API_HOST } from "../api-host";
|
||||||
import { NMPSettings } from "../settings";
|
import { NMPSettings } from "../settings";
|
||||||
import { IsImageLibReady, PrepareImageLib, WebpToJPG, UploadImageToWx } from "../imagelib";
|
import { IsImageLibReady, PrepareImageLib, WebpToJPG, UploadImageToWx } from "../imagelib";
|
||||||
import { convertJpegIfNeeded } from "../exif-orientation";
|
import { convertJpegIfNeeded } from "../exif-orientation";
|
||||||
@@ -644,7 +645,7 @@ export class LocalFile extends Extension{
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getExcalidrawUrl(data: string) {
|
static async getExcalidrawUrl(data: string) {
|
||||||
const url = 'https://obplugin.sunboshi.tech/math/excalidraw';
|
const url = `${API_HOST}/math/excalidraw`;
|
||||||
const req = await requestUrl({
|
const req = await requestUrl({
|
||||||
url,
|
url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -849,4 +850,4 @@ export class LocalFile extends Extension{
|
|||||||
}
|
}
|
||||||
}]};
|
}]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { MarkedExtension, Token, Tokens } from "marked";
|
|||||||
import { requestUrl } from "obsidian";
|
import { requestUrl } from "obsidian";
|
||||||
import { Extension } from "./extension";
|
import { Extension } from "./extension";
|
||||||
import { NMPSettings } from "src/settings";
|
import { NMPSettings } from "src/settings";
|
||||||
|
import { API_HOST } from "../api-host";
|
||||||
|
|
||||||
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/;
|
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/;
|
||||||
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
||||||
@@ -15,7 +16,7 @@ export function cleanMathCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MathRendererQueue {
|
export class MathRendererQueue {
|
||||||
private host = 'https://obplugin.sunboshi.tech';
|
private host = API_HOST;
|
||||||
private static instance: MathRendererQueue;
|
private static instance: MathRendererQueue;
|
||||||
private mathIndex: number = 0;
|
private mathIndex: number = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* 作用:Obsidian 设置面板集成,提供界面化配置入口。
|
* 作用:Obsidian 设置面板集成,提供界面化配置入口。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian';
|
import { App, ButtonComponent, TextAreaComponent, PluginSettingTab, Setting, Notice } from 'obsidian';
|
||||||
import Note2AnyPlugin from './main';
|
import Note2AnyPlugin from './main';
|
||||||
import { wxGetToken, wxEncrypt } from './wechat/weixin-api';
|
import { wxGetTokenWithFallback, wxEncrypt } from './wechat/weixin-api';
|
||||||
import { cleanMathCache } from './markdown/math';
|
import { cleanMathCache } from './markdown/math';
|
||||||
import { NMPSettings } from './settings';
|
import { NMPSettings } from './settings';
|
||||||
import { DocModal } from './doc-modal';
|
import { DocModal } from './doc-modal';
|
||||||
@@ -41,11 +41,6 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async testWXInfo() {
|
async testWXInfo() {
|
||||||
const authKey = this.settings.authKey;
|
|
||||||
if (authKey.length == 0) {
|
|
||||||
new Notice('请先设置authKey');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const wxInfo = this.settings.wxInfo;
|
const wxInfo = this.settings.wxInfo;
|
||||||
if (wxInfo.length == 0) {
|
if (wxInfo.length == 0) {
|
||||||
new Notice('请先设置公众号信息');
|
new Notice('请先设置公众号信息');
|
||||||
@@ -54,11 +49,11 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
try {
|
try {
|
||||||
const docUrl = 'https://mp.weixin.qq.com/s/rk5CTPGr5ftly8PtYgSjCQ';
|
const docUrl = 'https://mp.weixin.qq.com/s/rk5CTPGr5ftly8PtYgSjCQ';
|
||||||
for (let wx of wxInfo) {
|
for (let wx of wxInfo) {
|
||||||
const res = await wxGetToken(authKey, wx.appid, wx.secret.replace('SECRET', ''));
|
const res = await wxGetTokenWithFallback('', wx.appid, wx.secret);
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
const data = res.json;
|
const data = res.json;
|
||||||
const { code, message } = data;
|
const { code, message } = data;
|
||||||
let content = message;
|
let content = typeof message === 'string' ? message : '未知错误';
|
||||||
if (code === 50002) {
|
if (code === 50002) {
|
||||||
content = '用户受限,可能是您的公众号被冻结或注销,请联系微信客服处理';
|
content = '用户受限,可能是您的公众号被冻结或注销,请联系微信客服处理';
|
||||||
}
|
}
|
||||||
@@ -73,11 +68,12 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = res.json;
|
const data = res.json;
|
||||||
if (data.token.length == 0) {
|
const token = typeof data.token === 'string' ? data.token : '';
|
||||||
new Notice(`${wx.name}|${wx.appid} 测试失败`);
|
if (token.length == 0) {
|
||||||
break
|
new Notice(`${wx.name}|${wx.appid} 测试失败`);
|
||||||
}
|
break
|
||||||
|
}
|
||||||
new Notice(`${wx.name} 测试通过`);
|
new Notice(`${wx.name} 测试通过`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -118,18 +114,20 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await wxEncrypt(this.settings.authKey, wechat);
|
const res = await wxEncrypt('', wechat);
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
const data = res.json;
|
const data = res.json;
|
||||||
new Notice(`${data.message}`);
|
const message = typeof data.message === 'string' ? data.message : '保存失败';
|
||||||
return false;
|
new Notice(message);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const data = res.json;
|
const data = res.json;
|
||||||
for (let wx of wechat) {
|
for (let wx of wechat) {
|
||||||
wx.secret = data[wx.appid];
|
const encryptedSecret = data[wx.appid];
|
||||||
}
|
wx.secret = typeof encryptedSecret === 'string' ? encryptedSecret : '';
|
||||||
|
}
|
||||||
|
|
||||||
this.settings.wxInfo = wechat;
|
this.settings.wxInfo = wechat;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -391,10 +389,9 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
.inputEl.setAttr('style', 'width: 520px; height: 60px;');
|
.inputEl.setAttr('style', 'width: 520px; height: 60px;');
|
||||||
});
|
});
|
||||||
|
|
||||||
const customCSSDoc = '使用指南:<a href="https://sunboshi.tech/customcss">https://sunboshi.tech/customcss</a>';
|
|
||||||
new Setting(panel)
|
new Setting(panel)
|
||||||
.setName('自定义CSS笔记')
|
.setName('自定义CSS笔记')
|
||||||
.setDesc(sanitizeHTMLToDom(customCSSDoc))
|
.setDesc('填写用于追加自定义样式的笔记标题')
|
||||||
.addText(text => {
|
.addText(text => {
|
||||||
text.setPlaceholder('请输入自定义CSS笔记标题')
|
text.setPlaceholder('请输入自定义CSS笔记标题')
|
||||||
.setValue(this.settings.customCSSNote)
|
.setValue(this.settings.customCSSNote)
|
||||||
@@ -406,10 +403,9 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
.inputEl.setAttr('style', 'width: 320px;');
|
.inputEl.setAttr('style', 'width: 320px;');
|
||||||
});
|
});
|
||||||
|
|
||||||
const expertDoc = '使用指南:<a href="https://sunboshi.tech/expert">https://sunboshi.tech/expert</a>';
|
|
||||||
new Setting(panel)
|
new Setting(panel)
|
||||||
.setName('专家设置笔记')
|
.setName('专家设置笔记')
|
||||||
.setDesc(sanitizeHTMLToDom(expertDoc))
|
.setDesc('填写用于覆盖高级渲染配置的笔记标题')
|
||||||
.addText(text => {
|
.addText(text => {
|
||||||
text.setPlaceholder('请输入专家设置笔记标题')
|
text.setPlaceholder('请输入专家设置笔记标题')
|
||||||
.setValue(this.settings.expertSettingsNote)
|
.setValue(this.settings.expertSettingsNote)
|
||||||
@@ -501,34 +497,11 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderUserTab(panel: HTMLElement): void {
|
private renderUserTab(panel: HTMLElement): void {
|
||||||
let descHtml = '详情说明:<a href="https://sunboshi.tech/subscribe">https://sunboshi.tech/subscribe</a>';
|
|
||||||
if (this.settings.isVip) {
|
|
||||||
descHtml = '<span style="color:rgb(245, 70, 85);font-weight: bold;">👑永久会员</span><br/>' + descHtml;
|
|
||||||
}
|
|
||||||
else if (this.settings.expireat) {
|
|
||||||
const timestr = this.settings.expireat.toLocaleString();
|
|
||||||
descHtml = `有效期至:${timestr} <br/>${descHtml}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Setting(panel)
|
|
||||||
.setName('注册码(AuthKey)')
|
|
||||||
.setDesc(sanitizeHTMLToDom(descHtml))
|
|
||||||
.addText(text => {
|
|
||||||
text.setPlaceholder('请输入注册码')
|
|
||||||
.setValue(this.settings.authKey)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.settings.authKey = value.trim();
|
|
||||||
this.settings.getExpiredDate();
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
})
|
|
||||||
.inputEl.setAttr('style', 'width: 320px;');
|
|
||||||
}).descEl.setAttr('style', '-webkit-user-select: text; user-select: text;');
|
|
||||||
|
|
||||||
let isClear = this.settings.wxInfo.length > 0;
|
let isClear = this.settings.wxInfo.length > 0;
|
||||||
let isRealClear = false;
|
let isRealClear = false;
|
||||||
const buttonText = isClear ? '清空公众号信息' : '保存公众号信息';
|
const buttonText = isClear ? '清空公众号信息' : '保存公众号信息';
|
||||||
|
|
||||||
new Setting(panel)
|
const wechatSetting = new Setting(panel)
|
||||||
.setName('公众号信息')
|
.setName('公众号信息')
|
||||||
.addTextArea(text => {
|
.addTextArea(text => {
|
||||||
this.wxTextArea = text;
|
this.wxTextArea = text;
|
||||||
@@ -538,45 +511,43 @@ export class Note2AnySettingTab extends PluginSettingTab {
|
|||||||
this.wxInfo = value;
|
this.wxInfo = value;
|
||||||
})
|
})
|
||||||
.inputEl.setAttr('style', 'width: 520px; height: 120px;');
|
.inputEl.setAttr('style', 'width: 520px; height: 120px;');
|
||||||
})
|
|
||||||
.addButton(button => {
|
|
||||||
button.setButtonText(buttonText);
|
|
||||||
button.onClick(async () => {
|
|
||||||
if (isClear) {
|
|
||||||
isRealClear = true;
|
|
||||||
isClear = false;
|
|
||||||
button.setButtonText('确认清空?');
|
|
||||||
}
|
|
||||||
else if (isRealClear) {
|
|
||||||
isRealClear = false;
|
|
||||||
isClear = false;
|
|
||||||
this.clear();
|
|
||||||
button.setButtonText('保存公众号信息');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
button.setButtonText('保存中...');
|
|
||||||
if (await this.encrypt()) {
|
|
||||||
isClear = true;
|
|
||||||
isRealClear = false;
|
|
||||||
button.setButtonText('清空公众号信息');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
button.setButtonText('保存公众号信息');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.addButton(button => {
|
|
||||||
button.setButtonText('测试公众号');
|
|
||||||
button.onClick(async () => {
|
|
||||||
button.setButtonText('测试中...');
|
|
||||||
await this.testWXInfo();
|
|
||||||
button.setButtonText('测试公众号');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
//
|
|
||||||
const helpEl = panel.createEl('div', { cls: 'setting-help-section' });
|
wechatSetting.controlEl.addClass('note2any-settings-action-stack');
|
||||||
helpEl.createEl('h2', { text: '帮助文档', cls: 'setting-help-title' });
|
|
||||||
helpEl.createEl('a', { text: 'https://sunboshi.tech/doc', attr: { href: 'https://sunboshi.tech/doc' } });
|
const clearButton = new ButtonComponent(wechatSetting.controlEl);
|
||||||
|
clearButton.setButtonText(buttonText);
|
||||||
|
clearButton.onClick(async () => {
|
||||||
|
if (isClear) {
|
||||||
|
isRealClear = true;
|
||||||
|
isClear = false;
|
||||||
|
clearButton.setButtonText('确认清空?');
|
||||||
|
}
|
||||||
|
else if (isRealClear) {
|
||||||
|
isRealClear = false;
|
||||||
|
isClear = false;
|
||||||
|
await this.clear();
|
||||||
|
clearButton.setButtonText('保存公众号信息');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
clearButton.setButtonText('保存中...');
|
||||||
|
if (await this.encrypt()) {
|
||||||
|
isClear = true;
|
||||||
|
isRealClear = false;
|
||||||
|
clearButton.setButtonText('清空公众号信息');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
clearButton.setButtonText('保存公众号信息');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const testButton = new ButtonComponent(wechatSetting.controlEl);
|
||||||
|
testButton.setButtonText('测试公众号');
|
||||||
|
testButton.onClick(async () => {
|
||||||
|
testButton.setButtonText('测试中...');
|
||||||
|
await this.testWXInfo();
|
||||||
|
testButton.setButtonText('测试公众号');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,9 @@
|
|||||||
* - 默认值初始化
|
* - 默认值初始化
|
||||||
* - loadSettings: 反序列化存储数据并兼容旧字段
|
* - loadSettings: 反序列化存储数据并兼容旧字段
|
||||||
* - allSettings: 统一导出用于持久化
|
* - allSettings: 统一导出用于持久化
|
||||||
* - 会员 / 授权信息校验(isAuthKeyVaild)
|
|
||||||
* - 批量发布预设 / 图片处理 / 样式控制等选项
|
* - 批量发布预设 / 图片处理 / 样式控制等选项
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { wxKeyInfo } from './wechat/weixin-api';
|
|
||||||
|
|
||||||
export class NMPSettings {
|
export class NMPSettings {
|
||||||
defaultStyle: string;
|
defaultStyle: string;
|
||||||
defaultHighlight: string;
|
defaultHighlight: string;
|
||||||
@@ -26,6 +23,7 @@ export class NMPSettings {
|
|||||||
math: string;
|
math: string;
|
||||||
expireat: Date | null = null;
|
expireat: Date | null = null;
|
||||||
isVip: boolean = false;
|
isVip: boolean = false;
|
||||||
|
authStatus: string = '';
|
||||||
baseCSS: string;
|
baseCSS: string;
|
||||||
watermark: string;
|
watermark: string;
|
||||||
useFigcaption: boolean;
|
useFigcaption: boolean;
|
||||||
@@ -216,21 +214,12 @@ export class NMPSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getExpiredDate() {
|
getExpiredDate() {
|
||||||
if (this.authKey.length == 0) return;
|
this.isVip = false;
|
||||||
wxKeyInfo(this.authKey).then((res) => {
|
this.expireat = null;
|
||||||
if (res.status == 200) {
|
this.authStatus = '';
|
||||||
if (res.json.vip) {
|
|
||||||
this.isVip = true;
|
|
||||||
}
|
|
||||||
this.expireat = new Date(res.json.expireat);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthKeyVaild() {
|
isAuthKeyVaild() {
|
||||||
if (this.authKey.length == 0) return false;
|
return true;
|
||||||
if (this.isVip) return true;
|
|
||||||
if (this.expireat == null) return false;
|
|
||||||
return this.expireat > new Date();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,40 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { requestUrl, RequestUrlParam, getBlobArrayBuffer } from "obsidian";
|
import { requestUrl, RequestUrlParam, getBlobArrayBuffer } from "obsidian";
|
||||||
|
import { API_HOST } from "../api-host";
|
||||||
|
|
||||||
const PluginHost = 'https://obplugin.sunboshi.tech';
|
const PluginHost = API_HOST;
|
||||||
|
|
||||||
|
function normalizeMessageFields(json: Record<string, unknown> | undefined) {
|
||||||
|
if (!json) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = { ...json } as Record<string, unknown>;
|
||||||
|
if (typeof normalized.message !== 'string') {
|
||||||
|
if (typeof normalized.msg === 'string') {
|
||||||
|
normalized.message = normalized.msg;
|
||||||
|
} else if (typeof normalized.errmsg === 'string') {
|
||||||
|
normalized.message = normalized.errmsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResponseStatus(status: number, json: Record<string, unknown>) {
|
||||||
|
const code = typeof json.code === 'number' ? json.code : 0;
|
||||||
|
if (status === 200 && code !== 0) {
|
||||||
|
return 400;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取token
|
// 获取token
|
||||||
export async function wxGetToken(authkey:string, appid:string, secret:string) {
|
export async function wxGetToken(authkey:string, appid:string, secret:string) {
|
||||||
const url = PluginHost + '/v1/wx/token';
|
const url = PluginHost + '/v1/wx/token';
|
||||||
const body = {
|
const body: Record<string, string> = { appid, secret };
|
||||||
authkey,
|
if (authkey.trim().length > 0) {
|
||||||
appid,
|
body.authkey = authkey;
|
||||||
secret
|
|
||||||
}
|
}
|
||||||
const res = await requestUrl({
|
const res = await requestUrl({
|
||||||
url,
|
url,
|
||||||
@@ -24,15 +48,116 @@ export async function wxGetToken(authkey:string, appid:string, secret:string) {
|
|||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
return res;
|
|
||||||
|
const normalizedJson = normalizeMessageFields(res.json as Record<string, unknown> | undefined);
|
||||||
|
if (typeof normalizedJson.token !== 'string' && typeof normalizedJson.access_token === 'string') {
|
||||||
|
normalizedJson.token = normalizedJson.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
status: normalizeResponseStatus(res.status, normalizedJson),
|
||||||
|
json: normalizedJson
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSecretCandidates(secret: string): string[] {
|
||||||
|
const normalizedSecret = secret.trim();
|
||||||
|
if (normalizedSecret.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = [normalizedSecret];
|
||||||
|
if (normalizedSecret.startsWith('SECRET')) {
|
||||||
|
candidates.push(normalizedSecret.slice('SECRET'.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(candidates.filter(candidate => candidate.length > 0))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRetryWithNextSecretCandidate(res: { status: number; json?: Record<string, unknown> }): boolean {
|
||||||
|
if (res.status === 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = typeof res.json?.code === 'number' ? res.json.code : undefined;
|
||||||
|
const message = [res.json?.message, res.json?.msg, res.json?.errmsg]
|
||||||
|
.filter((item): item is string => typeof item === 'string' && item.length > 0)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
return code === 40125 || (message.includes('appsecret') && message.includes('decode')) || message.includes('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikePlainAppSecret(secret: string): boolean {
|
||||||
|
const normalizedSecret = secret.trim();
|
||||||
|
return /^[A-Za-z0-9]{24,64}$/.test(normalizedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wxEncryptSingleSecret(authkey: string, appid: string, secret: string): Promise<string> {
|
||||||
|
const res = await wxEncrypt(authkey, [{ name: '', appid, secret }]);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedSecret = res.json?.[appid];
|
||||||
|
return typeof encryptedSecret === 'string' ? encryptedSecret : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容历史/现有两种 secret 存储格式:
|
||||||
|
* 1. 直接存储服务端返回的加密串
|
||||||
|
* 2. 以 SECRET 前缀包裹的加密串
|
||||||
|
*/
|
||||||
|
export async function wxGetTokenWithFallback(authkey: string, appid: string, secret: string) {
|
||||||
|
const candidates = buildSecretCandidates(secret);
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return await wxGetToken(authkey, appid, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstRes = await wxGetToken(authkey, appid, candidates[0]);
|
||||||
|
let lastRes = firstRes;
|
||||||
|
|
||||||
|
if (!shouldRetryWithNextSecretCandidate(lastRes)) {
|
||||||
|
return lastRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidate of candidates.slice(1)) {
|
||||||
|
lastRes = await wxGetToken(authkey, appid, candidate);
|
||||||
|
|
||||||
|
if (lastRes.status === 200) {
|
||||||
|
return lastRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRetryWithNextSecretCandidate(lastRes)) {
|
||||||
|
return lastRes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainSecretCandidate = secret.trim().startsWith('SECRET')
|
||||||
|
? secret.trim().slice('SECRET'.length)
|
||||||
|
: secret.trim();
|
||||||
|
|
||||||
|
if (looksLikePlainAppSecret(plainSecretCandidate)) {
|
||||||
|
const encryptedSecret = await wxEncryptSingleSecret(authkey, appid, plainSecretCandidate);
|
||||||
|
if (encryptedSecret.length > 0) {
|
||||||
|
const encryptedRes = await wxGetToken(authkey, appid, encryptedSecret);
|
||||||
|
if (encryptedRes.status === 200 || !shouldRetryWithNextSecretCandidate(encryptedRes)) {
|
||||||
|
return encryptedRes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function wxEncrypt(authkey:string, wechat:any[]) {
|
export async function wxEncrypt(authkey:string, wechat:any[]) {
|
||||||
const url = PluginHost + '/v1/wx/encrypt';
|
const url = PluginHost + '/v1/wx/encrypt';
|
||||||
const body = JSON.stringify({
|
const payload: Record<string, unknown> = { wechat };
|
||||||
authkey,
|
if (authkey.trim().length > 0) {
|
||||||
wechat
|
payload.authkey = authkey;
|
||||||
});
|
}
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
const res = await requestUrl({
|
const res = await requestUrl({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -40,7 +165,20 @@ export async function wxEncrypt(authkey:string, wechat:any[]) {
|
|||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: body
|
body: body
|
||||||
});
|
});
|
||||||
return res
|
|
||||||
|
const normalizedJson = normalizeMessageFields(res.json as Record<string, unknown> | undefined);
|
||||||
|
const encryptedWechat = Array.isArray(normalizedJson.wechat) ? normalizedJson.wechat : [];
|
||||||
|
for (const item of encryptedWechat) {
|
||||||
|
if (item && typeof item.appid === 'string' && typeof item.secret === 'string') {
|
||||||
|
normalizedJson[item.appid] = item.secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
status: normalizeResponseStatus(res.status, normalizedJson),
|
||||||
|
json: normalizedJson
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function wxKeyInfo(authkey:string) {
|
export async function wxKeyInfo(authkey:string) {
|
||||||
@@ -51,22 +189,26 @@ export async function wxKeyInfo(authkey:string) {
|
|||||||
throw: false,
|
throw: false,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
});
|
});
|
||||||
return res
|
const normalizedJson = normalizeMessageFields(res.json as Record<string, unknown> | undefined);
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
status: normalizeResponseStatus(res.status, normalizedJson),
|
||||||
|
json: normalizedJson
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function wxWidget(authkey: string, params: string) {
|
export async function wxWidget(authkey: string, params: string) {
|
||||||
const host = 'https://obplugin.sunboshi.tech';
|
const host = API_HOST;
|
||||||
const path = '/math/widget';
|
const path = '/math/widget';
|
||||||
const url = `${host}${path}`;
|
const url = `${host}${path}`;
|
||||||
try {
|
try {
|
||||||
|
const headers = authkey.trim().length > 0 ? { authkey } : undefined;
|
||||||
const res = await requestUrl({
|
const res = await requestUrl({
|
||||||
url,
|
url,
|
||||||
throw: false,
|
throw: false,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
headers: {
|
headers,
|
||||||
authkey
|
|
||||||
},
|
|
||||||
body: params
|
body: params
|
||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -210,4 +352,4 @@ export async function wxBatchGetMaterial(token: string, type: string, offset: nu
|
|||||||
});
|
});
|
||||||
|
|
||||||
return await res.json;
|
return await res.json;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { App, Modal, MarkdownView } from "obsidian";
|
import { App, Modal, MarkdownView } from "obsidian";
|
||||||
|
import { API_HOST } from "./api-host";
|
||||||
import { uevent } from "./utils";
|
import { uevent } from "./utils";
|
||||||
|
|
||||||
export class WidgetsModal extends Modal {
|
export class WidgetsModal extends Modal {
|
||||||
listener: any = null;
|
listener: any = null;
|
||||||
url: string = 'https://widgets.sunboshi.tech';
|
url: string = `${API_HOST}/widgets`;
|
||||||
constructor(app: App) {
|
constructor(app: App) {
|
||||||
super(app);
|
super(app);
|
||||||
}
|
}
|
||||||
@@ -57,4 +58,4 @@ export class WidgetsModal extends Modal {
|
|||||||
let { contentEl } = this;
|
let { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -749,6 +749,13 @@ label:hover { color: var(--c-primary); }
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note2any-settings-action-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-mpcard-wrapper {
|
.note-mpcard-wrapper {
|
||||||
margin: 20px 20px;
|
margin: 20px 20px;
|
||||||
background-color: rgb(250, 250, 250);
|
background-color: rgb(250, 250, 250);
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。
|
|||||||
统一CSS处理流程。
|
统一CSS处理流程。
|
||||||
添加平台无关的基础样式。
|
添加平台无关的基础样式。
|
||||||
|
|
||||||
|
3. token失败,因为微信认证使用sunboshi的api服务器。改为biboer.cn
|
||||||
|
|
||||||
## 思路
|
## 思路
|
||||||
1. 网上图片模版,让AI快速生成css themes主题。快速套用。‼️
|
1. 网上图片模版,让AI快速生成css themes主题。快速套用。‼️
|
||||||
**样式和功能必须结构,样式必须可以0基础,快速选择,让用户可以选择足够多样式(AI生成)。**
|
**样式和功能必须结构,样式必须可以0基础,快速选择,让用户可以选择足够多样式(AI生成)。**
|
||||||
|
|||||||
Reference in New Issue
Block a user