pi-webapp 0.1.5 → 0.1.7
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/README.md +16 -3
- package/README.zh-CN.md +14 -3
- package/THIRD_PARTY_LICENSE.txt +24 -0
- package/bin/pi-webapp.mjs +93 -5
- package/dist/extension.js +9 -6
- package/package.json +28 -5
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/app.js +504 -55
- package/web/dist/assets/asciidoc.js +1 -0
- package/web/dist/assets/bat.js +1 -0
- package/web/dist/assets/bibtex.js +1 -0
- package/web/dist/assets/c.js +1 -0
- package/web/dist/assets/clojure.js +1 -0
- package/web/dist/assets/cmake.js +1 -0
- package/web/dist/assets/cpp.js +1 -0
- package/web/dist/assets/csharp.js +1 -0
- package/web/dist/assets/css.js +1 -0
- package/web/dist/assets/csv.js +1 -0
- package/web/dist/assets/dart.js +1 -0
- package/web/dist/assets/diff.js +1 -0
- package/web/dist/assets/dotenv.js +1 -0
- package/web/dist/assets/elixir.js +1 -0
- package/web/dist/assets/erlang.js +1 -0
- package/web/dist/assets/fish.js +1 -0
- package/web/dist/assets/fsharp.js +1 -0
- package/web/dist/assets/go.js +1 -0
- package/web/dist/assets/graphql.js +1 -0
- package/web/dist/assets/groovy.js +1 -0
- package/web/dist/assets/haskell.js +1 -0
- package/web/dist/assets/hcl.js +1 -0
- package/web/dist/assets/html-derivative.js +1 -0
- package/web/dist/assets/html.js +1 -0
- package/web/dist/assets/http.js +1 -0
- package/web/dist/assets/ini.js +1 -0
- package/web/dist/assets/java.js +1 -0
- package/web/dist/assets/javascript.js +1 -0
- package/web/dist/assets/julia.js +1 -0
- package/web/dist/assets/kotlin.js +1 -0
- package/web/dist/assets/latex.js +1 -0
- package/web/dist/assets/less.js +1 -0
- package/web/dist/assets/log.js +1 -0
- package/web/dist/assets/lua.js +1 -0
- package/web/dist/assets/make.js +1 -0
- package/web/dist/assets/markdown.js +1 -0
- package/web/dist/assets/mdx.js +1 -0
- package/web/dist/assets/nix.js +1 -0
- package/web/dist/assets/perl.js +1 -0
- package/web/dist/assets/php.js +1 -0
- package/web/dist/assets/powershell.js +1 -0
- package/web/dist/assets/proto.js +1 -0
- package/web/dist/assets/python.js +1 -0
- package/web/dist/assets/r.js +1 -0
- package/web/dist/assets/rst.js +1 -0
- package/web/dist/assets/ruby.js +1 -0
- package/web/dist/assets/rust.js +1 -0
- package/web/dist/assets/scala.js +1 -0
- package/web/dist/assets/scss.js +1 -0
- package/web/dist/assets/sql.js +1 -0
- package/web/dist/assets/style.css +1 -1
- package/web/dist/assets/svelte.js +1 -0
- package/web/dist/assets/swift.js +1 -0
- package/web/dist/assets/system-verilog.js +1 -0
- package/web/dist/assets/toml.js +1 -0
- package/web/dist/assets/vb.js +1 -0
- package/web/dist/assets/verilog.js +1 -0
- package/web/dist/assets/vue.js +1 -0
- package/web/dist/assets/xml.js +1 -0
- package/web/dist/assets/yaml.js +1 -0
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ npx pi-webapp
|
|
|
16
16
|
|
|
17
17
|
This starts Pi in the background and opens the default browser on a local graphical desktop, leaving the terminal free. If Pi is missing, the launcher installs the official `@earendil-works/pi-coding-agent` package with npm. An existing Pi installation must be version 0.87.1 or later. The launcher does not replace an older installation automatically. Running the launcher again reopens the existing server. Remote or headless sessions print the address instead; set `PI_WEBAPP_AUTO_OPEN=0` to disable automatic opening.
|
|
18
18
|
|
|
19
|
+
In Settings → General → App updates, the web page checks npm for a newer `pi-webapp` release. When started by the background launcher from an npm package, **Update and restart** installs the verified release under Pi's `pi-web/releases/` data directory, restarts the server, and reloads the same browser address. Wait for any active reply to finish first. If the new version cannot start, the launcher restores the previous service. This works for both `npx` and global-command launches; later launches select the managed release automatically. It does not change the npm global installation. Source checkouts and `/web` pages opened from an existing Pi terminal show version information but cannot restart from the web page. If the service does not return, run `pi-webapp status` and check `~/.pi/agent/pi-web/launcher.log`.
|
|
20
|
+
|
|
19
21
|
Use `npx pi-webapp status` to show the address and `npx pi-webapp stop` to stop the background service. After a global install, use `pi-webapp status` and `pi-webapp stop`.
|
|
20
22
|
|
|
21
23
|
You can also install the launcher globally:
|
|
@@ -37,12 +39,12 @@ In Pi, enter `/web` to open the interface in your default browser. Pi also print
|
|
|
37
39
|
## Features
|
|
38
40
|
|
|
39
41
|
- View the current conversation with Markdown, streaming output, expandable reasoning and tool calls, token usage, and a turn-by-turn trace.
|
|
40
|
-
- Send messages,
|
|
42
|
+
- Send messages, pause and continue a run, start a new Pi session, and change the current model or thinking level. The bottom-right control pauses an active run; an empty composer offers Continue after pausing, including after refreshing or reopening the session. Continue uses an internal control action without adding a user message; the model makes a new request based on existing progress, and plan mode still requires fresh approval.
|
|
41
43
|
- Browse workspaces and saved sessions. Add a workspace with the native directory picker when available, or use the built-in directory browser over SSH.
|
|
42
44
|
- Attach up to 20 files of 20 MB each by selecting, dropping, or pasting them. Images show previews and are also sent as Pi image content.
|
|
43
45
|
- Manage appearance, Pi packages, extensions, skills, and models from the settings panel. The Models page can add models, edit display names, context windows, output limits, and input types, and restore built-in defaults; changes are saved to Pi's `models.json`. Browser sign-in opens the provider's authorization page automatically; manual code entry remains available when a callback cannot reach Pi. Stored provider credentials can also be removed there. Credentials stay in Pi's local `auth.json` and are never returned to the browser.
|
|
44
46
|
|
|
45
|
-
If you enter `/web` manually in a Pi terminal, that terminal session stays synchronized with the browser. With the launcher, Pi runs in the background. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They
|
|
47
|
+
If you enter `/web` manually in a Pi terminal, that terminal session stays synchronized with the browser. With the launcher, Pi runs in the background. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They load enabled Web plugin Pi entries explicitly while keeping other Pi extensions isolated from the parent bridge.
|
|
46
48
|
|
|
47
49
|
On startup, the app discovers workspaces from Pi's saved sessions, so conversations from other directories appear in the sidebar. Workspace records and attachments stay under Pi's existing `pi-web/` data directory. Removing a workspace from the sidebar does not delete its files or sessions; you can add it again manually.
|
|
48
50
|
|
|
@@ -60,13 +62,24 @@ npm run stop
|
|
|
60
62
|
|
|
61
63
|
`npm run start` builds the project, starts Pi in the background, and opens the web interface. After editing the extension, run `npm run stop` followed by `npm run start`. If you develop with `pi -e .`, enter `/reload` and `/web` in Pi after rebuilding. Refreshing an old browser page alone does not replace the running bridge.
|
|
62
64
|
|
|
65
|
+
For local Web plugin development with the sibling `pi-extensions` workspace, follow its `README.md`. Set `PI_WEBAPP_PLUGIN_DEV_ROOTS=../pi-extensions/packages/plan-mode` when starting this app; browser asset changes refresh automatically and Pi entry changes trigger a safe reload when the agent is idle.
|
|
66
|
+
|
|
63
67
|
The source lives in `extension/`, `shared/`, and `web/src/`. The npm package contains only the bundled, minified JavaScript extension, built web assets, cover image, and the English and Chinese READMEs. No TypeScript source or source maps are published. Minification makes the shipped code harder to read; it does not encrypt JavaScript.
|
|
64
68
|
|
|
65
69
|
## Publish
|
|
66
70
|
|
|
71
|
+
CI resolves the published `@chengzhiyi/pi-web-protocol` version before `npm ci`.
|
|
72
|
+
Until the first protocol publication, verification builds a pinned `pi-extensions`
|
|
73
|
+
commit in an ignored `.ci/` checkout and uses that local package. Checks, tests,
|
|
74
|
+
and builds still run; the Actions summary explains why host publication is waiting.
|
|
75
|
+
Release builds always require the published npm dependency. Once it exists, the
|
|
76
|
+
next workflow uses its exact version and the release commit records the lockfile.
|
|
77
|
+
Use Actions' manual run after initializing npm; protocol releases do not trigger
|
|
78
|
+
this repository automatically.
|
|
79
|
+
|
|
67
80
|
`npm publish` runs the checks, tests, and production build. The `pi-package` keyword makes the published npm package eligible for the [Pi package catalog](https://pi.dev/packages); catalog updates may lag behind npm.
|
|
68
81
|
|
|
69
|
-
GitHub Actions runs `check`, `test`, and `build` on pull requests to `main`. A push to `main` repeats those checks
|
|
82
|
+
GitHub Actions runs `check`, `test`, and `build` on pull requests to `main`. A push to `main` repeats those checks, chooses the next patch version from the source version and npm `latest`, then commits the updated `package.json` and `package-lock.json` to `main` before publishing. The release commit uses the workflow's `GITHUB_TOKEN` and does not start another workflow run. To start a new minor or major series, raise the version in both package files in the pull request. Each release also updates the package's `pi.image` URL to its own version. The repository must allow the workflow to push to `main` for publishing to proceed.
|
|
70
83
|
|
|
71
84
|
Before the first automated release, configure npm Trusted Publishing for the `pi-webapp` package: GitHub owner `chengzhiyi`, repository `pi-webapp`, workflow filename `ci.yml`, no environment, and allow direct `npm publish`. This workflow uses GitHub's OIDC identity and does not need an npm token. Keep the package's repository URL in `package.json` aligned with the GitHub repository.
|
|
72
85
|
|
package/README.zh-CN.md
CHANGED
|
@@ -16,6 +16,8 @@ npx pi-webapp
|
|
|
16
16
|
|
|
17
17
|
该命令会在后台启动 Pi,并在本地图形桌面用默认浏览器打开网页,终端可继续使用。若未找到 Pi,会自动通过 npm 安装官方 `@earendil-works/pi-coding-agent`。已有 Pi 需要 0.87.1 或更新版本;启动器不会自动覆盖旧版本。再次运行启动命令会重新打开已有服务。远程或无图形界面时只显示地址;设置 `PI_WEBAPP_AUTO_OPEN=0` 可关闭自动打开。
|
|
18
18
|
|
|
19
|
+
网页的「设置 → 通用设置 → 应用更新」会检查 npm 上的 `pi-webapp` 新版本。通过 npm 包的后台启动器运行时,点击「升级并重启」会先将新版安装并校验到 Pi 数据目录的 `pi-web/releases/`,然后重启服务,网页会在原地址重新加载。请先等待当前回复结束。若新版无法启动,启动器会恢复旧版服务。`npx` 和全局命令启动均可使用;后续启动会自动选用已安装的新版。此操作不会改动 npm 全局安装。源码目录运行和已有 Pi 终端中的 `/web` 页面可以查看版本,但不能从网页重启。若服务未恢复,可运行 `pi-webapp status` 并查看 `~/.pi/agent/pi-web/launcher.log`。
|
|
20
|
+
|
|
19
21
|
可用 `npx pi-webapp status` 查看地址,用 `npx pi-webapp stop` 停止后台服务。全局安装后可直接运行 `pi-webapp status` 和 `pi-webapp stop`。
|
|
20
22
|
|
|
21
23
|
也可以将启动器安装为全局命令:
|
|
@@ -37,12 +39,12 @@ pi
|
|
|
37
39
|
## 功能
|
|
38
40
|
|
|
39
41
|
- 查看当前对话,包括 Markdown 内容、流式输出、可展开的推理过程与工具调用、Token 用量,以及逐轮执行轨迹。
|
|
40
|
-
-
|
|
42
|
+
- 发送消息、暂停与继续运行、启动新的 Pi 会话,以及切换当前模型或思考级别。运行时右下角显示暂停;暂停后清空输入框即可继续,刷新或重新打开会话后仍可恢复。继续通过内部控制动作恢复原任务,不新增用户消息;模型会基于已有进度发起新请求,计划模式仍需重新审批。
|
|
41
43
|
- 浏览工作区和已保存的会话。环境支持时可通过系统目录选择器添加工作区;通过 SSH 使用时,也可使用内置目录浏览器。
|
|
42
44
|
- 通过选择、拖放或粘贴附加文件,最多 20 个,每个不超过 20 MB。图片会显示预览,并作为 Pi 图片内容发送。
|
|
43
45
|
- 在设置面板中管理外观、Pi 软件包、扩展、技能和模型。模型页可添加模型、编辑显示名称、上下文窗口、最大输出 token 数和输入类型,或恢复内置模型的默认参数;配置写入 Pi 的 `models.json`。网页登录会自动打开提供方授权页;当回调无法到达 Pi 时,仍可手动粘贴授权码。已保存的提供方认证可在这里移除。凭据保留在 Pi 本地的 `auth.json`,不会返回给浏览器。
|
|
44
46
|
|
|
45
|
-
如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK
|
|
47
|
+
如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;已启用的 Web 插件 Pi 入口会显式加载,其他普通 Pi 扩展仍与主会话隔离。
|
|
46
48
|
|
|
47
49
|
启动时会从 Pi 已保存的会话中发现工作区,因此先前在其他目录使用 Pi 的会话也会出现在侧边栏。工作区记录和附件继续存放在 Pi 现有的 `pi-web/` 数据目录下。从侧边栏移除工作区不会删除其文件或会话;移除后如需再次显示,可以手动添加。
|
|
48
50
|
|
|
@@ -60,13 +62,22 @@ npm run stop
|
|
|
60
62
|
|
|
61
63
|
`npm run start` 会构建项目、在后台启动 Pi 并打开 Web 界面。修改扩展后,运行 `npm run stop`,再运行 `npm run start`;如果使用 `pi -e .` 开发,则在 Pi 中依次输入 `/reload` 和 `/web`。仅刷新旧的浏览器页面不会替换正在运行的连接桥接代码。
|
|
62
64
|
|
|
65
|
+
与同级 `pi-extensions` 联调时,先在该目录运行 `pnpm install && pnpm build`,再运行 `pnpm dev`;在本项目设置 `PI_WEBAPP_PLUGIN_DEV_ROOTS=../pi-extensions/packages/plan-mode` 后执行 `npm run start`。浏览器资源变化后页面会刷新,Pi 入口变化后会等待代理空闲并重载扩展。完整步骤见 `pi-extensions/README.md`。
|
|
66
|
+
|
|
63
67
|
源代码位于 `extension/`、`shared/` 和 `web/src/`。npm 软件包仅包含打包并压缩后的 JavaScript 扩展、构建后的 Web 资源、封面图片,以及英文和中文 README;不会发布 TypeScript 源码或 source map。代码压缩会增加阅读发布版本代码的难度,但不会对 JavaScript 加密。
|
|
64
68
|
|
|
65
69
|
## 发布
|
|
66
70
|
|
|
71
|
+
CI 会先从 npm 解析 `@chengzhiyi/pi-web-protocol` 的已发布版本,再运行 `npm ci`。
|
|
72
|
+
首次发布协议前,校验任务会将固定提交的 `pi-extensions` 检出到被忽略的 `.ci/`
|
|
73
|
+
目录,构建协议并使用本地包,完整执行检查、测试与构建;Actions 摘要会说明
|
|
74
|
+
宿主发布仍在等待协议初始化。正式发布始终要求准确的 npm 依赖,并将清单和
|
|
75
|
+
锁文件变化同步回仓库。协议发布后可以在 Actions 手动运行工作流;协议发布
|
|
76
|
+
不会主动触发这个仓库。
|
|
77
|
+
|
|
67
78
|
`npm publish` 会运行检查、测试和生产构建。`pi-package` 关键词使发布到 npm 的软件包有资格进入 [Pi 软件包目录](https://pi.dev/packages);目录更新可能晚于 npm。
|
|
68
79
|
|
|
69
|
-
GitHub Actions 会在针对 `main` 的拉取请求中运行 `check`、`test` 和 `build`。代码推送到 `main`
|
|
80
|
+
GitHub Actions 会在针对 `main` 的拉取请求中运行 `check`、`test` 和 `build`。代码推送到 `main` 后会再次运行这些检查,根据源码版本和 npm `latest` 版本确定下一个补丁版本,先将更新后的 `package.json` 和 `package-lock.json` 提交回 `main`,再发布到 npm。版本同步提交使用工作流的 `GITHUB_TOKEN`,不会再次触发工作流。如需开始新的次版本或主版本,请在拉取请求中同步提高两个包文件的版本。每次发布也会将包内 `pi.image` 的地址更新为该版本。仓库必须允许工作流推送到 `main`,发布才能继续。
|
|
70
81
|
|
|
71
82
|
首次自动发布前,请在 npm 的 `pi-webapp` 包设置中配置 Trusted Publishing:GitHub 所有者为 `chengzhiyi`,仓库为 `pi-webapp`,工作流文件名为 `ci.yml`,不设置环境,并允许直接执行 `npm publish`。工作流通过 GitHub OIDC 认证,无需 npm token。`package.json` 中的仓库地址应与 GitHub 仓库保持一致。
|
|
72
83
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
DSH presentation components are derived from DeepSeek Harness, commit
|
|
2
|
+
477b4f420553e8a52c2fbccc464d7561b239c443.
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 DeepSeek
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
package/bin/pi-webapp.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn as spawnNode } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { closeSync, existsSync, openSync } from "node:fs";
|
|
4
|
+
import { closeSync, existsSync, openSync, watch } from "node:fs";
|
|
5
5
|
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import spawn from "cross-spawn";
|
|
10
10
|
import { openWebPage } from "./open-web-page.mjs";
|
|
@@ -15,6 +15,8 @@ const extension = fileURLToPath(new URL("../dist/extension.js", import.meta.url)
|
|
|
15
15
|
const agentDir = resolve((process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")).replace(/^~(?=$|[\\/])/, homedir()));
|
|
16
16
|
const statePath = join(agentDir, "pi-web", "launcher.json");
|
|
17
17
|
const stopPath = join(agentDir, "pi-web", "launcher.stop");
|
|
18
|
+
const restartPath = join(agentDir, "pi-web", "launcher.restart");
|
|
19
|
+
const updatePath = join(agentDir, "pi-web", "update.json");
|
|
18
20
|
const logPath = join(agentDir, "pi-web", "launcher.log");
|
|
19
21
|
|
|
20
22
|
function versionParts(value) {
|
|
@@ -111,8 +113,9 @@ async function serve(pi, args) {
|
|
|
111
113
|
return;
|
|
112
114
|
} finally { await lock?.close().catch(() => {}); }
|
|
113
115
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
+
const pluginRoots = (process.env.PI_WEBAPP_PLUGIN_DEV_ROOTS ?? "").split(delimiter).filter(Boolean).map((root) => resolve(root));
|
|
117
|
+
const child = spawn(pi, ["--mode", "rpc", "-e", extension, ...pluginRoots.flatMap((root) => ["-e", root]), ...args], {
|
|
118
|
+
cwd: process.cwd(), env: { ...process.env, PI_WEBAPP_AUTO_OPEN: "0", PI_WEBAPP_LAUNCHER_NONCE: nonce }, stdio: ["pipe", "pipe", "pipe"],
|
|
116
119
|
});
|
|
117
120
|
child.stderr?.pipe(process.stderr, { end: false });
|
|
118
121
|
let ready = false;
|
|
@@ -121,8 +124,14 @@ async function serve(pi, args) {
|
|
|
121
124
|
let closed = false;
|
|
122
125
|
let stateWrite = Promise.resolve();
|
|
123
126
|
let stopping = false;
|
|
127
|
+
let restartRequest = null;
|
|
128
|
+
let runningUrl = null;
|
|
124
129
|
let buffer = "";
|
|
125
130
|
let killTimer;
|
|
131
|
+
const devWatchers = [];
|
|
132
|
+
let devReloadTimer;
|
|
133
|
+
let devReloadCount = 0;
|
|
134
|
+
let devReloadCommand;
|
|
126
135
|
const shutdown = () => {
|
|
127
136
|
if (stopping) return;
|
|
128
137
|
stopping = true;
|
|
@@ -140,11 +149,20 @@ async function serve(pi, args) {
|
|
|
140
149
|
const stopTimer = setInterval(async () => {
|
|
141
150
|
try { if ((await readFile(stopPath, "utf8")).trim() === nonce) shutdown(); }
|
|
142
151
|
catch { /* No stop request. */ }
|
|
152
|
+
if (stopping) return;
|
|
153
|
+
try {
|
|
154
|
+
const request = JSON.parse(await readFile(restartPath, "utf8"));
|
|
155
|
+
const prepared = JSON.parse(await readFile(updatePath, "utf8"));
|
|
156
|
+
if (request.nonce === nonce && request.launcher === prepared.launcher && existsSync(request.launcher)) {
|
|
157
|
+
restartRequest = request;
|
|
158
|
+
shutdown();
|
|
159
|
+
}
|
|
160
|
+
} catch { /* No valid restart request. */ }
|
|
143
161
|
}, 400);
|
|
144
162
|
process.on("SIGTERM", shutdown);
|
|
145
163
|
process.on("SIGINT", shutdown);
|
|
146
164
|
child.stdout?.on("data", (chunk) => {
|
|
147
|
-
if (
|
|
165
|
+
if (failed) return;
|
|
148
166
|
buffer += chunk.toString("utf8");
|
|
149
167
|
if (buffer.length > 8 * 1024 * 1024) { fail("Pi RPC 输出超出预期大小"); return; }
|
|
150
168
|
for (let index; (index = buffer.indexOf("\n")) !== -1;) {
|
|
@@ -152,6 +170,11 @@ async function serve(pi, args) {
|
|
|
152
170
|
buffer = buffer.slice(index + 1);
|
|
153
171
|
let record;
|
|
154
172
|
try { record = JSON.parse(line); } catch { continue; }
|
|
173
|
+
if (record?.type === "response" && /^dev-reload-\d+$/.test(record.id ?? "")) {
|
|
174
|
+
console.error(`pi-webapp: ${record.id} /${devReloadCommand ?? "web-dev-reload"}: ${record.success === true ? "reloaded" : `failed: ${record.error ?? "Unknown RPC error"}`}`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ready || opening) continue;
|
|
155
178
|
if (record?.type === "response" && record.id === "list-commands") {
|
|
156
179
|
if (record.success !== true || !Array.isArray(record.data?.commands)) {
|
|
157
180
|
fail("无法读取 Pi 扩展命令列表");
|
|
@@ -163,6 +186,8 @@ async function serve(pi, args) {
|
|
|
163
186
|
fail("Pi 没有加载当前项目的 Web 扩展");
|
|
164
187
|
return;
|
|
165
188
|
}
|
|
189
|
+
devReloadCommand = record.data.commands.find((item) => item?.source === "extension"
|
|
190
|
+
&& item.sourceInfo?.path === extension && /^web-dev-reload(?::\d+)?$/.test(item.name))?.name;
|
|
166
191
|
child.stdin?.write(JSON.stringify({ id: "open-web", type: "prompt", message: `/${command.name}` }) + "\n");
|
|
167
192
|
continue;
|
|
168
193
|
}
|
|
@@ -175,6 +200,7 @@ async function serve(pi, args) {
|
|
|
175
200
|
try { url = new URL(record.message.slice("pi-webapp: ".length)); }
|
|
176
201
|
catch { continue; }
|
|
177
202
|
if (url.hostname !== "127.0.0.1" || !url.hash) continue;
|
|
203
|
+
runningUrl = url.href;
|
|
178
204
|
opening = true;
|
|
179
205
|
stateWrite = saveState({ pid: process.pid, piPid: child.pid, nonce, cwd: process.cwd(), url: url.href })
|
|
180
206
|
.then(() => {
|
|
@@ -182,6 +208,19 @@ async function serve(pi, args) {
|
|
|
182
208
|
ready = true;
|
|
183
209
|
clearTimeout(startupTimer);
|
|
184
210
|
notifyParent({ type: "ready", pid: process.pid, url: url.href });
|
|
211
|
+
for (const root of pluginRoots) {
|
|
212
|
+
try {
|
|
213
|
+
devWatchers.push(watch(join(root, "dist"), (_event, file) => {
|
|
214
|
+
if (String(file ?? "") !== "extension.js") return;
|
|
215
|
+
clearTimeout(devReloadTimer);
|
|
216
|
+
devReloadTimer = setTimeout(() => {
|
|
217
|
+
if (closed || stopping) return;
|
|
218
|
+
if (!devReloadCommand) { console.error("pi-webapp: development reload unavailable: local extension command not found"); return; }
|
|
219
|
+
child.stdin?.write(JSON.stringify({ id: `dev-reload-${++devReloadCount}`, type: "prompt", message: `/${devReloadCommand}` }) + "\n");
|
|
220
|
+
}, 250);
|
|
221
|
+
}));
|
|
222
|
+
} catch (error) { console.error(`pi-webapp: cannot watch plugin ${root}: ${error}`); }
|
|
223
|
+
}
|
|
185
224
|
})
|
|
186
225
|
.catch((error) => fail(`无法保存启动状态:${error.message}`));
|
|
187
226
|
return;
|
|
@@ -199,6 +238,8 @@ async function serve(pi, args) {
|
|
|
199
238
|
});
|
|
200
239
|
await stateWrite;
|
|
201
240
|
clearTimeout(startupTimer);
|
|
241
|
+
clearTimeout(devReloadTimer);
|
|
242
|
+
for (const watcher of devWatchers) watcher.close();
|
|
202
243
|
clearInterval(stopTimer);
|
|
203
244
|
clearTimeout(killTimer);
|
|
204
245
|
try { if ((await readFile(stopPath, "utf8")).trim() === nonce) await unlink(stopPath); } catch { /* Already removed. */ }
|
|
@@ -206,6 +247,42 @@ async function serve(pi, args) {
|
|
|
206
247
|
try { if (JSON.parse(await readFile(statePath, "utf8")).nonce === nonce) await unlink(statePath); }
|
|
207
248
|
catch { /* Already removed. */ }
|
|
208
249
|
}
|
|
250
|
+
if (restartRequest && runningUrl) {
|
|
251
|
+
await unlink(restartPath).catch(() => {});
|
|
252
|
+
const oldUrl = new URL(runningUrl);
|
|
253
|
+
const fd = openSync(logPath, "a", 0o600);
|
|
254
|
+
const restartEnv = { ...process.env, PI_WEBAPP_AUTO_OPEN: "0", PI_WEBAPP_RESTART_PORT: oldUrl.port, PI_WEBAPP_RESTART_TOKEN: oldUrl.hash.slice(1) };
|
|
255
|
+
try {
|
|
256
|
+
const runLauncher = (path) => new Promise((resolveExit) => {
|
|
257
|
+
const next = spawnNode(process.execPath, [path, "start", ...args], { cwd: process.cwd(), env: restartEnv, stdio: ["ignore", fd, fd] });
|
|
258
|
+
next.once("error", () => resolveExit(1));
|
|
259
|
+
next.once("exit", (code) => resolveExit(code ?? 1));
|
|
260
|
+
});
|
|
261
|
+
if (await runLauncher(restartRequest.launcher) !== 0) {
|
|
262
|
+
await clearManagedLauncher(restartRequest.launcher);
|
|
263
|
+
console.error("pi-webapp: 新版启动失败,正在恢复旧版服务");
|
|
264
|
+
if (await runLauncher(launcher) !== 0) console.error(`pi-webapp: 旧版恢复失败。日志:${logPath}`);
|
|
265
|
+
}
|
|
266
|
+
} finally { closeSync(fd); }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function clearManagedLauncher(path) {
|
|
271
|
+
try {
|
|
272
|
+
if (JSON.parse(await readFile(updatePath, "utf8")).launcher === path) await unlink(updatePath);
|
|
273
|
+
} catch { /* No matching managed release. */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function managedLauncher() {
|
|
277
|
+
try {
|
|
278
|
+
if (existsSync(fileURLToPath(new URL("../.git", import.meta.url)))) return null;
|
|
279
|
+
const update = JSON.parse(await readFile(updatePath, "utf8"));
|
|
280
|
+
const current = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
281
|
+
if (!/^\d+\.\d+\.\d+$/.test(update.version) || !/^\d+\.\d+\.\d+$/.test(current.version)) return null;
|
|
282
|
+
if (!compatible(versionParts(update.version), versionParts(current.version))) return null;
|
|
283
|
+
if (update.version === current.version || !existsSync(update.launcher)) return null;
|
|
284
|
+
return update.launcher;
|
|
285
|
+
} catch { return null; }
|
|
209
286
|
}
|
|
210
287
|
|
|
211
288
|
async function start(args) {
|
|
@@ -215,6 +292,17 @@ async function start(args) {
|
|
|
215
292
|
if (existing.url) await openPageIfLocal(existing.url);
|
|
216
293
|
return;
|
|
217
294
|
}
|
|
295
|
+
const managed = await managedLauncher();
|
|
296
|
+
if (managed) {
|
|
297
|
+
const next = spawnNode(process.execPath, [managed, "start", ...args], { cwd: process.cwd(), env: process.env, stdio: "inherit" });
|
|
298
|
+
const code = await new Promise((resolveExit, rejectExit) => { next.once("error", rejectExit); next.once("exit", (status) => resolveExit(status)); });
|
|
299
|
+
if (code !== 0) {
|
|
300
|
+
await clearManagedLauncher(managed);
|
|
301
|
+
console.error(`pi-webapp: 新版启动失败(代码 ${code ?? "未知"}),正在恢复当前安装`);
|
|
302
|
+
await start(args);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
218
306
|
const nodeVersion = versionParts(process.versions.node);
|
|
219
307
|
if (!nodeVersion || !compatible(nodeVersion, [22, 19, 0])) throw new Error("需要 Node.js 22.19 或更新版本。");
|
|
220
308
|
if (!existsSync(extension)) throw new Error("找不到构建后的扩展。请先运行 npm run build。");
|