chore: add and update gitignore rules for project files
add a comprehensive gitignore file to exclude unnecessary files like dependencies, runtime data, uploads, local configs and debug temporary files
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// ============================================================
|
||||
// data_gateway/auth.js - 对外 API 鉴权模块
|
||||
// 职责:校验 X-API-Id + X-API-Key,匹配 skill config.json 中的哈希
|
||||
// ============================================================
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const SKILLS_DIR = path.join(__dirname, 'skills')
|
||||
|
||||
// 缓存已加载的 skill 配置
|
||||
const skillConfigCache = new Map()
|
||||
|
||||
const loadSkillConfig = (skillId) => {
|
||||
if (skillConfigCache.has(skillId)) return skillConfigCache.get(skillId)
|
||||
const cfgPath = path.join(SKILLS_DIR, skillId, 'config.json')
|
||||
if (!fs.existsSync(cfgPath)) return null
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
|
||||
skillConfigCache.set(skillId, cfg)
|
||||
return cfg
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存(用于配置热更新)
|
||||
const refreshCache = (skillId) => {
|
||||
skillConfigCache.delete(skillId)
|
||||
return loadSkillConfig(skillId)
|
||||
}
|
||||
|
||||
// 枚举所有已启用的 skill
|
||||
const listEnabledSkills = () => {
|
||||
const result = []
|
||||
if (!fs.existsSync(SKILLS_DIR)) return result
|
||||
const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
||||
for (const d of dirs) {
|
||||
if (!d.isDirectory()) continue
|
||||
const cfg = loadSkillConfig(d.name)
|
||||
if (cfg && cfg.enabled !== false) {
|
||||
result.push({ id: cfg.id, name: cfg.name })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 校验外部 API 请求
|
||||
const verifyApiKey = (req) => {
|
||||
const apiId = String(req.headers['x-api-id'] || '').trim()
|
||||
const apiKey = String(req.headers['x-api-key'] || '').trim()
|
||||
if (!apiId || !apiKey) return { ok: false, error: 'missing api id or key' }
|
||||
|
||||
const cfg = loadSkillConfig(apiId)
|
||||
if (!cfg) return { ok: false, error: 'skill not found' }
|
||||
if (cfg.enabled === false) return { ok: false, error: 'skill disabled' }
|
||||
|
||||
const expectedHash = String(cfg.api_key_hash || '')
|
||||
if (!expectedHash) return { ok: false, error: 'skill not configured' }
|
||||
|
||||
const actualHash = 'sha256:' + crypto.createHash('sha256').update(apiKey).digest('hex')
|
||||
if (actualHash !== expectedHash) return { ok: false, error: 'invalid api key' }
|
||||
|
||||
return { ok: true, skill: cfg }
|
||||
}
|
||||
|
||||
// 校验内部 Tool 读取请求
|
||||
const verifyInternalToken = (req) => {
|
||||
const token = String(req.headers['x-internal-token'] || '').trim()
|
||||
const skillId = String(req.headers['x-skill-id'] || req.params?.skillId || '').trim()
|
||||
if (!token || !skillId) return { ok: false, error: 'missing token or skill id' }
|
||||
|
||||
const cfg = loadSkillConfig(skillId)
|
||||
if (!cfg) return { ok: false, error: 'skill not found' }
|
||||
|
||||
const expectedHash = String(cfg.read_token_hash || '')
|
||||
if (!expectedHash) return { ok: false, error: 'skill not configured for internal read' }
|
||||
|
||||
const actualHash = 'sha256:' + crypto.createHash('sha256').update(token).digest('hex')
|
||||
if (actualHash !== expectedHash) return { ok: false, error: 'invalid internal token' }
|
||||
|
||||
return { ok: true, skill: cfg }
|
||||
}
|
||||
|
||||
module.exports = { loadSkillConfig, refreshCache, listEnabledSkills, verifyApiKey, verifyInternalToken }
|
||||
@@ -0,0 +1,125 @@
|
||||
// ============================================================
|
||||
// data_gateway/index.js - 对外 API 数据网关主入口
|
||||
// 暴露两个路由:
|
||||
// POST /api/v1/ingest/:skillId 外部数据写入(公网,需 API Key)
|
||||
// GET /api/v1/data/:skillId/query 内部数据读取(仅 localhost,需 Internal Token)
|
||||
// ============================================================
|
||||
const { logJSON } = require('../logger')
|
||||
const { verifyApiKey, verifyInternalToken, listEnabledSkills } = require('./auth')
|
||||
const { insert, upsertLatest, queryLatest, queryList, queryByTimeRange, count } = require('./store')
|
||||
const { loadSkillConfig } = require('./auth')
|
||||
const { bindRoutes: bindExternalStorageRoutes } = require('./skills/external_storage')
|
||||
|
||||
const setNoCache = (res) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
||||
res.set('Pragma', 'no-cache')
|
||||
res.set('Expires', '0')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const bindRoutes = (app) => {
|
||||
// ============================================================
|
||||
// 外部存储 skill(第三方 OSS 读写)
|
||||
// 路由挂载在 /api/v1/skills/external_storage/*
|
||||
// ============================================================
|
||||
try {
|
||||
bindExternalStorageRoutes(app)
|
||||
} catch (e) {
|
||||
console.error(`[data_gateway] Failed to bind external_storage routes: ${e.message}`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 外部写入接口(公网暴露)
|
||||
// POST /api/v1/ingest/:skillId
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// Body: { "source": "...", "data": { ... } }
|
||||
// ============================================================
|
||||
app.post('/api/v1/ingest/:skillId', (req, res) => {
|
||||
setNoCache(res)
|
||||
const skillId = String(req.params.skillId || '').trim()
|
||||
if (!skillId) return res.status(400).json({ ok: false, error: 'missing skill id' })
|
||||
|
||||
const auth = verifyApiKey(req)
|
||||
if (!auth.ok) {
|
||||
try { logJSON('data_gateway.auth.fail', { skillId, error: auth.error, ip: req.ip }, 'data_gateway') } catch {}
|
||||
return res.status(401).json({ ok: false, error: auth.error })
|
||||
}
|
||||
|
||||
const { source, data } = req.body || {}
|
||||
if (data === undefined && req.body && typeof req.body === 'object') {
|
||||
// 如果没传 data 字段,将整个 body 视为 data
|
||||
const bodyData = { ...req.body }
|
||||
delete bodyData.source
|
||||
const record = insert(skillId, { source: String(source || ''), data: Object.keys(bodyData).length > 0 ? bodyData : req.body })
|
||||
try { logJSON('data_gateway.ingest.ok', { skillId, source, recordId: record.id }, 'data_gateway') } catch {}
|
||||
return res.json({ ok: true, id: record.id, created_at: record.created_at })
|
||||
}
|
||||
|
||||
const record = insert(skillId, { source: String(source || ''), data: data || {} })
|
||||
try { logJSON('data_gateway.ingest.ok', { skillId, source, recordId: record.id }, 'data_gateway') } catch {}
|
||||
return res.json({ ok: true, id: record.id, created_at: record.created_at })
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 内部读取接口(仅 localhost)
|
||||
// GET /api/v1/data/:skillId/query?mode=latest|list&limit=100&offset=0
|
||||
// Headers: X-Internal-Token, X-Skill-Id
|
||||
// ============================================================
|
||||
app.get('/api/v1/data/:skillId/query', (req, res) => {
|
||||
setNoCache(res)
|
||||
const skillId = String(req.params.skillId || '').trim()
|
||||
if (!skillId) return res.status(400).json({ ok: false, error: 'missing skill id' })
|
||||
|
||||
const auth = verifyInternalToken(req)
|
||||
if (!auth.ok) return res.status(401).json({ ok: false, error: auth.error })
|
||||
|
||||
const mode = String(req.query.mode || 'latest').trim()
|
||||
const limit = parseInt(req.query.limit, 10) || 100
|
||||
const offset = parseInt(req.query.offset, 10) || 0
|
||||
const source = req.query.source || null
|
||||
const start = req.query.start || null
|
||||
const end = req.query.end || null
|
||||
|
||||
try {
|
||||
if (mode === 'list') {
|
||||
if (start || end) {
|
||||
const result = queryByTimeRange(skillId, { start, end, limit })
|
||||
return res.json({ ok: true, ...result })
|
||||
}
|
||||
const result = queryList(skillId, { limit, offset, source })
|
||||
return res.json({ ok: true, ...result })
|
||||
}
|
||||
|
||||
// mode === 'latest'(默认)
|
||||
const row = queryLatest(skillId)
|
||||
if (!row) return res.json({ ok: true, rows: [], total: 0 })
|
||||
return res.json({ ok: true, rows: [row], total: 1 })
|
||||
} catch (e) {
|
||||
try { logJSON('data_gateway.query.error', { skillId, error: String(e.message || e) }, 'data_gateway') } catch {}
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 管理接口:列出所有已启用的 skill(仅 localhost)
|
||||
// GET /api/v1/data/skills
|
||||
// ============================================================
|
||||
app.get('/api/v1/data/skills', (req, res) => {
|
||||
setNoCache(res)
|
||||
const skills = listEnabledSkills()
|
||||
const result = skills.map(s => {
|
||||
const cfg = loadSkillConfig(s.id)
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
mode: String(cfg && cfg.mode || 'append'),
|
||||
record_count: count(s.id),
|
||||
db_path: require('./store').getDbPath(s.id)
|
||||
}
|
||||
})
|
||||
res.json({ ok: true, skills: result })
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes, listEnabledSkills, loadSkillConfig }
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"_comment": "外部存储 skill - 供第三方程序读写 OSS external_storage 区域",
|
||||
"id": "external_storage",
|
||||
"name": "外部存储",
|
||||
"enabled": true,
|
||||
"api_key_comment": "这个发给对方,API Key,用于上传文件到外部存储区域",
|
||||
"api_key": "4a211084d059Pk0C6t8dlk3ew61d46",
|
||||
"api_key_hash": "sha256:17eb4c895064b955d80d007b15af992d7bb7bd1525ce5726f2f63f06229857c9",
|
||||
"oss_bucket": "android-o1-images",
|
||||
"oss_endpoint_backup1": "oss-accelerate.aliyuncs.com 传输加速域名(全地域上传下载加速)",
|
||||
"oss_endpoint_backup2": "oss-cn-shanghai.aliyuncs.com 上海区域",
|
||||
"oss_endpoint": "oss-accelerate.aliyuncs.com",
|
||||
"oss_prefix": "550e8400-e29b-41d4-a716-446655442082/external_storage"
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// ============================================================
|
||||
// external_storage/index.js - 外部存储 skill 路由入口
|
||||
// 提供第三方程序读写 OSS external_storage 区域的 API
|
||||
// ============================================================
|
||||
const express = require('express')
|
||||
const multer = require('multer')
|
||||
const path = require('path')
|
||||
const archiver = require('archiver')
|
||||
const { createOssClient } = require('./oss_client')
|
||||
const { sanitizePath, createSizeLimitMiddleware } = require('./middleware')
|
||||
const { loadSkillConfig } = require('../../auth')
|
||||
const { verifyApiKey } = require('../../auth')
|
||||
const { logJSON } = require('../../../logger')
|
||||
|
||||
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024 // 50MB
|
||||
|
||||
// 内存存储上传文件
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: MAX_UPLOAD_BYTES, files: 20 }
|
||||
})
|
||||
|
||||
// MIME 类型推断(支持中文文件名 UTF-8)
|
||||
const contentTypeByExt = (filename) => {
|
||||
const ext = path.extname(filename).toLowerCase().slice(1)
|
||||
const map = {
|
||||
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon', bmp: 'image/bmp', avif: 'image/avif',
|
||||
txt: 'text/plain; charset=utf-8', md: 'text/plain; charset=utf-8', html: 'text/html; charset=utf-8',
|
||||
js: 'application/javascript', ts: 'application/javascript', css: 'text/css', json: 'application/json',
|
||||
xml: 'text/xml', log: 'text/plain; charset=utf-8', csv: 'text/csv',
|
||||
pdf: 'application/pdf',
|
||||
doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
mp4: 'video/mp4', mov: 'video/quicktime', avi: 'video/x-msvideo', webm: 'video/webm', mkv: 'video/x-matroska',
|
||||
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
|
||||
zip: 'application/zip', rar: 'application/x-rar-compressed', '7z': 'application/x-7z-compressed',
|
||||
tar: 'application/x-tar', gz: 'application/gzip',
|
||||
exe: 'application/x-msdownload', dmg: 'application/x-apple-diskimage',
|
||||
font: 'font/woff', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf'
|
||||
}
|
||||
return map[ext] || 'application/octet-stream'
|
||||
}
|
||||
|
||||
// 安全文件名清理(保留中文,移除危险字符)
|
||||
const safeName = (name) => {
|
||||
let s = String(name || 'untitled')
|
||||
// 尝试修复编码(UTF-8 被误读为 latin1 的情况)
|
||||
try {
|
||||
const recovered = Buffer.from(s, 'latin1').toString('utf8')
|
||||
if (recovered !== s && !/\ufffd/.test(recovered)) s = recovered
|
||||
} catch {}
|
||||
// 移除危险字符,但保留中文、英文、数字、常见符号
|
||||
return s.replace(/[<>\":/\\|?*\x00-\x1F]/g, '_').trim() || 'untitled'
|
||||
}
|
||||
|
||||
// 设置无缓存响应头
|
||||
const setNoCache = (res) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
||||
res.set('Pragma', 'no-cache')
|
||||
res.set('Expires', '0')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定路由到 Express 应用
|
||||
* @param {express.Application} app
|
||||
*/
|
||||
const bindRoutes = (app) => {
|
||||
const skillId = 'external_storage'
|
||||
const skillConfig = loadSkillConfig(skillId)
|
||||
if (!skillConfig) {
|
||||
console.warn(`[external_storage] config.json not found, skill disabled`)
|
||||
return
|
||||
}
|
||||
|
||||
let ossClient
|
||||
try {
|
||||
ossClient = createOssClient(skillConfig)
|
||||
} catch (e) {
|
||||
console.error(`[external_storage] Failed to init OSS client: ${e.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// API Key 鉴权中间件
|
||||
router.use((req, res, next) => {
|
||||
const auth = verifyApiKey(req)
|
||||
if (!auth.ok) {
|
||||
try { logJSON('external_storage.auth.fail', { error: auth.error, ip: req.ip }, 'external_storage') } catch {}
|
||||
return res.status(401).json({ ok: false, error: auth.error })
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// 全局无缓存
|
||||
router.use((req, res, next) => {
|
||||
setNoCache(res)
|
||||
next()
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 上传文件
|
||||
// POST /api/v1/skills/external_storage/upload?path=subdir/
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// Body: multipart/form-data, field: "files"
|
||||
// ============================================================
|
||||
router.post('/upload', createSizeLimitMiddleware(MAX_UPLOAD_BYTES), upload.array('files', 20), async (req, res) => {
|
||||
try {
|
||||
const basePath = sanitizePath(req.query.path || '')
|
||||
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
||||
return res.status(400).json({ ok: false, error: '未收到文件' })
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const f of req.files) {
|
||||
const originalName = safeName(f.originalname || 'file')
|
||||
const keyPath = basePath ? `${basePath}/${originalName}` : originalName
|
||||
const contentType = f.mimetype || contentTypeByExt(originalName)
|
||||
|
||||
const { key, raw_url } = await ossClient.putObject(keyPath, f.buffer, contentType)
|
||||
results.push({
|
||||
path: keyPath,
|
||||
oss_key: key,
|
||||
raw_url,
|
||||
name: originalName,
|
||||
size: f.size,
|
||||
content_type: contentType
|
||||
})
|
||||
}
|
||||
|
||||
res.json({ ok: true, count: results.length, files: results })
|
||||
} catch (e) {
|
||||
if (String(e.message || '').includes('File too large')) {
|
||||
return res.status(413).json({ ok: false, error: '文件超过 50MB 上限' })
|
||||
}
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 下载文件(代理流)
|
||||
// GET /api/v1/skills/external_storage/download?path=xxx
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.get('/download', async (req, res) => {
|
||||
try {
|
||||
const filePath = sanitizePath(req.query.path)
|
||||
if (!filePath) return res.status(400).json({ ok: false, error: '缺少 path 参数' })
|
||||
|
||||
const displayName = path.basename(filePath)
|
||||
const { stream, headers } = await ossClient.getObjectStream(filePath)
|
||||
|
||||
// 使用 RFC 5987 格式支持 UTF-8 文件名
|
||||
res.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(displayName)}`)
|
||||
if (headers['content-type']) res.set('Content-Type', headers['content-type'])
|
||||
if (headers['content-length']) res.set('Content-Length', headers['content-length'])
|
||||
|
||||
stream.pipe(res)
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 获取原始 URL
|
||||
// GET /api/v1/skills/external_storage/raw_url?path=xxx
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.get('/raw_url', async (req, res) => {
|
||||
try {
|
||||
const filePath = sanitizePath(req.query.path)
|
||||
if (!filePath) return res.status(400).json({ ok: false, error: '缺少 path 参数' })
|
||||
|
||||
const raw_url = ossClient.getRawUrl(filePath)
|
||||
res.json({ ok: true, path: filePath, raw_url })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 列出文件
|
||||
// GET /api/v1/skills/external_storage/list?prefix=subdir/&recursive=true|false
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.get('/list', async (req, res) => {
|
||||
try {
|
||||
const prefix = sanitizePath(req.query.prefix || '')
|
||||
const recursive = req.query.recursive === 'true'
|
||||
|
||||
let result
|
||||
if (recursive) {
|
||||
const files = await ossClient.listRecursive(prefix)
|
||||
result = { files, folders: [] }
|
||||
} else {
|
||||
result = await ossClient.listObjects(prefix)
|
||||
}
|
||||
|
||||
res.json({ ok: true, prefix, recursive, ...result })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 删除文件
|
||||
// DELETE /api/v1/skills/external_storage/delete?path=xxx
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.delete('/delete', async (req, res) => {
|
||||
try {
|
||||
const filePath = sanitizePath(req.query.path)
|
||||
if (!filePath) return res.status(400).json({ ok: false, error: '缺少 path 参数' })
|
||||
|
||||
await ossClient.deleteObject(filePath)
|
||||
res.json({ ok: true, path: filePath })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 文件夹打包下载
|
||||
// GET /api/v1/skills/external_storage/folder/download?path=subdir/
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.get('/folder/download', async (req, res) => {
|
||||
try {
|
||||
const folderPath = sanitizePath(req.query.path)
|
||||
if (!folderPath) return res.status(400).json({ ok: false, error: '缺少 path 参数' })
|
||||
|
||||
const folderKey = folderPath.endsWith('/') ? folderPath : folderPath + '/'
|
||||
const folderName = path.basename(folderKey.replace(/\/+$/, '')) || 'folder'
|
||||
|
||||
const files = await ossClient.listRecursive(folderKey)
|
||||
|
||||
res.set('Content-Type', 'application/zip')
|
||||
res.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(folderName + '.zip')}`)
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 6 } })
|
||||
archive.on('error', err => {
|
||||
try { res.status(500).end() } catch {}
|
||||
})
|
||||
archive.pipe(res)
|
||||
|
||||
const prefixClean = folderKey.replace(/\/+$/, '') + '/'
|
||||
for (const f of files) {
|
||||
try {
|
||||
// 提取相对于文件夹前缀的路径
|
||||
const fullRel = f.key
|
||||
const relPart = fullRel.slice(prefixClean.length)
|
||||
const { stream } = await ossClient.getObjectStream(relPart)
|
||||
archive.append(stream, { name: relPart })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
archive.finalize()
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 获取文件信息
|
||||
// GET /api/v1/skills/external_storage/info?path=xxx
|
||||
// Headers: X-API-Id, X-API-Key
|
||||
// ============================================================
|
||||
router.get('/info', async (req, res) => {
|
||||
try {
|
||||
const filePath = sanitizePath(req.query.path)
|
||||
if (!filePath) return res.status(400).json({ ok: false, error: '缺少 path 参数' })
|
||||
|
||||
const isFolder = filePath.endsWith('/')
|
||||
const fullKey = ossClient.fullKey(filePath)
|
||||
const raw_url = ossClient.getRawUrl(filePath)
|
||||
|
||||
let size = 0
|
||||
let lastModified = 0
|
||||
|
||||
if (!isFolder) {
|
||||
try {
|
||||
const parent = filePath.includes('/') ? filePath.split('/').slice(0, -1).join('/') : ''
|
||||
const result = await ossClient.listObjects(parent)
|
||||
const targetKey = fullKey
|
||||
const hit = result.files.find(f => f.key === targetKey)
|
||||
if (hit) {
|
||||
size = hit.size
|
||||
lastModified = hit.lastModified
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
const files = await ossClient.listRecursive(filePath)
|
||||
size = files.reduce((s, f) => s + (Number(f.size) || 0), 0)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
info: {
|
||||
path: filePath,
|
||||
oss_key: fullKey,
|
||||
raw_url,
|
||||
isFolder,
|
||||
size,
|
||||
lastModified: lastModified ? new Date(lastModified).toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// 挂载路由
|
||||
app.use('/api/v1/skills/external_storage', router)
|
||||
console.log('[external_storage] Routes mounted at /api/v1/skills/external_storage')
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes }
|
||||
@@ -0,0 +1,83 @@
|
||||
// ============================================================
|
||||
// external_storage/middleware.js - 路径安全校验中间件
|
||||
// 职责:防止路径遍历攻击,确保所有操作限定在 external_storage 范围内
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 校验并清理路径
|
||||
* - 拒绝包含 ../ 的路径遍历
|
||||
* - 拒绝绝对路径
|
||||
* - 拒绝控制字符
|
||||
* - 统一使用 UTF-8 编码,正确处理中文
|
||||
* @param {string} rawPath - 原始路径参数
|
||||
* @returns {string} 清理后的安全路径
|
||||
* @throws {Error} 如果路径不合法
|
||||
*/
|
||||
const sanitizePath = (rawPath) => {
|
||||
if (rawPath === undefined || rawPath === null) return ''
|
||||
|
||||
// 确保使用 UTF-8 字符串
|
||||
let p = String(rawPath)
|
||||
|
||||
// 拒绝控制字符(除了正常的换行和制表符)
|
||||
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(p)) {
|
||||
throw new Error('路径包含非法控制字符')
|
||||
}
|
||||
|
||||
// 拒绝路径遍历
|
||||
if (p.includes('..')) {
|
||||
throw new Error('路径不允许包含 ".."')
|
||||
}
|
||||
|
||||
// 拒绝绝对路径
|
||||
if (p.startsWith('/') || p.startsWith('\\')) {
|
||||
throw new Error('路径不允许以 "/" 或 "\\" 开头')
|
||||
}
|
||||
|
||||
// 统一斜杠
|
||||
p = p.replace(/[\\]+/g, '/')
|
||||
|
||||
// 去除连续斜杠
|
||||
p = p.replace(/\/+/g, '/')
|
||||
|
||||
// 去除首尾斜杠
|
||||
p = p.replace(/^\/+|\/+$/g, '')
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验路径是否在允许的前缀范围内
|
||||
* @param {string} ossKey - 完整 OSS key
|
||||
* @param {string} allowedPrefix - 允许的前缀
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isPathWithinPrefix = (ossKey, allowedPrefix) => {
|
||||
const key = String(ossKey || '')
|
||||
const prefix = String(allowedPrefix || '').replace(/\/+$/, '')
|
||||
return key === prefix || key.startsWith(prefix + '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小校验中间件
|
||||
* @param {number} maxBytes - 最大字节数
|
||||
* @returns {Function} express 中间件
|
||||
*/
|
||||
const createSizeLimitMiddleware = (maxBytes = 50 * 1024 * 1024) => {
|
||||
return (req, res, next) => {
|
||||
const contentLength = parseInt(req.headers['content-length'] || '0', 10)
|
||||
if (contentLength > maxBytes) {
|
||||
return res.status(413).json({
|
||||
ok: false,
|
||||
error: `文件超过大小限制 (${Math.round(maxBytes / 1024 / 1024)}MB)`
|
||||
})
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sanitizePath,
|
||||
isPathWithinPrefix,
|
||||
createSizeLimitMiddleware
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// ============================================================
|
||||
// external_storage/oss_client.js - 阿里云 OSS 客户端封装
|
||||
// 职责:提供上传、下载、删除、列表、获取URL等基础操作
|
||||
// 所有操作限定在 config.json 配置的 oss_prefix 范围内
|
||||
// ============================================================
|
||||
const crypto = require('crypto')
|
||||
const https = require('https')
|
||||
const http = require('http')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { URLSearchParams } = require('url')
|
||||
|
||||
/**
|
||||
* 初始化 OSS 客户端配置
|
||||
* @param {Object} skillConfig - skill 的 config.json 内容
|
||||
* @returns {Object} 客户端实例
|
||||
*/
|
||||
const createOssClient = (skillConfig) => {
|
||||
const bucket = skillConfig.oss_bucket || 'android-o1-images'
|
||||
const endpoint = skillConfig.oss_endpoint || 'oss-cn-shanghai.aliyuncs.com'
|
||||
const prefix = (skillConfig.oss_prefix || '550e8400-e29b-41d4-a716-446655442082/external_storage').replace(/\/+$/, '')
|
||||
|
||||
// 从 process.env 读取(index.js 启动时已从 Toolbox_local_creds.env.local 加载)
|
||||
let accessKeyId = process.env.EXTERNAL_STORAGE_OSS_ACCESS_KEY_ID || ''
|
||||
let accessKeySecret = process.env.EXTERNAL_STORAGE_OSS_ACCESS_KEY_SECRET || ''
|
||||
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
throw new Error('OSS credentials not found. Please set oss_access_key_id and oss_access_key_secret in config.json')
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 OSS key:处理斜杠、去除危险字符
|
||||
*/
|
||||
const normalizeKey = (relativePath) => {
|
||||
let k = String(relativePath || '').replace(/[\\]+/g, '/').replace(/\/+/g, '/')
|
||||
if (k.startsWith('/')) k = k.slice(1)
|
||||
return k
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接完整 OSS key(自动加前缀)
|
||||
*/
|
||||
const fullKey = (relativePath) => {
|
||||
const rel = normalizeKey(relativePath)
|
||||
return rel ? `${prefix}/${rel}` : prefix
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 编码 OSS key(用于 HTTP 路径)
|
||||
*/
|
||||
const encodeKey = (key) => String(key || '').split('/').map(s => encodeURIComponent(s)).join('/')
|
||||
|
||||
/**
|
||||
* 构建 OSS 资源标识
|
||||
*/
|
||||
const ossResource = (key) => `/${bucket}/${String(key || '').replace(/^\/+/, '')}`
|
||||
|
||||
/**
|
||||
* OSS 请求签名(HMAC-SHA1)
|
||||
*/
|
||||
const signRequest = ({ method, contentType = '', date, key, extraHeaders = {} }) => {
|
||||
const canonicalHeaders = Object.keys(extraHeaders)
|
||||
.filter(k => k && k.toLowerCase().startsWith('x-oss-'))
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||
.map(k => `${k.toLowerCase()}:${String(extraHeaders[k] || '').trim()}`)
|
||||
.join('\n')
|
||||
const resource = ossResource(key)
|
||||
const stringToSign = `${method.toUpperCase()}\n\n${contentType}\n${date}\n${canonicalHeaders ? canonicalHeaders + '\n' : ''}${resource}`
|
||||
return crypto.createHmac('sha1', accessKeySecret).update(stringToSign).digest('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起 HTTP 请求
|
||||
*/
|
||||
const httpRequest = (options, body = null, timeoutMs = 60000) => new Promise((resolve, reject) => {
|
||||
const lib = options.protocol === 'https:' ? https : http
|
||||
const req = lib.request(options, (res) => {
|
||||
const chunks = []
|
||||
res.on('data', chunk => chunks.push(chunk))
|
||||
res.on('end', () => {
|
||||
const buf = Buffer.concat(chunks)
|
||||
resolve({ statusCode: res.statusCode, headers: res.headers, body: buf })
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.setTimeout(timeoutMs, () => req.destroy(new Error('request timeout')))
|
||||
if (body !== null && body !== undefined) {
|
||||
if (Buffer.isBuffer(body) || typeof body === 'string') req.write(body)
|
||||
}
|
||||
req.end()
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取文件流
|
||||
*/
|
||||
const getObjectStream = (relativePath) => new Promise((resolve, reject) => {
|
||||
const key = fullKey(relativePath)
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = `OSS ${accessKeyId}:${signRequest({ method: 'GET', date, key })}`
|
||||
const options = {
|
||||
hostname: `${bucket}.${endpoint}`,
|
||||
path: `/${encodeKey(key)}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Host: `${bucket}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
}
|
||||
const req = https.request(options, (res) => {
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const chunks = []
|
||||
res.on('data', c => chunks.push(c))
|
||||
res.on('end', () => reject(new Error(`OSS get failed: ${res.statusCode} ${Buffer.concat(chunks).toString('utf8').slice(0, 500)}`)))
|
||||
return
|
||||
}
|
||||
resolve({ stream: res, headers: res.headers })
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.setTimeout(60000, () => req.destroy(new Error('request timeout')))
|
||||
req.end()
|
||||
})
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} relativePath - 相对路径(自动加前缀)
|
||||
* @param {Buffer} body - 文件内容
|
||||
* @param {string} contentType - MIME 类型
|
||||
* @returns {Object} { key, raw_url }
|
||||
*/
|
||||
const putObject = async (relativePath, body, contentType = 'application/octet-stream') => {
|
||||
const key = fullKey(relativePath)
|
||||
const date = new Date().toUTCString()
|
||||
const extraHeaders = { 'x-oss-object-acl': 'public-read' }
|
||||
const authorization = `OSS ${accessKeyId}:${signRequest({ method: 'PUT', contentType, date, key, extraHeaders })}`
|
||||
const options = {
|
||||
hostname: `${bucket}.${endpoint}`,
|
||||
path: `/${encodeKey(key)}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Host: `${bucket}.${endpoint}`,
|
||||
Date: date,
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body || ''),
|
||||
...extraHeaders,
|
||||
Authorization: authorization
|
||||
}
|
||||
}
|
||||
const resp = await httpRequest(options, body, 120000)
|
||||
if (resp.statusCode < 200 || resp.statusCode >= 300) {
|
||||
throw new Error(`OSS put failed: ${resp.statusCode} ${resp.body.toString('utf8').slice(0, 500)}`)
|
||||
}
|
||||
const raw_url = `https://${bucket}.${endpoint}/${encodeKey(key)}`
|
||||
return { key, raw_url }
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
const deleteObject = async (relativePath) => {
|
||||
const key = fullKey(relativePath)
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = `OSS ${accessKeyId}:${signRequest({ method: 'DELETE', date, key })}`
|
||||
const options = {
|
||||
hostname: `${bucket}.${endpoint}`,
|
||||
path: `/${encodeKey(key)}`,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Host: `${bucket}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
}
|
||||
const resp = await httpRequest(options)
|
||||
if (resp.statusCode < 200 || resp.statusCode >= 300) {
|
||||
throw new Error(`OSS delete failed: ${resp.statusCode} ${resp.body.toString('utf8').slice(0, 500)}`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OSS ListObjects XML 响应
|
||||
*/
|
||||
const parseListXml = (xml) => {
|
||||
const extractTag = (tag, src) => {
|
||||
const open = `<${tag}>`
|
||||
const close = `</${tag}>`
|
||||
const result = []
|
||||
let idx = 0
|
||||
while (true) {
|
||||
const s = src.indexOf(open, idx)
|
||||
if (s < 0) break
|
||||
const e = src.indexOf(close, s + open.length)
|
||||
if (e < 0) break
|
||||
result.push(src.slice(s + open.length, e))
|
||||
idx = e + close.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
const extractInSection = (tag, sectionOpen, sectionClose, src) => {
|
||||
const result = []
|
||||
let searchFrom = 0
|
||||
while (true) {
|
||||
const sOpen = src.indexOf(sectionOpen, searchFrom)
|
||||
if (sOpen < 0) break
|
||||
const sClose = src.indexOf(sectionClose, sOpen)
|
||||
if (sClose < 0) break
|
||||
const section = src.slice(sOpen, sClose)
|
||||
result.push(...extractTag(tag, section))
|
||||
searchFrom = sClose + sectionClose.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
const folders = extractInSection('Prefix', '<CommonPrefixes>', '</CommonPrefixes>', xml).map(p => ({
|
||||
key: p,
|
||||
name: p.split('/').filter(Boolean).pop() + '/',
|
||||
isFolder: true,
|
||||
size: 0
|
||||
}))
|
||||
const fileKeys = extractTag('Key', xml)
|
||||
const fileSizes = extractTag('Size', xml)
|
||||
const fileLms = extractTag('LastModified', xml)
|
||||
const files = fileKeys.map((k, i) => ({
|
||||
key: k,
|
||||
name: k.split('/').filter(Boolean).pop(),
|
||||
isFolder: false,
|
||||
size: parseInt(fileSizes[i] || '0', 10) || 0,
|
||||
lastModified: fileLms[i] ? Date.parse(fileLms[i]) : 0
|
||||
})).filter(f => f.key && !f.key.endsWith('/'))
|
||||
return { folders, files }
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出文件(递归)
|
||||
* @param {string} relativePrefix - 相对前缀
|
||||
* @returns {Array} 文件列表
|
||||
*/
|
||||
const listRecursive = async (relativePrefix = '') => {
|
||||
const all = []
|
||||
const stack = [relativePrefix]
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
const keyPrefix = fullKey(current)
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = `OSS ${accessKeyId}:${signRequest({ method: 'GET', date, key: '' })}`
|
||||
const query = new URLSearchParams()
|
||||
query.set('prefix', keyPrefix)
|
||||
query.set('delimiter', '/')
|
||||
query.set('max-keys', '1000')
|
||||
const options = {
|
||||
hostname: `${bucket}.${endpoint}`,
|
||||
path: `/?${query.toString()}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Host: `${bucket}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
}
|
||||
const resp = await httpRequest(options)
|
||||
if (resp.statusCode < 200 || resp.statusCode >= 300) {
|
||||
throw new Error(`OSS list failed: ${resp.statusCode} ${resp.body.toString('utf8').slice(0, 500)}`)
|
||||
}
|
||||
const parsed = parseListXml(resp.body.toString('utf8'))
|
||||
all.push(...parsed.files)
|
||||
for (const f of parsed.folders) {
|
||||
stack.push(f.key.slice(keyPrefix.length > 0 ? (fullKey('').length) : 0))
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出对象(单层)
|
||||
*/
|
||||
const listObjects = async (relativePrefix = '') => {
|
||||
const keyPrefix = fullKey(relativePrefix)
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = `OSS ${accessKeyId}:${signRequest({ method: 'GET', date, key: '' })}`
|
||||
const query = new URLSearchParams()
|
||||
query.set('prefix', keyPrefix)
|
||||
query.set('delimiter', '/')
|
||||
query.set('max-keys', '1000')
|
||||
const options = {
|
||||
hostname: `${bucket}.${endpoint}`,
|
||||
path: `/?${query.toString()}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Host: `${bucket}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
}
|
||||
const resp = await httpRequest(options)
|
||||
if (resp.statusCode < 200 || resp.statusCode >= 300) {
|
||||
throw new Error(`OSS list failed: ${resp.statusCode} ${resp.body.toString('utf8').slice(0, 500)}`)
|
||||
}
|
||||
return parseListXml(resp.body.toString('utf8'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件原始 URL(公共读可直接访问)
|
||||
*/
|
||||
const getRawUrl = (relativePath) => {
|
||||
const key = fullKey(relativePath)
|
||||
return `https://${bucket}.${endpoint}/${encodeKey(key)}`
|
||||
}
|
||||
|
||||
return {
|
||||
fullKey,
|
||||
normalizeKey,
|
||||
putObject,
|
||||
deleteObject,
|
||||
getObjectStream,
|
||||
listObjects,
|
||||
listRecursive,
|
||||
getRawUrl
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createOssClient }
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"_comment": "天气数据 skill - 由外部 Hermes Agent 每30分钟写入一次",
|
||||
"id": "weather",
|
||||
"name": "天气数据",
|
||||
"enabled": true,
|
||||
"mode": "append",
|
||||
"mode_comment": "append: 增量追加保留所有历史;latest: 只保留最新一条",
|
||||
"api_key_hash": "sha256:7b4035f766bf66e39cff1a58a5e8c73f2fcbf75f3f2a368bf0003e1110a56255",
|
||||
"read_token_hash": "sha256:aa40833cd71a76b2fc558edb1384f4d993c9e8d966a730005f7c99d56ab19037"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// ============================================================
|
||||
// data_gateway/store.js - 数据存储模块
|
||||
// 职责:动态创建/管理每个 skill 的独立 SQLite 数据库
|
||||
// 表结构:id | source | data(JSON) | created_at
|
||||
// 支持 mode: "append"(增量追加)和 "latest"(覆盖最新)
|
||||
// ============================================================
|
||||
const Database = require('better-sqlite3')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data', 'ingest')
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })
|
||||
|
||||
// 已打开的数据库连接缓存
|
||||
const dbCache = new Map()
|
||||
|
||||
const getDbPath = (skillId) => path.join(DATA_DIR, `${skillId}.db`)
|
||||
|
||||
const getOrOpenDb = (skillId) => {
|
||||
if (dbCache.has(skillId)) return dbCache.get(skillId)
|
||||
const dbPath = getDbPath(skillId)
|
||||
const db = new Database(dbPath)
|
||||
// 启用 WAL 模式提高并发写入性能
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('synchronous = NORMAL')
|
||||
db.exec(
|
||||
`CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`
|
||||
)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_records_created ON records(created_at DESC)`)
|
||||
dbCache.set(skillId, db)
|
||||
return db
|
||||
}
|
||||
|
||||
// 写入一条记录
|
||||
const insert = (skillId, { source = '', data = {} }) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
const dataStr = JSON.stringify(data)
|
||||
const info = db.prepare(`INSERT INTO records (source, data) VALUES (?, ?)`).run(
|
||||
String(source || ''), dataStr
|
||||
)
|
||||
return { id: info.lastInsertRowid, source, data, created_at: new Date().toISOString() }
|
||||
}
|
||||
|
||||
// 覆盖写入(mode: "latest")- 先删后插,保证只有一条
|
||||
const upsertLatest = (skillId, { source = '', data = {} }) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
const dataStr = JSON.stringify(data)
|
||||
const tx = db.transaction(() => {
|
||||
db.prepare(`DELETE FROM records`).run()
|
||||
return db.prepare(`INSERT INTO records (source, data) VALUES (?, ?)`).run(
|
||||
String(source || ''), dataStr
|
||||
)
|
||||
})
|
||||
const info = tx()
|
||||
return { id: info.lastInsertRowid, source, data, created_at: new Date().toISOString() }
|
||||
}
|
||||
|
||||
// 查询最新一条记录
|
||||
const queryLatest = (skillId) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
const row = db.prepare(`SELECT * FROM records ORDER BY id DESC LIMIT 1`).get()
|
||||
if (!row) return null
|
||||
try { row.data = JSON.parse(row.data) } catch { /* keep as string */ }
|
||||
return row
|
||||
}
|
||||
|
||||
// 查询记录列表(分页)
|
||||
const queryList = (skillId, { limit = 100, offset = 0, source = null } = {}) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
let sql = `SELECT * FROM records`
|
||||
const params = []
|
||||
if (source) {
|
||||
sql += ` WHERE source = ?`
|
||||
params.push(String(source))
|
||||
}
|
||||
sql += ` ORDER BY id DESC LIMIT ? OFFSET ?`
|
||||
params.push(Math.min(Number(limit) || 100, 1000), Math.max(0, Number(offset) || 0))
|
||||
const rows = db.prepare(sql).all(...params)
|
||||
for (const row of rows) {
|
||||
try { row.data = JSON.parse(row.data) } catch { /* keep as string */ }
|
||||
}
|
||||
const total = db.prepare(`SELECT COUNT(*) as cnt FROM records`).get().cnt
|
||||
return { rows, total }
|
||||
}
|
||||
|
||||
// 按时间范围查询
|
||||
const queryByTimeRange = (skillId, { start, end, limit = 100 } = {}) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
let sql = `SELECT * FROM records WHERE 1=1`
|
||||
const params = []
|
||||
if (start) { sql += ` AND created_at >= ?`; params.push(String(start)) }
|
||||
if (end) { sql += ` AND created_at <= ?`; params.push(String(end)) }
|
||||
sql += ` ORDER BY id DESC LIMIT ?`
|
||||
params.push(Math.min(Number(limit) || 100, 1000))
|
||||
const rows = db.prepare(sql).all(...params)
|
||||
for (const row of rows) {
|
||||
try { row.data = JSON.parse(row.data) } catch { /* keep as string */ }
|
||||
}
|
||||
return { rows, total: rows.length }
|
||||
}
|
||||
|
||||
// 获取记录总数
|
||||
const count = (skillId) => {
|
||||
const db = getOrOpenDb(skillId)
|
||||
return db.prepare(`SELECT COUNT(*) as cnt FROM records`).get().cnt
|
||||
}
|
||||
|
||||
// 关闭所有数据库连接
|
||||
const closeAll = () => {
|
||||
for (const [skillId, db] of dbCache) {
|
||||
try { db.close() } catch {}
|
||||
dbCache.delete(skillId)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
insert, upsertLatest, queryLatest, queryList, queryByTimeRange, count, closeAll, getDbPath, getOrOpenDb
|
||||
}
|
||||
+147
-1
@@ -7,7 +7,9 @@ const Database = require('better-sqlite3')
|
||||
const crypto = require('crypto')
|
||||
|
||||
// ===== 从 ~/Toolbox_local_creds.env.local 加载变量到 process.env =====
|
||||
// 所有模块的 process.env.XXX fallback 依赖此加载器
|
||||
// ★ 规则:所有密码、密钥、Token、用户名等高危内容,禁止硬编码在源码里。
|
||||
// 一律放在 ~/Toolbox_local_creds.env.local 中,在此处自动加载到 process.env
|
||||
// 各模块通过 process.env.XXX 读取,不要直接 import 或 require 凭证。
|
||||
// 优先级:已存在的 process.env > env 文件 > 模块内 '' fallback
|
||||
try {
|
||||
const os = require('os')
|
||||
@@ -76,6 +78,8 @@ const plantHome = require('./plant_home')
|
||||
const globalNews = require('./global_news')
|
||||
const boxBackup = require('./box_backup')
|
||||
const yuanzhupai = require('./yuanzhupai')
|
||||
const dataGateway = require('./data_gateway')
|
||||
const ossFileCabinet = require('./oss_file_cabinet')
|
||||
yuanzhupai.initDb()
|
||||
|
||||
const app = express()
|
||||
@@ -1335,6 +1339,7 @@ const mapSystemIdToUrl = (systemId) => {
|
||||
'FitnessDiary': '/tools/fitness_diary',
|
||||
'Tools-doc_cloud_keeper': '/tools/doc_cloud_keeper',
|
||||
'Tools-yuanzhupai': '/tools/yuanzhupai',
|
||||
'Tools-oss_file_cabinet': '/tools/oss_file_cabinet',
|
||||
'Tools-plant_home': '/tools/plant_home',
|
||||
'Tools-apple_watch': '/tools/apple_watch',
|
||||
'Tools-private_clipboard': '/tools/private_clipboard',
|
||||
@@ -2078,6 +2083,145 @@ app.use('/api/private_clipboard', (req, res, next) => {
|
||||
return res.status(503).json({ ok: false, error: 'service_not_ready' })
|
||||
})
|
||||
|
||||
const OSS_FILE_CABINET_AUTH_COOKIE = 'oss_file_cabinet_gate'
|
||||
const readOssFileCabinetAuthConfig = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'public', 'tools', 'oss_file_cabinet', 'auth_config.json')
|
||||
if (fs.existsSync(p)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
if (cfg && typeof cfg === 'object') return cfg
|
||||
}
|
||||
} catch {}
|
||||
return {}
|
||||
}
|
||||
const readOssFileCabinetJwk = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'config', 'oss_file_cabinet.jwk.json')
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch {}
|
||||
return { kty: 'RSA', n: '', e: '' }
|
||||
}
|
||||
const getOssFileCabinetCookieName = () => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
const n = String(cfg.cookieName || '')
|
||||
if (n) return n
|
||||
} catch {}
|
||||
return OSS_FILE_CABINET_AUTH_COOKIE
|
||||
}
|
||||
const getOssFileCabinetMaxAge = () => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
const days = Number(cfg.max_age_days || 0)
|
||||
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
|
||||
} catch {}
|
||||
return 30 * 24 * 3600 * 1000
|
||||
}
|
||||
const getOssFileCabinetGuidConfig = () => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
|
||||
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
|
||||
return { key: keyRaw || 'guid', value: valueRaw }
|
||||
} catch { return { key: 'guid', value: '' } }
|
||||
}
|
||||
const hasValidOssFileCabinetGuid = (req) => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
const { key, value } = getOssFileCabinetGuidConfig()
|
||||
if (!value) return false
|
||||
const q = req && req.query ? req.query : {}
|
||||
const raw = q[key]
|
||||
if (raw === undefined || raw === null) return false
|
||||
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
|
||||
return incoming === value
|
||||
} catch { return false }
|
||||
}
|
||||
const hasOssFileCabinetAuth = (req) => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
if (cfg.enable_auth === false) return true
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
const name = getOssFileCabinetCookieName()
|
||||
return cookies[name] === '1'
|
||||
} catch { return false }
|
||||
}
|
||||
const isOssFileCabinetForceGuid = () => {
|
||||
try {
|
||||
const cfg = readOssFileCabinetAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
|
||||
return true
|
||||
} catch { return true }
|
||||
}
|
||||
app.get('/tools/oss_file_cabinet', (req, res, next) => {
|
||||
setNoCache(res)
|
||||
try {
|
||||
const forceGuid = isOssFileCabinetForceGuid()
|
||||
if (hasOssFileCabinetAuth(req)) return next()
|
||||
const guidKey = getOssFileCabinetGuidConfig().key
|
||||
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (hasValidOssFileCabinetGuid(req)) {
|
||||
const maxAgeOfc = getOssFileCabinetMaxAge()
|
||||
const cookieName = getOssFileCabinetCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
|
||||
return res.redirect('/tools/oss_file_cabinet/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
const token = String(req.query.token || '')
|
||||
if (!token) {
|
||||
if (forceGuid) return res.status(401).send('未授权')
|
||||
return next()
|
||||
}
|
||||
const payload = verifyJwtWithKeys(token, jwkKeys(readOssFileCabinetJwk()))
|
||||
if (!payload) return res.status(401).send('未授权')
|
||||
{
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-oss_file_cabinet')) return res.status(401).send('未授权')
|
||||
}
|
||||
const maxAgeOfc = getOssFileCabinetMaxAge()
|
||||
const cookieName = getOssFileCabinetCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
|
||||
return res.redirect('/tools/oss_file_cabinet/index.html')
|
||||
} catch { return res.status(401).send('未授权') }
|
||||
})
|
||||
app.use('/tools/oss_file_cabinet', (req, res, next) => {
|
||||
const forceGuid = isOssFileCabinetForceGuid()
|
||||
if (hasOssFileCabinetAuth(req)) return next()
|
||||
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
|
||||
const guidKey = getOssFileCabinetGuidConfig().key
|
||||
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (isHandshake && hasValidOssFileCabinetGuid(req)) {
|
||||
const maxAgeOfc = getOssFileCabinetMaxAge()
|
||||
const cookieName = getOssFileCabinetCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
|
||||
return res.redirect('/tools/oss_file_cabinet/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
if (isHandshake && String(req.query.token || '')) {
|
||||
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readOssFileCabinetJwk()))
|
||||
if (payload) {
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-oss_file_cabinet')) return res.status(401).send('未授权')
|
||||
const maxAgeOfc = getOssFileCabinetMaxAge()
|
||||
const cookieName = getOssFileCabinetCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
|
||||
return res.redirect('/tools/oss_file_cabinet/index.html')
|
||||
}
|
||||
}
|
||||
if (isHandshake && forceGuid) return res.status(401).send('未授权')
|
||||
return res.status(401).send('未授权')
|
||||
})
|
||||
app.use('/api/oss_file_cabinet', (req, res, next) => {
|
||||
if (req.path.startsWith('/share/access/')) return next()
|
||||
if (hasOssFileCabinetAuth(req)) return next()
|
||||
return res.status(401).json({ ok: false, error: '未授权' })
|
||||
})
|
||||
|
||||
const WEEKLY_AUTH_COOKIE = 'weekly_gate'
|
||||
const readWeeklyJwk = () => {
|
||||
try {
|
||||
@@ -5842,6 +5986,8 @@ expense.bindRoutes(app)
|
||||
investmentLedger.bindRoutes(app)
|
||||
styleCheck.bindRoutes(app)
|
||||
globalNews.bindRoutes(app)
|
||||
dataGateway.bindRoutes(app)
|
||||
ossFileCabinet.bindRoutes(app)
|
||||
|
||||
const marketsCfgAtBoot = getMarketsCfg()
|
||||
const currentPort = Number(process.env.PORT || '8976') || 8976
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "巴西IBOVESPA",
|
||||
"card_order": 18,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(iShares MSCI Brazil ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usEWZ",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usEWZ",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "北证50",
|
||||
"card_order": 6,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": false,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "bj899050",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/bj899050",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "巴�特指�,
|
||||
"card_order_comment": "å�¡ç‰‡æŽ’åº�åº�å�·ï¼Œè¶Šå¤§è¶Šé� å�Žï¼ˆå½“å‰�最大å€?87,设ä¸?88 排最å�Žï¼‰",
|
||||
"name_comment": "显示的名",
|
||||
"name": "巴菲特指",
|
||||
"card_order_comment": "卡片排序序号,越大越靠后(当前最大",
|
||||
"card_order": 88,
|
||||
"card_type_comment": "卡片类型:domestic(国内)/ overseas(海外)/ enhanced(增强)/ defensive(防御)",
|
||||
"card_type": "overseas",
|
||||
"card_span_comment": "å�¡ç‰‡æ¨ªå�‘å� ä½�列数ï¼?为默认,2为跨两列",
|
||||
"card_span_comment": "卡片横向占位列数",
|
||||
"card_span": 2,
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�1h 表示1�时)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "1h",
|
||||
"symbol_comment": "监控的代码(技能内部定义)",
|
||||
"symbol": "us_buffett_index",
|
||||
@@ -21,9 +21,9 @@
|
||||
"source": "fred",
|
||||
"alerts_comment": "预警阈值配置(基于S&P500/GDP比率,非Wilshire5000原值)",
|
||||
"alerts": {
|
||||
"ratio_high_warning_comment": "S&P500/GDP 比率高于æ¤å€¼è§¦å�‘估值å��高预è?,
|
||||
"ratio_high_warning_comment": "S&P500/GDP 比率高于此值触发估值偏高预",
|
||||
"ratio_high_warning": 0.19,
|
||||
"ratio_critical_comment": "S&P500/GDP 比率高于æ¤å€¼è§¦å�‘æž�端高估严é‡�告è?,
|
||||
"ratio_critical_comment": "S&P500/GDP 比率高于此值触发极端高估严重告",
|
||||
"ratio_critical": 0.21,
|
||||
"change_percent_down_comment": "单日跌幅告警阈值(比率变化百分比)",
|
||||
"change_percent_down": -5,
|
||||
@@ -31,20 +31,20 @@
|
||||
"change_percent_up": 5,
|
||||
"urgent_abs_change_percent_comment": "严重告警阈值(绝对波动百分比,日变动超过此值触发紧急推送)",
|
||||
"urgent_abs_change_percent": 8,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 240
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": false,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -55,19 +55,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": false,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": false,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": false,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -76,7 +76,7 @@
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"market_status_comment": "市场时区与交易时段定义,用于�端显示开休市状�,
|
||||
"market_status_comment": "市场时区与交易时段定义,用于前端显示开休市状",
|
||||
"market_status": {
|
||||
"timezone": "America/New_York",
|
||||
"sessions": [
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "ä¸éŸ©å�Šå¯¼ä½“指æ•?,
|
||||
"name_comment": "显示的名",
|
||||
"name": "中韩半导体指",
|
||||
"card_order": 19,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(科创50,原ä¸éŸ©å�Šå¯¼ä½“æŒ‡æ•°æ— ETF替代)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh000688",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sh000688",
|
||||
"source_comment": "数据来源",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -76,7 +76,7 @@
|
||||
"end": "15:30"
|
||||
}
|
||||
],
|
||||
"display_label": "ä¸éŸ©å�Šå¯¼ä½?
|
||||
},
|
||||
"display_label": "中韩半导"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "创业板指",
|
||||
"card_order": 5,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sz399006",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sz399006",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -80,7 +80,7 @@
|
||||
"end": "15:00"
|
||||
}
|
||||
],
|
||||
"display_label": "创业�
|
||||
},
|
||||
"display_label": "创业"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "COMEX黄金",
|
||||
"card_order": 15,
|
||||
"card_type": "defensive",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "hf_GC",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://quote.eastmoney.com/globalfuture/GC00Y.html",
|
||||
"silver_symbol_comment": "白银代ç � (用于计算金银æ¯?",
|
||||
"silver_symbol_comment": "白银代码 (用于计算金银",
|
||||
"silver_symbol": "hf_SI",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"price_min_comment": "æœ€ä½Žä»·æ ¼é¢„è?,
|
||||
"price_min_comment": "最低价格预",
|
||||
"price_min": 4800,
|
||||
"price_max_comment": "æœ€é«˜ä»·æ ¼é¢„è?,
|
||||
"price_max_comment": "最高价格预",
|
||||
"price_max": 5500,
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 3,
|
||||
"gold_silver_ratio_min_comment": "金银比最低预è?,
|
||||
"gold_silver_ratio_min_comment": "金银比最低预",
|
||||
"gold_silver_ratio_min": 60,
|
||||
"gold_silver_ratio_max_comment": "金银比最高预è?,
|
||||
"gold_silver_ratio_max_comment": "金银比最高预",
|
||||
"gold_silver_ratio_max": 65
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -57,19 +57,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -86,7 +86,7 @@
|
||||
"end": "17:00"
|
||||
}
|
||||
],
|
||||
"display_label": "纽约�
|
||||
},
|
||||
"display_label": "纽约"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "COMEX白银",
|
||||
"card_order": 15,
|
||||
"card_type": "defensive",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "hf_SI",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://quote.eastmoney.com/globalfuture/SI00Y.html",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"price_min_comment": "æœ€ä½Žä»·æ ¼é¢„è?,
|
||||
"price_min_comment": "最低价格预",
|
||||
"price_min": 75,
|
||||
"price_max_comment": "æœ€é«˜ä»·æ ¼é¢„è?,
|
||||
"price_max_comment": "最高价格预",
|
||||
"price_max": 95,
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 3
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -51,19 +51,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -80,7 +80,7 @@
|
||||
"end": "17:00"
|
||||
}
|
||||
],
|
||||
"display_label": "纽约�
|
||||
},
|
||||
"display_label": "纽约"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "沪深300",
|
||||
"card_order": 3,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh000300",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sh000300",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "��斯工业指�,
|
||||
"name_comment": "显示的名",
|
||||
"name": "道琼斯工业指",
|
||||
"card_order": 13.5,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usDJI",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usDJI",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -76,7 +76,7 @@
|
||||
"end": "16:00"
|
||||
}
|
||||
],
|
||||
"display_label": "���
|
||||
},
|
||||
"display_label": "道琼"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "德国DAX30",
|
||||
"card_order": 10,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(iShares MSCI Germany ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usEWG",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usEWG",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule": "5m",
|
||||
"name": "黄金白银净值跟�,
|
||||
"name": "黄金白银净值跟",
|
||||
"card_order": 16,
|
||||
"card_type": "defensive",
|
||||
"gold_fund_code": "518880",
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled": true,
|
||||
\"key\": \"\"
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:{{PORT}}/api/calendar/events",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "恒生指数",
|
||||
"card_order": 8,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "hkHSI",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/hkHSI",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "�度NFTY(�ETF",
|
||||
"name_comment": "显示的名",
|
||||
"name": "印度NFTY(",
|
||||
"card_order": 11,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(India Nifty 50 ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usNFTY",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usNFTY",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "韩国综合指数",
|
||||
"card_order": 86,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(iShares MSCI South Korea ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usEWY",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usEWY",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "流动性状态机",
|
||||
"card_order": 6,
|
||||
"card_type": "overseas",
|
||||
"card_span_comment": "å�¡ç‰‡æ¨ªå�‘å� ä½�列数ï¼?为默认,2为跨两列",
|
||||
"card_span_comment": "卡片横向占位列数",
|
||||
"card_span": 2,
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "6h",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "us_liquidity_state",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://fred.stlouisfed.org/series/FEDFUNDS",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "fred",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -20,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 40,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 240,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 20
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -49,19 +49,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "纳斯达克",
|
||||
"card_order": 13,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usIXIC",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usIXIC",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "日经225指数",
|
||||
"card_order": 9,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(iShares MSCI Japan ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usEWJ",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usEWJ",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "沙特ETF(富时沙特)",
|
||||
"card_order": 17,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(Franklin FTSE Saudi Arabia ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usFLSA",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usFLSA",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "上证指数",
|
||||
"card_order": 1,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh000001",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sh000001",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "ä¸å›½ç¥žå�Ž-299æ³?,
|
||||
"name_comment": "显示的名",
|
||||
"name": "中国神华-299",
|
||||
"card_order": 1,
|
||||
"card_type": "299",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": false,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh601088_299",
|
||||
"source_symbol_comment": "用于拉取K线的真实代码",
|
||||
"source_symbol": "sh601088",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://quote.eastmoney.com/sh601088.html",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -38,19 +38,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -71,7 +71,7 @@
|
||||
"end": "15:00"
|
||||
}
|
||||
],
|
||||
"display_label": "ä¸å›½Aè‚?
|
||||
},
|
||||
"display_label": "中国A"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "东南亚科技(海峡时报)",
|
||||
"card_order": 20,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(Global X 东å�—äº?ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usASEA",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usASEA",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "标普500",
|
||||
"card_order": 12,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usINX",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usINX",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "科创50",
|
||||
"card_order": 4,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh000688",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sh000688",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "深证成指",
|
||||
"card_order": 2,
|
||||
"card_type": "domestic",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sz399001",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/sz399001",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "宝岛加权指数",
|
||||
"card_order": 87,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(iShares MSCI Taiwan ETF)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "usEWT",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://gu.qq.com/usEWT",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "yahoo_probe",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预è¦é˜ˆå€?(负数,如 -2 表示è·?2%)",
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -2,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120,
|
||||
"change_percent_up_comment": "涨幅预è¦é˜ˆå€?(æ£æ•°ï¼Œå¦‚ 2 表示æ¶?2%)",
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 2
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -47,19 +47,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name_comment": "显示的名",
|
||||
"name": "美国战争指数",
|
||||
"card_order": 14,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(技能内部定ä¹?",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "us_war_pizzint",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://fred.stlouisfed.org/series/VIXCLS",
|
||||
@@ -21,34 +21,34 @@
|
||||
"fail_alert_consecutive": 3,
|
||||
"source_comment": "数据来源",
|
||||
"source": "fred_vix",
|
||||
"alerts_comment": "预è¦é˜ˆå€¼é…�ç½?,
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"doughcon_critical_max_comment": "DOUGHCON 小于等于该值触发高风险预警",
|
||||
"doughcon_critical_max": 2,
|
||||
"doughcon_warning_max_comment": "DOUGHCON å°�于ç‰äºŽè¯¥å€¼è§¦å�‘风险抬å�‡é¢„è?,
|
||||
"doughcon_warning_max_comment": "DOUGHCON 小于等于该值触发风险抬升预",
|
||||
"doughcon_warning_max": 3,
|
||||
"vix_warning_min_comment": "VIX 大于ç‰äºŽè¯¥å€¼è§¦å�‘风险抬å�‡é¢„è?,
|
||||
"vix_warning_min_comment": "VIX 大于等于该值触发风险抬升预",
|
||||
"vix_warning_min": 22,
|
||||
"vix_critical_min_comment": "VIX 大于等于该值触发高风险预警",
|
||||
"vix_critical_min": 30,
|
||||
"index_jump_threshold_comment": "战争指数å�•次上跳达到该值触å�‘快速å�‡æ¸©é¢„è?,
|
||||
"index_jump_threshold_comment": "战争指数单次上跳达到该值触发快速升温预",
|
||||
"index_jump_threshold": 20,
|
||||
"urgent_abs_change_percent_comment": "严é‡�告è¦é˜ˆå€?(ç»�对波动百分æ¯?",
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严é‡�告è¦å†·å�´åˆ†é’Ÿæ•?,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": false,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -59,19 +59,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": false,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": false,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": false,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"name_comment": "显示的��,
|
||||
"name": "ä¸å›½é“¶è¡Œ-299æ³?,
|
||||
"name_comment": "显示的名",
|
||||
"name": "中国银行-299",
|
||||
"card_order": 2,
|
||||
"card_type": "299",
|
||||
"card_span_comment": "å�¡ç‰‡æ¨ªå�‘å� ä½�列数ï¼?为默认,2为跨两列",
|
||||
"card_span_comment": "卡片横向占位列数",
|
||||
"card_span": 2,
|
||||
"card_custom_class_comment": "中国银行卡片专属样式类名,仅用于前端独立展示",
|
||||
"card_custom_class": "card-zhonghang-299",
|
||||
"enabled_comment": "是��用该监�(true/false)",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是å�¦æ˜¾ç¤ºè¯¥å�¡ç‰?(false 则ä¸�拉å�–æ•°æ�®ä¹Ÿä¸�å°�å˜)",
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代ç ?(腾讯财ç»�或其他æº�)",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "sh601988_299",
|
||||
"source_symbol_comment": "用于拉取K线的真实代码",
|
||||
"source_symbol": "sh601988",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://quote.eastmoney.com/sh601988.html",
|
||||
"source_comment": "数��� (�tencent)",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "tencent",
|
||||
"box_window_comment": "收盘价箱体窗�,仅支�0/30/40",
|
||||
"box_window_comment": "收盘价箱体窗口,仅支",
|
||||
"box_window": 30,
|
||||
"box_confirm_days_comment": "箱体突破确认天数",
|
||||
"box_confirm_days": 2,
|
||||
"box_volume_ma_window_comment": "突破日成交量均量窗口",
|
||||
"box_volume_ma_window": 5,
|
||||
"box_volume_expand_ratio_comment": "çª�ç ´æ—¥æ”¾é‡�å€�数阈å€?,
|
||||
"box_volume_expand_ratio_comment": "突破日放量倍数阈",
|
||||
"box_volume_expand_ratio": 1.2,
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": false,
|
||||
"from_comment": "�件人邮�,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列�,
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP �务器��,
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
@@ -50,19 +50,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是���邮件��,
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": false,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": false,
|
||||
"push_targets_comment": "推é€�ç›®æ ‡åˆ—è¡?(支æŒ�多路推é€?",
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": false,
|
||||
"key_comment": "Weekly 工具�Key",
|
||||
\"key\": \"\"
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
@@ -83,7 +83,7 @@
|
||||
"end": "15:00"
|
||||
}
|
||||
],
|
||||
"display_label": "ä¸å›½Aè‚?
|
||||
},
|
||||
"display_label": "中国A"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ if (!fs.existsSync(cfgPath)) {
|
||||
enabled_comment: '是否启用 Yahoo 探针',
|
||||
enabled: true,
|
||||
base_url_comment: 'Yahoo 探针基础地址(不带结尾斜杠)',
|
||||
base_url: 'http://47.252.30.127:19071',
|
||||
base_url: 'https://yang-probe.umersoft.com',
|
||||
token_comment: 'Yahoo 探针 token,仅后端使用,需在 config/markets.json 中配置',
|
||||
token: '',
|
||||
endpoint_comment: 'Yahoo 探针统一路由',
|
||||
|
||||
@@ -175,10 +175,10 @@ const readMarketsConfig = () => {
|
||||
const getYahooProbeConfig = () => {
|
||||
const cfg = readMarketsConfig()
|
||||
const raw = cfg && typeof cfg.yahoo_probe === 'object' ? cfg.yahoo_probe : {}
|
||||
const baseUrl = String(raw.base_url || 'http://47.252.30.127:19071').trim().replace(/\/$/, '')
|
||||
const baseUrl = String(raw.base_url || 'https://yang-probe.umersoft.com').trim().replace(/\/$/, '')
|
||||
return {
|
||||
enabled: raw.enabled !== false,
|
||||
base_url: baseUrl || 'http://47.252.30.127:19071',
|
||||
base_url: baseUrl || 'https://yang-probe.umersoft.com',
|
||||
token: String(raw.token || '').trim(),
|
||||
quote_endpoint: String(raw.quote_endpoint || raw.endpoint || '/probe/yahoo').trim() || '/probe/yahoo',
|
||||
chart_endpoint: String(raw.chart_endpoint || raw.endpoint || '/probe/yahoo').trim() || '/probe/yahoo',
|
||||
|
||||
@@ -228,10 +228,10 @@ function getConfig() {
|
||||
function getAlphaVantageConfig() {
|
||||
const cfg = getConfig()
|
||||
const raw = cfg && typeof cfg.alphavantage_probe === 'object' ? cfg.alphavantage_probe : {}
|
||||
const baseUrl = String(raw.base_url || 'http://47.252.30.127:19071').trim().replace(/\/$/, '')
|
||||
const baseUrl = String(raw.base_url || 'https://yang-probe.umersoft.com').trim().replace(/\/$/, '')
|
||||
return {
|
||||
enabled: raw.enabled !== false,
|
||||
base_url: baseUrl || 'http://47.252.30.127:19071',
|
||||
base_url: baseUrl || 'https://yang-probe.umersoft.com',
|
||||
token: String(raw.token || '').trim(),
|
||||
candle_endpoint: String(raw.candle_endpoint || '/probe/alphavantage/candle').trim() || '/probe/alphavantage/candle',
|
||||
request_timeout_ms: Math.max(5000, Number(raw.request_timeout_ms || 20000)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+45
-25
@@ -890,7 +890,6 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
return weatherCache.payload
|
||||
}
|
||||
|
||||
// 使用AI来获取天气信息,而不是依赖付费API
|
||||
const locInfo = cfg.location || {}
|
||||
const city = safeString(locInfo.city) || '上海'
|
||||
const district = safeString(locInfo.district) || '宝山区'
|
||||
@@ -916,14 +915,47 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
]
|
||||
}
|
||||
|
||||
// 使用AI来生成更准确的天气信息
|
||||
// 优先从 data_gateway 读取外部 Agent 预写入的天气数据
|
||||
const ai = getAiConfig()
|
||||
let adviceText = ''
|
||||
let reviewedByAi = false
|
||||
let dataGatewayUsed = false
|
||||
|
||||
if (ai.enabled && ai.apiKey) {
|
||||
try {
|
||||
const dataGateway = require('./data_gateway/store')
|
||||
const row = dataGateway.queryLatest('weather')
|
||||
if (row && row.data && typeof row.data === 'object') {
|
||||
const d = row.data
|
||||
// 合并 data_gateway 数据到 payload(外部 Agent 写入的字段覆盖默认值)
|
||||
if (d.text !== undefined) payload.text = safeString(d.text)
|
||||
if (d.temp !== undefined) payload.temp = safeString(d.temp)
|
||||
if (d.feelsLike !== undefined) payload.feelsLike = safeString(d.feelsLike)
|
||||
if (d.humidity !== undefined) payload.humidity = safeString(d.humidity)
|
||||
if (d.windDir !== undefined) payload.windDir = safeString(d.windDir)
|
||||
if (d.windScale !== undefined) payload.windScale = safeString(d.windScale)
|
||||
if (d.icon !== undefined) payload.icon = safeString(d.icon)
|
||||
if (d.updateTime !== undefined) payload.updateTime = safeString(d.updateTime)
|
||||
if (d.location !== undefined) payload.location = { ...payload.location, ...d.location }
|
||||
if (Array.isArray(d.daily)) {
|
||||
payload.daily = d.daily.slice(0, 3).map(day => ({
|
||||
fxDate: safeString(day.fxDate || day.fxdate || ''),
|
||||
textDay: safeString(day.textDay || day.textday || ''),
|
||||
textNight: safeString(day.textNight || day.textnight || ''),
|
||||
tempMin: safeString(day.tempMin || day.tempmin || ''),
|
||||
tempMax: safeString(day.tempMax || day.tempmax || '')
|
||||
}))
|
||||
}
|
||||
// 记录 data_gateway 更新时间(UTC),前端转为北京时间
|
||||
if (row.created_at) payload._data_gateway_updated_at = safeString(row.created_at)
|
||||
dataGatewayUsed = true
|
||||
}
|
||||
} catch (e) {
|
||||
// data_gateway 不可用时静默回退,不影响 plant_home 正常运行
|
||||
}
|
||||
|
||||
// 如果 data_gateway 没有数据,回退到 AI 获取天气(保留兼容)
|
||||
if (!dataGatewayUsed && ai.enabled && ai.apiKey) {
|
||||
try {
|
||||
// 构建一个请求天气信息的消息
|
||||
const weatherPrompt = `请基于以下位置信息提供当前天气情况和未来3天天气预报:位置:${city}${district}。请提供以下信息:当前天气状况、温度、体感温度、湿度、风向风力,以及未来3天的天气预报(包括每日最高最低温度和天气状况)。请以JSON格式返回,格式如下:{"current":{"text":"天气状况","temp":"温度","feelsLike":"体感温度","humidity":"湿度","windDir":"风向","windScale":"风力"},"daily":[{"fxDate":"日期","textDay":"白天天气","textNight":"夜间天气","tempMin":"最低温度","tempMax":"最高温度"}]}`
|
||||
|
||||
const aiResponse = await callDashscope({
|
||||
@@ -935,12 +967,10 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
]
|
||||
})
|
||||
|
||||
// 尝试解析AI返回的天气信息
|
||||
const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)```|```([\s\S]*?)```/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const jsonData = JSON.parse(jsonMatch[1] || jsonMatch[2])
|
||||
|
||||
if (jsonData && jsonData.current) {
|
||||
payload.text = safeString(jsonData.current.text)
|
||||
payload.temp = safeString(jsonData.current.temp)
|
||||
@@ -949,7 +979,6 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
payload.windDir = safeString(jsonData.current.windDir)
|
||||
payload.windScale = safeString(jsonData.current.windScale)
|
||||
}
|
||||
|
||||
if (jsonData && Array.isArray(jsonData.daily)) {
|
||||
payload.daily = jsonData.daily.slice(0, 3).map(day => ({
|
||||
fxDate: safeString(day.fxDate),
|
||||
@@ -963,8 +992,14 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
console.warn('AI天气JSON解析失败:', parseError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成植物养护建议
|
||||
} catch (error) {
|
||||
console.warn('AI天气获取失败,使用默认值:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成植物养护建议(plant_home 保留本地 AI 生成)
|
||||
if (ai.enabled && ai.apiKey) {
|
||||
try {
|
||||
adviceText = await callDashscope({
|
||||
model: ai.model,
|
||||
temperature: 0.2,
|
||||
@@ -975,22 +1010,7 @@ const fetchWeather = async ({ force = false } = {}) => {
|
||||
})
|
||||
reviewedByAi = !!adviceText
|
||||
} catch (error) {
|
||||
console.warn('AI天气获取失败,使用默认值:', error.message)
|
||||
// 如果AI调用失败,使用默认的天气信息
|
||||
if (ai.autoWeatherReview) {
|
||||
try {
|
||||
// 使用AI基于默认天气生成建议
|
||||
adviceText = await callDashscope({
|
||||
model: ai.model,
|
||||
temperature: 0.2,
|
||||
messages: [
|
||||
{ role: 'system', content: '输出简短、克制、实用的植物养护提醒。不要编造天气。' },
|
||||
{ role: 'user', content: buildWeatherAdvicePrompt(payload) }
|
||||
]
|
||||
})
|
||||
reviewedByAi = !!adviceText
|
||||
} catch {}
|
||||
}
|
||||
console.warn('养护建议生成失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,109 @@ const formatRow = (row) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 登录鉴权 =====
|
||||
const AUTH_COOKIE = 'private_clipboard_auth'
|
||||
const AUTH_MAX_AGE_MS = 24 * 3600 * 1000 // 1天
|
||||
const AUTH_HMAC_SECRET = process.env.PRIVATE_CLIPBOARD_AUTH_SECRET || 'pc-auth-secret-change-me'
|
||||
|
||||
const signAuthPayload = (username) => {
|
||||
const payload = { sub: username, iat: Date.now() }
|
||||
const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
const sig = crypto.createHmac('sha256', AUTH_HMAC_SECRET).update(payloadStr).digest('base64url')
|
||||
return `${payloadStr}.${sig}`
|
||||
}
|
||||
|
||||
const verifyAuthPayload = (token) => {
|
||||
if (!token || typeof token !== 'string') return null
|
||||
const [payloadStr, sig] = token.split('.')
|
||||
if (!payloadStr || !sig) return null
|
||||
const expected = crypto.createHmac('sha256', AUTH_HMAC_SECRET).update(payloadStr).digest('base64url')
|
||||
if (expected.length !== sig.length) return null
|
||||
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return null
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(payloadStr, 'base64url').toString())
|
||||
if (!payload || typeof payload !== 'object') return null
|
||||
return payload
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
const parseCookies = (cookieHeader) => {
|
||||
const cookies = {}
|
||||
if (!cookieHeader) return cookies
|
||||
cookieHeader.split(';').forEach(part => {
|
||||
const idx = part.indexOf('=')
|
||||
if (idx < 0) return
|
||||
const key = part.slice(0, idx).trim()
|
||||
const val = decodeURIComponent(part.slice(idx + 1).trim())
|
||||
if (key) cookies[key] = val
|
||||
})
|
||||
return cookies
|
||||
}
|
||||
|
||||
const hasAuth = (req) => {
|
||||
const cookies = parseCookies(req.headers.cookie || '')
|
||||
const token = cookies[AUTH_COOKIE]
|
||||
if (!token) return false
|
||||
const payload = verifyAuthPayload(token)
|
||||
return !!payload
|
||||
}
|
||||
|
||||
const mustAuth = (req, res, next) => {
|
||||
if (hasAuth(req)) return next()
|
||||
return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
}
|
||||
|
||||
const readLocalCreds = () => {
|
||||
try {
|
||||
const os = require('os')
|
||||
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
|
||||
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
|
||||
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
|
||||
try {
|
||||
if (fs.existsSync(usersRoot)) {
|
||||
fs.readdirSync(usersRoot).forEach(name => {
|
||||
const dir = path.join(usersRoot, name)
|
||||
if (dir !== userProfile && fs.existsSync(dir)) {
|
||||
candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
let content = null
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break }
|
||||
}
|
||||
if (!content) return {}
|
||||
const creds = {}
|
||||
content.split(/\r?\n/).forEach(line => {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) return
|
||||
const idx = trimmed.indexOf('=')
|
||||
if (idx > 0) creds[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim()
|
||||
})
|
||||
return creds
|
||||
} catch { return {} }
|
||||
}
|
||||
|
||||
const getExpectedUsername = () => {
|
||||
const fromEnv = String(process.env.PRIVATE_CLIPBOARD_USERNAME || '').trim()
|
||||
if (fromEnv) return fromEnv
|
||||
const creds = readLocalCreds()
|
||||
return String(creds.PRIVATE_CLIPBOARD_USERNAME || '').trim()
|
||||
}
|
||||
const getExpectedPassword = () => {
|
||||
const fromEnv = String(process.env.PRIVATE_CLIPBOARD_PASSWORD || '').trim()
|
||||
if (fromEnv) return fromEnv
|
||||
const creds = readLocalCreds()
|
||||
return String(creds.PRIVATE_CLIPBOARD_PASSWORD || '').trim()
|
||||
}
|
||||
const getExpectedToken = () => {
|
||||
const fromEnv = String(process.env.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
|
||||
if (fromEnv) return fromEnv
|
||||
const creds = readLocalCreds()
|
||||
return String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
|
||||
}
|
||||
|
||||
const createRouter = () => {
|
||||
const router = express.Router()
|
||||
|
||||
@@ -124,6 +227,66 @@ const createRouter = () => {
|
||||
next()
|
||||
})
|
||||
|
||||
// ===== Auth 路由(白名单,不需要登录)=====
|
||||
router.post('/auth/login', (req, res) => {
|
||||
try {
|
||||
const username = String((req.body && req.body.username) || '').trim()
|
||||
const password = String((req.body && req.body.password) || '')
|
||||
const expectedUser = getExpectedUsername()
|
||||
const expectedPass = getExpectedPassword()
|
||||
if (!expectedUser || !expectedPass) {
|
||||
return res.status(500).json({ ok: false, error: 'server_not_configured' })
|
||||
}
|
||||
if (username !== expectedUser || password !== expectedPass) {
|
||||
return res.status(401).json({ ok: false, error: 'invalid_credentials' })
|
||||
}
|
||||
const token = signAuthPayload(username)
|
||||
res.cookie(AUTH_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: AUTH_MAX_AGE_MS,
|
||||
path: '/'
|
||||
})
|
||||
res.json({ ok: true })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/auth/logout', (req, res) => {
|
||||
res.clearCookie(AUTH_COOKIE, { path: '/' })
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
router.get('/auth/me', (req, res) => {
|
||||
if (hasAuth(req)) {
|
||||
return res.json({ ok: true, authorized: true })
|
||||
}
|
||||
res.json({ ok: false, authorized: false })
|
||||
})
|
||||
|
||||
router.get('/auth/local_creds', (req, res) => {
|
||||
try {
|
||||
const token = String((req.query && req.query.token) || '').trim()
|
||||
const expectedToken = getExpectedToken()
|
||||
if (!expectedToken || token !== expectedToken) {
|
||||
return res.status(401).json({ ok: false, error: 'invalid_token' })
|
||||
}
|
||||
const creds = readLocalCreds()
|
||||
const username = String(creds.PRIVATE_CLIPBOARD_USERNAME || '').trim()
|
||||
const password = String(creds.PRIVATE_CLIPBOARD_PASSWORD || '').trim()
|
||||
if (!username || !password) {
|
||||
return res.status(500).json({ ok: false, error: 'no_local_creds' })
|
||||
}
|
||||
res.json({ ok: true, creds: { username, password } })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 以下业务路由需要登录 =====
|
||||
router.use(mustAuth)
|
||||
|
||||
router.get('/list', (req, res) => {
|
||||
try {
|
||||
const rows = getDb().prepare(`
|
||||
|
||||
Reference in New Issue
Block a user