diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d35b621..7ee3510 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -190,7 +190,6 @@ jobs: fi WINDOWS_ARM64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("arm64.*\\.exe$"))][0] // ""')" MAC_ASSET="$(pick_asset "\\.dmg$")" - LINUX_DEB_ASSET="$(pick_asset "\\.deb$")" LINUX_TAR_ASSET="$(pick_asset "\\.tar\\.gz$")" LINUX_APPIMAGE_ASSET="$(pick_asset "\\.AppImage$")" @@ -204,7 +203,6 @@ jobs: WINDOWS_URL="$(build_link "$WINDOWS_ASSET")" WINDOWS_ARM64_URL="$(build_link "$WINDOWS_ARM64_ASSET")" MAC_URL="$(build_link "$MAC_ASSET")" - LINUX_DEB_URL="$(build_link "$LINUX_DEB_ASSET")" LINUX_TAR_URL="$(build_link "$LINUX_TAR_ASSET")" LINUX_APPIMAGE_URL="$(build_link "$LINUX_APPIMAGE_ASSET")" @@ -219,7 +217,6 @@ jobs: - Windows x64(Win10+): ${WINDOWS_URL:-$RELEASE_PAGE} - Windows arm64: ${WINDOWS_ARM64_URL:-$RELEASE_PAGE} - macOS(M系列芯片): ${MAC_URL:-$RELEASE_PAGE} - - Linux (.deb) (即将废弃): ${LINUX_DEB_URL:-$RELEASE_PAGE} - Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE} - linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE} diff --git a/README.md b/README.md index 7c4637f..bfd936a 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,15 @@ WeFlow 是一个**完全本地**的微信**实时**聊天记录查看、分析 |------|----------|--------| | Windows | Windows10+、x64(amd64) | `.exe` | | macOS | Apple Silicon(M 系列,arm64) | `.dmg` | -| Linux | x64 设备(amd64) | `.deb`、`.tar.gz` | +| Linux | x64 设备(amd64) | `.AppImage`、`.tar.gz` | ## 快速开始 若你只想使用成品版本,可前往 [Releases](https://github.com/hicccc77/WeFlow/releases) 下载并安装。 +> ArchLinux 用户可以选择 `yay -S weflow` 快速安装 + ## 详细功能清单 当前版本已支持以下能力: diff --git a/docs/HTTP-API.md b/docs/HTTP-API.md index ca2a89a..052dd8a 100644 --- a/docs/HTTP-API.md +++ b/docs/HTTP-API.md @@ -1,6 +1,6 @@ # WeFlow HTTP API / Push 文档 -WeFlow 提供本地 HTTP API,便于外部脚本或工具读取聊天记录、会话、联系人、群成员和导出的媒体文件;也支持在检测到新消息后通过固定 SSE 地址主动推送消息事件。 +WeFlow 提供本地 HTTP API(已支持GET 和 POST请求),便于外部脚本或工具读取聊天记录、会话、联系人、群成员和导出的媒体文件;也支持在检测到新消息后通过固定 SSE 地址主动推送消息事件。 ## 启用方式 @@ -11,17 +11,27 @@ WeFlow 提供本地 HTTP API,便于外部脚本或工具读取聊天记录、 - 基础地址:`http://127.0.0.1:5031` - 可选开启 `主动推送`,检测到新收到的消息后会通过 `GET /api/v1/push/messages` 推送给 SSE 订阅端 +**状态记忆**:API 服务和主动推送的状态及端口会自动保存,重启 WeFlow 后会自动恢复运行。 + +## 鉴权规范 + +**鉴权规范 (Access Token)** 除健康检查接口外,所有 `/api/v1/*` 接口均受 Token 保护。支持三种传参方式(任选其一): + +1. **HTTP Header (推荐)**: `Authorization: Bearer <您的Token>` +2. **Query 参数**: `?access_token=<您的Token>`(SSE 长连接推荐此方式) +3. **JSON Body**: `{"access_token": "<您的Token>"}`(仅限 POST 请求) + ## 接口列表 -- `GET /health` -- `GET /api/v1/health` -- `GET /api/v1/push/messages` -- `GET /api/v1/messages` -- `GET /api/v1/messages/new` -- `GET /api/v1/sessions` -- `GET /api/v1/contacts` -- `GET /api/v1/group-members` -- `GET /api/v1/media/*` +- `GET|POST /health` +- `GET|POST /api/v1/health` +- `GET|POST /api/v1/push/messages` +- `GET|POST /api/v1/messages` +- `GET|POST /api/v1/messages/new` +- `GET|POST /api/v1/sessions` +- `GET|POST /api/v1/contacts` +- `GET|POST /api/v1/group-members` +- `GET|POST /api/v1/media/*` --- @@ -80,7 +90,7 @@ GET /api/v1/push/messages ### 示例 ```bash -curl -N "http://127.0.0.1:5031/api/v1/push/messages" +curl -N "http://127.0.0.1:5031/api/v1/push/messages?access_token=YOUR_TOKEN ``` 示例事件: @@ -94,6 +104,8 @@ data: {"event":"message.new","sessionId":"xxx@chatroom","messageKey":"server:123 ## 3. 获取消息 +> 当使用 POST 时,请将参数放在 JSON Body 中(Content-Type: application/json) + 读取指定会话的消息,支持原始 JSON 和 ChatLab 格式。 **请求** @@ -231,6 +243,8 @@ curl "http://127.0.0.1:5031/api/v1/messages?talker=xxx@chatroom&media=1&image=1& ## 4. 获取会话列表 +> 当使用 POST 时,请将参数放在 JSON Body 中(Content-Type: application/json) + **请求** ```http @@ -276,6 +290,8 @@ GET /api/v1/sessions ## 5. 获取联系人列表 +> 当使用 POST 时,请将参数放在 JSON Body 中(Content-Type: application/json) + **请求** ```http @@ -325,6 +341,8 @@ GET /api/v1/contacts ## 6. 获取群成员列表 +> 当使用 POST 时,请将参数放在 JSON Body 中(Content-Type: application/json) + 返回群成员的 `wxid`、群昵称、备注、微信号等信息。 **请求** @@ -417,6 +435,8 @@ curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom&include ## 7. 访问导出媒体 +> 当使用 POST 时,请将参数放在 JSON Body 中(Content-Type: application/json) + 通过消息接口启用 `media=1` 后,接口会先把图片、语音、视频、表情导出到本地缓存目录,再返回可访问的 HTTP 地址。 **请求** @@ -461,19 +481,23 @@ curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/emojis/emoji_300.gif" ### PowerShell ```powershell -Invoke-RestMethod http://127.0.0.1:5031/health -Invoke-RestMethod http://127.0.0.1:5031/api/v1/sessions -Invoke-RestMethod "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=10" -Invoke-RestMethod "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom&includeMessageCounts=1" +$headers = @{ "Authorization" = "Bearer YOUR_TOKEN" } +$body = @{ talker = "wxid_xxx"; limit = 10 } | ConvertTo-Json + +Invoke-RestMethod -Uri "http://127.0.0.1:5031/api/v1/messages" -Method POST -Headers $headers -Body $body -ContentType "application/json" ``` ### cURL ```bash -curl http://127.0.0.1:5031/health -curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&chatlab=1" -curl "http://127.0.0.1:5031/api/v1/contacts?keyword=张三" -curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom" +# GET 带 Token Header +curl -H "Authorization: Bearer YOUR_TOKEN" "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx" + +# POST 带 JSON Body +curl -X POST http://127.0.0.1:5031/api/v1/messages \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"talker": "xxx@chatroom", "chatlab": true}' ``` ### Python @@ -482,19 +506,21 @@ curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom" import requests BASE_URL = "http://127.0.0.1:5031" +headers = {"Authorization": "Bearer YOUR_TOKEN", "Content-Type": "application/json"} -messages = requests.get( - f"{BASE_URL}/api/v1/messages", - params={"talker": "xxx@chatroom", "limit": 50} +# POST 方式获取消息 +messages = requests.post( + f"{BASE_URL}/api/v1/messages", + json={"talker": "xxx@chatroom", "limit": 50}, + headers=headers ).json() +# GET 方式获取群成员 members = requests.get( f"{BASE_URL}/api/v1/group-members", - params={"chatroomId": "xxx@chatroom", "includeMessageCounts": 1} + params={"chatroomId": "xxx@chatroom", "includeMessageCounts": 1}, + headers=headers ).json() - -print(messages) -print(members) ``` --- diff --git a/electron/main.ts b/electron/main.ts index 6ac7211..c0a6aa6 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2825,6 +2825,8 @@ app.whenReady().then(async () => { // 启动时检测更新(不阻塞启动) checkForUpdatesOnStartup() + await httpService.autoStart() + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createWindow() diff --git a/electron/services/config.ts b/electron/services/config.ts index c293ee1..47939bf 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -52,13 +52,16 @@ interface ConfigSchema { notificationFilterMode: 'all' | 'whitelist' | 'blacklist' notificationFilterList: string[] messagePushEnabled: boolean + httpApiEnabled: boolean + httpApiPort: number + httpApiToken: string windowCloseBehavior: 'ask' | 'tray' | 'quit' quoteLayout: 'quote-top' | 'quote-bottom' wordCloudExcludeWords: string[] } // 需要 safeStorage 加密的字段(普通模式) -const ENCRYPTED_STRING_KEYS: Set = new Set(['decryptKey', 'imageAesKey', 'authPassword']) +const ENCRYPTED_STRING_KEYS: Set = new Set(['decryptKey', 'imageAesKey', 'authPassword', 'httpApiToken']) const ENCRYPTED_BOOL_KEYS: Set = new Set(['authEnabled', 'authUseHello']) const ENCRYPTED_NUMBER_KEYS: Set = new Set(['imageXorKey']) @@ -119,6 +122,9 @@ export class ConfigService { notificationPosition: 'top-right', notificationFilterMode: 'all', notificationFilterList: [], + httpApiToken: '', + httpApiEnabled: false, + httpApiPort: 5031, messagePushEnabled: false, windowCloseBehavior: 'ask', quoteLayout: 'quote-top', @@ -662,11 +668,9 @@ export class ConfigService { // 即使 authEnabled 被删除/篡改,如果密钥是 lock: 格式,说明曾开启过应用锁 const rawDecryptKey: any = this.store.get('decryptKey') - if (typeof rawDecryptKey === 'string' && rawDecryptKey.startsWith(LOCK_PREFIX)) { - return true - } + return typeof rawDecryptKey === 'string' && rawDecryptKey.startsWith(LOCK_PREFIX); + - return false } // === 工具方法 === diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts index b59a014..c67770e 100644 --- a/electron/services/httpService.ts +++ b/electron/services/httpService.ts @@ -246,49 +246,115 @@ class HttpService { } } - /** - * 处理 HTTP 请求 - */ - private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { - // 设置 CORS 头 - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') - - if (req.method === 'OPTIONS') { - res.writeHead(204) - res.end() - return - } - - const url = new URL(req.url || '/', `http://127.0.0.1:${this.port}`) - const pathname = url.pathname - - try { - // 路由处理 - if (pathname === '/health' || pathname === '/api/v1/health') { - this.sendJson(res, { status: 'ok' }) - } else if (pathname === '/api/v1/push/messages') { - this.handleMessagePushStream(req, res) - } else if (pathname === '/api/v1/messages') { - await this.handleMessages(url, res) - } else if (pathname === '/api/v1/sessions') { - await this.handleSessions(url, res) - } else if (pathname === '/api/v1/contacts') { - await this.handleContacts(url, res) - } else if (pathname === '/api/v1/group-members') { - await this.handleGroupMembers(url, res) - } else if (pathname.startsWith('/api/v1/media/')) { - this.handleMediaRequest(pathname, res) - } else { - this.sendError(res, 404, 'Not Found') + async autoStart(): Promise { + const enabled = this.configService.get('httpApiEnabled') + if (enabled) { + const port = Number(this.configService.get('httpApiPort')) || 5031 + try { + await this.start(port) + console.log(`[HttpService] Auto-started on port ${port}`) + } catch (err) { + console.error('[HttpService] Auto-start failed:', err) } - } catch (error) { - console.error('[HttpService] Request error:', error) - this.sendError(res, 500, String(error)) } } + /** + * 解析 POST 请求的 JSON Body + */ + private async parseBody(req: http.IncomingMessage): Promise> { + if (req.method !== 'POST') return {} + return new Promise((resolve) => { + let body = '' + req.on('data', chunk => { body += chunk.toString() }) + req.on('end', () => { + try { + resolve(JSON.parse(body)) + } catch { + resolve({}) + } + }) + req.on('error', () => resolve({})) + }) + } + + /** + * 鉴权拦截器 + */ + private verifyToken(req: http.IncomingMessage, url: URL, body: Record): boolean { + const expectedToken = String(this.configService.get('httpApiToken') || '').trim() + if (!expectedToken) return true + + const authHeader = req.headers.authorization + if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) { + const token = authHeader.substring(7).trim() + if (token === expectedToken) return true + } + + const queryToken = url.searchParams.get('access_token') + if (queryToken && queryToken.trim() === expectedToken) return true + + const bodyToken = body['access_token'] + return !!(bodyToken && String(bodyToken).trim() === expectedToken); + + + } + + /** + * 处理 HTTP 请求 (重构后) + */ + private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') + + if (req.method === 'OPTIONS') { + res.writeHead(204) + res.end() + return + } + + const url = new URL(req.url || '/', `http://127.0.0.1:${this.port}`) + const pathname = url.pathname + + try { + const bodyParams = await this.parseBody(req) + + for (const [key, value] of Object.entries(bodyParams)) { + if (!url.searchParams.has(key)) { + url.searchParams.set(key, String(value)) + } + } + + if (pathname !== '/health' && pathname !== '/api/v1/health') { + if (!this.verifyToken(req, url, bodyParams)) { + this.sendError(res, 401, 'Unauthorized: Invalid or missing access_token') + return + } + } + + if (pathname === '/health' || pathname === '/api/v1/health') { + this.sendJson(res, { status: 'ok' }) + } else if (pathname === '/api/v1/push/messages') { + this.handleMessagePushStream(req, res) + } else if (pathname === '/api/v1/messages') { + await this.handleMessages(url, res) + } else if (pathname === '/api/v1/sessions') { + await this.handleSessions(url, res) + } else if (pathname === '/api/v1/contacts') { + await this.handleContacts(url, res) + } else if (pathname === '/api/v1/group-members') { + await this.handleGroupMembers(url, res) + } else if (pathname.startsWith('/api/v1/media/')) { + this.handleMediaRequest(pathname, res) + } else { + this.sendError(res, 404, 'Not Found') + } + } catch (error) { + console.error('[HttpService] Request error:', error) + this.sendError(res, 500, String(error)) + } + } private startMessagePushHeartbeat(): void { if (this.messagePushHeartbeatTimer) return this.messagePushHeartbeatTimer = setInterval(() => { diff --git a/package.json b/package.json index 0b237bc..6b32450 100644 --- a/package.json +++ b/package.json @@ -96,12 +96,17 @@ "icon": "public/icon.png", "target": [ "appimage", - "deb", "tar.gz" ], "category": "Utility", "executableName": "weflow", - "synopsis": "WeFlow for Linux" + "synopsis": "WeFlow for Linux", + "extraFiles": [ + { + "from": "resources/linux/install.sh", + "to": "install.sh" + } + ] }, "nsis": { "oneClick": false, diff --git a/resources/linux/install.sh b/resources/linux/install.sh new file mode 100644 index 0000000..eacc714 --- /dev/null +++ b/resources/linux/install.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +APP_NAME="weflow" +APP_EXEC="weflow" +OPT_DIR="/opt/$APP_NAME" +BIN_LINK="/usr/bin/$APP_NAME" +DESKTOP_DIR="/usr/share/applications" +ICON_DIR="/usr/share/pixmaps" + +if [ "$EUID" -ne 0 ]; then + echo "❌ 请使用 root 权限运行此脚本 (例如: sudo ./install.sh)" + exit 1 +fi + +echo "🚀 开始安装 $APP_NAME..." + +echo "📦 正在复制文件到 $OPT_DIR..." +rm -rf "$OPT_DIR" +mkdir -p "$OPT_DIR" +cp -r ./* "$OPT_DIR/" +chmod -R 755 "$OPT_DIR" +chmod +x "$OPT_DIR/$APP_EXEC" + +echo "🔗 正在创建软链接 $BIN_LINK..." +ln -sf "$OPT_DIR/$APP_EXEC" "$BIN_LINK" + +echo "📝 正在创建桌面快捷方式..." +cat <"$DESKTOP_DIR/${APP_NAME}.desktop" +[Desktop Entry] +Name=WeFlow +Exec=$OPT_DIR/$APP_EXEC %U +Terminal=false +Type=Application +Icon=$APP_NAME +StartupWMClass=WeFlow +Comment=A local WeChat database decryption and analysis tool +Categories=Utility; +EOF +chmod 644 "$DESKTOP_DIR/${APP_NAME}.desktop" + +echo "🖼️ 正在安装图标..." +if [ -f "$OPT_DIR/resources/icon.png" ]; then + cp "$OPT_DIR/resources/icon.png" "$ICON_DIR/${APP_NAME}.png" + chmod 644 "$ICON_DIR/${APP_NAME}.png" +elif [ -f "$OPT_DIR/icon.png" ]; then + cp "$OPT_DIR/icon.png" "$ICON_DIR/${APP_NAME}.png" + chmod 644 "$ICON_DIR/${APP_NAME}.png" +else + echo "⚠️ 警告: 未找到图标文件,跳过图标安装。" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + echo "🔄 更新桌面数据库..." + update-desktop-database "$DESKTOP_DIR" +fi + +echo "✅ 安装完成!你现在可以在应用菜单中找到 WeFlow,或者在终端输入 'weflow' 启动。" diff --git a/resources/xkey_helper_linux b/resources/xkey_helper_linux index 54f7cb3..8deb6d4 100755 Binary files a/resources/xkey_helper_linux and b/resources/xkey_helper_linux differ diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index d7c8899..de8d6f7 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -103,6 +103,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [whisperProgressData, setWhisperProgressData] = useState<{ downloaded: number; total: number; speed: number }>({ downloaded: 0, total: 0, speed: 0 }) const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null) + const [httpApiToken, setHttpApiToken] = useState('') + const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; @@ -111,6 +113,25 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; + const generateRandomToken = async () => { + // 生成 32 字符的十六进制随机字符串 (16 bytes) + const array = new Uint8Array(16) + crypto.getRandomValues(array) + const token = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('') + + setHttpApiToken(token) + await configService.setHttpApiToken(token) + showMessage('已生成并保存新的 Access Token', true) + } + + const clearApiToken = async () => { + setHttpApiToken('') + await configService.setHttpApiToken('') + showMessage('已清除 Access Token,API 将允许无鉴权访问', true) + } + + + const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false) const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) @@ -192,6 +213,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { checkWaylandStatus() }, []) + + // 检查 Hello 可用性 useEffect(() => { setHelloAvailable(isWindows) @@ -319,6 +342,13 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled() const savedAuthUseHello = await configService.getAuthUseHello() const savedIsLockMode = await window.electronAPI.auth.isLockMode() + + const savedHttpApiToken = await configService.getHttpApiToken() + if (savedHttpApiToken) setHttpApiToken(savedHttpApiToken) + + const savedApiPort = await configService.getHttpApiPort() + if (savedApiPort) setHttpApiPort(savedApiPort) + setAuthEnabled(savedAuthEnabled) setAuthUseHello(savedAuthUseHello) setIsLockMode(savedIsLockMode) @@ -361,6 +391,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAnalyticsConsent = await configService.getAnalyticsConsent() setAnalyticsConsent(savedAnalyticsConsent ?? false) + + // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { const defaultLanguages = ['zh'] @@ -1825,6 +1857,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { try { await window.electronAPI.http.stop() setHttpApiRunning(false) + await configService.setHttpApiEnabled(false) showMessage('API 服务已停止', true) } catch (e: any) { showMessage(`操作失败: ${e}`, false) @@ -1842,6 +1875,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { if (result.success) { setHttpApiRunning(true) if (result.port) setHttpApiPort(result.port) + + await configService.setHttpApiEnabled(true) + await configService.setHttpApiPort(result.port || httpApiPort) + showMessage(`API 服务已启动,端口 ${result.port}`, true) } else { showMessage(`启动失败: ${result.error}`, false) @@ -1890,17 +1927,51 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { API 服务监听的端口号(1024-65535) setHttpApiPort(parseInt(e.target.value, 10) || 5031)} - disabled={httpApiRunning} - style={{ width: 120 }} - min={1024} - max={65535} + type="number" + className="field-input" + value={httpApiPort} + onChange={(e) => { + const port = parseInt(e.target.value, 10) || 5031 + setHttpApiPort(port) + scheduleConfigSave('httpApiPort', () => configService.setHttpApiPort(port)) + }} + disabled={httpApiRunning} + style={{ width: 120 }} + min={1024} + max={65535} /> +
+ + + 设置后,请求头需携带 Authorization: Bearer <token>, + 或者参数中携带 ?access_token=<token> + +
+ { + const val = e.target.value + setHttpApiToken(val) + scheduleConfigSave('httpApiToken', () => configService.setHttpApiToken(val)) + }} + style={{ flex: 1, fontFamily: 'monospace' }} + /> + + {httpApiToken && ( + + )} +
+
+ {httpApiRunning && (
@@ -1956,18 +2027,18 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 外部软件连接这个 SSE 地址即可接收新消息推送;需要先开启上方 `HTTP API 服务`
diff --git a/src/services/config.ts b/src/services/config.ts index acbbdf9..7e184f4 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -64,6 +64,9 @@ export const CONFIG_KEYS = { NOTIFICATION_POSITION: 'notificationPosition', NOTIFICATION_FILTER_MODE: 'notificationFilterMode', NOTIFICATION_FILTER_LIST: 'notificationFilterList', + HTTP_API_TOKEN: 'httpApiToken', + HTTP_API_ENABLED: 'httpApiEnabled', + HTTP_API_PORT: 'httpApiPort', MESSAGE_PUSH_ENABLED: 'messagePushEnabled', WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior', QUOTE_LAYOUT: 'quoteLayout', @@ -117,6 +120,17 @@ export async function getDbPath(): Promise { return value as string | null } +// 获取api access_token +export async function getHttpApiToken(): Promise { + const value = await config.get(CONFIG_KEYS.HTTP_API_TOKEN) + return (value as string) || '' +} + +// 设置access_token +export async function setHttpApiToken(token: string): Promise { + await config.set(CONFIG_KEYS.HTTP_API_TOKEN, token) +} + // 设置数据库路径 export async function setDbPath(path: string): Promise { await config.set(CONFIG_KEYS.DB_PATH, path) @@ -1472,3 +1486,26 @@ export async function getAnalyticsDenyCount(): Promise { export async function setAnalyticsDenyCount(count: number): Promise { await config.set(CONFIG_KEYS.ANALYTICS_DENY_COUNT, count) } + + +// 获取 HTTP API 自动启动状态 +export async function getHttpApiEnabled(): Promise { + const value = await config.get(CONFIG_KEYS.HTTP_API_ENABLED) + return value === true +} + +// 设置 HTTP API 自动启动状态 +export async function setHttpApiEnabled(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.HTTP_API_ENABLED, enabled) +} + +// 获取 HTTP API 端口 +export async function getHttpApiPort(): Promise { + const value = await config.get(CONFIG_KEYS.HTTP_API_PORT) + return typeof value === 'number' ? value : 5031 +} + +// 设置 HTTP API 端口 +export async function setHttpApiPort(port: number): Promise { + await config.set(CONFIG_KEYS.HTTP_API_PORT, port) +}