@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
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
* (工作区还没有任何提交时记成基线)
|
|
6
6
|
* - agent_start → agent_settled 是一轮:结束时提交本轮,提问首行做标题、用户消息 entryId 写进 trailers;
|
|
7
7
|
* 没有改动也提交空 commit,轮号与对话一一对应
|
|
8
|
+
* - 工作区还不是仓库根目录时,要先征得同意才 `git init`:没决定就向浏览器发一次 growth_consent 并暂不记录,
|
|
9
|
+
* 拒绝了就停用;同意后(consentAnswered)立即拍基线
|
|
8
10
|
* 提交失败不影响 pi,只向浏览器报一次错;项目过大 / 没有 git 时本会话停用。
|
|
9
11
|
*/
|
|
10
12
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { GrowthError, commitRound, recordWorkspaceChanges } from "./growth-service";
|
|
13
|
+
import { type CommitOptions, GrowthError, commitRound, recordWorkspaceChanges } from "./growth-service";
|
|
12
14
|
import { promptsFromEntries } from "./growth-turns";
|
|
13
|
-
import type { GrowthRound, GrowthRoundStatus, WebEvent } from "./types";
|
|
15
|
+
import type { GrowthConsentRequest, GrowthRound, GrowthRoundStatus, WebEvent } from "./types";
|
|
14
16
|
|
|
15
17
|
const ERROR_THROTTLE_MS = 60_000;
|
|
16
|
-
export const GROWTH_TRACKER_VERSION =
|
|
18
|
+
export const GROWTH_TRACKER_VERSION = 5;
|
|
17
19
|
|
|
18
20
|
interface SessionEntryLike {
|
|
19
21
|
type?: string;
|
|
@@ -24,11 +26,15 @@ interface SessionEntryLike {
|
|
|
24
26
|
export interface GrowthTracker {
|
|
25
27
|
version: number;
|
|
26
28
|
getError(): string | null;
|
|
29
|
+
/** 等用户回答的建仓库请求(快照里带上,页面重开也能看到) */
|
|
30
|
+
getConsentRequest(): GrowthConsentRequest | null;
|
|
27
31
|
/** 发 prompt 前完成「你的修改」提交,避免它被算进 pi 这一轮 */
|
|
28
32
|
prepare(): Promise<void>;
|
|
29
33
|
onEvent(evt: AgentSessionEvent): void;
|
|
30
34
|
/** 立即记录(项目栏按钮):把当前改动提交为「你的修改」;没改动返回 null */
|
|
31
35
|
recordNow(): Promise<GrowthRound | null>;
|
|
36
|
+
/** 用户回答了建仓库请求(决定已由调用方存好):同意就立即拍基线,拒绝就停用 */
|
|
37
|
+
consentAnswered(allow: boolean): Promise<GrowthRound | null>;
|
|
32
38
|
dispose(): void;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -50,8 +56,10 @@ export function createGrowthTracker(opts: {
|
|
|
50
56
|
publish: (evt: WebEvent) => void;
|
|
51
57
|
/** 会话的持久条目(SessionManager.getEntries):轮结束时从中取本轮的提问 */
|
|
52
58
|
entries: () => readonly SessionEntryLike[];
|
|
59
|
+
/** 该工作区是否同意建仓库:true 同意 / false 拒绝 / null 还没问过 */
|
|
60
|
+
consent: () => Promise<boolean | null>;
|
|
53
61
|
}): GrowthTracker {
|
|
54
|
-
const { cwd, sessionPath, publish, entries } = opts;
|
|
62
|
+
const { cwd, sessionPath, publish, entries, consent } = opts;
|
|
55
63
|
let disabled = false;
|
|
56
64
|
let disabledFailure: GrowthError | null = null;
|
|
57
65
|
let disposed = false;
|
|
@@ -61,6 +69,11 @@ export function createGrowthTracker(opts: {
|
|
|
61
69
|
let chain: Promise<void> = Promise.resolve();
|
|
62
70
|
/** 本轮开始时会话条目的数量;null = 不在一轮之中 */
|
|
63
71
|
let runStart: number | null = null;
|
|
72
|
+
/** 每开始一轮加一:prepare 排队期间 pi 已经开工,就不能再把工作区记成「你的修改」 */
|
|
73
|
+
let runEpoch = 0;
|
|
74
|
+
/** 已知工作区要先建仓库;consentRequest = 正在等用户回答 */
|
|
75
|
+
let needsInit = false;
|
|
76
|
+
let consentRequest: GrowthConsentRequest | null = null;
|
|
64
77
|
|
|
65
78
|
// 提交失败不打扰用户(项目栏退化成普通文件树 + 查看器):只在服务端日志记一次,没 git / 项目过大就停用本会话的跟踪
|
|
66
79
|
const reportError = (error: unknown) => {
|
|
@@ -95,14 +108,57 @@ export function createGrowthTracker(opts: {
|
|
|
95
108
|
return round;
|
|
96
109
|
};
|
|
97
110
|
|
|
111
|
+
const declined = () => {
|
|
112
|
+
disabled = true;
|
|
113
|
+
disabledFailure = new GrowthError("growth tracking was declined for this workspace", "unavailable");
|
|
114
|
+
runStart = null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 带同意检查地提交:返回 undefined 表示这次没提交(还在等同意 / 已拒绝)。
|
|
119
|
+
* 已知要建仓库且还没决定时直接跳过,不再每轮起一个 git 进程去探测。
|
|
120
|
+
*/
|
|
121
|
+
const commit = async (run: (options: CommitOptions) => Promise<GrowthRound | null>, abortIf?: () => boolean): Promise<GrowthRound | null | undefined> => {
|
|
122
|
+
const decision = needsInit ? await consent() : null;
|
|
123
|
+
if (decision === false) {
|
|
124
|
+
declined();
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (needsInit && decision === null) return undefined;
|
|
128
|
+
try {
|
|
129
|
+
return await run({ allowInit: decision === true, abortIf });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!(error instanceof GrowthError) || error.code !== "needs-consent") throw error;
|
|
132
|
+
needsInit = true;
|
|
133
|
+
const answer = await consent();
|
|
134
|
+
if (answer === false) {
|
|
135
|
+
declined();
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (answer === true) return run({ allowInit: true, abortIf });
|
|
139
|
+
if (!consentRequest && !disposed) {
|
|
140
|
+
consentRequest = { cwd, reason: error.target?.reason ?? "not-git", parent: error.target?.parent };
|
|
141
|
+
publish({ type: "growth_consent", request: consentRequest, ts: Date.now() });
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const recordUserChanges = (abortIf?: () => boolean) =>
|
|
148
|
+
commit((options) => recordWorkspaceChanges(cwd, sessionPath, options), abortIf);
|
|
149
|
+
|
|
98
150
|
return {
|
|
99
151
|
version: GROWTH_TRACKER_VERSION,
|
|
100
152
|
getError: () => lastErrorMessage,
|
|
153
|
+
getConsentRequest: () => consentRequest,
|
|
101
154
|
prepare() {
|
|
102
155
|
if (disabled || disposed) return chain;
|
|
156
|
+
const epoch = runEpoch;
|
|
103
157
|
return enqueue(async () => {
|
|
104
158
|
if (disabled || disposed) return;
|
|
105
|
-
|
|
159
|
+
// 5 秒没拍完提问就放行了:pi 开工后再拍的快照会把它的改动记成「你的修改」
|
|
160
|
+
const round = await recordUserChanges(() => runEpoch !== epoch);
|
|
161
|
+
if (round !== undefined) published(round);
|
|
106
162
|
});
|
|
107
163
|
},
|
|
108
164
|
onEvent(evt) {
|
|
@@ -110,7 +166,10 @@ export function createGrowthTracker(opts: {
|
|
|
110
166
|
switch (evt.type) {
|
|
111
167
|
case "agent_start":
|
|
112
168
|
// 自动重试会再次 agent_start:一轮只从第一次开始算
|
|
113
|
-
if (runStart === null)
|
|
169
|
+
if (runStart === null) {
|
|
170
|
+
runStart = entries().length;
|
|
171
|
+
runEpoch += 1;
|
|
172
|
+
}
|
|
114
173
|
break;
|
|
115
174
|
case "agent_settled": {
|
|
116
175
|
if (runStart === null) break;
|
|
@@ -120,7 +179,8 @@ export function createGrowthTracker(opts: {
|
|
|
120
179
|
runStart = null;
|
|
121
180
|
void enqueue(async () => {
|
|
122
181
|
if (disabled || disposed) return;
|
|
123
|
-
|
|
182
|
+
const round = await commit((options) => commitRound(cwd, { kind: "round", session: sessionPath, title, promptIds: ids, status: roundStatus(run) }, options));
|
|
183
|
+
if (round !== undefined) published(round);
|
|
124
184
|
});
|
|
125
185
|
break;
|
|
126
186
|
}
|
|
@@ -135,15 +195,37 @@ export function createGrowthTracker(opts: {
|
|
|
135
195
|
await enqueue(async () => {
|
|
136
196
|
try {
|
|
137
197
|
if (disabled) throw disabledFailure ?? new GrowthError("growth tracking is disabled for this workspace", "unavailable");
|
|
138
|
-
|
|
198
|
+
const round = await recordUserChanges();
|
|
199
|
+
if (round === undefined) {
|
|
200
|
+
throw disabled
|
|
201
|
+
? disabledFailure ?? new GrowthError("growth tracking is disabled for this workspace", "unavailable")
|
|
202
|
+
: new GrowthError("growth tracking is waiting for permission to create a git repository", "needs-consent");
|
|
203
|
+
}
|
|
204
|
+
result = published(round);
|
|
139
205
|
} catch (error) {
|
|
140
206
|
failure = error;
|
|
141
|
-
reportError(error);
|
|
207
|
+
if (!(error instanceof GrowthError && error.code === "needs-consent")) reportError(error);
|
|
142
208
|
}
|
|
143
209
|
});
|
|
144
210
|
if (failure) throw failure;
|
|
145
211
|
return result;
|
|
146
212
|
},
|
|
213
|
+
async consentAnswered(allow) {
|
|
214
|
+
const pending = consentRequest;
|
|
215
|
+
consentRequest = null;
|
|
216
|
+
if (pending && !disposed) publish({ type: "growth_consent", request: null, ts: Date.now() });
|
|
217
|
+
if (!allow) {
|
|
218
|
+
declined();
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (disabled || disposed) return null;
|
|
222
|
+
let result: GrowthRound | null = null;
|
|
223
|
+
await enqueue(async () => {
|
|
224
|
+
const round = await recordUserChanges();
|
|
225
|
+
result = round === undefined ? null : published(round);
|
|
226
|
+
});
|
|
227
|
+
return result;
|
|
228
|
+
},
|
|
147
229
|
dispose() {
|
|
148
230
|
disposed = true;
|
|
149
231
|
},
|
|
@@ -11,7 +11,8 @@ import { BlockList, isIP } from "node:net";
|
|
|
11
11
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { getNodeValue, parseTree, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
13
13
|
import { filterModelsByPiScope } from "./models/model-scope";
|
|
14
|
-
import {
|
|
14
|
+
import { invalidateJsonFilesNamed, readJsonObjectSafe } from "./agent-config";
|
|
15
|
+
import { getAgentDir, getModelRuntime, refreshModelRuntime } from "./pi";
|
|
15
16
|
import { getEnabledModelPatterns } from "./pi-settings";
|
|
16
17
|
import { reloadSessionsForCwd } from "./agent-manager";
|
|
17
18
|
import {
|
|
@@ -67,8 +68,24 @@ let builtInProviderIdsPromise: Promise<Set<string>> | null = null;
|
|
|
67
68
|
let listCache: { at: number; value: Promise<{ providers: ProviderView[]; models: ModelView[] }> } | null = null;
|
|
68
69
|
const LIST_TTL_MS = 5_000;
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 认证探测结果单独缓存:目录只活 5 秒,但「这个 provider 能不能用」在一次会话里几乎不变。
|
|
73
|
+
* 设置面板每次打开都会重建目录,没有这层缓存就要为每个已配置 provider 重跑一次网络往返
|
|
74
|
+
* (单次超时 5 秒),面板首屏因此抖动。
|
|
75
|
+
*/
|
|
76
|
+
const authCheckCache = new Map<string, { at: number; ready: boolean; source?: string; error?: string }>();
|
|
77
|
+
const AUTH_CHECK_TTL_MS = 60_000;
|
|
78
|
+
|
|
70
79
|
export function invalidateModelList(): void {
|
|
71
80
|
listCache = null;
|
|
81
|
+
authCheckCache.clear();
|
|
82
|
+
// 凭证/自定义 provider 变了,auth.json 的读盘缓存也要作废(按文件名作废:这里不该去解析 agent 目录)
|
|
83
|
+
invalidateJsonFilesNamed("auth.json");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 目录版本戳:路由层用它决定能不能复用已经压好的响应体 */
|
|
87
|
+
export function modelListStamp(): string {
|
|
88
|
+
return listCache ? String(listCache.at) : "none";
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
/** Read the unmodified SDK catalog through its public modelsPath:null option. */
|
|
@@ -123,7 +140,7 @@ export async function listModelsForPiWeb(): Promise<{ providers: ProviderView[];
|
|
|
123
140
|
async function readStoredAuthTypes(): Promise<Map<string, "api_key" | "oauth">> {
|
|
124
141
|
const out = new Map<string, "api_key" | "oauth">();
|
|
125
142
|
try {
|
|
126
|
-
const parsed =
|
|
143
|
+
const parsed = await readJsonObjectSafe(path.join(getAgentDir(), "auth.json")) as Record<string, any>;
|
|
127
144
|
const entries: Array<[string, any]> = [
|
|
128
145
|
...Object.entries(parsed ?? {}),
|
|
129
146
|
...Object.entries(parsed?.providers ?? {}),
|
|
@@ -165,12 +182,20 @@ async function listModelsUncached(): Promise<{
|
|
|
165
182
|
let resolvedAuthSource: string | undefined;
|
|
166
183
|
let authError: string | undefined;
|
|
167
184
|
if (auth?.configured === true || authFastPath) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
authReady =
|
|
171
|
-
resolvedAuthSource =
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
const cached = authCheckCache.get(pid);
|
|
186
|
+
if (cached && Date.now() - cached.at < AUTH_CHECK_TTL_MS) {
|
|
187
|
+
authReady = cached.ready;
|
|
188
|
+
resolvedAuthSource = cached.source;
|
|
189
|
+
authError = cached.error;
|
|
190
|
+
} else {
|
|
191
|
+
try {
|
|
192
|
+
const checked = await rt.checkAuth(pid, { signal: AbortSignal.timeout(5000) });
|
|
193
|
+
authReady = checked !== undefined;
|
|
194
|
+
resolvedAuthSource = checked?.source;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
authError = error instanceof Error ? error.message : String(error);
|
|
197
|
+
}
|
|
198
|
+
authCheckCache.set(pid, { at: Date.now(), ready: authReady, source: resolvedAuthSource, error: authError });
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
return { pid, p, auth, authFastPath, authReady, resolvedAuthSource, authError };
|
|
@@ -1311,9 +1336,9 @@ export async function writeCustomProviders(content: string, revision?: string):
|
|
|
1311
1336
|
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
1312
1337
|
}
|
|
1313
1338
|
}));
|
|
1314
|
-
|
|
1339
|
+
await refreshModelRuntime();
|
|
1315
1340
|
invalidateModelList();
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1341
|
+
// 会话与共享 runtime 是同一个实例,刷新后目录已是新的;再让活跃会话重载一次,
|
|
1342
|
+
// 把当前模型对象换成新配置(改了 baseUrl / contextWindow 等字段时生效)
|
|
1318
1343
|
await reloadSessionsForCwd();
|
|
1319
1344
|
}
|
package/src/lib/pi-settings.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { invalidateJsonFile, readJsonObject, readJsonObjectFresh } from "./agent-config";
|
|
8
9
|
import { getAgentDir, reloadSettingsManagers } from "./pi";
|
|
9
10
|
import { withExternalSettingsLock, withSettingsWriteLock } from "./settings-write-lock";
|
|
10
11
|
|
|
@@ -30,17 +31,13 @@ const DEFAULTS: PiSettings = {
|
|
|
30
31
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
/** settings.json 走统一缓存:文件没变就不重复读盘解析(每次快照 / 模型目录都会读它) */
|
|
35
|
+
function settingsFile(): string {
|
|
36
|
+
return path.join(getAgentDir(), "settings.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
async function readSettingsFileStrict(): Promise<Record<string, unknown>> {
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
text = await fs.readFile(path.join(getAgentDir(), "settings.json"), "utf8");
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
const parsed: unknown = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
42
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("settings.json must contain a JSON object");
|
|
43
|
-
return parsed as Record<string, unknown>;
|
|
40
|
+
return readJsonObject(settingsFile());
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/** enabledModels 是 pi 的模型范围;没写或写错就不过滤 PiWeb 的选择器 */
|
|
@@ -103,9 +100,9 @@ export function validatePiSettingsPatch(value: unknown): { compaction?: Partial<
|
|
|
103
100
|
|
|
104
101
|
export async function patchPiSettings(value: unknown): Promise<void> {
|
|
105
102
|
const patch = validatePiSettingsPatch(value);
|
|
106
|
-
const file =
|
|
103
|
+
const file = settingsFile();
|
|
107
104
|
await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
|
|
108
|
-
const raw = await
|
|
105
|
+
const raw = await readJsonObjectFresh(file);
|
|
109
106
|
if (patch.compaction) raw.compaction = { ...section(raw, "compaction"), ...patch.compaction };
|
|
110
107
|
if (patch.retry) raw.retry = { ...section(raw, "retry"), ...patch.retry };
|
|
111
108
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
@@ -116,6 +113,7 @@ export async function patchPiSettings(value: unknown): Promise<void> {
|
|
|
116
113
|
} finally {
|
|
117
114
|
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
118
115
|
}
|
|
116
|
+
invalidateJsonFile(file);
|
|
119
117
|
}));
|
|
120
118
|
// 正在运行的会话每次压缩/重试都从 SettingsManager 读:让它们重新读盘,改动立即生效
|
|
121
119
|
await reloadSettingsManagers();
|
package/src/lib/pi.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
+
import { readJsonObjectSync } from "./agent-config";
|
|
7
8
|
import {
|
|
8
9
|
type ModelRuntime as PiModelRuntime,
|
|
9
10
|
type SettingsManager,
|
|
@@ -14,6 +15,9 @@ import {
|
|
|
14
15
|
SessionManager,
|
|
15
16
|
SettingsManager as SettingsManagerClass,
|
|
16
17
|
createAgentSession,
|
|
18
|
+
createCodemodeExtension,
|
|
19
|
+
createMcpExtension,
|
|
20
|
+
createToolSearchExtension,
|
|
17
21
|
getAgentDir,
|
|
18
22
|
hasTrustRequiringProjectResources,
|
|
19
23
|
loadProjectContextFiles,
|
|
@@ -37,19 +41,34 @@ export function getModelRuntime(): Promise<PiModelRuntime> {
|
|
|
37
41
|
return modelRuntimePromise;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
/**
|
|
45
|
+
* models.json 落盘后调用:就地刷新共享的 ModelRuntime,让目录与认证立即生效。
|
|
46
|
+
* 不能换一个新实例 —— 活跃会话、扩展注册的 provider 都挂在旧实例上,换掉后会话里
|
|
47
|
+
* 选新 provider 的模型会报 "No API key",扩展 provider 也会从目录里消失。
|
|
48
|
+
* 只读本地配置不联网;刷新失败才退回整体重建。
|
|
49
|
+
*/
|
|
50
|
+
export async function refreshModelRuntime(): Promise<void> {
|
|
51
|
+
const pending = modelRuntimePromise;
|
|
52
|
+
if (!pending) return;
|
|
53
|
+
try {
|
|
54
|
+
await (await pending).refresh({ allowNetwork: false });
|
|
55
|
+
} catch {
|
|
56
|
+
if (modelRuntimePromise === pending) modelRuntimePromise = null;
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
const settingsCache = new Map<string, SettingsManager>();
|
|
61
|
+
/** 缓存键 → 原始目录。键在 Windows 上是小写的,不能拿它当目录交给 SDK 或算资源指纹 */
|
|
62
|
+
const settingsCwds = new Map<string, string>();
|
|
46
63
|
|
|
47
64
|
// ---------- 项目信任(与 pi 终端同一份 ~/.pi/agent/trust.json) ----------
|
|
48
65
|
|
|
49
|
-
let trustStore: ProjectTrustStore | null = null;
|
|
66
|
+
let trustStore: { agentDir: string; store: ProjectTrustStore } | null = null;
|
|
67
|
+
/** 跟着 agentDir 走:readTrustFile 每次按当前 agentDir 读,写也必须落在同一份 trust.json */
|
|
50
68
|
function getTrustStore(): ProjectTrustStore {
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
const agentDir = getAgentDir();
|
|
70
|
+
if (trustStore?.agentDir !== agentDir) trustStore = { agentDir, store: new ProjectTrustStore(agentDir) };
|
|
71
|
+
return trustStore.store;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export type ProjectTrustState = {
|
|
@@ -61,15 +80,83 @@ export type ProjectTrustState = {
|
|
|
61
80
|
source: "not-required" | "remembered" | "default-always" | "default-never" | "undecided";
|
|
62
81
|
};
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
/**
|
|
84
|
+
* 交给 SDK 信任存储的目录写法:Windows 上取真实大小写。
|
|
85
|
+
*
|
|
86
|
+
* SDK 用 JS 版 realpathSync 规范化 trust.json 的键,它在 Windows 上**保留输入的大小写**
|
|
87
|
+
* (`d:\aiapp\piweb` 原样返回,只有 .native 才给 `D:\AIApp\PiWeb`),再按字符串精确匹配。
|
|
88
|
+
* 同一个目录写进去和查出来的大小写只要不一样,记住的决定就查不到。
|
|
89
|
+
*/
|
|
90
|
+
function trustPath(cwd: string): string {
|
|
91
|
+
const resolved = path.resolve(cwd);
|
|
92
|
+
try {
|
|
93
|
+
return process.platform === "win32" ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
94
|
+
} catch {
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** trust.json 的解析结果按文件修订缓存:resolveProjectTrust 在指纹、资源清单、快照里被频繁调用 */
|
|
100
|
+
interface TrustFile {
|
|
101
|
+
revision: string;
|
|
102
|
+
/** 原样的键 */
|
|
103
|
+
keys: string[];
|
|
104
|
+
/** 折叠后的键(Windows 小写)→ 决定 */
|
|
105
|
+
decisions: Map<string, boolean>;
|
|
106
|
+
}
|
|
107
|
+
let trustFileCache: TrustFile | null = null;
|
|
108
|
+
const foldTrustKey = (value: string) => (process.platform === "win32" ? value.toLowerCase() : value);
|
|
109
|
+
|
|
110
|
+
/** 读不懂(被写了一半、格式不对)返回 null,调用方退回 SDK 加锁读 */
|
|
111
|
+
function readTrustFile(): TrustFile | null {
|
|
112
|
+
const file = path.join(getAgentDir(), "trust.json");
|
|
113
|
+
let revision: string;
|
|
114
|
+
try {
|
|
115
|
+
const stat = fs.statSync(file);
|
|
116
|
+
revision = `${file}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
|
|
117
|
+
} catch {
|
|
118
|
+
return { revision: "missing", keys: [], decisions: new Map() }; // 还没有 trust.json:什么都没记住
|
|
119
|
+
}
|
|
120
|
+
if (trustFileCache?.revision === revision) return trustFileCache;
|
|
65
121
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
122
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")) as unknown;
|
|
123
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
124
|
+
const decisions = new Map<string, boolean>();
|
|
125
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
126
|
+
if (value === true || value === false) decisions.set(foldTrustKey(key), value);
|
|
127
|
+
else if (value !== null) return null; // SDK 认定为坏文件,交给它报错
|
|
128
|
+
}
|
|
129
|
+
trustFileCache = { revision, keys: Object.keys(parsed), decisions };
|
|
130
|
+
return trustFileCache;
|
|
68
131
|
} catch {
|
|
69
|
-
return
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 记住的信任决定:与 SDK 一样从该目录逐级往上找最近的一条,但 Windows 上不分大小写
|
|
138
|
+
* (pi 终端写进去的键是当时 shell 里 cwd 的写法,大小写不一定是真实的)。
|
|
139
|
+
* 不走 SDK 的 get():它每次都同步加文件锁、失败还会自旋等待。读不懂时再退回 SDK。
|
|
140
|
+
*/
|
|
141
|
+
function rememberedProjectTrust(cwd: string): boolean | null {
|
|
142
|
+
const trust = readTrustFile();
|
|
143
|
+
if (!trust) return getTrustStore().get(trustPath(cwd));
|
|
144
|
+
let current = trustPath(cwd);
|
|
145
|
+
for (;;) {
|
|
146
|
+
const decision = trust.decisions.get(foldTrustKey(current));
|
|
147
|
+
if (decision !== undefined) return decision;
|
|
148
|
+
const parent = path.dirname(current);
|
|
149
|
+
if (parent === current) return null;
|
|
150
|
+
current = parent;
|
|
70
151
|
}
|
|
71
152
|
}
|
|
72
153
|
|
|
154
|
+
function readDefaultProjectTrust(): "ask" | "always" | "never" {
|
|
155
|
+
// 同步读且被 getSettingsManager / 资源指纹反复调用:走统一缓存(文件没变就不重新读盘解析)
|
|
156
|
+
const raw = readJsonObjectSync(path.join(getAgentDir(), "settings.json")) as { defaultProjectTrust?: string };
|
|
157
|
+
return raw.defaultProjectTrust === "always" || raw.defaultProjectTrust === "never" ? raw.defaultProjectTrust : "ask";
|
|
158
|
+
}
|
|
159
|
+
|
|
73
160
|
/**
|
|
74
161
|
* 与 pi 终端 main.ts 同一套判定:目录里没有需要信任的项目资源 → 直接信任;
|
|
75
162
|
* 否则看 trust.json 里记住的决定,再看 defaultProjectTrust;「每次询问」在 Web 里没有阻塞式弹窗,
|
|
@@ -84,7 +171,7 @@ export function resolveProjectTrust(cwd: string): ProjectTrustState {
|
|
|
84
171
|
required = false;
|
|
85
172
|
}
|
|
86
173
|
if (!required) return { required: false, trusted: true, source: "not-required" };
|
|
87
|
-
const remembered =
|
|
174
|
+
const remembered = rememberedProjectTrust(resolved);
|
|
88
175
|
if (remembered !== null) return { required: true, trusted: remembered, source: "remembered" };
|
|
89
176
|
const fallback = readDefaultProjectTrust();
|
|
90
177
|
if (fallback === "always") return { required: true, trusted: true, source: "default-always" };
|
|
@@ -98,7 +185,13 @@ export function resolveProjectTrust(cwd: string): ProjectTrustState {
|
|
|
98
185
|
*/
|
|
99
186
|
export function setProjectTrust(cwd: string, decision: boolean | null): ProjectTrustState {
|
|
100
187
|
const resolved = path.resolve(cwd);
|
|
101
|
-
|
|
188
|
+
const target = trustPath(resolved);
|
|
189
|
+
// 同一目录大小写不同的旧键先删掉:查找不分大小写,留着它会顶替(或在「忘记」后复活)这次的决定
|
|
190
|
+
for (const key of readTrustFile()?.keys ?? []) {
|
|
191
|
+
if (key !== target && foldTrustKey(key) === foldTrustKey(target)) getTrustStore().set(key, null);
|
|
192
|
+
}
|
|
193
|
+
getTrustStore().set(target, decision);
|
|
194
|
+
trustFileCache = null; // 同一毫秒内连写两次时修订号可能不变,别读到旧缓存
|
|
102
195
|
const state = resolveProjectTrust(resolved);
|
|
103
196
|
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
104
197
|
const sm = settingsCache.get(key);
|
|
@@ -109,9 +202,11 @@ export function setProjectTrust(cwd: string, decision: boolean | null): ProjectT
|
|
|
109
202
|
/** 按当前 trust.json / defaultProjectTrust 重新计算每个已缓存目录的信任态并就地写入其 SettingsManager */
|
|
110
203
|
export function syncProjectTrust(): void {
|
|
111
204
|
trustStore = null;
|
|
205
|
+
trustFileCache = null;
|
|
112
206
|
for (const [key, sm] of settingsCache) {
|
|
113
207
|
try {
|
|
114
|
-
|
|
208
|
+
// 缓存键在 Windows 上是小写的,拿原始目录去算(见 settingsCwds)
|
|
209
|
+
sm.setProjectTrusted(resolveProjectTrust(settingsCwds.get(key) ?? key).trusted);
|
|
115
210
|
} catch {
|
|
116
211
|
/* ignore */
|
|
117
212
|
}
|
|
@@ -126,11 +221,14 @@ export function getSettingsManager(cwd: string): SettingsManager {
|
|
|
126
221
|
if (!sm) {
|
|
127
222
|
sm = SettingsManagerClass.create(resolved, getAgentDir(), { projectTrusted: resolveProjectTrust(resolved).trusted });
|
|
128
223
|
settingsCache.set(key, sm);
|
|
224
|
+
settingsCwds.set(key, resolved);
|
|
129
225
|
}
|
|
130
226
|
return sm;
|
|
131
227
|
}
|
|
132
228
|
|
|
133
229
|
interface LoaderEntry {
|
|
230
|
+
/** 原始目录(缓存键在 Windows 上是小写的) */
|
|
231
|
+
cwd: string;
|
|
134
232
|
loader: DefaultResourceLoader;
|
|
135
233
|
ready: Promise<void>;
|
|
136
234
|
/** 发现缓存的版本;每个热会话分别追踪自己已应用的版本。 */
|
|
@@ -151,6 +249,18 @@ export function withResourceLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
151
249
|
return run;
|
|
152
250
|
}
|
|
153
251
|
|
|
252
|
+
/**
|
|
253
|
+
* SDK 的内置扩展(MCP、codemode、tool_search):pi 命令行自己会带上,SDK 使用方要自己传,
|
|
254
|
+
* 不传 PiWeb 里就没有 mcp.json 配的服务器、也没有 codemode。名字与 CLI 一致(路径 `builtin:<name>`),
|
|
255
|
+
* settings 里的 `-builtin:mcp` 才能停用它们。llama.cpp 那个内置扩展 SDK 没有公开导出,补不了。
|
|
256
|
+
* 工厂每次加载都新建自己的状态;MCP 要等 session_start 才连服务器,只做清单的发现缓存不会拉起进程。
|
|
257
|
+
*/
|
|
258
|
+
const BUILTIN_EXTENSIONS = [
|
|
259
|
+
{ name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true as const },
|
|
260
|
+
{ name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true as const },
|
|
261
|
+
{ name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true as const },
|
|
262
|
+
];
|
|
263
|
+
|
|
154
264
|
function loaderKey(cwd: string): string {
|
|
155
265
|
const resolved = path.resolve(cwd);
|
|
156
266
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
@@ -160,7 +270,7 @@ function loaderKey(cwd: string): string {
|
|
|
160
270
|
export async function createSessionResourceLoader(cwd: string): Promise<DefaultResourceLoader> {
|
|
161
271
|
const settingsManager = getSettingsManager(cwd);
|
|
162
272
|
settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
|
|
163
|
-
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager });
|
|
273
|
+
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager, extensionFactories: BUILTIN_EXTENSIONS });
|
|
164
274
|
const reload = loader.reload.bind(loader);
|
|
165
275
|
loader.reload = async (options) => {
|
|
166
276
|
settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
|
|
@@ -183,8 +293,8 @@ export function getResourceLoader(cwd: string): DefaultResourceLoader {
|
|
|
183
293
|
const key = loaderKey(cwd);
|
|
184
294
|
let entry = loaderCache.get(key);
|
|
185
295
|
if (!entry) {
|
|
186
|
-
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager: getSettingsManager(cwd) });
|
|
187
|
-
entry = { loader, ready: Promise.resolve(), version: 0, fingerprint: "" };
|
|
296
|
+
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager: getSettingsManager(cwd), extensionFactories: BUILTIN_EXTENSIONS });
|
|
297
|
+
entry = { cwd: path.resolve(cwd), loader, ready: Promise.resolve(), version: 0, fingerprint: "" };
|
|
188
298
|
loaderCache.set(key, entry);
|
|
189
299
|
queueLoaderReload(cwd, entry);
|
|
190
300
|
}
|
|
@@ -315,7 +425,8 @@ export async function reloadAllLoaders(): Promise<string[]> {
|
|
|
315
425
|
const reloaded: string[] = [];
|
|
316
426
|
for (const [key, entry] of loaderCache) {
|
|
317
427
|
try {
|
|
318
|
-
|
|
428
|
+
// 用原始目录:小写键会让资源指纹对不上(每次都多重载一轮),信任查找也会出错
|
|
429
|
+
await queueLoaderReload(entry.cwd, entry);
|
|
319
430
|
reloaded.push(key);
|
|
320
431
|
} catch {
|
|
321
432
|
/* 单个失败不影响其他 */
|
|
@@ -362,6 +473,22 @@ export function openSessionManager(sessionPath: string): SessionManager {
|
|
|
362
473
|
/** 工具预设 → pi 工具白名单 */
|
|
363
474
|
export const TOOL_PRESETS: Record<"readonly" | "standard" | "full", string[]> = {
|
|
364
475
|
readonly: ["read", "grep", "find", "ls"],
|
|
365
|
-
standard: ["read", "grep", "find", "ls", "bash", "edit", "write"],
|
|
476
|
+
standard: ["read", "grep", "find", "ls", "bash", "edit", "write", "codemode", "tool_search"],
|
|
366
477
|
full: [], // 空 = 不限制(全部可用工具)
|
|
367
478
|
};
|
|
479
|
+
|
|
480
|
+
const MCP_RESOURCE_TOOLS = new Set(["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"]);
|
|
481
|
+
|
|
482
|
+
/** 与 SDK isMcpToolName 一致:MCP 服务器工具 `mcp__<server>__<tool>` 与三个资源工具 */
|
|
483
|
+
export function isMcpToolName(name: string): boolean {
|
|
484
|
+
return name.startsWith("mcp__") || MCP_RESOURCE_TOOLS.has(name);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 档位是否放行某个工具(只是上限,开不开由 SDK / 扩展决定)。
|
|
489
|
+
* standard 另外放行 MCP 工具:服务器是用户自己在 mcp.json 里配的;readonly 不放,MCP 工具能写盘、调外部服务。
|
|
490
|
+
*/
|
|
491
|
+
export function presetAllowsTool(preset: keyof typeof TOOL_PRESETS, name: string): boolean {
|
|
492
|
+
if (preset === "full") return true;
|
|
493
|
+
return TOOL_PRESETS[preset].includes(name) || (preset === "standard" && isMcpToolName(name));
|
|
494
|
+
}
|