Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad49c4064 | ||
|
|
1496d77bcc | ||
|
|
393f83bb9a | ||
|
|
d39fb0e0d2 |
@@ -2,13 +2,22 @@
|
|||||||
|
|
||||||
更新日期:2026-10-09。
|
更新日期:2026-10-09。
|
||||||
|
|
||||||
## AI 接入与本轮部署(2026-10-09,进行中)
|
## AI 接入与本轮部署(2026-10-09)
|
||||||
|
|
||||||
|
- 用户后来指定 GPT-6.1-Astra;实际供应商目录没有此 ID,请求 404。用户确认改用 gpt-6.1-sol,模型目录匹配且约 6.8 秒真实结构化连接测试通过;主配置已保存该模型,密钥保留。
|
||||||
- 用户明确要求 AI 分析/审核通过后自动发布、不逐条人工操作,并授权通过 CI/CD 部署到现有服务器。新增十五版迁移:审核策略配置与候选分析任务,所有表使用 veyra_、字符串 UUID。
|
- 用户明确要求 AI 分析/审核通过后自动发布、不逐条人工操作,并授权通过 CI/CD 部署到现有服务器。新增十五版迁移:审核策略配置与候选分析任务,所有表使用 veyra_、字符串 UUID。
|
||||||
- 接入 Responses / Chat Completions 结构化调用,密钥从现有 Vault 解密,禁止重定向携带密钥;主分析与独立复核严格验证证券、Schema、证据索引与文本边界。不改写价格、不生成用户计划或成交。
|
- 接入 Responses / Chat Completions 结构化调用,密钥从现有 Vault 解密,禁止重定向携带密钥;主分析与独立复核严格验证证券、Schema、证据索引与文本边界。不改写价格、不生成用户计划或成交。
|
||||||
- 后台增加 AI 审核和自动发布开关、任务状态、处理入口、分析与复核结果;审核模型可安全复用主配置。Worker 每 30 秒检查,五分钟租约、版本去重、失败最多三次并间隔五分钟;提交时重新核对配置、授权身份、策略与候选,结果和发布/审计/Outbox 同事务。
|
- 后台增加 AI 审核和自动发布开关、任务状态、处理入口、分析与复核结果;审核模型可安全复用主配置。Worker 每 30 秒检查,五分钟租约、版本去重、失败最多三次并间隔五分钟;提交时重新核对配置、授权身份、策略与候选,结果和发布/审计/Outbox 同事务。
|
||||||
- App 详情增加真实 AI 分析、复核与资料缺口;统一 Screen 隐藏纵横滚动条,保留滑动。完整 Go race、类型检查、管理端构建与 App 双端 Hermes 导出通过。已用现有 Vault 密钥实际调用 gpt-5.5:约 3.4 秒返回有效结构化 REJECT(明确缺少研究数据的连接测试),接口可用且没有发布测试候选。PR #3 合并 bf643b0,run 40 因源码下载截断失败,未部署;正在补下载重试与 tar 完整性校验。
|
- App 详情增加真实 AI 分析、复核与资料缺口;统一 Screen 隐藏纵横滚动条,保留滑动。完整 Go race、类型检查、管理端构建与 App 双端 Hermes 导出通过。已用现有 Vault 密钥实际调用 gpt-5.5:约 3.4 秒返回有效结构化 REJECT(明确缺少研究数据的连接测试),接口可用且没有发布测试候选。PR #3 合并 bf643b0,run 40 因源码下载截断失败,未部署;下载重试与 tar 完整性校验经 PR #4 合并 b47889c,run 41 的 verify/deploy 均成功,容器 healthy。
|
||||||
- iPhone 包新增 Expo 云端未签名设备 IPA 构建;只上传隔离 App 源码,不上传后端私有环境。首轮自定义步骤名称错误已修正为显式 pod install;已提交构建 e36bba44-4949-4069-a2fb-e1f491dac7f1,普通 Apple ID 安装仍需个人重签,尚未声称安装包完成或可直接安装。
|
- iPhone 包新增 Expo 云端未签名设备 IPA 构建;只上传隔离 App 源码,不上传后端私有环境。首轮自定义步骤名称错误已修正为显式 pod install;已提交构建 e36bba44-4949-4069-a2fb-e1f491dac7f1,构建已 FINISHED;设备 IPA 已下载到 Downloads/Veyra-unsigned.ipa(10,953,223 字节),zip 完整性、cn.sundynix.veyra、iPhoneOS/arm64、内嵌 Hermes 和公开 HTTPS 地址验证通过,无签名。SHA-256 19b6650dd73712fcf612f9b84b3cd7efdc8e5470d99b3f0d17bbaabe5972a9ac。IPA 与校验文件已上传 Gitea 预发布 v0.1.0-preview.20261009;普通 Apple ID 仍需个人重签,实际安装运行待用户完成。
|
||||||
|
|
||||||
|
- 实际主分析与复核均已启用 gpt-6.1-sol,现有加密密钥保留;AI 审核策略版本 2 已启用自动发布。核对实际审计:两条已有候选在 22:11 已经人工发布,一个计划在 22:17 已确认,均早于本轮 AI 启用;App 当前可见两条候选、一个计划、原有模拟账户现金 1000.00。不能把既有人工发布称作 AI 结果;暂无待审核候选,AI 任务列表为空,后续新候选自动处理。没有创建或改写用户资金、计划、成交。
|
||||||
|
- 部署后的配置校验发现旧数据库 timeout_seconds 上限仍为 30,已新增十六版迁移匹配调用端 120 秒上限,并使用实际数据库隔离 schema 测试 120 秒配置持久化;Go race 与 go vet 通过,正在通过 PR / CI 部署修复。
|
||||||
|
|
||||||
|
## 股票名称搜索修复(2026-10-09)
|
||||||
|
|
||||||
|
- 实际 HTTPS 查询复现:名称“诺普信”无结果,但代码 002215 和名称“诺 普 信”均返回该深交所股票。数据源在名称中填充了空格,原 SQL LIKE 连续匹配漏掉无空格查询,证券并未漏同步。
|
||||||
|
- 查询统一忽略名称与输入中的空格、制表符和全角空格,保持原始源名称;转义 LIKE 通配符为字面匹配,避免输入 % 或 _ 返回全部股票。管理端无搜索结果文案与无同步数据区分。真实 PostgreSQL 隔离 schema 的 race 回归覆盖名称/代码/空格和通配符,go vet、管理端类型检查/build/embed 通过;待 PR / CI 上线后实查。
|
||||||
|
|
||||||
## App Expo Go 真机入口(2026-10-09)
|
## App Expo Go 真机入口(2026-10-09)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Keep the persisted limit aligned with the model client's configurable deadline.
|
||||||
|
ALTER TABLE veyra_provider_config
|
||||||
|
DROP CONSTRAINT veyra_provider_config_timeout_seconds_check;
|
||||||
|
ALTER TABLE veyra_provider_config
|
||||||
|
ADD CONSTRAINT veyra_provider_config_timeout_seconds_check
|
||||||
|
CHECK (timeout_seconds BETWEEN 1 AND 120);
|
||||||
@@ -43,11 +43,14 @@ func testProviderLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Pool
|
|||||||
t.Fatal("stale configuration accepted")
|
t.Fatal("stale configuration accepted")
|
||||||
}
|
}
|
||||||
token := "test-private-provider-key"
|
token := "test-private-provider-key"
|
||||||
input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 15, Enabled: true, APIKey: &token, ExpectedVersion: config.Version}
|
input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 120, Enabled: true, APIKey: &token, ExpectedVersion: config.Version}
|
||||||
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
|
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if config.TimeoutSeconds != 120 {
|
||||||
|
t.Fatal("model deadline was not persisted")
|
||||||
|
}
|
||||||
data, _ := json.Marshal(config)
|
data, _ := json.Marshal(config)
|
||||||
if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) {
|
if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) {
|
||||||
t.Fatal("API exposes credential")
|
t.Fatal("API exposes credential")
|
||||||
|
|||||||
@@ -47,6 +47,22 @@ func testScreeningLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Poo
|
|||||||
if err := repository.NewMarketRepository(pool).Save(ctx, provider, []model.Instrument{instrument}, time.Now().UTC(), ""); err != nil {
|
if err := repository.NewMarketRepository(pool).Save(ctx, provider, []model.Instrument{instrument}, time.Now().UTC(), ""); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
searchRepo := repository.NewMarketRepository(pool)
|
||||||
|
if err := searchRepo.Save(ctx, provider, []model.Instrument{{Symbol: "002215", Exchange: "SZSE", Name: "诺 普 信", PriceCents: 1000, ObservedAt: observed}}, time.Now().UTC(), ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, query := range []string{"诺普信", "诺 普 信", " 诺\t普 信 ", "002215", "002 215"} {
|
||||||
|
stocks, err := searchRepo.List(ctx, query)
|
||||||
|
if err != nil || len(stocks) != 1 || stocks[0].Symbol != "002215" || stocks[0].Name != "诺 普 信" {
|
||||||
|
t.Fatalf("padded security search %q: %v, %v", query, stocks, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, query := range []string{"not-a-security", "%", "_"} {
|
||||||
|
stocks, err := searchRepo.List(ctx, query)
|
||||||
|
if err != nil || len(stocks) != 0 {
|
||||||
|
t.Fatalf("literal security search %q matched: %v, %v", query, stocks, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
repo := repository.NewScreeningRepository(pool)
|
repo := repository.NewScreeningRepository(pool)
|
||||||
targets, err := repo.Targets(ctx, provider)
|
targets, err := repo.Targets(ctx, provider)
|
||||||
if err != nil || len(targets) != 1 {
|
if err != nil || len(targets) != 1 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +15,10 @@ type MarketRepository struct{ pool *pgxpool.Pool }
|
|||||||
|
|
||||||
func NewMarketRepository(pool *pgxpool.Pool) *MarketRepository { return &MarketRepository{pool} }
|
func NewMarketRepository(pool *pgxpool.Pool) *MarketRepository { return &MarketRepository{pool} }
|
||||||
func (r *MarketRepository) List(ctx context.Context, query string) ([]model.Instrument, error) {
|
func (r *MarketRepository) List(ctx context.Context, query string) ([]model.Instrument, error) {
|
||||||
rows, e := r.pool.Query(ctx, `SELECT id,symbol,exchange,name,price_cents,observed_at,received_at,source_id,source_version FROM veyra_instrument WHERE symbol LIKE $1 OR name LIKE $1 ORDER BY (price_cents>0) DESC,symbol LIMIT 100`, "%"+query+"%")
|
// Vendors pad some short names with whitespace (for example "诺 普 信").
|
||||||
|
// Normalize only for matching; preserve the supplied name in stored data.
|
||||||
|
pattern := "%" + strings.NewReplacer(`\`, `\\`, "%", `\%`, "_", `\_`).Replace(query) + "%"
|
||||||
|
rows, e := r.pool.Query(ctx, `SELECT id,symbol,exchange,name,price_cents,observed_at,received_at,source_id,source_version FROM veyra_instrument WHERE symbol LIKE regexp_replace($1,'[[:space:] ]+','','g') OR regexp_replace(name,'[[:space:] ]+','','g') LIKE regexp_replace($1,'[[:space:] ]+','','g') ORDER BY (price_cents>0) DESC,symbol LIMIT 100`, pattern)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, mapError(e)
|
return nil, mapError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export function MarketPanel({onSessionExpired,initialSymbol}:{onSessionExpired:(
|
|||||||
function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"行情读取失败")}
|
function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"行情读取失败")}
|
||||||
async function load(){setBusy(true);setError("");try{const [stocks,runs]=await Promise.all([request<Instrument[]>(`/api/admin/v1/instruments?q=${encodeURIComponent(query)}`),request<Sync[]>("/api/admin/v1/market/syncs")]);setList(stocks);setTasks(runs)}catch(e){fail(e)}finally{setBusy(false)}}
|
async function load(){setBusy(true);setError("");try{const [stocks,runs]=await Promise.all([request<Instrument[]>(`/api/admin/v1/instruments?q=${encodeURIComponent(query)}`),request<Sync[]>("/api/admin/v1/market/syncs")]);setList(stocks);setTasks(runs)}catch(e){fail(e)}finally{setBusy(false)}}
|
||||||
useEffect(()=>{void load()},[])
|
useEffect(()=>{void load()},[])
|
||||||
return <Card className="mt-6"><CardHeader><CardTitle>沪深股票池 · 行情与日线</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-[560px] overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3"><button className="text-left text-base font-medium hover:text-primary" onClick={()=>setSelected(v)}>{v.name}</button><p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td className="font-mono text-lg">{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">暂无同步数据,请检查系统设置中的行情源与同步记录。</p>}</div>{selected&&<div><div className="mb-4 flex items-end justify-between"><div><h2 className="text-xl font-semibold">{selected.name}</h2><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange}:{selected.symbol}</p></div><strong className="font-mono text-3xl">¥{selected.price}</strong></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card>
|
return <Card className="mt-6"><CardHeader><CardTitle>沪深股票池 · 行情与日线</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-[560px] overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3"><button className="text-left text-base font-medium hover:text-primary" onClick={()=>setSelected(v)}>{v.name}</button><p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td className="font-mono text-lg">{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">{query.trim()?"未找到匹配股票,请尝试股票代码或其他名称。":"暂无同步数据,请检查系统设置中的行情源与同步记录。"}</p>}</div>{selected&&<div><div className="mb-4 flex items-end justify-between"><div><h2 className="text-xl font-semibold">{selected.name}</h2><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange}:{selected.symbol}</p></div><strong className="font-mono text-3xl">¥{selected.price}</strong></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user