@rexvane/piweb 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +7 -6
- package/.next/build-manifest.json +6 -6
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +7 -7
- package/.next/routes-manifest.json +6 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -0
- package/.next/server/app/api/prompts/route.js.nft.json +1 -0
- package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -6
- package/.next/server/chunks/229.js +1 -0
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/686.js +2 -2
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/2kt2UiWjv_viXpNlfjvBT/_buildManifest.js +1 -0
- package/.next/static/chunks/2155.2f27fe7098987699.js +1 -0
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
- package/.next/static/chunks/2716.a9e808190a4e2424.js +1 -0
- package/.next/static/chunks/{794-d1a40dcf24d651d9.js → 3794-3679f1c05160977c.js} +1 -1
- package/.next/static/chunks/4127.dff5c3b33c6bfd1b.js +1 -0
- package/.next/static/chunks/4673.d3d0a7f2599080fb.js +1 -0
- package/.next/static/chunks/4686-5bbee099126af797.js +1 -0
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +1 -0
- package/.next/static/chunks/app/_global-error/page-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/config/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/files/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/git/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/growth/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/health/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/models/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/security/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/skills/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/update/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/version/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-961c055baebb3872.js +1 -0
- package/.next/static/chunks/app/layout-50d06e2078d6ce76.js +1 -0
- package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +1 -0
- package/.next/static/chunks/app/page-00c8ebf40ec5eaca.js +1 -0
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +1 -0
- package/.next/static/chunks/{main-a315d7c58b599962.js → main-6159b22a8cb0b7c5.js} +2 -2
- package/.next/static/chunks/main-app-e9b53f0e1c6591b2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-961c055baebb3872.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-961c055baebb3872.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-961c055baebb3872.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-961c055baebb3872.js +1 -0
- package/.next/static/chunks/webpack-05a9e85529a18b93.js +1 -0
- package/.next/static/css/b2d35abc828d4713.css +1 -0
- package/.next/types/app/api/prompts/route.ts +353 -0
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/package.json +1 -1
- package/src/app/api/prompts/route.ts +51 -0
- package/src/components/AppShell.tsx +42 -19
- package/src/components/ChatWindow.tsx +63 -36
- package/src/components/PromptPanel.tsx +257 -0
- package/src/components/SettingsPanel.tsx +183 -55
- package/src/hooks/usePiWeb.ts +13 -1
- package/src/i18n.tsx +80 -2
- package/src/lib/agent-manager.ts +47 -8
- package/src/lib/clipboard.ts +27 -0
- package/src/lib/command-validation.ts +1 -0
- package/src/lib/event-batcher.ts +62 -0
- package/src/lib/models-service.ts +430 -19
- package/src/lib/prompt-sources.ts +201 -0
- package/src/lib/provider-usage.ts +91 -0
- package/src/lib/session-retention.ts +119 -0
- package/src/lib/types.ts +1 -0
- package/.next/server/chunks/780.js +0 -1
- package/.next/static/CLy0QZWDpEExcItT1yudl/_buildManifest.js +0 -1
- package/.next/static/chunks/155.a3c40ae481da1358.js +0 -1
- package/.next/static/chunks/18.8b025ee3303c3dc7.js +0 -1
- package/.next/static/chunks/4bd1b696-92152b0f5947070d.js +0 -1
- package/.next/static/chunks/608.5b8aae42a9998ee7.js +0 -1
- package/.next/static/chunks/673.93445c8600bc1dd9.js +0 -1
- package/.next/static/chunks/686-97c833615d1565c5.js +0 -1
- package/.next/static/chunks/716.5266fe10b5578591.js +0 -1
- package/.next/static/chunks/app/_global-error/page-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/_not-found/page-9de30f6f614fce02.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/config/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/files/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/git/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/growth/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/health/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/models/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/security/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/skills/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/update/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/version/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/layout-eb62bcd99abe79f9.js +0 -1
- package/.next/static/chunks/app/login/page-d2308b48d013d1a6.js +0 -1
- package/.next/static/chunks/app/page-19e37fe240ae3e20.js +0 -1
- package/.next/static/chunks/framework-0f597d6e68ffafed.js +0 -1
- package/.next/static/chunks/main-app-1a92e20ec190a32b.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-7bb5c4afcbc1772b.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/webpack-eebc3d300017fa47.js +0 -1
- package/.next/static/css/080ec3dba8907e83.css +0 -1
- /package/.next/static/{CLy0QZWDpEExcItT1yudl → 2kt2UiWjv_viXpNlfjvBT}/_ssgManifest.js +0 -0
|
@@ -35,7 +35,8 @@ import { useI18n } from "@/i18n";
|
|
|
35
35
|
import { applyPebrelTheme, loadPebrelTheme, loadThemeMode, type PebrelTheme, type ThemeMode } from "@/lib/theme";
|
|
36
36
|
import { modelDraftFromConfig, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
|
|
37
37
|
import { customApiOptions } from "@/lib/provider-display";
|
|
38
|
-
import type
|
|
38
|
+
import { quotaWindowKind, supportsUsageProbe, type BalanceAmount, type BalanceDetailKey, type ProviderUsage, type QuotaWindow, type XaiUsage } from "@/lib/provider-usage";
|
|
39
|
+
import type { ProviderView } from "@/lib/models-service";
|
|
39
40
|
import type { ToolPreset } from "@/lib/types";
|
|
40
41
|
|
|
41
42
|
type Section = "general" | "models" | "tools" | "skills" | "plugins";
|
|
@@ -886,7 +887,7 @@ function ModelsSection() {
|
|
|
886
887
|
}, [oauthProvider, load]);
|
|
887
888
|
|
|
888
889
|
// xAI 订阅配额(x-ratelimit-* 响应头;后端 5 分钟缓存,force 跳过)
|
|
889
|
-
const [xaiUsage, setXaiUsage] = useState<
|
|
890
|
+
const [xaiUsage, setXaiUsage] = useState<XaiUsage | null>(null);
|
|
890
891
|
const [usageBusy, setUsageBusy] = useState(false);
|
|
891
892
|
const [usageError, setUsageError] = useState(false);
|
|
892
893
|
const fetchUsage = useCallback(async (force: boolean) => {
|
|
@@ -904,6 +905,106 @@ function ModelsSection() {
|
|
|
904
905
|
}
|
|
905
906
|
}, []);
|
|
906
907
|
|
|
908
|
+
// 窗口制/余额制配额:各提供商走各自的只读端点(后端 5 分钟缓存,force 跳过)
|
|
909
|
+
const [providerUsages, setProviderUsages] = useState<Record<string, ProviderUsage | null>>({});
|
|
910
|
+
const [usageBusyId, setUsageBusyId] = useState<string | null>(null);
|
|
911
|
+
const [usageFailedIds, setUsageFailedIds] = useState<Record<string, boolean>>({});
|
|
912
|
+
const fetchProviderUsage = useCallback(async (providerId: string, force: boolean) => {
|
|
913
|
+
setUsageBusyId(providerId);
|
|
914
|
+
setUsageFailedIds((prev) => ({ ...prev, [providerId]: false }));
|
|
915
|
+
try {
|
|
916
|
+
const r = await fetch("/api/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "providerUsage", providerId, force }) });
|
|
917
|
+
const j = await r.json();
|
|
918
|
+
if (!r.ok || !j.success) throw new Error(j.error || "usage probe failed");
|
|
919
|
+
setProviderUsages((prev) => ({ ...prev, [providerId]: j.data.usage ?? null }));
|
|
920
|
+
} catch {
|
|
921
|
+
setUsageFailedIds((prev) => ({ ...prev, [providerId]: true }));
|
|
922
|
+
} finally {
|
|
923
|
+
setUsageBusyId((current) => (current === providerId ? null : current));
|
|
924
|
+
}
|
|
925
|
+
}, []);
|
|
926
|
+
|
|
927
|
+
// 这些端点都是只读探测、不消耗额度,登录后自动取一次;xAI 那条会发一次可能计费的请求,仍然只手动触发
|
|
928
|
+
const requestedUsage = useRef(new Set<string>());
|
|
929
|
+
useEffect(() => {
|
|
930
|
+
for (const p of providers) {
|
|
931
|
+
if (!p.authReady || !supportsUsageProbe(p.id) || p.id === "xai" || requestedUsage.current.has(p.id)) continue;
|
|
932
|
+
requestedUsage.current.add(p.id);
|
|
933
|
+
void fetchProviderUsage(p.id, false);
|
|
934
|
+
}
|
|
935
|
+
}, [providers, fetchProviderUsage]);
|
|
936
|
+
|
|
937
|
+
// 窗口时长 → 标签:7 天叫「周限」、30 天叫「月限」,更短的按小时/天如实标注
|
|
938
|
+
const quotaWindowLabel = (seconds: number): string => {
|
|
939
|
+
const kind = quotaWindowKind(seconds);
|
|
940
|
+
if (kind === "weekly") return t.usageWindowWeekly;
|
|
941
|
+
if (kind === "monthly") return t.usageWindowMonthly;
|
|
942
|
+
return (kind === "hours" ? t.usageWindowHours : t.usageWindowDays).replace("{n}", String(Math.round(seconds / (kind === "hours" ? 3600 : 86400))));
|
|
943
|
+
};
|
|
944
|
+
// 服务端不给周期时(Copilot 的额度类别、OpenCode 的 rolling)按名字标注
|
|
945
|
+
const quotaWindowName = (name: string): string => {
|
|
946
|
+
if (name === "premium") return t.usageWindowPremium;
|
|
947
|
+
if (name === "chat") return t.usageWindowChat;
|
|
948
|
+
if (name === "completions") return t.usageWindowCompletions;
|
|
949
|
+
if (name === "rolling") return t.usageWindowRolling;
|
|
950
|
+
return titleCase(name);
|
|
951
|
+
};
|
|
952
|
+
const quotaVariantLabel = (variant: string): string => {
|
|
953
|
+
if (variant === "opus") return t.usageVariantOpus;
|
|
954
|
+
if (variant === "code") return t.usageVariantCode;
|
|
955
|
+
return titleCase(variant);
|
|
956
|
+
};
|
|
957
|
+
const quotaWindowHeading = (w: QuotaWindow): string => {
|
|
958
|
+
if (w.limitWindowSeconds !== undefined) return `${quotaWindowLabel(w.limitWindowSeconds)}${w.variant ? ` (${quotaVariantLabel(w.variant)})` : ""}`;
|
|
959
|
+
return w.name ? quotaWindowName(w.name) : "";
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// 余额明细项的标签由界面翻译,服务端只给语义 key
|
|
963
|
+
const balanceDetailLabel = (key: BalanceDetailKey): string => {
|
|
964
|
+
if (key === "granted") return t.usageBalanceGranted;
|
|
965
|
+
if (key === "toppedUp") return t.usageBalanceToppedUp;
|
|
966
|
+
if (key === "voucher") return t.usageBalanceVoucher;
|
|
967
|
+
if (key === "cash") return t.usageBalanceCash;
|
|
968
|
+
if (key === "used") return t.usageBalanceUsed;
|
|
969
|
+
return t.usageBalanceLimit;
|
|
970
|
+
};
|
|
971
|
+
const fmtAmount = (value: BalanceAmount): string => `${value.currency === "USD" ? "$" : value.currency === "CNY" ? "¥" : ""}${value.amount}${value.currency && value.currency !== "USD" && value.currency !== "CNY" ? ` ${value.currency}` : ""}`;
|
|
972
|
+
|
|
973
|
+
// 一个提供商的配额行:窗口制显示每个窗口的剩余与重置,余额制显示主余额与明细
|
|
974
|
+
const renderProviderUsage = (providerId: string) => {
|
|
975
|
+
const usage = providerUsages[providerId];
|
|
976
|
+
const busy = usageBusyId === providerId;
|
|
977
|
+
const failed = usageFailedIds[providerId];
|
|
978
|
+
return (
|
|
979
|
+
<>
|
|
980
|
+
<div className="flex items-center gap-2">
|
|
981
|
+
{!usage && <span>{failed ? t.usageProbeFailed : busy ? t.queryingQuota : t.usageReadOnlyDisclosure}</span>}
|
|
982
|
+
{usage?.kind === "quota" && usage.planType && <span>{t.usagePlan} {titleCase(usage.planType)}</span>}
|
|
983
|
+
{usage?.kind === "balance" && usage.primary && (
|
|
984
|
+
<span>{usage.primaryKey === "used" ? t.usageBalanceUsed : t.usageQuotaRemaining} <span style={{ color: "var(--dsw-label-secondary)" }}>{fmtAmount(usage.primary)}</span></span>
|
|
985
|
+
)}
|
|
986
|
+
<button type="button" className="pw-chip" disabled={busy} title={t.usageReadOnlyDisclosure} onClick={() => void fetchProviderUsage(providerId, true)}>
|
|
987
|
+
{busy ? t.usageProbing : usage ? t.usageRefresh : t.usageProbe}
|
|
988
|
+
</button>
|
|
989
|
+
</div>
|
|
990
|
+
{usage?.kind === "balance" && usage.details.length > 0 && (
|
|
991
|
+
<span>{usage.details.map((detail) => `${balanceDetailLabel(detail.key)} ${fmtAmount(detail)}`).join(" · ")}</span>
|
|
992
|
+
)}
|
|
993
|
+
{usage?.kind === "quota" && usage.windows.length === 0 && <span>{t.usageQuotaRemaining} —</span>}
|
|
994
|
+
{usage?.kind === "quota" && usage.windows.map((w, index) => (
|
|
995
|
+
<div key={`${w.limitWindowSeconds ?? w.name ?? "window"}-${w.variant ?? ""}-${index}`} className="flex items-center gap-2">
|
|
996
|
+
<span style={{ width: 84, flex: "none" }}>{quotaWindowHeading(w)}</span>
|
|
997
|
+
<span style={{ width: 92, flex: "none" }}>{t.usageQuotaRemaining} {w.remainingPercent}%</span>
|
|
998
|
+
<span>{t.usageReset} {fmtQuotaReset(w.resetAt)}</span>
|
|
999
|
+
<div className="h-1 overflow-hidden rounded-full" style={{ width: 120, flex: "none", background: "var(--dsw-border-l2)" }}>
|
|
1000
|
+
<div className="h-full rounded-full" style={{ width: `${w.remainingPercent}%`, background: quotaToneColor(w.remainingPercent) }} />
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
))}
|
|
1004
|
+
</>
|
|
1005
|
+
);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
907
1008
|
const notify = (ok: boolean, msg?: string) => {
|
|
908
1009
|
setToast({ ok, msg: msg ?? (ok ? t.toastSaved : t.toastError) });
|
|
909
1010
|
setTimeout(() => setToast(null), 2400);
|
|
@@ -1129,69 +1230,71 @@ function ModelsSection() {
|
|
|
1129
1230
|
const editingThis = editing === p.id;
|
|
1130
1231
|
return (
|
|
1131
1232
|
<div key={p.id}>
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
<div className="
|
|
1135
|
-
<
|
|
1136
|
-
<div className="
|
|
1137
|
-
{
|
|
1233
|
+
{/* 卡片:身份/操作行在上,额度显示放在同一个框内的第二段(分隔线区分) */}
|
|
1234
|
+
<div className="flex flex-col rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
|
|
1235
|
+
<div className="flex items-center gap-3">
|
|
1236
|
+
<ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
|
|
1237
|
+
<div className="min-w-0 flex-1">
|
|
1238
|
+
<div className="truncate" style={{ fontSize: 14, fontWeight: 500 }}>{providerName(p.id)}</div>
|
|
1239
|
+
<div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
1240
|
+
{p.id}{p.baseUrl ? ` · ${p.baseUrl}` : ""}
|
|
1241
|
+
</div>
|
|
1138
1242
|
</div>
|
|
1139
|
-
|
|
1140
|
-
|
|
1243
|
+
{isCustom && (
|
|
1244
|
+
<span
|
|
1245
|
+
className="rounded-md px-1.5 py-0.5"
|
|
1246
|
+
style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
|
|
1247
|
+
>
|
|
1248
|
+
{t.customBadge}
|
|
1249
|
+
</span>
|
|
1250
|
+
)}
|
|
1251
|
+
<span
|
|
1252
|
+
className="inline-block h-2 w-2 rounded-full"
|
|
1253
|
+
style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
|
|
1254
|
+
title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
|
|
1255
|
+
/>
|
|
1141
1256
|
<span
|
|
1142
1257
|
className="rounded-md px-1.5 py-0.5"
|
|
1143
1258
|
style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
|
|
1144
1259
|
>
|
|
1145
|
-
{t.
|
|
1260
|
+
{t.credentialSource.replace("{source}", credentialSource(p))}
|
|
1146
1261
|
</span>
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
className="inline-block h-2 w-2 rounded-full"
|
|
1150
|
-
style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
|
|
1151
|
-
title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
|
|
1152
|
-
/>
|
|
1153
|
-
<span
|
|
1154
|
-
className="rounded-md px-1.5 py-0.5"
|
|
1155
|
-
style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
|
|
1156
|
-
>
|
|
1157
|
-
{t.credentialSource.replace("{source}", credentialSource(p))}
|
|
1158
|
-
</span>
|
|
1159
|
-
<button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
|
|
1160
|
-
{t.edit}
|
|
1161
|
-
</button>
|
|
1162
|
-
{p.storedAuthType === "oauth" && (
|
|
1163
|
-
<button
|
|
1164
|
-
style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
|
|
1165
|
-
onClick={async () => {
|
|
1166
|
-
if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
|
|
1167
|
-
await call({ action: "removeKey", providerId: p.id });
|
|
1168
|
-
}}
|
|
1169
|
-
>
|
|
1170
|
-
{t.removeOAuth}
|
|
1171
|
-
</button>
|
|
1172
|
-
)}
|
|
1173
|
-
{p.authTypes?.includes("oauth") && (
|
|
1174
|
-
<button
|
|
1175
|
-
className="btn-outline"
|
|
1176
|
-
style={{ height: 28, padding: "0 12px", fontSize: 12 }}
|
|
1177
|
-
onClick={() => {
|
|
1178
|
-
setOauthDone(null);
|
|
1179
|
-
void call({ action: "loginStart", providerId: p.id });
|
|
1180
|
-
setOauthProvider(p.id);
|
|
1181
|
-
}}
|
|
1182
|
-
>
|
|
1183
|
-
{t.oauthLogin}
|
|
1262
|
+
<button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
|
|
1263
|
+
{t.edit}
|
|
1184
1264
|
</button>
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1265
|
+
{p.storedAuthType === "oauth" && (
|
|
1266
|
+
<button
|
|
1267
|
+
style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
|
|
1268
|
+
onClick={async () => {
|
|
1269
|
+
if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
|
|
1270
|
+
await call({ action: "removeKey", providerId: p.id });
|
|
1271
|
+
}}
|
|
1272
|
+
>
|
|
1273
|
+
{t.removeOAuth}
|
|
1274
|
+
</button>
|
|
1275
|
+
)}
|
|
1276
|
+
{p.authTypes?.includes("oauth") && (
|
|
1277
|
+
<button
|
|
1278
|
+
className="btn-outline"
|
|
1279
|
+
style={{ height: 28, padding: "0 12px", fontSize: 12 }}
|
|
1280
|
+
onClick={() => {
|
|
1281
|
+
setOauthDone(null);
|
|
1282
|
+
void call({ action: "loginStart", providerId: p.id });
|
|
1283
|
+
setOauthProvider(p.id);
|
|
1284
|
+
}}
|
|
1285
|
+
>
|
|
1286
|
+
{t.oauthLogin}
|
|
1287
|
+
</button>
|
|
1288
|
+
)}
|
|
1289
|
+
{customIds.has(p.id) && (
|
|
1290
|
+
<button style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} onClick={() => deleteCustom(p.id)}>
|
|
1291
|
+
{t.delete}
|
|
1292
|
+
</button>
|
|
1293
|
+
)}
|
|
1191
1294
|
</div>
|
|
1192
1295
|
|
|
1193
1296
|
{p.id === "xai" && p.authReady && (
|
|
1194
|
-
<div className="mt-
|
|
1297
|
+
<div className="mt-3 flex items-center gap-2 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
|
|
1195
1298
|
{xaiUsage ? (
|
|
1196
1299
|
<span>{t.usageQuotaLabel} · {xaiUsage.model} · {t.usageRequests} {xaiUsage.requestRemaining.toLocaleString()}/{xaiUsage.requestLimit.toLocaleString()} · Tokens {fmtQuotaTokens(xaiUsage.tokenRemaining)}/{fmtQuotaTokens(xaiUsage.tokenLimit)}</span>
|
|
1197
1300
|
) : <span>{usageError ? t.usageProbeFailed : t.usageProbeDisclosure}</span>}
|
|
@@ -1201,6 +1304,13 @@ function ModelsSection() {
|
|
|
1201
1304
|
</div>
|
|
1202
1305
|
)}
|
|
1203
1306
|
|
|
1307
|
+
{supportsUsageProbe(p.id) && p.id !== "xai" && p.authReady && (
|
|
1308
|
+
<div className="mt-3 flex flex-col gap-1.5 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
|
|
1309
|
+
{renderProviderUsage(p.id)}
|
|
1310
|
+
</div>
|
|
1311
|
+
)}
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1204
1314
|
{editingThis && (
|
|
1205
1315
|
<div className="mt-2 rounded-2xl p-4" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
|
|
1206
1316
|
{isCustom ? (
|
|
@@ -1501,6 +1611,24 @@ function fmtQuotaTokens(value: number): string {
|
|
|
1501
1611
|
return value.toLocaleString();
|
|
1502
1612
|
}
|
|
1503
1613
|
|
|
1614
|
+
/** 套餐值来自服务端声明(如 "pro"/"plus"),首字母大写后直接跟在标签后显示。 */
|
|
1615
|
+
function titleCase(value: string): string {
|
|
1616
|
+
return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function fmtQuotaReset(value: string | undefined): string {
|
|
1620
|
+
if (!value) return "—";
|
|
1621
|
+
const date = new Date(value);
|
|
1622
|
+
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** 剩余越少越警示:<30% 红、≤60% 黄、其余常规。 */
|
|
1626
|
+
function quotaToneColor(percent: number): string {
|
|
1627
|
+
if (percent < 30) return "var(--dsw-danger)";
|
|
1628
|
+
if (percent <= 60) return "var(--dsw-warning)";
|
|
1629
|
+
return "var(--dsw-success)";
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1504
1632
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
1505
1633
|
return (
|
|
1506
1634
|
<div>
|
package/src/hooks/usePiWeb.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 中央状态:会话列表轮询、当前会话 SSE 订阅、事件折叠(快照→增量)、命令发送。
|
|
5
5
|
*/
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { createEventBatcher, type EventBatcher } from "@/lib/event-batcher";
|
|
7
8
|
import type {
|
|
8
9
|
GrowthStep,
|
|
9
10
|
SessionSummary,
|
|
@@ -334,6 +335,8 @@ export function usePiWeb() {
|
|
|
334
335
|
const [currentId, setCurrentId] = useState<string | null>(null);
|
|
335
336
|
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
|
336
337
|
const [state, setState] = useState<PiWebState>(emptyState);
|
|
338
|
+
/** 当前 SSE 连接的增量批量器:按帧合并事件,避免每个 token 一次整树渲染 */
|
|
339
|
+
const batcherRef = useRef<EventBatcher<WebEvent> | null>(null);
|
|
337
340
|
const [models, setModels] = useState<{ providers: any[]; models: any[] } | null>(null);
|
|
338
341
|
const [addedWorkspaces, setAddedWorkspaces] = useState<string[]>([]);
|
|
339
342
|
const [removedWorkspaces, setRemovedWorkspaces] = useState<string[]>([]);
|
|
@@ -637,6 +640,11 @@ export function usePiWeb() {
|
|
|
637
640
|
if (esRef.current === null) connect();
|
|
638
641
|
}, delay);
|
|
639
642
|
};
|
|
643
|
+
// 流式事件按帧合并:token 速率再高也只每 16ms 渲染一次(见 event-batcher)
|
|
644
|
+
const batcher = createEventBatcher<WebEvent>({
|
|
645
|
+
flush: (events) => setState((s) => events.reduce((acc, evt) => foldPiWebEvent(acc, evt), s)),
|
|
646
|
+
});
|
|
647
|
+
batcherRef.current = batcher;
|
|
640
648
|
es.onmessage = (e) => {
|
|
641
649
|
let parsed: any;
|
|
642
650
|
try {
|
|
@@ -646,6 +654,8 @@ export function usePiWeb() {
|
|
|
646
654
|
return;
|
|
647
655
|
}
|
|
648
656
|
if (parsed.type === "snapshot" && parsed.snapshot) {
|
|
657
|
+
// 快照是权威状态:之前缓冲的增量已经过时,直接丢弃
|
|
658
|
+
batcher.cancel();
|
|
649
659
|
retryDelayMs = 2000;
|
|
650
660
|
const snap = parsed.snapshot as WebSnapshot;
|
|
651
661
|
setSnapshotEpoch((epoch) => epoch + 1);
|
|
@@ -667,13 +677,15 @@ export function usePiWeb() {
|
|
|
667
677
|
growth: { ...prev.growth, error: snap.growthError ?? null },
|
|
668
678
|
}));
|
|
669
679
|
} else {
|
|
670
|
-
|
|
680
|
+
batcher.push(parsed as WebEvent);
|
|
671
681
|
}
|
|
672
682
|
};
|
|
673
683
|
};
|
|
674
684
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
675
685
|
connect();
|
|
676
686
|
return () => {
|
|
687
|
+
batcherRef.current?.cancel();
|
|
688
|
+
batcherRef.current = null;
|
|
677
689
|
if (retryTimer) clearTimeout(retryTimer);
|
|
678
690
|
esRef.current?.close();
|
|
679
691
|
esRef.current = null;
|
package/src/i18n.tsx
CHANGED
|
@@ -21,7 +21,6 @@ const zh = {
|
|
|
21
21
|
copyFailed: "复制失败",
|
|
22
22
|
editMessage: "编辑这条消息",
|
|
23
23
|
sendEdit: "重新发送",
|
|
24
|
-
editWhileRunning: "会话正在运行,先停止再编辑",
|
|
25
24
|
editMessageFailed: "编辑重发失败",
|
|
26
25
|
export: "导出",
|
|
27
26
|
settings: "设置",
|
|
@@ -210,6 +209,27 @@ const zh = {
|
|
|
210
209
|
usageProbing: "检测中…",
|
|
211
210
|
usageProbeDisclosure: "检测会向 xAI 发送一次可能计费的最小请求。",
|
|
212
211
|
usageProbeFailed: "配额检测失败,未获得用量信息。",
|
|
212
|
+
usagePlan: "套餐",
|
|
213
|
+
usageQuotaRemaining: "额度剩余",
|
|
214
|
+
queryingQuota: "正在查询额度…",
|
|
215
|
+
usageReadOnlyDisclosure: "读取提供商的只读用量端点,不消耗额度。",
|
|
216
|
+
usageReset: "刷新时间",
|
|
217
|
+
usageWindowWeekly: "周限",
|
|
218
|
+
usageWindowMonthly: "月限",
|
|
219
|
+
usageWindowHours: "{n} 小时",
|
|
220
|
+
usageWindowDays: "{n} 天",
|
|
221
|
+
usageWindowRolling: "滚动窗口",
|
|
222
|
+
usageWindowChat: "对话",
|
|
223
|
+
usageWindowCompletions: "补全",
|
|
224
|
+
usageWindowPremium: "高级请求",
|
|
225
|
+
usageVariantOpus: "Opus",
|
|
226
|
+
usageVariantCode: "代码",
|
|
227
|
+
usageBalanceGranted: "赠金",
|
|
228
|
+
usageBalanceToppedUp: "充值",
|
|
229
|
+
usageBalanceVoucher: "代金券",
|
|
230
|
+
usageBalanceCash: "现金",
|
|
231
|
+
usageBalanceUsed: "已用",
|
|
232
|
+
usageBalanceLimit: "上限",
|
|
213
233
|
oauthWait: "等待授权…",
|
|
214
234
|
oauthCancel: "取消登录",
|
|
215
235
|
noSessions: "暂无会话",
|
|
@@ -264,6 +284,25 @@ const zh = {
|
|
|
264
284
|
contextSystem: "系统提示词",
|
|
265
285
|
contextMessages: "对话消息",
|
|
266
286
|
contextTools: "工具",
|
|
287
|
+
promptPanel: "提示词来源",
|
|
288
|
+
promptRefresh: "刷新",
|
|
289
|
+
promptLoading: "加载中…",
|
|
290
|
+
promptColdHint: "会话尚未启动:先列出磁盘上的提示词文件;打开会话后才会出现「组装后的系统提示词」与工具定义。",
|
|
291
|
+
promptOriginProject: "项目",
|
|
292
|
+
promptOriginAgent: "个人",
|
|
293
|
+
promptOriginPackage: "包",
|
|
294
|
+
promptOriginExtension: "扩展",
|
|
295
|
+
promptKindSystem: "系统提示词文件",
|
|
296
|
+
promptKindAppend: "追加提示词",
|
|
297
|
+
promptKindAgents: "项目记忆(AGENTS.md)",
|
|
298
|
+
promptKindSkill: "技能提示词",
|
|
299
|
+
promptKindTemplate: "提示模板",
|
|
300
|
+
promptKindTool: "工具提示词",
|
|
301
|
+
promptKindAssembled: "组装后的系统提示词",
|
|
302
|
+
promptAssembledKey: "当前生效的完整系统提示词",
|
|
303
|
+
promptToolBuiltin: "内置",
|
|
304
|
+
promptToolExtension: "扩展",
|
|
305
|
+
copySuccess: "已复制",
|
|
267
306
|
contextSystemPrompt: "System Prompt",
|
|
268
307
|
contextSystemTools: "System Tools",
|
|
269
308
|
contextCustomTools: "Custom Tools",
|
|
@@ -557,7 +596,6 @@ const en: Dict = {
|
|
|
557
596
|
copyFailed: "Copy failed",
|
|
558
597
|
editMessage: "Edit this message",
|
|
559
598
|
sendEdit: "Send again",
|
|
560
|
-
editWhileRunning: "This session is running; stop it before editing",
|
|
561
599
|
editMessageFailed: "Edit and resend failed",
|
|
562
600
|
export: "Export",
|
|
563
601
|
settings: "Settings",
|
|
@@ -745,6 +783,27 @@ const en: Dict = {
|
|
|
745
783
|
usageProbing: "Checking…",
|
|
746
784
|
usageProbeDisclosure: "Checking sends one minimal request to xAI that may be billed.",
|
|
747
785
|
usageProbeFailed: "Quota check failed; no usage data was returned.",
|
|
786
|
+
usagePlan: "Plan",
|
|
787
|
+
usageQuotaRemaining: "Remaining quota",
|
|
788
|
+
queryingQuota: "Querying quota…",
|
|
789
|
+
usageReadOnlyDisclosure: "Reads the provider's read-only usage endpoint; consumes no quota.",
|
|
790
|
+
usageReset: "Resets",
|
|
791
|
+
usageWindowWeekly: "Weekly",
|
|
792
|
+
usageWindowMonthly: "Monthly",
|
|
793
|
+
usageWindowHours: "{n}h",
|
|
794
|
+
usageWindowDays: "{n}d",
|
|
795
|
+
usageWindowRolling: "Rolling",
|
|
796
|
+
usageWindowChat: "Chat",
|
|
797
|
+
usageWindowCompletions: "Completions",
|
|
798
|
+
usageWindowPremium: "Premium requests",
|
|
799
|
+
usageVariantOpus: "Opus",
|
|
800
|
+
usageVariantCode: "Code",
|
|
801
|
+
usageBalanceGranted: "Granted",
|
|
802
|
+
usageBalanceToppedUp: "Topped up",
|
|
803
|
+
usageBalanceVoucher: "Voucher",
|
|
804
|
+
usageBalanceCash: "Cash",
|
|
805
|
+
usageBalanceUsed: "Used",
|
|
806
|
+
usageBalanceLimit: "Limit",
|
|
748
807
|
oauthWait: "Waiting for authorization…",
|
|
749
808
|
oauthCancel: "Cancel login",
|
|
750
809
|
noSessions: "No sessions yet",
|
|
@@ -797,6 +856,25 @@ const en: Dict = {
|
|
|
797
856
|
contextSystem: "System prompt",
|
|
798
857
|
contextMessages: "Messages",
|
|
799
858
|
contextTools: "Tools",
|
|
859
|
+
promptPanel: "Prompt sources",
|
|
860
|
+
promptRefresh: "Refresh",
|
|
861
|
+
promptLoading: "Loading…",
|
|
862
|
+
promptColdHint: "Session not started yet: disk files are listed now; the assembled system prompt and tool definitions appear once the session is running.",
|
|
863
|
+
promptOriginProject: "project",
|
|
864
|
+
promptOriginAgent: "user",
|
|
865
|
+
promptOriginPackage: "package",
|
|
866
|
+
promptOriginExtension: "extension",
|
|
867
|
+
promptKindSystem: "System prompt file",
|
|
868
|
+
promptKindAppend: "Appended prompt",
|
|
869
|
+
promptKindAgents: "Project memory (AGENTS.md)",
|
|
870
|
+
promptKindSkill: "Skill prompts",
|
|
871
|
+
promptKindTemplate: "Prompt templates",
|
|
872
|
+
promptKindTool: "Tool prompts",
|
|
873
|
+
promptKindAssembled: "Assembled system prompt",
|
|
874
|
+
promptAssembledKey: "Effective system prompt for this session",
|
|
875
|
+
promptToolBuiltin: "builtin",
|
|
876
|
+
promptToolExtension: "extension",
|
|
877
|
+
copySuccess: "Copied",
|
|
800
878
|
contextSystemPrompt: "System Prompt",
|
|
801
879
|
contextSystemTools: "System Tools",
|
|
802
880
|
contextCustomTools: "Custom Tools",
|
package/src/lib/agent-manager.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createExtensionUiBridge, type ExtensionUiBridge } from "./extension-ui"
|
|
|
30
30
|
import { createGrowthTracker, GROWTH_TRACKER_VERSION, type GrowthTracker } from "./growth-tracker";
|
|
31
31
|
import { userTurnsFromEntries } from "./growth-turns";
|
|
32
32
|
import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajectory";
|
|
33
|
+
import { startArchivedSessionRetention } from "./session-retention";
|
|
33
34
|
import { sanitizeToolOutput } from "./text-sanitize";
|
|
34
35
|
import { BoundaryError } from "./path-security";
|
|
35
36
|
import { estimateTokensOf } from "./process-format";
|
|
@@ -683,17 +684,15 @@ async function buildContextBreakdown(
|
|
|
683
684
|
// Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
|
|
684
685
|
const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
|
|
685
686
|
breakdown.memory = estimateTokensOf(memoryText);
|
|
686
|
-
// Skills:从资源加载器取技能清单,按
|
|
687
|
+
// Skills:从资源加载器取技能清单,按 SDK 的 formatSkillsForPrompt 段落定位。
|
|
688
|
+
// 该段落没有 Markdown 标题(原文以 "The following skills provide specialized instructions…" 开头,
|
|
689
|
+
// 结尾是 </available_skills>),以前按 "## Skills" 找永远找不到,技能段一直是 0。
|
|
687
690
|
try {
|
|
688
691
|
const { skills } = session.resourceLoader.getSkills();
|
|
689
692
|
if (skills.length && prompt) {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const next = prompt.slice(header + 1).search(/^## /m);
|
|
694
|
-
const section = next >= 0 ? prompt.slice(header, header + 1 + next) : prompt.slice(header);
|
|
695
|
-
breakdown.skills = estimateTokensOf(section);
|
|
696
|
-
}
|
|
693
|
+
const start = prompt.indexOf("The following skills provide specialized instructions");
|
|
694
|
+
const end = prompt.indexOf("</available_skills>");
|
|
695
|
+
if (start >= 0 && end > start) breakdown.skills = estimateTokensOf(prompt.slice(start, end + "</available_skills>".length));
|
|
697
696
|
}
|
|
698
697
|
} catch {
|
|
699
698
|
/* loader 不可用则跳过 */
|
|
@@ -969,6 +968,17 @@ export function reap(): void {
|
|
|
969
968
|
|
|
970
969
|
setInterval(reap, 60_000).unref?.();
|
|
971
970
|
|
|
971
|
+
/** 归档会话的保留期清理:只删「已归档且长期无活动」的会话;正在使用的会话一律跳过 */
|
|
972
|
+
function isSessionInUse(sessionPath: string): boolean {
|
|
973
|
+
const key = process.platform === "win32" ? sessionPath.toLowerCase() : sessionPath;
|
|
974
|
+
for (const managed of sessions.keys()) {
|
|
975
|
+
if ((process.platform === "win32" ? managed.toLowerCase() : managed) === key) return true;
|
|
976
|
+
}
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
startArchivedSessionRetention({ isActive: isSessionInUse, log: (message) => console.log(message) });
|
|
981
|
+
|
|
972
982
|
// ---------- 资源清单(技能 / 提示模板 / 扩展命令 / 信任 / 诊断) ----------
|
|
973
983
|
|
|
974
984
|
type ResourceBundle = Pick<WebSnapshot, "skills" | "promptTemplates" | "extensionCommands" | "projectTrust" | "resourceDiagnostics">;
|
|
@@ -1430,6 +1440,35 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1430
1440
|
const r = await session.navigateTree(entryId);
|
|
1431
1441
|
return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
|
|
1432
1442
|
}
|
|
1443
|
+
case "editAndResend": {
|
|
1444
|
+
// 编辑用户消息并重发:先停在跑的回合(用户改口即表示不要这一轮的输出),
|
|
1445
|
+
// 再回到该消息之前用新文本重新提问。放在服务端串行做,避免客户端"先 abort 再发"的竞态。
|
|
1446
|
+
const entryId = String((cmd as any).entryId ?? "");
|
|
1447
|
+
const text = (cmd.text ?? "").trim();
|
|
1448
|
+
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1449
|
+
if (!text) return { ok: false, error: "empty prompt" };
|
|
1450
|
+
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1451
|
+
m.promptSubmitting = true;
|
|
1452
|
+
try {
|
|
1453
|
+
const session = await ensureSession(m);
|
|
1454
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
1455
|
+
if (session.isStreaming || m.runActive) {
|
|
1456
|
+
await session.abort().catch(() => undefined);
|
|
1457
|
+
// 等 SDK 真正回到空闲(abort 是异步的;最多等 5 秒,超时就按当前状态继续)
|
|
1458
|
+
for (let i = 0; i < 50 && !m.disposed && (session.isStreaming || m.runActive); i += 1) {
|
|
1459
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
1463
|
+
const moved = await session.navigateTree(entryId);
|
|
1464
|
+
if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
|
|
1465
|
+
await growthOf(m).prepare().catch(() => undefined);
|
|
1466
|
+
await session.prompt(text);
|
|
1467
|
+
return { ok: true };
|
|
1468
|
+
} finally {
|
|
1469
|
+
m.promptSubmitting = false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1433
1472
|
case "rename": {
|
|
1434
1473
|
const name = (cmd.text ?? "").trim();
|
|
1435
1474
|
if (m.session) m.session.setSessionName(name);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 剪贴板工具:优先用异步 Clipboard API,非安全上下文(局域网 http)退回 textarea + execCommand。
|
|
3
|
+
* 从 ChatWindow 抽出来共用(消息操作行与提示词面板都用它)。
|
|
4
|
+
*/
|
|
5
|
+
export async function copyText(text: string): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
if (navigator.clipboard?.writeText) {
|
|
8
|
+
await navigator.clipboard.writeText(text);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
// Non-secure local-network contexts need the legacy fallback below.
|
|
13
|
+
}
|
|
14
|
+
const field = document.createElement("textarea");
|
|
15
|
+
field.value = text;
|
|
16
|
+
field.style.position = "fixed";
|
|
17
|
+
field.style.opacity = "0";
|
|
18
|
+
document.body.appendChild(field);
|
|
19
|
+
field.select();
|
|
20
|
+
try {
|
|
21
|
+
return document.execCommand("copy");
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
} finally {
|
|
25
|
+
field.remove();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件批量器:把高频流式事件合并成"每帧一次"的批量更新。
|
|
3
|
+
*
|
|
4
|
+
* 服务端按 token 推 delta,客户端原本每个事件一次 setState → 一次整树渲染。
|
|
5
|
+
* 合并后一次渲染里按顺序折叠整批事件,token 速率再高也只有每 16ms 一次渲染,
|
|
6
|
+
* 主线程不再被切碎(点击、滚动因此变顺)。
|
|
7
|
+
*/
|
|
8
|
+
export interface EventBatcher<T> {
|
|
9
|
+
/** 入队;达到上限立刻冲刷,避免极端情况下无限堆积 */
|
|
10
|
+
push(event: T): void;
|
|
11
|
+
flush(): void;
|
|
12
|
+
/** 丢弃未处理的事件(例如快照到达:旧事件已经过时) */
|
|
13
|
+
cancel(): void;
|
|
14
|
+
size(): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createEventBatcher<T>({
|
|
18
|
+
flush,
|
|
19
|
+
schedule = (run) => {
|
|
20
|
+
const id = setTimeout(run, 16);
|
|
21
|
+
return () => clearTimeout(id);
|
|
22
|
+
},
|
|
23
|
+
limit = 400,
|
|
24
|
+
}: {
|
|
25
|
+
flush: (events: T[]) => void;
|
|
26
|
+
schedule?: (run: () => void) => () => void;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}): EventBatcher<T> {
|
|
29
|
+
let pending: T[] = [];
|
|
30
|
+
let cancelScheduled: (() => void) | null = null;
|
|
31
|
+
|
|
32
|
+
const stopScheduled = () => {
|
|
33
|
+
if (!cancelScheduled) return;
|
|
34
|
+
cancelScheduled();
|
|
35
|
+
cancelScheduled = null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const flushNow = () => {
|
|
39
|
+
stopScheduled();
|
|
40
|
+
if (!pending.length) return;
|
|
41
|
+
const batch = pending;
|
|
42
|
+
pending = [];
|
|
43
|
+
flush(batch);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
push(event) {
|
|
48
|
+
pending.push(event);
|
|
49
|
+
if (pending.length >= limit) {
|
|
50
|
+
flushNow();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
cancelScheduled ??= schedule(flushNow);
|
|
54
|
+
},
|
|
55
|
+
flush: flushNow,
|
|
56
|
+
cancel() {
|
|
57
|
+
stopScheduled();
|
|
58
|
+
pending = [];
|
|
59
|
+
},
|
|
60
|
+
size: () => pending.length,
|
|
61
|
+
};
|
|
62
|
+
}
|