Support selectable demo exchanges and configurable analysis limits

Persist manual requests until exchange confirmation, preserve stop protection, and reconcile fills before settling plans. Tighten model stream, crawler, skill, research accounting, and frontend race handling, with regression coverage. Keep queued analyses pending until a fresh account snapshot is available.
This commit is contained in:
eezd committed 2026-10-20 18:45:00 +08:00
1 parent fcaedfb358
commit 285b3952d7
33 files changed
+1890 -325

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"
import { createRequire, Module } from "node:module"
import { runInNewContext } from "node:vm"
import ts from "typescript"
import * as vue from "vue"
import { compileScript, parse } from "vue/compiler-sfc"
const require = createRequire(import.meta.url)
interface Version {
id: number
content: string
version_no: number
}
function setupPage(page: string, modules: Record<string, unknown>, confirm: () => Promise<unknown> = async () => {}) {
const filename = new URL(`../src/pages/${page}/index.vue`, import.meta.url)
const { descriptor } = parse(readFileSync(filename, "utf8"))
const script = compileScript(descriptor, { id: page }).content
const code = ts.transpileModule(script, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const globals = {
ref: vue.ref,
reactive: vue.reactive,
computed: vue.computed,
watch: vue.watch,
onMounted() {},
onBeforeUnmount() {},
onBeforeRouteLeave() {},
setInterval() {},
ElMessage: { success() {} },
ElMessageBox: { confirm }
}
const mod = new Module(filename.pathname)
const load = (path: string) => path in modules ? modules[path] : require(path)
runInNewContext(code, { require: load, module: mod, exports: mod.exports, ...globals }, { filename: `${page}.vue` })
return mod.exports.default.setup({}, { expose() {} })
}
function skills(api: Record<string, unknown>, confirm?: () => Promise<unknown>) {
return setupPage("skills", {
"@/apis/skills": api,
"@/components/SkillTree.vue": {},
"@/components/SkillVersionDrawer.vue": {},
"@/utils/markdown": { renderMarkdown: (text: string) => text }
}, confirm)
}
async function checkSkillSwitch() {
const a = Promise.withResolvers<Version[]>()
const b = Promise.withResolvers<Version[]>()
const drafts: { id: number, content: string }[] = []
const nodes = [{ id: 1, kind: "skill" }, { id: 2, kind: "skill" }]
const page = skills({
listSkillTree: async () => nodes,
listSkillVersions: (id: number) => id === 1 ? a.promise : b.promise,
saveSkillDraft: async (id: number, content: string) => {
drafts.push({ id, content })
return { id: 20, version_no: 2, content }
}
})
await page.loadTree()
const first = page.select(nodes[0])
const second = page.select(nodes[1])
assert.equal(await page.saveDraft(), null)
b.resolve([{ id: 2, content: "B 原文", version_no: 1 }])
await second
a.resolve([{ id: 1, content: "A 迟到", version_no: 1 }])
await first
assert.equal(page.content.value, "B 原文")
page.content.value = "B 修改"
await page.saveDraft()
assert.equal(drafts.length, 1)
assert.equal(drafts[0].id, 2)
assert.equal(drafts[0].content, "B 修改")
}
async function checkSkillReload() {
const old = Promise.withResolvers<Version[]>()
let calls = 0
const page = skills({
listSkillTree: async () => [{ id: 1, kind: "skill" }],
listSkillVersions: () => ++calls === 1 ? old.promise : Promise.resolve([{ id: 2, content: "最新" }])
})
await page.loadTree()
page.selectedId.value = 1
const first = page.loadContent()
await page.loadContent()
old.resolve([{ id: 1, content: "旧内容", version_no: 1 }])
await first
assert.equal(page.content.value, "最新")
}
async function checkPublishSwitch() {
const saved = Promise.withResolvers<Version>()
const published: [number, number][] = []
const nodes = [{ id: 1, kind: "skill" }, { id: 2, kind: "skill" }]
const page = skills({
listSkillTree: async () => nodes,
listSkillVersions: async (id: number) => [{ id, content: `内容 ${id}` }],
saveSkillDraft: () => saved.promise,
publishSkillVersion: async (id: number, version: number) => published.push([id, version])
})
await page.loadTree()
await page.select(nodes[0])
page.content.value = "待发布修改"
const publishing = page.publishLatest()
await page.select(nodes[1])
saved.resolve({ id: 10, version_no: 2, content: "待发布修改" })
await publishing
assert.deepEqual(published, [])
assert.equal(page.content.value, "内容 2")
}
async function checkTickerSwitch() {
const btc = Promise.withResolvers<Record<string, string>>()
const eth = Promise.withResolvers<Record<string, string>>()
const page = setupPage("market", {
"@/apis/market": { getTicker: (_: string, symbol: string) => symbol === "BTCUSDT" ? btc.promise : eth.promise },
"@/utils/storage": { usePersisted: (_: string, value: unknown) => vue.ref(value) },
"./chart": { DEFAULT_FAVORITE_INTERVALS: [], INTERVAL_LABELS: {}, MAIN_INDICATORS: [], SUB_INDICATORS: [] },
"./useMarketSocket": { useMarketSocket: () => ({ connected: vue.ref(true), subscribe() {} }) },
"./components/AddSymbolDialog.vue": {},
"./components/SymbolManager.vue": {},
"klinecharts": {}
})
const first = page.loadTicker()
page.selection.value = { market: "um", symbol: "ETHUSDT" }
await vue.nextTick()
eth.resolve({ lastPrice: "2000", highPrice: "2100", lowPrice: "1900", volume: "5", quoteVolume: "10000", priceChangePercent: "1" })
await vue.nextTick()
await new Promise(resolve => setImmediate(resolve))
btc.resolve({ lastPrice: "80000", highPrice: "81000", lowPrice: "79000", volume: "1", quoteVolume: "80000", priceChangePercent: "2" })
await first
assert.equal(page.ticker.last, 2000)
assert.equal(page.ticker.changePercent, 1)
}
async function checkPublishConfirmationSwitch() {
const confirmation = Promise.withResolvers<void>()
const published: [number, number][] = []
const nodes = [{ id: 1, kind: "skill" }, { id: 2, kind: "skill" }]
const page = skills({
listSkillTree: async () => nodes,
listSkillVersions: async (id: number) => [{ id, content: `内容 ${id}`, version_no: 1 }],
publishSkillVersion: async (id: number, version: number) => published.push([id, version])
}, () => confirmation.promise)
await page.loadTree()
await page.select(nodes[0])
const publishing = page.publishLatest()
await Promise.resolve()
await page.select(nodes[1])
confirmation.resolve()
await publishing
assert.deepEqual(published, [])
}
await checkSkillSwitch()
await checkSkillReload()
await checkPublishSwitch()
await checkTickerSwitch()
await checkPublishConfirmationSwitch()
console.log("页面异步竞态:5 项回归检查通过")