Compare commits
2
Commits
5003a44d2b
...
d65b6bc84f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d65b6bc84f | ||
|
|
c8c61d29bf |
@@ -19,7 +19,8 @@
|
||||
- 实际 HTTPS 查询复现:名称“诺普信”无结果,但代码 002215 和名称“诺 普 信”均返回该深交所股票。数据源在名称中填充了空格,原 SQL LIKE 连续匹配漏掉无空格查询,证券并未漏同步。
|
||||
- 查询统一忽略名称与输入中的空格、制表符和全角空格,保持原始源名称;转义 LIKE 通配符为字面匹配,避免输入 % 或 _ 返回全部股票。管理端无搜索结果文案与无同步数据区分。真实 PostgreSQL 隔离 schema 的 race 回归覆盖名称/代码/空格和通配符,go vet、管理端类型检查/build/embed 通过;已通过 PR #6 合并,待 CI 上线后实查。
|
||||
|
||||
- 按用户补充要求,管理端行情列表/详情、候选队列、审核详情/确认、总览观察与信号卡将代码与名称并列展示,代码使用等宽字形;选股、回测、策略证券列表以及 App 候选/计划/持仓/成交原已有代码,继续保留。管理端统一 StockIdentity 组件,类型检查/build/embed 通过。
|
||||
- 按用户补充要求,管理端行情列表/详情、候选队列、审核详情/确认、总览观察与信号卡将代码与名称并列展示,代码使用等宽字形;选股、回测详情、策略证券列表以及 App 候选/计划/持仓/成交原已有代码,继续保留。另补齐回测任务列表与 AI 任务执行中/失败时的股票身份:代码从不可覆盖输入/候选快照读取,不等待模型输出。管理端统一 StockIdentity 组件,类型检查/build/embed 通过。
|
||||
- 完整 Go race 与 go vet、管理端类型检查/build/embed 通过;新增列表集成检查保留证券代码且不返回完整回测输入。真实 gpt-6.1-sol 使用中洲控股的现有日线完成分析与独立复核(约 40 秒合计),两次结构化输出及证据索引校验通过;仅只读连接验证,没有写入 AI 发布事实。
|
||||
- 用户最新指示先不处理 App 安装,保留已有 IPA 产物;当前优先验证名称搜索、股票代码展示与模型运行配置。
|
||||
|
||||
## App Expo Go 真机入口(2026-10-09)
|
||||
|
||||
@@ -129,6 +129,11 @@ func testCandidateAI(t *testing.T, ctx context.Context, pool *pgxpool.Pool, admi
|
||||
if err != nil || len(results) != 4 {
|
||||
t.Fatal("durable results missing", err, len(results))
|
||||
}
|
||||
for _, result := range results {
|
||||
if result.Name != "Fixture" || result.Symbol != "000042" || result.Exchange != "SZSE" {
|
||||
t.Fatal("analysis task lost candidate identity")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type rejectModel struct{ calls int }
|
||||
|
||||
@@ -95,6 +95,10 @@ func testStrategyBacktest(t *testing.T, ctx context.Context, pool *pgxpool.Pool,
|
||||
if e != nil || done.Status != "COMPLETED" || done.FinishedAt == nil || len(done.Result) == 0 {
|
||||
t.Fatalf("completed job: %v", e)
|
||||
}
|
||||
listed, e := repo.List(ctx)
|
||||
if e != nil || len(listed) != 1 || listed[0].Symbol != bi.Symbol || listed[0].Input != nil || listed[0].Result != nil {
|
||||
t.Fatal("backtest list must retain the symbol without returning large inputs")
|
||||
}
|
||||
if _, e := pool.Exec(ctx, `UPDATE veyra_backtest_run SET result='{}'::jsonb WHERE id=$1`, job.ID); e == nil {
|
||||
t.Fatal("completed result can be overwritten")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type ResearchInput struct {
|
||||
Parameters StrategyParameters `json:"parameters"`
|
||||
}
|
||||
type BacktestRun struct {
|
||||
Symbol string `json:"symbol"`
|
||||
ID string `db:"id" json:"id"`
|
||||
StrategyVersionID string `db:"strategy_version_id" json:"strategy_version_id"`
|
||||
Status string `db:"status" json:"status"`
|
||||
|
||||
@@ -23,6 +23,9 @@ type AIConclusion struct {
|
||||
MissingInformation []string `json:"missing_information"`
|
||||
}
|
||||
type CandidateAnalysis struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Symbol string `json:"symbol,omitempty"`
|
||||
Exchange string `json:"exchange,omitempty"`
|
||||
ID string `db:"id" json:"id"`
|
||||
CandidateRevisionID string `db:"candidate_revision_id" json:"candidate_revision_id"`
|
||||
InputVersion int64 `db:"input_version" json:"input_version"`
|
||||
|
||||
@@ -21,6 +21,13 @@ const backtestColumns = `id,strategy_version_id,status,input,input_hash,result,f
|
||||
func scanBacktest(row pgx.Row) (model.BacktestRun, error) {
|
||||
var v model.BacktestRun
|
||||
err := row.Scan(&v.ID, &v.StrategyVersionID, &v.Status, &v.Input, &v.InputHash, &v.Result, &v.Failure, &v.LeaseID, &v.CreatedAt, &v.FinishedAt)
|
||||
if err == nil {
|
||||
var identity struct {
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
err = json.Unmarshal(v.Input, &identity)
|
||||
v.Symbol = identity.Symbol
|
||||
}
|
||||
return v, mapError(err)
|
||||
}
|
||||
func (r *BacktestRepository) Detail(ctx context.Context, id string) (model.BacktestRun, error) {
|
||||
|
||||
@@ -183,7 +183,7 @@ func (r *CandidateAnalysisRepository) Complete(ctx context.Context, p model.AIRe
|
||||
return mapError(tx.Commit(ctx))
|
||||
}
|
||||
func (r *CandidateAnalysisRepository) List(ctx context.Context) ([]model.CandidateAnalysis, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT id,candidate_revision_id,input_version,policy_version,status,attempts,primary_model,review_model,primary_version,review_version,analysis,review,message,created_at,updated_at FROM veyra_candidate_analysis ORDER BY updated_at DESC LIMIT 100`)
|
||||
rows, err := r.pool.Query(ctx, `SELECT a.id,a.candidate_revision_id,a.input_version,a.policy_version,a.status,a.attempts,a.primary_model,a.review_model,a.primary_version,a.review_version,a.analysis,a.review,a.message,a.created_at,a.updated_at,r.snapshot->>'name',r.snapshot->>'symbol',r.snapshot->>'exchange' FROM veyra_candidate_analysis a JOIN veyra_candidate_revision r ON r.id=a.candidate_revision_id ORDER BY a.updated_at DESC LIMIT 100`)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (r *CandidateAnalysisRepository) List(ctx context.Context) ([]model.Candida
|
||||
for rows.Next() {
|
||||
var v model.CandidateAnalysis
|
||||
var a, b []byte
|
||||
if err = rows.Scan(&v.ID, &v.CandidateRevisionID, &v.InputVersion, &v.PolicyVersion, &v.Status, &v.Attempts, &v.PrimaryModel, &v.ReviewModel, &v.PrimaryVersion, &v.ReviewVersion, &a, &b, &v.Message, &v.CreatedAt, &v.UpdatedAt); err != nil {
|
||||
if err = rows.Scan(&v.ID, &v.CandidateRevisionID, &v.InputVersion, &v.PolicyVersion, &v.Status, &v.Attempts, &v.PrimaryModel, &v.ReviewModel, &v.PrimaryVersion, &v.ReviewVersion, &a, &b, &v.Message, &v.CreatedAt, &v.UpdatedAt, &v.Name, &v.Symbol, &v.Exchange); err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
if len(a) > 0 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {StockIdentity} from "./StockIdentity"
|
||||
import {useEffect,useState} from "react"
|
||||
import {request,ApiError} from "@/lib/auth"
|
||||
import {Button} from "@/components/ui/button"
|
||||
@@ -5,7 +6,7 @@ import {Badge} from "@/components/ui/badge"
|
||||
import {Card,CardHeader,CardTitle,CardContent,CardDescription} from "@/components/ui/card"
|
||||
type Policy={enabled:boolean;auto_publish:boolean;version:number}
|
||||
type Conclusion={symbol:string;decision:string;summary:string;risks:string;missing_information:string[]}
|
||||
type Analysis={id:string;status:string;attempts:number;message:string;primary_model:string;review_model:string;analysis?:Conclusion;review?:Conclusion;updated_at:string}
|
||||
type Analysis={id:string;name:string;symbol:string;exchange:string;status:string;attempts:number;message:string;primary_model:string;review_model:string;analysis?:Conclusion;review?:Conclusion;updated_at:string}
|
||||
const states:Record<string,string>={RUNNING:"分析中",FAILED:"调用失败",APPROVED:"已通过 · 等待发布",REJECTED:"AI 未通过",PUBLISHED:"已自动发布",STALE:"版本或规则已失效"}
|
||||
export function AIReviewPanel({onSessionExpired}:{onSessionExpired:()=>void}){
|
||||
const [policy,setPolicy]=useState<Policy|null>(null),[results,setResults]=useState<Analysis[]>([]),[enabled,setEnabled]=useState(false),[auto,setAuto]=useState(false),[error,setError]=useState(""),[busy,setBusy]=useState(false),[notice,setNotice]=useState("")
|
||||
@@ -13,5 +14,5 @@ export function AIReviewPanel({onSessionExpired}:{onSessionExpired:()=>void}){
|
||||
useEffect(()=>{const c=new AbortController();request<Policy>("/api/admin/v1/ai-review-policy",{signal:c.signal}).then(p=>{setPolicy(p);setEnabled(p.enabled);setAuto(p.auto_publish)}).catch(e=>{if(!c.signal.aborted)failure(e)});const load=()=>request<Analysis[]>("/api/admin/v1/candidate-analyses",{signal:c.signal}).then(setResults).catch(e=>{if(!c.signal.aborted)failure(e)});void load();const timer=setInterval(()=>void load(),5000);return()=>{c.abort();clearInterval(timer)}},[onSessionExpired])
|
||||
async function save(){if(!policy)return;setBusy(true);setError("");try{const p=await request<Policy>("/api/admin/v1/ai-review-policy",{method:"PUT",body:JSON.stringify({enabled,auto_publish:auto,expected_version:policy.version})});setPolicy(p);setNotice("审核策略已保存,后台会处理待审核候选。")}catch(e){failure(e)}finally{setBusy(false)}}
|
||||
async function run(){setBusy(true);setError("");try{await request("/api/admin/v1/candidate-analyses/run",{method:"POST"});setNotice("已通知后台处理;结果自动刷新。")}catch(e){failure(e)}finally{setBusy(false)}}
|
||||
return <Card><CardHeader><CardTitle>AI 候选分析与自动审核</CardTitle><CardDescription>策略筛选 → 主模型分析 → 审核模型复核 → 服务端规则校验 → App 发布。仅限模拟研究;不会生成计划或成交。</CardDescription></CardHeader><CardContent className="space-y-5"><div className="flex flex-wrap gap-5"><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enabled} disabled={busy||!policy} onChange={e=>setEnabled(e.target.checked)}/>启用 AI 分析与审核</label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={auto} disabled={busy||!policy} onChange={e=>setAuto(e.target.checked)}/>审核与规则通过后自动发布</label></div><p className="text-sm text-muted-foreground">主模型和审核模型必须配置并启用。关闭自动发布时保留 AI 意见供人工复核;失败最多重试三次,候选过期不发布。</p><div className="flex gap-3"><Button disabled={busy||!policy} onClick={()=>void save()}>保存审核策略</Button><Button variant="outline" disabled={busy||!policy?.enabled} onClick={()=>void run()}>立即处理待审核候选</Button></div>{notice&&<p role="status" className="text-sm">{notice}</p>}{error&&<p role="alert" className="text-sm text-destructive">{error}</p>}<div className="space-y-3">{results.length===0&&<p className="text-sm text-muted-foreground">暂无 AI 任务。启用后自动处理有效待审核候选。</p>}{results.map(a=><div key={a.id} className="rounded-xl border p-4 space-y-2"><div className="flex justify-between gap-3"><strong>{a.analysis?.symbol??"候选分析任务"}</strong><Badge variant="outline">{states[a.status]??a.status}</Badge></div><p className="text-sm">{a.message||"正在调用模型…"}</p>{a.analysis&&<><p className="text-sm leading-6">分析:{a.analysis.summary}</p><p className="text-sm text-muted-foreground">风险:{a.analysis.risks}</p></>}{a.review&&<p className="text-sm leading-6">复核:{a.review.summary}</p>}<p className="text-xs text-muted-foreground">{a.primary_model||"待调用"} / {a.review_model||"待调用"} · 尝试 {a.attempts} · {new Date(a.updated_at).toLocaleString("zh-CN")}</p></div>)}</div></CardContent></Card>
|
||||
return <Card><CardHeader><CardTitle>AI 候选分析与自动审核</CardTitle><CardDescription>策略筛选 → 主模型分析 → 审核模型复核 → 服务端规则校验 → App 发布。仅限模拟研究;不会生成计划或成交。</CardDescription></CardHeader><CardContent className="space-y-5"><div className="flex flex-wrap gap-5"><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enabled} disabled={busy||!policy} onChange={e=>setEnabled(e.target.checked)}/>启用 AI 分析与审核</label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={auto} disabled={busy||!policy} onChange={e=>setAuto(e.target.checked)}/>审核与规则通过后自动发布</label></div><p className="text-sm text-muted-foreground">主模型和审核模型必须配置并启用。关闭自动发布时保留 AI 意见供人工复核;失败最多重试三次,候选过期不发布。</p><div className="flex gap-3"><Button disabled={busy||!policy} onClick={()=>void save()}>保存审核策略</Button><Button variant="outline" disabled={busy||!policy?.enabled} onClick={()=>void run()}>立即处理待审核候选</Button></div>{notice&&<p role="status" className="text-sm">{notice}</p>}{error&&<p role="alert" className="text-sm text-destructive">{error}</p>}<div className="space-y-3">{results.length===0&&<p className="text-sm text-muted-foreground">暂无 AI 任务。启用后自动处理有效待审核候选。</p>}{results.map(a=><div key={a.id} className="rounded-xl border p-4 space-y-2"><div className="flex justify-between gap-3"><strong><StockIdentity name={a.name} symbol={a.symbol}/></strong><Badge variant="outline">{states[a.status]??a.status}</Badge></div><p className="text-sm">{a.message||"正在调用模型…"}</p>{a.analysis&&<><p className="text-sm leading-6">分析:{a.analysis.summary}</p><p className="text-sm text-muted-foreground">风险:{a.analysis.risks}</p></>}{a.review&&<p className="text-sm leading-6">复核:{a.review.summary}</p>}<p className="text-xs text-muted-foreground">{a.primary_model||"待调用"} / {a.review_model||"待调用"} · 尝试 {a.attempts} · {new Date(a.updated_at).toLocaleString("zh-CN")}</p></div>)}</div></CardContent></Card>
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user