@rexvane/piweb 0.3.19 → 0.3.21
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 +4 -4
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +4 -4
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- 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_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 +3 -3
- 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/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +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/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 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +2 -2
- 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/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +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_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.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +4 -4
- package/.next/server/chunks/421.js +1 -0
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/662.js +2 -0
- package/.next/server/chunks/686.js +1 -1
- package/.next/server/chunks/807.js +1 -1
- package/.next/server/chunks/924.js +2 -2
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- 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/chunks/4686-882531aef1d2e250.js +1 -0
- package/.next/static/chunks/{4778.e4c9ed093a413bc4.js → 4778.305dc6abb9889627.js} +1 -1
- package/.next/static/chunks/839.bfd2db36483faa3e.js +1 -0
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
- package/.next/static/chunks/app/page-c04af72e549695bd.js +1 -0
- package/.next/static/chunks/webpack-9b4bb574e5817654.js +1 -0
- package/.next/static/css/ea27b9e30f76303f.css +1 -0
- package/README.md +14 -5
- package/next.config.ts +4 -2
- package/package.json +1 -1
- package/scripts/release.mjs +1 -1
- package/src/app/api/agent/[id]/events/route.ts +42 -13
- package/src/app/api/remote/route.ts +12 -2
- package/src/app/api/update/route.ts +18 -2
- package/src/app/globals.css +89 -0
- package/src/components/AppShell.tsx +7 -3
- package/src/components/ChatInput.tsx +1 -0
- package/src/components/ChatWindow.tsx +105 -81
- package/src/components/ContextMeter.tsx +31 -0
- package/src/components/DrawerPanel.tsx +19 -26
- package/src/components/ExtensionUI.tsx +8 -2
- package/src/components/MarkdownBlock.tsx +333 -37
- package/src/components/RemoteSection.tsx +41 -8
- package/src/components/SettingsPanel.tsx +122 -53
- package/src/components/chat/chat-scroll.ts +18 -0
- package/src/components/update/CopyCommand.tsx +34 -0
- package/src/components/update/UpdateNotice.tsx +130 -0
- package/src/components/update/update-store.ts +155 -0
- package/src/hooks/usePiWeb.ts +42 -25
- package/src/i18n.tsx +82 -10
- package/src/instrumentation-node.ts +3 -0
- package/src/lib/agent-manager.ts +100 -57
- package/src/lib/chat/event-stream.ts +113 -0
- package/src/lib/chat/stream-settings.ts +192 -0
- package/src/lib/remote/mobile-proxy.ts +8 -2
- package/src/lib/remote/supervisor.ts +18 -8
- package/src/lib/types.ts +7 -0
- package/src/lib/update/startup-check.ts +117 -0
- package/src/lib/update-service.ts +23 -3
- package/.next/server/chunks/131.js +0 -1
- package/.next/server/chunks/243.js +0 -1
- package/.next/static/chunks/4686-e3825f3b6a384aa1.js +0 -1
- package/.next/static/chunks/839.7403c6bdace4c56c.js +0 -1
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
- package/.next/static/chunks/webpack-bd2b9202ecbcdcaa.js +0 -1
- package/.next/static/css/7391c42e1ae617f9.css +0 -1
- /package/.next/static/{JDGSR-TIbk9qZWcUNYIX3 → sNX5GQkOWuK5YEID1hT-C}/_buildManifest.js +0 -0
- /package/.next/static/{JDGSR-TIbk9qZWcUNYIX3 → sNX5GQkOWuK5YEID1hT-C}/_ssgManifest.js +0 -0
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 { pollEventStream, usesEventPolling } from "@/lib/chat/event-stream";
|
|
7
8
|
import { createEventBatcher, type EventBatcher } from "@/lib/event-batcher";
|
|
8
9
|
import type {
|
|
9
10
|
GrowthConsentRequest,
|
|
@@ -194,6 +195,9 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
|
|
|
194
195
|
tools: { active: evt.active, all: evt.all.map((n) => ({ name: n })) },
|
|
195
196
|
};
|
|
196
197
|
return s;
|
|
198
|
+
case "context_breakdown":
|
|
199
|
+
if (s.snapshot) s.snapshot = { ...s.snapshot, contextBreakdown: evt.breakdown };
|
|
200
|
+
return s;
|
|
197
201
|
case "resources":
|
|
198
202
|
if (s.snapshot) {
|
|
199
203
|
s.snapshot = {
|
|
@@ -350,7 +354,7 @@ export function usePiWeb() {
|
|
|
350
354
|
const [orderBy, setOrderByState] = useState<"updated" | "manual">("updated");
|
|
351
355
|
const [resyncNonce, setResyncNonce] = useState(0);
|
|
352
356
|
const [snapshotEpoch, setSnapshotEpoch] = useState(0);
|
|
353
|
-
const esRef = useRef<
|
|
357
|
+
const esRef = useRef<{ close(): void } | null>(null);
|
|
354
358
|
const promptRequestsRef = useRef(new Set<string>());
|
|
355
359
|
const presetRestoreAttemptRef = useRef<string | null>(null);
|
|
356
360
|
const presetRestorePromiseRef = useRef<Promise<void> | null>(null);
|
|
@@ -674,38 +678,21 @@ export function usePiWeb() {
|
|
|
674
678
|
? { ...current, connected: false }
|
|
675
679
|
: { ...emptyState(savedToolPreset()), connected: false });
|
|
676
680
|
let retryDelayMs = 2000;
|
|
681
|
+
const eventsUrl = `/api/agent/${currentId}/events`;
|
|
677
682
|
const connect = () => {
|
|
678
|
-
const es = new EventSource(`/api/agent/${currentId}/events`);
|
|
679
|
-
esRef.current = es;
|
|
680
|
-
es.onopen = () => {
|
|
681
|
-
retryDelayMs = 2000;
|
|
682
|
-
setState((s) => ({ ...s, connected: true }));
|
|
683
|
-
};
|
|
684
|
-
es.onerror = () => {
|
|
685
|
-
setState((s) => ({ ...s, connected: false }));
|
|
686
|
-
if (esRef.current !== es) return;
|
|
687
|
-
// 网络断开时浏览器自己会重连,并带上 Last-Event-ID 让服务端从环形缓冲续放
|
|
688
|
-
// (只补漏掉的几条事件)。这时手动重建就等于丢掉续传、每次都拉一份全量快照。
|
|
689
|
-
if (es.readyState !== EventSource.CLOSED) return;
|
|
690
|
-
// 浏览器对非 200 响应会永久放弃自动重连:这里手动兜底,关掉后延迟重建,
|
|
691
|
-
// 重建时会拿到全新快照。
|
|
692
|
-
es.close();
|
|
693
|
-
esRef.current = null;
|
|
694
|
-
const delay = retryDelayMs;
|
|
695
|
-
retryDelayMs = Math.min(retryDelayMs * 2, 30_000);
|
|
696
|
-
retryTimer = setTimeout(() => {
|
|
697
|
-
if (esRef.current === null) connect();
|
|
698
|
-
}, delay);
|
|
699
|
-
};
|
|
700
683
|
// 流式事件按帧合并:token 速率再高也只每 16ms 渲染一次(见 event-batcher)
|
|
701
684
|
const batcher = createEventBatcher<WebEvent>({
|
|
702
685
|
flush: (events) => setState((s) => events.reduce((acc, evt) => foldPiWebEvent(acc, evt), s)),
|
|
703
686
|
});
|
|
704
687
|
batcherRef.current = batcher;
|
|
705
|
-
|
|
688
|
+
const onOpen = () => {
|
|
689
|
+
retryDelayMs = 2000;
|
|
690
|
+
setState((s) => ({ ...s, connected: true }));
|
|
691
|
+
};
|
|
692
|
+
const onMessage = (data: string) => {
|
|
706
693
|
let parsed: any;
|
|
707
694
|
try {
|
|
708
|
-
parsed = JSON.parse(
|
|
695
|
+
parsed = JSON.parse(data);
|
|
709
696
|
} catch {
|
|
710
697
|
// 单帧损坏(代理截断等)只丢弃该帧,不能让 EventSource 回调抛异常
|
|
711
698
|
return;
|
|
@@ -737,6 +724,36 @@ export function usePiWeb() {
|
|
|
737
724
|
batcher.push(parsed as WebEvent);
|
|
738
725
|
}
|
|
739
726
|
};
|
|
727
|
+
// Cloudflare 快速隧道不支持 SSE(流被攒成 128KB 一块,快照永远收不全):改用长轮询,
|
|
728
|
+
// 失败退避、Last-Event-ID 续传都在 pollEventStream 里
|
|
729
|
+
if (usesEventPolling(window.location.hostname)) {
|
|
730
|
+
esRef.current = pollEventStream(eventsUrl, {
|
|
731
|
+
onOpen,
|
|
732
|
+
onMessage,
|
|
733
|
+
onError: () => setState((s) => ({ ...s, connected: false })),
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const es = new EventSource(eventsUrl);
|
|
738
|
+
esRef.current = es;
|
|
739
|
+
es.onopen = onOpen;
|
|
740
|
+
es.onerror = () => {
|
|
741
|
+
setState((s) => ({ ...s, connected: false }));
|
|
742
|
+
if (esRef.current !== es) return;
|
|
743
|
+
// 网络断开时浏览器自己会重连,并带上 Last-Event-ID 让服务端从环形缓冲续放
|
|
744
|
+
// (只补漏掉的几条事件)。这时手动重建就等于丢掉续传、每次都拉一份全量快照。
|
|
745
|
+
if (es.readyState !== EventSource.CLOSED) return;
|
|
746
|
+
// 浏览器对非 200 响应会永久放弃自动重连:这里手动兜底,关掉后延迟重建,
|
|
747
|
+
// 重建时会拿到全新快照。
|
|
748
|
+
es.close();
|
|
749
|
+
esRef.current = null;
|
|
750
|
+
const delay = retryDelayMs;
|
|
751
|
+
retryDelayMs = Math.min(retryDelayMs * 2, 30_000);
|
|
752
|
+
retryTimer = setTimeout(() => {
|
|
753
|
+
if (esRef.current === null) connect();
|
|
754
|
+
}, delay);
|
|
755
|
+
};
|
|
756
|
+
es.onmessage = (e) => onMessage(e.data);
|
|
740
757
|
};
|
|
741
758
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
742
759
|
connect();
|
package/src/i18n.tsx
CHANGED
|
@@ -199,7 +199,7 @@ const zh = {
|
|
|
199
199
|
webAuth: "访问认证",
|
|
200
200
|
webAuthTokenOn: "已启用(启动令牌)",
|
|
201
201
|
loginTokenTitle: "这个实例用启动令牌登录",
|
|
202
|
-
loginTokenDesc: "请打开启动 PiWeb 时终端里打印的登录链接(形如 …/?token
|
|
202
|
+
loginTokenDesc: "请打开启动 PiWeb 时终端里打印的登录链接(形如 …/?token=…),或在已登录的电脑上打开 设置 → 远程连接,复制带令牌的地址。令牌每次重启都会更换;打开一次后浏览器会记住 30 天。",
|
|
203
203
|
logout: "退出登录",
|
|
204
204
|
authMethodApiKey: "API 密钥",
|
|
205
205
|
oauthDevicePrompt: "请在浏览器中完成授权",
|
|
@@ -323,6 +323,9 @@ const zh = {
|
|
|
323
323
|
contextCompacted: "Compacted Data",
|
|
324
324
|
contextAutoCompactBuffer: "Auto-Compact Buffer",
|
|
325
325
|
contextFreeSpace: "Free Space",
|
|
326
|
+
contextColdHint: "会话还没启动,System Prompt 和工具定义要启动后才算得出",
|
|
327
|
+
contextCountNow: "现在统计",
|
|
328
|
+
contextCounting: "统计中…",
|
|
326
329
|
// 统计条
|
|
327
330
|
turns: "轮次",
|
|
328
331
|
toolCalls: "工具调用",
|
|
@@ -417,9 +420,10 @@ const zh = {
|
|
|
417
420
|
setRemote: "远程连接",
|
|
418
421
|
remoteBuiltin: "内置插件",
|
|
419
422
|
remoteLanTitle: "局域网入口",
|
|
420
|
-
remoteLanHint: "同一个 WiFi
|
|
423
|
+
remoteLanHint: "同一个 WiFi 下的设备直接打开下面地址即可,手机排版已适配。默认开启。链接带登录令牌:点「复制」发到手机,打开就直接登录;令牌即凭据,别发给别人。",
|
|
421
424
|
remotePublicTitle: "公网入口",
|
|
422
|
-
remotePublicHint: "开启后任何浏览器都能打开,无需安装客户端。地址由 Cloudflare
|
|
425
|
+
remotePublicHint: "开启后任何浏览器都能打开,无需安装客户端。地址由 Cloudflare 随机分配,每次启动都会变。链接同样带登录令牌。",
|
|
426
|
+
remoteLinkCopied: "已复制(含令牌)",
|
|
423
427
|
remoteStart: "开启远程连接",
|
|
424
428
|
remoteStop: "关闭",
|
|
425
429
|
remoteWorking: "启动中…",
|
|
@@ -531,9 +535,9 @@ const zh = {
|
|
|
531
535
|
trustAlways: "总是信任",
|
|
532
536
|
trustNever: "从不信任",
|
|
533
537
|
versionPiweb: "PiWeb 版本",
|
|
534
|
-
versionPiwebDesc: "本 Web
|
|
538
|
+
versionPiwebDesc: "本 Web 应用的版本;启动时自动检查,有新版本在这里直接更新(点版本号打开仓库)",
|
|
535
539
|
versionPi: "Pi 版本",
|
|
536
|
-
versionPiDesc: "pi
|
|
540
|
+
versionPiDesc: "pi 智能体内核的版本;启动时自动检查,有新版本在这里直接更新(点版本号打开仓库)",
|
|
537
541
|
checkUpdate: "检查更新",
|
|
538
542
|
checkingUpdate: "检查中…",
|
|
539
543
|
upToDate: "已是最新",
|
|
@@ -542,6 +546,18 @@ const zh = {
|
|
|
542
546
|
updatedTo: "已更新到 {v},重启 PiWeb 后生效",
|
|
543
547
|
updateFailed: "更新失败",
|
|
544
548
|
updateManualHint: "先停止 piweb,再执行上面的命令(Windows 无法替换运行中的安装目录)",
|
|
549
|
+
updateCopied: "已复制",
|
|
550
|
+
updateBusy: "有会话正在运行,等它结束再更新",
|
|
551
|
+
updateCommits: "main 新增 {n} 个提交",
|
|
552
|
+
updateNoticeTitle: "有新版本",
|
|
553
|
+
updateNoticeUpdatingTitle: "正在更新…",
|
|
554
|
+
updateNoticeDoneTitle: "更新完成",
|
|
555
|
+
updateNoticeUpdate: "更新",
|
|
556
|
+
updateNoticeLater: "稍后",
|
|
557
|
+
updateNoticeOk: "知道了",
|
|
558
|
+
updateNoticeUpdated: "已更新到 {v}",
|
|
559
|
+
updateRunCommandHint: "先停掉 piweb,再在终端运行:",
|
|
560
|
+
updateRestartHint: "重启 PiWeb 后生效:在运行 piweb 的终端按 Ctrl+C,再重新运行 piweb",
|
|
545
561
|
skillsDesc: "关闭后技能不会出现在系统提示中,仅可通过 /skill:名称 手动调用",
|
|
546
562
|
confirmDeleteSkill: "确定删除技能“{name}”吗?将删除其所在技能目录,不可恢复。",
|
|
547
563
|
deleteSkillHint: "删除技能目录",
|
|
@@ -652,6 +668,26 @@ const zh = {
|
|
|
652
668
|
viewerNotInSnapshot: "此文件不在快照里(超过 1MB 或被排除),显示磁盘上的当前内容",
|
|
653
669
|
viewerLoadFailed: "加载失败",
|
|
654
670
|
viewerEmpty: "从左侧树里选择一个文件",
|
|
671
|
+
// 流式输出
|
|
672
|
+
streamStyleTitle: "流式输出样式",
|
|
673
|
+
streamStyleDesc: "回答逐字出现的方式;改完立刻生效,正在流出的回答也一并换",
|
|
674
|
+
streamStyleSmooth: "平滑",
|
|
675
|
+
streamStyleSmoothDesc: "匀速长出,不跳字",
|
|
676
|
+
streamStyleTypewriter: "打字机",
|
|
677
|
+
streamStyleTypewriterDesc: "固定节奏逐字敲出",
|
|
678
|
+
streamStyleFade: "渐显",
|
|
679
|
+
streamStyleFadeDesc: "整词推进,段落柔和淡入",
|
|
680
|
+
streamStyleInstant: "即时",
|
|
681
|
+
streamStyleInstantDesc: "立即显示,不做平滑",
|
|
682
|
+
streamSpeedTitle: "推进速度",
|
|
683
|
+
streamSpeedDesc: "平滑 / 打字机 / 渐显的节奏快慢;即时样式不受影响",
|
|
684
|
+
streamSpeed1: "慢",
|
|
685
|
+
streamSpeed2: "较慢",
|
|
686
|
+
streamSpeed3: "标准",
|
|
687
|
+
streamSpeed4: "快",
|
|
688
|
+
streamSpeed5: "极快",
|
|
689
|
+
streamCaretTitle: "打字光标",
|
|
690
|
+
streamCaretDesc: "流式期间在文字末尾显示闪烁光标",
|
|
655
691
|
} as const;
|
|
656
692
|
|
|
657
693
|
type Dict = { [K in keyof typeof zh]: string };
|
|
@@ -848,7 +884,7 @@ const en: Dict = {
|
|
|
848
884
|
webAuth: "Access authentication",
|
|
849
885
|
webAuthTokenOn: "Enabled (launch token)",
|
|
850
886
|
loginTokenTitle: "This instance signs in with a launch token",
|
|
851
|
-
loginTokenDesc: "Open the login link printed in the terminal when PiWeb started (…/?token=…). The token changes on every restart; once opened, the browser remembers it for 30 days.",
|
|
887
|
+
loginTokenDesc: "Open the login link printed in the terminal when PiWeb started (…/?token=…), or copy a link from Settings → Remote access on a signed-in computer. The token changes on every restart; once opened, the browser remembers it for 30 days.",
|
|
852
888
|
logout: "Sign out",
|
|
853
889
|
authMethodApiKey: "API key",
|
|
854
890
|
oauthDevicePrompt: "Finish authorization in your browser",
|
|
@@ -970,6 +1006,9 @@ const en: Dict = {
|
|
|
970
1006
|
contextCompacted: "Compacted Data",
|
|
971
1007
|
contextAutoCompactBuffer: "Auto-Compact Buffer",
|
|
972
1008
|
contextFreeSpace: "Free Space",
|
|
1009
|
+
contextColdHint: "Session not started yet — System Prompt and tool definitions are counted once it starts",
|
|
1010
|
+
contextCountNow: "Count now",
|
|
1011
|
+
contextCounting: "Counting…",
|
|
973
1012
|
turns: "turns",
|
|
974
1013
|
toolCalls: "tool calls",
|
|
975
1014
|
cost: "cost",
|
|
@@ -1059,9 +1098,10 @@ const en: Dict = {
|
|
|
1059
1098
|
setRemote: "Remote access",
|
|
1060
1099
|
remoteBuiltin: "Built-in plugin",
|
|
1061
1100
|
remoteLanTitle: "Local network",
|
|
1062
|
-
remoteLanHint: "Devices on the same Wi-Fi can open the address below; the mobile layout is already applied. On by default.",
|
|
1101
|
+
remoteLanHint: "Devices on the same Wi-Fi can open the address below; the mobile layout is already applied. On by default. Links carry the login token: copy one to your phone and it signs in directly. The token is a credential — don't share it.",
|
|
1063
1102
|
remotePublicTitle: "Public access",
|
|
1064
|
-
remotePublicHint: "Once on, any browser can open it with nothing to install. Cloudflare assigns a random address that changes on every start.",
|
|
1103
|
+
remotePublicHint: "Once on, any browser can open it with nothing to install. Cloudflare assigns a random address that changes on every start. The link carries the login token too.",
|
|
1104
|
+
remoteLinkCopied: "Copied (with token)",
|
|
1065
1105
|
remoteStart: "Turn on",
|
|
1066
1106
|
remoteStop: "Turn off",
|
|
1067
1107
|
remoteWorking: "Starting…",
|
|
@@ -1173,9 +1213,9 @@ const en: Dict = {
|
|
|
1173
1213
|
trustAlways: "Always trust",
|
|
1174
1214
|
trustNever: "Never trust",
|
|
1175
1215
|
versionPiweb: "PiWeb version",
|
|
1176
|
-
versionPiwebDesc: "Version of this web app; click to open the repo",
|
|
1216
|
+
versionPiwebDesc: "Version of this web app; checked at startup and updatable right here (click the version to open the repo)",
|
|
1177
1217
|
versionPi: "Pi version",
|
|
1178
|
-
versionPiDesc: "Version of the pi agent engine; click to open the repo",
|
|
1218
|
+
versionPiDesc: "Version of the pi agent engine; checked at startup and updatable right here (click the version to open the repo)",
|
|
1179
1219
|
checkUpdate: "Check for updates",
|
|
1180
1220
|
checkingUpdate: "Checking…",
|
|
1181
1221
|
upToDate: "Up to date",
|
|
@@ -1184,6 +1224,18 @@ const en: Dict = {
|
|
|
1184
1224
|
updatedTo: "Updated to {v}; restart PiWeb to apply",
|
|
1185
1225
|
updateFailed: "Update failed",
|
|
1186
1226
|
updateManualHint: "Stop piweb, then run the command above (Windows cannot replace a running installation)",
|
|
1227
|
+
updateCopied: "Copied",
|
|
1228
|
+
updateBusy: "A session is running; update after it finishes",
|
|
1229
|
+
updateCommits: "{n} new commits on main",
|
|
1230
|
+
updateNoticeTitle: "Update available",
|
|
1231
|
+
updateNoticeUpdatingTitle: "Updating…",
|
|
1232
|
+
updateNoticeDoneTitle: "Update installed",
|
|
1233
|
+
updateNoticeUpdate: "Update",
|
|
1234
|
+
updateNoticeLater: "Later",
|
|
1235
|
+
updateNoticeOk: "Got it",
|
|
1236
|
+
updateNoticeUpdated: "Updated to {v}",
|
|
1237
|
+
updateRunCommandHint: "Stop piweb, then run in a terminal:",
|
|
1238
|
+
updateRestartHint: "Restart PiWeb to apply: press Ctrl+C in the terminal running piweb, then run piweb again",
|
|
1187
1239
|
skillsDesc: "Disabled skills are hidden from the system prompt and only callable via /skill:name",
|
|
1188
1240
|
confirmDeleteSkill: "Delete skill \"{name}\"? Its skill directory will be removed and cannot be recovered.",
|
|
1189
1241
|
deleteSkillHint: "Delete the skill directory",
|
|
@@ -1293,6 +1345,26 @@ const en: Dict = {
|
|
|
1293
1345
|
viewerNotInSnapshot: "Not in the snapshot (over 1MB or excluded); showing the current content from disk",
|
|
1294
1346
|
viewerLoadFailed: "Failed to load",
|
|
1295
1347
|
viewerEmpty: "Pick a file from the tree on the left",
|
|
1348
|
+
// Streaming text
|
|
1349
|
+
streamStyleTitle: "Streaming text",
|
|
1350
|
+
streamStyleDesc: "How the answer appears while it arrives; applies to the stream already running",
|
|
1351
|
+
streamStyleSmooth: "Smooth",
|
|
1352
|
+
streamStyleSmoothDesc: "Even pace, no jumps",
|
|
1353
|
+
streamStyleTypewriter: "Typewriter",
|
|
1354
|
+
streamStyleTypewriterDesc: "Fixed cadence, character by character",
|
|
1355
|
+
streamStyleFade: "Fade in",
|
|
1356
|
+
streamStyleFadeDesc: "Whole words, paragraphs fade in",
|
|
1357
|
+
streamStyleInstant: "Instant",
|
|
1358
|
+
streamStyleInstantDesc: "Show immediately, no smoothing",
|
|
1359
|
+
streamSpeedTitle: "Streaming speed",
|
|
1360
|
+
streamSpeedDesc: "Cadence for Smooth / Typewriter / Fade in; Instant ignores it",
|
|
1361
|
+
streamSpeed1: "Slow",
|
|
1362
|
+
streamSpeed2: "Relaxed",
|
|
1363
|
+
streamSpeed3: "Standard",
|
|
1364
|
+
streamSpeed4: "Fast",
|
|
1365
|
+
streamSpeed5: "Fastest",
|
|
1366
|
+
streamCaretTitle: "Typing caret",
|
|
1367
|
+
streamCaretDesc: "Blinking caret at the end of the text while streaming",
|
|
1296
1368
|
};
|
|
1297
1369
|
|
|
1298
1370
|
const dicts: Record<string, Dict> = { zh, en };
|
|
@@ -12,6 +12,9 @@ export async function registerNode() {
|
|
|
12
12
|
const { authEnabled } = await import("./lib/web-auth");
|
|
13
13
|
if (authEnabled()) (await import("./lib/web-launch-token")).publishLaunchToken();
|
|
14
14
|
|
|
15
|
+
// 每次启动检查一次 PiWeb / pi 有没有新版本:不等结果,查到了在终端打一行,页面右下角也会提醒
|
|
16
|
+
void (await import("./lib/update/startup-check")).ensureStartupCheck().catch(() => {});
|
|
17
|
+
|
|
15
18
|
const { remoteBlocked, remoteLanEnabled, startLan } = await import("./lib/remote/supervisor");
|
|
16
19
|
if (!remoteLanEnabled()) return;
|
|
17
20
|
|
package/src/lib/agent-manager.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { formatSkillsForPrompt, generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import {
|
|
11
11
|
getResourceLoader,
|
|
12
12
|
createSessionResourceLoader,
|
|
@@ -63,6 +63,10 @@ interface Managed {
|
|
|
63
63
|
session: AgentSession | null;
|
|
64
64
|
creating: Promise<AgentSession> | null;
|
|
65
65
|
subscribers: Set<(seq: number, json: string) => void>;
|
|
66
|
+
/** 长轮询的浏览器在两次请求之间没有订阅,到这个时间点之前仍算有人在看(见 holdViewer) */
|
|
67
|
+
viewerGraceUntil?: number;
|
|
68
|
+
/** 本轮已排队推送上下文分项(publishContextBreakdown 合并同一轮的多次触发) */
|
|
69
|
+
breakdownQueued?: boolean;
|
|
66
70
|
buffer: { seq: number; json: string }[];
|
|
67
71
|
seq: number;
|
|
68
72
|
/**
|
|
@@ -579,6 +583,7 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
579
583
|
ts: now,
|
|
580
584
|
});
|
|
581
585
|
publishUsage(m);
|
|
586
|
+
publishContextBreakdown(m);
|
|
582
587
|
if ((evt as any).reason === "manual") {
|
|
583
588
|
publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: now });
|
|
584
589
|
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
@@ -733,45 +738,46 @@ function loadContextResources(cwd: string, loader: AgentSession["resourceLoader"
|
|
|
733
738
|
];
|
|
734
739
|
}
|
|
735
740
|
|
|
741
|
+
const SKILLS_SECTION_START = "The following skills provide specialized instructions";
|
|
742
|
+
const SKILLS_SECTION_END = "</available_skills>";
|
|
743
|
+
|
|
736
744
|
/**
|
|
737
745
|
* 上下文占用的服务端估算分项。systemPrompt 是整段拼接(正文 + memory + skills + 工具列表),
|
|
738
746
|
* 所以 Memory / Skills 从资源加载器与 systemPrompt 的资源段差分得出:
|
|
739
747
|
* SDK 的拼接顺序固定为 正文 → context files → skills → 工具列表,按内容定位切块。
|
|
748
|
+
*
|
|
749
|
+
* 冷会话(只浏览历史、不冷启动 agent)拿不到组装后的 system prompt 和工具定义,这三项为 0、标 cold;
|
|
750
|
+
* Memory / Skills / Compacted / Buffer 不依赖 agent,照常从发现缓存、会话文件和设置里算,
|
|
751
|
+
* 不能因为没启动就整块归零(那样弹窗只剩消息几类)。
|
|
740
752
|
*/
|
|
741
|
-
async function buildContextBreakdown(
|
|
742
|
-
|
|
743
|
-
session: AgentSession | null,
|
|
744
|
-
resources: ContextResource[],
|
|
745
|
-
): Promise<ContextBreakdown> {
|
|
753
|
+
async function buildContextBreakdown(m: Managed, resources: ContextResource[]): Promise<ContextBreakdown> {
|
|
754
|
+
const session = m.session;
|
|
746
755
|
const breakdown: ContextBreakdown = {
|
|
747
756
|
systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0, autoCompactBuffer: 0,
|
|
757
|
+
...(session ? {} : { cold: true }),
|
|
748
758
|
};
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
|
|
754
|
-
breakdown.memory = estimateTokensOf(memoryText);
|
|
755
|
-
// Skills:从资源加载器取技能清单,按 SDK 的 formatSkillsForPrompt 段落定位。
|
|
756
|
-
// 该段落没有 Markdown 标题(原文以 "The following skills provide specialized instructions…" 开头,
|
|
757
|
-
// 结尾是 </available_skills>),以前按 "## Skills" 找永远找不到,技能段一直是 0。
|
|
759
|
+
// Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
|
|
760
|
+
const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
|
|
761
|
+
breakdown.memory = estimateTokensOf(memoryText);
|
|
762
|
+
if (session) {
|
|
758
763
|
try {
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
+
const prompt = session.systemPrompt ?? "";
|
|
765
|
+
// Skills:按 SDK 的 formatSkillsForPrompt 段落定位。该段落没有 Markdown 标题(以
|
|
766
|
+
// "The following skills provide specialized instructions…" 开头、</available_skills> 结尾),
|
|
767
|
+
// 以前按 "## Skills" 找永远找不到,技能段一直是 0。
|
|
768
|
+
try {
|
|
769
|
+
if (session.resourceLoader.getSkills().skills.length && prompt) {
|
|
770
|
+
const start = prompt.indexOf(SKILLS_SECTION_START);
|
|
771
|
+
const end = prompt.indexOf(SKILLS_SECTION_END);
|
|
772
|
+
if (start >= 0 && end > start) breakdown.skills = estimateTokensOf(prompt.slice(start, end + SKILLS_SECTION_END.length));
|
|
773
|
+
}
|
|
774
|
+
} catch {
|
|
775
|
+
/* loader 不可用则跳过 */
|
|
764
776
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const estimatedSections = breakdown.memory + breakdown.skills;
|
|
770
|
-
breakdown.systemPrompt = Math.max(0, estimateTokensOf(prompt) - estimatedSections);
|
|
771
|
-
// 工具:builtin 与扩展注册的分开统计(schema + description + guidelines 序列化估算)
|
|
772
|
-
try {
|
|
773
|
-
const tools = session.getAllTools();
|
|
774
|
-
for (const tool of tools) {
|
|
777
|
+
// System Prompt 正文 = 全文减去资源段与 skills 段的近似差分,下限 0
|
|
778
|
+
breakdown.systemPrompt = Math.max(0, estimateTokensOf(prompt) - breakdown.memory - breakdown.skills);
|
|
779
|
+
// 工具:builtin 与扩展注册的分开统计(schema + description + guidelines 序列化估算)
|
|
780
|
+
for (const tool of session.getAllTools()) {
|
|
775
781
|
const size = estimateTokensOf(JSON.stringify({
|
|
776
782
|
name: tool.name,
|
|
777
783
|
description: tool.description,
|
|
@@ -782,38 +788,61 @@ async function buildContextBreakdown(
|
|
|
782
788
|
else breakdown.customTools += size;
|
|
783
789
|
}
|
|
784
790
|
} catch {
|
|
785
|
-
/* getAllTools
|
|
791
|
+
/* systemPrompt / getAllTools 不可用则保持 0 */
|
|
786
792
|
}
|
|
787
|
-
|
|
793
|
+
} else {
|
|
794
|
+
// 冷会话:技能段用发现缓存里的清单按 SDK 同一个格式化函数估算(启动后改用实际 prompt 里的那段)
|
|
788
795
|
try {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
const entry = entries[i] as { type?: string; summary?: string };
|
|
792
|
-
if (entry?.type === "compaction" && typeof entry.summary === "string") {
|
|
793
|
-
breakdown.compacted = estimateTokensOf(entry.summary);
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
796
|
+
const loader = peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
|
|
797
|
+
breakdown.skills = estimateTokensOf(formatSkillsForPrompt(loader.getSkills().skills).trim());
|
|
797
798
|
} catch {
|
|
798
|
-
/*
|
|
799
|
+
/* 发现缓存还没好则跳过 */
|
|
799
800
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
801
|
+
}
|
|
802
|
+
// 最新一次压缩的摘要(会话文件里就有,冷热都能算)
|
|
803
|
+
try {
|
|
804
|
+
const entries = m.sm.getEntries() as unknown as { type?: string; summary?: string }[];
|
|
805
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
806
|
+
const entry = entries[i];
|
|
807
|
+
if (entry?.type === "compaction" && typeof entry.summary === "string") {
|
|
808
|
+
breakdown.compacted = estimateTokensOf(entry.summary);
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
806
811
|
}
|
|
807
812
|
} catch {
|
|
808
|
-
/*
|
|
813
|
+
/* entries 不可用则跳过 */
|
|
814
|
+
}
|
|
815
|
+
// auto-compact 预留 = compaction.reserveTokens(启用时)
|
|
816
|
+
try {
|
|
817
|
+
const settings = await getPiSettings();
|
|
818
|
+
if (settings.compaction.enabled) breakdown.autoCompactBuffer = settings.compaction.reserveTokens;
|
|
819
|
+
} catch {
|
|
820
|
+
/* 设置读取失败则保持 0 */
|
|
809
821
|
}
|
|
810
822
|
return breakdown;
|
|
811
823
|
}
|
|
812
824
|
|
|
813
|
-
/**
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
825
|
+
/**
|
|
826
|
+
* 分项变了就推一次(会话启动、工具 / 资源重载、压缩之后)。分项以前只随快照下发:
|
|
827
|
+
* 打开冷会话时是 0,之后会话启动了弹窗也一直停在那份 0 上,直到刷新页面。
|
|
828
|
+
* 同一轮里多次触发(启动时 publishTools + publishResources)合并成一次。
|
|
829
|
+
*/
|
|
830
|
+
function publishContextBreakdown(m: Managed): void {
|
|
831
|
+
if (m.breakdownQueued || m.disposed) return;
|
|
832
|
+
m.breakdownQueued = true;
|
|
833
|
+
queueMicrotask(() => {
|
|
834
|
+
m.breakdownQueued = false;
|
|
835
|
+
if (m.disposed) return;
|
|
836
|
+
let resources: ContextResource[] = [];
|
|
837
|
+
try {
|
|
838
|
+
resources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
839
|
+
} catch {
|
|
840
|
+
/* 读不到注入文件就按空算 Memory */
|
|
841
|
+
}
|
|
842
|
+
void buildContextBreakdown(m, resources).then((breakdown) => {
|
|
843
|
+
if (!m.disposed) publish(m, { type: "context_breakdown", breakdown, ts: Date.now() });
|
|
844
|
+
}, () => undefined);
|
|
845
|
+
});
|
|
817
846
|
}
|
|
818
847
|
|
|
819
848
|
/** resources 已经算过(快照)就传进来,免得资源发现结果再读一遍 */
|
|
@@ -877,7 +906,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
877
906
|
installSessionPolicy(m, session);
|
|
878
907
|
applyToolPolicy(m, session);
|
|
879
908
|
session.subscribe((evt) => { if (!m.disposed) translate(m, evt); });
|
|
880
|
-
const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && m
|
|
909
|
+
const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && hasViewers(m) });
|
|
881
910
|
m.ui = ui;
|
|
882
911
|
await session.bindExtensions({
|
|
883
912
|
mode: "rpc",
|
|
@@ -989,6 +1018,7 @@ function shutdownSession(session: AgentSession): Promise<void> {
|
|
|
989
1018
|
|
|
990
1019
|
function publishTools(m: Managed, session: AgentSession): void {
|
|
991
1020
|
publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
|
|
1021
|
+
publishContextBreakdown(m);
|
|
992
1022
|
}
|
|
993
1023
|
|
|
994
1024
|
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
@@ -1086,13 +1116,13 @@ export async function disposeSessionPath(sessionPath: string): Promise<void> {
|
|
|
1086
1116
|
export function reap(): void {
|
|
1087
1117
|
const now = Date.now();
|
|
1088
1118
|
for (const m of [...sessions.values()]) {
|
|
1089
|
-
if (m
|
|
1119
|
+
if (hasViewers(m) || now - m.lastActive <= IDLE_MS) continue;
|
|
1090
1120
|
if (!m.creating && !isManagedBusy(m)) disposeSession(m);
|
|
1091
1121
|
}
|
|
1092
1122
|
const active = [...sessions.values()].filter((m) => m.session);
|
|
1093
1123
|
if (active.length > MAX_ACTIVE) {
|
|
1094
1124
|
const evictable = active
|
|
1095
|
-
.filter((m) => !m.creating && !isManagedBusy(m) && m
|
|
1125
|
+
.filter((m) => !m.creating && !isManagedBusy(m) && !hasViewers(m))
|
|
1096
1126
|
.sort((a, b) => a.lastActive - b.lastActive);
|
|
1097
1127
|
for (const m of evictable.slice(0, active.length - MAX_ACTIVE)) disposeSession(m);
|
|
1098
1128
|
}
|
|
@@ -1166,6 +1196,7 @@ function collectResources(m: Managed): ResourceBundle {
|
|
|
1166
1196
|
|
|
1167
1197
|
export function publishResources(m: Managed): void {
|
|
1168
1198
|
publish(m, { type: "resources", ...collectResources(m), ts: Date.now() });
|
|
1199
|
+
publishContextBreakdown(m);
|
|
1169
1200
|
}
|
|
1170
1201
|
|
|
1171
1202
|
/**
|
|
@@ -1198,7 +1229,7 @@ export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
|
|
|
1198
1229
|
if (cwd && key(m.cwd) !== key(cwd)) continue;
|
|
1199
1230
|
if (!m.session) {
|
|
1200
1231
|
// 冷会话没有扩展运行时可重建,但页面可能开着:把新清单(模板 / 技能 / 信任)推过去
|
|
1201
|
-
if (m
|
|
1232
|
+
if (hasViewers(m)) publishResources(m);
|
|
1202
1233
|
continue;
|
|
1203
1234
|
}
|
|
1204
1235
|
if (m.creating || isManagedBusy(m)) {
|
|
@@ -1359,7 +1390,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1359
1390
|
// 服务端可得的上下文分项(估算;失败不影响快照)
|
|
1360
1391
|
let contextBreakdown: ContextBreakdown | undefined;
|
|
1361
1392
|
try {
|
|
1362
|
-
contextBreakdown = await buildContextBreakdown(m
|
|
1393
|
+
contextBreakdown = await buildContextBreakdown(m, contextResources);
|
|
1363
1394
|
} catch {
|
|
1364
1395
|
contextBreakdown = undefined;
|
|
1365
1396
|
}
|
|
@@ -1446,6 +1477,18 @@ export function unsubscribe(m: Managed, send: (seq: number, json: string) => voi
|
|
|
1446
1477
|
m.subscribers.delete(send);
|
|
1447
1478
|
}
|
|
1448
1479
|
|
|
1480
|
+
/**
|
|
1481
|
+
* 长轮询(快速隧道)的一次请求结束时调用:下一次请求到来前的这段空档仍算有人在看,
|
|
1482
|
+
* 否则扩展弹窗会被当成无人值守直接取默认值,会话也可能被当成没人开着回收掉。
|
|
1483
|
+
*/
|
|
1484
|
+
export function holdViewer(m: Managed, ms: number): void {
|
|
1485
|
+
m.viewerGraceUntil = Math.max(m.viewerGraceUntil ?? 0, Date.now() + ms);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function hasViewers(m: Managed): boolean {
|
|
1489
|
+
return m.subscribers.size > 0 || (m.viewerGraceUntil ?? 0) > Date.now();
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1449
1492
|
// ---------- 命令执行 ----------
|
|
1450
1493
|
|
|
1451
1494
|
export interface CommandResult {
|