update at 2026-04-21 17:55:41

This commit is contained in:
douboer
2026-04-21 17:55:41 +08:00
parent dda7eaeec1
commit 6199ec79d4
37 changed files with 1106 additions and 174 deletions

View File

@@ -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}}}

View 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

View 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

View 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

View File

View File

@@ -1,6 +1,9 @@
#!/bin/bash
set -e # 出错立即退出
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 1. 构建
MODE="$1"
BUILD_CMD=(npm run build)
@@ -55,5 +58,17 @@ else
echo
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

View File

@@ -16,6 +16,7 @@
"build": "npm run build:bundle --",
"build:obf": "OBFUSCATE=1 npm run build:bundle --",
"build:bundle": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"build:server": "tsc -p server/tsconfig.json",
"download": "node tools/download.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},

20
run.sh
View File

@@ -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

1
runit.sh Symbolic link
View File

@@ -0,0 +1 @@
/Users/gavin/tools/runit.sh

28
server/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
/**
* 运行时 API Host。
* 公众号鉴权、数学公式渲染、Excalidraw 转换等后端接口统一走这里。
*/
export const API_HOST = 'https://api.biboer.cn';

View File

@@ -9,7 +9,7 @@ import { UploadImageToWx } from './imagelib';
import { NMPSettings } from './settings';
import AssetsManager from './assets';
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 { MarkedParser } from './markdown/parser';
import { LocalImageManager, LocalFile } from './markdown/local-file';
@@ -535,23 +535,19 @@ export class ArticleRender implements MDRendererCallback {
async getToken(appid: string) {
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) {
const data = res.json;
throw new Error('获取token失败: ' + data.message);
throw new Error('获取token失败: ' + message);
}
const token = res.json.token;
const token = typeof res.json?.token === 'string' ? res.json.token : '';
if (token === '') {
throw new Error('获取token失败: ' + res.json.message);
throw new Error('获取token失败: ' + message);
}
return token;
}
async uploadImages(appid: string) {
if (!this.settings.authKey) {
throw new Error('请先设置注册码AuthKey');
}
let metadata = this.getMetadata();
if (metadata.appid) {
appid = metadata.appid;
@@ -590,17 +586,13 @@ export class ArticleRender implements MDRendererCallback {
getSecret(appid: string) {
for (const wx of this.settings.wxInfo) {
if (wx.appid === appid) {
return wx.secret.replace('SECRET', '');
return wx.secret;
}
}
return '';
}
async postArticle(appid:string, localCover: File | null = null) {
if (!this.settings.authKey) {
throw new Error('请先设置注册码AuthKey');
}
let metadata = this.getMetadata();
if (metadata.appid) {
appid = metadata.appid;
@@ -675,10 +667,6 @@ export class ArticleRender implements MDRendererCallback {
}
async postImages(appid: string) {
if (!this.settings.authKey) {
throw new Error('请先设置注册码AuthKey');
}
let metadata = this.getMetadata();
if (metadata.appid) {
appid = metadata.appid;

View File

@@ -14,7 +14,6 @@ export interface PlatformConfig {
export interface WechatConfig extends PlatformConfig {
wxInfo: Array<{name: string, appid: string, secret: string}>;
authKey: string;
}
export interface XhsConfig extends PlatformConfig {
@@ -61,7 +60,6 @@ export class ConfigManager {
name: 'WeChat',
enabled: this.settings.wxInfo.length > 0,
wxInfo: this.settings.wxInfo,
authKey: this.settings.authKey,
validate: () => this.validateWechatConfig()
};
}
@@ -94,7 +92,6 @@ export class ConfigManager {
private validateWechatConfig(): boolean {
try {
ErrorHandler.validateRequired(this.settings.authKey, 'WeChat 授权密钥');
if (this.settings.wxInfo.length === 0) {
throw new ValidationError('WeChat 配置信息不能为空');
}
@@ -131,7 +128,7 @@ export class ConfigManager {
isTokenValid(platform: 'wechat'): boolean {
if (platform === 'wechat') {
return this.settings.isAuthKeyVaild();
return true;
}
return false;
}

View File

@@ -3,6 +3,7 @@
import { Token, Tokens, MarkedExtension } from "marked";
import { Notice, TAbstractFile, TFile, Vault, MarkdownView, requestUrl, Platform } from "obsidian";
import { Extension } from "./extension";
import { API_HOST } from "../api-host";
import { NMPSettings } from "../settings";
import { IsImageLibReady, PrepareImageLib, WebpToJPG, UploadImageToWx } from "../imagelib";
import { convertJpegIfNeeded } from "../exif-orientation";
@@ -644,7 +645,7 @@ export class LocalFile extends Extension{
}
static async getExcalidrawUrl(data: string) {
const url = 'https://obplugin.sunboshi.tech/math/excalidraw';
const url = `${API_HOST}/math/excalidraw`;
const req = await requestUrl({
url,
method: 'POST',

View File

@@ -4,6 +4,7 @@ import { MarkedExtension, Token, Tokens } from "marked";
import { requestUrl } from "obsidian";
import { Extension } from "./extension";
import { NMPSettings } from "src/settings";
import { API_HOST } from "../api-host";
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/;
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
@@ -15,7 +16,7 @@ export function cleanMathCache() {
}
export class MathRendererQueue {
private host = 'https://obplugin.sunboshi.tech';
private host = API_HOST;
private static instance: MathRendererQueue;
private mathIndex: number = 0;

View File

@@ -3,9 +3,9 @@
* 作用Obsidian 设置面板集成,提供界面化配置入口。
*/
import { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian';
import { App, ButtonComponent, TextAreaComponent, PluginSettingTab, Setting, Notice } from 'obsidian';
import Note2AnyPlugin from './main';
import { wxGetToken, wxEncrypt } from './wechat/weixin-api';
import { wxGetTokenWithFallback, wxEncrypt } from './wechat/weixin-api';
import { cleanMathCache } from './markdown/math';
import { NMPSettings } from './settings';
import { DocModal } from './doc-modal';
@@ -41,11 +41,6 @@ export class Note2AnySettingTab extends PluginSettingTab {
}
async testWXInfo() {
const authKey = this.settings.authKey;
if (authKey.length == 0) {
new Notice('请先设置authKey');
return;
}
const wxInfo = this.settings.wxInfo;
if (wxInfo.length == 0) {
new Notice('请先设置公众号信息');
@@ -54,11 +49,11 @@ export class Note2AnySettingTab extends PluginSettingTab {
try {
const docUrl = 'https://mp.weixin.qq.com/s/rk5CTPGr5ftly8PtYgSjCQ';
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) {
const data = res.json;
const { code, message } = data;
let content = message;
let content = typeof message === 'string' ? message : '未知错误';
if (code === 50002) {
content = '用户受限,可能是您的公众号被冻结或注销,请联系微信客服处理';
}
@@ -74,7 +69,8 @@ export class Note2AnySettingTab extends PluginSettingTab {
}
const data = res.json;
if (data.token.length == 0) {
const token = typeof data.token === 'string' ? data.token : '';
if (token.length == 0) {
new Notice(`${wx.name}|${wx.appid} 测试失败`);
break
}
@@ -119,16 +115,18 @@ export class Note2AnySettingTab extends PluginSettingTab {
}
try {
const res = await wxEncrypt(this.settings.authKey, wechat);
const res = await wxEncrypt('', wechat);
if (res.status != 200) {
const data = res.json;
new Notice(`${data.message}`);
const message = typeof data.message === 'string' ? data.message : '保存失败';
new Notice(message);
return false;
}
const data = res.json;
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;
@@ -391,10 +389,9 @@ export class Note2AnySettingTab extends PluginSettingTab {
.inputEl.setAttr('style', 'width: 520px; height: 60px;');
});
const customCSSDoc = '使用指南:<a href="https://sunboshi.tech/customcss">https://sunboshi.tech/customcss</a>';
new Setting(panel)
.setName('自定义CSS笔记')
.setDesc(sanitizeHTMLToDom(customCSSDoc))
.setDesc('填写用于追加自定义样式的笔记标题')
.addText(text => {
text.setPlaceholder('请输入自定义CSS笔记标题')
.setValue(this.settings.customCSSNote)
@@ -406,10 +403,9 @@ export class Note2AnySettingTab extends PluginSettingTab {
.inputEl.setAttr('style', 'width: 320px;');
});
const expertDoc = '使用指南:<a href="https://sunboshi.tech/expert">https://sunboshi.tech/expert</a>';
new Setting(panel)
.setName('专家设置笔记')
.setDesc(sanitizeHTMLToDom(expertDoc))
.setDesc('填写用于覆盖高级渲染配置的笔记标题')
.addText(text => {
text.setPlaceholder('请输入专家设置笔记标题')
.setValue(this.settings.expertSettingsNote)
@@ -501,34 +497,11 @@ export class Note2AnySettingTab extends PluginSettingTab {
}
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 isRealClear = false;
const buttonText = isClear ? '清空公众号信息' : '保存公众号信息';
new Setting(panel)
const wechatSetting = new Setting(panel)
.setName('公众号信息')
.addTextArea(text => {
this.wxTextArea = text;
@@ -538,45 +511,43 @@ export class Note2AnySettingTab extends PluginSettingTab {
this.wxInfo = value;
})
.inputEl.setAttr('style', 'width: 520px; height: 120px;');
})
.addButton(button => {
button.setButtonText(buttonText);
button.onClick(async () => {
});
wechatSetting.controlEl.addClass('note2any-settings-action-stack');
const clearButton = new ButtonComponent(wechatSetting.controlEl);
clearButton.setButtonText(buttonText);
clearButton.onClick(async () => {
if (isClear) {
isRealClear = true;
isClear = false;
button.setButtonText('确认清空?');
clearButton.setButtonText('确认清空?');
}
else if (isRealClear) {
isRealClear = false;
isClear = false;
this.clear();
button.setButtonText('保存公众号信息');
await this.clear();
clearButton.setButtonText('保存公众号信息');
}
else {
button.setButtonText('保存中...');
clearButton.setButtonText('保存中...');
if (await this.encrypt()) {
isClear = true;
isRealClear = false;
button.setButtonText('清空公众号信息');
clearButton.setButtonText('清空公众号信息');
}
else {
button.setButtonText('保存公众号信息');
clearButton.setButtonText('保存公众号信息');
}
}
});
})
.addButton(button => {
button.setButtonText('测试公众号');
button.onClick(async () => {
button.setButtonText('测试中...');
const testButton = new ButtonComponent(wechatSetting.controlEl);
testButton.setButtonText('测试公众号');
testButton.onClick(async () => {
testButton.setButtonText('测试中...');
await this.testWXInfo();
button.setButtonText('测试公众号');
testButton.setButtonText('测试公众号');
});
});
//
const helpEl = panel.createEl('div', { cls: 'setting-help-section' });
helpEl.createEl('h2', { text: '帮助文档', cls: 'setting-help-title' });
helpEl.createEl('a', { text: 'https://sunboshi.tech/doc', attr: { href: 'https://sunboshi.tech/doc' } });
}
}

View File

@@ -5,12 +5,9 @@
* - 默认值初始化
* - loadSettings: 反序列化存储数据并兼容旧字段
* - allSettings: 统一导出用于持久化
* - 会员 / 授权信息校验isAuthKeyVaild
* - 批量发布预设 / 图片处理 / 样式控制等选项
*/
import { wxKeyInfo } from './wechat/weixin-api';
export class NMPSettings {
defaultStyle: string;
defaultHighlight: string;
@@ -26,6 +23,7 @@ export class NMPSettings {
math: string;
expireat: Date | null = null;
isVip: boolean = false;
authStatus: string = '';
baseCSS: string;
watermark: string;
useFigcaption: boolean;
@@ -216,21 +214,12 @@ export class NMPSettings {
}
getExpiredDate() {
if (this.authKey.length == 0) return;
wxKeyInfo(this.authKey).then((res) => {
if (res.status == 200) {
if (res.json.vip) {
this.isVip = true;
}
this.expireat = new Date(res.json.expireat);
}
})
this.isVip = false;
this.expireat = null;
this.authStatus = '';
}
isAuthKeyVaild() {
if (this.authKey.length == 0) return false;
if (this.isVip) return true;
if (this.expireat == null) return false;
return this.expireat > new Date();
return true;
}
}

View File

@@ -6,16 +6,40 @@
*/
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
export async function wxGetToken(authkey:string, appid:string, secret:string) {
const url = PluginHost + '/v1/wx/token';
const body = {
authkey,
appid,
secret
const body: Record<string, string> = { appid, secret };
if (authkey.trim().length > 0) {
body.authkey = authkey;
}
const res = await requestUrl({
url,
@@ -24,15 +48,116 @@ export async function wxGetToken(authkey:string, appid:string, secret:string) {
contentType: 'application/json',
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[]) {
const url = PluginHost + '/v1/wx/encrypt';
const body = JSON.stringify({
authkey,
wechat
});
const payload: Record<string, unknown> = { wechat };
if (authkey.trim().length > 0) {
payload.authkey = authkey;
}
const body = JSON.stringify(payload);
const res = await requestUrl({
url: url,
method: 'POST',
@@ -40,7 +165,20 @@ export async function wxEncrypt(authkey:string, wechat:any[]) {
contentType: 'application/json',
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) {
@@ -51,22 +189,26 @@ export async function wxKeyInfo(authkey:string) {
throw: false,
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) {
const host = 'https://obplugin.sunboshi.tech';
const host = API_HOST;
const path = '/math/widget';
const url = `${host}${path}`;
try {
const headers = authkey.trim().length > 0 ? { authkey } : undefined;
const res = await requestUrl({
url,
throw: false,
method: 'POST',
contentType: 'application/json',
headers: {
authkey
},
headers,
body: params
})
if (res.status === 200) {

View File

@@ -4,11 +4,12 @@
*/
import { App, Modal, MarkdownView } from "obsidian";
import { API_HOST } from "./api-host";
import { uevent } from "./utils";
export class WidgetsModal extends Modal {
listener: any = null;
url: string = 'https://widgets.sunboshi.tech';
url: string = `${API_HOST}/widgets`;
constructor(app: App) {
super(app);
}

View File

@@ -749,6 +749,13 @@ label:hover { color: var(--c-primary); }
flex-direction: column;
}
.note2any-settings-action-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.note-mpcard-wrapper {
margin: 20px 20px;
background-color: rgb(250, 250, 250);

View File

@@ -180,6 +180,8 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
统一CSS处理流程。
添加平台无关的基础样式。
3. token失败因为微信认证使用sunboshi的api服务器。改为biboer.cn
## 思路
1. 网上图片模版让AI快速生成css themes主题。快速套用。‼
**样式和功能必须结构样式必须可以0基础快速选择让用户可以选择足够多样式(AI生成)。**