Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
b341d3c
ci(build): add manual docker build workflow
Greenplumwine Aug 13, 2026
b91fabe
Merge branch 'Tencent:main' into main
Greenplumwine Aug 14, 2026
f1f5bd3
Merge branch 'Tencent:main' into main
Greenplumwine Aug 14, 2026
96e6d4e
Merge branch 'Tencent:main' into main
Greenplumwine Aug 17, 2026
519576a
Merge branch 'Tencent:main' into main
Greenplumwine Aug 23, 2026
b7ad143
Merge branch 'Tencent:main' into main
Greenplumwine Aug 25, 2026
b59e177
Merge branch 'Tencent:main' into main
Greenplumwine Aug 26, 2026
569020e
Merge branch 'Tencent:main' into main
Greenplumwine Aug 28, 2026
5b28de5
Merge branch 'Tencent:main' into main
Greenplumwine Sep 2, 2026
3d24cbc
Merge branch 'Tencent:main' into main
Greenplumwine Sep 2, 2026
d470ef0
Merge branch 'Tencent:main' into main
Greenplumwine Sep 3, 2026
458b978
Merge branch 'Tencent:main' into main
Greenplumwine Sep 3, 2026
78eff2e
Merge branch 'Tencent:main' into main
Greenplumwine Sep 7, 2026
917d8c9
Merge branch 'Tencent:main' into main
Greenplumwine Sep 7, 2026
4e05421
Merge branch 'Tencent:main' into main
Greenplumwine Sep 7, 2026
f94f95b
Merge branch 'Tencent:main' into main
Greenplumwine Sep 8, 2026
9456f09
feat(model): capability declaration layer, parameter sharding, thinki…
Greenplumwine Sep 9, 2026
b8f865b
feat(frontend): capability-driven model editor, thinking levels, cata…
Greenplumwine Sep 9, 2026
a8400ea
fix(model): close probe API-key exposure, honor level resolution on m…
Greenplumwine Sep 9, 2026
b7be8ec
Merge branch 'Tencent:main' into main
Greenplumwine Sep 9, 2026
0635a2b
feat(model): unified invocation core — entry, adapters, executor, gol…
Greenplumwine Sep 10, 2026
e8f4bf5
refactor(model): switch all callers to invoke and remove legacy chat/…
Greenplumwine Sep 10, 2026
392f413
feat(frontend): thinking controls dynamic adaptation and credential s…
Greenplumwine Sep 10, 2026
d5bb47d
feat(model): embedding facet on the unified invocation core
Greenplumwine Sep 10, 2026
8f424e1
refactor(model): route embedding callers through invoke.Embed, drop m…
Greenplumwine Sep 10, 2026
cb8bd4e
feat(model): rerank + ASR facets on the unified invocation core
Greenplumwine Sep 11, 2026
3906420
refactor(model): route rerank/ASR callers through the unified entry
Greenplumwine Sep 11, 2026
aaf0406
refactor(model): dissolve models/utils — slices to internal/utils, HM…
Greenplumwine Sep 11, 2026
6da3a65
refactor(model): fold models/limiter into invoke — concurrency gating…
Greenplumwine Sep 11, 2026
26859b5
feat(model): remote-catalog probe rides the ListModels facet; catalog…
Greenplumwine Sep 11, 2026
1cab8cc
refactor(model): delete models/provider — types+knowledge to invoke, …
Greenplumwine Sep 11, 2026
bc1583e
fix(model): P4 review round — conflict-marker rebuild follow-ups, har…
Greenplumwine Sep 11, 2026
1d68421
fix(model): second review round — C-5 completion, nonce uniformity, t…
Greenplumwine Sep 11, 2026
223990d
feat(model): native Gemini generateContent adapter (P5-1) — chat, str…
Greenplumwine Sep 11, 2026
782999d
fix(model): Gemini R1 — thinking dispatch per model family; stream fo…
Greenplumwine Sep 11, 2026
5479bfd
refactor(model): Gemini rides the official OpenAI-compat layer (裁定 #3…
Greenplumwine Sep 12, 2026
cfb096d
refactor(model): home the gemini embed wire in gemini.go; rename the …
Greenplumwine Sep 12, 2026
be2c547
refactor(model): move the facet vendor wire into per-vendor adapter f…
Greenplumwine Sep 12, 2026
e633dcd
feat(model): aliyun rides the native DashScope wire (chat/embedding/l…
Greenplumwine Sep 12, 2026
a7d9829
fix(model): aliyun review round — stream tail loss, base-URL strip, w…
Greenplumwine Sep 12, 2026
f9b7905
feat(model): aliyun rerank dual protocol + base normalization (review…
Greenplumwine Sep 12, 2026
8c251e9
feat(model): type-filtered paginated remote listing + aliyun thinking…
Greenplumwine Sep 12, 2026
18ddc7c
feat(model): native DashScope explicit-cache breakpoints (default-on)…
Greenplumwine Sep 12, 2026
68b309c
fix(model): review round — unwrap-cycle P0, stream terminal-state gap…
Greenplumwine Sep 12, 2026
3620558
fix(frontend): review round — state leaks, dead forms, contract drift…
Greenplumwine Sep 12, 2026
f33d853
feat(model): ruling batch — multi-event bridges, A4 stream-Data reviv…
Greenplumwine Sep 13, 2026
414101b
fix(model): aliyun vision branch restores structured output (B4)
Greenplumwine Sep 13, 2026
27ae6c8
fix(frontend): dialog mount crash — watch registered before formData …
Greenplumwine Sep 13, 2026
26faf88
fix(frontend): model editor field order, credential label requirednes…
Greenplumwine Sep 13, 2026
93312f1
feat(frontend): source control retired — ollama becomes a provider; f…
Greenplumwine Sep 13, 2026
77e113e
fix(frontend): close the remote-branch template in the model editor
Greenplumwine Sep 13, 2026
aa33917
fix(frontend): center the modality icon on the model card meta row
Greenplumwine Sep 13, 2026
ef4113d
fix(frontend): animate the thinking-levels collapse (no more layout s…
Greenplumwine Sep 13, 2026
0a7e7b5
fix(frontend): model card shows one icon per checked input modality
Greenplumwine Sep 13, 2026
b336e7d
chore(frontend): drop implementation-speak from the modality hint
Greenplumwine Sep 13, 2026
d7c1fe5
feat(frontend): split the advanced section — model parameters vs adva…
Greenplumwine Sep 13, 2026
7dcbd25
fix(frontend): chat toolbar spacing, thinking popup typography, dead …
Greenplumwine Sep 13, 2026
e148a93
fix(model): surface DashScope in-band stream errors instead of droppi…
Greenplumwine Sep 13, 2026
6c07605
feat(model): zero-event stream black-box diagnostics + failure-shape …
Greenplumwine Sep 13, 2026
ef844c7
fix(model): warn on ANY content-less 2xx stream, not just zero-event …
Greenplumwine Sep 13, 2026
3bca460
fix(model): DashScope streams terminate on intermediate frames — fini…
Greenplumwine Sep 13, 2026
75ca5cd
feat(model): server-side list filter, embedding base guard, non-agent…
Greenplumwine Sep 14, 2026
66395e5
feat(frontend): provider logos, drawer save contract, azure extra fie…
Greenplumwine Sep 14, 2026
ce6974d
fix(embedding): aliyun multimodal index field + empty-vector guards e…
Greenplumwine Sep 14, 2026
00af651
fix(frontend): type-filtered model dropdown, option layout, display-n…
Greenplumwine Sep 14, 2026
b84e466
fix(frontend): center the model-card badge against the text block
Greenplumwine Sep 14, 2026
b3eb277
fix(frontend): model dropdown shows the model ID only
Greenplumwine Sep 14, 2026
04afe2f
fix(frontend): restore the remote model dropdown data source
Greenplumwine Sep 14, 2026
d28354e
fix(model): default embedding CanOverrideDimension to true — probe va…
Greenplumwine Sep 14, 2026
a5553de
fix(frontend): kick initial remote-catalog probe after edit-open hydr…
Greenplumwine Sep 14, 2026
99c8e22
feat(model): ollama thinking toggle + fix icon/text alignment in edit…
Greenplumwine Sep 14, 2026
9e55f74
Merge branch 'Tencent:main' into main
Greenplumwine Sep 15, 2026
8db43a5
Merge branch 'main' into feat/model-mgmt-unified-invocation
Greenplumwine Sep 15, 2026
14bae9d
feat(chat): session thinking toggle + agent-default level display
Greenplumwine Sep 15, 2026
3f3dd90
fix(models): recognize bare v<digits> version tails in remote-catalog…
Greenplumwine Sep 15, 2026
e182cc3
fix(aliyun): always-thinking predicate covers the bare glm-5.3 direct…
Greenplumwine Sep 15, 2026
a24d5e1
fix(anthropic): surface extended-thinking output instead of dropping it
Greenplumwine Sep 15, 2026
1fe148d
feat(frontend): provider logo assets for the model gallery (#15)
Greenplumwine Sep 15, 2026
5f16848
chore: keep model-management design docs local-only
Greenplumwine Sep 15, 2026
da5edba
style: gofmt internal/types/qa_request.go
Greenplumwine Sep 15, 2026
61df94c
chore: keep the manual docker build workflow fork-local
Greenplumwine Sep 15, 2026
b64e988
style: use Role constants instead of string literals for the neutral …
Greenplumwine Sep 16, 2026
9e9b7a3
style: cover multiline call sites in the Role-constant sweep
Greenplumwine Sep 16, 2026
6868506
feat(model)!: converge model types to four facets, vision rides input…
Greenplumwine Sep 16, 2026
0fe7969
fix(model): declare provider-level thinking for generic OpenAI-compat…
Greenplumwine Sep 16, 2026
c61e0eb
fix(invoke): inline application-stored image references before adapte…
Greenplumwine Sep 16, 2026
de8ca1a
feat(model): model-level thinking master switch becomes a hard gate
Greenplumwine Sep 17, 2026
36bb80e
refactor(frontend): hide thinking controls entirely when unavailable
Greenplumwine Sep 17, 2026
2b3a0bd
Merge branch 'main' of https://github.com/Tencent/WeKnora into feat/m…
Greenplumwine Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ REDIS_PREFIX=stream:
# 后台任务每模型并发上限(0/负数禁用;交互 chat 不受其限制)
# WEKNORA_MODEL_MAX_CONCURRENCY=32

# 模型参数目录来源:GITHUB / CNB / LOCAL(默认 LOCAL,使用内置 models.json 离线兜底;
# 镜像地址确定后切 GITHUB/CNB,非法值告警并回落 LOCAL)
# WEKNORA_MODELS_CATALOG=LOCAL
# 远端模型列表探测超时秒数(POST /models/remote-catalog,默认 8)
# WEKNORA_REMOTE_CATALOG_TIMEOUT_SECONDS=8

# ========== B3. 文件存储(通用)==========
# 文件存储类型:local / minio / cos / tos / s3 / obs / oss / dummy。
STORAGE_TYPE=local
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,13 @@ miniprogram/project.private.config.json

# Locally built BrowserSkill runtime and extension bundles
/artifacts/browserskill/

# 模型管理设计文档(本地评审用,不入库/不随 PR 提交,2026-09-15 裁定)
/CONTEXT.md
/docs/adr/0001-saved-model-config-never-auto-refreshed.md
/docs/adr/0002-model-level-thinking-levels-catalog-or-user-pick.md
/docs/adr/0003-scope-model-management-plus-unified-invocation.md
/docs/adr/0004-model-types-converge-to-four-facets-vision-rides-modalities.md

# fork 本地的手动 docker 构建流水线(不入上游,2026-09-15)
/.github/workflows/docker-build.yaml
8 changes: 8 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ linters:
- lll # 控制行宽
- govet
- revive
- exhaustive # 仅检查标注 //exhaustive:enforce 的 switch(如适配器 role 映射)
settings:
lll:
line-length: 120
tab-width: 4
exhaustive:
explicit-exhaustive-switch: true
errcheck:
exclude-functions:
# gin.Context.Error returns the error for chaining; repo-wide the return is
# deliberately ignored (64 pre-existing bare calls).
- (*github.com/gin-gonic/gin.Context).Error
exclusions:
paths:
# swag embeds OpenAPI as single-line JSON strings; line-length is not meaningful.
Expand Down
12 changes: 5 additions & 7 deletions cli/cmd/model/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var createSourceValues = []string{string(sdk.ModelSourceLocal), string(sdk.Model
// canonicalModelType maps the server's frontend term "chat" to the KnowledgeQA
// enum — that is the server's own /models/providers vocabulary (see its
// frontendToModelType map). The other frontend terms (embedding / rerank /
// vllm / asr) already match the enum case-insensitively, so no alias is needed.
// asr) already match the enum case-insensitively, so no alias is needed.
// Returns the input unchanged when it isn't an alias.
func canonicalModelType(t string) string {
if strings.EqualFold(strings.TrimSpace(t), "chat") {
Expand Down Expand Up @@ -66,13 +66,11 @@ type CreateService interface {
}

// frontendModelType maps the create enum to the server's /models/providers
// "model_type" query vocabulary (KnowledgeQA→chat, VLLM→vllm; others lowercase).
// "model_type" query vocabulary (KnowledgeQA→chat; others lowercase).
func frontendModelType(t string) string {
switch t {
case string(sdk.ModelTypeKnowledgeQA):
return "chat"
case string(sdk.ModelTypeVLLM):
return "vllm"
default:
return strings.ToLower(t)
}
Expand All @@ -83,7 +81,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
opts := &CreateOptions{}
cmd := &cobra.Command{
Use: "create <name>",
Short: "Register a model (embedding / rerank / chat / VLLM / ASR)",
Short: "Register a model (embedding / rerank / chat / ASR)",
Long: `Register a model on the server so it can back a knowledge base's embedding /
summary config (see 'weknora kb config set') or an agent (--model).

Expand All @@ -103,7 +101,7 @@ Two modes:
--base-url defaults to that provider's URL for the type when
omitted.

--type accepts the server's term "chat" for KnowledgeQA (embedding/rerank/vllm/
--type accepts the server's term "chat" for KnowledgeQA (embedding/rerank/
asr match the enum directly). Embedding models take --dimension. Pipe the
provider key via --api-key-stdin so it never lands in argv/history. Anything
else goes through repeatable --param key=value.`,
Expand Down Expand Up @@ -178,7 +176,7 @@ else goes through repeatable --param key=value.`,
cmdutil.AddFormatFlag(cmd, modelCreateFields...)
cmdutil.AddDryRunFlag(cmd, &opts.DryRun)
cmdutil.SetAgentHelp(cmd, cmdutil.AgentHelp{
UsedFor: "register a model (embedding/rerank/chat/VLLM/ASR) so a KB or agent can use it; capture .data.id to pass to `weknora kb config set` / `agent create --model`.",
UsedFor: "register a model (embedding/rerank/chat/ASR) so a KB or agent can use it; capture .data.id to pass to `weknora kb config set` / `agent create --model`.",
RequiredFlags: []string{"<name> (positional)", "--type", "--source (local|remote)", "--provider (when --source remote)"},
Examples: []string{
`weknora model create nomic-embed-text --type Embedding --source local --dimension 768 # Ollama (server pulls it)`,
Expand Down
6 changes: 3 additions & 3 deletions cli/cmd/model/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var modelSourceValues = cmdutil.EnumStrings(sdk.AllModelSources())
// ListOptions captures `model list` filter flag state.
type ListOptions struct {
// Type / Source, when set, restrict output to models of that type
// (Embedding, Rerank, KnowledgeQA, VLLM, ASR) or provider (local, openai,
// (Embedding, Rerank, KnowledgeQA, ASR) or provider (local, openai,
// …), matched case-insensitively. Empty shows everything.
Type string
Source string
Expand All @@ -58,7 +58,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
Use: "list",
Short: "List models configured on the server",
Long: `List the models configured on the server, sorted by type then name. Pass
--type to restrict to one model type (Embedding, Rerank, KnowledgeQA, VLLM, ASR).`,
--type to restrict to one model type (Embedding, Rerank, KnowledgeQA, ASR).`,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
fopts, err := cmdutil.CheckFormatFlag(c)
Expand All @@ -78,7 +78,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
return runList(c.Context(), opts, fopts, cli)
},
}
cmd.Flags().StringVar(&opts.Type, "type", "", "Only show models of this type (Embedding, Rerank, KnowledgeQA, VLLM, ASR)")
cmd.Flags().StringVar(&opts.Type, "type", "", "Only show models of this type (Embedding, Rerank, KnowledgeQA, ASR)")
cmd.Flags().StringVar(&opts.Source, "source", "", "Only show models from this provider (local, remote, openai, aliyun, …)")
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum results to return — client-side cap; meta.has_more/total_count report the full size (1..10000)")
cmdutil.AddFormatFlag(cmd, modelListFields...)
Expand Down
7 changes: 3 additions & 4 deletions client/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ type ModelType string
const (
ModelTypeEmbedding ModelType = "Embedding" // Embedding model
ModelTypeRerank ModelType = "Rerank" // Rerank model
ModelTypeKnowledgeQA ModelType = "KnowledgeQA" // KnowledgeQA model
ModelTypeVLLM ModelType = "VLLM" // VLLM model
ModelTypeKnowledgeQA ModelType = "KnowledgeQA" // KnowledgeQA model (vision rides input modalities)
ModelTypeASR ModelType = "ASR" // ASR (Automatic Speech Recognition) model
)

Expand All @@ -26,7 +25,7 @@ const (
// re-typing the string set, so they can't drift from the SDK.
func AllModelTypes() []ModelType {
return []ModelType{
ModelTypeEmbedding, ModelTypeRerank, ModelTypeKnowledgeQA, ModelTypeVLLM, ModelTypeASR,
ModelTypeEmbedding, ModelTypeRerank, ModelTypeKnowledgeQA, ModelTypeASR,
}
}

Expand Down Expand Up @@ -215,7 +214,7 @@ type ModelProviderListResponse struct {
}

// ListModelProviders retrieves the list of supported model providers.
// modelType is optional and can be used to filter by type: "chat", "embedding", "rerank", "vllm".
// modelType is optional and can be used to filter by type: "chat", "embedding", "rerank", "asr".
func (c *Client) ListModelProviders(ctx context.Context, modelType string) ([]ModelProvider, error) {
var queryParams url.Values
if modelType != "" {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/api/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface CustomAgentConfig {
temperature?: number;
max_completion_tokens?: number; // 0 = 跟随系统默认(快速问答 2048;智能推理 4096,绑沙箱可写文件时 24576)。大于 0 为自定义上限
thinking?: boolean; // 是否启用思考模式(支持扩展思考的模型)
thinking_level?: string; // 思考档位(空 = 跟随模型默认;开关为 config.thinking)
citation_enabled?: boolean; // 是否在最终回答中输出知识库/网页来源引用(默认开启)

// ===== Agent模式设置 =====
Expand Down Expand Up @@ -135,6 +136,18 @@ export interface CustomAgentConfig {
// 意图提示词:非检索意图(问候、闲聊等)时覆盖主系统提示词
intent_prompts?: Record<string, string>;

// ===== 上下文模板 / 检索历史 / 数据分析 / FAQ / 联网抓取 / 查询理解 =====
// (后端 custom_agent.go 全量替换 config——按本接口从零构造会清零这些
// 字段,2026-09-13 审查补齐镜像;context_template_id 声明见上方模型设置组)
retain_retrieval_history?: boolean;
data_analysis_enabled?: boolean;
faq_priority_enabled?: boolean;
faq_direct_answer_threshold?: number;
faq_score_boost?: number;
web_fetch_enabled?: boolean;
web_fetch_top_n?: number;
query_understand_model_id?: string;

// ===== 已废弃字段(保留兼容)=====
welcome_message?: string;
question_suggestions?: QuestionSuggestionConfig;
Expand Down
50 changes: 31 additions & 19 deletions frontend/src/api/chat/streame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,26 @@ interface StreamOptions {
}

export function useStream() {
// 响应式状态
const output = ref('') // 显示内容
// 响应式状态(output/isLoading 曾是有写无读的死状态、buffer 是只进不读的
// 跨流死缓冲——随流累积,2026-09-13 审查删除;消费者只取下方 return 的键)
const isStreaming = ref(false) // 流状态
const isLoading = ref(false) // 初始加载
const error = ref<string | null>(null)// 错误信息
const lastStreamRequest = ref<StreamRequestMeta | null>(null)
let controller = new AbortController()
let streamGeneration = 0

// 流式渲染缓冲
let buffer: string[] = []
let renderTimer: number | null = null

// 启动流式请求
const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; agent_source_tenant_id?: string | number; web_search_enabled?: boolean; local_browser_enabled?: boolean; summary_model_id?: string; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; attachment_ids?: string[]; suggestion_attribution?: { suggestion_set_id: string; question_id: string }; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => {
const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; agent_source_tenant_id?: string | number; web_search_enabled?: boolean; local_browser_enabled?: boolean; summary_model_id?: string; thinking_level?: string; thinking?: boolean; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; attachment_ids?: string[]; suggestion_attribution?: { suggestion_set_id: string; question_id: string }; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => {
const myGeneration = ++streamGeneration
// 终止在途旧流(2026-09-13 审查):此前 generation guard 只丢 UI 渲染,
// 旧连接仍在耗网络与上游 tokens——先 abort 旧 controller 再换新;
// 旧流的 onclose/catch 需校验 generation,防止清理动作杀死新流。
controller.abort()
controller = new AbortController()
const streamAbort = controller
// 重置状态
output.value = '';
error.value = null;
isStreaming.value = true;
isLoading.value = true;

// 获取API配置
const apiUrl = getApiBaseUrl();
Expand Down Expand Up @@ -137,6 +135,13 @@ export function useStream() {
if (params.images !== undefined && params.images.length > 0) {
postBody.images = params.images;
}
// 会话级思考覆盖(design §8.1.3):空 = 跟随智能体/模型默认,不发送
if (params.thinking !== undefined) {
postBody.thinking = params.thinking;
}
if (params.thinking_level) {
postBody.thinking_level = params.thinking_level;
}
// Include attachment_uploads if provided (documents, audio, etc.)
if (params.attachment_uploads !== undefined && params.attachment_uploads.length > 0) {
postBody.attachment_uploads = params.attachment_uploads;
Expand Down Expand Up @@ -183,20 +188,26 @@ export function useStream() {
if (res.status === 401) throw new StreamAuthError(res.status);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(`[TTFB] response:headers request_id=${requestID} elapsed_ms=${(performance.now() - sentAt).toFixed(1)}`);
isLoading.value = false;
},

onmessage: (ev) => {
if (myGeneration !== streamGeneration) return
const parsed = JSON.parse(ev.data);
// 坏帧跳过(2026-09-13 审查):裸 JSON.parse 的一帧异常会 reject
// 整条流;SSE 语义本可跳过坏帧续传。
let parsed: any;
try {
parsed = JSON.parse(ev.data);
} catch {
console.warn('[stream] skip non-JSON SSE frame:', String(ev.data).slice(0, 120));
return;
}
// Log first answer chunk for end-to-end TTFB measurement.
// Filter by event type so non-answer events (references, tool
// calls, etc.) don't count as the "first token" arrival.
if (!firstAnswerLogged && (parsed?.response_type === 'answer' || parsed?.type === 'answer')) {
firstAnswerLogged = true;
console.log(`[TTFB] response:first_answer request_id=${requestID} elapsed_ms=${(performance.now() - sentAt).toFixed(1)}`);
}
buffer.push(parsed); // 数据存入缓冲
// 执行自定义处理
if (chunkHandler) {
chunkHandler(parsed);
Expand All @@ -209,7 +220,8 @@ export function useStream() {
},

onclose: () => {
stopStream();
// generation 守卫:被新流 abort 的旧连接走到这里时不得清理新流
if (myGeneration === streamGeneration) stopStream();
},
});

Expand All @@ -227,8 +239,11 @@ export function useStream() {
reloginMessage: i18n.global.t('error.pleaseRelogin'),
});
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
stopStream()
// generation 守卫:旧流被 abort 的报错不得写入新流的 error 状态
if (myGeneration === streamGeneration) {
error.value = err instanceof Error ? err.message : String(err)
stopStream()
}
}
}

Expand All @@ -245,16 +260,13 @@ export function useStream() {
controller.abort();
controller = new AbortController(); // 重置控制器(如需重新发起)
isStreaming.value = false;
isLoading.value = false;
}

// 组件卸载时自动清理
onUnmounted(stopStream)

return {
output, // 显示内容
isStreaming, // 是否在流式传输中
isLoading, // 初始连接状态
error,
lastStreamRequest,
onChunk,
Expand Down
Loading
Loading