pisper 0.6.14 → 0.6.15
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/bin/pisper.mjs +5 -1
- package/lib/executor-node.mjs +590 -0
- package/lib/npm-update.mjs +2 -0
- package/package.json +5 -5
- package/web/.vite/manifest.json +852 -856
- package/web/assets/{AppDialog-DrhCZm1r.js → AppDialog-CgJEhcYw.js} +1 -1
- package/web/assets/{AppOverlays-CxgReqGn.js → AppOverlays-BBlYyb_U.js} +1 -1
- package/web/assets/{AppSelect-BGVBQRP1.js → AppSelect-BD6RAaJ5.js} +1 -1
- package/web/assets/{AssetsPage-CKK-byQY.js → AssetsPage-D3F4T0_j.js} +1 -1
- package/web/assets/BoardPage-CO7nU2Mw.js +3 -0
- package/web/assets/BotAvatar-CNL-APwP.js +1 -0
- package/web/assets/ChannelsPage-CsRbj9St.js +1 -0
- package/web/assets/{ChatAppearancePage-BhqRLf-2.js → ChatAppearancePage-Sc1V75I3.js} +1 -1
- package/web/assets/{ChatHistoryPage-DBsNyGYj.js → ChatHistoryPage-BT0mIaDN.js} +1 -1
- package/web/assets/ChatPage-C-hswZf9.js +553 -0
- package/web/assets/ConfigPage-DH2d8YGn.js +5 -0
- package/web/assets/{DesktopWindowControls-DCtIkpTs.js → DesktopWindowControls-Bo5vvIFH.js} +1 -1
- package/web/assets/{FeaturePage-Bkt1wWVg.js → FeaturePage-B3wCvGpC.js} +1 -1
- package/web/assets/{FileAttachmentPreview-Cnmi_2U-.js → FileAttachmentPreview-D8YqjbeC.js} +1 -1
- package/web/assets/{FloatingWidgets-BIJSFz34.js → FloatingWidgets-Dz_T7XWC.js} +1 -1
- package/web/assets/FocusRuntimeControls-hYDoXwg7.js +1 -0
- package/web/assets/{GitDiffViewer-rjt5_KOt.js → GitDiffViewer-CBju_Sj4.js} +3 -3
- package/web/assets/ManageProjectsDialog-Dtb2K7BK.js +1 -0
- package/web/assets/{MarkdownMessage-yl-PSRAz.js → MarkdownMessage-BDztlYIy.js} +1 -1
- package/web/assets/{McpPage-D6GyZCpt.js → McpPage-DQ6AILOH.js} +1 -1
- package/web/assets/MemoryPage-CK4evs_P.js +1 -0
- package/web/assets/{MobileNavigation-DcBH2I9Y.js → MobileNavigation-BICCops3.js} +1 -1
- package/web/assets/MobileViewportStabilizer-uKI4Zp7f.js +1 -0
- package/web/assets/{MorePage-BBfiQbiP.js → MorePage-DuML7bhr.js} +1 -1
- package/web/assets/NewBotDialog--2DSe9Sy.js +1 -0
- package/web/assets/{NewFolderDialog-BinpbTs8.js → NewFolderDialog-S-Qzjzfu.js} +1 -1
- package/web/assets/{PageHeader-D_J-19ni.js → PageHeader-B78dHJed.js} +1 -1
- package/web/assets/{PhoneDrawer-CwxDkWZQ.js → PhoneDrawer-DHRBi4XA.js} +1 -1
- package/web/assets/PhoneMenuButton-BjDV-dPl.js +1 -0
- package/web/assets/PhoneTopBar-BuyzXL4Z.js +1 -0
- package/web/assets/PluginsPage-Cd9bu2AS.js +1 -0
- package/web/assets/{SafeModePrompt-CLyt-z14.js → SafeModePrompt-BnoccbWI.js} +1 -1
- package/web/assets/SchedulesPage-DUhUUQP5.js +1 -0
- package/web/assets/SessionBrowserPane-B3weAiaB.js +2 -0
- package/web/assets/SessionContextPanel-BtxUlk9m.js +2 -0
- package/web/assets/SessionHome-ByiFmH18.js +1 -0
- package/web/assets/SessionInspector-VKOz4Rxs.js +1 -0
- package/web/assets/SessionShell-BsDAwWHd.js +1 -0
- package/web/assets/{SettingsSidebar-IqsOv5h1.js → SettingsSidebar-B-COw6Uw.js} +1 -1
- package/web/assets/{ShortcutHelpDialog-DKDAF94g.js → ShortcutHelpDialog-DquhK-Uh.js} +1 -1
- package/web/assets/{ShortcutKeys-DQFnMnX1.js → ShortcutKeys-BgwCWvNY.js} +1 -1
- package/web/assets/{SidebarUpdateStatus-Cbwy5ii9.js → SidebarUpdateStatus-Ci0ugb91.js} +1 -1
- package/web/assets/{SkillsPage-DTOzYSaS.js → SkillsPage-B46uoO_g.js} +1 -1
- package/web/assets/{TaskProcessesPane-DRFmGkE3.js → TaskProcessesPane-CI_5lWbf.js} +1 -1
- package/web/assets/{TaskTerminalView-DWNACXyI.js → TaskTerminalView-C2acHnAH.js} +1 -1
- package/web/assets/{TerminalPanel-QEt-SRiM.js → TerminalPanel-BAQ2VwKg.js} +1 -1
- package/web/assets/{WebDesktopPet-4CDch3gC.js → WebDesktopPet-D3bdxKPz.js} +1 -1
- package/web/assets/{WorkflowsPage-AMKQBlPo.js → WorkflowsPage-Dp1PRKQb.js} +1 -1
- package/web/assets/WorkspacePicker-BJhBGRH0.js +1 -0
- package/web/assets/accordion-CiJMkjqX.js +1 -0
- package/web/assets/{api-BO7LAPSQ.js → api-CKJOip3Q.js} +1 -1
- package/web/assets/{app-update-DKWPHTUY.js → app-update-yPQVXuw5.js} +1 -1
- package/web/assets/{approval-confirm-ByiI22Wo.js → approval-confirm-Cv754IjO.js} +1 -1
- package/web/assets/board-messages-CIYKjAla.js +1 -0
- package/web/assets/board-projects-wd79Rqt5.js +2 -0
- package/web/assets/channels-messages-KabR1HOR.js +1 -0
- package/web/assets/chat-api-Bvirerdj.js +1 -0
- package/web/assets/{client-store-6GNC8uyK.js → client-store-BaTctJrB.js} +1 -1
- package/web/assets/collapsible-Dcoy-qnz.js +1 -0
- package/web/assets/config-search-KYVClDpx.js +1 -0
- package/web/assets/{dialog-CUhFgsVd.js → dialog-Cb_sjjQV.js} +1 -1
- package/web/assets/{collapsible-DMaUQRX4.js → dist-DUSSw9eQ.js} +1 -1
- package/web/assets/events-CZTn9wa-.js +1 -0
- package/web/assets/file-diff-GxOMJkLj.js +1 -0
- package/web/assets/folder-paths--euvEC5U.js +1 -0
- package/web/assets/{hosts-1yQREdMi.js → hosts-BRPznOHU.js} +1 -1
- package/web/assets/i18n-DEs6BBob.js +1 -0
- package/web/assets/{index-CsE52ydO.js → index-C5aKcTGw.js} +2 -2
- package/web/assets/index-DD2H2OOC.css +2 -0
- package/web/assets/message-circle-question-mark-D7ehy_mi.js +1 -0
- package/web/assets/{mobile-remote-fallback-NJCTBu85.js → mobile-remote-fallback-DG_urwIg.js} +1 -1
- package/web/assets/mount-app-BdXRP9D-.js +9 -0
- package/web/assets/navigation-DkcKi1_5.js +1 -0
- package/web/assets/new-task-uLEgjs2Z.js +1 -0
- package/web/assets/{palette-navigation-ByC1_QS5.js → palette-navigation-HbVGltCo.js} +1 -1
- package/web/assets/{permission-rules-api-BoJ_1W-m.js → permission-rules-api-C9oRUm6Z.js} +1 -1
- package/web/assets/{pick-system-directory-B_RJmP26.js → pick-system-directory-B__qKy-X.js} +1 -1
- package/web/assets/placements-Bn0vPyEI.js +2 -0
- package/web/assets/{popover--UmYQ9yB.js → popover-CiVxYUEU.js} +1 -1
- package/web/assets/public-9meoHs0K.js +4 -0
- package/web/assets/public-D6X148yH.js +2 -0
- package/web/assets/public-DenVz3gW.js +2 -0
- package/web/assets/{public-components-DYGekkA7.js → public-components-BfHcj0Pn.js} +1 -1
- package/web/assets/public-diff-BqcNo4lt.js +1 -0
- package/web/assets/public-events-DCyxz5zX.js +1 -0
- package/web/assets/public-flags-BOI2nkm4.js +1 -0
- package/web/assets/public-launch-D0t4Hqf5.js +1 -0
- package/web/assets/public-sidebar-CdxAohEH.js +1 -0
- package/web/assets/react-bits-BsPlLi8F.css +1 -0
- package/web/assets/routes-DIeaqWVc.js +1 -0
- package/web/assets/runtime-capabilities-CHYOF_hE.js +1 -0
- package/web/assets/{runtime-capabilities-store-C2Qv7uDP.js → runtime-capabilities-store-D2d_9z0D.js} +1 -1
- package/web/assets/{safe-mode-instance-BiAPYJ2-.js → safe-mode-instance-hnF1BJYr.js} +1 -1
- package/web/assets/safe-mode-sentinel-CPmhLLXT.js +1 -0
- package/web/assets/{session-change-summary-api-DgRT32uh.js → session-change-summary-api-DmmNGbP7.js} +1 -1
- package/web/assets/{session-home-BktP6sGF.js → session-home-BAFT2Fk1.js} +1 -1
- package/web/assets/{session-organization-api-DFu3Chto.js → session-organization-api-Bs8DdxU1.js} +1 -1
- package/web/assets/{session-scope-DQPNgQMm.js → session-scope-XRVRtwow.js} +1 -1
- package/web/assets/{session-tree-actions-DfO8QHBO.js → session-tree-actions-Dzj7hrvM.js} +1 -1
- package/web/assets/{session-tree-summary-BwYRdui2.js → session-tree-summary-BXVKYODc.js} +1 -1
- package/web/assets/settings-navigation-DiP_AM1C.js +1 -0
- package/web/assets/{shortcuts-Cvp9WD9O.js → shortcuts-IA6Uq_O6.js} +1 -1
- package/web/assets/side-chat-context-CZ4IqNxt.js +7 -0
- package/web/assets/sidebar-Do_8-Hyh.js +2 -0
- package/web/assets/sidebar-store-DuRlD5qg.js +1 -0
- package/web/assets/{speech-session-DK_09Wky.js → speech-session-D_W5Ulli.js} +1 -1
- package/web/assets/{startup-queries-6Oztqpto.js → startup-queries-DeNu46nT.js} +1 -1
- package/web/assets/storage-DH3bQdy6.js +1 -0
- package/web/assets/tool-labels-ghvpPFmc.js +1 -0
- package/web/assets/{use-i18n-B5p-Kzvz.js → use-i18n-CUS2_EGy.js} +1 -1
- package/web/assets/use-mobile-C2tPBAza.js +1 -0
- package/web/assets/use-studio-actions-CYnwfaOn.js +3 -0
- package/web/assets/useNewProjectPicker-CacWA0IS.js +1 -0
- package/web/assets/usePhoneShell-cYrS53S8.js +1 -0
- package/web/assets/useSessionRowActions-DtunSg_Q.js +1 -0
- package/web/assets/useSidebarModel-6sqN4VA5.js +1 -0
- package/web/assets/web-preview-Dgng80ZD.js +1 -0
- package/web/assets/{web-preview-events-DQchQHt3.js → web-preview-events-JGegs4uF.js} +1 -1
- package/web/assets/workflow-messages-Cj35aJZc.js +1 -0
- package/web/assets/workspace-order-store-BuPmheRT.js +1 -0
- package/web/index.html +2 -2
- package/web/assets/BoardPage-vwv5vTDm.js +0 -3
- package/web/assets/ChannelsPage-L-wcVDSN.js +0 -1
- package/web/assets/ChatPage-DS0euFxR.js +0 -553
- package/web/assets/ConfigPage-pOzONr8e.js +0 -5
- package/web/assets/DispatchOverlay-n-qbkOP-.js +0 -1
- package/web/assets/FocusRuntimeControls-C7NIdpV4.js +0 -1
- package/web/assets/LaunchConsole-DtxzpXJo.js +0 -3
- package/web/assets/ManageProjectsDialog-RiFg3h1p.js +0 -1
- package/web/assets/MemoryPage-Dsx_6JpI.js +0 -1
- package/web/assets/MobileViewportStabilizer-OtwVmIPm.js +0 -1
- package/web/assets/PhoneMenuButton-B0AwvqEo.js +0 -1
- package/web/assets/PhoneTopBar-Dw0mIaWC.js +0 -1
- package/web/assets/PluginsPage-BzxPAX26.js +0 -1
- package/web/assets/QuickPromptIdeas-Cd3SwBqS.js +0 -1
- package/web/assets/SchedulesPage-Cbgl5v_8.js +0 -1
- package/web/assets/SessionBrowserPane-CB7jn-MS.js +0 -2
- package/web/assets/SessionContextPanel-CNmAp0ed.js +0 -2
- package/web/assets/SessionHome-Ck_daJT0.js +0 -1
- package/web/assets/SessionInspector-cSzwsc4b.js +0 -1
- package/web/assets/SessionShell-BOZ7h0yz.js +0 -1
- package/web/assets/WorkspacePicker-B9vYtfab.js +0 -1
- package/web/assets/board-launch-BmmPYZe8.js +0 -1
- package/web/assets/board-messages-BOWWyhz_.js +0 -1
- package/web/assets/board-projects-BU9fZB1q.js +0 -1
- package/web/assets/channels-messages-DazltGVm.js +0 -1
- package/web/assets/chat-api-DkdyVQY9.js +0 -1
- package/web/assets/config-search-HBKDuym0.js +0 -1
- package/web/assets/events-DrAPv0se.js +0 -1
- package/web/assets/file-text-N9JICX6E.js +0 -1
- package/web/assets/folder-paths-DQTpuGh9.js +0 -1
- package/web/assets/i18n-CHmr7GzM.js +0 -1
- package/web/assets/index-CfTBR7Rs.css +0 -2
- package/web/assets/list-checks-B5Jl4aiO.js +0 -1
- package/web/assets/mount-app-DfSFF0xv.js +0 -9
- package/web/assets/navigation-EoDz3gT8.js +0 -1
- package/web/assets/new-task-C3wuHTor.js +0 -1
- package/web/assets/placements-CqwqXPTp.js +0 -2
- package/web/assets/public-diff-DK-VNIar.js +0 -1
- package/web/assets/public-fqyTjQ0F.js +0 -2
- package/web/assets/public-jQjEc6OO.js +0 -1
- package/web/assets/public-launch-CT_m9KWo.js +0 -1
- package/web/assets/react-bits-DQli9s7O.css +0 -1
- package/web/assets/routes-Da0OHSy6.js +0 -1
- package/web/assets/runtime-capabilities-4_ZwLBqF.js +0 -1
- package/web/assets/safe-mode-sentinel-Ccu3Vmta.js +0 -1
- package/web/assets/session-workspaces-BWTBYaGT.js +0 -1
- package/web/assets/settings-navigation-_yUm71Uk.js +0 -1
- package/web/assets/shield-B1ruzHd0.js +0 -1
- package/web/assets/side-chat-context-BfOm4dFp.js +0 -7
- package/web/assets/sidebar-BLeRyb55.js +0 -2
- package/web/assets/sidebar-commands-ZA1UOA_F.js +0 -1
- package/web/assets/sidebar-store-1-aNPlDu.js +0 -1
- package/web/assets/storage-BY6wEHPw.js +0 -1
- package/web/assets/tool-labels-BUEvdohv.js +0 -1
- package/web/assets/use-mobile-DauOEda6.js +0 -1
- package/web/assets/use-studio-actions-D6c_pSJK.js +0 -3
- package/web/assets/useBoardItems-CzEbgTLJ.js +0 -1
- package/web/assets/useFleetSystemInfo-Bd8zJIFa.js +0 -1
- package/web/assets/useNewProjectPicker-DQY1X_ua.js +0 -1
- package/web/assets/useNow-Bxn4_OW9.js +0 -1
- package/web/assets/usePagePrimaryAction-CWPC0C_f.js +0 -1
- package/web/assets/usePhoneShell-Bvb7m-m4.js +0 -1
- package/web/assets/useSessionListQuery-D44XZfGr.js +0 -2
- package/web/assets/useSessionRowActions-Cr6SUAeE.js +0 -1
- package/web/assets/useSidebarModel-CWED0rsw.js +0 -1
- package/web/assets/wand-sparkles-CvEBNUFz.js +0 -1
- package/web/assets/web-preview-CqwSe9Zn.js +0 -1
- package/web/assets/workflow-messages-CMCQrs5q.js +0 -1
- package/web/assets/workspace-order-store-RYWvGOeR.js +0 -1
- /package/web/assets/{AnimatedList-D8fxDNGr.js → AnimatedList-DB5vqafO.js} +0 -0
- /package/web/assets/{ClickSpark-DmnEAJNI.js → ClickSpark-B1Rn7ZwL.js} +0 -0
package/bin/pisper.mjs
CHANGED
|
@@ -10,7 +10,11 @@ const frontendRoot = fileURLToPath(new URL('../web', import.meta.url))
|
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
12
|
const arguments_ = process.argv.slice(2)
|
|
13
|
-
|
|
13
|
+
// 执行节点只用 Node 内置模块,不需要下载应用本体。
|
|
14
|
+
if (arguments_[0] === 'executor') {
|
|
15
|
+
const { executorCommand } = await import('../lib/executor-node.mjs')
|
|
16
|
+
process.exitCode = await executorCommand(arguments_.slice(1))
|
|
17
|
+
} else if (!handleNpmHelp(arguments_) && !(await handleNpmUpdate(arguments_))) {
|
|
14
18
|
const installation = await ensurePisperInstallation()
|
|
15
19
|
const child = spawn(installation.executable, arguments_, {
|
|
16
20
|
env: {
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// Pisper 执行节点(B6):让另一台电脑上的 Pisper(“大脑”)在这台电脑上替 Bot 读写代码、跑命令。
|
|
2
|
+
// pisper executor pair --brain https://<地址>:<端口> --fingerprint SHA256:… --code ABCD-EF23 --root <目录> [--root …] [--name 名字] [--no-shell]
|
|
3
|
+
// pisper executor # 连上大脑,开始干活(Ctrl+C 停止)
|
|
4
|
+
// 设计要点(runtime/services/bots/README.md「执行环境接口」):
|
|
5
|
+
// - 节点主动连回大脑(HTTPS 长轮询),不开放端口;只认配对时确认过的证书指纹。
|
|
6
|
+
// - 节点不保存会话、不持有模型密钥;只保存配对信息(0600)与 Bot 任务的 worktree。
|
|
7
|
+
// - 文件操作只在 --root 允许的目录与自己的 worktree 目录里,按真实路径检查(符号链接绕不出去);
|
|
8
|
+
// git 只放行 worktree 管理用到的子命令;--no-shell 时不执行任意命令。
|
|
9
|
+
// 只用 Node 内置模块:npm 包里的这个文件不依赖下载的应用本体。
|
|
10
|
+
import { execFile, spawn } from 'node:child_process'
|
|
11
|
+
import { X509Certificate } from 'node:crypto'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import {
|
|
14
|
+
access,
|
|
15
|
+
mkdir,
|
|
16
|
+
readdir,
|
|
17
|
+
readFile,
|
|
18
|
+
realpath,
|
|
19
|
+
stat,
|
|
20
|
+
writeFile,
|
|
21
|
+
chmod,
|
|
22
|
+
} from 'node:fs/promises'
|
|
23
|
+
import https from 'node:https'
|
|
24
|
+
import { homedir, hostname } from 'node:os'
|
|
25
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
26
|
+
import process from 'node:process'
|
|
27
|
+
import tls from 'node:tls'
|
|
28
|
+
|
|
29
|
+
const MAX_READ_BYTES = 16 * 1024 * 1024
|
|
30
|
+
const MAX_EXEC_OUTPUT = 8 * 1024 * 1024
|
|
31
|
+
const MAX_GLOB_VISITS = 200_000
|
|
32
|
+
const POLL_TIMEOUT_MS = 40_000
|
|
33
|
+
const CHUNK_FLUSH_MS = 150
|
|
34
|
+
// worktree 管理用到的 git 子命令;config 只允许读提交身份。
|
|
35
|
+
const GIT_SUBCOMMANDS = new Set([
|
|
36
|
+
'rev-parse',
|
|
37
|
+
'worktree',
|
|
38
|
+
'check-ref-format',
|
|
39
|
+
'add',
|
|
40
|
+
'diff',
|
|
41
|
+
'commit',
|
|
42
|
+
'status',
|
|
43
|
+
'cherry-pick',
|
|
44
|
+
'branch',
|
|
45
|
+
'config',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
export class NodeOperationError extends Error {
|
|
49
|
+
constructor(code, message) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.code = code
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fail(code, message) {
|
|
56
|
+
throw new NodeOperationError(code, message)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function executorHome(env = process.env) {
|
|
60
|
+
return env.PISPER_EXECUTOR_HOME || join(homedir(), '.pisper-executor')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// glob → 正则:支持 **、*、?、{a,b} 与 [...];路径分隔符统一成 /。
|
|
64
|
+
export function globToRegExp(pattern) {
|
|
65
|
+
let source = ''
|
|
66
|
+
const text = String(pattern).replaceAll('\\', '/')
|
|
67
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
68
|
+
const char = text[index]
|
|
69
|
+
if (char === '*') {
|
|
70
|
+
if (text[index + 1] === '*') {
|
|
71
|
+
const slash = text[index + 2] === '/'
|
|
72
|
+
source += slash ? '(?:.*/)?' : '.*'
|
|
73
|
+
index += slash ? 2 : 1
|
|
74
|
+
} else source += '[^/]*'
|
|
75
|
+
} else if (char === '?') source += '[^/]'
|
|
76
|
+
else if (char === '{') {
|
|
77
|
+
const end = text.indexOf('}', index)
|
|
78
|
+
if (end < 0) source += '\\{'
|
|
79
|
+
else {
|
|
80
|
+
source += `(?:${text
|
|
81
|
+
.slice(index + 1, end)
|
|
82
|
+
.split(',')
|
|
83
|
+
.map((part) => part.replace(/[.+^$()|[\]\\]/g, '\\$&').replaceAll('*', '[^/]*'))
|
|
84
|
+
.join('|')})`
|
|
85
|
+
index = end
|
|
86
|
+
}
|
|
87
|
+
} else if (char === '[') {
|
|
88
|
+
const end = text.indexOf(']', index)
|
|
89
|
+
if (end < 0) source += '\\['
|
|
90
|
+
else {
|
|
91
|
+
source += `[${text.slice(index + 1, end).replace(/^!/, '^')}]`
|
|
92
|
+
index = end
|
|
93
|
+
}
|
|
94
|
+
} else source += char.replace(/[.+^$()|{}\\]/g, '\\$&')
|
|
95
|
+
}
|
|
96
|
+
return new RegExp(`^${source}$`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ExecutorNode {
|
|
100
|
+
constructor({ roots, worktreeRoot, shell = true }) {
|
|
101
|
+
this.roots = roots.map((root) => resolve(root))
|
|
102
|
+
this.worktreeRoot = resolve(worktreeRoot)
|
|
103
|
+
this.shell = shell
|
|
104
|
+
this.running = new Map()
|
|
105
|
+
this.realRoots = null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
report() {
|
|
109
|
+
return {
|
|
110
|
+
roots: this.roots,
|
|
111
|
+
worktreeRoot: this.worktreeRoot,
|
|
112
|
+
capabilities: { shell: this.shell, git: true },
|
|
113
|
+
platform: process.platform,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async #allowedRoots() {
|
|
118
|
+
if (this.realRoots) return this.realRoots
|
|
119
|
+
await mkdir(this.worktreeRoot, { recursive: true })
|
|
120
|
+
const roots = []
|
|
121
|
+
for (const root of [...this.roots, this.worktreeRoot]) {
|
|
122
|
+
try {
|
|
123
|
+
roots.push(await realpath(root))
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
this.realRoots = roots
|
|
127
|
+
return roots
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 路径必须是绝对路径,且按真实路径(最近一个已存在的上级解析符号链接)落在允许目录里。
|
|
131
|
+
async allowed(path) {
|
|
132
|
+
const text = String(path ?? '')
|
|
133
|
+
if (!text || text.includes('\0') || !isAbsolute(text))
|
|
134
|
+
fail('path_invalid', '路径必须是绝对路径。')
|
|
135
|
+
let existing = resolve(text)
|
|
136
|
+
const rest = []
|
|
137
|
+
for (;;) {
|
|
138
|
+
try {
|
|
139
|
+
existing = await realpath(existing)
|
|
140
|
+
break
|
|
141
|
+
} catch {
|
|
142
|
+
const parent = dirname(existing)
|
|
143
|
+
if (parent === existing) fail('path_not_allowed', `不在允许的目录里:${text}`)
|
|
144
|
+
rest.unshift(existing.slice(parent.length).replace(/^[\\/]+/, ''))
|
|
145
|
+
existing = parent
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const real = rest.length ? join(existing, ...rest) : existing
|
|
149
|
+
const roots = await this.#allowedRoots()
|
|
150
|
+
if (!roots.some((root) => real === root || real.startsWith(`${root}${sep}`)))
|
|
151
|
+
fail('path_not_allowed', `不在允许的目录里:${text}`)
|
|
152
|
+
return real
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #git(args, cwd, timeoutMs) {
|
|
156
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string' || arg.includes('\0')))
|
|
157
|
+
fail('command_invalid', '命令参数无效。')
|
|
158
|
+
let index = 0
|
|
159
|
+
// 只接受提交身份的 -c;其余全局选项(-C、--git-dir、-c core.* 等)一律拒绝。
|
|
160
|
+
while (args[index] === '-c') {
|
|
161
|
+
if (!/^user\.(name|email)=/.test(args[index + 1] || ''))
|
|
162
|
+
fail('command_not_allowed', '只允许设置提交身份。')
|
|
163
|
+
index += 2
|
|
164
|
+
}
|
|
165
|
+
const subcommand = args[index]
|
|
166
|
+
if (!GIT_SUBCOMMANDS.has(subcommand))
|
|
167
|
+
fail('command_not_allowed', `不允许 git ${subcommand || ''}`.trim())
|
|
168
|
+
if (subcommand === 'config' && !(args.length === index + 2 && args[index + 1] === 'user.email'))
|
|
169
|
+
fail('command_not_allowed', '只允许读取提交身份。')
|
|
170
|
+
// 参数里的绝对路径(如 worktree 目录)同样要在允许目录里;相对路径不许用 .. 往上走
|
|
171
|
+
// (git 的引用名本来就不能含 ..,正常参数不受影响)。
|
|
172
|
+
for (const arg of args.slice(index + 1)) {
|
|
173
|
+
if (/(^|[\\/])\.\.([\\/]|$)/.test(arg)) fail('path_not_allowed', '参数里不能有 ..。')
|
|
174
|
+
if (isAbsolute(arg) && !arg.startsWith('-')) await this.allowed(arg)
|
|
175
|
+
}
|
|
176
|
+
const directory = await this.allowed(cwd)
|
|
177
|
+
return new Promise((resolveResult, reject) => {
|
|
178
|
+
execFile(
|
|
179
|
+
'git',
|
|
180
|
+
args,
|
|
181
|
+
{
|
|
182
|
+
cwd: directory,
|
|
183
|
+
timeout: Math.min(Math.max(1000, Number(timeoutMs) || 60_000), 10 * 60_000),
|
|
184
|
+
maxBuffer: MAX_EXEC_OUTPUT,
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_EDITOR: 'true' },
|
|
187
|
+
},
|
|
188
|
+
(error, stdout, stderr) => {
|
|
189
|
+
if (error && typeof error.code !== 'number') {
|
|
190
|
+
reject(new NodeOperationError('command_failed', `git 执行失败:${error.message}`))
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
resolveResult({
|
|
194
|
+
code: error ? error.code : 0,
|
|
195
|
+
stdout: String(stdout),
|
|
196
|
+
stderr: String(stderr),
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async #shell(id, { command, cwd, timeoutMs }, emit) {
|
|
204
|
+
if (!this.shell) fail('shell_disabled', '这台电脑没有允许执行命令(--no-shell)。')
|
|
205
|
+
const directory = await this.allowed(cwd)
|
|
206
|
+
const windows = process.platform === 'win32'
|
|
207
|
+
const shellPath = windows
|
|
208
|
+
? process.env.ComSpec || 'cmd.exe'
|
|
209
|
+
: existsSync('/bin/bash')
|
|
210
|
+
? '/bin/bash'
|
|
211
|
+
: '/bin/sh'
|
|
212
|
+
const child = spawn(shellPath, windows ? ['/d', '/s', '/c', command] : ['-c', command], {
|
|
213
|
+
cwd: directory,
|
|
214
|
+
detached: !windows,
|
|
215
|
+
windowsHide: true,
|
|
216
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
217
|
+
})
|
|
218
|
+
const kill = () => {
|
|
219
|
+
try {
|
|
220
|
+
if (!windows && child.pid) process.kill(-child.pid, 'SIGKILL')
|
|
221
|
+
else child.kill('SIGKILL')
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
this.running.set(id, kill)
|
|
225
|
+
const timer = setTimeout(
|
|
226
|
+
kill,
|
|
227
|
+
Math.min(Math.max(1000, Number(timeoutMs) || 600_000), 3_600_000),
|
|
228
|
+
)
|
|
229
|
+
child.stdout.on('data', (data) => emit(data))
|
|
230
|
+
child.stderr.on('data', (data) => emit(data))
|
|
231
|
+
return new Promise((resolveResult, reject) => {
|
|
232
|
+
child.once('error', (error) => {
|
|
233
|
+
clearTimeout(timer)
|
|
234
|
+
this.running.delete(id)
|
|
235
|
+
reject(new NodeOperationError('command_failed', error.message))
|
|
236
|
+
})
|
|
237
|
+
child.once('close', (code, signal) => {
|
|
238
|
+
clearTimeout(timer)
|
|
239
|
+
this.running.delete(id)
|
|
240
|
+
resolveResult({
|
|
241
|
+
exitCode: typeof code === 'number' ? code : signal ? 128 + 9 : null,
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async #glob({ pattern, cwd, ignore, limit }) {
|
|
248
|
+
const base = await this.allowed(cwd)
|
|
249
|
+
const matcher = globToRegExp(pattern)
|
|
250
|
+
const ignored = (Array.isArray(ignore) ? ignore : []).map(globToRegExp)
|
|
251
|
+
const max = Math.min(Math.max(1, Number(limit) || 1000), 10_000)
|
|
252
|
+
const found = []
|
|
253
|
+
let visits = 0
|
|
254
|
+
const walk = async (directory) => {
|
|
255
|
+
if (found.length >= max || visits >= MAX_GLOB_VISITS) return
|
|
256
|
+
let entries
|
|
257
|
+
try {
|
|
258
|
+
entries = await readdir(directory, { withFileTypes: true })
|
|
259
|
+
} catch {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
visits += 1
|
|
264
|
+
if (found.length >= max || visits >= MAX_GLOB_VISITS) return
|
|
265
|
+
const path = join(directory, entry.name)
|
|
266
|
+
const rel = relative(base, path).replaceAll('\\', '/')
|
|
267
|
+
const probe = entry.isDirectory() ? `${rel}/` : rel
|
|
268
|
+
if (ignored.some((rule) => rule.test(rel) || rule.test(probe))) continue
|
|
269
|
+
if (entry.name === '.git') continue
|
|
270
|
+
if (matcher.test(rel)) found.push(rel)
|
|
271
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) await walk(path)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
await walk(base)
|
|
275
|
+
return found
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 执行一个请求;emit 用来流式交回命令输出。返回 { ok, value } 或 { ok: false, error }。
|
|
279
|
+
async handle(request, emit = () => {}) {
|
|
280
|
+
const { id, op } = request
|
|
281
|
+
const args = request.args && typeof request.args === 'object' ? request.args : {}
|
|
282
|
+
try {
|
|
283
|
+
switch (op) {
|
|
284
|
+
case 'cancel':
|
|
285
|
+
this.running.get(String(args.id))?.()
|
|
286
|
+
return { ok: true, value: null }
|
|
287
|
+
case 'exec':
|
|
288
|
+
if (args.file !== 'git') fail('command_not_allowed', `不允许执行 ${args.file}`)
|
|
289
|
+
return { ok: true, value: await this.#git(args.args, args.cwd, args.timeoutMs) }
|
|
290
|
+
case 'shell':
|
|
291
|
+
return { ok: true, value: await this.#shell(id, args, emit) }
|
|
292
|
+
case 'fs.read': {
|
|
293
|
+
const path = await this.allowed(args.path)
|
|
294
|
+
const info = await stat(path)
|
|
295
|
+
if (info.size > MAX_READ_BYTES) fail('file_too_large', '文件太大,超过 16 MB。')
|
|
296
|
+
return { ok: true, value: (await readFile(path)).toString('base64') }
|
|
297
|
+
}
|
|
298
|
+
case 'fs.access': {
|
|
299
|
+
const path = await this.allowed(args.path)
|
|
300
|
+
await access(path, args.mode === 'rw' ? 6 : 4)
|
|
301
|
+
return { ok: true, value: null }
|
|
302
|
+
}
|
|
303
|
+
case 'fs.write': {
|
|
304
|
+
const path = await this.allowed(args.path)
|
|
305
|
+
await mkdir(dirname(path), { recursive: true })
|
|
306
|
+
await writeFile(path, String(args.content ?? ''), 'utf8')
|
|
307
|
+
return { ok: true, value: null }
|
|
308
|
+
}
|
|
309
|
+
case 'fs.mkdir':
|
|
310
|
+
await mkdir(await this.allowed(args.path), { recursive: true })
|
|
311
|
+
return { ok: true, value: null }
|
|
312
|
+
case 'fs.stat': {
|
|
313
|
+
const path = await this.allowed(args.path)
|
|
314
|
+
const info = await stat(path).catch(() => null)
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
value: { exists: Boolean(info), isDirectory: Boolean(info?.isDirectory()) },
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
case 'fs.readdir':
|
|
321
|
+
return { ok: true, value: await readdir(await this.allowed(args.path)) }
|
|
322
|
+
case 'fs.glob':
|
|
323
|
+
return { ok: true, value: await this.#glob(args) }
|
|
324
|
+
default:
|
|
325
|
+
fail('op_unsupported', `不支持的操作:${op}`)
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
error: {
|
|
331
|
+
code:
|
|
332
|
+
error instanceof NodeOperationError
|
|
333
|
+
? error.code
|
|
334
|
+
: error?.code === 'ENOENT'
|
|
335
|
+
? 'not_found'
|
|
336
|
+
: 'op_failed',
|
|
337
|
+
message: String(error?.message || error).slice(0, 500),
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
cancelAll() {
|
|
344
|
+
for (const kill of this.running.values()) kill()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// HTTPS 客户端:先取一次大脑的证书,确认指纹与配对时一致,之后把这张证书当作唯一可信的 CA。
|
|
349
|
+
export function normalizeFingerprint(value) {
|
|
350
|
+
const hex = String(value || '')
|
|
351
|
+
.replace(/^SHA256:/i, '')
|
|
352
|
+
.replace(/[^0-9a-f]/gi, '')
|
|
353
|
+
.toUpperCase()
|
|
354
|
+
return hex.length === 64 ? `SHA256:${hex.match(/../g).join(':')}` : ''
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function pinnedCertificate(brain, fingerprint) {
|
|
358
|
+
const url = new URL(brain)
|
|
359
|
+
return new Promise((resolveCert, reject) => {
|
|
360
|
+
const socket = tls.connect(
|
|
361
|
+
{
|
|
362
|
+
host: url.hostname.replace(/^\[|\]$/g, ''),
|
|
363
|
+
port: Number(url.port || 443),
|
|
364
|
+
servername: /^[\d.]+$|:/.test(url.hostname) ? undefined : url.hostname,
|
|
365
|
+
rejectUnauthorized: false,
|
|
366
|
+
},
|
|
367
|
+
() => {
|
|
368
|
+
const raw = socket.getPeerCertificate(true)?.raw
|
|
369
|
+
socket.end()
|
|
370
|
+
if (!raw) return reject(new Error('读不到大脑的证书。'))
|
|
371
|
+
const cert = new X509Certificate(raw)
|
|
372
|
+
if (normalizeFingerprint(cert.fingerprint256) !== normalizeFingerprint(fingerprint))
|
|
373
|
+
return reject(new Error('大脑的证书指纹和配对时不一致,已停止连接。'))
|
|
374
|
+
resolveCert(cert.toString())
|
|
375
|
+
},
|
|
376
|
+
)
|
|
377
|
+
socket.setTimeout(15_000, () => socket.destroy(new Error('连接大脑超时。')))
|
|
378
|
+
socket.once('error', reject)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function createBrainClient({ brain, fingerprint }) {
|
|
383
|
+
const url = new URL(brain)
|
|
384
|
+
if (url.protocol !== 'https:') throw new Error('大脑地址必须是 https://(远程访问地址)。')
|
|
385
|
+
const expected = normalizeFingerprint(fingerprint)
|
|
386
|
+
if (!expected) throw new Error('证书指纹格式不对,应形如 SHA256:AB:CD:…')
|
|
387
|
+
const ca = await pinnedCertificate(brain, expected)
|
|
388
|
+
const agent = new https.Agent({
|
|
389
|
+
ca,
|
|
390
|
+
keepAlive: true,
|
|
391
|
+
// 大脑的自签证书不带这台机器认识的主机名;只认指纹,不认主机名。
|
|
392
|
+
checkServerIdentity: (_host, cert) =>
|
|
393
|
+
normalizeFingerprint(cert.fingerprint256) === expected
|
|
394
|
+
? undefined
|
|
395
|
+
: new Error('大脑的证书指纹和配对时不一致。'),
|
|
396
|
+
})
|
|
397
|
+
return {
|
|
398
|
+
post(path, body, { token = '', timeoutMs = 30_000 } = {}) {
|
|
399
|
+
const payload = Buffer.from(JSON.stringify(body ?? {}))
|
|
400
|
+
return new Promise((resolveResponse, reject) => {
|
|
401
|
+
const request = https.request(
|
|
402
|
+
new URL(path, url),
|
|
403
|
+
{
|
|
404
|
+
method: 'POST',
|
|
405
|
+
agent,
|
|
406
|
+
headers: {
|
|
407
|
+
'Content-Type': 'application/json',
|
|
408
|
+
'Content-Length': payload.length,
|
|
409
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
(response) => {
|
|
413
|
+
const chunks = []
|
|
414
|
+
response.on('data', (chunk) => chunks.push(chunk))
|
|
415
|
+
response.on('end', () => {
|
|
416
|
+
let data = {}
|
|
417
|
+
try {
|
|
418
|
+
data = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
|
|
419
|
+
} catch {}
|
|
420
|
+
if ((response.statusCode || 500) >= 400) {
|
|
421
|
+
const error = new Error(data.error || `大脑返回 ${response.statusCode}`)
|
|
422
|
+
error.status = response.statusCode
|
|
423
|
+
error.code = data.code
|
|
424
|
+
reject(error)
|
|
425
|
+
} else resolveResponse(data)
|
|
426
|
+
})
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
request.setTimeout(timeoutMs, () => request.destroy(new Error('请求大脑超时。')))
|
|
430
|
+
request.once('error', reject)
|
|
431
|
+
request.end(payload)
|
|
432
|
+
})
|
|
433
|
+
},
|
|
434
|
+
close() {
|
|
435
|
+
agent.destroy()
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 连上大脑干活:一直挂着一次轮询,领到请求就并行执行,结果与输出尽快交回。
|
|
441
|
+
export async function runExecutorLoop({ client, token, node, signal, log = () => {} }) {
|
|
442
|
+
const outbox = []
|
|
443
|
+
let flushing = null
|
|
444
|
+
const flush = async () => {
|
|
445
|
+
if (flushing || !outbox.length) return flushing
|
|
446
|
+
const results = outbox.splice(0, outbox.length)
|
|
447
|
+
flushing = client
|
|
448
|
+
.post('/api/executor/results', { results }, { token })
|
|
449
|
+
.catch((error) => log(`交回结果失败:${error.message}`))
|
|
450
|
+
.finally(() => {
|
|
451
|
+
flushing = null
|
|
452
|
+
if (outbox.length) void flush()
|
|
453
|
+
})
|
|
454
|
+
return flushing
|
|
455
|
+
}
|
|
456
|
+
let flushTimer = null
|
|
457
|
+
const queueChunk = (id, data) => {
|
|
458
|
+
outbox.push({ id, chunk: Buffer.from(data).toString('base64') })
|
|
459
|
+
flushTimer ??= setTimeout(() => {
|
|
460
|
+
flushTimer = null
|
|
461
|
+
void flush()
|
|
462
|
+
}, CHUNK_FLUSH_MS)
|
|
463
|
+
}
|
|
464
|
+
let delay = 1000
|
|
465
|
+
while (!signal?.aborted) {
|
|
466
|
+
try {
|
|
467
|
+
const { requests = [] } = await client.post(
|
|
468
|
+
'/api/executor/poll',
|
|
469
|
+
{ ...node.report() },
|
|
470
|
+
{ token, timeoutMs: POLL_TIMEOUT_MS },
|
|
471
|
+
)
|
|
472
|
+
delay = 1000
|
|
473
|
+
for (const request of Array.isArray(requests) ? requests : []) {
|
|
474
|
+
void node
|
|
475
|
+
.handle(request, (data) => queueChunk(request.id, data))
|
|
476
|
+
.then((result) => {
|
|
477
|
+
if (request.op === 'cancel') return
|
|
478
|
+
outbox.push({ id: request.id, ...result })
|
|
479
|
+
void flush()
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error.status === 401) throw new Error('大脑不认这台电脑了(可能已被移除),请重新配对。')
|
|
484
|
+
log(`连接大脑失败:${error.message},${Math.round(delay / 1000)} 秒后重试。`)
|
|
485
|
+
await new Promise((done) => setTimeout(done, delay))
|
|
486
|
+
delay = Math.min(delay * 2, 30_000)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
node.cancelAll()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function parseArgs(argv) {
|
|
493
|
+
const options = { roots: [], shell: true }
|
|
494
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
495
|
+
const arg = argv[index]
|
|
496
|
+
const next = () => argv[++index] ?? ''
|
|
497
|
+
if (arg === '--brain') options.brain = next()
|
|
498
|
+
else if (arg === '--fingerprint') options.fingerprint = next()
|
|
499
|
+
else if (arg === '--code') options.code = next()
|
|
500
|
+
else if (arg === '--root') options.roots.push(resolve(next()))
|
|
501
|
+
else if (arg === '--name') options.name = next()
|
|
502
|
+
else if (arg === '--no-shell') options.shell = false
|
|
503
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
504
|
+
else if (!options.command) options.command = arg
|
|
505
|
+
else throw new Error(`不认识的参数:${arg}`)
|
|
506
|
+
}
|
|
507
|
+
return options
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const HELP = `pisper executor —— 让另一台电脑上的 Pisper Bot 在这台电脑上干活
|
|
511
|
+
|
|
512
|
+
pisper executor pair --brain <https 地址> --fingerprint <SHA256:…> --code <配对码> --root <目录> [--root …] [--name 名字] [--no-shell]
|
|
513
|
+
用 Pisper「Bot › 电脑 › 添加电脑」里显示的配对码配对。只有 --root 列出的目录可以读写。
|
|
514
|
+
pisper executor
|
|
515
|
+
连上大脑开始干活,Ctrl+C 停止。
|
|
516
|
+
|
|
517
|
+
配对信息保存在 ${executorHome()}(可用 PISPER_EXECUTOR_HOME 修改)。`
|
|
518
|
+
|
|
519
|
+
async function readConfig(home) {
|
|
520
|
+
try {
|
|
521
|
+
return JSON.parse(await readFile(join(home, 'config.json'), 'utf8'))
|
|
522
|
+
} catch {
|
|
523
|
+
return null
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function executorCommand(argv, { env = process.env, log = console.log } = {}) {
|
|
528
|
+
const options = parseArgs(argv)
|
|
529
|
+
if (options.help) {
|
|
530
|
+
log(HELP)
|
|
531
|
+
return 0
|
|
532
|
+
}
|
|
533
|
+
const home = executorHome(env)
|
|
534
|
+
if (options.command === 'pair') {
|
|
535
|
+
if (!options.brain || !options.fingerprint || !options.code || !options.roots.length)
|
|
536
|
+
throw new Error('配对需要 --brain、--fingerprint、--code 和至少一个 --root。')
|
|
537
|
+
for (const root of options.roots)
|
|
538
|
+
if (!(await stat(root).catch(() => null))?.isDirectory())
|
|
539
|
+
throw new Error(`目录不存在:${root}`)
|
|
540
|
+
const node = new ExecutorNode({
|
|
541
|
+
roots: options.roots,
|
|
542
|
+
worktreeRoot: join(home, 'worktrees'),
|
|
543
|
+
shell: options.shell,
|
|
544
|
+
})
|
|
545
|
+
const client = await createBrainClient(options)
|
|
546
|
+
try {
|
|
547
|
+
const paired = await client.post('/api/executor/pair', {
|
|
548
|
+
code: options.code,
|
|
549
|
+
name: options.name || hostname(),
|
|
550
|
+
...node.report(),
|
|
551
|
+
})
|
|
552
|
+
await mkdir(home, { recursive: true, mode: 0o700 })
|
|
553
|
+
const config = {
|
|
554
|
+
brain: options.brain,
|
|
555
|
+
fingerprint: normalizeFingerprint(options.fingerprint),
|
|
556
|
+
token: paired.token,
|
|
557
|
+
roots: options.roots,
|
|
558
|
+
shell: options.shell,
|
|
559
|
+
}
|
|
560
|
+
await writeFile(join(home, 'config.json'), `${JSON.stringify(config, null, 2)}\n`, {
|
|
561
|
+
mode: 0o600,
|
|
562
|
+
})
|
|
563
|
+
await chmod(join(home, 'config.json'), 0o600).catch(() => {})
|
|
564
|
+
log(`已配对为「${paired.environment?.name || ''}」。运行 pisper executor 开始干活。`)
|
|
565
|
+
} finally {
|
|
566
|
+
client.close()
|
|
567
|
+
}
|
|
568
|
+
return 0
|
|
569
|
+
}
|
|
570
|
+
if (options.command && options.command !== 'run')
|
|
571
|
+
throw new Error(`不认识的命令:${options.command}`)
|
|
572
|
+
const config = await readConfig(home)
|
|
573
|
+
if (!config?.token)
|
|
574
|
+
throw new Error('还没有配对。先运行 pisper executor pair …(--help 查看用法)')
|
|
575
|
+
const node = new ExecutorNode({
|
|
576
|
+
roots: config.roots,
|
|
577
|
+
worktreeRoot: join(home, 'worktrees'),
|
|
578
|
+
shell: config.shell !== false,
|
|
579
|
+
})
|
|
580
|
+
const client = await createBrainClient(config)
|
|
581
|
+
const controller = new AbortController()
|
|
582
|
+
for (const name of ['SIGINT', 'SIGTERM']) process.once(name, () => controller.abort())
|
|
583
|
+
log(`已连上 ${config.brain},允许的目录:${config.roots.join('、')}。Ctrl+C 停止。`)
|
|
584
|
+
try {
|
|
585
|
+
await runExecutorLoop({ client, token: config.token, node, signal: controller.signal, log })
|
|
586
|
+
} finally {
|
|
587
|
+
client.close()
|
|
588
|
+
}
|
|
589
|
+
return 0
|
|
590
|
+
}
|
package/lib/npm-update.mjs
CHANGED
|
@@ -38,6 +38,7 @@ Usage:
|
|
|
38
38
|
pisper doctor [OPTIONS]
|
|
39
39
|
pisper web [OPTIONS]
|
|
40
40
|
pisper update [--check]
|
|
41
|
+
pisper executor [pair ...]
|
|
41
42
|
pisper help [COMMAND]
|
|
42
43
|
|
|
43
44
|
Commands:
|
|
@@ -45,6 +46,7 @@ Commands:
|
|
|
45
46
|
doctor Check the TUI, Runtime connection, and capability catalogs
|
|
46
47
|
web Open the bundled Web UI and Provider settings in your browser
|
|
47
48
|
update Update the complete Pisper distribution through npm
|
|
49
|
+
executor Let a Pisper Bot on another computer work on this one (see pisper executor --help)
|
|
48
50
|
help Print this help or help for a command
|
|
49
51
|
|
|
50
52
|
Options:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pisper",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
4
4
|
"description": "Pisper CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"pisper": {
|
|
43
43
|
"repository": "ling-kong-ran/pisper",
|
|
44
44
|
"tuiVersion": "0.6.9",
|
|
45
|
-
"runtimeVersion": "0.6.
|
|
45
|
+
"runtimeVersion": "0.6.13"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
|
-
"pisper-binary-win32-x64": "npm:pisper@0.6.
|
|
49
|
-
"pisper-binary-darwin-arm64": "npm:pisper@0.6.
|
|
50
|
-
"pisper-binary-linux-x64": "npm:pisper@0.6.
|
|
48
|
+
"pisper-binary-win32-x64": "npm:pisper@0.6.15-win32-x64.0",
|
|
49
|
+
"pisper-binary-darwin-arm64": "npm:pisper@0.6.15-darwin-arm64.0",
|
|
50
|
+
"pisper-binary-linux-x64": "npm:pisper@0.6.15-linux-x64.0"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public",
|