Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/shots/mock/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const CORE: { [N in WebviewEndpoint]: Handler<N> } = {
},
Sessions: (req) => sessions(req.limit ?? 200),
SessionDetail: (_req, [id]) => ({ session: sessionView(id!) ?? notFound(`Session ${id}`), turns: turns(id!) }),
SessionTranscript: (_req, [id]) => (sessionView(id!) ? transcript(id!) : notFound(`Session ${id}`)),
SessionTranscript: (req, [id]) => (sessionView(id!) ? transcript(id!, req.from_turn ?? 0) : notFound(`Session ${id}`)),
SpeedQuote: refuse,
SpeedRun: refuse,
ReplayQuote: refuse,
Expand Down
12 changes: 8 additions & 4 deletions scripts/shots/mock/traffic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,18 +891,22 @@ export function turns(id: string): TurnView[] {

/**
* 会话的对话(`GET /sessions/{id}/transcript`)。截图里不打开「对话」那一页,给一段读得通的:
* 第一轮是用户的话,之后每一轮一句回答。失败、取消的那一轮没有回答,和 core 一样不算缺口
* 第一轮是用户的话,之后每一轮一句回答。失败、取消的那一轮没有回答,和 core 一样不算缺口。
* 都已落库,全部轮次都不会再变;`fromTurn` 照 core 的 `from_turn` 只给后面那些
*/
export function transcript(id: string): Transcript {
export function transcript(id: string, fromTurn = 0): Transcript {
const rows = HISTORY.filter((h) => h.session === id);
return {
session: id,
system: null,
turns: HISTORY.filter((h) => h.session === id).map((h, i) => ({
total_turns: rows.length,
settled_turns: rows.length,
turns: rows.slice(fromTurn).map((h, j) => ({
id: String(h.id),
restart: false,
system_changed: null,
input:
i === 0
fromTurn + j === 0
? [{ role: "user", parts: [{ kind: "text", text: L("修复登录页的表单校验", "Fix the form validation on the sign-in page") }] }]
: [],
output:
Expand Down
47 changes: 25 additions & 22 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ license = "MIT"
# twcore 二进制必须和这里编译进去的协议镜像来自同一个 core 版本 —— 打包脚本正是
# 从 tw-api 锁到的 tag 去取二进制的(见 scripts/fetch-core.sh)。
[workspace.dependencies]
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.64.1" }
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }

[lib]
name = "thinkwatch_lite_lib"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/tw-scan/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//! | 全盘搜 `.claude/` | **不扫** |
//!
//! 最后一条是硬约束,不是偷懒。项目级的 `.claude/` 散落全盘,无界的
//! FSEvents 监听既是性能问题,也和「空闲时接近零」的目标直接
//! 文件监听既是性能问题,也和「空闲时接近零」的目标直接
//! 冲突。而且一个用户 clone 过的仓库可能有几百个,其中绝大多数他这辈子
//! 都不会再打开 —— 为它们持续烧 CPU 换不到任何东西。

Expand Down
5 changes: 3 additions & 2 deletions src-tauri/crates/tw-scan/src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//! # 范围仍然是那几个目录
//!
//! 监听的目录集合就是 [`crate::sources`] 划定的那一批,一个不多。无界的
//! FSEvents 监听既是性能问题,也和「空闲时接近零」的目标冲突。
//! 文件监听既是性能问题,也和「空闲时接近零」的目标冲突。
//!
//! **盯目录不盯文件、不递归、去抖**,和配置文件的监听是同一份([`tw_watch`])。

Expand Down Expand Up @@ -222,7 +222,8 @@ pub fn watch_plan(plan: &Plan) -> Result<(Watch, tokio::sync::mpsc::Receiver<()>
/// 根不算(盯它等于什么都盯)。
///
/// home 以外的(换过位置的目录)**只看上一层**:那个目录自己都不在的话,再往上找就要
/// 盯到 `/Volumes` 这种地方去 —— macOS 上盯一个目录,其实是盯它底下的一整棵树。
/// 盯到 `/Volumes` 这种地方去。macOS 上平时用 kqueue,只看那个目录自己的条目;但 fd
/// 名额不够、退回 FSEvents 时,盯一个目录就是盯它底下的一整棵树(见 `tw_watch`)。
fn nearest_dir(path: &Path, home: &Path) -> Option<PathBuf> {
let inside = path.starts_with(home);
path.ancestors()
Expand Down
43 changes: 39 additions & 4 deletions src/generated/tw-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by tw-api (`tw_api::ts::export_all`). Do not edit by hand.

export const CONTROL_API_VERSION = 40;
export const CONTROL_API_VERSION = 41;

/**
* 一个账号上游登的是哪个账号。
Expand Down Expand Up @@ -966,7 +966,14 @@ success_rate?: number | null,
* 按快慢、成败算出的系数,乘在权重([`Self::weight`])上:大于 1 分得多,小于 1
* 分得少,没有样本的那一项算 1。只在 `balance_by` 不是 `weights` 的 `load-balance` 里有
*/
balance_factor?: number | null, };
balance_factor?: number | null,
/**
* 照此刻的数字轮下去,它分到的新对话占这个组的多少,0 到 1:有效权重(权重 × 系数)
* 除以这一轮参加的各家之和,和数据面挑排头是同一份数字。只在 `load-balance` 里有,
* 同一个组里有份额的各家加起来是 1。熔断着、冷却着、并发数满着的这一轮不参加,没有
* 份额 —— 全都这样时都参加
*/
share?: number | null, };

/**
* 一次试算的结论。
Expand Down Expand Up @@ -4572,6 +4579,9 @@ samples: number, };
* 太大的只留开头,没存下来的也有。读不到的地方,那一轮的 `gaps` 说出来。
*
* **已脱敏**,和请求详情里的正文同一套打码。图片只说类型和大小,从不带数据。
*
* **可以只要后面的几轮**([`TranscriptQuery::from_turn`]):会话开着时每来一轮重读一次,
* 前面不会再变的那些(`settled_turns`)不必再传一遍。
*/
export type Transcript = { session: string,
/**
Expand All @@ -4581,7 +4591,22 @@ export type Transcript = { session: string,
*/
system: string | null,
/**
* 和 [`SessionDetail::turns`] 同样的请求,同样的顺序
* 这次会话一共几轮,就是 [`SessionDetail::turns`] 有几条。给了
* [`TranscriptQuery::from_turn`] 时,`turns` 是其中从那一轮起的那些
*/
total_turns: number,
/**
* 开头这么多轮不会再变了:它们的正文都落了盘(或者不会再来),后面的请求不会再改写
* 它们,这次会话里还在跑的请求落库时也不会排到它们前面。**下一次从这里要起**
* (`from_turn = settled_turns`),留着手上的前这么多轮,换掉后面的。
*
* 后面那几轮还会变:最后一轮的回答可能还没落盘、工具调用的号会被下一轮换成客户端
* 记下的那个、一个开始得早结束得晚的请求会插到它们中间
*/
settled_turns: number,
/**
* 和 [`SessionDetail::turns`] 同样的请求,同样的顺序。给了
* [`TranscriptQuery::from_turn`] 的,只有从那一轮起的那些
*/
turns: Array<TranscriptTurn>, };

Expand All @@ -4600,6 +4625,16 @@ export type TranscriptMessage = { role: TranscriptRole, parts: Array<TranscriptP
*/
export type TranscriptPart = { "kind": "text", text: string, } | { "kind": "thinking", text: string, } | { "kind": "tool_call", id: string, name: string, input: string, } | { "kind": "tool_result", call_id: string, text: string, is_error: boolean, } | { "kind": "image", media_type: string | null, bytes: number | null, } | { "kind": "other", label: string, };

/**
* `GET /sessions/{id}/transcript` 的查询串。
*/
export type TranscriptQuery = {
/**
* 只要从这一轮起的那些(从 0 数,[`Transcript::turns`] 里的第几条)。不给是整段;
* 比总轮数还大的,`turns` 是空的
*/
from_turn?: number | null, };

/**
* 一条消息是谁说的。
*/
Expand Down Expand Up @@ -4998,7 +5033,7 @@ export type Endpoints = {
Fixture: { req: null; res: string };
Sessions: { req: ListQuery; res: Array<SessionView> };
SessionDetail: { req: null; res: SessionDetail };
SessionTranscript: { req: null; res: Transcript };
SessionTranscript: { req: TranscriptQuery; res: Transcript };
SpeedQuote: { req: SpeedRunRequest; res: SpeedQuote };
SpeedRun: { req: SpeedRunRequest; res: Array<SpeedResult> };
ReplayQuote: { req: ReplayRequest; res: ReplayQuote };
Expand Down
5 changes: 2 additions & 3 deletions src/routing/DryRunDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { FormItem } from "@/upstreams/parts";
import { api } from "./api";
import { dryRunText } from "./DryRunDialog.i18n";
import { ModelInput, onOpenFocus } from "./fields";
import { DIALECTS, balanceShares, usersOf } from "./model";
import { DIALECTS, usersOf } from "./model";
import { KeyIcon, TargetIcon } from "./parts";
import { routingText } from "./routing.i18n";
import { modelViaOf, type ModelVia } from "./target";
Expand Down Expand Up @@ -378,7 +378,6 @@ function Result({
*/
const short = r.outcome === "intercepted";
const candidates = r.candidate_models;
const shares = balanceShares(r);
const target = r.outcome === "route" ? (r.via_group ?? candidates[0]?.provider ?? null) : null;

// 这一趟经过的路:密钥 → 路由 → 规则 → 去向。和路由图同一套标志
Expand Down Expand Up @@ -483,7 +482,7 @@ function Result({
<span className={cn("font-medium", open && "text-muted-foreground line-through")}>{c}</span>
{cv.sent_model && via && <SentModel model={cv.sent_model} via={via} />}
{cv.weight != null && (
<BalanceFacts c={cv} by={r.balance_by ?? "weights"} share={open ? null : (shares[i] ?? null)} />
<BalanceFacts c={cv} by={r.balance_by ?? "weights"} share={open ? null : (cv.share ?? null)} />
)}
{open && (
<span className="inline-flex items-center gap-1 tw-label text-warning">
Expand Down
22 changes: 1 addition & 21 deletions src/routing/model.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { describe, expect, it } from "vitest";
import { setLang } from "@/i18n";
import type { ConditionView, DryRunCandidate, GroupView, RouteView, RuleView } from "@/types";
import type { ConditionView, GroupView, RouteView, RuleView } from "@/types";
import {
addOnsText,
balanceNotes,
balanceShares,
blankPinned,
blankRule,
canLift,
Expand Down Expand Up @@ -300,23 +299,4 @@ describe("轮询组的比例和分配依据", () => {
"Round robin (7 : 3 · By reliability)",
);
});

it("试算的占比按权重 × 系数分,熔断着的不参加", () => {
const c = (provider: string, weight: number | null, balance_factor: number | null = null): DryRunCandidate => ({
provider,
weight,
balance_factor,
});
expect(balanceShares({ candidate_models: [c("a", 7), c("b", 3)], circuit_open: [] })).toEqual([0.7, 0.3]);
const auto = balanceShares({ candidate_models: [c("a", 2, 2.25), c("b", 1, 0.5)], circuit_open: [] });
expect(auto[0]).toBeCloseTo(0.9);
expect(auto[1]).toBeCloseTo(0.1);
expect(balanceShares({ candidate_models: [c("a", 1), c("b", 1), c("c", 2)], circuit_open: ["c"] })).toEqual([
0.5, 0.5, 0,
]);
// 全都熔断着时都算:网关照样一家家试
expect(balanceShares({ candidate_models: [c("a", 3), c("b", 1)], circuit_open: ["a", "b"] })).toEqual([0.75, 0.25]);
// 不是轮询组:没有权重,也就没有占比
expect(balanceShares({ candidate_models: [c("a", null)], circuit_open: [] })).toEqual([null]);
});
});
Loading
Loading