@rexvane/piweb 0.3.17 → 0.3.19
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 +6 -4
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +23 -9
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +12 -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 +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/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -0
- package/.next/server/app/api/fs/route.js.nft.json +1 -0
- package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/git/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/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/pi-settings/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 +8 -0
- package/.next/server/app/api/remote/route.js.nft.json +1 -0
- package/.next/server/app/api/remote/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/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/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 +1 -1
- 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/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +3 -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.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -4
- package/.next/server/chunks/229.js +1 -1
- package/.next/server/chunks/243.js +1 -0
- package/.next/server/chunks/393.js +1 -0
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -0
- package/.next/server/chunks/686.js +1 -4
- package/.next/server/chunks/802.js +2 -0
- package/.next/server/chunks/807.js +1 -0
- package/.next/server/chunks/920.js +4 -0
- package/.next/server/chunks/924.js +6 -0
- package/.next/server/instrumentation.js +1 -0
- package/.next/server/instrumentation.js.nft.json +1 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +4 -3
- 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/JDGSR-TIbk9qZWcUNYIX3/_buildManifest.js +1 -0
- package/.next/static/chunks/2716.7ad073038e94a802.js +1 -0
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +1 -0
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +1 -0
- package/.next/static/chunks/4686-e3825f3b6a384aa1.js +1 -0
- package/.next/static/chunks/4778.e4c9ed093a413bc4.js +1 -0
- package/.next/static/chunks/5173.91ddb803785009f8.js +1 -0
- package/.next/static/chunks/839.7403c6bdace4c56c.js +1 -0
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +1 -0
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/login/page-5e276a1eeb8867df.js +1 -0
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/webpack-bd2b9202ecbcdcaa.js +1 -0
- package/.next/static/css/7391c42e1ae617f9.css +1 -0
- package/.next/types/app/api/fs/route.ts +353 -0
- package/.next/types/app/api/remote/route.ts +353 -0
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +18 -0
- package/README.md +56 -10
- package/next.config.ts +3 -2
- package/package.json +3 -3
- package/scripts/launcher.mjs +194 -13
- package/scripts/release.mjs +0 -2
- package/src/app/api/agent/[id]/events/route.ts +11 -6
- package/src/app/api/fs/route.ts +119 -0
- package/src/app/api/growth/route.ts +11 -6
- package/src/app/api/models/route.ts +21 -9
- package/src/app/api/remote/route.ts +67 -0
- package/src/app/api/sessions/[id]/export/route.ts +11 -1
- package/src/app/api/sessions/route.ts +3 -1
- package/src/app/api/web-auth/route.ts +45 -49
- package/src/app/api/workspaces/route.ts +16 -0
- package/src/app/globals.css +11 -3
- package/src/app/login/page.tsx +8 -60
- package/src/components/AppShell.tsx +202 -155
- package/src/components/ChatInput.tsx +61 -6
- package/src/components/ChatWindow.tsx +274 -132
- package/src/components/DirectoryPicker.tsx +189 -0
- package/src/components/DrawerPanel.tsx +164 -0
- package/src/components/FileViewer.tsx +2 -2
- package/src/components/GrowthTree.tsx +6 -5
- package/src/components/MarkdownBlock.tsx +134 -0
- package/src/components/OutlineRail.tsx +22 -1
- package/src/components/ProjectPanel.tsx +197 -14
- package/src/components/RemoteSection.tsx +232 -0
- package/src/components/SessionImportSection.tsx +60 -38
- package/src/components/SettingsPanel.tsx +104 -57
- package/src/components/icons.tsx +23 -0
- package/src/hooks/useGrowth.ts +47 -25
- package/src/hooks/usePiWeb.ts +100 -14
- package/src/i18n.tsx +115 -28
- package/src/instrumentation-node.ts +35 -0
- package/src/instrumentation.ts +18 -0
- package/src/lib/agent-config.ts +134 -0
- package/src/lib/agent-manager.ts +243 -61
- package/src/lib/auth-throttle.ts +102 -0
- package/src/lib/auth.ts +35 -27
- package/src/lib/files-service.ts +61 -1
- package/src/lib/font.ts +84 -0
- package/src/lib/git-service.ts +5 -3
- package/src/lib/growth-service.ts +78 -30
- package/src/lib/growth-tracker.ts +91 -9
- package/src/lib/models-service.ts +36 -11
- package/src/lib/pi-settings.ts +10 -12
- package/src/lib/pi.ts +145 -18
- package/src/lib/plugins-service.ts +67 -35
- package/src/lib/remote/assets.json +4 -0
- package/src/lib/remote/mobile-proxy.ts +149 -0
- package/src/lib/remote/supervisor.ts +482 -0
- package/src/lib/security-service.ts +6 -11
- package/src/lib/session-import/claude.ts +45 -21
- package/src/lib/session-reader.ts +106 -3
- package/src/lib/skills-service.ts +34 -12
- package/src/lib/text-sanitize.ts +12 -0
- package/src/lib/theme.ts +3 -0
- package/src/lib/types.ts +16 -2
- package/src/lib/update-service.ts +0 -1
- package/src/lib/web-auth-shared.ts +5 -1
- package/src/lib/web-auth.ts +139 -22
- package/src/lib/web-launch-token.ts +55 -0
- package/src/lib/web-secret.ts +182 -0
- package/src/lib/workspace-store.ts +91 -7
- package/src/proxy.ts +46 -12
- package/.next/server/chunks/532.js +0 -1
- package/.next/static/chunks/2155.2f27fe7098987699.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/2716.93be8ad5c970c516.js +0 -1
- package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
- package/.next/static/chunks/4673.c1fb1580efc6ace7.js +0 -1
- package/.next/static/chunks/4686-f4a037d609cea3f2.js +0 -1
- package/.next/static/chunks/692-7992db0ae9cc3021.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +0 -1
- package/.next/static/chunks/app/page-b51087831582ee37.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/webpack-c31263fd83d75ce6.js +0 -1
- package/.next/static/css/850c2fd130ffa2fc.css +0 -1
- package/.next/static/zsynuO_9Kx6nrXgkTUJ04/_buildManifest.js +0 -1
- /package/.next/static/{zsynuO_9Kx6nrXgkTUJ04 → JDGSR-TIbk9qZWcUNYIX3}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-config:`~/.pi/agent` 下被反复读取的 JSON 配置(settings.json / auth.json / 项目 settings.json)
|
|
3
|
+
* 的统一读盘缓存。
|
|
4
|
+
*
|
|
5
|
+
* 这些文件在启动、首屏、每个会话快照里都会被读很多次:
|
|
6
|
+
* - settings.json 单次 read+parse 约 0.3ms,但 `resolveProjectTrust` 走的是**同步**读,出现在
|
|
7
|
+
* 每次会话创建、每次资源指纹、每次加载器发现里;
|
|
8
|
+
* - auth.json 在每次模型目录刷新(设置面板/模型选择器)时读一次;
|
|
9
|
+
* - 项目 `.pi/settings.json` 在插件面板里读。
|
|
10
|
+
*
|
|
11
|
+
* 缓存按 (mtime, ctime, size, ino) 校验:文件没变就直接返回内存里的解析结果,文件换了
|
|
12
|
+
* (pi 自己写、或 PiWeb 的写路径)立刻回源。调用方拿到的是副本,可以随意修改;
|
|
13
|
+
* 写路径改完文件后应当调用 `invalidateJsonFile()`——同一毫秒内的等长写入不会改变校验戳。
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, statSync } from "node:fs";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export type JsonObject = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
interface Entry {
|
|
22
|
+
stamp: string;
|
|
23
|
+
value?: JsonObject;
|
|
24
|
+
error?: Error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cache = new Map<string, Entry>();
|
|
28
|
+
|
|
29
|
+
/** 校验戳:mtime + ctime + 大小 + inode,任一变化都回源 */
|
|
30
|
+
function stampOf(stat: { mtimeMs: number; ctimeMs: number; size: number; ino: number }): string {
|
|
31
|
+
return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${stat.ino}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseJsonObject(text: string, file: string): JsonObject {
|
|
35
|
+
const parsed: unknown = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${file} must contain a JSON object`);
|
|
37
|
+
return parsed as JsonObject;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copyOf(value: JsonObject | undefined): JsonObject {
|
|
41
|
+
return value ? structuredClone(value) : {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readEntry(file: string): Promise<Entry> {
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = await fs.stat(file);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
50
|
+
const missing: Entry = { stamp: "missing", value: {} };
|
|
51
|
+
cache.set(file, missing);
|
|
52
|
+
return missing;
|
|
53
|
+
}
|
|
54
|
+
const stamp = stampOf(stat);
|
|
55
|
+
const cached = cache.get(file);
|
|
56
|
+
if (cached && cached.stamp === stamp) return cached;
|
|
57
|
+
const entry: Entry = { stamp };
|
|
58
|
+
try {
|
|
59
|
+
entry.value = parseJsonObject(await fs.readFile(file, "utf8"), file);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
entry.error = error as Error;
|
|
62
|
+
}
|
|
63
|
+
cache.set(file, entry);
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 严格读:文件不存在当 `{}`,内容非法抛错(与 pi 自己读 settings.json 的口径一致)。返回副本。 */
|
|
68
|
+
export async function readJsonObject(file: string): Promise<JsonObject> {
|
|
69
|
+
const entry = await readEntry(file);
|
|
70
|
+
if (entry.error) throw entry.error;
|
|
71
|
+
return copyOf(entry.value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 读-改-写的临界区里用:绕过校验戳直接读盘。别的写入者(pi 的 SettingsManager、pi CLI)不会作废这里的缓存,
|
|
76
|
+
* 同一时刻落盘的等长内容戳不变,拿缓存去合并会把对方刚写的字段盖回去。返回副本。
|
|
77
|
+
*/
|
|
78
|
+
export async function readJsonObjectFresh(file: string): Promise<JsonObject> {
|
|
79
|
+
cache.delete(file);
|
|
80
|
+
return readJsonObject(file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 宽松读:读不到、解析失败都退化成 `{}`("没配就当默认"的场景)。返回副本。 */
|
|
84
|
+
export async function readJsonObjectSafe(file: string): Promise<JsonObject> {
|
|
85
|
+
try {
|
|
86
|
+
return await readJsonObject(file);
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 同步宽松读:给 `resolveProjectTrust` 这类同步路径用,任何失败都返回 `{}`。返回副本。 */
|
|
93
|
+
export function readJsonObjectSync(file: string): JsonObject {
|
|
94
|
+
try {
|
|
95
|
+
const stamp = stampOf(statSync(file));
|
|
96
|
+
const cached = cache.get(file);
|
|
97
|
+
let entry: Entry;
|
|
98
|
+
if (cached && cached.stamp === stamp) {
|
|
99
|
+
entry = cached;
|
|
100
|
+
} else {
|
|
101
|
+
entry = { stamp };
|
|
102
|
+
try {
|
|
103
|
+
entry.value = parseJsonObject(readFileSync(file, "utf8"), file);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
entry.error = error as Error;
|
|
106
|
+
}
|
|
107
|
+
cache.set(file, entry);
|
|
108
|
+
}
|
|
109
|
+
if (entry.error) throw entry.error;
|
|
110
|
+
return copyOf(entry.value);
|
|
111
|
+
} catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 写路径改完文件后调用:同一毫秒内的等长写入不会改变校验戳,缓存必须被显式作废。 */
|
|
117
|
+
export function invalidateJsonFile(file: string): void {
|
|
118
|
+
cache.delete(file);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 按文件名作废(不需要先解析出 agent 目录)。
|
|
123
|
+
* 凭证登录/登出这类路径只知道"auth.json 变了",用它避免为了一个文件名去碰用户目录。
|
|
124
|
+
*/
|
|
125
|
+
export function invalidateJsonFilesNamed(basename: string): void {
|
|
126
|
+
for (const key of [...cache.keys()]) {
|
|
127
|
+
if (path.basename(key) === basename) cache.delete(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 测试与"整目录切换"场景用 */
|
|
132
|
+
export function invalidateAllJsonFiles(): void {
|
|
133
|
+
cache.clear();
|
|
134
|
+
}
|
package/src/lib/agent-manager.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resourceLoaderReady,
|
|
22
22
|
withResourceLock,
|
|
23
23
|
TOOL_PRESETS,
|
|
24
|
+
presetAllowsTool,
|
|
24
25
|
SessionManager,
|
|
25
26
|
createAgentSession,
|
|
26
27
|
getModelRuntime,
|
|
@@ -35,6 +36,7 @@ import { sanitizeToolOutput } from "./text-sanitize";
|
|
|
35
36
|
import { BoundaryError } from "./path-security";
|
|
36
37
|
import { estimateTokensOf } from "./process-format";
|
|
37
38
|
import { getPiSettings } from "./pi-settings";
|
|
39
|
+
import { getGrowthInitConsent, setGrowthInitConsent } from "./workspace-store";
|
|
38
40
|
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
39
41
|
import type {
|
|
40
42
|
AgentCommand,
|
|
@@ -52,6 +54,7 @@ import type {
|
|
|
52
54
|
const IDLE_MS = 10 * 60 * 1000;
|
|
53
55
|
const MAX_ACTIVE = 6;
|
|
54
56
|
const BUFFER_CAP = 400;
|
|
57
|
+
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
55
58
|
|
|
56
59
|
interface Managed {
|
|
57
60
|
sessionPath: string;
|
|
@@ -62,11 +65,22 @@ interface Managed {
|
|
|
62
65
|
subscribers: Set<(seq: number, json: string) => void>;
|
|
63
66
|
buffer: { seq: number; json: string }[];
|
|
64
67
|
seq: number;
|
|
68
|
+
/**
|
|
69
|
+
* 这一份 Managed 的身份,拼进 SSE 事件 id(`<epoch>.<seq>`)。
|
|
70
|
+
* seq 每次重新注册(服务重启、空闲回收后再打开)都从 0 数起,光凭 seq 续传
|
|
71
|
+
* 会把旧连接的 Last-Event-ID 对到新一轮的事件上。
|
|
72
|
+
*/
|
|
73
|
+
epoch: string;
|
|
65
74
|
ledger: TrajLedger;
|
|
66
75
|
lastActive: number;
|
|
67
76
|
toolPreset: ToolPreset;
|
|
68
77
|
/** 显式逐项选择优先于 preset;保留失效名称以便资源重新启用后恢复用户选择。 */
|
|
69
78
|
customActiveTools?: string[];
|
|
79
|
+
/**
|
|
80
|
+
* SDK / 扩展要求打开的工具(defaultTools 里的 +codemode、MCP 打开的 codemode、tool_search 载入的工具…)。
|
|
81
|
+
* 档位过滤会把它们从激活集里拿掉,记在这里,换到放行它们的档位时才能恢复。
|
|
82
|
+
*/
|
|
83
|
+
requestedTools: Set<string>;
|
|
70
84
|
resourceVersion: number;
|
|
71
85
|
resourceReloadPending: boolean;
|
|
72
86
|
resourceReloading: boolean;
|
|
@@ -147,12 +161,25 @@ export function toWebMessage(m: any, entryId?: string): WebMessage {
|
|
|
147
161
|
return out;
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
/** 消息对象 → 持久化条目 ID 的索引;条目数一变就重建(会话只追加,改写分支时条目数也会变) */
|
|
165
|
+
const entryIdIndexes = new WeakMap<SessionManager, { count: number; ids: WeakMap<object, string> }>();
|
|
166
|
+
|
|
150
167
|
function entryIdForMessage(sm: SessionManager, message: any): string | undefined {
|
|
151
168
|
// SDK 持久化保存同一对象引用。role+timestamp 并不唯一(同毫秒的 queued user/assistant)。
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
// 不能每条消息都扫一遍:getEntries() 本身就复制整个会话,快照逐条投影时会退化成 O(n²)。
|
|
170
|
+
if (!message || typeof message !== "object") return undefined;
|
|
171
|
+
const counter = (sm as unknown as { getEntryCount?: () => number }).getEntryCount;
|
|
172
|
+
const entries = counter ? null : (sm.getEntries() as any[]);
|
|
173
|
+
const count = counter ? counter.call(sm) : entries!.length;
|
|
174
|
+
let index = entryIdIndexes.get(sm);
|
|
175
|
+
if (!index || index.count !== count) {
|
|
176
|
+
index = { count, ids: new WeakMap() };
|
|
177
|
+
for (const entry of entries ?? (sm.getEntries() as any[])) {
|
|
178
|
+
if (entry?.type === "message" && entry.message && typeof entry.message === "object") index.ids.set(entry.message, entry.id);
|
|
179
|
+
}
|
|
180
|
+
entryIdIndexes.set(sm, index);
|
|
154
181
|
}
|
|
155
|
-
return
|
|
182
|
+
return index.ids.get(message);
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
function messageStreamId(m: Managed, message: unknown): string | undefined {
|
|
@@ -209,7 +236,8 @@ export function captureWriteBase(cwd: string, sessionPath: string, toolCallId: s
|
|
|
209
236
|
if (!stat.isFile() || stat.size > WRITE_BASE_MAX_BYTES) return;
|
|
210
237
|
const previous = readFileSync(abs, "utf8");
|
|
211
238
|
if (previous.includes("\u0000")) return; // 二进制不做行级 diff
|
|
212
|
-
|
|
239
|
+
// 只淘汰最早的一条:整表清空会把其它会话里正在执行的 write 的旧内容一起丢掉
|
|
240
|
+
if (writeBases.size >= 256) writeBases.delete(writeBases.keys().next().value!);
|
|
213
241
|
writeBases.set(`${sessionPath}\u0000${toolCallId}`, previous);
|
|
214
242
|
} catch {
|
|
215
243
|
/* 新文件或读不到:没有旧内容,走全绿兜底 */
|
|
@@ -487,11 +515,11 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
487
515
|
break;
|
|
488
516
|
case "tool_execution_update": {
|
|
489
517
|
const partial = sanitizeToolOutput(coercePartial((evt as any).partialResult) ?? "");
|
|
518
|
+
// 不带 args:start 已经发过,前端沿用上一次的值;write 的 args 含整份文件内容,每次进度都重发很浪费
|
|
490
519
|
publish(m, {
|
|
491
520
|
type: "tool",
|
|
492
521
|
id: evt.toolCallId,
|
|
493
522
|
name: evt.toolName,
|
|
494
|
-
args: (evt as any).args,
|
|
495
523
|
state: "running",
|
|
496
524
|
partialResult: partial.text || undefined,
|
|
497
525
|
encodingLoss: partial.encodingLoss || undefined,
|
|
@@ -592,6 +620,7 @@ function growthOf(m: Managed): GrowthTracker {
|
|
|
592
620
|
sessionPath: m.sessionPath,
|
|
593
621
|
publish: (evt) => publish(m, evt),
|
|
594
622
|
entries: () => m.sm.getEntries() as never,
|
|
623
|
+
consent: () => getGrowthInitConsent(m.cwd).catch(() => null),
|
|
595
624
|
}));
|
|
596
625
|
}
|
|
597
626
|
|
|
@@ -601,12 +630,44 @@ export async function growthRecord(m: Managed): Promise<GrowthRound | null> {
|
|
|
601
630
|
return growthOf(m).recordNow();
|
|
602
631
|
}
|
|
603
632
|
|
|
633
|
+
/**
|
|
634
|
+
* 用户回答了「要不要在这个工作区建 git 仓库」:决定按工作区记住,同一工作区的其他会话一并收起弹窗;
|
|
635
|
+
* 同意时由发起的会话立即拍基线(返回这一笔)。
|
|
636
|
+
*/
|
|
637
|
+
export async function growthConsent(m: Managed, allow: boolean): Promise<GrowthRound | null> {
|
|
638
|
+
touch(m);
|
|
639
|
+
await setGrowthInitConsent(m.cwd, allow);
|
|
640
|
+
const same = (p: string) => (process.platform === "win32" ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
641
|
+
for (const other of sessions.values()) {
|
|
642
|
+
if (other !== m && other.growth?.getConsentRequest() && same(other.cwd) === same(m.cwd)) void other.growth.consentAnswered(allow).catch(() => undefined);
|
|
643
|
+
}
|
|
644
|
+
return growthOf(m).consentAnswered(allow);
|
|
645
|
+
}
|
|
646
|
+
|
|
604
647
|
// ---------- 会话生命周期 ----------
|
|
605
648
|
|
|
606
649
|
function touch(m: Managed): void {
|
|
607
650
|
m.lastActive = Date.now();
|
|
608
651
|
}
|
|
609
652
|
|
|
653
|
+
const warmedResourceCwds = new Set<string>();
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 打开会话的首个快照要等资源发现(扩展 / 技能 / AGENTS.md / prompt 模板)跑完,本机实测约
|
|
657
|
+
* 140ms;侧栏一拿到会话列表就把最可能被点开的那个工作区先发现掉,用户点开时直接命中缓存。
|
|
658
|
+
* 每个 cwd 只做一次,最多两个(别把一堆工作区的扩展全加载起来),失败静默。
|
|
659
|
+
*/
|
|
660
|
+
export function warmSessionResources(cwds: string[]): void {
|
|
661
|
+
for (const cwd of cwds) {
|
|
662
|
+
if (!cwd || warmedResourceCwds.size >= 2) return;
|
|
663
|
+
const key = process.platform === "win32" ? cwd.toLowerCase() : cwd;
|
|
664
|
+
if (warmedResourceCwds.has(key)) continue;
|
|
665
|
+
warmedResourceCwds.add(key);
|
|
666
|
+
if (peekResourceLoader(cwd)) continue;
|
|
667
|
+
void resourceLoaderReady(cwd).catch(() => undefined);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
610
671
|
export function getManaged(sessionPath: string): Managed {
|
|
611
672
|
let m = sessions.get(sessionPath);
|
|
612
673
|
if (!m) {
|
|
@@ -634,12 +695,14 @@ function registerManaged(sessionPath: string, cwd: string, sm: SessionManager):
|
|
|
634
695
|
subscribers: new Set(),
|
|
635
696
|
buffer: [],
|
|
636
697
|
seq: 0,
|
|
698
|
+
epoch: randomUUID().replace(/-/g, "").slice(0, 12),
|
|
637
699
|
inflight: null,
|
|
638
700
|
ui: null,
|
|
639
701
|
growth: null,
|
|
640
702
|
ledger: new TrajLedger(initialTrajectory),
|
|
641
703
|
lastActive: Date.now(),
|
|
642
704
|
toolPreset: "readonly",
|
|
705
|
+
requestedTools: new Set(),
|
|
643
706
|
resourceVersion: 0,
|
|
644
707
|
resourceReloadPending: false,
|
|
645
708
|
resourceReloading: false,
|
|
@@ -753,7 +816,8 @@ function m_smEntries(session: AgentSession): unknown[] {
|
|
|
753
816
|
return s.sm?.getEntries?.() ?? s.sessionManager?.getEntries?.() ?? [];
|
|
754
817
|
}
|
|
755
818
|
|
|
756
|
-
|
|
819
|
+
/** resources 已经算过(快照)就传进来,免得资源发现结果再读一遍 */
|
|
820
|
+
function environmentTrajectory(m: Managed, session: AgentSession | null, known?: ContextResource[]): TrajEntry[] {
|
|
757
821
|
const ts = m.ledger.entries[0]?.ts ?? Date.now();
|
|
758
822
|
const entries: TrajEntry[] = [];
|
|
759
823
|
if (session?.systemPrompt) {
|
|
@@ -767,7 +831,7 @@ function environmentTrajectory(m: Managed, session: AgentSession | null): TrajEn
|
|
|
767
831
|
}
|
|
768
832
|
|
|
769
833
|
try {
|
|
770
|
-
const resources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
834
|
+
const resources = known ?? loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
771
835
|
resources.forEach((resource, index) => entries.push({
|
|
772
836
|
seq: -2 - index,
|
|
773
837
|
kind: "context",
|
|
@@ -808,6 +872,8 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
808
872
|
created = session;
|
|
809
873
|
if (m.disposed) throw new Error("session is disposed");
|
|
810
874
|
m.session = session;
|
|
875
|
+
// SDK 按 defaultTools 设置算出的初始激活集(含 "+codemode" 这类显式开启)
|
|
876
|
+
m.requestedTools = new Set(session.getActiveToolNames());
|
|
811
877
|
installSessionPolicy(m, session);
|
|
812
878
|
applyToolPolicy(m, session);
|
|
813
879
|
session.subscribe((evt) => { if (!m.disposed) translate(m, evt); });
|
|
@@ -832,8 +898,10 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
832
898
|
} catch (error) {
|
|
833
899
|
m.ui?.dispose();
|
|
834
900
|
m.ui = null;
|
|
835
|
-
|
|
836
|
-
loader.getExtensions().runtime
|
|
901
|
+
// session_start 可能已经跑过(MCP 在连服务器):先发 session_shutdown 收尾,再废掉扩展 runtime
|
|
902
|
+
const runtime = loader.getExtensions().runtime;
|
|
903
|
+
if (created) void shutdownSession(created).finally(() => runtime.invalidate());
|
|
904
|
+
else runtime.invalidate();
|
|
837
905
|
m.session = null;
|
|
838
906
|
throw error;
|
|
839
907
|
}
|
|
@@ -849,12 +917,74 @@ function isManagedBusy(m: Managed): boolean {
|
|
|
849
917
|
}
|
|
850
918
|
|
|
851
919
|
function allowedToolNames(m: Managed, session: AgentSession): string[] {
|
|
852
|
-
const
|
|
853
|
-
return listAllToolNames(session).filter((name) =>
|
|
920
|
+
const custom = m.customActiveTools;
|
|
921
|
+
return listAllToolNames(session).filter((name) => (custom ? custom.includes(name) : presetAllowsTool(m.toolPreset, name)));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** SDK 注册即激活的规则:direct / model-only 曝光、且没有写 defaultActive: false */
|
|
925
|
+
function activatedOnRegistration(session: AgentSession): Set<string> {
|
|
926
|
+
return new Set(session.getAllTools()
|
|
927
|
+
.filter((tool) => (tool.exposure === "direct" || tool.exposure === "model-only") && session.getToolDefinition(tool.name)?.defaultActive !== false)
|
|
928
|
+
.map((tool) => tool.name));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* 档位只是上限,开哪些仍按 SDK 的规则:注册即激活的,加上 SDK / 扩展明确要求打开的(requestedTools)。
|
|
933
|
+
* codemode、tool_search 是 defaultActive: false,延迟加载的 MCP 工具不直接声明——把放行的全打开,
|
|
934
|
+
* 每次请求要多带几千 token,延迟加载也白做。逐项选择(customActiveTools)是用户点名要的,照单全开。
|
|
935
|
+
*/
|
|
936
|
+
function policyToolNames(m: Managed, session: AgentSession): string[] {
|
|
937
|
+
const allowed = allowedToolNames(m, session);
|
|
938
|
+
if (m.customActiveTools) return allowed;
|
|
939
|
+
const auto = activatedOnRegistration(session);
|
|
940
|
+
return allowed.filter((name) => auto.has(name) || m.requestedTools.has(name));
|
|
854
941
|
}
|
|
855
942
|
|
|
943
|
+
/** installSessionPolicy 包装前的原始 setActiveToolsByName:PiWeb 自己落策略时不能被记成"扩展要求" */
|
|
944
|
+
const rawSetActiveTools = new WeakMap<AgentSession, (names: string[]) => void>();
|
|
945
|
+
|
|
856
946
|
function applyToolPolicy(m: Managed, session: AgentSession): void {
|
|
857
|
-
|
|
947
|
+
const names = policyToolNames(m, session);
|
|
948
|
+
(rawSetActiveTools.get(session) ?? session.setActiveToolsByName.bind(session))(names);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* SDK 的 session.dispose() 不发 session_shutdown(只有 AgentSessionRuntime 发):不补发,扩展收不到收尾信号,
|
|
953
|
+
* 内置 MCP 拉起的 stdio 服务器进程会一直挂着。先同步停掉 agent,收尾最多等 5 秒,再 dispose。
|
|
954
|
+
*/
|
|
955
|
+
const shuttingDown = new WeakMap<AgentSession, Promise<void>>();
|
|
956
|
+
|
|
957
|
+
function shutdownSession(session: AgentSession): Promise<void> {
|
|
958
|
+
// 创建中途被回收时,disposeSession 与 ensureSession 的 catch 会各收尾一次
|
|
959
|
+
const existing = shuttingDown.get(session);
|
|
960
|
+
if (existing) return existing;
|
|
961
|
+
try {
|
|
962
|
+
session.agent.abort();
|
|
963
|
+
} catch {
|
|
964
|
+
/* ignore */
|
|
965
|
+
}
|
|
966
|
+
let shutdown: Promise<unknown> = Promise.resolve();
|
|
967
|
+
try {
|
|
968
|
+
const runner = session.extensionRunner;
|
|
969
|
+
if (runner.hasHandlers("session_shutdown")) shutdown = runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
970
|
+
} catch {
|
|
971
|
+
/* 扩展 runtime 已失效:没有可收尾的了 */
|
|
972
|
+
}
|
|
973
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
974
|
+
const timeout = new Promise<void>((resolve) => {
|
|
975
|
+
timer = setTimeout(resolve, SHUTDOWN_TIMEOUT_MS);
|
|
976
|
+
timer.unref?.();
|
|
977
|
+
});
|
|
978
|
+
const done = Promise.race([shutdown.then(() => undefined, () => undefined), timeout]).finally(() => {
|
|
979
|
+
clearTimeout(timer);
|
|
980
|
+
try {
|
|
981
|
+
session.dispose();
|
|
982
|
+
} catch {
|
|
983
|
+
/* ignore */
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
shuttingDown.set(session, done);
|
|
987
|
+
return done;
|
|
858
988
|
}
|
|
859
989
|
|
|
860
990
|
function publishTools(m: Managed, session: AgentSession): void {
|
|
@@ -864,9 +994,14 @@ function publishTools(m: Managed, session: AgentSession): void {
|
|
|
864
994
|
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
865
995
|
function installSessionPolicy(m: Managed, session: AgentSession): void {
|
|
866
996
|
const setActive = session.setActiveToolsByName.bind(session);
|
|
997
|
+
rawSetActiveTools.set(session, setActive);
|
|
867
998
|
session.setActiveToolsByName = (names) => {
|
|
999
|
+
const wanted = [...new Set(names)];
|
|
1000
|
+
// 扩展传的是完整列表(多半是 getActiveTools() 加减几项):原本激活、这次没列的算关掉,其余都算要求打开
|
|
1001
|
+
for (const name of listActiveTools(session)) if (!wanted.includes(name)) m.requestedTools.delete(name);
|
|
1002
|
+
for (const name of wanted) m.requestedTools.add(name);
|
|
868
1003
|
const allowed = allowedToolNames(m, session);
|
|
869
|
-
setActive(
|
|
1004
|
+
setActive(wanted.filter((name) => allowed.includes(name)));
|
|
870
1005
|
};
|
|
871
1006
|
const reload = session.reload.bind(session);
|
|
872
1007
|
session.reload = async (options) => {
|
|
@@ -877,6 +1012,8 @@ function installSessionPolicy(m: Managed, session: AgentSession): void {
|
|
|
877
1012
|
await withResourceLock(async () => {
|
|
878
1013
|
if (m.disposed || m.runActive || !session.isIdle) throw new Error("session is busy");
|
|
879
1014
|
await reload({ beforeSessionStart: async () => {
|
|
1015
|
+
// reload 绕过包装直接按 defaultTools 重算了激活集(新加进 defaultTools 的工具会被打开)
|
|
1016
|
+
for (const name of listActiveTools(session)) m.requestedTools.add(name);
|
|
880
1017
|
applyToolPolicy(m, session);
|
|
881
1018
|
await options?.beforeSessionStart?.();
|
|
882
1019
|
applyToolPolicy(m, session);
|
|
@@ -913,22 +1050,12 @@ export function disposeSession(m: Managed): void {
|
|
|
913
1050
|
m.ui = null;
|
|
914
1051
|
m.growth?.dispose();
|
|
915
1052
|
m.growth = null;
|
|
916
|
-
|
|
917
|
-
m.session?.dispose();
|
|
918
|
-
} catch {
|
|
919
|
-
/* ignore */
|
|
920
|
-
}
|
|
1053
|
+
if (m.session) void shutdownSession(m.session);
|
|
921
1054
|
// 正在创建中的会话:创建完成后立即销毁,否则它会挂在已被移出池的
|
|
922
1055
|
// Managed 上,订阅与句柄永久泄漏(同一 JSONL 还会出现双活跃会话)。
|
|
923
1056
|
if (m.creating) {
|
|
924
1057
|
void m.creating
|
|
925
|
-
.then((session) =>
|
|
926
|
-
try {
|
|
927
|
-
session?.dispose();
|
|
928
|
-
} catch {
|
|
929
|
-
/* ignore */
|
|
930
|
-
}
|
|
931
|
-
})
|
|
1058
|
+
.then((session) => (session ? shutdownSession(session) : undefined))
|
|
932
1059
|
.catch(() => undefined);
|
|
933
1060
|
}
|
|
934
1061
|
for (const send of m.subscribers) {
|
|
@@ -971,7 +1098,13 @@ export function reap(): void {
|
|
|
971
1098
|
}
|
|
972
1099
|
}
|
|
973
1100
|
|
|
974
|
-
|
|
1101
|
+
// 定时器挂 globalThis:开发热重载会重新求值本模块,不清掉旧的就会一份份叠加
|
|
1102
|
+
{
|
|
1103
|
+
const store = globalThis as typeof globalThis & { __piWebReaper?: ReturnType<typeof setInterval> };
|
|
1104
|
+
if (store.__piWebReaper) clearInterval(store.__piWebReaper);
|
|
1105
|
+
store.__piWebReaper = setInterval(reap, 60_000);
|
|
1106
|
+
store.__piWebReaper.unref?.();
|
|
1107
|
+
}
|
|
975
1108
|
|
|
976
1109
|
/** 归档会话的保留期清理:只删「已归档且长期无活动」的会话;正在使用的会话一律跳过 */
|
|
977
1110
|
function isSessionInUse(sessionPath: string): boolean {
|
|
@@ -1044,6 +1177,11 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1044
1177
|
m.resourceReloadPending = false;
|
|
1045
1178
|
try {
|
|
1046
1179
|
await m.session.reload();
|
|
1180
|
+
// SDK 的 reload 不会换当前模型对象:models.json 改了 baseUrl / contextWindow 后,
|
|
1181
|
+
// 会话手里还是旧对象。按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
|
|
1182
|
+
const current = m.session.model;
|
|
1183
|
+
const fresh = current && m.session.modelRuntime.getModel(current.provider, current.id);
|
|
1184
|
+
if (fresh && fresh !== current) m.session.agent.state.model = fresh;
|
|
1047
1185
|
return true;
|
|
1048
1186
|
} catch (err) {
|
|
1049
1187
|
publish(m, { type: "error", message: `重载扩展失败:${String((err as Error)?.message ?? err)}`, ts: Date.now() });
|
|
@@ -1209,10 +1347,12 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1209
1347
|
name = "";
|
|
1210
1348
|
}
|
|
1211
1349
|
|
|
1212
|
-
// 上下文注入资源(AGENTS.md 与扩展附加 prompt
|
|
1350
|
+
// 上下文注入资源(AGENTS.md 与扩展附加 prompt);读失败时轨迹那边仍按原样自己再试一次
|
|
1213
1351
|
let contextResources: ContextResource[] = [];
|
|
1352
|
+
let contextResourcesLoaded = false;
|
|
1214
1353
|
try {
|
|
1215
1354
|
contextResources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
1355
|
+
contextResourcesLoaded = true;
|
|
1216
1356
|
} catch {
|
|
1217
1357
|
contextResources = [];
|
|
1218
1358
|
}
|
|
@@ -1228,7 +1368,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1228
1368
|
const resources = collectResources(m);
|
|
1229
1369
|
|
|
1230
1370
|
const baseTrajectory = m.ledger.entries.length ? m.ledger.entries : coldTrajectory(m);
|
|
1231
|
-
const trajectory = [...environmentTrajectory(m, session), ...baseTrajectory];
|
|
1371
|
+
const trajectory = [...environmentTrajectory(m, session, contextResourcesLoaded ? contextResources : undefined), ...baseTrajectory];
|
|
1232
1372
|
|
|
1233
1373
|
return {
|
|
1234
1374
|
seq: snapSeq ?? m.seq,
|
|
@@ -1242,12 +1382,15 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1242
1382
|
messages,
|
|
1243
1383
|
userTurns: userTurnsFromEntries(m.sm.getEntries() as any[]),
|
|
1244
1384
|
growthError: m.growth?.getError?.() ?? null,
|
|
1385
|
+
growthConsent: m.growth?.getConsentRequest?.() ?? null,
|
|
1245
1386
|
model,
|
|
1246
1387
|
thinkingLevel,
|
|
1247
1388
|
thinkingLevels,
|
|
1248
1389
|
toolPreset: m.toolPreset,
|
|
1249
1390
|
customActiveTools: m.customActiveTools ?? null,
|
|
1250
1391
|
extensionUiRequests: m.ui?.getPendingRequests() ?? [],
|
|
1392
|
+
// 扩展 setStatus 的当前值:重连拿快照时页脚状态不能丢
|
|
1393
|
+
extensionStatuses: Object.fromEntries(m.ui?.statuses ?? []),
|
|
1251
1394
|
tools,
|
|
1252
1395
|
stats,
|
|
1253
1396
|
contextUsage,
|
|
@@ -1267,6 +1410,19 @@ function coldTrajectory(m: Managed): TrajEntry[] {
|
|
|
1267
1410
|
|
|
1268
1411
|
// ---------- SSE 订阅 ----------
|
|
1269
1412
|
|
|
1413
|
+
/** SSE 事件 id:`<epoch>.<seq>` */
|
|
1414
|
+
export function sseEventId(m: Managed, seq: number): string {
|
|
1415
|
+
return `${m.epoch}.${seq}`;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** 解析浏览器带回的 Last-Event-ID;不是这一份 Managed 发的(epoch 对不上)一律当没有,走快照 */
|
|
1419
|
+
export function parseLastEventId(m: Managed, raw: string | null): number | undefined {
|
|
1420
|
+
const match = /^([A-Za-z0-9]+)\.(\d+)$/.exec(raw?.trim() ?? "");
|
|
1421
|
+
if (!match || match[1] !== m.epoch) return undefined;
|
|
1422
|
+
const seq = Number(match[2]);
|
|
1423
|
+
return Number.isSafeInteger(seq) ? seq : undefined;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1270
1426
|
export function subscribe(
|
|
1271
1427
|
m: Managed,
|
|
1272
1428
|
send: (seq: number, json: string) => void,
|
|
@@ -1298,6 +1454,47 @@ export interface CommandResult {
|
|
|
1298
1454
|
data?: Record<string, unknown>;
|
|
1299
1455
|
}
|
|
1300
1456
|
|
|
1457
|
+
/**
|
|
1458
|
+
* 发 prompt 前给生长记录拍基线。基线不能无限阻塞提问(git 不可用时每次探测至多 30s 超时):
|
|
1459
|
+
* 5s 内没完成就放行,快照后台补拍(工具结束后照常记步,外部修改兜底)。
|
|
1460
|
+
*/
|
|
1461
|
+
async function prepareGrowthBriefly(m: Managed): Promise<void> {
|
|
1462
|
+
await Promise.race([
|
|
1463
|
+
growthOf(m).prepare().catch(() => undefined),
|
|
1464
|
+
new Promise<void>((resolve) => setTimeout(resolve, 5000)),
|
|
1465
|
+
]);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** 交给 SDK,并在「受理」那一刻返回,而不是等整轮跑完;受理之后的错误经 SSE 可见 */
|
|
1469
|
+
function submitPrompt(
|
|
1470
|
+
m: Managed,
|
|
1471
|
+
session: AgentSession,
|
|
1472
|
+
text: string,
|
|
1473
|
+
options: Pick<NonNullable<Parameters<AgentSession["prompt"]>[1]>, "images" | "streamingBehavior"> = {},
|
|
1474
|
+
): Promise<CommandResult> {
|
|
1475
|
+
return new Promise<CommandResult>((resolve) => {
|
|
1476
|
+
let accepted = false;
|
|
1477
|
+
const run = session.prompt(text, {
|
|
1478
|
+
...options,
|
|
1479
|
+
source: "rpc",
|
|
1480
|
+
preflightResult: (success) => {
|
|
1481
|
+
if (success) {
|
|
1482
|
+
accepted = true;
|
|
1483
|
+
resolve({ ok: true, data: { accepted: true } });
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1487
|
+
// 同步挂好 rejection handler;接受后的错误仍经 SSE 可见,不能成为未处理拒绝。
|
|
1488
|
+
void run.then(() => {
|
|
1489
|
+
if (!accepted) resolve({ ok: false, error: "prompt completed without acceptance" });
|
|
1490
|
+
}, (error) => {
|
|
1491
|
+
const message = String(error?.message ?? error);
|
|
1492
|
+
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
1493
|
+
else resolve({ ok: false, error: message });
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1301
1498
|
export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
|
|
1302
1499
|
touch(m);
|
|
1303
1500
|
try {
|
|
@@ -1316,37 +1513,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1316
1513
|
if (m.disposed) throw new Error("session is disposed");
|
|
1317
1514
|
// 只有用户显式选择 steer/followUp 才排队;旧的普通提交不能悄悄成为 steering。
|
|
1318
1515
|
if ((session.isStreaming || m.runActive) && !cmd.behavior) return { ok: false, error: "session is busy; choose steer or followUp" };
|
|
1319
|
-
if (!session.isStreaming)
|
|
1320
|
-
// 基线快照不能无限阻塞首条 prompt(git 不可用时每次探测至多 30s 超时):
|
|
1321
|
-
// 5s 内没完成就放行 prompt,快照后台补拍(工具结束后照常记步,外部修改兜底)。
|
|
1322
|
-
await Promise.race([
|
|
1323
|
-
growthOf(m).prepare().catch(() => undefined),
|
|
1324
|
-
new Promise<void>((resolve) => setTimeout(resolve, 5000)),
|
|
1325
|
-
]);
|
|
1326
|
-
}
|
|
1516
|
+
if (!session.isStreaming) await prepareGrowthBriefly(m);
|
|
1327
1517
|
if (m.disposed) throw new Error("session is disposed");
|
|
1328
|
-
return await
|
|
1329
|
-
let accepted = false;
|
|
1330
|
-
const run = session.prompt(text, {
|
|
1331
|
-
images: cmd.images,
|
|
1332
|
-
streamingBehavior: cmd.behavior,
|
|
1333
|
-
source: "rpc",
|
|
1334
|
-
preflightResult: (success) => {
|
|
1335
|
-
if (success) {
|
|
1336
|
-
accepted = true;
|
|
1337
|
-
resolve({ ok: true, data: { accepted: true } });
|
|
1338
|
-
}
|
|
1339
|
-
},
|
|
1340
|
-
});
|
|
1341
|
-
// 同步挂好 rejection handler;接受后的错误仍经 SSE 可见,不能成为未处理拒绝。
|
|
1342
|
-
void run.then(() => {
|
|
1343
|
-
if (!accepted) resolve({ ok: false, error: "prompt completed without acceptance" });
|
|
1344
|
-
}, (error) => {
|
|
1345
|
-
const message = String(error?.message ?? error);
|
|
1346
|
-
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
1347
|
-
else resolve({ ok: false, error: message });
|
|
1348
|
-
});
|
|
1349
|
-
});
|
|
1518
|
+
return await submitPrompt(m, session, text, { images: cmd.images, streamingBehavior: cmd.behavior });
|
|
1350
1519
|
} finally {
|
|
1351
1520
|
m.promptSubmitting = false;
|
|
1352
1521
|
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
@@ -1405,6 +1574,15 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1405
1574
|
const preset = cmd.preset as ToolPreset;
|
|
1406
1575
|
if (!Object.hasOwn(TOOL_PRESETS, preset)) return { ok: false, error: "invalid tool preset" };
|
|
1407
1576
|
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1577
|
+
if (!m.session && !m.creating) {
|
|
1578
|
+
// 冷会话只记下选择,等真正启动时(ensureSession → applyToolPolicy)再生效。
|
|
1579
|
+
// 不能为了切个档位就冷启动 agent:打开会话时前端会恢复本地档位,
|
|
1580
|
+
// 若这里 ensureSession,光是浏览历史会话就会跑一遍扩展的 session_start。
|
|
1581
|
+
m.toolPreset = preset;
|
|
1582
|
+
m.customActiveTools = undefined;
|
|
1583
|
+
publish(m, { type: "tools", active: [], all: [], toolPreset: preset, customActiveTools: null, ts: Date.now() });
|
|
1584
|
+
return { ok: true };
|
|
1585
|
+
}
|
|
1408
1586
|
const session = await ensureSession(m);
|
|
1409
1587
|
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1410
1588
|
m.toolPreset = preset;
|
|
@@ -1467,11 +1645,15 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1467
1645
|
if (m.disposed) throw new Error("session is disposed");
|
|
1468
1646
|
const moved = await session.navigateTree(entryId);
|
|
1469
1647
|
if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
|
|
1470
|
-
await
|
|
1471
|
-
|
|
1472
|
-
|
|
1648
|
+
await prepareGrowthBriefly(m);
|
|
1649
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
1650
|
+
// 与普通 prompt 一样在受理时就返回:等整轮跑完的话,这期间 promptSubmitting 一直为真,
|
|
1651
|
+
// 插话 / 排队 / 新消息全被拒,HTTP 请求也一直挂着
|
|
1652
|
+
return await submitPrompt(m, session, text);
|
|
1473
1653
|
} finally {
|
|
1474
1654
|
m.promptSubmitting = false;
|
|
1655
|
+
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
1656
|
+
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
1475
1657
|
}
|
|
1476
1658
|
}
|
|
1477
1659
|
case "rename": {
|