refactor: unify data preparations and research input snapshots
Deploy production / deploy (push) Successful in 53s

This commit is contained in:
yuxuanhui
2026-09-12 01:24:02 +08:00
parent 849f86fef7
commit 394438e753
82 changed files with 4146 additions and 2076 deletions
+1 -1
View File
@@ -222,7 +222,7 @@ export function ExperimentView({
})
}
>
{input.dataset_id} · {input.scope.region}/{input.scope.universe}/D
{input.name} · {input.scope.region}/{input.scope.universe}/D
{input.scope.delay}
</Button>
))}
+17 -17
View File
@@ -1,3 +1,7 @@
import {
ResearchDataInput,
researchSelection,
} from "../preparations/ResearchDataInput";
import { ResearchList, ResearchDetail } from "./ResearchList";
import { useEffect, useRef, useState } from "react";
import {
@@ -66,9 +70,7 @@ export function FeaturesPage({
`/research/assets?kind=feature&q=${encodeURIComponent(q)}&offset=${(page - 1) * 25}`,
{ signal: controller.signal },
),
api<{ items: InputSnapshot[] }>("/research/inputs", {
signal: controller.signal,
}),
Promise.resolve({ items: inputs }),
])
.then(([list, fixed]) => {
setItems(list.items);
@@ -128,7 +130,7 @@ export function FeaturesPage({
body: JSON.stringify({
kind: "feature",
version: asset?.version,
content: draft,
content: { ...draft, ...researchSelection(draft.input_ids, inputs) },
}),
},
);
@@ -317,7 +319,7 @@ export function FeaturesPage({
.filter((i) => draft.input_ids.includes(i.id))
.map((i) => ({
id: i.id,
name: `${i.dataset_id} · ${i.scope.region}/${i.scope.universe}/D${i.scope.delay} · ${i.field_ids.length} 字段 · ${i.id}`,
name: `${i.name} · ${i.scope.region}/${i.scope.universe}/D${i.scope.delay} · ${i.field_ids.length} 字段 · ${i.id}`,
}))}
disabled={!!busy}
busy={busy === "generate"}
@@ -330,7 +332,7 @@ export function FeaturesPage({
await post<FeatureAsset>("/research/generate", {
name: draft.name,
hypothesis: draft.hypothesis,
input_ids: draft.input_ids,
...researchSelection(draft.input_ids, inputs),
method: "feature",
}),
);
@@ -347,23 +349,21 @@ export function FeaturesPage({
</section>
<label>
固定输入
<ResearchSelect
label="特征固定输入"
multiple
filter
value={draft.input_ids}
optionList={inputs.map((i) => ({
value: i.id,
label: `${i.dataset_id} · ${i.scope.region}/${i.scope.universe}/D${i.scope.delay} · ${i.field_ids.length} 字段 · ${i.id.slice(0, 8)}`,
}))}
onChange={(v) => setDraft({ ...draft, input_ids: v as string[] })}
<ResearchDataInput
label="选择特征数据准备"
ids={draft.input_ids}
inputs={inputs}
onChange={(ids, rows) => {
setInputs(rows);
setDraft({ ...draft, input_ids: ids });
}}
/>
</label>
{inputs
.filter((i) => draft.input_ids.includes(i.id))
.map((i) => (
<details key={i.id}>
<summary>{i.dataset_id} 的固定字段</summary>
<summary>{i.name} 的固定字段</summary>
<p className="muted">{i.field_ids.join("、")}</p>
</details>
))}
+24 -21
View File
@@ -1,3 +1,7 @@
import {
ResearchDataInput,
researchSelection,
} from "../preparations/ResearchDataInput";
import { useEffect, useState } from "react";
import {
Banner,
@@ -54,7 +58,7 @@ export function FlowLaunchForm({
useEffect(() => {
const c = new AbortController();
Promise.all([
api<{ items: InputSnapshot[] }>("/research/inputs", { signal: c.signal }),
Promise.resolve({ items: inputs }),
api<{ items: Asset[] }>("/research/assets?kind=template&limit=100", {
signal: c.signal,
}),
@@ -73,7 +77,7 @@ export function FlowLaunchForm({
setConfirmation({
request_id: crypto.randomUUID(),
name,
input_ids: ids,
...researchSelection(ids, inputs),
hypothesis,
settings,
budget,
@@ -133,22 +137,17 @@ export function FlowLaunchForm({
</label>
<label>
固定数据范围
<ResearchSelect
label="自动研究固定输入"
multiple
filter
value={ids}
optionList={inputs.map((i) => ({
value: i.id,
label: `${i.dataset_id} · ${i.scope.region}/${i.scope.universe}/D${i.scope.delay} · ${i.id.slice(0, 8)}`,
}))}
onChange={(v) => {
const next = v as string[];
setIds(next);
const first = inputs.find((i) => i.id === next[0]);
<ResearchDataInput
label="选择研究数据准备"
ids={ids}
inputs={inputs}
onChange={(ids, rows) => {
setInputs(rows);
setIds(ids);
const first = rows.find((r) => r.id === ids[0]);
if (first)
setSettings((s) => ({
...s,
setSettings((old) => ({
...old,
region: first.scope.region,
universe: first.scope.universe,
delay: first.scope.delay,
@@ -290,8 +289,10 @@ export function FlowLaunchForm({
</p>
)}
<p>
{confirmation.input_ids.length} 个固定输入 · {settings.region}/
{settings.universe}/D{settings.delay}
{confirmation.input_ids.length +
(confirmation.preparation_refs?.length ?? 0)}{" "}
个固定输入 · {settings.region}/{settings.universe}/D
{settings.delay}
</p>
<ul>
{confirmation.input_ids.map((id) => {
@@ -299,7 +300,7 @@ export function FlowLaunchForm({
return (
<li key={id}>
{input
? `${input.dataset_id} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay}`
? `${input.name} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay}`
: id}{" "}
· {id.slice(0, 8)}
</li>
@@ -315,7 +316,9 @@ export function FlowLaunchForm({
{confirmation.template_id && (
<p>
初始模板:
{templates.find((t) => t.id === confirmation.template_id)?.name}{" "}
{
templates.find((t) => t.id === confirmation.template_id)?.name
}{" "}
· v{confirmation.template_version}
</p>
)}
+31 -28
View File
@@ -1,3 +1,7 @@
import {
ResearchDataInput,
researchSelection,
} from "../preparations/ResearchDataInput";
import { ResearchList, ResearchDetail } from "./ResearchList";
import { ResearchSelect } from "./ResearchSelect";
import { useEffect, useRef, useState } from "react";
@@ -101,7 +105,7 @@ export function ResearchWorkspace({
api<{ items: Asset[]; total: number }>(
`/research/assets?kind=template&limit=25&offset=${assetPage * 25}&q=${encodeURIComponent(search)}`,
),
api<{ items: InputSnapshot[] }>("/research/inputs"),
Promise.resolve({ items: inputs }),
api<{ items: typeof history; total: number }>(
`/research/experiments?limit=25&offset=${historyPage * 25}`,
),
@@ -129,7 +133,11 @@ export function ResearchWorkspace({
research_asset_id: asset?.id,
research_experiment_id: experiment?.id,
alpha_id: parent.split(/[,,\s]+/)[0] || undefined,
template_input_id: inputIds.length === 1 ? inputIds[0] : undefined,
input_snapshot_id:
inputIds.length === 1 &&
!inputs.find((i) => i.id === inputIds[0])?.preparation_ref
? inputIds[0]
: undefined,
});
}, [active, page, asset?.id, experiment?.id, parent, inputIds, onContext]);
useEffect(() => {
@@ -180,17 +188,6 @@ export function ResearchWorkspace({
setMethod("structure");
}
}, [active, action]);
function selectInputs(ids: string[]) {
setInputIds(ids);
const first = inputs.find((item) => item.id === ids[0]);
if (first)
setSettings((old) => ({
...old,
region: first.scope.region,
universe: first.scope.universe,
delay: first.scope.delay,
}));
}
async function refreshSettings() {
const data = await post<{
content: {
@@ -236,7 +233,7 @@ export function ResearchWorkspace({
const next = await post<Asset>("/research/generate", {
name: template.name,
hypothesis,
input_ids: inputIds,
...researchSelection(inputIds, inputs),
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
method: page === "variants" ? "structure" : "template",
});
@@ -274,7 +271,7 @@ export function ResearchWorkspace({
const next = await post<Experiment>("/research/experiments", {
asset_id: asset!.id,
version: asset!.version,
input_ids: inputIds,
...researchSelection(inputIds, inputs),
hypothesis,
settings,
mode,
@@ -580,20 +577,26 @@ export function ResearchWorkspace({
)}
<label>
固定研究输入
<ResearchSelect
multiple
filter
label="固定研究输入"
value={inputIds}
optionList={inputs.map((input) => ({
value: input.id,
label: `${input.dataset_id} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id.slice(0, 8)}`,
}))}
onChange={(value) => selectInputs(value as string[])}
<ResearchDataInput
label="选择数据准备"
ids={inputIds}
inputs={inputs}
onChange={(ids, rows) => {
setInputs(rows);
setInputIds(ids);
const first = rows.find((r) => r.id === ids[0]);
if (first)
setSettings((old) => ({
...old,
region: first.scope.region,
universe: first.scope.universe,
delay: first.scope.delay,
}));
}}
/>
</label>
<p className="research-hint">
在数据目录中保存字段选择。跨数据集分别关联输入,跨市场使用目标范围的独立输入。
从数据准备选择集合;每个集合可包含同范围下多个数据集的字段。
</p>
{page === "variants" && method === "settings" ? (
<label>
@@ -624,7 +627,7 @@ export function ResearchWorkspace({
references={[
...selectedInputs.map((input) => ({
id: input.id,
name: `${input.dataset_id} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id}`,
name: `${input.name} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id}`,
})),
...(page === "variants" && parent
? [
@@ -857,7 +860,7 @@ export function ResearchWorkspace({
setExperiment(
await post("/research/variants/settings", {
alpha_id: parent,
input_ids: inputIds,
...researchSelection(inputIds, inputs),
...(hypothesis ? { hypothesis } : {}),
}),
);
+12 -3
View File
@@ -62,19 +62,28 @@ export function SourceDetails({
打开研究会话
</Button>
)}
{source.template_input_id && (
{[
...new Set([
...(source.input_snapshot_ids ?? []),
...(source.input_snapshot_id ? [source.input_snapshot_id] : []),
]),
].map((id, index) => (
<Button
key={id}
onClick={() =>
onAction({
type: "open_research_input",
input_id: source.template_input_id!,
input_id: id,
nonce: Date.now(),
})
}
>
查看研究输入
{(source.input_snapshot_ids?.length ?? 0) > 1
? ` ${index + 1}`
: ""}
</Button>
)}
))}
{source.parent_run_id && (
<Button
onClick={() =>
+1
View File
@@ -21,6 +21,7 @@ export type FlowLaunch = {
request_id: string;
name: string;
input_ids: string[];
preparation_refs?: { id: string; version: number }[];
hypothesis: string;
settings: SimulationSettings;
budget: Budget;
+7 -2
View File
@@ -1,3 +1,4 @@
import type { FieldRecord, PreparationRef } from "../preparations/types";
import type { Candidate } from "../backtests/types";
export type Variable = {
kind:
@@ -43,10 +44,14 @@ export type InputSnapshot = {
universe: string;
delay: 0 | 1;
};
dataset_id: string;
name: string;
dataset_ids: string[];
fields: FieldRecord[];
preparation_ref?: PreparationRef;
field_ids: string[];
field_types: Record<string, string>;
collection_version: string;
preparation_id?: string;
preparation_version?: number;
created_at: string;
};
export type ResearchCandidate = Candidate & {